Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F2752777
backfillLocalAccounts.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
11 KB
Referenced Files
None
Subscribers
None
backfillLocalAccounts.php
View Options
<?php
/**
* Create missing local users for existing global user accounts.
*
* 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
*/
namespace
MediaWiki\Extension\CentralAuth\Maintenance
;
$IP
=
getenv
(
'MW_INSTALL_PATH'
);
if
(
$IP
===
false
)
{
$IP
=
__DIR__
.
'/../../..'
;
}
require_once
"$IP/maintenance/Maintenance.php"
;
use
MediaWiki\CheckUser\Services\AccountCreationDetailsLookup
;
use
MediaWiki\Context\RequestContext
;
use
MediaWiki\Extension\CentralAuth\CentralAuthHooks
;
use
MediaWiki\Extension\CentralAuth\CentralAuthServices
;
use
MediaWiki\Extension\CentralAuth\User\CentralAuthUser
;
use
MediaWiki\Maintenance\Maintenance
;
use
MediaWiki\Registration\ExtensionRegistry
;
use
MediaWiki\User\User
;
use
MediaWiki\User\UserFactory
;
use
MediaWiki\WikiMap\WikiMap
;
use
RuntimeException
;
use
Wikimedia\Rdbms\IReadableDatabase
;
use
Wikimedia\Rdbms\IResultWrapper
;
use
Wikimedia\Rdbms\LBFactory
;
use
Wikimedia\Rdbms\RawSQLExpression
;
use
Wikimedia\Rdbms\SelectQueryBuilder
;
use
Wikimedia\ScopedCallback
;
use
Wikimedia\Timestamp\ConvertibleTimestamp
;
/**
* Given a starting date, check for global users created on or
* later than that date, that do not have local accounts on the
* wiki this script is run on, and attempt to create them,
* using the user agent and ip address from the creation
* of the local account on their home wiki, if that info
* is available, or skipping the account creation otherwise.
* Errors will be shown if a missing account cannot be created.
*/
class
BackfillLocalAccounts
extends
Maintenance
{
/** @var string */
private
$startdateTS
;
/** @var UserFactory */
private
$userFactory
;
/** @var User|null */
private
$performer
;
public
function
__construct
()
{
parent
::
__construct
();
$this
->
requireExtension
(
'CentralAuth'
);
$this
->
addOption
(
'startdate'
,
'Backfill for global users created later than this date'
,
true
,
true
);
$this
->
addOption
(
'dryrun'
,
'Display commands that would be run instead of running them'
,
false
,
false
);
$this
->
addOption
(
'verbose'
,
'Display extra progress information while running'
,
false
,
false
);
$this
->
setBatchSize
(
$this
->
getOption
(
'batch-size'
,
1000
)
);
}
/**
* get the smallest global uid registered on or after the specified start date
*
* @param IReadableDatabase $cadb
* @param string $startdate
*
* @return int|false global uid
*/
private
function
getStartUID
(
$cadb
,
$startdate
)
{
$startid
=
$cadb
->
newSelectQueryBuilder
()
->
select
(
'gu_id'
)
->
from
(
'globaluser'
)
->
where
(
$cadb
->
expr
(
'gu_registration'
,
'>='
,
$startdate
)
)
->
orderBy
(
'gu_id'
,
SelectQueryBuilder
::
SORT_ASC
)
->
limit
(
1
)
->
caller
(
__METHOD__
)
->
fetchField
();
return
$startid
;
}
/**
* get the maximum global uid in the central auth database
*
* @param IReadableDatabase $cadb
*
* @return int|null global uid
*/
private
function
getMaxUID
(
$cadb
)
{
$maxid
=
$cadb
->
newSelectQueryBuilder
()
->
select
(
'MAX(gu_id)'
)
->
from
(
'globaluser'
)
->
caller
(
__METHOD__
)
->
fetchField
();
return
$maxid
;
}
/**
* create a local account on the wiki this script is running on,
* for the specific user name
*
* @param string $username
* @param bool $verbose
*/
private
function
createLocalAccount
(
$username
,
$verbose
)
{
$status
=
CentralAuthServices
::
getForcedLocalCreationService
()
->
attemptAutoCreateLocalUserFromName
(
$username
,
$this
->
performer
,
"Backfilled by autocreation script"
);
if
(
!
$status
->
isGood
()
)
{
$this
->
error
(
"autoCreateUser failed for $username:"
);
$this
->
error
(
$status
);
return
;
}
if
(
$verbose
)
{
$this
->
output
(
"User '$username' created
\n
"
);
}
}
/**
* make sure the user does not already exist locally,
* and get the home wiki for the user, if possible
*
* @param string $gu_name
* @param UserFactory $userFactory
* @param bool $verbose
*
* @return string|null home wiki of user, or null on error/missing
* @throws RuntimeException
*/
public
function
checkUserAndGetHomeWiki
(
$gu_name
,
$userFactory
,
$verbose
)
{
$user
=
$userFactory
->
newFromName
(
$gu_name
);
if
(
!
$user
)
{
$this
->
error
(
"Bad user name "
.
$gu_name
);
}
$globalUser
=
CentralAuthUser
::
getInstanceByName
(
$gu_name
);
$homeWiki
=
$globalUser
->
getHomeWiki
();
if
(
!
$homeWiki
)
{
$this
->
output
(
"Skipping user name "
.
$gu_name
.
" , missing home wiki
\n
"
);
return
null
;
}
return
$homeWiki
;
}
/**
* return session info containing the ip address and user agent for the specified
* user name, if found, or null otherwise
*
* @param AccountCreationDetailsLookup $accountLookup
* @param IReadableDatabase $dbr
* @param string $gu_name
* @param string $gu_registration
* @param bool $verbose
*
* @return array{userId: 0, ip: string, headers: array, sessionId: ''}|null
*/
private
function
getFakeSession
(
$accountLookup
,
$dbr
,
$gu_name
,
$gu_registration
,
$verbose
)
{
$fakeSession
=
null
;
$accountInfo
=
null
;
if
(
ExtensionRegistry
::
getInstance
()->
isLoaded
(
'CheckUser'
)
)
{
$accountInfo
=
$accountLookup
->
getAccountCreationIPAndUserAgent
(
$gu_name
,
$dbr
);
if
(
$accountInfo
==
null
)
{
// maybe this account was created by someone else; we'll try to get the
// performer info instead
[
$performer
,
$logId
]
=
$accountLookup
->
findPerformerAndLogId
(
$dbr
,
$gu_name
,
$gu_registration
);
if
(
$performer
)
{
$accountInfo
=
$accountLookup
->
getAccountCreationIPAndUserAgent
(
$performer
,
$dbr
,
$logId
);
}
}
}
if
(
$accountInfo
!=
null
)
{
$fakeSession
=
[
// no user in the local wiki, no session either
'userId'
=>
0
,
'sessionId'
=>
''
,
// the useful bits are here
'ip'
=>
$accountInfo
[
'ip'
],
'headers'
=>
[
'User-Agent'
=>
$accountInfo
[
'agent'
]
]
];
if
(
$verbose
)
{
$this
->
output
(
"Using ip {$fakeSession['ip']} and agent {$fakeSession['headers']['User-Agent']}
\n
"
);
}
}
return
$fakeSession
;
}
/**
* @param IReadableDatabase $cadb
* @param int $batchStartUID
* @param int $maxGlobalUID
* @param string $wikiID
*
* @return array{0:int,1:IResultWrapper}
*/
protected
function
getGlobalUserBatch
(
$cadb
,
$batchStartUID
,
$maxGlobalUID
,
$wikiID
)
{
$subQuery
=
$cadb
->
newSelectQueryBuilder
()
->
select
(
'1'
)
->
from
(
'localuser'
)
->
where
(
'lu_name = gu_name'
)
->
andWhere
(
[
'lu_wiki'
=>
$wikiID
]
);
$result
=
null
;
do
{
$result
=
$cadb
->
newSelectQueryBuilder
()
->
select
(
[
'gu_name'
,
'gu_id'
,
'gu_registration'
]
)
->
from
(
'globaluser'
)
->
where
(
$cadb
->
expr
(
'gu_id'
,
'>='
,
$batchStartUID
)
)
->
andWhere
(
$cadb
->
expr
(
'gu_id'
,
'<'
,
$batchStartUID
+
$this
->
mBatchSize
)
)
// we want to filter out rows where there is already a corresponding
// local user on the wiki where this script is being run
->
andWhere
(
new
RawSQLExpression
(
'NOT EXISTS('
.
$subQuery
->
getSQL
()
.
')'
)
)
->
orderBy
(
'gu_id'
,
SelectQueryBuilder
::
SORT_ASC
)
->
caller
(
__METHOD__
)
->
fetchResultSet
();
$batchStartUID
+=
$this
->
mBatchSize
;
if
(
$batchStartUID
>
$maxGlobalUID
)
{
break
;
}
}
while
(
!
$result
->
numRows
()
);
return
[
intval
(
$batchStartUID
),
$result
];
}
/**
* retrieve global users in batches with uid in the specified
* range, and if we can find their home wiki, create a
* local user on the wiki where this script runs, with the
* user's ip and user agent from account creation on their
* home wiki if available, otherwise skip
*
* @param IReadableDatabase $cadb
* @param UserFactory $userFactory
* @param AccountCreationDetailsLookup $accountLookup
* @param LBFactory $lbFactory
* @param bool $dryrun
* @param bool $verbose
* @param int $startGlobalUID
* @param int $maxGlobalUID
* @param string $wikiID
*/
public
function
checkAndCreateAccounts
(
$cadb
,
$userFactory
,
$accountLookup
,
$lbFactory
,
$dryrun
,
$verbose
,
$startGlobalUID
,
$maxGlobalUID
,
$wikiID
)
{
$createdUsers
=
0
;
$currentUID
=
$startGlobalUID
;
$dbw
=
$this
->
getPrimaryDB
();
do
{
[
$startGlobalUID
,
$result
]
=
$this
->
getGlobalUserBatch
(
$cadb
,
$startGlobalUID
,
$maxGlobalUID
,
$wikiID
);
$this
->
beginTransaction
(
$dbw
,
__METHOD__
);
foreach
(
$result
as
$row
)
{
$homeWiki
=
$this
->
checkUserAndGetHomeWiki
(
$row
->
gu_name
,
$userFactory
,
$verbose
);
if
(
!
$homeWiki
)
{
continue
;
}
// get the user agent and ip address with which the user account was created on
// their home wiki, if available, and create a local account for that user,
// with that user agent and ip
$dbr
=
$lbFactory
->
getReplicaDatabase
(
$homeWiki
);
if
(
$dryrun
)
{
$this
->
output
(
"Would create user $row->gu_name from guid "
.
strval
(
$row
->
gu_id
)
.
" and home wiki "
.
"$homeWiki
\n
"
);
}
else
{
$fakeSession
=
$this
->
getFakeSession
(
$accountLookup
,
$dbr
,
$row
->
gu_name
,
$row
->
gu_registration
,
$verbose
);
if
(
!
$fakeSession
)
{
if
(
$verbose
)
{
$this
->
output
(
"Skipping user $row->gu_name creation, no IP/UA info available
\n
"
);
}
continue
;
}
$callback
=
RequestContext
::
importScopedSession
(
$fakeSession
);
// Dig down far enough and this uses User::addToDatabase() which relies on
// MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase()
// which should be the same as our $dbw arg to begin/commitTransaction(). But I don't like it.
$this
->
createLocalAccount
(
$row
->
gu_name
,
$verbose
);
ScopedCallback
::
consume
(
$callback
);
$createdUsers
++;
}
}
$this
->
commitTransaction
(
$dbw
,
__METHOD__
);
if
(
$startGlobalUID
>
$maxGlobalUID
)
{
break
;
}
}
while
(
true
);
if
(
$verbose
)
{
$this
->
output
(
"Created users: {$createdUsers}, done.
\n
"
);
}
}
public
function
execute
()
{
$services
=
$this
->
getServiceContainer
();
$this
->
userFactory
=
$services
->
getUserFactory
();
$dryrun
=
$this
->
hasOption
(
'dryrun'
);
$verbose
=
$this
->
hasOption
(
'verbose'
);
$date
=
new
ConvertibleTimestamp
(
strtotime
(
$this
->
getOption
(
'startdate'
)
)
);
$this
->
startdateTS
=
$date
->
getTimestamp
(
TS_MW
);
$enddate
=
new
ConvertibleTimestamp
(
strtotime
(
'now'
)
);
$enddateTS
=
$enddate
->
getTimestamp
(
TS_MW
);
$this
->
performer
=
User
::
newSystemUser
(
CentralAuthHooks
::
BACKFILL_ACCOUNT_CREATOR
,
[
'steal'
=>
true
]
);
if
(
!
$this
->
performer
)
{
$this
->
fatalError
(
"ERROR - unable to get/create system user "
.
CentralAuthHooks
::
BACKFILL_ACCOUNT_CREATOR
);
}
$cadb
=
CentralAuthServices
::
getDatabaseManager
()->
getCentralReplicaDB
();
$maxGlobalUID
=
$this
->
getMaxUID
(
$cadb
);
$startGlobalUID
=
$this
->
getStartUID
(
$cadb
,
$this
->
startdateTS
);
if
(
!
$maxGlobalUID
||
!
$startGlobalUID
)
{
$this
->
output
(
"No accounts eligible for autocreation
\n
"
);
return
;
}
if
(
$verbose
)
{
$this
->
output
(
"Starting guid: $startGlobalUID
\n
"
);
$this
->
output
(
"Ending guid: $maxGlobalUID
\n
"
);
}
$lbFactory
=
$services
->
getDBLoadBalancerFactory
();
$wikiID
=
WikiMap
::
getCurrentWikiId
();
$this
->
checkAndCreateAccounts
(
$cadb
,
$this
->
userFactory
,
$services
->
get
(
'AccountCreationDetailsLookup'
),
$lbFactory
,
$dryrun
,
$verbose
,
$startGlobalUID
,
$maxGlobalUID
,
$wikiID
);
}
}
$maintClass
=
BackfillLocalAccounts
::
class
;
require_once
RUN_MAINTENANCE_IF_MAIN
;
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Fri, Jul 3, 20:22 (1 d, 21 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
0f/b6/7988cb0413b110f3d6580ba42b7f
Default Alt Text
backfillLocalAccounts.php (11 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment