diff --git a/extension.json b/extension.json index eb054258..7a323606 100644 --- a/extension.json +++ b/extension.json @@ -1,6 +1,6 @@ { "name": "Replace Text", - "version": "1.6", + "version": "1.7", "author": [ "Yaron Koren", "Niklas Laxström", diff --git a/src/Job.php b/src/Job.php index 04826585..66e4381c 100644 --- a/src/Job.php +++ b/src/Job.php @@ -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,43 +111,81 @@ 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(); - $target_str = $this->params['target_str']; - $replacement_str = $this->params['replacement_str']; - $num_matches = 0; - - if ( $this->params['use_regex'] ) { - $new_text = - preg_replace( '/' . $target_str . '/Uu', $replacement_str, $article_text, -1, $num_matches ); + if ( isset( $this->params['roles'] ) ) { + $slotRoles = $this->params['roles']; } else { - $new_text = str_replace( $target_str, $replacement_str, $article_text, $num_matches ); + $slotRoles = $latestRevision->getSlotRoles(); } - // If there's at least one replacement, modify the page, + $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']; + $num_matches = 0; + + if ( $this->params['use_regex'] ) { + $new_text = + preg_replace( '/' . $target_str . '/Uu', $replacement_str, $slot_text, -1, $num_matches ); + } else { + $new_text = str_replace( $target_str, $replacement_str, $slot_text, $num_matches ); + } + + // If there's at least one replacement, modify the slot. + if ( $num_matches > 0 ) { + $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 ( $num_matches > 0 ) { - $updater = $wikiPage->newPageUpdater( $current_user ); - $updater->setContent( SlotRecord::MAIN, new WikitextContent( $new_text ) ); + if ( $hasMatches ) { $edit_summary = CommentStoreComment::newUnsavedComment( $this->params['edit_summary'] ); $flags = EDIT_MINOR; if ( $permissionManager->userHasRight( $current_user, 'bot' ) ) { $flags |= EDIT_FORCE_BOT; } if ( isset( $this->params['doAnnounce'] ) && - !$this->params['doAnnounce'] ) { + !$this->params['doAnnounce'] ) { $flags |= EDIT_SUPPRESS_RC; # fixme log this action } diff --git a/src/Search.php b/src/Search.php index b1d74092..09ba9b0a 100644 --- a/src/Search.php +++ b/src/Search.php @@ -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 { diff --git a/src/SpecialReplaceText.php b/src/SpecialReplaceText.php index 854c8eea..1d7ade48 100644 --- a/src/SpecialReplaceText.php +++ b/src/SpecialReplaceText.php @@ -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 ( strpos( $key, 'move-' ) !== false ) { - $title = Title::newFromID( (int)substr( $key, 5 ) ); - $replacement_params['move_page'] = true; - } else { - $title = Title::newFromID( (int)$key ); - } + 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 ); } + 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 ] ); - $labelText = $linkRenderer->makeLink( $title, null ) . "
$context"; + if ( $role === SlotRecord::MAIN ) { + $labelText = $linkRenderer->makeLink( $title, null ) . "
$context"; + } else { + $labelText = $linkRenderer->makeLink( $title, null ) . " ($role)
$context"; + } $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', '  ', $msg );