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', '&#160; ', $msg );
-               $msg = preg_replace( '/ $/m', ' &#160;', $msg );
-               $msg = preg_replace( '/  /', '&#160; ', $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', '&#160; ', $msg );
+               $msg = preg_replace( '/ $/m', ' &#160;', $msg );
+               $msg = preg_replace( '/  /', '&#160; ', $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

Reply via email to