Bartosz Dziewoński has uploaded a new change for review. ( 
https://gerrit.wikimedia.org/r/377652 )

Change subject: Split off remaining helper classes for special pages to 
separate files
......................................................................

Split off remaining helper classes for special pages to separate files

includes/specials/formfields/
* EditWatchlistCheckboxSeriesField (for SpecialEditWatchlist)
* UploadSourceField                (for SpecialUpload)
* Licenses                         (for SpecialUpload)

includes/specials/forms/
* EditWatchlistNormalHTMLForm      (for SpecialEditWatchlist)
* PreferencesForm                  (for SpecialPreferences)
* UploadForm                       (for SpecialUpload)

includes/specials/helpers/
* ImportReporter                   (for SpecialImport)
* License                          (for SpecialUpload)

Change-Id: I58abcbb44dbf9bf1762b4252555f7552bfa7c253
---
M autoload.php
M includes/Preferences.php
M includes/specials/SpecialEditWatchlist.php
M includes/specials/SpecialImport.php
M includes/specials/SpecialUpload.php
A includes/specials/formfields/EditWatchlistCheckboxSeriesField.php
R includes/specials/formfields/Licenses.php
A includes/specials/formfields/UploadSourceField.php
A includes/specials/forms/EditWatchlistNormalHTMLForm.php
A includes/specials/forms/PreferencesForm.php
A includes/specials/forms/UploadForm.php
A includes/specials/helpers/ImportReporter.php
A includes/specials/helpers/License.php
13 files changed, 967 insertions(+), 828 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core 
refs/changes/52/377652/1

diff --git a/autoload.php b/autoload.php
index 4448204..61fd192 100644
--- a/autoload.php
+++ b/autoload.php
@@ -421,8 +421,8 @@
        'EditAction' => __DIR__ . '/includes/actions/EditAction.php',
        'EditCLI' => __DIR__ . '/maintenance/edit.php',
        'EditPage' => __DIR__ . '/includes/EditPage.php',
-       'EditWatchlistCheckboxSeriesField' => __DIR__ . 
'/includes/specials/SpecialEditWatchlist.php',
-       'EditWatchlistNormalHTMLForm' => __DIR__ . 
'/includes/specials/SpecialEditWatchlist.php',
+       'EditWatchlistCheckboxSeriesField' => __DIR__ . 
'/includes/specials/formfields/EditWatchlistCheckboxSeriesField.php',
+       'EditWatchlistNormalHTMLForm' => __DIR__ . 
'/includes/specials/forms/EditWatchlistNormalHTMLForm.php',
        'EmailConfirmation' => __DIR__ . 
'/includes/specials/SpecialConfirmemail.php',
        'EmailInvalidation' => __DIR__ . 
'/includes/specials/SpecialEmailInvalidate.php',
        'EmailNotification' => __DIR__ . '/includes/mail/EmailNotification.php',
@@ -633,7 +633,7 @@
        'ImageQueryPage' => __DIR__ . 
'/includes/specialpage/ImageQueryPage.php',
        'ImportImages' => __DIR__ . '/maintenance/importImages.php',
        'ImportLogFormatter' => __DIR__ . 
'/includes/logging/ImportLogFormatter.php',
-       'ImportReporter' => __DIR__ . '/includes/specials/SpecialImport.php',
+       'ImportReporter' => __DIR__ . 
'/includes/specials/helpers/ImportReporter.php',
        'ImportSiteScripts' => __DIR__ . '/maintenance/importSiteScripts.php',
        'ImportSites' => __DIR__ . '/maintenance/importSites.php',
        'ImportSource' => __DIR__ . '/includes/import/ImportSource.php',
@@ -746,8 +746,8 @@
        'Languages' => __DIR__ . '/maintenance/language/languages.inc',
        'LayeredParameterizedPassword' => __DIR__ . 
'/includes/password/LayeredParameterizedPassword.php',
        'LegacyLogFormatter' => __DIR__ . '/includes/logging/LogFormatter.php',
-       'License' => __DIR__ . '/includes/Licenses.php',
-       'Licenses' => __DIR__ . '/includes/Licenses.php',
+       'License' => __DIR__ . '/includes/specials/helpers/License.php',
+       'Licenses' => __DIR__ . '/includes/specials/formfields/Licenses.php',
        'LinkBatch' => __DIR__ . '/includes/cache/LinkBatch.php',
        'LinkCache' => __DIR__ . '/includes/cache/LinkCache.php',
        'LinkFilter' => __DIR__ . '/includes/LinkFilter.php',
@@ -1135,7 +1135,7 @@
        'PostgresInstaller' => __DIR__ . 
'/includes/installer/PostgresInstaller.php',
        'PostgresUpdater' => __DIR__ . 
'/includes/installer/PostgresUpdater.php',
        'Preferences' => __DIR__ . '/includes/Preferences.php',
-       'PreferencesForm' => __DIR__ . '/includes/Preferences.php',
+       'PreferencesForm' => __DIR__ . 
'/includes/specials/forms/PreferencesForm.php',
        'PrefixSearch' => __DIR__ . '/includes/PrefixSearch.php',
        'PreprocessDump' => __DIR__ . '/maintenance/preprocessDump.php',
        'Preprocessor' => __DIR__ . '/includes/parser/Preprocessor.php',
@@ -1532,14 +1532,14 @@
        'UploadChunkVerificationException' => __DIR__ . 
'/includes/upload/UploadFromChunks.php',
        'UploadChunkZeroLengthFileException' => __DIR__ . 
'/includes/upload/UploadFromChunks.php',
        'UploadDumper' => __DIR__ . '/maintenance/dumpUploads.php',
-       'UploadForm' => __DIR__ . '/includes/specials/SpecialUpload.php',
+       'UploadForm' => __DIR__ . '/includes/specials/forms/UploadForm.php',
        'UploadFromChunks' => __DIR__ . '/includes/upload/UploadFromChunks.php',
        'UploadFromFile' => __DIR__ . '/includes/upload/UploadFromFile.php',
        'UploadFromStash' => __DIR__ . '/includes/upload/UploadFromStash.php',
        'UploadFromUrl' => __DIR__ . '/includes/upload/UploadFromUrl.php',
        'UploadLogFormatter' => __DIR__ . 
'/includes/logging/UploadLogFormatter.php',
        'UploadSourceAdapter' => __DIR__ . 
'/includes/import/UploadSourceAdapter.php',
-       'UploadSourceField' => __DIR__ . '/includes/specials/SpecialUpload.php',
+       'UploadSourceField' => __DIR__ . 
'/includes/specials/formfields/UploadSourceField.php',
        'UploadStash' => __DIR__ . '/includes/upload/UploadStash.php',
        'UploadStashBadPathException' => __DIR__ . 
'/includes/upload/UploadStash.php',
        'UploadStashCleanup' => __DIR__ . '/maintenance/cleanupUploadStash.php',
diff --git a/includes/Preferences.php b/includes/Preferences.php
index c29c4b9..0bb1d28 100644
--- a/includes/Preferences.php
+++ b/includes/Preferences.php
@@ -1,7 +1,5 @@
 <?php
 /**
- * Form to edit user preferences.
- *
  * 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
@@ -1633,125 +1631,5 @@
                }
 
                return $timeZoneList;
-       }
-}
-
-/** Some tweaks to allow js prefs to work */
-class PreferencesForm extends HTMLForm {
-       // Override default value from HTMLForm
-       protected $mSubSectionBeforeFields = false;
-
-       private $modifiedUser;
-
-       /**
-        * @param User $user
-        */
-       public function setModifiedUser( $user ) {
-               $this->modifiedUser = $user;
-       }
-
-       /**
-        * @return User
-        */
-       public function getModifiedUser() {
-               if ( $this->modifiedUser === null ) {
-                       return $this->getUser();
-               } else {
-                       return $this->modifiedUser;
-               }
-       }
-
-       /**
-        * Get extra parameters for the query string when redirecting after
-        * successful save.
-        *
-        * @return array
-        */
-       public function getExtraSuccessRedirectParameters() {
-               return [];
-       }
-
-       /**
-        * @param string $html
-        * @return string
-        */
-       function wrapForm( $html ) {
-               $html = Xml::tags( 'div', [ 'id' => 'preferences' ], $html );
-
-               return parent::wrapForm( $html );
-       }
-
-       /**
-        * @return string
-        */
-       function getButtons() {
-               $attrs = [ 'id' => 'mw-prefs-restoreprefs' ];
-
-               if ( !$this->getModifiedUser()->isAllowedAny( 
'editmyprivateinfo', 'editmyoptions' ) ) {
-                       return '';
-               }
-
-               $html = parent::getButtons();
-
-               if ( $this->getModifiedUser()->isAllowed( 'editmyoptions' ) ) {
-                       $t = $this->getTitle()->getSubpage( 'reset' );
-
-                       $linkRenderer = 
MediaWikiServices::getInstance()->getLinkRenderer();
-                       $html .= "\n" . $linkRenderer->makeLink( $t, 
$this->msg( 'restoreprefs' )->text(),
-                               Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' 
] ) );
-
-                       $html = Xml::tags( 'div', [ 'class' => 
'mw-prefs-buttons' ], $html );
-               }
-
-               return $html;
-       }
-
-       /**
-        * Separate multi-option preferences into multiple preferences, since we
-        * have to store them separately
-        * @param array $data
-        * @return array
-        */
-       function filterDataForSubmit( $data ) {
-               foreach ( $this->mFlatFields as $fieldname => $field ) {
-                       if ( $field instanceof HTMLNestedFilterable ) {
-                               $info = $field->mParams;
-                               $prefix = isset( $info['prefix'] ) ? 
$info['prefix'] : $fieldname;
-                               foreach ( $field->filterDataForSubmit( 
$data[$fieldname] ) as $key => $value ) {
-                                       $data["$prefix$key"] = $value;
-                               }
-                               unset( $data[$fieldname] );
-                       }
-               }
-
-               return $data;
-       }
-
-       /**
-        * Get the whole body of the form.
-        * @return string
-        */
-       function getBody() {
-               return $this->displaySection( $this->mFieldTree, '', 
'mw-prefsection-' );
-       }
-
-       /**
-        * Get the "<legend>" for a given section key. Normally this is the
-        * prefs-$key message but we'll allow extensions to override it.
-        * @param string $key
-        * @return string
-        */
-       function getLegend( $key ) {
-               $legend = parent::getLegend( $key );
-               Hooks::run( 'PreferencesGetLegend', [ $this, $key, &$legend ] );
-               return $legend;
-       }
-
-       /**
-        * Get the keys of each top level preference section.
-        * @return array of section keys
-        */
-       function getPreferenceSections() {
-               return array_keys( array_filter( $this->mFieldTree, 'is_array' 
) );
        }
 }
diff --git a/includes/specials/SpecialEditWatchlist.php 
b/includes/specials/SpecialEditWatchlist.php
index e1ecfe8..d2940e4 100644
--- a/includes/specials/SpecialEditWatchlist.php
+++ b/includes/specials/SpecialEditWatchlist.php
@@ -770,38 +770,3 @@
                );
        }
 }
-
-/**
- * Extend HTMLForm purely so we can have a more sane way of getting the 
section headers
- */
-class EditWatchlistNormalHTMLForm extends HTMLForm {
-       public function getLegend( $namespace ) {
-               $namespace = substr( $namespace, 2 );
-
-               return $namespace == NS_MAIN
-                       ? $this->msg( 'blanknamespace' )->escaped()
-                       : htmlspecialchars( 
$this->getContext()->getLanguage()->getFormattedNsText( $namespace ) );
-       }
-
-       public function getBody() {
-               return $this->displaySection( $this->mFieldTree, '', 
'editwatchlist-' );
-       }
-}
-
-class EditWatchlistCheckboxSeriesField extends HTMLMultiSelectField {
-       /**
-        * HTMLMultiSelectField throws validation errors if we get input data
-        * that doesn't match the data set in the form setup. This causes
-        * problems if something gets removed from the watchlist while the
-        * form is open (T34126), but we know that invalid items will
-        * be harmless so we can override it here.
-        *
-        * @param string $value The value the field was submitted with
-        * @param array $alldata The data collected from the form
-        * @return bool|string Bool true on success, or String error to display.
-        */
-       function validate( $value, $alldata ) {
-               // Need to call into grandparent to be a good citizen. :)
-               return HTMLFormField::validate( $value, $alldata );
-       }
-}
diff --git a/includes/specials/SpecialImport.php 
b/includes/specials/SpecialImport.php
index beb454d..5d79b66 100644
--- a/includes/specials/SpecialImport.php
+++ b/includes/specials/SpecialImport.php
@@ -524,172 +524,3 @@
                return 'pagetools';
        }
 }
-
-/**
- * Reporting callback
- * @ingroup SpecialPage
- */
-class ImportReporter extends ContextSource {
-       private $reason = false;
-       private $logTags = [];
-       private $mOriginalLogCallback = null;
-       private $mOriginalPageOutCallback = null;
-       private $mLogItemCount = 0;
-
-       /**
-        * @param WikiImporter $importer
-        * @param bool $upload
-        * @param string $interwiki
-        * @param string|bool $reason
-        */
-       function __construct( $importer, $upload, $interwiki, $reason = false ) 
{
-               $this->mOriginalPageOutCallback =
-                       $importer->setPageOutCallback( [ $this, 'reportPage' ] 
);
-               $this->mOriginalLogCallback =
-                       $importer->setLogItemCallback( [ $this, 'reportLogItem' 
] );
-               $importer->setNoticeCallback( [ $this, 'reportNotice' ] );
-               $this->mPageCount = 0;
-               $this->mIsUpload = $upload;
-               $this->mInterwiki = $interwiki;
-               $this->reason = $reason;
-       }
-
-       /**
-        * Sets change tags to apply to the import log entry and null revision.
-        *
-        * @param array $tags
-        * @since 1.29
-        */
-       public function setChangeTags( array $tags ) {
-               $this->logTags = $tags;
-       }
-
-       function open() {
-               $this->getOutput()->addHTML( "<ul>\n" );
-       }
-
-       function reportNotice( $msg, array $params ) {
-               $this->getOutput()->addHTML(
-                       Html::element( 'li', [], $this->msg( $msg, $params 
)->text() )
-               );
-       }
-
-       function reportLogItem( /* ... */ ) {
-               $this->mLogItemCount++;
-               if ( is_callable( $this->mOriginalLogCallback ) ) {
-                       call_user_func_array( $this->mOriginalLogCallback, 
func_get_args() );
-               }
-       }
-
-       /**
-        * @param Title $title
-        * @param ForeignTitle $foreignTitle
-        * @param int $revisionCount
-        * @param int $successCount
-        * @param array $pageInfo
-        * @return void
-        */
-       public function reportPage( $title, $foreignTitle, $revisionCount,
-                       $successCount, $pageInfo ) {
-               $args = func_get_args();
-               call_user_func_array( $this->mOriginalPageOutCallback, $args );
-
-               if ( $title === null ) {
-                       # Invalid or non-importable title; a notice is already 
displayed
-                       return;
-               }
-
-               $this->mPageCount++;
-               $linkRenderer = 
MediaWikiServices::getInstance()->getLinkRenderer();
-               if ( $successCount > 0 ) {
-                       // <bdi> prevents jumbling of the versions count
-                       // in RTL wikis in case the page title is LTR
-                       $this->getOutput()->addHTML(
-                               "<li>" . $linkRenderer->makeLink( $title ) . " 
" .
-                                       "<bdi>" .
-                                       $this->msg( 'import-revision-count' 
)->numParams( $successCount )->escaped() .
-                                       "</bdi>" .
-                                       "</li>\n"
-                       );
-
-                       $logParams = [ '4:number:count' => $successCount ];
-                       if ( $this->mIsUpload ) {
-                               $detail = $this->msg( 
'import-logentry-upload-detail' )->numParams(
-                                       $successCount 
)->inContentLanguage()->text();
-                               $action = 'upload';
-                       } else {
-                               $pageTitle = $foreignTitle->getFullText();
-                               $fullInterwikiPrefix = $this->mInterwiki;
-                               Hooks::run( 'ImportLogInterwikiLink', [ 
&$fullInterwikiPrefix, &$pageTitle ] );
-
-                               $interwikiTitleStr = $fullInterwikiPrefix . ':' 
. $pageTitle;
-                               $interwiki = '[[:' . $interwikiTitleStr . ']]';
-                               $detail = $this->msg( 
'import-logentry-interwiki-detail' )->numParams(
-                                       $successCount )->params( $interwiki 
)->inContentLanguage()->text();
-                               $action = 'interwiki';
-                               $logParams['5:title-link:interwiki'] = 
$interwikiTitleStr;
-                       }
-                       if ( $this->reason ) {
-                               $detail .= $this->msg( 'colon-separator' 
)->inContentLanguage()->text()
-                                       . $this->reason;
-                       }
-
-                       $comment = $detail; // quick
-                       $dbw = wfGetDB( DB_MASTER );
-                       $latest = $title->getLatestRevID();
-                       $nullRevision = Revision::newNullRevision(
-                               $dbw,
-                               $title->getArticleID(),
-                               $comment,
-                               true,
-                               $this->getUser()
-                       );
-
-                       $nullRevId = null;
-                       if ( !is_null( $nullRevision ) ) {
-                               $nullRevId = $nullRevision->insertOn( $dbw );
-                               $page = WikiPage::factory( $title );
-                               # Update page record
-                               $page->updateRevisionOn( $dbw, $nullRevision );
-                               Hooks::run(
-                                       'NewRevisionFromEditComplete',
-                                       [ $page, $nullRevision, $latest, 
$this->getUser() ]
-                               );
-                       }
-
-                       // Create the import log entry
-                       $logEntry = new ManualLogEntry( 'import', $action );
-                       $logEntry->setTarget( $title );
-                       $logEntry->setComment( $this->reason );
-                       $logEntry->setPerformer( $this->getUser() );
-                       $logEntry->setParameters( $logParams );
-                       $logid = $logEntry->insert();
-                       if ( count( $this->logTags ) ) {
-                               $logEntry->setTags( $this->logTags );
-                       }
-                       // Make sure the null revision will be tagged as well
-                       $logEntry->setAssociatedRevId( $nullRevId );
-
-                       $logEntry->publish( $logid );
-
-               } else {
-                       $this->getOutput()->addHTML( "<li>" . 
$linkRenderer->makeKnownLink( $title ) . " " .
-                               $this->msg( 'import-nonewrevisions' 
)->escaped() . "</li>\n" );
-               }
-       }
-
-       function close() {
-               $out = $this->getOutput();
-               if ( $this->mLogItemCount > 0 ) {
-                       $msg = $this->msg( 'imported-log-entries' )->numParams( 
$this->mLogItemCount )->parse();
-                       $out->addHTML( Xml::tags( 'li', null, $msg ) );
-               } elseif ( $this->mPageCount == 0 && $this->mLogItemCount == 0 
) {
-                       $out->addHTML( "</ul>\n" );
-
-                       return Status::newFatal( 'importnopages' );
-               }
-               $out->addHTML( "</ul>\n" );
-
-               return Status::newGood( $this->mPageCount );
-       }
-}
diff --git a/includes/specials/SpecialUpload.php 
b/includes/specials/SpecialUpload.php
index b98fad1..bbec122 100644
--- a/includes/specials/SpecialUpload.php
+++ b/includes/specials/SpecialUpload.php
@@ -846,476 +846,3 @@
                return $bitmapHandler->autoRotateEnabled();
        }
 }
-
-/**
- * Sub class of HTMLForm that provides the form section of SpecialUpload
- */
-class UploadForm extends HTMLForm {
-       protected $mWatch;
-       protected $mForReUpload;
-       protected $mSessionKey;
-       protected $mHideIgnoreWarning;
-       protected $mDestWarningAck;
-       protected $mDestFile;
-
-       protected $mComment;
-       protected $mTextTop;
-       protected $mTextAfterSummary;
-
-       protected $mSourceIds;
-
-       protected $mMaxFileSize = [];
-
-       protected $mMaxUploadSize = [];
-
-       public function __construct( array $options = [], IContextSource 
$context = null,
-               LinkRenderer $linkRenderer = null
-       ) {
-               if ( $context instanceof IContextSource ) {
-                       $this->setContext( $context );
-               }
-
-               if ( !$linkRenderer ) {
-                       $linkRenderer = 
MediaWikiServices::getInstance()->getLinkRenderer();
-               }
-
-               $this->mWatch = !empty( $options['watch'] );
-               $this->mForReUpload = !empty( $options['forreupload'] );
-               $this->mSessionKey = isset( $options['sessionkey'] ) ? 
$options['sessionkey'] : '';
-               $this->mHideIgnoreWarning = !empty( 
$options['hideignorewarning'] );
-               $this->mDestWarningAck = !empty( $options['destwarningack'] );
-               $this->mDestFile = isset( $options['destfile'] ) ? 
$options['destfile'] : '';
-
-               $this->mComment = isset( $options['description'] ) ?
-                       $options['description'] : '';
-
-               $this->mTextTop = isset( $options['texttop'] )
-                       ? $options['texttop'] : '';
-
-               $this->mTextAfterSummary = isset( $options['textaftersummary'] )
-                       ? $options['textaftersummary'] : '';
-
-               $sourceDescriptor = $this->getSourceSection();
-               $descriptor = $sourceDescriptor
-                       + $this->getDescriptionSection()
-                       + $this->getOptionsSection();
-
-               Hooks::run( 'UploadFormInitDescriptor', [ &$descriptor ] );
-               parent::__construct( $descriptor, $context, 'upload' );
-
-               # Add a link to edit MediaWiki:Licenses
-               if ( $this->getUser()->isAllowed( 'editinterface' ) ) {
-                       $this->getOutput()->addModuleStyles( 
'mediawiki.special.upload.styles' );
-                       $licensesLink = $linkRenderer->makeKnownLink(
-                               $this->msg( 'licenses' 
)->inContentLanguage()->getTitle(),
-                               $this->msg( 'licenses-edit' )->text(),
-                               [],
-                               [ 'action' => 'edit' ]
-                       );
-                       $editLicenses = '<p class="mw-upload-editlicenses">' . 
$licensesLink . '</p>';
-                       $this->addFooterText( $editLicenses, 'description' );
-               }
-
-               # Set some form properties
-               $this->setSubmitText( $this->msg( 'uploadbtn' )->text() );
-               $this->setSubmitName( 'wpUpload' );
-               # Used message keys: 'accesskey-upload', 'tooltip-upload'
-               $this->setSubmitTooltip( 'upload' );
-               $this->setId( 'mw-upload-form' );
-
-               # Build a list of IDs for javascript insertion
-               $this->mSourceIds = [];
-               foreach ( $sourceDescriptor as $field ) {
-                       if ( !empty( $field['id'] ) ) {
-                               $this->mSourceIds[] = $field['id'];
-                       }
-               }
-       }
-
-       /**
-        * Get the descriptor of the fieldset that contains the file source
-        * selection. The section is 'source'
-        *
-        * @return array Descriptor array
-        */
-       protected function getSourceSection() {
-               if ( $this->mSessionKey ) {
-                       return [
-                               'SessionKey' => [
-                                       'type' => 'hidden',
-                                       'default' => $this->mSessionKey,
-                               ],
-                               'SourceType' => [
-                                       'type' => 'hidden',
-                                       'default' => 'Stash',
-                               ],
-                       ];
-               }
-
-               $canUploadByUrl = UploadFromUrl::isEnabled()
-                       && ( UploadFromUrl::isAllowed( $this->getUser() ) === 
true )
-                       && $this->getConfig()->get( 
'CopyUploadsFromSpecialUpload' );
-               $radio = $canUploadByUrl;
-               $selectedSourceType = strtolower( $this->getRequest()->getText( 
'wpSourceType', 'File' ) );
-
-               $descriptor = [];
-               if ( $this->mTextTop ) {
-                       $descriptor['UploadFormTextTop'] = [
-                               'type' => 'info',
-                               'section' => 'source',
-                               'default' => $this->mTextTop,
-                               'raw' => true,
-                       ];
-               }
-
-               $this->mMaxUploadSize['file'] = min(
-                       UploadBase::getMaxUploadSize( 'file' ),
-                       UploadBase::getMaxPhpUploadSize()
-               );
-
-               $help = $this->msg( 'upload-maxfilesize',
-                               $this->getContext()->getLanguage()->formatSize( 
$this->mMaxUploadSize['file'] )
-                       )->parse();
-
-               // If the user can also upload by URL, there are 2 different 
file size limits.
-               // This extra message helps stress which limit corresponds to 
what.
-               if ( $canUploadByUrl ) {
-                       $help .= $this->msg( 'word-separator' )->escaped();
-                       $help .= $this->msg( 'upload_source_file' )->parse();
-               }
-
-               $descriptor['UploadFile'] = [
-                       'class' => 'UploadSourceField',
-                       'section' => 'source',
-                       'type' => 'file',
-                       'id' => 'wpUploadFile',
-                       'radio-id' => 'wpSourceTypeFile',
-                       'label-message' => 'sourcefilename',
-                       'upload-type' => 'File',
-                       'radio' => &$radio,
-                       'help' => $help,
-                       'checked' => $selectedSourceType == 'file',
-               ];
-
-               if ( $canUploadByUrl ) {
-                       $this->mMaxUploadSize['url'] = 
UploadBase::getMaxUploadSize( 'url' );
-                       $descriptor['UploadFileURL'] = [
-                               'class' => 'UploadSourceField',
-                               'section' => 'source',
-                               'id' => 'wpUploadFileURL',
-                               'radio-id' => 'wpSourceTypeurl',
-                               'label-message' => 'sourceurl',
-                               'upload-type' => 'url',
-                               'radio' => &$radio,
-                               'help' => $this->msg( 'upload-maxfilesize',
-                                       
$this->getContext()->getLanguage()->formatSize( $this->mMaxUploadSize['url'] )
-                               )->parse() .
-                                       $this->msg( 'word-separator' 
)->escaped() .
-                                       $this->msg( 'upload_source_url' 
)->parse(),
-                               'checked' => $selectedSourceType == 'url',
-                       ];
-               }
-               Hooks::run( 'UploadFormSourceDescriptors', [ &$descriptor, 
&$radio, $selectedSourceType ] );
-
-               $descriptor['Extensions'] = [
-                       'type' => 'info',
-                       'section' => 'source',
-                       'default' => $this->getExtensionsMessage(),
-                       'raw' => true,
-               ];
-
-               return $descriptor;
-       }
-
-       /**
-        * Get the messages indicating which extensions are preferred and 
prohibitted.
-        *
-        * @return string HTML string containing the message
-        */
-       protected function getExtensionsMessage() {
-               # Print a list of allowed file extensions, if so configured.  
We ignore
-               # MIME type here, it's incomprehensible to most people and too 
long.
-               $config = $this->getConfig();
-
-               if ( $config->get( 'CheckFileExtensions' ) ) {
-                       $fileExtensions = array_unique( $config->get( 
'FileExtensions' ) );
-                       if ( $config->get( 'StrictFileExtensions' ) ) {
-                               # Everything not permitted is banned
-                               $extensionsList =
-                                       '<div id="mw-upload-permitted">' .
-                                       $this->msg( 'upload-permitted' )
-                                               ->params( 
$this->getLanguage()->commaList( $fileExtensions ) )
-                                               ->numParams( count( 
$fileExtensions ) )
-                                               ->parseAsBlock() .
-                                       "</div>\n";
-                       } else {
-                               # We have to list both preferred and prohibited
-                               $fileBlacklist = array_unique( $config->get( 
'FileBlacklist' ) );
-                               $extensionsList =
-                                       '<div id="mw-upload-preferred">' .
-                                               $this->msg( 'upload-preferred' )
-                                                       ->params( 
$this->getLanguage()->commaList( $fileExtensions ) )
-                                                       ->numParams( count( 
$fileExtensions ) )
-                                                       ->parseAsBlock() .
-                                       "</div>\n" .
-                                       '<div id="mw-upload-prohibited">' .
-                                               $this->msg( 'upload-prohibited' 
)
-                                                       ->params( 
$this->getLanguage()->commaList( $fileBlacklist ) )
-                                                       ->numParams( count( 
$fileBlacklist ) )
-                                                       ->parseAsBlock() .
-                                       "</div>\n";
-                       }
-               } else {
-                       # Everything is permitted.
-                       $extensionsList = '';
-               }
-
-               return $extensionsList;
-       }
-
-       /**
-        * Get the descriptor of the fieldset that contains the file description
-        * input. The section is 'description'
-        *
-        * @return array Descriptor array
-        */
-       protected function getDescriptionSection() {
-               $config = $this->getConfig();
-               if ( $this->mSessionKey ) {
-                       $stash = 
RepoGroup::singleton()->getLocalRepo()->getUploadStash( $this->getUser() );
-                       try {
-                               $file = $stash->getFile( $this->mSessionKey );
-                       } catch ( Exception $e ) {
-                               $file = null;
-                       }
-                       if ( $file ) {
-                               global $wgContLang;
-
-                               $mto = $file->transform( [ 'width' => 120 ] );
-                               if ( $mto ) {
-                                       $this->addHeaderText(
-                                               '<div class="thumb t' . 
$wgContLang->alignEnd() . '">' .
-                                               Html::element( 'img', [
-                                                       'src' => $mto->getUrl(),
-                                                       'class' => 'thumbimage',
-                                               ] ) . '</div>', 'description' );
-                               }
-                       }
-               }
-
-               $descriptor = [
-                       'DestFile' => [
-                               'type' => 'text',
-                               'section' => 'description',
-                               'id' => 'wpDestFile',
-                               'label-message' => 'destfilename',
-                               'size' => 60,
-                               'default' => $this->mDestFile,
-                               # @todo FIXME: Hack to work around poor 
handling of the 'default' option in HTMLForm
-                               'nodata' => strval( $this->mDestFile ) !== '',
-                       ],
-                       'UploadDescription' => [
-                               'type' => 'textarea',
-                               'section' => 'description',
-                               'id' => 'wpUploadDescription',
-                               'label-message' => $this->mForReUpload
-                                       ? 'filereuploadsummary'
-                                       : 'fileuploadsummary',
-                               'default' => $this->mComment,
-                               'cols' => 80,
-                               'rows' => 8,
-                       ]
-               ];
-               if ( $this->mTextAfterSummary ) {
-                       $descriptor['UploadFormTextAfterSummary'] = [
-                               'type' => 'info',
-                               'section' => 'description',
-                               'default' => $this->mTextAfterSummary,
-                               'raw' => true,
-                       ];
-               }
-
-               $descriptor += [
-                       'EditTools' => [
-                               'type' => 'edittools',
-                               'section' => 'description',
-                               'message' => 'edittools-upload',
-                       ]
-               ];
-
-               if ( $this->mForReUpload ) {
-                       $descriptor['DestFile']['readonly'] = true;
-               } else {
-                       $descriptor['License'] = [
-                               'type' => 'select',
-                               'class' => 'Licenses',
-                               'section' => 'description',
-                               'id' => 'wpLicense',
-                               'label-message' => 'license',
-                       ];
-               }
-
-               if ( $config->get( 'UseCopyrightUpload' ) ) {
-                       $descriptor['UploadCopyStatus'] = [
-                               'type' => 'text',
-                               'section' => 'description',
-                               'id' => 'wpUploadCopyStatus',
-                               'label-message' => 'filestatus',
-                       ];
-                       $descriptor['UploadSource'] = [
-                               'type' => 'text',
-                               'section' => 'description',
-                               'id' => 'wpUploadSource',
-                               'label-message' => 'filesource',
-                       ];
-               }
-
-               return $descriptor;
-       }
-
-       /**
-        * Get the descriptor of the fieldset that contains the upload options,
-        * such as "watch this file". The section is 'options'
-        *
-        * @return array Descriptor array
-        */
-       protected function getOptionsSection() {
-               $user = $this->getUser();
-               if ( $user->isLoggedIn() ) {
-                       $descriptor = [
-                               'Watchthis' => [
-                                       'type' => 'check',
-                                       'id' => 'wpWatchthis',
-                                       'label-message' => 'watchthisupload',
-                                       'section' => 'options',
-                                       'default' => $this->mWatch,
-                               ]
-                       ];
-               }
-               if ( !$this->mHideIgnoreWarning ) {
-                       $descriptor['IgnoreWarning'] = [
-                               'type' => 'check',
-                               'id' => 'wpIgnoreWarning',
-                               'label-message' => 'ignorewarnings',
-                               'section' => 'options',
-                       ];
-               }
-
-               $descriptor['DestFileWarningAck'] = [
-                       'type' => 'hidden',
-                       'id' => 'wpDestFileWarningAck',
-                       'default' => $this->mDestWarningAck ? '1' : '',
-               ];
-
-               if ( $this->mForReUpload ) {
-                       $descriptor['ForReUpload'] = [
-                               'type' => 'hidden',
-                               'id' => 'wpForReUpload',
-                               'default' => '1',
-                       ];
-               }
-
-               return $descriptor;
-       }
-
-       /**
-        * Add the upload JS and show the form.
-        */
-       public function show() {
-               $this->addUploadJS();
-               parent::show();
-       }
-
-       /**
-        * Add upload JS to the OutputPage
-        */
-       protected function addUploadJS() {
-               $config = $this->getConfig();
-
-               $useAjaxDestCheck = $config->get( 'UseAjax' ) && $config->get( 
'AjaxUploadDestCheck' );
-               $useAjaxLicensePreview = $config->get( 'UseAjax' ) &&
-                       $config->get( 'AjaxLicensePreview' ) && $config->get( 
'EnableAPI' );
-               $this->mMaxUploadSize['*'] = UploadBase::getMaxUploadSize();
-
-               $scriptVars = [
-                       'wgAjaxUploadDestCheck' => $useAjaxDestCheck,
-                       'wgAjaxLicensePreview' => $useAjaxLicensePreview,
-                       'wgUploadAutoFill' => !$this->mForReUpload &&
-                               // If we received mDestFile from the request, 
don't autofill
-                               // the wpDestFile textbox
-                               $this->mDestFile === '',
-                       'wgUploadSourceIds' => $this->mSourceIds,
-                       'wgCheckFileExtensions' => $config->get( 
'CheckFileExtensions' ),
-                       'wgStrictFileExtensions' => $config->get( 
'StrictFileExtensions' ),
-                       'wgFileExtensions' => array_values( array_unique( 
$config->get( 'FileExtensions' ) ) ),
-                       'wgCapitalizeUploads' => MWNamespace::isCapitalized( 
NS_FILE ),
-                       'wgMaxUploadSize' => $this->mMaxUploadSize,
-                       'wgFileCanRotate' => SpecialUpload::rotationEnabled(),
-               ];
-
-               $out = $this->getOutput();
-               $out->addJsConfigVars( $scriptVars );
-
-               $out->addModules( [
-                       'mediawiki.special.upload', // Extras for thumbnail and 
license preview.
-               ] );
-       }
-
-       /**
-        * Empty function; submission is handled elsewhere.
-        *
-        * @return bool False
-        */
-       function trySubmit() {
-               return false;
-       }
-}
-
-/**
- * A form field that contains a radio box in the label
- */
-class UploadSourceField extends HTMLTextField {
-
-       /**
-        * @param array $cellAttributes
-        * @return string
-        */
-       function getLabelHtml( $cellAttributes = [] ) {
-               $id = $this->mParams['id'];
-               $label = Html::rawElement( 'label', [ 'for' => $id ], 
$this->mLabel );
-
-               if ( !empty( $this->mParams['radio'] ) ) {
-                       if ( isset( $this->mParams['radio-id'] ) ) {
-                               $radioId = $this->mParams['radio-id'];
-                       } else {
-                               // Old way. For the benefit of extensions that 
do not define
-                               // the 'radio-id' key.
-                               $radioId = 'wpSourceType' . 
$this->mParams['upload-type'];
-                       }
-
-                       $attribs = [
-                               'name' => 'wpSourceType',
-                               'type' => 'radio',
-                               'id' => $radioId,
-                               'value' => $this->mParams['upload-type'],
-                       ];
-
-                       if ( !empty( $this->mParams['checked'] ) ) {
-                               $attribs['checked'] = 'checked';
-                       }
-
-                       $label .= Html::element( 'input', $attribs );
-               }
-
-               return Html::rawElement( 'td', [ 'class' => 'mw-label' ] + 
$cellAttributes, $label );
-       }
-
-       /**
-        * @return int
-        */
-       function getSize() {
-               return isset( $this->mParams['size'] )
-                       ? $this->mParams['size']
-                       : 60;
-       }
-}
diff --git a/includes/specials/formfields/EditWatchlistCheckboxSeriesField.php 
b/includes/specials/formfields/EditWatchlistCheckboxSeriesField.php
new file mode 100644
index 0000000..cb93bb2
--- /dev/null
+++ b/includes/specials/formfields/EditWatchlistCheckboxSeriesField.php
@@ -0,0 +1,37 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+class EditWatchlistCheckboxSeriesField extends HTMLMultiSelectField {
+       /**
+        * HTMLMultiSelectField throws validation errors if we get input data
+        * that doesn't match the data set in the form setup. This causes
+        * problems if something gets removed from the watchlist while the
+        * form is open (T34126), but we know that invalid items will
+        * be harmless so we can override it here.
+        *
+        * @param string $value The value the field was submitted with
+        * @param array $alldata The data collected from the form
+        * @return bool|string Bool true on success, or String error to display.
+        */
+       function validate( $value, $alldata ) {
+               // Need to call into grandparent to be a good citizen. :)
+               return HTMLFormField::validate( $value, $alldata );
+       }
+}
diff --git a/includes/Licenses.php b/includes/specials/formfields/Licenses.php
similarity index 91%
rename from includes/Licenses.php
rename to includes/specials/formfields/Licenses.php
index 6467777..f499cc1 100644
--- a/includes/Licenses.php
+++ b/includes/specials/formfields/Licenses.php
@@ -187,24 +187,3 @@
                return Html::rawElement( 'select', $attribs, $this->html );
        }
 }
-
-/**
- * A License class for use on Special:Upload (represents a single type of 
license).
- */
-class License {
-       /** @var string */
-       public $template;
-
-       /** @var string */
-       public $text;
-
-       /**
-        * @param string $str License name??
-        */
-       function __construct( $str ) {
-               list( $text, $template ) = explode( '|', strrev( $str ), 2 );
-
-               $this->template = strrev( $template );
-               $this->text = strrev( $text );
-       }
-}
diff --git a/includes/specials/formfields/UploadSourceField.php 
b/includes/specials/formfields/UploadSourceField.php
new file mode 100644
index 0000000..251a286
--- /dev/null
+++ b/includes/specials/formfields/UploadSourceField.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * A form field that contains a radio box in the label
+ */
+class UploadSourceField extends HTMLTextField {
+
+       /**
+        * @param array $cellAttributes
+        * @return string
+        */
+       function getLabelHtml( $cellAttributes = [] ) {
+               $id = $this->mParams['id'];
+               $label = Html::rawElement( 'label', [ 'for' => $id ], 
$this->mLabel );
+
+               if ( !empty( $this->mParams['radio'] ) ) {
+                       if ( isset( $this->mParams['radio-id'] ) ) {
+                               $radioId = $this->mParams['radio-id'];
+                       } else {
+                               // Old way. For the benefit of extensions that 
do not define
+                               // the 'radio-id' key.
+                               $radioId = 'wpSourceType' . 
$this->mParams['upload-type'];
+                       }
+
+                       $attribs = [
+                               'name' => 'wpSourceType',
+                               'type' => 'radio',
+                               'id' => $radioId,
+                               'value' => $this->mParams['upload-type'],
+                       ];
+
+                       if ( !empty( $this->mParams['checked'] ) ) {
+                               $attribs['checked'] = 'checked';
+                       }
+
+                       $label .= Html::element( 'input', $attribs );
+               }
+
+               return Html::rawElement( 'td', [ 'class' => 'mw-label' ] + 
$cellAttributes, $label );
+       }
+
+       /**
+        * @return int
+        */
+       function getSize() {
+               return isset( $this->mParams['size'] )
+                       ? $this->mParams['size']
+                       : 60;
+       }
+}
diff --git a/includes/specials/forms/EditWatchlistNormalHTMLForm.php 
b/includes/specials/forms/EditWatchlistNormalHTMLForm.php
new file mode 100644
index 0000000..723093a
--- /dev/null
+++ b/includes/specials/forms/EditWatchlistNormalHTMLForm.php
@@ -0,0 +1,36 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Extend HTMLForm purely so we can have a more sane way of getting the 
section headers
+ */
+class EditWatchlistNormalHTMLForm extends HTMLForm {
+       public function getLegend( $namespace ) {
+               $namespace = substr( $namespace, 2 );
+
+               return $namespace == NS_MAIN
+                       ? $this->msg( 'blanknamespace' )->escaped()
+                       : htmlspecialchars( 
$this->getContext()->getLanguage()->getFormattedNsText( $namespace ) );
+       }
+
+       public function getBody() {
+               return $this->displaySection( $this->mFieldTree, '', 
'editwatchlist-' );
+       }
+}
diff --git a/includes/specials/forms/PreferencesForm.php 
b/includes/specials/forms/PreferencesForm.php
new file mode 100644
index 0000000..591625d
--- /dev/null
+++ b/includes/specials/forms/PreferencesForm.php
@@ -0,0 +1,141 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Form to edit user preferences.
+ */
+class PreferencesForm extends HTMLForm {
+       // Override default value from HTMLForm
+       protected $mSubSectionBeforeFields = false;
+
+       private $modifiedUser;
+
+       /**
+        * @param User $user
+        */
+       public function setModifiedUser( $user ) {
+               $this->modifiedUser = $user;
+       }
+
+       /**
+        * @return User
+        */
+       public function getModifiedUser() {
+               if ( $this->modifiedUser === null ) {
+                       return $this->getUser();
+               } else {
+                       return $this->modifiedUser;
+               }
+       }
+
+       /**
+        * Get extra parameters for the query string when redirecting after
+        * successful save.
+        *
+        * @return array
+        */
+       public function getExtraSuccessRedirectParameters() {
+               return [];
+       }
+
+       /**
+        * @param string $html
+        * @return string
+        */
+       function wrapForm( $html ) {
+               $html = Xml::tags( 'div', [ 'id' => 'preferences' ], $html );
+
+               return parent::wrapForm( $html );
+       }
+
+       /**
+        * @return string
+        */
+       function getButtons() {
+               $attrs = [ 'id' => 'mw-prefs-restoreprefs' ];
+
+               if ( !$this->getModifiedUser()->isAllowedAny( 
'editmyprivateinfo', 'editmyoptions' ) ) {
+                       return '';
+               }
+
+               $html = parent::getButtons();
+
+               if ( $this->getModifiedUser()->isAllowed( 'editmyoptions' ) ) {
+                       $t = $this->getTitle()->getSubpage( 'reset' );
+
+                       $linkRenderer = 
MediaWikiServices::getInstance()->getLinkRenderer();
+                       $html .= "\n" . $linkRenderer->makeLink( $t, 
$this->msg( 'restoreprefs' )->text(),
+                               Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' 
] ) );
+
+                       $html = Xml::tags( 'div', [ 'class' => 
'mw-prefs-buttons' ], $html );
+               }
+
+               return $html;
+       }
+
+       /**
+        * Separate multi-option preferences into multiple preferences, since we
+        * have to store them separately
+        * @param array $data
+        * @return array
+        */
+       function filterDataForSubmit( $data ) {
+               foreach ( $this->mFlatFields as $fieldname => $field ) {
+                       if ( $field instanceof HTMLNestedFilterable ) {
+                               $info = $field->mParams;
+                               $prefix = isset( $info['prefix'] ) ? 
$info['prefix'] : $fieldname;
+                               foreach ( $field->filterDataForSubmit( 
$data[$fieldname] ) as $key => $value ) {
+                                       $data["$prefix$key"] = $value;
+                               }
+                               unset( $data[$fieldname] );
+                       }
+               }
+
+               return $data;
+       }
+
+       /**
+        * Get the whole body of the form.
+        * @return string
+        */
+       function getBody() {
+               return $this->displaySection( $this->mFieldTree, '', 
'mw-prefsection-' );
+       }
+
+       /**
+        * Get the "<legend>" for a given section key. Normally this is the
+        * prefs-$key message but we'll allow extensions to override it.
+        * @param string $key
+        * @return string
+        */
+       function getLegend( $key ) {
+               $legend = parent::getLegend( $key );
+               Hooks::run( 'PreferencesGetLegend', [ $this, $key, &$legend ] );
+               return $legend;
+       }
+
+       /**
+        * Get the keys of each top level preference section.
+        * @return array of section keys
+        */
+       function getPreferenceSections() {
+               return array_keys( array_filter( $this->mFieldTree, 'is_array' 
) );
+       }
+}
diff --git a/includes/specials/forms/UploadForm.php 
b/includes/specials/forms/UploadForm.php
new file mode 100644
index 0000000..07290b2
--- /dev/null
+++ b/includes/specials/forms/UploadForm.php
@@ -0,0 +1,443 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Sub class of HTMLForm that provides the form section of SpecialUpload
+ */
+class UploadForm extends HTMLForm {
+       protected $mWatch;
+       protected $mForReUpload;
+       protected $mSessionKey;
+       protected $mHideIgnoreWarning;
+       protected $mDestWarningAck;
+       protected $mDestFile;
+
+       protected $mComment;
+       protected $mTextTop;
+       protected $mTextAfterSummary;
+
+       protected $mSourceIds;
+
+       protected $mMaxFileSize = [];
+
+       protected $mMaxUploadSize = [];
+
+       public function __construct( array $options = [], IContextSource 
$context = null,
+               LinkRenderer $linkRenderer = null
+       ) {
+               if ( $context instanceof IContextSource ) {
+                       $this->setContext( $context );
+               }
+
+               if ( !$linkRenderer ) {
+                       $linkRenderer = 
MediaWikiServices::getInstance()->getLinkRenderer();
+               }
+
+               $this->mWatch = !empty( $options['watch'] );
+               $this->mForReUpload = !empty( $options['forreupload'] );
+               $this->mSessionKey = isset( $options['sessionkey'] ) ? 
$options['sessionkey'] : '';
+               $this->mHideIgnoreWarning = !empty( 
$options['hideignorewarning'] );
+               $this->mDestWarningAck = !empty( $options['destwarningack'] );
+               $this->mDestFile = isset( $options['destfile'] ) ? 
$options['destfile'] : '';
+
+               $this->mComment = isset( $options['description'] ) ?
+                       $options['description'] : '';
+
+               $this->mTextTop = isset( $options['texttop'] )
+                       ? $options['texttop'] : '';
+
+               $this->mTextAfterSummary = isset( $options['textaftersummary'] )
+                       ? $options['textaftersummary'] : '';
+
+               $sourceDescriptor = $this->getSourceSection();
+               $descriptor = $sourceDescriptor
+                       + $this->getDescriptionSection()
+                       + $this->getOptionsSection();
+
+               Hooks::run( 'UploadFormInitDescriptor', [ &$descriptor ] );
+               parent::__construct( $descriptor, $context, 'upload' );
+
+               # Add a link to edit MediaWiki:Licenses
+               if ( $this->getUser()->isAllowed( 'editinterface' ) ) {
+                       $this->getOutput()->addModuleStyles( 
'mediawiki.special.upload.styles' );
+                       $licensesLink = $linkRenderer->makeKnownLink(
+                               $this->msg( 'licenses' 
)->inContentLanguage()->getTitle(),
+                               $this->msg( 'licenses-edit' )->text(),
+                               [],
+                               [ 'action' => 'edit' ]
+                       );
+                       $editLicenses = '<p class="mw-upload-editlicenses">' . 
$licensesLink . '</p>';
+                       $this->addFooterText( $editLicenses, 'description' );
+               }
+
+               # Set some form properties
+               $this->setSubmitText( $this->msg( 'uploadbtn' )->text() );
+               $this->setSubmitName( 'wpUpload' );
+               # Used message keys: 'accesskey-upload', 'tooltip-upload'
+               $this->setSubmitTooltip( 'upload' );
+               $this->setId( 'mw-upload-form' );
+
+               # Build a list of IDs for javascript insertion
+               $this->mSourceIds = [];
+               foreach ( $sourceDescriptor as $field ) {
+                       if ( !empty( $field['id'] ) ) {
+                               $this->mSourceIds[] = $field['id'];
+                       }
+               }
+       }
+
+       /**
+        * Get the descriptor of the fieldset that contains the file source
+        * selection. The section is 'source'
+        *
+        * @return array Descriptor array
+        */
+       protected function getSourceSection() {
+               if ( $this->mSessionKey ) {
+                       return [
+                               'SessionKey' => [
+                                       'type' => 'hidden',
+                                       'default' => $this->mSessionKey,
+                               ],
+                               'SourceType' => [
+                                       'type' => 'hidden',
+                                       'default' => 'Stash',
+                               ],
+                       ];
+               }
+
+               $canUploadByUrl = UploadFromUrl::isEnabled()
+                       && ( UploadFromUrl::isAllowed( $this->getUser() ) === 
true )
+                       && $this->getConfig()->get( 
'CopyUploadsFromSpecialUpload' );
+               $radio = $canUploadByUrl;
+               $selectedSourceType = strtolower( $this->getRequest()->getText( 
'wpSourceType', 'File' ) );
+
+               $descriptor = [];
+               if ( $this->mTextTop ) {
+                       $descriptor['UploadFormTextTop'] = [
+                               'type' => 'info',
+                               'section' => 'source',
+                               'default' => $this->mTextTop,
+                               'raw' => true,
+                       ];
+               }
+
+               $this->mMaxUploadSize['file'] = min(
+                       UploadBase::getMaxUploadSize( 'file' ),
+                       UploadBase::getMaxPhpUploadSize()
+               );
+
+               $help = $this->msg( 'upload-maxfilesize',
+                               $this->getContext()->getLanguage()->formatSize( 
$this->mMaxUploadSize['file'] )
+                       )->parse();
+
+               // If the user can also upload by URL, there are 2 different 
file size limits.
+               // This extra message helps stress which limit corresponds to 
what.
+               if ( $canUploadByUrl ) {
+                       $help .= $this->msg( 'word-separator' )->escaped();
+                       $help .= $this->msg( 'upload_source_file' )->parse();
+               }
+
+               $descriptor['UploadFile'] = [
+                       'class' => 'UploadSourceField',
+                       'section' => 'source',
+                       'type' => 'file',
+                       'id' => 'wpUploadFile',
+                       'radio-id' => 'wpSourceTypeFile',
+                       'label-message' => 'sourcefilename',
+                       'upload-type' => 'File',
+                       'radio' => &$radio,
+                       'help' => $help,
+                       'checked' => $selectedSourceType == 'file',
+               ];
+
+               if ( $canUploadByUrl ) {
+                       $this->mMaxUploadSize['url'] = 
UploadBase::getMaxUploadSize( 'url' );
+                       $descriptor['UploadFileURL'] = [
+                               'class' => 'UploadSourceField',
+                               'section' => 'source',
+                               'id' => 'wpUploadFileURL',
+                               'radio-id' => 'wpSourceTypeurl',
+                               'label-message' => 'sourceurl',
+                               'upload-type' => 'url',
+                               'radio' => &$radio,
+                               'help' => $this->msg( 'upload-maxfilesize',
+                                       
$this->getContext()->getLanguage()->formatSize( $this->mMaxUploadSize['url'] )
+                               )->parse() .
+                                       $this->msg( 'word-separator' 
)->escaped() .
+                                       $this->msg( 'upload_source_url' 
)->parse(),
+                               'checked' => $selectedSourceType == 'url',
+                       ];
+               }
+               Hooks::run( 'UploadFormSourceDescriptors', [ &$descriptor, 
&$radio, $selectedSourceType ] );
+
+               $descriptor['Extensions'] = [
+                       'type' => 'info',
+                       'section' => 'source',
+                       'default' => $this->getExtensionsMessage(),
+                       'raw' => true,
+               ];
+
+               return $descriptor;
+       }
+
+       /**
+        * Get the messages indicating which extensions are preferred and 
prohibitted.
+        *
+        * @return string HTML string containing the message
+        */
+       protected function getExtensionsMessage() {
+               # Print a list of allowed file extensions, if so configured.  
We ignore
+               # MIME type here, it's incomprehensible to most people and too 
long.
+               $config = $this->getConfig();
+
+               if ( $config->get( 'CheckFileExtensions' ) ) {
+                       $fileExtensions = array_unique( $config->get( 
'FileExtensions' ) );
+                       if ( $config->get( 'StrictFileExtensions' ) ) {
+                               # Everything not permitted is banned
+                               $extensionsList =
+                                       '<div id="mw-upload-permitted">' .
+                                       $this->msg( 'upload-permitted' )
+                                               ->params( 
$this->getLanguage()->commaList( $fileExtensions ) )
+                                               ->numParams( count( 
$fileExtensions ) )
+                                               ->parseAsBlock() .
+                                       "</div>\n";
+                       } else {
+                               # We have to list both preferred and prohibited
+                               $fileBlacklist = array_unique( $config->get( 
'FileBlacklist' ) );
+                               $extensionsList =
+                                       '<div id="mw-upload-preferred">' .
+                                               $this->msg( 'upload-preferred' )
+                                                       ->params( 
$this->getLanguage()->commaList( $fileExtensions ) )
+                                                       ->numParams( count( 
$fileExtensions ) )
+                                                       ->parseAsBlock() .
+                                       "</div>\n" .
+                                       '<div id="mw-upload-prohibited">' .
+                                               $this->msg( 'upload-prohibited' 
)
+                                                       ->params( 
$this->getLanguage()->commaList( $fileBlacklist ) )
+                                                       ->numParams( count( 
$fileBlacklist ) )
+                                                       ->parseAsBlock() .
+                                       "</div>\n";
+                       }
+               } else {
+                       # Everything is permitted.
+                       $extensionsList = '';
+               }
+
+               return $extensionsList;
+       }
+
+       /**
+        * Get the descriptor of the fieldset that contains the file description
+        * input. The section is 'description'
+        *
+        * @return array Descriptor array
+        */
+       protected function getDescriptionSection() {
+               $config = $this->getConfig();
+               if ( $this->mSessionKey ) {
+                       $stash = 
RepoGroup::singleton()->getLocalRepo()->getUploadStash( $this->getUser() );
+                       try {
+                               $file = $stash->getFile( $this->mSessionKey );
+                       } catch ( Exception $e ) {
+                               $file = null;
+                       }
+                       if ( $file ) {
+                               global $wgContLang;
+
+                               $mto = $file->transform( [ 'width' => 120 ] );
+                               if ( $mto ) {
+                                       $this->addHeaderText(
+                                               '<div class="thumb t' . 
$wgContLang->alignEnd() . '">' .
+                                               Html::element( 'img', [
+                                                       'src' => $mto->getUrl(),
+                                                       'class' => 'thumbimage',
+                                               ] ) . '</div>', 'description' );
+                               }
+                       }
+               }
+
+               $descriptor = [
+                       'DestFile' => [
+                               'type' => 'text',
+                               'section' => 'description',
+                               'id' => 'wpDestFile',
+                               'label-message' => 'destfilename',
+                               'size' => 60,
+                               'default' => $this->mDestFile,
+                               # @todo FIXME: Hack to work around poor 
handling of the 'default' option in HTMLForm
+                               'nodata' => strval( $this->mDestFile ) !== '',
+                       ],
+                       'UploadDescription' => [
+                               'type' => 'textarea',
+                               'section' => 'description',
+                               'id' => 'wpUploadDescription',
+                               'label-message' => $this->mForReUpload
+                                       ? 'filereuploadsummary'
+                                       : 'fileuploadsummary',
+                               'default' => $this->mComment,
+                               'cols' => 80,
+                               'rows' => 8,
+                       ]
+               ];
+               if ( $this->mTextAfterSummary ) {
+                       $descriptor['UploadFormTextAfterSummary'] = [
+                               'type' => 'info',
+                               'section' => 'description',
+                               'default' => $this->mTextAfterSummary,
+                               'raw' => true,
+                       ];
+               }
+
+               $descriptor += [
+                       'EditTools' => [
+                               'type' => 'edittools',
+                               'section' => 'description',
+                               'message' => 'edittools-upload',
+                       ]
+               ];
+
+               if ( $this->mForReUpload ) {
+                       $descriptor['DestFile']['readonly'] = true;
+               } else {
+                       $descriptor['License'] = [
+                               'type' => 'select',
+                               'class' => 'Licenses',
+                               'section' => 'description',
+                               'id' => 'wpLicense',
+                               'label-message' => 'license',
+                       ];
+               }
+
+               if ( $config->get( 'UseCopyrightUpload' ) ) {
+                       $descriptor['UploadCopyStatus'] = [
+                               'type' => 'text',
+                               'section' => 'description',
+                               'id' => 'wpUploadCopyStatus',
+                               'label-message' => 'filestatus',
+                       ];
+                       $descriptor['UploadSource'] = [
+                               'type' => 'text',
+                               'section' => 'description',
+                               'id' => 'wpUploadSource',
+                               'label-message' => 'filesource',
+                       ];
+               }
+
+               return $descriptor;
+       }
+
+       /**
+        * Get the descriptor of the fieldset that contains the upload options,
+        * such as "watch this file". The section is 'options'
+        *
+        * @return array Descriptor array
+        */
+       protected function getOptionsSection() {
+               $user = $this->getUser();
+               if ( $user->isLoggedIn() ) {
+                       $descriptor = [
+                               'Watchthis' => [
+                                       'type' => 'check',
+                                       'id' => 'wpWatchthis',
+                                       'label-message' => 'watchthisupload',
+                                       'section' => 'options',
+                                       'default' => $this->mWatch,
+                               ]
+                       ];
+               }
+               if ( !$this->mHideIgnoreWarning ) {
+                       $descriptor['IgnoreWarning'] = [
+                               'type' => 'check',
+                               'id' => 'wpIgnoreWarning',
+                               'label-message' => 'ignorewarnings',
+                               'section' => 'options',
+                       ];
+               }
+
+               $descriptor['DestFileWarningAck'] = [
+                       'type' => 'hidden',
+                       'id' => 'wpDestFileWarningAck',
+                       'default' => $this->mDestWarningAck ? '1' : '',
+               ];
+
+               if ( $this->mForReUpload ) {
+                       $descriptor['ForReUpload'] = [
+                               'type' => 'hidden',
+                               'id' => 'wpForReUpload',
+                               'default' => '1',
+                       ];
+               }
+
+               return $descriptor;
+       }
+
+       /**
+        * Add the upload JS and show the form.
+        */
+       public function show() {
+               $this->addUploadJS();
+               parent::show();
+       }
+
+       /**
+        * Add upload JS to the OutputPage
+        */
+       protected function addUploadJS() {
+               $config = $this->getConfig();
+
+               $useAjaxDestCheck = $config->get( 'UseAjax' ) && $config->get( 
'AjaxUploadDestCheck' );
+               $useAjaxLicensePreview = $config->get( 'UseAjax' ) &&
+                       $config->get( 'AjaxLicensePreview' ) && $config->get( 
'EnableAPI' );
+               $this->mMaxUploadSize['*'] = UploadBase::getMaxUploadSize();
+
+               $scriptVars = [
+                       'wgAjaxUploadDestCheck' => $useAjaxDestCheck,
+                       'wgAjaxLicensePreview' => $useAjaxLicensePreview,
+                       'wgUploadAutoFill' => !$this->mForReUpload &&
+                               // If we received mDestFile from the request, 
don't autofill
+                               // the wpDestFile textbox
+                               $this->mDestFile === '',
+                       'wgUploadSourceIds' => $this->mSourceIds,
+                       'wgCheckFileExtensions' => $config->get( 
'CheckFileExtensions' ),
+                       'wgStrictFileExtensions' => $config->get( 
'StrictFileExtensions' ),
+                       'wgFileExtensions' => array_values( array_unique( 
$config->get( 'FileExtensions' ) ) ),
+                       'wgCapitalizeUploads' => MWNamespace::isCapitalized( 
NS_FILE ),
+                       'wgMaxUploadSize' => $this->mMaxUploadSize,
+                       'wgFileCanRotate' => SpecialUpload::rotationEnabled(),
+               ];
+
+               $out = $this->getOutput();
+               $out->addJsConfigVars( $scriptVars );
+
+               $out->addModules( [
+                       'mediawiki.special.upload', // Extras for thumbnail and 
license preview.
+               ] );
+       }
+
+       /**
+        * Empty function; submission is handled elsewhere.
+        *
+        * @return bool False
+        */
+       function trySubmit() {
+               return false;
+       }
+}
diff --git a/includes/specials/helpers/ImportReporter.php 
b/includes/specials/helpers/ImportReporter.php
new file mode 100644
index 0000000..325c6732
--- /dev/null
+++ b/includes/specials/helpers/ImportReporter.php
@@ -0,0 +1,188 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Reporting callback
+ * @ingroup SpecialPage
+ */
+class ImportReporter extends ContextSource {
+       private $reason = false;
+       private $logTags = [];
+       private $mOriginalLogCallback = null;
+       private $mOriginalPageOutCallback = null;
+       private $mLogItemCount = 0;
+
+       /**
+        * @param WikiImporter $importer
+        * @param bool $upload
+        * @param string $interwiki
+        * @param string|bool $reason
+        */
+       function __construct( $importer, $upload, $interwiki, $reason = false ) 
{
+               $this->mOriginalPageOutCallback =
+                       $importer->setPageOutCallback( [ $this, 'reportPage' ] 
);
+               $this->mOriginalLogCallback =
+                       $importer->setLogItemCallback( [ $this, 'reportLogItem' 
] );
+               $importer->setNoticeCallback( [ $this, 'reportNotice' ] );
+               $this->mPageCount = 0;
+               $this->mIsUpload = $upload;
+               $this->mInterwiki = $interwiki;
+               $this->reason = $reason;
+       }
+
+       /**
+        * Sets change tags to apply to the import log entry and null revision.
+        *
+        * @param array $tags
+        * @since 1.29
+        */
+       public function setChangeTags( array $tags ) {
+               $this->logTags = $tags;
+       }
+
+       function open() {
+               $this->getOutput()->addHTML( "<ul>\n" );
+       }
+
+       function reportNotice( $msg, array $params ) {
+               $this->getOutput()->addHTML(
+                       Html::element( 'li', [], $this->msg( $msg, $params 
)->text() )
+               );
+       }
+
+       function reportLogItem( /* ... */ ) {
+               $this->mLogItemCount++;
+               if ( is_callable( $this->mOriginalLogCallback ) ) {
+                       call_user_func_array( $this->mOriginalLogCallback, 
func_get_args() );
+               }
+       }
+
+       /**
+        * @param Title $title
+        * @param ForeignTitle $foreignTitle
+        * @param int $revisionCount
+        * @param int $successCount
+        * @param array $pageInfo
+        * @return void
+        */
+       public function reportPage( $title, $foreignTitle, $revisionCount,
+                       $successCount, $pageInfo ) {
+               $args = func_get_args();
+               call_user_func_array( $this->mOriginalPageOutCallback, $args );
+
+               if ( $title === null ) {
+                       # Invalid or non-importable title; a notice is already 
displayed
+                       return;
+               }
+
+               $this->mPageCount++;
+               $linkRenderer = 
MediaWikiServices::getInstance()->getLinkRenderer();
+               if ( $successCount > 0 ) {
+                       // <bdi> prevents jumbling of the versions count
+                       // in RTL wikis in case the page title is LTR
+                       $this->getOutput()->addHTML(
+                               "<li>" . $linkRenderer->makeLink( $title ) . " 
" .
+                                       "<bdi>" .
+                                       $this->msg( 'import-revision-count' 
)->numParams( $successCount )->escaped() .
+                                       "</bdi>" .
+                                       "</li>\n"
+                       );
+
+                       $logParams = [ '4:number:count' => $successCount ];
+                       if ( $this->mIsUpload ) {
+                               $detail = $this->msg( 
'import-logentry-upload-detail' )->numParams(
+                                       $successCount 
)->inContentLanguage()->text();
+                               $action = 'upload';
+                       } else {
+                               $pageTitle = $foreignTitle->getFullText();
+                               $fullInterwikiPrefix = $this->mInterwiki;
+                               Hooks::run( 'ImportLogInterwikiLink', [ 
&$fullInterwikiPrefix, &$pageTitle ] );
+
+                               $interwikiTitleStr = $fullInterwikiPrefix . ':' 
. $pageTitle;
+                               $interwiki = '[[:' . $interwikiTitleStr . ']]';
+                               $detail = $this->msg( 
'import-logentry-interwiki-detail' )->numParams(
+                                       $successCount )->params( $interwiki 
)->inContentLanguage()->text();
+                               $action = 'interwiki';
+                               $logParams['5:title-link:interwiki'] = 
$interwikiTitleStr;
+                       }
+                       if ( $this->reason ) {
+                               $detail .= $this->msg( 'colon-separator' 
)->inContentLanguage()->text()
+                                       . $this->reason;
+                       }
+
+                       $comment = $detail; // quick
+                       $dbw = wfGetDB( DB_MASTER );
+                       $latest = $title->getLatestRevID();
+                       $nullRevision = Revision::newNullRevision(
+                               $dbw,
+                               $title->getArticleID(),
+                               $comment,
+                               true,
+                               $this->getUser()
+                       );
+
+                       $nullRevId = null;
+                       if ( !is_null( $nullRevision ) ) {
+                               $nullRevId = $nullRevision->insertOn( $dbw );
+                               $page = WikiPage::factory( $title );
+                               # Update page record
+                               $page->updateRevisionOn( $dbw, $nullRevision );
+                               Hooks::run(
+                                       'NewRevisionFromEditComplete',
+                                       [ $page, $nullRevision, $latest, 
$this->getUser() ]
+                               );
+                       }
+
+                       // Create the import log entry
+                       $logEntry = new ManualLogEntry( 'import', $action );
+                       $logEntry->setTarget( $title );
+                       $logEntry->setComment( $this->reason );
+                       $logEntry->setPerformer( $this->getUser() );
+                       $logEntry->setParameters( $logParams );
+                       $logid = $logEntry->insert();
+                       if ( count( $this->logTags ) ) {
+                               $logEntry->setTags( $this->logTags );
+                       }
+                       // Make sure the null revision will be tagged as well
+                       $logEntry->setAssociatedRevId( $nullRevId );
+
+                       $logEntry->publish( $logid );
+
+               } else {
+                       $this->getOutput()->addHTML( "<li>" . 
$linkRenderer->makeKnownLink( $title ) . " " .
+                               $this->msg( 'import-nonewrevisions' 
)->escaped() . "</li>\n" );
+               }
+       }
+
+       function close() {
+               $out = $this->getOutput();
+               if ( $this->mLogItemCount > 0 ) {
+                       $msg = $this->msg( 'imported-log-entries' )->numParams( 
$this->mLogItemCount )->parse();
+                       $out->addHTML( Xml::tags( 'li', null, $msg ) );
+               } elseif ( $this->mPageCount == 0 && $this->mLogItemCount == 0 
) {
+                       $out->addHTML( "</ul>\n" );
+
+                       return Status::newFatal( 'importnopages' );
+               }
+               $out->addHTML( "</ul>\n" );
+
+               return Status::newGood( $this->mPageCount );
+       }
+}
diff --git a/includes/specials/helpers/License.php 
b/includes/specials/helpers/License.php
new file mode 100644
index 0000000..4f94b4d
--- /dev/null
+++ b/includes/specials/helpers/License.php
@@ -0,0 +1,46 @@
+<?php
+/**
+ * License selector for use on Special:Upload.
+ *
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ * @author Ævar Arnfjörð Bjarmason <ava...@gmail.com>
+ * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 
2.0 or later
+ */
+
+/**
+ * A License class for use on Special:Upload (represents a single type of 
license).
+ */
+class License {
+       /** @var string */
+       public $template;
+
+       /** @var string */
+       public $text;
+
+       /**
+        * @param string $str License name??
+        */
+       function __construct( $str ) {
+               list( $text, $template ) = explode( '|', strrev( $str ), 2 );
+
+               $this->template = strrev( $template );
+               $this->text = strrev( $text );
+       }
+}

-- 
To view, visit https://gerrit.wikimedia.org/r/377652
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I58abcbb44dbf9bf1762b4252555f7552bfa7c253
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: Bartosz Dziewoński <matma....@gmail.com>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to