Add support for slots other than 'main'

This commit adds support for replacing text in multiple content slots.
The names of all the slots that contain the target string are shown on
the special page, and the user can select or deselect which slots they
want to edit. Edits to different slots for the same page are bundled
into a multi-content revision.

If a replace is performed through the 'replaceAll.php' maintenance
script, all slots are taken into account.

Change-Id: Ic4f36fa76e1eaeac8200c60f196c53c2ed78fa45
This commit is contained in:
Marijn van Wezel 2021-11-19 21:59:06 +01:00
parent 9c5199b4f9
commit ed9c752309
No known key found for this signature in database
GPG key ID: 04BEAD6BC9F5AF93
4 changed files with 106 additions and 36 deletions

View file

@ -1,6 +1,6 @@
{
"name": "Replace Text",
"version": "1.6",
"version": "1.7",
"author": [
"Yaron Koren",
"Niklas Laxström",

View file

@ -24,8 +24,8 @@ namespace MediaWiki\Extension\ReplaceText;
use CommentStoreComment;
use Job as JobParent;
use MediaWiki\MediaWikiServices;
use MediaWiki\Revision\SlotRecord;
use RequestContext;
use TextContent;
use Title;
use User;
use WatchAction;
@ -111,19 +111,52 @@ class Job extends JobParent {
}
} else {
if ( $this->title->getContentModel() !== CONTENT_MODEL_WIKITEXT ) {
$this->error = 'replaceText: Wiki page "' .
$this->title->getPrefixedDBkey() . '" does not hold regular wikitext.';
return false;
}
$wikiPage = new WikiPage( $this->title );
$wikiPageContent = $wikiPage->getContent();
if ( $wikiPageContent === null ) {
$latestRevision = $wikiPage->getRevisionRecord();
if ( $latestRevision === null ) {
$this->error =
'replaceText: No contents found for wiki page at "' . $this->title->getPrefixedDBkey() . '."';
'replaceText: No revision found for wiki page at "' . $this->title->getPrefixedDBkey() . '".';
return false;
}
$article_text = $wikiPageContent->getNativeData();
if ( isset( $this->params['roles'] ) ) {
$slotRoles = $this->params['roles'];
} else {
$slotRoles = $latestRevision->getSlotRoles();
}
$revisionSlots = $latestRevision->getSlots();
$updater = $wikiPage->newPageUpdater( $current_user );
$hasMatches = false;
foreach ( $slotRoles as $role ) {
if ( !$revisionSlots->hasSlot( $role ) ) {
$this->error =
'replaceText: Slot "' . $role .
'" does not exist for wiki page "' . $this->title->getPrefixedDBkey() . '".';
return false;
}
$slotContent = $revisionSlots->getContent( $role );
if ( $slotContent->getModel() !== CONTENT_MODEL_WIKITEXT ) {
// The slot does not contain wikitext, give an error.
$this->error =
'replaceText: Slot "' . $role .
'" does not hold regular wikitext for wiki page "' . $this->title->getPrefixedDBkey() . '".';
return false;
}
if ( !( $slotContent instanceof TextContent ) ) {
// Sanity check: Does the slot actually contain TextContent?
$this->error =
'replaceText: Slot "' . $role .
'" does not hold regular wikitext for wiki page "' . $this->title->getPrefixedDBkey() . '".';
return false;
}
$slot_text = $slotContent->getText();
$target_str = $this->params['target_str'];
$replacement_str = $this->params['replacement_str'];
@ -131,16 +164,21 @@ class Job extends JobParent {
if ( $this->params['use_regex'] ) {
$new_text =
preg_replace( '/' . $target_str . '/Uu', $replacement_str, $article_text, -1, $num_matches );
preg_replace( '/' . $target_str . '/Uu', $replacement_str, $slot_text, -1, $num_matches );
} else {
$new_text = str_replace( $target_str, $replacement_str, $article_text, $num_matches );
$new_text = str_replace( $target_str, $replacement_str, $slot_text, $num_matches );
}
// If there's at least one replacement, modify the page,
// using the passed-in edit summary.
// If there's at least one replacement, modify the slot.
if ( $num_matches > 0 ) {
$updater = $wikiPage->newPageUpdater( $current_user );
$updater->setContent( SlotRecord::MAIN, new WikitextContent( $new_text ) );
$hasMatches = true;
$updater->setContent( $role, new WikitextContent( $new_text ) );
}
}
// If at least one slot is edited, modify the page,
// using the passed-in edit summary.
if ( $hasMatches ) {
$edit_summary = CommentStoreComment::newUnsavedComment( $this->params['edit_summary'] );
$flags = EDIT_MINOR;
if ( $permissionManager->userHasRight( $current_user, 'bot' ) ) {

View file

@ -40,7 +40,7 @@ class Search {
$dbr = wfGetDB( DB_REPLICA );
$tables = [ 'page', 'revision', 'text', 'slots', 'content' ];
$vars = [ 'page_id', 'page_namespace', 'page_title', 'old_text' ];
$vars = [ 'page_id', 'page_namespace', 'page_title', 'old_text', 'slot_role_id' ];
if ( $use_regex ) {
$comparisonCond = self::regexCond( $dbr, 'old_text', $search );
} else {

View file

@ -23,6 +23,7 @@ use ErrorPageError;
use Html;
use JobQueueGroup;
use MediaWiki\MediaWikiServices;
use MediaWiki\Storage\SlotRecord;
use OOUI;
use PermissionsError;
use SpecialPage;
@ -276,22 +277,36 @@ class SpecialReplaceText extends SpecialPage {
}
$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' ) {
if ( $key === 'replace' || $key === 'use_regex' ) {
continue;
}
if ( strpos( $key, 'move-' ) !== false ) {
$title = Title::newFromID( (int)substr( $key, 5 ) );
$replacement_params['move_page'] = true;
} else {
$title = Title::newFromID( (int)$key );
}
if ( $title !== null ) {
$jobs[] = new Job( $title, $replacement_params );
}
unset( $replacement_params['move_page'] );
} else {
// Bundle multiple edits to the same page for a different slot into one job
list( $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 );
}
unset( $replacement_params['roles'] );
}
return $jobs;
}
@ -318,9 +333,11 @@ class SpecialReplaceText extends SpecialPage {
if ( $title == null ) {
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 );
$titles_for_edit[] = [ $title, $context ];
$role = $this->extractRole( (int)$row->slot_role_id );
$titles_for_edit[] = [ $title, $context, $role ];
}
return $titles_for_edit;
@ -687,12 +704,16 @@ class SpecialReplaceText extends SpecialPage {
/**
* @var $title Title
*/
list( $title, $context ) = $title_and_context;
list( $title, $context, $role ) = $title_and_context;
$checkbox = new OOUI\CheckboxInputWidget( [
'name' => $title->getArticleID(),
'name' => $title->getArticleID() . "|" . $role,
'selected' => true
] );
if ( $role === SlotRecord::MAIN ) {
$labelText = $linkRenderer->makeLink( $title, null ) . "<br /><small>$context</small>";
} else {
$labelText = $linkRenderer->makeLink( $title, null ) . " ($role) <br /><small>$context</small>";
}
$checkboxLabel = new OOUI\LabelWidget( [
'label' => new OOUI\HtmlSnippet( $labelText )
] );
@ -823,6 +844,17 @@ class SpecialReplaceText extends SpecialPage {
return $context;
}
/**
* Extracts the role name
*
* @param int $role_id
* @return string
*/
private function extractRole( $role_id ) {
$roleStore = MediaWikiServices::getInstance()->getSlotRoleStore();
return $roleStore->getName( $role_id );
}
private function convertWhiteSpaceToHTML( $message ) {
$msg = htmlspecialchars( $message );
$msg = preg_replace( '/^ /m', '&#160; ', $msg );