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' );
129 JobQueueGroup::singleton()->push( $jobs );
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 ) ) {
184 $category_title = Title::makeTitleSafe(
NS_CATEGORY, $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 ) {
275 $title = Title::newFromID( substr( $key, 5 ) );
276 $replacement_params[
'move_page'] =
true;
278 $title = Title::newFromID( $key );
296 $titles_for_edit = [];
300 $this->selected_namespaces,
306 foreach (
$res as $row ) {
307 $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
315 return $titles_for_edit;
327 $titles_for_move = [];
328 $unmoveable_titles = [];
332 $this->selected_namespaces,
338 foreach (
$res as $row ) {
339 $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
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' ) {
463 $out->addHTML( Xml::tags(
'p',
null,
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
534 Xml::submitButton( $this->
msg(
'replacetext_continue' )->text() ) .
535 Xml::closeElement(
'form' )
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 ) ) .
564 Xml::closeElement(
'td' ) .
"\n";
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 ) {
575 $tables .= Xml::openElement(
'table', [
'style' => $tableStyle ] );
576 for ( $j = $i; $j < $i + $rowsPerTable && $j < $numRows; $j++ ) {
577 $tables .=
"<tr>\n" . $rows[$j] .
"</tr>";
579 $tables .= Xml::closeElement(
'table' ) .
"\n";
589 function pageListForm( $titles_for_edit, $titles_for_move, $unmoveable_titles ) {
595 'id' =>
'choose_pages',
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() )
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 ) {
635 Xml::check(
$title->getArticleID(),
true ) .
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 ) {
650 Xml::check(
'move-' .
$title->getArticleID(),
true ) .
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 />' );
671 Xml::submitButton( $this->
msg(
'replacetext_replace' )->text() ) .
"\n"
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'
686 Xml::element(
'input', $buttonOpts )
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 );
$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.
static getSubject( $index)
Get the subject namespace index for a given namespace Special namespaces (NS_MEDIA,...
Handles the backend logic of moving a page from one title to another.
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...
convertWhiteSpaceToHTML( $message)
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