possible for fallback images such as png.
* (T44246) $wgFilterLogTypes will no longer ignore 'patrol' when user does
not have the right to mark things patrolled.
+* Wikis that contain imported revisions or CentralAuth global blocks should run
+ maintenance/cleanupUsersWithNoId.php.
=== New features in 1.31 ===
* Wikimedia\Rdbms\IDatabase->select() and similar methods now support
* As a first pass in standardizing dialog boxes across the MediaWiki product,
Html class now provides helper methods for messageBox, successBox, errorBox and
warningBox generation.
+* (T9240) Imports will now record unknown (and, optionally, known) usernames in
+ a format like "iw>Example".
+* (T20209) Linker (used on history pages, log pages, and so on) will display
+ usernames formed like "iw>Example" as interwiki links, as if by wikitext like
+ [[iw:User:Example|iw>Example]].
+* (T111605) The 'ImportHandleUnknownUser' hook allows extensions to auto-create
+ users during an import.
=== External library changes in 1.31 ===
* Passing a ParserOptions object to OutputPage::parserOptions() is deprecated.
* Browser support for Opera 12 and older was removed.
Opera 15+ continues at Grade A support.
-* …
+* The Block class will no longer accept usable-but-missing usernames for
+ 'byText' or ->setBlocker(). Callers should either ensure the blocker exists
+ locally or use a new interwiki-format username like "iw>Example".
== Compatibility ==
MediaWiki 1.31 requires PHP 5.5.9 or later. There is experimental support for
'CleanupPreferences' => __DIR__ . '/maintenance/cleanupPreferences.php',
'CleanupRemovedModules' => __DIR__ . '/maintenance/cleanupRemovedModules.php',
'CleanupSpam' => __DIR__ . '/maintenance/cleanupSpam.php',
+ 'CleanupUsersWithNoId' => __DIR__ . '/maintenance/cleanupUsersWithNoId.php',
'ClearInterwikiCache' => __DIR__ . '/maintenance/clearInterwikiCache.php',
'ClearUserWatchlistJob' => __DIR__ . '/includes/jobqueue/jobs/ClearUserWatchlistJob.php',
'CliInstaller' => __DIR__ . '/includes/installer/CliInstaller.php',
Return false to stop further processing of the tag
$reader: XMLReader object
+'ImportHandleUnknownUser': When a user does exist locally, this hook is called
+to give extensions an opportunity to auto-create it. If the auto-creation is
+successful, return false.
+$name: User name
+
'ImportHandleUploadXMLTag': When parsing a XML tag in a file upload.
Return false to stop further processing of the tag
$reader: XMLReader object
/**
* Set the user who implemented (or will implement) this block
- * @param User|string $user Local User object or username string for foreign users
+ * @param User|string $user Local User object or username string
*/
public function setBlocker( $user ) {
+ if ( is_string( $user ) ) {
+ $user = User::newFromName( $user, false );
+ }
+
+ if ( $user->isAnon() && User::isUsableName( $user->getName() ) ) {
+ throw new InvalidArgumentException(
+ 'Blocker must be a local user or a name that cannot be a local user'
+ );
+ }
+
$this->blocker = $user;
}
'msg:double-redirect-fixer', // Automatic double redirect fix
'msg:usermessage-editor', // Default user for leaving user messages
'msg:proxyblocker', // For $wgProxyList and Special:Blockme (removed in 1.22)
+ 'msg:sorbs', // For $wgEnableDnsBlacklist etc.
'msg:spambot_username', // Used by cleanupSpam.php
'msg:autochange-username', // Used by anon category RC entries (parser functions, Lua & purges)
];
*/
public static function userLink( $userId, $userName, $altUserName = false ) {
$classes = 'mw-userlink';
+ $page = null;
if ( $userId == 0 ) {
- $page = SpecialPage::getTitleFor( 'Contributions', $userName );
- if ( $altUserName === false ) {
- $altUserName = IP::prettifyIP( $userName );
+ $pos = strpos( $userName, '>' );
+ if ( $pos !== false ) {
+ $iw = explode( ':', substr( $userName, 0, $pos ) );
+ $firstIw = array_shift( $iw );
+ $interwikiLookup = MediaWikiServices::getInstance()->getInterwikiLookup();
+ if ( $interwikiLookup->isValidInterwiki( $firstIw ) ) {
+ $title = MWNamespace::getCanonicalName( NS_USER ) . ':' . substr( $userName, $pos + 1 );
+ if ( $iw ) {
+ $title = join( ':', $iw ) . ':' . $title;
+ }
+ $page = Title::makeTitle( NS_MAIN, $title, '', $firstIw );
+ }
+ $classes .= ' mw-extuserlink';
+ } else {
+ $page = SpecialPage::getTitleFor( 'Contributions', $userName );
+ if ( $altUserName === false ) {
+ $altUserName = IP::prettifyIP( $userName );
+ }
}
$classes .= ' mw-anonuserlink'; // Separate link class for anons (T45179)
} else {
}
// Wrap the output with <bdi> tags for directionality isolation
- return self::link(
- $page,
- '<bdi>' . htmlspecialchars( $altUserName !== false ? $altUserName : $userName ) . '</bdi>',
- [ 'class' => $classes ]
- );
+ $linkText =
+ '<bdi>' . htmlspecialchars( $altUserName !== false ? $altUserName : $userName ) . '</bdi>';
+
+ return $page
+ ? self::link( $page, $linkText, [ 'class' => $classes ] )
+ : Html::rawElement( 'span', [ 'class' => $classes ], $linkText );
}
/**
$blockable = !( $flags & self::TOOL_LINKS_NOBLOCK );
$addEmailLink = $flags & self::TOOL_LINKS_EMAIL && $userId;
+ if ( $userId == 0 && strpos( $userText, '>' ) !== false ) {
+ // No tools for an external user
+ return '';
+ }
+
$items = [];
if ( $talkable ) {
$items[] = self::userTalkLink( $userId, $userText );
$params['fullhistory'],
$params['templates']
);
+ $usernamePrefix = $params['interwikisource'];
} else {
$isUpload = true;
if ( !$user->isAllowed( 'importupload' ) ) {
$this->dieWithError( 'apierror-cantimport-upload' );
}
$source = ImportStreamSource::newFromUpload( 'xml' );
+ $usernamePrefix = (string)$params['interwikiprefix'];
+ if ( $usernamePrefix === '' ) {
+ $encParamName = $this->encodeParamName( 'interwikiprefix' );
+ $this->dieWithError( [ 'apierror-missingparam', $encParamName ] );
+ }
}
if ( !$source->isOK() ) {
$this->dieStatus( $source );
$this->dieStatus( $statusRootPage );
}
}
+ $importer->setUsernamePrefix( $usernamePrefix, $params['assignknownusers'] );
$reporter = new ApiImportReporter(
$importer,
$isUpload,
'xml' => [
ApiBase::PARAM_TYPE => 'upload',
],
+ 'interwikiprefix' => [
+ ApiBase::PARAM_TYPE => 'string',
+ ],
'interwikisource' => [
ApiBase::PARAM_TYPE => $this->getAllowedImportSources(),
],
'namespace' => [
ApiBase::PARAM_TYPE => 'namespace'
],
+ 'assignknownusers' => false,
'rootpage' => null,
'tags' => [
ApiBase::PARAM_TYPE => 'tags',
"apihelp-import-extended-description": "Note that the HTTP POST must be done as a file upload (i.e. using multipart/form-data) when sending a file for the <var>xml</var> parameter.",
"apihelp-import-param-summary": "Log entry import summary.",
"apihelp-import-param-xml": "Uploaded XML file.",
+ "apihelp-import-param-interwikiprefix": "For uploaded imports: interwiki prefix to apply to unknown user names (and known users if <var>$1assignknownusers</var> is set).",
+ "apihelp-import-param-assignknownusers": "Assign edits to local users where the named user exists locally.",
"apihelp-import-param-interwikisource": "For interwiki imports: wiki to import from.",
"apihelp-import-param-interwikipage": "For interwiki imports: page to import.",
"apihelp-import-param-fullhistory": "For interwiki imports: import the full history, not just the current version.",
"apihelp-import-extended-description": "{{doc-apihelp-extended-description|import}}",
"apihelp-import-param-summary": "{{doc-apihelp-param|import|summary|info=The parameter being documented here provides the summary used on the log messages about the import. The phrase \"Import summary\" here is grammatically equivalent to a phrase such as \"science book\", not \"eat food\".}}",
"apihelp-import-param-xml": "{{doc-apihelp-param|import|xml}}",
+ "apihelp-import-param-interwikiprefix": "{{doc-apihelp-param|import|interwikiprefix}}",
+ "apihelp-import-param-assignknownusers": "{{doc-apihelp-param|import|assignknownusers}}",
"apihelp-import-param-interwikisource": "{{doc-apihelp-param|import|interwikisource}}",
"apihelp-import-param-interwikipage": "{{doc-apihelp-param|import|interwikipage}}",
"apihelp-import-param-fullhistory": "{{doc-apihelp-param|import|fullhistory}}",
private $countableCache = [];
/** @var bool */
private $disableStatisticsUpdate = false;
+ private $usernamePrefix = 'imported';
+ private $assignKnownUsers = false;
+ private $triedCreations = [];
/**
* Creates an ImportXMLReader drawing from the source provided
$this->mImportUploads = $import;
}
+ /**
+ * @since 1.31
+ * @param string $usernamePrefix Prefix to apply to unknown (and possibly also known) usernames
+ * @param bool $assignKnownUsers Whether to apply the prefix to usernames that exist locally
+ */
+ public function setUsernamePrefix( $usernamePrefix, $assignKnownUsers ) {
+ $this->usernamePrefix = rtrim( (string)$usernamePrefix, ':>' );
+ $this->assignKnownUsers = (bool)$assignKnownUsers;
+ }
+
/**
* Statistics update can cause a lot of time
* @since 1.29
}
if ( !isset( $logInfo['contributor']['username'] ) ) {
- $revision->setUsername( 'Unknown user' );
+ $revision->setUsername( $this->usernamePrefix . '>Unknown user' );
} else {
- $revision->setUsername( $logInfo['contributor']['username'] );
+ $revision->setUsername( $this->prefixUsername( $logInfo['contributor']['username'] ) );
}
return $this->logItemCallback( $revision );
if ( isset( $revisionInfo['contributor']['ip'] ) ) {
$revision->setUserIP( $revisionInfo['contributor']['ip'] );
} elseif ( isset( $revisionInfo['contributor']['username'] ) ) {
- $revision->setUsername( $revisionInfo['contributor']['username'] );
+ $revision->setUsername( $this->prefixUsername( $revisionInfo['contributor']['username'] ) );
} else {
- $revision->setUsername( 'Unknown user' );
+ $revision->setUsername( $this->usernamePrefix . '>Unknown user' );
}
if ( isset( $revisionInfo['sha1'] ) ) {
$revision->setSha1Base36( $revisionInfo['sha1'] );
$revision->setUserIP( $uploadInfo['contributor']['ip'] );
}
if ( isset( $uploadInfo['contributor']['username'] ) ) {
- $revision->setUsername( $uploadInfo['contributor']['username'] );
+ $revision->setUsername( $this->prefixUsername( $uploadInfo['contributor']['username'] ) );
}
$revision->setNoUpdates( $this->mNoUpdates );
return call_user_func( $this->mUploadCallback, $revision );
}
+ /**
+ * Add an interwiki prefix to the username, if appropriate
+ * @since 1.31
+ * @param string $name Name being imported
+ * @return string Name, possibly with the prefix prepended.
+ */
+ protected function prefixUsername( $name ) {
+ if ( !User::isUsableName( $name ) ) {
+ return $name;
+ }
+
+ if ( $this->assignKnownUsers ) {
+ if ( User::idFromName( $name ) ) {
+ return $name;
+ }
+
+ // See if any extension wants to create it.
+ if ( !isset( $this->triedCreations[$name] ) ) {
+ $this->triedCreations[$name] = true;
+ if ( !Hooks::run( 'ImportHandleUnknownUser', [ $name ] ) &&
+ User::idFromName( $name, User::READ_LATEST )
+ ) {
+ return $name;
+ }
+ }
+ }
+
+ return substr( $this->usernamePrefix . '>' . $name, 0, 255 );
+ }
+
/**
* @return array
*/
private $includeTemplates = false;
private $pageLinkDepth;
private $importSources;
+ private $assignKnownUsers;
+ private $usernamePrefix;
public function __construct() {
parent::__construct( 'Import', 'import' );
$isUpload = false;
$request = $this->getRequest();
$this->sourceName = $request->getVal( "source" );
+ $this->assignKnownUsers = $request->getCheck( 'assignKnownUsers' );
$this->logcomment = $request->getText( 'log-comment' );
$this->pageLinkDepth = $this->getConfig()->get( 'ExportMaxLinkDepth' ) == 0
$source = Status::newFatal( 'import-token-mismatch' );
} elseif ( $this->sourceName === 'upload' ) {
$isUpload = true;
+ $this->usernamePrefix = $this->fullInterwikiPrefix = $request->getVal( 'usernamePrefix' );
if ( $user->isAllowed( 'importupload' ) ) {
$source = ImportStreamSource::newFromUpload( "xmlimport" );
} else {
$source = Status::newFatal( "importunknownsource" );
}
+ if ( (string)$this->fullInterwikiPrefix === '' ) {
+ $source->fatal( 'importnoprefix' );
+ }
+
$out = $this->getOutput();
if ( !$source->isGood() ) {
$out->addWikiText( "<p class=\"error\">\n" .
return;
}
}
+ $importer->setUsernamePrefix( $this->fullInterwikiPrefix, $this->assignKnownUsers );
$out->addWikiMsg( "importstart" );
Html::input( 'xmlimport', '', 'file', [ 'id' => 'xmlimport' ] ) . ' ' .
"</td>
</tr>
+ <tr>
+ <td class='mw-label'>" .
+ Xml::label( $this->msg( 'import-upload-username-prefix' )->text(),
+ 'mw-import-usernamePrefix' ) .
+ "</td>
+ <td class='mw-input'>" .
+ Xml::input( 'usernamePrefix', 50,
+ $this->usernamePrefix,
+ [ 'id' => 'usernamePrefix', 'type' => 'text' ] ) . ' ' .
+ "</td>
+ </tr>
+ <tr>
+ <td></td>
+ <td class='mw-input'>" .
+ Xml::checkLabel(
+ $this->msg( 'import-assign-known-users' )->text(),
+ 'assignKnownUsers',
+ 'assignKnownUsers',
+ $this->assignKnownUsers
+ ) .
+ "</td>
+ </tr>
<tr>
<td class='mw-label'>" .
Xml::label( $this->msg( 'import-comment' )->text(), 'mw-import-comment' ) .
) .
"</td>
</tr>
+ <tr>
+ <td></td>
+ <td class='mw-input'>" .
+ Xml::checkLabel(
+ $this->msg( 'import-assign-known-users' )->text(),
+ 'assignKnownUsers',
+ 'assignKnownUsers',
+ $this->assignKnownUsers
+ ) .
+ "</td>
+ </tr>
$importDepth
<tr>
<td class='mw-label'>" .
"import-mapping-namespace": "Import to a namespace:",
"import-mapping-subpage": "Import as subpages of the following page:",
"import-upload-filename": "Filename:",
+ "import-upload-username-prefix": "Interwiki prefix:",
+ "import-assign-known-users": "Assign edits to local users where the named user exists locally",
"import-comment": "Comment:",
"importtext": "Please export the file from the source wiki using the [[Special:Export|export utility]].\nSave it to your computer and upload it here.",
"importstart": "Importing pages...",
"imported-log-entries": "Imported $1 {{PLURAL:$1|log entry|log entries}}.",
"importfailed": "Import failed: <nowiki>$1</nowiki>",
"importunknownsource": "Unknown import source type",
+ "importnoprefix": "No interwiki prefix was supplied",
"importcantopen": "Could not open import file",
"importbadinterwiki": "Bad interwiki link",
"importsuccess": "Import finished!",
"import-mapping-namespace": "Used as label for the second of three radio buttons in Import form on [[Special:Import]]. The radio button is followed by a drop-down list from which the user can select a namespace.\n\nSee also:\n* {{msg-mw|Import-mapping-default}}\n* {{msg-mw|Import-mapping-subpage}}",
"import-mapping-subpage": "Used as label for the third of three radio buttons in Import form on [[Special:Import]]. The radio button is followed by a text box in which the user can type a page name. The imported pages will be created as subpages of the entered page name.\n\nSee also:\n* {{msg-mw|Import-mapping-default}}\n* {{msg-mw|Import-mapping-namespace}}",
"import-upload-filename": "Used on [[Special:Import]] as label for upload of an XML file containing the pages to import.\n{{Identical|Filename}}",
+ "import-upload-username-prefix": "Used as label for input box in [[Special:Import]].",
+ "import-assign-known-users": "Use as label for checkbox in [[Special:Import]].",
"import-comment": "Used as label for input box in [[Special:Import]].\n\nSee also:\n* {{msg-mw|Import-interwiki-history}}\n* {{msg-mw|Import-interwiki-templates}}\n* {{msg-mw|Import-interwiki-namespace}}\n* {{msg-mw|Import-interwiki-rootpage}}\n* {{msg-mw|Import-interwiki-submit}}\n{{Identical|Comment}}",
"importtext": "Used in the Import form on [[Special:Import]].",
"importstart": "Used in [[Special:Import]].\n\nSee also:\n* {{msg-mw|Importsuccess}}\n* {{msg-mw|Importfailed}}",
"imported-log-entries": "Used as success message. Parameters:\n* $1 - number of log items\nSee also:\n* {{msg-mw|Importnopages}} - fatal error message",
"importfailed": "Used as error message in [[Special:Import]]. Parameters:\n* $1 - import source\nSee also:\n* {{msg-mw|Importstart}}\n* {{msg-mw|Importsuccess}}",
"importunknownsource": "Used as error message in [[Special:Import]].\n\nSee also:\n* {{msg-mw|import-token-mismatch}}\n* {{msg-mw|import-invalid-interwiki}}\n* {{msg-mw|Importunknownsource}}",
+ "importnoprefix": "Used as error message in [[Special:Import]]. Usually this error means that import via upload was attempted and the {{msg-mw|import-upload-username-prefix}} field was left empty.",
"importcantopen": "Used as error message when importing from file or from URL.",
"importbadinterwiki": "Used as error message when importing from interwiki.\n\nSee also:\n* {{msg-mw|Import-noarticle}}\n* {{msg-mw|Importbadinterwiki}}",
"importsuccess": "Used in [[Special:Import]].\n\nSee also:\n* {{msg-mw|Importstart}}\n* {{msg-mw|Importfailed}}",
--- /dev/null
+<?php
+/**
+ * Cleanup tables that have valid usernames with no user ID
+ *
+ * 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 Maintenance
+ */
+
+use Wikimedia\Rdbms\IDatabase;
+
+require_once __DIR__ . '/Maintenance.php';
+
+/**
+ * Maintenance script that cleans up tables that have valid usernames with no
+ * user ID.
+ *
+ * @ingroup Maintenance
+ * @since 1.31
+ */
+class CleanupUsersWithNoId extends LoggedUpdateMaintenance {
+ private $prefix, $table, $assign;
+ private $triedCreations = [];
+
+ public function __construct() {
+ parent::__construct();
+ $this->addDescription( 'Cleans up tables that have valid usernames with no user ID' );
+ $this->addOption( 'prefix', 'Interwiki prefix to apply to the usernames', true, true, 'p' );
+ $this->addOption( 'table', 'Only clean up this table', false, true );
+ $this->addOption( 'assign', 'Assign edits to existing local users if they exist', false, false );
+ $this->setBatchSize( 100 );
+ }
+
+ protected function getUpdateKey() {
+ return __CLASS__;
+ }
+
+ protected function doDBUpdates() {
+ $this->prefix = $this->getOption( 'prefix' );
+ $this->table = $this->getOption( 'table', null );
+ $this->assign = $this->getOption( 'assign' );
+
+ $this->cleanup(
+ 'revision', 'rev_id', 'rev_user', 'rev_user_text',
+ [ 'rev_user' => 0 ], [ 'rev_timestamp', 'rev_id' ]
+ );
+ $this->cleanup(
+ 'archive', 'ar_id', 'ar_user', 'ar_user_text',
+ [], [ 'ar_id' ]
+ );
+ $this->cleanup(
+ 'logging', 'log_id', 'log_user', 'log_user_text',
+ [ 'log_user' => 0 ], [ 'log_timestamp', 'log_id' ]
+ );
+ $this->cleanup(
+ 'image', 'img_name', 'img_user', 'img_user_text',
+ [ 'img_user' => 0 ], [ 'img_timestamp', 'img_name' ]
+ );
+ $this->cleanup(
+ 'oldimage', [ 'oi_name', 'oi_timestamp' ], 'oi_user', 'oi_user_text',
+ [], [ 'oi_name', 'oi_timestamp' ]
+ );
+ $this->cleanup(
+ 'filearchive', 'fa_id', 'fa_user', 'fa_user_text',
+ [], [ 'fa_id' ]
+ );
+ $this->cleanup(
+ 'ipblocks', 'ipb_id', 'ipb_by', 'ipb_by_text',
+ [], [ 'ipb_id' ]
+ );
+ $this->cleanup(
+ 'recentchanges', 'rc_id', 'rc_user', 'rc_user_text',
+ [], [ 'rc_id' ]
+ );
+
+ return true;
+ }
+
+ /**
+ * Calculate a "next" condition and progress display string
+ * @param IDatabase $dbw
+ * @param string[] $indexFields Fields in the index being ordered by
+ * @param object $row Database row
+ * @return array [ string $next, string $display ]
+ */
+ private function makeNextCond( $dbw, $indexFields, $row ) {
+ $next = '';
+ $display = [];
+ for ( $i = count( $indexFields ) - 1; $i >= 0; $i-- ) {
+ $field = $indexFields[$i];
+ $display[] = $field . '=' . $row->$field;
+ $value = $dbw->addQuotes( $row->$field );
+ if ( $next === '' ) {
+ $next = "$field > $value";
+ } else {
+ $next = "$field > $value OR $field = $value AND ($next)";
+ }
+ }
+ $display = join( ' ', array_reverse( $display ) );
+ return [ $next, $display ];
+ }
+
+ /**
+ * Cleanup a table
+ *
+ * @param string $table Table to migrate
+ * @param string|string[] $primaryKey Primary key of the table.
+ * @param string $idField User ID field name
+ * @param string $nameField User name field name
+ * @param array $conds Query conditions
+ * @param string[] $orderby Fields to order by
+ */
+ protected function cleanup(
+ $table, $primaryKey, $idField, $nameField, array $conds, array $orderby
+ ) {
+ if ( $this->table !== null && $this->table !== $table ) {
+ return;
+ }
+
+ $primaryKey = (array)$primaryKey;
+ $pkFilter = array_flip( $primaryKey );
+ $this->output(
+ "Beginning cleanup of $table\n"
+ );
+
+ $dbw = $this->getDB( DB_MASTER );
+ $next = '1=1';
+ $countAssigned = 0;
+ $countPrefixed = 0;
+ while ( true ) {
+ // Fetch the rows needing update
+ $res = $dbw->select(
+ $table,
+ array_merge( $primaryKey, [ $idField, $nameField ], $orderby ),
+ array_merge( $conds, [ $next ] ),
+ __METHOD__,
+ [
+ 'ORDER BY' => $orderby,
+ 'LIMIT' => $this->mBatchSize,
+ ]
+ );
+ if ( !$res->numRows() ) {
+ break;
+ }
+
+ // Update the existing rows
+ foreach ( $res as $row ) {
+ $name = $row->$nameField;
+ if ( $row->$idField || !User::isUsableName( $name ) ) {
+ continue;
+ }
+
+ $id = 0;
+ if ( $this->assign ) {
+ $id = (int)User::idFromName( $name );
+ if ( !$id ) {
+ // See if any extension wants to create it.
+ if ( !isset( $this->triedCreations[$name] ) ) {
+ $this->triedCreations[$name] = true;
+ if ( !Hooks::run( 'ImportHandleUnknownUser', [ $name ] ) ) {
+ $id = (int)User::idFromName( $name, User::READ_LATEST );
+ }
+ }
+ }
+ }
+ if ( $id ) {
+ $set = [ $idField => $id ];
+ $counter = &$countAssigned;
+ } else {
+ $set = [ $nameField => substr( $this->prefix . '>' . $name, 0, 255 ) ];
+ $counter = &$countPrefixed;
+ }
+
+ $dbw->update(
+ $table,
+ $set,
+ array_intersect_key( (array)$row, $pkFilter ) + [
+ $idField => 0,
+ $nameField => $name,
+ ],
+ __METHOD__
+ );
+ $counter += $dbw->affectedRows();
+ }
+
+ list( $next, $display ) = $this->makeNextCond( $dbw, $orderby, $row );
+ $this->output( "... $display\n" );
+ wfWaitForSlaves();
+ }
+
+ $this->output(
+ "Completed cleanup, assigned $countAssigned and prefixed $countPrefixed row(s)\n"
+ );
+ }
+}
+
+$maintClass = "CleanupUsersWithNoId";
+require_once RUN_MAINTENANCE_IF_MAIN;
);
$this->addOption( 'image-base-path', 'Import files from a specified path', false, true );
$this->addOption( 'skip-to', 'Start from nth page by skipping first n-1 pages', false, true );
+ $this->addOption( 'username-interwiki', 'Use interwiki usernames with this prefix', false, true );
+ $this->addOption( 'no-local-users',
+ 'Treat all usernames as interwiki. ' .
+ 'The default is to assign edits to local users where they exist.',
+ false, false
+ );
$this->addArg( 'file', 'Dump file to import [else use stdin]', false );
}
if ( $this->hasOption( 'no-updates' ) ) {
$importer->setNoUpdates( true );
}
+ if ( $this->hasOption( 'username-prefix' ) ) {
+ $importer->setUsernamePrefix(
+ $this->getOption( 'username-prefix' ),
+ !$this->hasOption( 'no-local-users' )
+ );
+ }
if ( $this->hasOption( 'rootpage' ) ) {
$statusRootPage = $importer->setTargetRootPage( $this->getOption( 'rootpage' ) );
if ( !$statusRootPage->isGood() ) {
'enableAutoblock' => true,
'hideName' => true,
'blockEmail' => true,
- 'byText' => 'MetaWikiUser',
+ 'byText' => 'm>MetaWikiUser',
];
$block = new Block( $blockOptions );
$block->insert();
'enableAutoblock' => true,
'hideName' => true,
'blockEmail' => true,
- 'byText' => 'MetaWikiUser',
+ 'byText' => 'Meta>MetaWikiUser',
];
$block = new Block( $blockOptions );
'Correct blockee name'
);
$this->assertEquals( $userId, $block->getTarget()->getId(), 'Correct blockee id' );
- $this->assertEquals( 'MetaWikiUser', $block->getBlocker(), 'Correct blocker name' );
- $this->assertEquals( 'MetaWikiUser', $block->getByName(), 'Correct blocker name' );
+ $this->assertEquals( 'Meta>MetaWikiUser', $block->getBlocker()->getName(),
+ 'Correct blocker name' );
+ $this->assertEquals( 'Meta>MetaWikiUser', $block->getByName(), 'Correct blocker name' );
$this->assertEquals( 0, $block->getBy(), 'Correct blocker id' );
}
],
];
+ $blocker = $this->getTestUser()->getUser();
foreach ( $blockList as $insBlock ) {
$target = $insBlock['target'];
$block = new Block();
$block->setTarget( $target );
- $block->setBlocker( 'testblocker@global' );
+ $block->setBlocker( $blocker );
$block->mReason = $insBlock['desc'];
$block->mExpiry = 'infinity';
$block->prevents( 'createaccount', $insBlock['ACDisable'] );
'reason' => 'test system block',
'timestamp' => wfTimestampNow(),
'expiry' => $this->db->getInfinity(),
- 'byText' => 'MetaWikiUser',
+ 'byText' => 'MediaWiki default',
'systemBlock' => 'test',
'enableAutoblock' => true,
];
// @codingStandardsIgnoreEnd
}
+ /**
+ * @dataProvider provideUnknownUserHandling
+ * @param bool $assign
+ * @param bool $create
+ */
+ public function testUnknownUserHandling( $assign, $create ) {
+ $hookId = -99;
+ $this->setMwGlobals( 'wgHooks', [
+ 'ImportHandleUnknownUser' => [ function ( $name ) use ( $assign, $create, &$hookId ) {
+ if ( !$assign ) {
+ $this->fail( 'ImportHandleUnknownUser was called unexpectedly' );
+ }
+
+ $this->assertEquals( 'UserDoesNotExist', $name );
+ if ( $create ) {
+ $user = User::createNew( $name );
+ $this->assertNotNull( $user );
+ $hookId = $user->getId();
+ return false;
+ }
+ return true;
+ } ]
+ ] );
+
+ $user = $this->getTestUser()->getUser();
+
+ $n = ( $assign ? 1 : 0 ) + ( $create ? 2 : 0 );
+
+ // @codingStandardsIgnoreStart Generic.Files.LineLength
+ $source = $this->getDataSource( <<<EOF
+<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.10/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.10/ http://www.mediawiki.org/xml/export-0.10.xsd" version="0.10" xml:lang="en">
+ <page>
+ <title>TestImportPage</title>
+ <ns>0</ns>
+ <id>14</id>
+ <revision>
+ <id>15</id>
+ <timestamp>2016-01-01T0$n:00:00Z</timestamp>
+ <contributor>
+ <username>UserDoesNotExist</username>
+ <id>1</id>
+ </contributor>
+ <model>wikitext</model>
+ <format>text/x-wiki</format>
+ <text xml:space="preserve" bytes="3">foo</text>
+ <sha1>1e6gpc3ehk0mu2jqu8cg42g009s796b</sha1>
+ </revision>
+ <revision>
+ <id>16</id>
+ <timestamp>2016-01-01T0$n:00:01Z</timestamp>
+ <contributor>
+ <username>{$user->getName()}</username>
+ <id>{$user->getId()}</id>
+ </contributor>
+ <model>wikitext</model>
+ <format>text/x-wiki</format>
+ <text xml:space="preserve" bytes="3">bar</text>
+ <sha1>bjhlo6dxh5wivnszm93u4b78fheiy4t</sha1>
+ </revision>
+ </page>
+</mediawiki>
+EOF
+ );
+ // @codingStandardsIgnoreEnd
+
+ $importer = new WikiImporter( $source, MediaWikiServices::getInstance()->getMainConfig() );
+ $importer->setUsernamePrefix( 'Xxx', $assign );
+ $importer->doImport();
+
+ $db = wfGetDB( DB_MASTER );
+
+ $row = $db->selectRow(
+ 'revision',
+ [ 'rev_user', 'rev_user_text' ],
+ [ 'rev_timestamp' => "201601010{$n}0000" ],
+ __METHOD__
+ );
+ $this->assertSame(
+ $assign && $create ? 'UserDoesNotExist' : 'Xxx>UserDoesNotExist',
+ $row->rev_user_text
+ );
+ $this->assertSame( $assign && $create ? $hookId : 0, (int)$row->rev_user );
+
+ $row = $db->selectRow(
+ 'revision',
+ [ 'rev_user', 'rev_user_text' ],
+ [ 'rev_timestamp' => "201601010{$n}0001" ],
+ __METHOD__
+ );
+ $this->assertSame( ( $assign ? '' : 'Xxx>' ) . $user->getName(), $row->rev_user_text );
+ $this->assertSame( $assign ? $user->getId() : 0, (int)$row->rev_user );
+ }
+
+ public static function provideUnknownUserHandling() {
+ return [
+ 'no assign' => [ false, false ],
+ 'assign, no create' => [ true, false ],
+ 'assign, create' => [ true, true ],
+ ];
+ }
+
}