MarkAHershberger has uploaded a new change for review. (
https://gerrit.wikimedia.org/r/382173 )
Change subject: Refactor & Reorganize ReplaceText for Readability
......................................................................
Refactor & Reorganize ReplaceText for Readability
* Moving files into place
* Also updating .gitignore and adding .dir-locals.el for emacs
* Use ResourceLoader for our JS snippet
* Show bad category notice before searching for titles.
* Using RL needed some adjustments.
* Create our own JS in place of using mediawiki.special.search since
theirs has dependencies we don't provide.
* Break up showForm() into bits for easier comprehension.
* Add CSS to highlight matches
Bug: T177290
Change-Id: I819e08eb6ec59cfb46c5156556c2aae5899cfc97
---
A .dir-locals.el
M .gitignore
D ReplaceText.hooks.php
D ReplaceText.js
D ReplaceText.php
D ReplaceTextJob.php
D SpecialReplaceText.php
M extension.json
R maintenance/ReplaceAll.php
A modules/ReplaceText.css
A modules/ReplaceText.js
A modules/ReplaceTextSearch.js
M package.json
A src/Hook.php
A src/Job.php
R src/Search.php
A src/SpecialPage.php
R src/i18n/Alias.php
18 files changed, 1,559 insertions(+), 902 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/ReplaceText
refs/changes/73/382173/1
diff --git a/.dir-locals.el b/.dir-locals.el
new file mode 100644
index 0000000..75ce2ef
--- /dev/null
+++ b/.dir-locals.el
@@ -0,0 +1,89 @@
+((nil . ((mode . flycheck)
+ (mode . company)
+ (mode . edep)
+ (mode . subword)
+ (tab-width . 4)
+ (c-basic-offset . 4)
+ (indent-tabs-mode . t)
+ (lice:default-license . "gpl-3.0")
+ (eval . (progn (when (fboundp 'delete-trailing-whitespace)
+ (delete-trailing-whitespace))
+ (tabify (point-min)
(point-max))))
+ (c-hanging-braces-alist
+ (defun-open after)
+ (block-open after)
+ (defun-close))
+ (c-offsets-alist . (
+ (access-label . -)
+ (annotation-top-cont .
0)
+ (annotation-var-cont .
+)
+ (arglist-close .
php-lineup-arglist-close)
+ (arglist-cont-nonempty
first
+
php-lineup-cascaded-calls
+
c-lineup-arglist)
+ (arglist-intro .
php-lineup-arglist-intro)
+ (block-close . 0)
+ (block-open . 0)
+ (brace-entry-open . 0)
+ (brace-list-close . 0)
+ (brace-list-entry . 0)
+ (brace-list-intro . +)
+ (brace-list-open . 0)
+ (c .
c-lineup-C-comments)
+ (case-label . 0)
+ (catch-clause . 0)
+ (class-close . 0)
+ (comment-intro . 0)
+ (composition-close . 0)
+ (composition-open . 0)
+ (cpp-define-intro
c-lineup-cpp-define +)
+ (cpp-macro . [0])
+ (cpp-macro-cont . +)
+ (defun-block-intro . +)
+ (defun-close . 0)
+ (defun-open . 0)
+ (do-while-closure . 0)
+ (else-clause . 0)
+ (extern-lang-close . 0)
+ (extern-lang-open . 0)
+ (friend . 0)
+ (func-decl-cont . +)
+ (inclass . +)
+ (incomposition . +)
+ (inexpr-class . +)
+ (inexpr-statement . +)
+ (inextern-lang . +)
+ (inher-cont .
c-lineup-multi-inher)
+ (inher-intro . +)
+ (inlambda . 0)
+ (inline-close . 0)
+ (inline-open . 0)
+ (inmodule . +)
+ (innamespace . +)
+ (knr-argdecl . 0)
+ (knr-argdecl-intro . +)
+ (label . +)
+ (lambda-intro-cont . +)
+ (member-init-cont .
c-lineup-multi-inher)
+ (member-init-intro . +)
+ (module-close . 0)
+ (module-open . 0)
+ (namespace-close . 0)
+ (namespace-open . 0)
+ (statement . 0)
+ (statement-block-intro
. +)
+ (statement-case-intro
. +)
+ (statement-case-open .
0)
+ (statement-cont first
+
php-lineup-cascaded-calls
+
php-lineup-string-cont +)
+ (stream-op .
c-lineup-streamop)
+ (string .
c-lineup-dont-change)
+ (substatement . +)
+ (substatement-label .
2)
+ (substatement-open . 0)
+ (template-args-cont
c-lineup-template-args +)
+ (topmost-intro . 0)
+ (topmost-intro-cont
first php-lineup-cascaded-calls +)
+ ))
+ )))
diff --git a/.gitignore b/.gitignore
index 560f9c4..4bdcced 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@
node_modules/
vendor/
composer.lock
+PHPTAGS.sqlite
\ No newline at end of file
diff --git a/ReplaceText.hooks.php b/ReplaceText.hooks.php
deleted file mode 100644
index 4edf513..0000000
--- a/ReplaceText.hooks.php
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php
-/**
- */
-
-class ReplaceTextHooks {
-
- public static function addToAdminLinks( ALTree &$adminLinksTree ) {
- $generalSection = $adminLinksTree->getSection( wfMessage(
'adminlinks_general' )->text() );
- $extensionsRow = $generalSection->getRow( 'extensions' );
-
- if ( is_null( $extensionsRow ) ) {
- $extensionsRow = new ALRow( 'extensions' );
- $generalSection->addRow( $extensionsRow );
- }
-
- $extensionsRow->addItem( ALItem::newFromSpecialPage(
'ReplaceText' ) );
-
- return true;
- }
-
-}
diff --git a/ReplaceText.js b/ReplaceText.js
deleted file mode 100644
index ddab7b2..0000000
--- a/ReplaceText.js
+++ /dev/null
@@ -1,16 +0,0 @@
-function invertSelections() {
- 'use strict';
-
- var form = document.getElementById('choose_pages' ),
- num_elements = form.elements.length,
- i,
- cur_element;
-
- for (i = 0; i < num_elements; i++) {
- cur_element = form.elements[i];
-
- if (cur_element.type === "checkbox" && cur_element.id !==
'create-redirect' && cur_element.id !== 'watch-pages') {
- form.elements[i].checked = form.elements[i].checked !==
true;
- }
- }
-}
diff --git a/ReplaceText.php b/ReplaceText.php
deleted file mode 100644
index 81c592a..0000000
--- a/ReplaceText.php
+++ /dev/null
@@ -1,67 +0,0 @@
-<?php
-/**
- * Replace Text - a MediaWiki extension that provides a special page to
- * allow administrators to do a global string find-and-replace on all the
- * content pages of a wiki.
- *
- * https://www.mediawiki.org/wiki/Extension:Replace_Text
- *
- * The special page created is 'Special:ReplaceText', and it provides
- * a form to do a global search-and-replace, with the changes to every
- * page showing up as a wiki edit, with the administrator who performed
- * the replacement as the user, and an edit summary that looks like
- * "Text replace: 'search string' * to 'replacement string'".
- *
- * If the replacement string is blank, or is already found in the wiki,
- * the page provides a warning prompt to the user before doing the
- * replacement, since it is not easily reversible.
- */
-
-if ( function_exists( 'wfLoadExtension' ) ) {
- wfLoadExtension( 'ReplaceText' );
- // Keep i18n globals so mergeMessageFileList.php doesn't break
- $wgMessagesDirs['ReplaceText'] = __DIR__ . '/i18n';
- $wgExtensionMessagesFiles['ReplaceTextAlias'] = __DIR__ .
'/ReplaceText.alias.php';
- /* wfWarn(
- 'Deprecated PHP entry point used for Replace Text extension. ' .
- 'Please use wfLoadExtension instead, ' .
- 'see https://www.mediawiki.org/wiki/Extension_registration for
more details.'
- ); */
- return;
-}
-
-if ( !defined( 'MEDIAWIKI' ) ) {
- die();
-}
-
-define( 'REPLACE_TEXT_VERSION', '1.2' );
-
-// credits
-$wgExtensionCredits['specialpage'][] = [
- 'path' => __FILE__,
- 'name' => 'Replace Text',
- 'version' => REPLACE_TEXT_VERSION,
- 'author' => [ 'Yaron Koren', 'Niklas Laxström', '...' ],
- 'url' => 'https://www.mediawiki.org/wiki/Extension:Replace_Text',
- 'descriptionmsg' => 'replacetext-desc',
- 'license-name' => 'GPL-2.0+'
-];
-
-$wgMessagesDirs['ReplaceText'] = __DIR__ . '/i18n';
-$wgExtensionMessagesFiles['ReplaceTextAlias'] = __DIR__ .
'/ReplaceText.alias.php';
-$wgJobClasses['replaceText'] = 'ReplaceTextJob';
-
-// This extension uses its own permission type, 'replacetext'
-$wgAvailableRights[] = 'replacetext';
-$wgGroupPermissions['sysop']['replacetext'] = true;
-
-$wgHooks['AdminLinks'][] = 'ReplaceTextHooks::addToAdminLinks';
-
-$wgSpecialPages['ReplaceText'] = 'SpecialReplaceText';
-$wgAutoloadClasses['ReplaceTextHooks'] = __DIR__ . '/ReplaceText.hooks.php';
-$wgAutoloadClasses['SpecialReplaceText'] = __DIR__ . '/SpecialReplaceText.php';
-$wgAutoloadClasses['ReplaceTextJob'] = __DIR__ . '/ReplaceTextJob.php';
-$wgAutoloadClasses['ReplaceTextSearch'] = __DIR__ . '/ReplaceTextSearch.php';
-
-// Global variables
-$wgReplaceTextUser = null;
diff --git a/ReplaceTextJob.php b/ReplaceTextJob.php
deleted file mode 100644
index 0a8f74b..0000000
--- a/ReplaceTextJob.php
+++ /dev/null
@@ -1,114 +0,0 @@
-<?php
-
-/**
- * Background job to replace text in a given page
- * - based on /includes/RefreshLinksJob.php
- *
- * @author Yaron Koren
- * @author Ankit Garg
- */
-class ReplaceTextJob extends Job {
- function __construct( $title, $params = '', $id = 0 ) {
- parent::__construct( 'replaceText', $title, $params, $id );
- }
-
- /**
- * Run a replaceText job
- * @return bool success
- */
- function run() {
- wfProfileIn( __METHOD__ );
-
- if ( is_null( $this->title ) ) {
- $this->error = "replaceText: Invalid title";
- wfProfileOut( __METHOD__ );
- return false;
- }
-
- if ( array_key_exists( 'move_page', $this->params ) ) {
- global $wgUser;
- $actual_user = $wgUser;
- $wgUser = User::newFromId( $this->params['user_id'] );
- $cur_page_name = $this->title->getText();
- if ( $this->params['use_regex'] ) {
- $new_page_name = preg_replace(
- "/" . $this->params['target_str'] .
"/Uu", $this->params['replacement_str'], $cur_page_name
- );
- } else {
- $new_page_name =
- str_replace(
$this->params['target_str'], $this->params['replacement_str'], $cur_page_name );
- }
-
- $new_title = Title::newFromText( $new_page_name,
$this->title->getNamespace() );
- $reason = $this->params['edit_summary'];
- $create_redirect = $this->params['create_redirect'];
- $this->title->moveTo( $new_title, true, $reason,
$create_redirect );
- if ( $this->params['watch_page'] ) {
- if ( class_exists( 'WatchAction' ) ) {
- // Class was added in MW 1.19
- WatchAction::doWatch( $new_title,
$wgUser );
- } else {
- Action::factory( 'watch', new WikiPage(
$new_title ) )->execute();
- }
- }
- $wgUser = $actual_user;
- } else {
- if ( $this->title->getContentModel() !==
CONTENT_MODEL_WIKITEXT ) {
- $this->error = 'replaceText: Wiki page "' .
- $this->title->getPrefixedDBkey() . '"
does not hold regular wikitext.';
- wfProfileOut( __METHOD__ );
- return false;
- }
- $wikiPage = new WikiPage( $this->title );
- // Is this check necessary?
- if ( !$wikiPage ) {
- $this->error =
- 'replaceText: Wiki page not found for
"' . $this->title->getPrefixedDBkey() . '."';
- wfProfileOut( __METHOD__ );
- return false;
- }
- $wikiPageContent = $wikiPage->getContent();
- if ( is_null( $wikiPageContent ) ) {
- $this->error =
- 'replaceText: No contents found for
wiki page at "' . $this->title->getPrefixedDBkey() . '."';
- wfProfileOut( __METHOD__ );
- return false;
- }
- $article_text = $wikiPageContent->getNativeData();
-
- wfProfileIn( __METHOD__ . '-replace' );
- $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 );
- } else {
- $new_text = str_replace( $target_str,
$replacement_str, $article_text, $num_matches );
- }
-
- // If there's at least one replacement, modify the page,
- // using the passed-in edit summary.
- if ( $num_matches > 0 ) {
- // Change global $wgUser variable to the one
- // specified by the job only for the extent of
- // this replacement.
- global $wgUser;
- $actual_user = $wgUser;
- $wgUser = User::newFromId(
$this->params['user_id'] );
- $edit_summary = $this->params['edit_summary'];
- $flags = EDIT_MINOR;
- if ( $wgUser->isAllowed( 'bot' ) ) {
- $flags |= EDIT_FORCE_BOT;
- }
- $new_content = new WikitextContent( $new_text );
- $wikiPage->doEditContent( $new_content,
$edit_summary, $flags );
- $wgUser = $actual_user;
- }
- wfProfileOut( __METHOD__ . '-replace' );
- }
- wfProfileOut( __METHOD__ );
- return true;
- }
-}
diff --git a/SpecialReplaceText.php b/SpecialReplaceText.php
deleted file mode 100644
index c8d08b4..0000000
--- a/SpecialReplaceText.php
+++ /dev/null
@@ -1,663 +0,0 @@
-<?php
-
-class SpecialReplaceText extends SpecialPage {
- private $target, $replacement, $use_regex,
- $category, $prefix, $edit_pages, $move_pages,
- $selected_namespaces;
-
- public function __construct() {
- parent::__construct( 'ReplaceText', 'replacetext' );
- }
-
- public function doesWrites() {
- return true;
- }
-
- function execute( $query ) {
- if ( !$this->getUser()->isAllowed( 'replacetext' ) ) {
- throw new PermissionsError( 'replacetext' );
- }
-
- $this->setHeaders();
- $out = $this->getOutput();
- if ( !is_null( $out->getResourceLoader()->getModule(
'mediawiki.special' ) ) ) {
- $out->addModuleStyles( 'mediawiki.special' );
- }
- $this->doSpecialReplaceText();
- }
-
- function getSelectedNamespaces() {
- $all_namespaces = SearchEngine::searchableNamespaces();
- $selected_namespaces = [];
- foreach ( $all_namespaces as $ns => $name ) {
- if ( $this->getRequest()->getCheck( 'ns' . $ns ) ) {
- $selected_namespaces[] = $ns;
- }
- }
- return $selected_namespaces;
- }
-
- function doSpecialReplaceText() {
- wfProfileIn( __METHOD__ );
- $out = $this->getOutput();
- $request = $this->getRequest();
-
- $this->target = $request->getText( 'target' );
- $this->replacement = $request->getText( 'replacement' );
- $this->use_regex = $request->getBool( 'use_regex' );
- $this->category = $request->getText( 'category' );
- $this->prefix = $request->getText( 'prefix' );
- $this->edit_pages = $request->getBool( 'edit_pages' );
- $this->move_pages = $request->getBool( 'move_pages' );
- $this->selected_namespaces = $this->getSelectedNamespaces();
-
- if ( $request->getCheck( 'continue' ) && $this->target === '' )
{
- $this->showForm( 'replacetext_givetarget' );
- wfProfileOut( __METHOD__ );
- return;
- }
-
- if ( $request->getCheck( 'replace' ) ) {
- global $wgReplaceTextUser;
-
- $replacement_params = [];
- if ( $wgReplaceTextUser != null ) {
- $user = User::newFromName( $wgReplaceTextUser );
- } else {
- $user = $this->getUser();
- }
- $replacement_params['user_id'] = $user->getId();
- $replacement_params['target_str'] = $this->target;
- $replacement_params['replacement_str'] =
$this->replacement;
- $replacement_params['use_regex'] = $this->use_regex;
- $replacement_params['edit_summary'] = $this->msg(
- 'replacetext_editsummary',
- $this->target, $this->replacement
- )->inContentLanguage()->plain();
- $replacement_params['create_redirect'] = false;
- $replacement_params['watch_page'] = false;
- 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 = [];
- foreach ( $request->getValues() as $key => $value ) {
- if ( $value == '1' && $key !== 'replace' &&
$key !== 'use_regex' ) {
- if ( strpos( $key, 'move-' ) !== false
) {
- $title = Title::newFromID(
substr( $key, 5 ) );
-
$replacement_params['move_page'] = true;
- } else {
- $title = Title::newFromID( $key
);
- }
- if ( $title !== null ) {
- $jobs[] = new ReplaceTextJob(
$title, $replacement_params );
- }
- }
- }
-
- JobQueueGroup::singleton()->push( $jobs );
-
- $count = $this->getLanguage()->formatNum( count( $jobs
) );
- $out->addWikiMsg(
- 'replacetext_success',
- "<code><nowiki>{$this->target}</nowiki></code>",
-
"<code><nowiki>{$this->replacement}</nowiki></code>",
- $count
- );
-
- // Link back
- $out->addHTML(
- Linker::link( $this->getTitle(),
- $this->msg( 'replacetext_return'
)->escaped() )
- );
-
- wfProfileOut( __METHOD__ );
- return;
- } elseif ( $request->getCheck( 'target' ) ) { // very long
elseif, look for "end elseif"
- // 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' );
- wfProfileOut( __METHOD__ );
- return;
- }
- if ( ! $this->edit_pages && ! $this->move_pages ) {
- $this->showForm( 'replacetext_editormove' );
- wfProfileOut( __METHOD__ );
- return;
- }
-
- $titles_for_edit = [];
- $titles_for_move = [];
- $unmoveable_titles = [];
-
- // if user is replacing text within pages...
- if ( $this->edit_pages ) {
- $res = ReplaceTextSearch::doSearchQuery(
- $this->target,
- $this->selected_namespaces,
- $this->category,
- $this->prefix,
- $this->use_regex
- );
-
- foreach ( $res as $row ) {
- $title = Title::makeTitleSafe(
$row->page_namespace, $row->page_title );
- if ( $title == null ) {
- continue;
- }
- $context = $this->extractContext(
$row->old_text, $this->target, $this->use_regex );
- $titles_for_edit[] = [ $title, $context
];
- }
- }
- if ( $this->move_pages ) {
- $res = $this->getMatchingTitles(
- $this->target,
- $this->selected_namespaces,
- $this->category,
- $this->prefix,
- $this->use_regex
- );
-
- foreach ( $res as $row ) {
- $title = Title::makeTitleSafe(
$row->page_namespace, $row->page_title );
- if ( $title == null ) {
- continue;
- }
- // See if this move can happen.
- $cur_page_name = str_replace( '_', ' ',
$row->page_title );
-
- if ( $this->use_regex ) {
- $new_page_name =
- preg_replace( "/" .
$this->target . "/Uu", $this->replacement, $cur_page_name );
- } else {
- $new_page_name =
- str_replace(
$this->target, $this->replacement, $cur_page_name );
- }
-
- $new_title = Title::makeTitleSafe(
$row->page_namespace, $new_page_name );
- $err = $title->isValidMoveOperation(
$new_title );
-
- if ( $title->userCan( 'move' ) &&
!is_array( $err ) ) {
- $titles_for_move[] = $title;
- } else {
- $unmoveable_titles[] = $title;
- }
- }
- }
-
- // 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 ) {
- $bad_cat_name = false;
-
- if ( !empty( $this->category ) ) {
- $category_title = Title::makeTitleSafe(
NS_CATEGORY, $this->category );
- if ( !$category_title->exists() ) {
- $bad_cat_name = true;
- }
- }
-
- if ( $bad_cat_name ) {
- $link = Linker::link( $category_title,
htmlspecialchars( ucfirst( $this->category ) ) );
- $out->addHTML(
- $this->msg(
'replacetext_nosuchcategory' )->rawParams( $link )->escaped()
- );
- } else {
- if ( $this->edit_pages ) {
- $out->addWikiMsg(
-
'replacetext_noreplacement', "<code><nowiki>{$this->target}</nowiki></code>"
- );
- }
-
- if ( $this->move_pages ) {
- $out->addWikiMsg(
'replacetext_nomove', "<code><nowiki>{$this->target}</nowiki></code>" );
- }
- }
- // link back to starting form
- $out->addHTML(
- '<p>' .
- Linker::link(
- $this->getTitle(),
- $this->msg( 'replacetext_return'
)->escaped() )
- . '</p>'
- );
- } else {
- // Show a 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).
- $warning_msg = null;
-
- if ( $this->replacement === '' ) {
- $warning_msg = $this->msg(
'replacetext_blankwarning' )->text();
- } elseif ( count( $titles_for_edit ) > 0 ) {
- $res = ReplaceTextSearch::doSearchQuery(
- $this->replacement,
- $this->selected_namespaces,
- $this->category,
- $this->prefix,
- $this->use_regex
- );
- $count = $res->numRows();
- if ( $count > 0 ) {
- $warning_msg = $this->msg(
'replacetext_warning' )->numParams( $count )
- ->params(
"<code><nowiki>{$this->replacement}</nowiki></code>" )->text();
- }
- } elseif ( count( $titles_for_move ) > 0 ) {
- $res = $this->getMatchingTitles(
- $this->replacement,
- $this->selected_namespaces,
- $this->category,
- $this->prefix, $this->use_regex
- );
- $count = $res->numRows();
- if ( $count > 0 ) {
- $warning_msg = $this->msg(
'replacetext_warning' )->numParams( $count )
- ->params(
$this->replacement )->text();
- }
- }
-
- if ( ! is_null( $warning_msg ) ) {
- $out->addWikiText( "<div
class=\"errorbox\">$warning_msg</div><br clear=\"both\" />" );
- }
-
- $this->pageListForm( $titles_for_edit,
$titles_for_move, $unmoveable_titles );
- }
- wfProfileOut( __METHOD__ );
- return;
- }
-
- // If we're still here, show the starting form.
- $this->showForm();
- wfProfileOut( __METHOD__ );
- }
-
- function showForm( $warning_msg = null ) {
- global $wgVersion;
-
- $out = $this->getOutput();
-
- $out->addHTML(
- Xml::openElement(
- 'form',
- [
- 'id' => 'powersearch',
- 'action' =>
$this->getTitle()->getFullUrl(),
- 'method' => 'post'
- ]
- ) . "\n" .
- Html::hidden( 'title',
$this->getTitle()->getPrefixedText() ) .
- Html::hidden( 'continue', 1 )
- );
- if ( is_null( $warning_msg ) ) {
- $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(
- Xml::textarea( 'target', $this->target, 100, 5, [
'style' => 'width: auto;' ] )
- );
- $out->addHTML( '</td></tr><tr><td style="vertical-align:
top;">' );
- $out->addWikiMsg( 'replacetext_replacementtext' );
- $out->addHTML( '</td><td>' );
- $out->addHTML(
- Xml::textarea( 'replacement', $this->replacement, 100,
5, [ 'style' => 'width: auto;' ] )
- );
- $out->addHTML( '</td></tr></table>' );
- $out->addHTML( Xml::tags( 'p', null,
- Xml::checkLabel(
- $this->msg( 'replacetext_useregex'
)->text(),
- 'use_regex', 'use_regex'
- )
- ) . "\n" .
- Xml::element( 'p',
- [ 'style' => 'font-style: italic' ],
- $this->msg( 'replacetext_regexdocu' )->text()
- )
- );
-
- // The interface is heavily based on the one in Special:Search.
- $namespaces = SearchEngine::searchableNamespaces();
- $tables = $this->namespaceTables( $namespaces );
- $out->addHTML(
- "<div class=\"mw-search-formheader\"></div>\n" .
- "<fieldset id=\"mw-searchoptions\">\n" .
- Xml::tags( 'h4', null, $this->msg( 'powersearch-ns'
)->parse() )
- );
- // 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
- } elseif ( version_compare( $wgVersion, '1.20', '>=' ) ) {
- // In MediaWiki 1.20, this became a lot simpler after
- // the main work was passed off to Javascript
- $out->addHTML(
- Html::element(
- 'div',
- [ 'id' => 'mw-search-togglebox' ]
- )
- );
- } else { // MW <= 1.19
- $out->addHTML(
- Xml::tags(
- 'div',
- [ 'id' => 'mw-search-togglebox' ],
- Xml::label( $this->msg(
'powersearch-togglelabel' )->text(), 'mw-search-togglelabel' ) .
- Xml::element(
- 'input',
- [
- 'type' => 'button',
- 'id' =>
'mw-search-toggleall',
- // 'onclick' value
needed for MW 1.16
- 'onclick' =>
'mwToggleSearchCheckboxes("all");',
- 'value' => $this->msg(
'powersearch-toggleall' )->text()
- ]
- ) .
- Xml::element(
- 'input',
- [
- 'type' => 'button',
- 'id' =>
'mw-search-togglenone',
- // 'onclick' value
needed for MW 1.16
- 'onclick' =>
'mwToggleSearchCheckboxes("none");',
- 'value' => $this->msg(
'powersearch-togglenone' )->text()
- ]
- )
- )
- );
- } // end if
- $out->addHTML(
- Xml::element( 'div', [ 'class' => 'divider' ], '',
false ) .
- "$tables\n</fieldset>"
- );
- // @todo FIXME: raw html messages
- $category_search_label = $this->msg(
'replacetext_categorysearch' )->escaped();
- $prefix_search_label = $this->msg( 'replacetext_prefixsearch'
)->escaped();
- $out->addHTML(
- "<fieldset id=\"mw-searchoptions\">\n" .
- Xml::tags( 'h4', null, $this->msg(
'replacetext_optionalfilters' )->parse() ) .
- Xml::element( 'div', [ 'class' => 'divider' ], '',
false ) .
- "<p>$category_search_label\n" .
- Xml::input( 'category', 20, $this->category, [ 'type'
=> 'text' ] ) . '</p>' .
- "<p>$prefix_search_label\n" .
- Xml::input( 'prefix', 20, $this->prefix, [ 'type' =>
'text' ] ) . '</p>' .
- "</fieldset>\n" .
- "<p>\n" .
- Xml::checkLabel(
- $this->msg( 'replacetext_editpages' )->text(),
'edit_pages', 'edit_pages', true
- ) . '<br />' .
- Xml::checkLabel( $this->msg( 'replacetext_movepages'
)->text(), 'move_pages', 'move_pages' ) .
- "</p>\n" .
- Xml::submitButton( $this->msg( 'replacetext_continue'
)->text() ) .
- Xml::closeElement( 'form' )
- );
- // Add Javascript specific to Special:Search
- $out->addModules( 'mediawiki.special.search' );
- }
-
- /**
- * Copied almost exactly from MediaWiki's SpecialSearch class, i.e.
- * the search page
- */
- function namespaceTables( $namespaces, $rowsPerTable = 3 ) {
- global $wgContLang;
- // 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 = MWNamespace::getSubject( $ns );
- if ( !array_key_exists( $subj, $rows ) ) {
- $rows[$subj] = "";
- }
- $name = str_replace( '_', ' ', $name );
- if ( '' == $name ) {
- $name = $this->msg( 'blanknamespace' )->text();
- }
- $rows[$subj] .= Xml::openElement( 'td', [ 'style' =>
'white-space: nowrap' ] ) .
- Xml::checkLabel( $name, "ns{$ns}",
"mw-search-ns{$ns}", in_array( $ns, $namespaces ) ) .
- Xml::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
- // Float to the right on RTL wikis
- $tableStyle = $wgContLang->isRTL() ?
- 'float: right; margin: 0 0 0em 1em' : 'float: left;
margin: 0 1em 0em 0';
- // Build the final HTML table...
- for ( $i = 0; $i < $numRows; $i += $rowsPerTable ) {
- $tables .= Xml::openElement( 'table', [ 'style' =>
$tableStyle ] );
- for ( $j = $i; $j < $i + $rowsPerTable && $j <
$numRows; $j++ ) {
- $tables .= "<tr>\n" . $rows[$j] . "</tr>";
- }
- $tables .= Xml::closeElement( 'table' ) . "\n";
- }
- return $tables;
- }
-
- function pageListForm( $titles_for_edit, $titles_for_move,
$unmoveable_titles ) {
- global $wgLang, $wgScriptPath;
-
- $out = $this->getOutput();
-
- $formOpts = [
- 'id' => 'choose_pages',
- 'method' => 'post',
- 'action' => $this->getTitle()->getFullUrl()
- ];
- $out->addHTML(
- Xml::openElement( 'form', $formOpts ) . "\n" .
- Html::hidden( 'title',
$this->getTitle()->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( 'replace', 1 )
- );
-
- foreach ( $this->selected_namespaces as $ns ) {
- $out->addHTML( Html::hidden( 'ns' . $ns, 1 ) );
- }
-
- $out->addScriptFile(
"$wgScriptPath/extensions/ReplaceText/ReplaceText.js" );
-
- if ( count( $titles_for_edit ) > 0 ) {
- $out->addWikiMsg(
- 'replacetext_choosepagesforedit',
- "<code><nowiki>{$this->target}</nowiki></code>",
-
"<code><nowiki>{$this->replacement}</nowiki></code>",
- $wgLang->formatNum( count( $titles_for_edit ) )
- );
-
- foreach ( $titles_for_edit as $title_and_context ) {
- /**
- * @var $title Title
- */
- list( $title, $context ) = $title_and_context;
- $out->addHTML(
- Xml::check( $title->getArticleID(),
true ) .
- Linker::link( $title ) . " -
<small>$context</small><br />\n"
- );
- }
- $out->addHTML( '<br />' );
- }
-
- if ( count( $titles_for_move ) > 0 ) {
- $out->addWikiMsg(
- 'replacetext_choosepagesformove',
- $this->target, $this->replacement,
$wgLang->formatNum( count( $titles_for_move ) )
- );
- foreach ( $titles_for_move as $title ) {
- $out->addHTML(
- Xml::check( 'move-' .
$title->getArticleID(), true ) .
- Linker::link( $title ) . "<br />\n"
- );
- }
- $out->addHTML( '<br />' );
- $out->addWikiMsg( 'replacetext_formovedpages' );
- $out->addHTML(
- Xml::checkLabel(
- $this->msg(
'replacetext_savemovedpages' )->text(),
- 'create-redirect',
'create-redirect', true ) . "<br />\n" .
- Xml::checkLabel(
- $this->msg(
'replacetext_watchmovedpages' )->text(), 'watch-pages', 'watch-pages', false
- )
- );
- $out->addHTML( '<br />' );
- }
-
- $out->addHTML(
- "<br />\n" .
- Xml::submitButton( $this->msg( 'replacetext_replace'
)->text() ) . "\n"
- );
-
- // Only show "invert selections" link if there are more than
- // five pages.
- if ( count( $titles_for_edit ) + count( $titles_for_move ) > 5
) {
- $buttonOpts = [
- 'type' => 'button',
- 'value' => $this->msg(
'replacetext_invertselections' )->text(),
- 'onclick' => 'invertSelections(); return false;'
- ];
-
- $out->addHTML(
- Xml::element( 'input', $buttonOpts )
- );
- }
-
- $out->addHTML( '</form>' );
-
- if ( count( $unmoveable_titles ) > 0 ) {
- $out->addWikiMsg( 'replacetext_cannotmove',
$wgLang->formatNum( count( $unmoveable_titles ) ) );
- $text = "<ul>\n";
- foreach ( $unmoveable_titles as $title ) {
- $text .= "<li>" . Linker::link( $title ) . "<br
/>\n";
- }
- $text .= "</ul>\n";
- $out->addHTML( $text );
- }
- }
-
- /**
- * Extract context and highlights search text
- *
- * @todo The bolding needs to be fixed for regular expressions.
- */
- function extractContext( $text, $target, $use_regex = false ) {
- global $wgLang;
-
- wfProfileIn( __METHOD__ );
- $cw = $this->getUser()->getOption( 'contextchars', 40 );
-
- // Get all indexes
- if ( $use_regex ) {
- preg_match_all( "/$target/Uu", $text, $matches,
PREG_OFFSET_CAPTURE );
- } else {
- $targetq = preg_quote( $target, '/' );
- preg_match_all( "/$targetq/", $text, $matches,
PREG_OFFSET_CAPTURE );
- }
-
- $poss = [];
- foreach ( $matches[0] as $_ ) {
- $poss[] = $_[1];
- }
-
- $cuts = [];
- // @codingStandardsIgnoreStart
- for ( $i = 0; $i < count( $poss ); $i++ ) {
- // @codingStandardsIgnoreEnd
- $index = $poss[$i];
- $len = strlen( $target );
-
- // 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 {
- break; // Can't merge, exit the inner
loop
- }
- }
- $cuts[] = [ $index, $len ];
- }
-
- $context = '';
- foreach ( $cuts as $_ ) {
- list( $index, $len, ) = $_;
- $context .= $this->convertWhiteSpaceToHTML(
- $wgLang->truncate( substr( $text, 0, $index ),
- $cw, '...', false )
- );
- $snippet = $this->convertWhiteSpaceToHTML( substr(
$text, $index, $len ) );
- if ( $use_regex ) {
- $targetStr = "/$target/Uu";
- } else {
- $targetq = preg_quote(
$this->convertWhiteSpaceToHTML( $target ), '/' );
- $targetStr = "/$targetq/i";
- }
- $context .= preg_replace( $targetStr, '<span
class="searchmatch">\0</span>', $snippet );
-
- $context .= $this->convertWhiteSpaceToHTML(
- $wgLang->truncate( substr( $text, $index + $len
), $cw, '...', false )
- );
- }
- wfProfileOut( __METHOD__ );
- return $context;
- }
-
- private function convertWhiteSpaceToHTML( $msg ) {
- $msg = htmlspecialchars( $msg );
- $msg = preg_replace( '/^ /m', '  ', $msg );
- $msg = preg_replace( '/ $/m', '  ', $msg );
- $msg = preg_replace( '/ /', '  ', $msg );
- # $msg = str_replace( "\n", '<br />', $msg );
- return $msg;
- }
-
- function getMatchingTitles( $str, $namespaces, $category, $prefix,
$use_regex = false ) {
- $dbr = wfGetDB( DB_REPLICA );
-
- $tables = [ 'page' ];
- $vars = [ 'page_title', 'page_namespace' ];
-
- $str = str_replace( ' ', '_', $str );
- if ( $use_regex ) {
- $comparisonCond = ReplaceTextSearch::regexCond( $dbr,
'page_title', $str );
- } else {
- $any = $dbr->anyString();
- $comparisonCond = 'page_title ' . $dbr->buildLike(
$any, $str, $any );
- }
- $conds = [
- $comparisonCond,
- 'page_namespace' => $namespaces,
- ];
-
- ReplaceTextSearch::categoryCondition( $category, $tables,
$conds );
- ReplaceTextSearch::prefixCondition( $prefix, $conds );
- $sort = [ 'ORDER BY' => 'page_namespace, page_title' ];
-
- return $dbr->select( $tables, $vars, $conds, __METHOD__, $sort
);
- }
-
- protected function getGroupName() {
- return 'wiki';
- }
-}
diff --git a/extension.json b/extension.json
index b239331..23dc50f 100644
--- a/extension.json
+++ b/extension.json
@@ -19,10 +19,10 @@
"replacetext"
],
"SpecialPages": {
- "ReplaceText": "SpecialReplaceText"
+ "ReplaceText": "ReplaceText\\SpecialPage"
},
"JobClasses": {
- "replaceText": "ReplaceTextJob"
+ "replaceText": "ReplaceText\\Job"
},
"MessagesDirs": {
"ReplaceText": [
@@ -30,21 +30,47 @@
]
},
"ExtensionMessagesFiles": {
- "ReplaceTextAlias": "ReplaceText.alias.php"
+ "ReplaceTextAlias": "src/i18n/Alias.php"
},
"AutoloadClasses": {
- "ReplaceTextHooks": "ReplaceText.hooks.php",
- "SpecialReplaceText": "SpecialReplaceText.php",
- "ReplaceTextJob": "ReplaceTextJob.php",
- "ReplaceTextSearch": "ReplaceTextSearch.php"
+ "ReplaceText\\Hook": "src/Hook.php",
+ "ReplaceText\\SpecialPage": "src/SpecialPage.php",
+ "ReplaceText\\Job": "src/Job.php",
+ "ReplaceText\\Search": "src/Search.php"
},
"Hooks": {
"AdminLinks": [
- "ReplaceTextHooks::addToAdminLinks"
+ "ReplaceText\\Hook::addToAdminLinks"
]
},
"config": {
"ReplaceTextUser": null
},
+ "ResourceModules": {
+ "ext.ReplaceText.results": {
+ "styles": "modules/ReplaceText.css"
+ },
+ "ext.ReplaceText.form": {
+ "scripts": "modules/ReplaceText.js",
+ "dependencies": [
+ "mediawiki.special"
+ ]
+ },
+ "ext.ReplaceText.search": {
+ "scripts": "modules/ReplaceTextSearch.js",
+ "dependencies": [
+ "oojs-ui-core"
+ ],
+ "messages": [
+ "powersearch-togglelabel",
+ "powersearch-toggleall",
+ "powersearch-togglenone"
+ ]
+ }
+ },
+ "ResourceFileModulePaths": {
+ "localBasePath": "",
+ "remoteExtPath": "ReplaceText"
+ },
"manifest_version": 1
}
diff --git a/replaceAll.php b/maintenance/ReplaceAll.php
similarity index 94%
rename from replaceAll.php
rename to maintenance/ReplaceAll.php
index 30fe93e..f2c2f5e 100755
--- a/replaceAll.php
+++ b/maintenance/ReplaceAll.php
@@ -31,9 +31,23 @@
* @link https://www.mediawiki.org/wiki/Extension:Replace_Text
*
*/
-// @codingStandardsIgnoreStart
-require_once ( dirname( __FILE__ ) . '/../../maintenance/Maintenance.php' );
-// @codingStandardsIgnoreEnd
+
+namespace ReplaceText;
+
+use Maintenance;
+use MWException;
+use Title;
+use User;
+
+$IP = __DIR__ . '/../../';
+if ( !is_readable( "$IP/maintenance/Maintenance.php" ) ) {
+ $IP = getenv( "MW_INSTALL_PATH" );
+ if ( $IP === false ) {
+ die( "MW_INSTALL_PATH needs to be set to your MediaWiki
installation." );
+ }
+}
+
+require_once "$IP/maintenance/Maintenance.php";
/**
* Maintenance script that generates a plaintext link dump.
@@ -42,7 +56,7 @@
* @SuppressWarnings(StaticAccess)
* @SuppressWarnings(LongVariable)
*/
-class ReplaceText extends Maintenance {
+class ReplaceAll extends Maintenance {
protected $user;
protected $target;
protected $replacement;
@@ -290,7 +304,7 @@
'edit_summary' => $this->summaryMsg,
];
echo "Replacing on $title... ";
- $job = new ReplaceTextJob( $title, $param, 0 );
+ $job = new Job( $title, $param, 0 );
if ( $job->run() !== true ) {
$this->error( "Trouble on the page '$title'." );
}
@@ -353,7 +367,7 @@
}
echo "\n";
}
- $res = ReplaceTextSearch::doSearchQuery(
$target,
+ $res = Search::doSearchQuery( $target,
$this->namespaces, $this->category,
$this->prefix, $useRegex );
if ( $res->numRows() === 0 ) {
@@ -383,5 +397,5 @@
}
}
-$maintClass = "ReplaceText";
+$maintClass = "ReplaceText\\ReplaceAll";
require_once RUN_MAINTENANCE_IF_MAIN;
diff --git a/modules/ReplaceText.css b/modules/ReplaceText.css
new file mode 100644
index 0000000..af1fa8a
--- /dev/null
+++ b/modules/ReplaceText.css
@@ -0,0 +1 @@
+.rt-searchmatch { font-weight: bold; font-size: 1.4em; }
diff --git a/modules/ReplaceText.js b/modules/ReplaceText.js
new file mode 100644
index 0000000..b19f3b2
--- /dev/null
+++ b/modules/ReplaceText.js
@@ -0,0 +1,26 @@
+/* @license GPL 2.0 */
+/* @author Yaron Koren */
+( function ( mw, $ ) {
+ $( function () {
+ invertSelections = function() {
+ 'use strict';
+
+ var form = document.getElementById( 'choose_pages' ),
+ num_elements = form.elements.length,
+ i,
+ cur_element;
+
+ for ( i = 0; i < num_elements; i++ ) {
+ cur_element = form.elements[i];
+
+ if (
+ cur_element.type === "checkbox" &&
+ cur_element.id !==
'create-redirect' &&
+ cur_element.id !== 'watch-pages'
+ ) {
+ form.elements[i].checked =
form.elements[i].checked !== true;
+ }
+ }
+ };
+ } );
+}( mediaWiki, jQuery ) );
diff --git a/modules/ReplaceTextSearch.js b/modules/ReplaceTextSearch.js
new file mode 100644
index 0000000..67e4185
--- /dev/null
+++ b/modules/ReplaceTextSearch.js
@@ -0,0 +1,43 @@
+/*!
+ * JavaScript for Special:ReplaceText
+ */
+( function ( mw, $ ) {
+ $( function () {
+ var $checkboxes, $headerLinks, updateHeaderLinks, searchWidget;
+
+ // Emulate HTML5 autofocus behavior in non HTML5 compliant
browsers
+ if ( !( 'autofocus' in document.createElement( 'input' ) ) ) {
+ $( 'input[autofocus]' ).eq( 0 ).focus();
+ }
+
+ // Create check all/none button
+ $checkboxes = $( '#powersearch input[id^=mw-search-ns]' );
+ $( '#mw-search-togglebox' ).append(
+ $( '<label>' )
+ .text( mw.msg( 'powersearch-togglelabel' ) )
+ ).append(
+ $( '<input>' ).attr( 'type', 'button' )
+ .attr( 'id', 'mw-search-toggleall' )
+ .prop( 'value', mw.msg( 'powersearch-toggleall'
) )
+ .click( function () {
+ $checkboxes.prop( 'checked', true );
+ } )
+ ).append(
+ $( '<input>' ).attr( 'type', 'button' )
+ .attr( 'id', 'mw-search-togglenone' )
+ .prop( 'value', mw.msg(
'powersearch-togglenone' ) )
+ .click( function () {
+ $checkboxes.prop( 'checked', false );
+ } )
+ );
+
+ // Bit stripped here since it was OOjs
+
+ // When saving settings, use the proper request method (POST
instead of GET).
+ $( '#mw-search-powersearch-remember' ).change( function () {
+ this.form.method = this.checked ? 'post' : 'get';
+ } ).trigger( 'change' );
+
+ } );
+
+}( mediaWiki, jQuery ) );
diff --git a/package.json b/package.json
index f36f964..66b28bd 100644
--- a/package.json
+++ b/package.json
@@ -1,12 +1,12 @@
{
"private": true,
"scripts": {
- "test": "grunt test"
+ "test": "grunt test"
},
"devDependencies": {
- "grunt": "1.0.1",
- "grunt-banana-checker": "0.5.0",
- "grunt-contrib-jshint": "1.0.0",
- "grunt-jsonlint": "1.0.7"
+ "grunt": "1.0.1",
+ "grunt-contrib-jshint": "1.0.0",
+ "grunt-banana-checker": "0.5.0",
+ "grunt-jsonlint": "1.0.7"
}
}
diff --git a/src/Hook.php b/src/Hook.php
new file mode 100644
index 0000000..c8f30ac
--- /dev/null
+++ b/src/Hook.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * Hook for AdminLinks
+ *
+ * Copyright (C) 2015 Yaron Koren
+ *
+ * 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.
+ */
+
+namespace ReplaceText;
+
+use ALTree;
+use ALRow;
+
+class Hook {
+
+ /**
+ * Hook for AdminLinks to include ReplaceText
+ * @param ALTree $adminLinksTree the tree
+ */
+ public static function addToAdminLinks( ALTree $adminLinksTree ) {
+ $generalSection = $adminLinksTree->getSection(
+ wfMessage( 'adminlinks_general' )->text()
+ );
+ $extensionsRow = $generalSection->getRow( 'extensions' );
+
+ if ( is_null( $extensionsRow ) ) {
+ $extensionsRow = new ALRow( 'extensions' );
+ $generalSection->addRow( $extensionsRow );
+ }
+
+ $extensionsRow->addItem( ALItem::newFromSpecialPage(
'ReplaceText' ) );
+ }
+
+}
diff --git a/src/Job.php b/src/Job.php
new file mode 100644
index 0000000..d0b9c71
--- /dev/null
+++ b/src/Job.php
@@ -0,0 +1,206 @@
+<?php
+
+/**
+ * Background job to replace text in a given page
+ * - based on /includes/RefreshLinksJob.php
+ *
+ * Copyright (C) 2008-2017 Yaron Koren
+ *
+ * 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.
+ *
+ * @author Yaron Koren
+ * @author Ankit Garg
+ */
+
+namespace ReplaceText;
+
+use Title;
+use User;
+use WatchAction;
+use WikiPage;
+use WikitextContent;
+
+class Job extends \Job {
+ protected $wikiPage;
+ protected $oldText;
+ protected $newText;
+
+ /**
+ * Ye ole constructor
+ * @param Title $title we're working on
+ * @param mixed $params for job
+ * @param int $id default
+ */
+ public function __construct( Title $title, array $params = [], $id = 0
) {
+ parent::__construct( 'replaceText', $title, $params, $id );
+ }
+
+ /**
+ * Run a replaceText job
+ * @return bool success
+ */
+ public function run() {
+ wfProfileIn( __METHOD__ );
+
+ if ( is_null( $this->title ) ) {
+ $this->error = "replaceText: Invalid title";
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ if ( array_key_exists( 'move_page', $this->params ) ) {
+ $this->movePage();
+ } elseif ( $this->replaceText() === false ) {
+ return false;
+ }
+ wfProfileOut( __METHOD__ );
+ return true;
+ }
+
+ /**
+ * ReplaceText job to move the page.
+ */
+ protected function movePage() {
+ global $wgUser;
+ $actual_user = $wgUser;
+ $wgUser = User::newFromId( $this->params['user_id'] );
+
+ $cur_page_name = $this->title->getText();
+ if ( $this->params['use_regex'] ) {
+ $new_page_name = preg_replace(
+ "/" . $this->params['target_str'] . "/Uu",
+ $this->params['replacement_str'], $cur_page_name
+ );
+ } else {
+ $new_page_name = str_replace(
+ $this->params['target_str'],
+ $this->params['replacement_str'],
+ $cur_page_name
+ );
+ }
+
+ $new_title = Title::newFromText(
+ $new_page_name,
+ $this->title->getNamespace()
+ );
+
+ $this->title->moveTo(
+ $new_title, true, $this->params['edit_summary'],
+ $this->params['create_redirect']
+ );
+
+ if ( $this->params['watch_page'] ) {
+ WatchAction::doWatch( $new_title, $wgUser );
+ }
+
+ $wgUser = $actual_user;
+ }
+
+ /**
+ * General ReplaceText
+ * @return bool for the job
+ */
+ protected function replaceText() {
+ if ( $this->canReplaceText() ) {
+ wfProfileIn( __METHOD__ . '-replace' );
+ if ( $this->hasReplacements() ) {
+ return $this->makeReplacments();
+ }
+ wfProfileOut( __METHOD__ . '-replace' );
+ }
+ return false;
+ }
+
+ /**
+ * Determine if we can even do this
+ * @return bool true if we can
+ */
+ protected function canReplaceText() {
+ if ( $this->title->getContentModel() !== CONTENT_MODEL_WIKITEXT
) {
+ $this->error = 'replaceText: Wiki page "' .
+
$this->title->getPrefixedDBkey() .
+ '" does not hold regular
wikitext.';
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+ $this->wikiPage = new WikiPage( $this->title );
+ // Is this check necessary?
+ if ( !$this->wikiPage ) {
+ $this->error = 'replaceText: Wiki page not found for "'
+ .
$this->title->getPrefixedDBkey() . '."';
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+ if ( is_null( $this->wikiPage->getContent() ) ) {
+ $this->error = 'replaceText: No contents found for wiki
page at "' .
+
$this->title->getPrefixedDBkey() . '."';
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+ $this->oldText = $this->wikiPage->getContent()->getNativeData();
+
+ return true;
+ }
+
+ /**
+ * Determine if any replacments are needed.
+ * @return bool true if matches > 0
+ */
+ protected function hasReplacements() {
+ $target_str = $this->params['target_str'];
+ $replacement_str = $this->params['replacement_str'];
+ $num_matches = 0;
+
+ if ( $this->params['use_regex'] ) {
+ $this->newText = preg_replace(
+ '/' . $target_str . '/Uu', $replacement_str,
+ $this->oldText, -1, $num_matches
+ );
+ } else {
+ $this->newText = str_replace(
+ $target_str, $replacement_str,
+ $this->oldText, $num_matches );
+ }
+
+ return $num_matches > 0;
+ }
+
+ /**
+ * Take care of the actual replacements.
+ * @return bool true for now, may find errors that should be false later
+ */
+ protected function makeReplacments() {
+ // Change global $wgUser variable to the one
+ // specified by the job only for the extent of
+ // this replacement.
+ global $wgUser;
+ $actual_user = $wgUser;
+
+ $wgUser = User::newFromId( $this->params['user_id'] );
+ $flags = EDIT_MINOR;
+ if ( $wgUser->isAllowed( 'bot' ) ) {
+ $flags |= EDIT_FORCE_BOT;
+ }
+
+ $new_content = new WikitextContent( $this->newText );
+ $this->wikiPage->doEditContent(
+ $new_content, $this->params['edit_summary'], $flags
+ );
+
+ $wgUser = $actual_user;
+ return true;
+ }
+}
diff --git a/ReplaceTextSearch.php b/src/Search.php
similarity index 64%
rename from ReplaceTextSearch.php
rename to src/Search.php
index f9bf4a3..0c05a0c 100644
--- a/ReplaceTextSearch.php
+++ b/src/Search.php
@@ -1,6 +1,32 @@
<?php
-class ReplaceTextSearch {
+/**
+ * Class to hold the logic for searches.
+ *
+ * Copyright (C) 2014-2017 Mark A. Hershberger
+ *
+ * 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.
+ */
+
+namespace ReplaceText;
+
+use DatabasePostgres;
+use Title;
+
+class Search {
public static function doSearchQuery(
$search, $namespaces, $category, $prefix, $use_regex = false
) {
diff --git a/src/SpecialPage.php b/src/SpecialPage.php
new file mode 100644
index 0000000..6bf7d41
--- /dev/null
+++ b/src/SpecialPage.php
@@ -0,0 +1,1057 @@
+<?php
+
+/**
+ * Special Page for ReplaceText
+ *
+ * Copyright (C) 2008-2017 Yaron Koren
+ * Copyright (C) 2017 Mark A. Hershberger
+ *
+ * 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.
+ */
+
+namespace ReplaceText;
+
+use Html;
+use JobQueueGroup;
+use Linker;
+use MWNamespace;
+use PermissionsError;
+use SearchEngine;
+use Title;
+use User;
+use XML;
+
+class SpecialPage extends \SpecialPage {
+ private $target, $replacement, $use_regex,
+ $category, $prefix, $edit_pages, $move_pages,
+ $selected_namespaces;
+
+ public function __construct() {
+ parent::__construct( 'ReplaceText', 'replacetext' );
+ }
+
+ /**
+ * Indicate that this page cannot operate on a read-only wiki
+ * @return bool
+ */
+ public function doesWrites() {
+ return true;
+ }
+
+ /**
+ * Take care of the actual page load
+ * @param string $query any url stem (not used)
+ * @throws PermissionError
+ * @return mixed
+ */
+ public function execute( $query ) {
+ if ( !$this->getUser()->isAllowed( 'replacetext' ) ) {
+ throw new PermissionsError( 'replacetext' );
+ }
+
+ $this->setHeaders();
+ $out = $this->getOutput();
+ return $this->doSpecialReplaceText();
+ }
+
+ /**
+ * Get the list of selected NS
+ * @return array
+ */
+ protected function getSelectedNamespaces() {
+ $all_namespaces = SearchEngine::searchableNamespaces();
+ $selected_namespaces = [];
+ foreach ( $all_namespaces as $ns => $name ) {
+ if ( $this->getRequest()->getCheck( 'ns' . $ns ) ) {
+ $selected_namespaces[] = $ns;
+ }
+ }
+ return $selected_namespaces;
+ }
+
+ /**
+ * Take care of the form
+ * @return mixed
+ */
+ protected function doSpecialReplaceText() {
+ wfProfileIn( __METHOD__ );
+ $request = $this->getRequest();
+
+ $this->target = $request->getText( 'target' );
+ $this->replacement = $request->getText( 'replacement' );
+ $this->use_regex = $request->getBool( 'use_regex' );
+ $this->category = $request->getText( 'category' );
+ $this->prefix = $request->getText( 'prefix' );
+ $this->edit_pages = $request->getBool( 'edit_pages' );
+ $this->move_pages = $request->getBool( 'move_pages' );
+ $this->selected_namespaces = $this->getSelectedNamespaces();
+
+ if ( $request->getCheck( 'continue' ) && $this->target === '' )
{
+ $this->showForm( 'replacetext_givetarget' );
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+
+ // Don't even try if it isn't going to work
+ if ( $this->hasBadCategory() ) {
+ $this->showBadCategory();
+ return;
+ }
+
+ if ( $request->getCheck( 'replace' ) ) {
+ $response = $this->doReplaceRequest();
+ wfProfileOut( __METHOD__ );
+ return $response;
+ } elseif ( $request->getCheck( 'target' ) ) {
+ $response = $this->doTargetRequest();
+ wfProfileOut( __METHOD__ );
+ return $response;
+ }
+
+ // If we're still here, show the starting form.
+ $this->showForm();
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Examine the request for parameters & replacements
+ * @return array
+ */
+ protected function getReplacementParamsFromRequest() {
+ global $wgReplaceTextUser;
+
+ $request = $this->getRequest();
+ $user = $this->getUser();
+ if ( $wgReplaceTextUser != null ) {
+ $user = User::newFromName( $wgReplaceTextUser );
+ }
+ $replacement_params['user_id'] = $user->getId();
+ $replacement_params['target_str'] = $this->target;
+ $replacement_params['replacement_str'] = $this->replacement;
+ $replacement_params['use_regex'] = $this->use_regex;
+ $replacement_params['edit_summary'] = $this->msg(
+ 'replacetext_editsummary',
+ $this->target, $this->replacement
+ )->inContentLanguage()->plain();
+ $replacement_params['create_redirect'] = false;
+ $replacement_params['watch_page'] = false;
+ 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;
+ }
+ }
+ return $replacement_params;
+ }
+
+ /**
+ * See what ReplaceText jobs we can get from this request
+ * @param array $replacement_params the args we already have
+ * @return array
+ */
+ protected function getJobsFromRequest( $replacement_params ) {
+ $request = $this->getRequest();
+ $jobs = [];
+ foreach ( $request->getValues() as $key => $value ) {
+ if (
+ $value == '1'
+ && $key !== 'replace'
+ && $key !== 'use_regex'
+ ) {
+ if ( strpos( $key, 'move-' ) !== false ) {
+ $title = Title::newFromID( substr(
$key, 5 ) );
+ $replacement_params['move_page'] = true;
+ } else {
+ $title = Title::newFromID( $key );
+ }
+ if ( $title !== null ) {
+ $jobs[] = new Job( $title,
$replacement_params );
+ }
+ }
+ }
+ return $jobs;
+ }
+
+ /**
+ * Handle requests for replacments
+ */
+ protected function doReplaceRequest() {
+ $request = $this->getRequest();
+
+ $jobs = $this->getJobsFromRequest(
+ $this->getReplacementParamsFromRequest()
+ );
+
+ JobQueueGroup::singleton()->push( $jobs );
+
+ $count = $this->getLanguage()->formatNum( count( $jobs ) );
+ $out = $this->getOutput();
+ $out->addWikiMsg(
+ 'replacetext_success',
+ "<code><nowiki>{$this->target}</nowiki></code>",
+ "<code><nowiki>{$this->replacement}</nowiki></code>",
+ $count
+ );
+
+ // Link back
+ $out->addHTML(
+ Linker::link( $this->getTitle(),
+ $this->msg(
'replacetext_return' )->escaped() )
+ );
+
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+
+ /**
+ * Transform the page name
+ * @param string $cur_page_name currently called
+ * @return string
+ */
+ protected function getNewPageName( $cur_page_name ) {
+ // See if this move can happen.
+ $cur_page_name = str_replace( '_', ' ', $cur_page_name );
+
+ if ( $this->use_regex ) {
+ $new_page_name = preg_replace(
+ "/" . $this->target . "/Uu",
+ $this->replacement, $cur_page_name
+ );
+ } else {
+ $new_page_name = str_replace(
+ $this->target, $this->replacement,
+ $cur_page_name
+ );
+ }
+ return $new_page_name;
+ }
+
+ /**
+ * Get the title we are editing
+ * @return array
+ */
+ protected function getTitlesForEdit() {
+ $titles_for_edit = [];
+
+ $res = Search::doSearchQuery(
+ $this->target,
+ $this->selected_namespaces,
+ $this->category,
+ $this->prefix,
+ $this->use_regex
+ );
+
+ foreach ( $res as $row ) {
+ $title = Title::makeTitleSafe(
+ $row->page_namespace, $row->page_title
+ );
+ if ( $title == null ) {
+ continue;
+ }
+ $context = $this->extractContext(
+ $row->old_text, $this->target, $this->use_regex
+ );
+ $titles_for_edit[] = [ $title, $context ];
+ }
+
+ return $titles_for_edit;
+ }
+
+ /**
+ * Get the titles that we need to move
+ * @return array
+ */
+ protected function getTitlesForMove() {
+ $titles_for_move = [];
+ $unmoveable_titles = [];
+
+ $res = $this->getMatchingTitles(
+ $this->target,
+ $this->selected_namespaces,
+ $this->category,
+ $this->prefix,
+ $this->use_regex
+ );
+
+ foreach ( $res as $row ) {
+ $title = Title::makeTitleSafe(
+ $row->page_namespace, $row->page_title
+ );
+ if ( $title == null ) {
+ continue;
+ }
+
+ $new_title = Title::makeTitleSafe(
+ $row->page_namespace, $this->getNewPageName(
$row->page_title )
+ );
+ $err = $title->isValidMoveOperation( $new_title );
+
+ if ( $title->userCan( 'move' ) && !is_array( $err ) ) {
+ $titles_for_move[] = $title;
+ } else {
+ $unmoveable_titles[] = $title;
+ }
+ }
+ return [ $titles_for_move, $unmoveable_titles ];
+ }
+
+ /**
+ * Get the titles we want to edit
+ * @return array
+ */
+ protected function getTitlesFromTargetRequest() {
+ $res = Search::doSearchQuery(
+ $this->target,
+ $this->selected_namespaces,
+ $this->category,
+ $this->prefix,
+ $this->use_regex
+ );
+
+ $titles_for_edit = [];
+ foreach ( $res as $row ) {
+ $title = Title::makeTitleSafe(
+ $row->page_namespace, $row->page_title
+ );
+ if ( $title == null ) {
+ continue;
+ }
+ $context = $this->extractContext(
+ $row->old_text, $this->target, $this->use_regex
+ );
+ $titles_for_edit[] = [ $title, $context ];
+ }
+ return $titles_for_edit;
+ }
+
+ /**
+ * Get Title object for the chosen category.
+ * @return Title
+ */
+ protected function getCategoryTitle() {
+ return Title::makeTitleSafe( NS_CATEGORY, $this->category );
+ }
+
+ /**
+ * Make sure we have a usable category
+ * @return bool
+ */
+ protected function hasBadCategory() {
+ // If no results were found, check to see if a bad
+ // category name was entered.
+ if ( !empty( $this->category ) ) {
+ if ( !$this->getCategoryTitle()->exists() ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Show them the way back since they chose a bad category
+ */
+ protected function showBadCategory() {
+ $link = Linker::link(
+ $this->getCategoryTitle(),
+ htmlspecialchars( ucfirst( $this->category ) )
+ );
+ $this->getOutput()->addHTML(
+ $this->msg( 'replacetext_nosuchcategory' )
+ ->rawParams( $link )->escaped()
+ );
+ $this->showLinkBack();
+ }
+
+ /**
+ * Show a link back to the start
+ */
+ protected function showLinkBack() {
+ $this->getOutput()->addHTML(
+ '<p>' .
+ Linker::link(
+ $this->getTitle(),
+ $this->msg( 'replacetext_return' )->escaped() )
+ . '</p>'
+ );
+ }
+
+ /**
+ * What to do if we don't match anything
+ */
+ protected function handleNoMatches() {
+ $out = $this->getOutput();
+
+ if ( $this->hasBadCategory() ) {
+ $this->showBadCategory();
+ } else {
+ if ( $this->edit_pages ) {
+ $out->addWikiMsg(
+ 'replacetext_noreplacement',
+
"<code><nowiki>{$this->target}</nowiki></code>"
+ );
+ }
+
+ if ( $this->move_pages ) {
+ $out->addWikiMsg(
+ 'replacetext_nomove',
+
"<code><nowiki>{$this->target}</nowiki></code>"
+ );
+ }
+ $this->showLinkBack();
+ }
+ }
+
+ /**
+ * Give the list of pages, maybe get a warning message
+ * @param array $titles_for_edit list of titles to edit
+ * @param array $titles_for_move list of titles to move
+ * @return string
+ */
+ protected function maybeShowWarningMsg( $titles_for_edit,
$titles_for_move ) {
+ $warning_msg = '';
+ if ( $this->replacement === '' ) {
+ $warning_msg = $this->msg(
+ 'replacetext_blankwarning'
+ )->text();
+ } elseif ( count( $titles_for_edit ) > 0 ) {
+ $res = Search::doSearchQuery(
+ $this->replacement,
+ $this->selected_namespaces,
+ $this->category,
+ $this->prefix,
+ $this->use_regex
+ );
+ $count = $res->numRows();
+ if ( $count > 0 ) {
+ $warning_msg = $this->msg(
'replacetext_warning' )
+ ->numParams( $count )
+ ->params(
'<code><nowiki>' .
+
$this->replacement .
+
'</nowiki></code>' )->text();
+ }
+ } elseif ( count( $titles_for_move ) > 0 ) {
+ $res = $this->getMatchingTitles(
+ $this->replacement,
+ $this->selected_namespaces,
+ $this->category,
+ $this->prefix, $this->use_regex
+ );
+ $count = $res->numRows();
+ if ( $count > 0 ) {
+ $warning_msg = $this->msg(
'replacetext_warning' )
+ ->numParams( $count )
+ ->params(
$this->replacement )->text();
+ }
+ }
+ if ( $warning_msg ) {
+ $this->getOutput()->addWikiText(
+ '<div class="errorbox">' . $warning_msg .
+ '</div><br clear="both" />'
+ );
+ }
+ }
+
+ /**
+ * Handle pages we have targed
+ */
+ protected function doTargetRequest() {
+ $out = $this->getOutput();
+ wfProfileIn( __METHOD__ );
+
+ // 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' );
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+ if ( ! $this->edit_pages && ! $this->move_pages ) {
+ $this->showForm( 'replacetext_editormove' );
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+
+ $titles_for_edit = $titles_for_move = $unmoveable_titles = [];
+
+ // if user is replacing text within pages...
+ if ( $this->edit_pages ) {
+ $titles_for_edit = $this->getTitlesForEdit();
+ }
+ if ( $this->move_pages ) {
+ list( $titles_for_move, $unmoveable_titles )
+ = $this->getTitlesForMove();
+ }
+
+ if (
+ count( $titles_for_edit ) == 0
+ && count( $titles_for_move ) == 0
+ ) {
+ $this->handleNoMatches();
+ } else {
+ $this->maybeShowWarningMsg(
+ $titles_for_edit, $titles_for_move
+ );
+
+ $this->pageListForm(
+ $titles_for_edit, $titles_for_move,
$unmoveable_titles
+ );
+ }
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+
+ /**
+ * Header of the form
+ * @param string $warning_msg to show
+ */
+ protected function showFormHeader( $warning_msg = null ) {
+ $out = $this->getOutput();
+ $out->addHTML(
+ Xml::openElement(
+ 'form',
+ [
+ 'id' => 'powersearch',
+ 'action' =>
$this->getTitle()->getFullUrl(),
+ 'method' => 'post'
+ ]
+ ) . "\n" .
+ Html::hidden( 'title',
$this->getTitle()->getPrefixedText() ) .
+ Html::hidden( 'continue', 1 )
+ );
+ if ( is_null( $warning_msg ) ) {
+ $out->addWikiMsg( 'replacetext_docu' );
+ } else {
+ $out->wrapWikiMsg(
+ "<div class=\"errorbox\">\n$1\n</div><br
clear=\"both\" />",
+ $warning_msg
+ );
+ }
+ }
+
+ /**
+ * The boxes to specify search/replace text.
+ */
+ protected function showFormSearchAndReplace() {
+ $out = $this->getOutput();
+ $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(
+ Xml::textarea(
+ 'target', $this->target, 100, 5, [ 'style' =>
'width: auto;' ]
+ )
+ );
+ $out->addHTML( '</td></tr><tr><td style="vertical-align:
top;">' );
+ $out->addWikiMsg( 'replacetext_replacementtext' );
+ $out->addHTML( '</td><td>' );
+ $out->addHTML(
+ Xml::textarea(
+ 'replacement', $this->replacement, 100, 5,
+ [ 'style' => 'width: auto;' ]
+ )
+ );
+ $out->addHTML( '</td></tr></table>' );
+ $out->addHTML( Xml::tags( 'p', null,
+ Xml::checkLabel(
+ $this->msg( 'replacetext_useregex'
)->text(),
+ 'use_regex', 'use_regex'
+ )
+ ) . "\n" .
+ Xml::element( 'p',
+ [ 'style' => 'font-style: italic' ],
+ $this->msg( 'replacetext_regexdocu' )->text()
+ )
+ );
+ }
+
+ /**
+ * The namespace picker
+ */
+ protected function showFormNamespaces() {
+ $out = $this->getOutput();
+ // The interface is heavily based on the one in Special:Search.
+ $namespaces = SearchEngine::searchableNamespaces();
+ $tables = $this->namespaceTables( $namespaces );
+ $out->addHTML(
+ "<div class=\"mw-search-formheader\"></div>\n" .
+ "<fieldset id=\"mw-searchoptions\">\n" .
+ Xml::tags( 'h4', null, $this->msg( 'powersearch-ns'
)->parse() )
+ );
+ // 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() ) {
+ $out->addHTML(
+ Html::element(
+ 'div',
+ [ 'id' => 'mw-search-togglebox' ]
+ )
+ );
+ }
+ $out->addHTML(
+ Xml::element( 'div', [ 'class' => 'divider' ], '',
false ) .
+ "$tables\n</fieldset>"
+ );
+
+ // Add Javascript specific to Special:ReplaceText
+ $out->addModules( 'ext.ReplaceText.search' );
+ }
+
+ /**
+ * Our optional filters
+ */
+ protected function showFormOptionalFilters() {
+ $out = $this->getOutput();
+
+ // @todo FIXME: raw html messages
+ $category_search_label
+ = $this->msg( 'replacetext_categorysearch' )->escaped();
+ $prefix_search_label
+ = $this->msg( 'replacetext_prefixsearch' )->escaped();
+ $out->addHTML(
+ "<fieldset id=\"mw-searchoptions\">\n" .
+ Xml::tags(
+ 'h4', null,
+ $this->msg( 'replacetext_optionalfilters'
)->parse()
+ ) .
+ Xml::element( 'div', [ 'class' => 'divider' ], '',
false ) .
+ "<p>$category_search_label\n" .
+ Xml::input(
+ 'category', 20,
+ $this->category, [ 'type' => 'text' ]
+ ) . "</p><p>$prefix_search_label\n" .
+ Xml::input(
+ 'prefix', 20,
+ $this->prefix, [ 'type' => 'text' ]
+ ) . "</p></fieldset>\n<p>\n" .
+ Xml::checkLabel(
+ $this->msg( 'replacetext_editpages' )->text(),
+ 'edit_pages', 'edit_pages', true
+ ) . '<br />' .
+ Xml::checkLabel(
+ $this->msg( 'replacetext_movepages' )->text(),
+ 'move_pages', 'move_pages'
+ ) .
+ "</p>\n" .
+ Xml::submitButton( $this->msg( 'replacetext_continue'
)->text() ) .
+ Xml::closeElement( 'form' )
+ );
+ }
+
+ /**
+ * Show the ReplaceText form
+ * @param string $warning_msg to show, maybe
+ */
+ protected function showForm( $warning_msg = null ) {
+ $this->showFormHeader( $warning_msg );
+ $this->showFormSearchAndReplace();
+ $this->showFormNamespaces();
+ $this->showFormOptionalFilters();
+ }
+
+ /**
+ * Copied almost exactly from MediaWiki's SpecialSearch class, i.e.
+ * the search page
+ * @param array $namespaces to list
+ * @param int $rowsPerTable to show
+ * @return string
+ */
+ protected function namespaceTables( $namespaces, $rowsPerTable = 3 ) {
+ global $wgContLang;
+ // 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 = MWNamespace::getSubject( $ns );
+ if ( !array_key_exists( $subj, $rows ) ) {
+ $rows[$subj] = "";
+ }
+ $name = str_replace( '_', ' ', $name );
+ if ( '' == $name ) {
+ $name = $this->msg( 'blanknamespace' )->text();
+ }
+ $rows[$subj] .= Xml::openElement(
+ 'td', [ 'style' => 'white-space: nowrap' ]
+ ) . Xml::checkLabel(
+ $name, "ns{$ns}", "mw-search-ns{$ns}",
+ in_array( $ns, $namespaces )
+ ) . Xml::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
+ // Float to the right on RTL wikis
+ $tableStyle = $wgContLang->isRTL()
+ ? 'float: right; margin: 0 0 0em 1em'
+ : 'float: left; margin: 0 1em 0em 0';
+ // Build the final HTML table...
+ for ( $i = 0; $i < $numRows; $i += $rowsPerTable ) {
+ $tables .= Xml::openElement( 'table', [ 'style' =>
$tableStyle ] );
+ for ( $j = $i; $j < $i + $rowsPerTable && $j <
$numRows; $j++ ) {
+ $tables .= "<tr>\n" . $rows[$j] . "</tr>";
+ }
+ $tables .= Xml::closeElement( 'table' ) . "\n";
+ }
+ return $tables;
+ }
+
+ /**
+ * Show the titles that should be edited
+ * @param array $titles_for_edit list of titles to show
+ */
+ protected function showTitlesForEdit( $titles_for_edit ) {
+ global $wgLang;
+ $out = $this->getOutput();
+
+ if ( count( $titles_for_edit ) > 0 ) {
+ $out->addWikiMsg(
+ 'replacetext_choosepagesforedit',
+ "<code><nowiki>{$this->target}</nowiki></code>",
+
"<code><nowiki>{$this->replacement}</nowiki></code>",
+ $wgLang->formatNum( count( $titles_for_edit ) )
+ );
+
+ foreach ( $titles_for_edit as $title_and_context ) {
+ /**
+ * @var $title Title
+ */
+ list( $title, $context ) = $title_and_context;
+ $out->addHTML(
+ Xml::check( $title->getArticleID(),
true ) .
+ Linker::link( $title ) .
+ " - <small>$context</small><br />\n"
+ );
+ }
+ $out->addHTML( '<br />' );
+ }
+ }
+
+ /**
+ * Show the titles slated to be moved
+ * @param array $titles_for_move the titles
+ */
+ protected function showTitlesForMove( $titles_for_move ) {
+ global $wgLang;
+ $out = $this->getOutput();
+
+ if ( count( $titles_for_move ) > 0 ) {
+ $out->addWikiMsg(
+ 'replacetext_choosepagesformove',
+ $this->target, $this->replacement,
+ $wgLang->formatNum( count( $titles_for_move ) )
+ );
+ foreach ( $titles_for_move as $title ) {
+ $out->addHTML(
+ Xml::check( 'move-' .
$title->getArticleID(), true ) .
+ Linker::link( $title ) . "<br />\n"
+ );
+ }
+ $out->addHTML( '<br />' );
+ $out->addWikiMsg( 'replacetext_formovedpages' );
+ $out->addHTML(
+ Xml::checkLabel(
+ $this->msg(
'replacetext_savemovedpages' )->text(),
+ 'create-redirect', 'create-redirect',
true ) . "<br />\n" .
+ Xml::checkLabel(
+ $this->msg(
'replacetext_watchmovedpages' )->text(),
+ 'watch-pages', 'watch-pages', false
+ )
+ );
+ $out->addHTML( '<br />' );
+ }
+ }
+
+ /**
+ * Show the page list form's header
+ */
+ protected function showPageListHeader() {
+ $out = $this->getOutput();
+
+ $formOpts = [
+ 'id' => 'choose_pages',
+ 'method' => 'post',
+ 'action' => $this->getTitle()->getFullUrl()
+ ];
+ $out->addHTML(
+ Xml::openElement( 'form', $formOpts ) . "\n" .
+ Html::hidden( 'title',
$this->getTitle()->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( 'replace', 1 )
+ );
+
+ foreach ( $this->selected_namespaces as $ns ) {
+ $out->addHTML( Html::hidden( 'ns' . $ns, 1 ) );
+ }
+
+ $out->addModules( 'ext.ReplaceText.form' );
+ }
+
+ /**
+ * Show the submit button
+ * @param array $titles_for_edit list of titles to edit
+ * @param array $titles_for_move list of titles to move
+ * @param int $limit if there are more show the invert selection
+ */
+ protected function showPageListFooter(
+ $titles_for_edit, $titles_for_move, $limit
+ ) {
+ $out = $this->getOutput();
+ $out->addHTML(
+ "<br />\n" .
+ Xml::submitButton(
+ $this->msg( 'replacetext_replace' )->text()
+ ) . "\n"
+ );
+ // Only show "invert selections" link if there are more than
+ // five pages.
+ if ( count( $titles_for_edit ) + count( $titles_for_move ) >
$limit ) {
+ $buttonOpts = [
+ 'type' => 'button',
+ 'value' => $this->msg(
'replacetext_invertselections' )->text(),
+ 'onclick' => 'invertSelections(); return false;'
+ ];
+
+ $out->addHTML(
+ Xml::element( 'input', $buttonOpts )
+ );
+ }
+
+ $out->addHTML( '</form>' );
+ }
+
+ /**
+ * Show the unmovable titles, if any
+ * @param array $unmoveable_titles list of titles
+ */
+ protected function showUnmoveableTitles( $unmoveable_titles ) {
+ global $wgLang;
+ $out = $this->getOutput();
+
+ if ( count( $unmoveable_titles ) > 0 ) {
+ $out->addWikiMsg(
+ 'replacetext_cannotmove',
+ $wgLang->formatNum( count( $unmoveable_titles )
)
+ );
+ $text = "<ul>\n";
+ foreach ( $unmoveable_titles as $title ) {
+ $text .= "<li>" . Linker::link( $title ) . "<br
/>\n";
+ }
+ $text .= "</ul>\n";
+ $out->addHTML( $text );
+ }
+ }
+
+ /**
+ * Get the form for the page lists given
+ * @param array $titles_for_edit list of titles to edit
+ * @param array $titles_for_move list of titles to move
+ * @param array $unmoveable_titles titles that can't be moved
+ */
+ protected function pageListForm(
+ $titles_for_edit, $titles_for_move, $unmoveable_titles
+ ) {
+ $this->showPageListHeader();
+
+ $this->showTitlesForEdit( $titles_for_edit );
+ $this->showTitlesForMove( $titles_for_move );
+ $this->showPageListFooter( $titles_for_edit, $titles_for_move,
5 );
+ $this->showUnmoveableTitles( $unmoveable_titles );
+ }
+
+ /**
+ * Get the cuts of text
+ * @param array $poss list of positions
+ * @param string $target to match on
+ * @return array
+ */
+ protected function getCuts( $poss, $target ) {
+ $cuts = [];
+ $cw = $this->getUser()->getOption( 'contextchars', 40 );
+
+ // @codingStandardsIgnoreStart
+ for ( $i = 0; $i < count( $poss ); $i++ ) {
+ // @codingStandardsIgnoreEnd
+ $index = $poss[$i];
+ $len = strlen( $target );
+
+ // 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 ];
+ }
+ return $cuts;
+ }
+
+ /**
+ * Extract context and highlights search text
+ * @param string $text string to match
+ * @param string $target string to perform match on
+ * @param bool $use_regex treat text as regex or no
+ * @return string
+ * @todo The bolding needs to be fixed for regular expressions.
+ */
+ protected function extractContext( $text, $target, $use_regex = false )
{
+ wfProfileIn( __METHOD__ );
+
+ // Get all indexes
+ if ( $use_regex ) {
+ preg_match_all(
+ "/$target/Uu", $text, $matches,
PREG_OFFSET_CAPTURE
+ );
+ } else {
+ $targetq = preg_quote( $target, '/' );
+ preg_match_all(
+ "/$targetq/", $text, $matches,
PREG_OFFSET_CAPTURE
+ );
+ }
+
+ $poss = [];
+ foreach ( $matches[0] as $_ ) {
+ $poss[] = $_[1];
+ }
+
+ $cuts = $this->getCuts( $poss, $target );
+
+ $context = $this->getCutContext( $cuts, $text, $target,
$use_regex );
+
+ wfProfileOut( __METHOD__ );
+ return $context;
+ }
+
+ /**
+ * Get the context of the cut
+ * @param array $cuts list of cuts
+ * @param string $text matching string
+ * @param string $target string to perform match on
+ * @param bool $use_regex treat text as regex or no
+ * @return string
+ */
+ protected function getCutContext( $cuts, $text, $target, $use_regex ) {
+ global $wgLang;
+ $cw = $this->getUser()->getOption( 'contextchars', 40 );
+
+ $context = '';
+ foreach ( $cuts as $_ ) {
+ list( $index, $len, ) = $_;
+ $context .= $this->convertWhiteSpaceToHTML(
+ $wgLang->truncate(
+ substr( $text, 0, $index ), - $cw,
'...', false
+ )
+ );
+ $snippet = $this->convertWhiteSpaceToHTML(
+ substr( $text, $index, $len )
+ );
+ if ( $use_regex ) {
+ $targetStr = "/$target/Uu";
+ } else {
+ $targetq = preg_quote(
+ $this->convertWhiteSpaceToHTML( $target
), '/'
+ );
+ $targetStr = "/$targetq/i";
+ }
+ $context .= preg_replace(
+ $targetStr, '<span
class="rt-searchmatch">\0</span>', $snippet
+ );
+
+ $context .= $this->convertWhiteSpaceToHTML(
$wgLang->truncate(
+ substr( $text, $index + $len ), $cw, '...',
false
+ ) );
+ }
+ $this->getOutput()->addModules( 'ext.ReplaceText.results' );
+
+ return $context;
+ }
+
+ /**
+ * convert the w/s to something htmlish
+ * @param string $msg to convert
+ * @return string
+ */
+ protected function convertWhiteSpaceToHTML( $msg ) {
+ $msg = htmlspecialchars( $msg );
+ $msg = preg_replace( '/^ /m', '  ', $msg );
+ $msg = preg_replace( '/ $/m', '  ', $msg );
+ $msg = preg_replace( '/ /', '  ', $msg );
+ # $msg = str_replace( "\n", '<br />', $msg );
+ return $msg;
+ }
+
+ /**
+ * Get a list of matching titles
+ * @param string $str what to match
+ * @param array $namespaces where to look
+ * @param string $category in which category
+ * @param string $prefix starts with
+ * @param bool $use_regex match uses regular expressions
+ * @return ResultWrapper
+ */
+ protected function getMatchingTitles(
+ $str, array $namespaces, $category, $prefix, $use_regex = false
+ ) {
+ $dbr = wfGetDB( DB_REPLICA );
+
+ $tables = [ 'page' ];
+ $vars = [ 'page_title', 'page_namespace' ];
+
+ $str = str_replace( ' ', '_', $str );
+ if ( $use_regex ) {
+ $comparisonCond = Search::regexCond(
+ $dbr, 'page_title', $str
+ );
+ } else {
+ $any = $dbr->anyString();
+ $comparisonCond = 'page_title ' .
+ $dbr->buildLike( $any,
$str, $any );
+ }
+ $conds = [
+ $comparisonCond,
+ 'page_namespace' => $namespaces,
+ ];
+
+ Search::categoryCondition( $category, $tables, $conds );
+ Search::prefixCondition( $prefix, $conds );
+ $sort = [ 'ORDER BY' => 'page_namespace, page_title' ];
+
+ return $dbr->select( $tables, $vars, $conds, __METHOD__, $sort
);
+ }
+
+ /**
+ * Where we want this special page to show up
+ * @return string
+ */
+ public function getGroupName() {
+ return 'wiki';
+ }
+}
diff --git a/ReplaceText.alias.php b/src/i18n/Alias.php
similarity index 100%
rename from ReplaceText.alias.php
rename to src/i18n/Alias.php
--
To view, visit https://gerrit.wikimedia.org/r/382173
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I819e08eb6ec59cfb46c5156556c2aae5899cfc97
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/ReplaceText
Gerrit-Branch: master
Gerrit-Owner: MarkAHershberger <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits