35 parent::__construct(
'ReplaceText',
'replacetext' );
51 if ( !$this->
getUser()->isAllowed(
'replacetext' ) ) {
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>" );
64 $errorMsg =
"Error: text replacements cannot be run if \$wgExternalStores is non-empty.";
65 $out->addWikiTextAsContent(
"<div class=\"errorbox\">$errorMsg</div>" );
70 if ( !is_null( $out->getResourceLoader()->getModule(
'mediawiki.special' ) ) ) {
71 $out->addModuleStyles(
'mediawiki.special' );
80 if ( class_exists( MediaWikiServices::class ) ) {
82 $all_namespaces = MediaWikiServices::getInstance()->getSearchEngineConfig()
83 ->searchableNamespaces();
86 $all_namespaces = SearchEngine::searchableNamespaces();
89 foreach ( $all_namespaces as $ns => $name ) {
90 if ( $this->
getRequest()->getCheck(
'ns' . $ns ) ) {
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' );
114 if ( $request->getCheck(
'continue' ) && $this->target ===
'' ) {
115 $this->
showForm(
'replacetext_givetarget' );
119 if ( $request->getCheck(
'replace' ) ) {
123 if ( !$user->matchEditToken( $request->getVal(
'token' ) ) ) {
124 $out->addWikiMsg(
'sessionfailure' );
131 $count = $this->
getLanguage()->formatNum( count( $jobs ) );
133 'replacetext_success',
134 "<code><nowiki>{$this->target}</nowiki></code>",
135 "<code><nowiki>{$this->replacement}</nowiki></code>",
143 $this->
msg(
'replacetext_return' )->text()
149 if ( $request->getCheck(
'target' ) ) {
152 if ( !$user->matchEditToken( $request->getVal(
'token' ) ) ) {
153 $out->addWikiMsg(
'sessionfailure' );
160 if ( count( $this->selected_namespaces ) == 0 ) {
161 $this->
showForm(
'replacetext_nonamespace' );
164 if ( !$this->edit_pages && !$this->move_pages ) {
165 $this->
showForm(
'replacetext_editormove' );
170 $titles_for_edit = $titles_for_move = $unmoveable_titles = [];
171 if ( $this->edit_pages ) {
174 if ( $this->move_pages ) {
180 if ( count( $titles_for_edit ) == 0 && count( $titles_for_move ) == 0 ) {
181 $bad_cat_name =
false;
183 if ( !empty( $this->category ) ) {
185 if ( !$category_title->exists() ) {
186 $bad_cat_name =
true;
190 if ( $bad_cat_name ) {
193 ucfirst( $this->category )
196 $this->
msg(
'replacetext_nosuchcategory' )->rawParams( $link )->escaped()
199 if ( $this->edit_pages ) {
201 'replacetext_noreplacement',
"<code><nowiki>{$this->target}</nowiki></code>"
205 if ( $this->move_pages ) {
206 $out->addWikiMsg(
'replacetext_nomove',
"<code><nowiki>{$this->target}</nowiki></code>" );
214 $this->
msg(
'replacetext_return' )->text()
220 if ( !is_null( $warning_msg ) ) {
221 $out->addWikiTextAsContent(
222 "<div class=\"errorbox\">$warning_msg</div><br clear=\"both\" />"
226 $this->
pageListForm( $titles_for_edit, $titles_for_move, $unmoveable_titles );
243 $replacement_params = [];
250 $replacement_params[
'user_id'] = $user->getId();
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;
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;
272 foreach ( $request->getValues() as $key => $value ) {
273 if ( $value ==
'1' && $key !==
'replace' && $key !==
'use_regex' ) {
274 if ( strpos( $key,
'move-' ) !== false ) {
276 $replacement_params[
'move_page'] =
true;
296 $titles_for_edit = [];
300 $this->selected_namespaces,
306 foreach (
$res as $row ) {
315 return $titles_for_edit;
327 $titles_for_move = [];
328 $unmoveable_titles = [];
332 $this->selected_namespaces,
338 foreach (
$res as $row ) {
352 $moveStatus = $mvPage->isValidMove();
353 $permissionStatus = $mvPage->checkPermissions( $this->
getUser(),
null );
355 if ( $permissionStatus->isOK() && $moveStatus->isOK() ) {
356 $titles_for_move[] =
$title;
358 $unmoveable_titles[] =
$title;
362 return [ $titles_for_move, $unmoveable_titles ];
375 if ( $this->replacement ===
'' ) {
376 return $this->
msg(
'replacetext_blankwarning' )->text();
377 } elseif ( $this->use_regex ) {
382 } elseif ( count( $titles_for_edit ) > 0 ) {
385 $this->selected_namespaces,
390 $count =
$res->numRows();
392 return $this->
msg(
'replacetext_warning' )->numParams( $count )
393 ->params(
"<code><nowiki>{$this->replacement}</nowiki></code>" )->text();
395 } elseif ( count( $titles_for_move ) > 0 ) {
398 $this->selected_namespaces,
403 $count =
$res->numRows();
405 return $this->
msg(
'replacetext_warning' )->numParams( $count )
406 ->params( $this->replacement )->text();
423 'id' =>
'powersearch',
428 Html::hidden(
'title', $this->
getPageTitle()->getPrefixedText() ) .
429 Html::hidden(
'continue', 1 ) .
430 Html::hidden(
'token', $out->getUser()->getEditToken() )
432 if ( is_null( $warning_msg ) ) {
433 $out->addWikiMsg(
'replacetext_docu' );
436 "<div class=\"errorbox\">\n$1\n</div><br clear=\"both\" />",
441 $out->addHTML(
'<table><tr><td style="vertical-align: top;">' );
442 $out->addWikiMsg(
'replacetext_originaltext' );
443 $out->addHTML(
'</td><td>' );
448 Xml::textarea(
'target', $this->target, 100, 5, [
'style' =>
'width: auto;' ] )
450 $out->addHTML(
'</td></tr><tr><td style="vertical-align: top;">' );
451 $out->addWikiMsg(
'replacetext_replacementtext' );
452 $out->addHTML(
'</td><td>' );
454 Xml::textarea(
'replacement', $this->replacement, 100, 5, [
'style' =>
'width: auto;' ] )
456 $out->addHTML(
'</td></tr></table>' );
462 if (
$dbr->getType() !=
'sqlite' &&
$dbr->getType() !=
'mssql' ) {
465 $this->
msg(
'replacetext_useregex' )->text(),
466 'use_regex',
'use_regex'
470 [
'style' =>
'font-style: italic' ],
471 $this->
msg(
'replacetext_regexdocu' )->text()
477 if ( class_exists( MediaWikiServices::class ) ) {
479 $namespaces = MediaWikiServices::getInstance()->getSearchEngineConfig()
480 ->searchableNamespaces();
483 $namespaces = SearchEngine::searchableNamespaces();
487 "<div class=\"mw-search-formheader\"></div>\n" .
488 "<fieldset id=\"mw-searchoptions\">\n" .
489 Xml::tags(
'h4',
null, $this->
msg(
'powersearch-ns' )->parse() )
495 if ( $this->
msg(
'powersearch-togglelabel' )->isDisabled() ) {
501 [
'id' =>
'mw-search-togglebox' ]
506 Xml::element(
'div', [
'class' =>
'divider' ],
'',
false ) .
507 "$tables\n</fieldset>"
510 $category_search_label = $this->
msg(
'replacetext_categorysearch' )->escaped();
511 $prefix_search_label = $this->
msg(
'replacetext_prefixsearch' )->escaped();
513 $rcPageName = $rcPage->getPrefixedText();
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>' .
525 $this->
msg(
'replacetext_editpages' )->text(),
'edit_pages',
'edit_pages',
true
528 $this->
msg(
'replacetext_movepages' )->text(),
'move_pages',
'move_pages'
531 $this->
msg(
'replacetext_announce', $rcPageName )->text(),
'doAnnounce',
'doAnnounce',
true
537 $out->addModules(
'ext.ReplaceText' );
553 foreach ( $namespaces as $ns => $name ) {
555 if ( !array_key_exists( $subj, $rows ) ) {
558 $name = str_replace(
'_',
' ', $name );
560 $name = $this->
msg(
'blanknamespace' )->text();
562 $rows[$subj] .=
Xml::openElement(
'td', [
'style' =>
'white-space: nowrap' ] ) .
563 Xml::checkLabel( $name,
"ns{$ns}",
"mw-search-ns{$ns}", in_array( $ns, $namespaces ) ) .
566 $rows = array_values( $rows );
567 $numRows = count( $rows );
572 'float: right; margin: 0 0 0em 1em' :
'float: left; margin: 0 1em 0em 0';
574 for ( $i = 0; $i < $numRows; $i += $rowsPerTable ) {
576 for ( $j = $i; $j < $i + $rowsPerTable && $j < $numRows; $j++ ) {
577 $tables .=
"<tr>\n" . $rows[$j] .
"</tr>";
589 function pageListForm( $titles_for_edit, $titles_for_move, $unmoveable_titles ) {
595 'id' =>
'choose_pages',
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() )
612 foreach ( $this->selected_namespaces as $ns ) {
613 $out->addHTML( Html::hidden(
'ns' . $ns, 1 ) );
616 $out->addModules(
"ext.ReplaceText" );
617 $out->addModuleStyles(
"ext.ReplaceTextStyles" );
619 $out->addModuleStyles(
"mediawiki.special.search.styles" );
621 if ( count( $titles_for_edit ) > 0 ) {
623 'replacetext_choosepagesforedit',
624 "<code><nowiki>{$this->target}</nowiki></code>",
625 "<code><nowiki>{$this->replacement}</nowiki></code>",
626 $wgLang->formatNum( count( $titles_for_edit ) )
629 foreach ( $titles_for_edit as $title_and_context ) {
637 " - <small>$context</small><br />\n"
640 $out->addHTML(
'<br />' );
643 if ( count( $titles_for_move ) > 0 ) {
645 'replacetext_choosepagesformove',
646 $this->target, $this->replacement,
$wgLang->formatNum( count( $titles_for_move ) )
648 foreach ( $titles_for_move as
$title ) {
654 $out->addHTML(
'<br />' );
655 $out->addWikiMsg(
'replacetext_formovedpages' );
657 $rcPageName = $rcPage->getPrefixedText();
660 $this->
msg(
'replacetext_savemovedpages' )->text(),
661 'create-redirect',
'create-redirect',
true ) .
"<br />\n" .
663 $this->
msg(
'replacetext_watchmovedpages' )->text(),
664 'watch-pages',
'watch-pages',
false ) .
'<br />'
666 $out->addHTML(
'<br />' );
676 if ( count( $titles_for_edit ) + count( $titles_for_move ) > 5 ) {
679 'value' => $this->
msg(
'replacetext_invertselections' )->text(),
681 'id' =>
'replacetext-invert',
682 'class' =>
'mw-replacetext-invert'
690 $out->addHTML(
'</form>' );
692 if ( count( $unmoveable_titles ) > 0 ) {
693 $out->addWikiMsg(
'replacetext_cannotmove',
$wgLang->formatNum( count( $unmoveable_titles ) ) );
695 foreach ( $unmoveable_titles as
$title ) {
699 $out->addHTML( $text );
715 $cw = $this->
getUser()->getOption(
'contextchars', 40 );
719 preg_match_all(
"/$target/Uu", $text,
$matches, PREG_OFFSET_CAPTURE );
721 $targetq = preg_quote(
$target,
'/' );
722 preg_match_all(
"/$targetq/", $text,
$matches, PREG_OFFSET_CAPTURE );
732 for ( $i = 0; $i < count( $poss ); $i++ ) {
738 while ( isset( $poss[$i + 1] ) ) {
739 if ( $poss[$i + 1] < $index + $len + $cw * 2 ) {
740 $len += $poss[$i + 1] - $poss[$i];
747 $cuts[] = [ $index, $len ];
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' ] ) ) {
759 $wgLang->truncate( $contextBefore, - $cw,
'...',
false );
761 $wgLang->truncate( $contextAfter, $cw,
'...',
false );
764 $wgLang->truncateForDatabase( $contextBefore, - $cw,
'...',
false );
766 $wgLang->truncateForDatabase( $contextAfter, $cw,
'...',
false );
771 $targetStr =
"/$target/Uu";
774 $targetStr =
"/$targetq/i";
776 $context .= preg_replace( $targetStr,
'<span class="searchmatch">\0</span>', $snippet );
784 $msg = htmlspecialchars( $message );
785 $msg = preg_replace(
'/^ /m',
'  ', $msg );
786 $msg = preg_replace(
'/ $/m',
'  ', $msg );
787 $msg = preg_replace(
'/ /',
'  ', $msg );
788 # $msg = str_replace( "\n", '<br />', $msg );