diff --git a/TestPlugin.php b/TestPlugin.php new file mode 100644 index 000000000..d53eaa33f --- /dev/null +++ b/TestPlugin.php @@ -0,0 +1,720 @@ +mName = $username; + } + + /** + * this code is crap + */ + function exists() { + $dbr = wfGetDB( DB_MASTER, 'CentralAuth' ); + $ok = $dbr->selectField( + 'globaluser', + '1', + array( 'gu_name' => $this->mName ), + __METHOD__ ); + if( $ok ) { + wfDebugLog( 'CentralAuth', + "checked for global account '$this->mName', found" ); + } else { + wfDebugLog( 'CentralAuth', + "checked for global account '$this->mName', missing" ); + } + return (bool)$ok; + } + + /** + * this code is crapper + */ + function register( $password ) { + $dbw = wfGetDB( DB_MASTER, 'CentralAuth' ); + list( $salt, $hash ) = $this->saltedPassword( $password ); + $ok = $dbw->insert( + 'globaluser', + array( + 'gu_name' => $this->mName, + + 'gu_email' => null, // FIXME + 'gu_email_authenticated' => null, // FIXME + + 'gu_salt' => $salt, + 'gu_password' => $hash, + + 'gu_locked' => 0, + 'gu_hidden' => 0, + + 'gu_registration' => $dbw->timestamp(), + ), + __METHOD__ ); + + if( $ok ) { + wfDebugLog( 'CentralAuth', + "registered global account '$this->mName'" ); + } else { + wfDebugLog( 'CentralAuth', + "registration failed for global account '$this->mName'" ); + } + return $ok; + } + + /** + * For use in migration pass zero. + * Store local user data into the auth server's migration table. + */ + static function storeLocalData( $dbname, $row, $editCount ) { + $dbw = wfGetDB( DB_MASTER, 'CentralAuth' ); + $ok = $dbw->insert( + 'localuser', + array( + 'lu_dbname' => $dbname, + 'lu_id' => $row->user_id, + 'lu_name' => $row->user_name, + 'lu_password' => $row->user_password, + 'lu_email' => $row->user_email, + 'lu_email_authenticated' => $row->user_email_authenticated, + 'lu_editcount' => $editCount, + 'lu_attached' => 0, // Not yet migrated! + ), + __METHOD__ ); + wfDebugLog( 'CentralAuth', + "stored migration data for '$row->user_name' on $dbname" ); + } + + /** + * For use in migration pass one. + * Store global user data in the auth server's main table. + */ + function storeGlobalData( $salt, $hash, $email, $emailAuth ) { + $dbw = wfGetDB( DB_MASTER, 'CentralAuth' ); + $dbw->insert( 'globaluser', + array( + 'gu_name' => $this->mName, + 'gu_salt' => $salt, + 'gu_password' => $hash, + 'gu_email' => $email, + 'gu_email_authenticated' => $emailAuth, + ), + __METHOD__ ); + } + + function storeAndMigrate() { + $dbw = wfGetDB( DB_MASTER, 'CentralAuth' ); + $dbw->begin(); + + $ret = $this->attemptAutoMigration(); + + $dbw->commit(); + return $ret; + } + + /** + * Pick a winning master account and try to auto-merge as many as possible. + * @fixme add some locking or something + */ + function attemptAutoMigration( $password='' ) { + $rows = $this->fetchUnattached(); + + $winner = false; + $max = -1; + $attach = array(); + $unattach = array(); + + // We have to pick a master account + // The winner is the one with the most edits, usually + foreach( $rows as $row ) { + if( $row->lu_editcount > $max ) { + $winner = $row; + $max = $row->lu_editcount; + } + } + assert( isset( $winner ) ); + + // Do they all match? + $allMatch = true; + $allMatchOrEmpty = true; + $allMatchOrUnused = true; + $isConflict = false; + $winningMail = ($winner->lu_email == '' ? false : $winner->lu_email); + + foreach( $rows as $row ) { + if( $row->lu_dbname == $winner->lu_dbname ) { + $attach[] = $row; + } else { + if( $row->lu_email !== $winningMail ) { + $allMatch = false; + if( $row->lu_email !== '' ) { + $allMatchOrEmpty = false; + } + if( $row->lu_editcount == 0 ) { + // Unused accounts are fair game for reclaiming + $attach[] = $row; + } else { + $allMatchOrUnused = false; + $unattach[] = $row; + $isConflict = true; + } + } else { + $attach[] = $row; + } + } + } + + if( $allMatch ) { + if( count( $rows ) == 1 ) { + wfDebugLog( 'CentralAuth', + "Singleton migration for '$this->mName'" ); + } else { + wfDebugLog( 'CentralAuth', + "Full automatic migration for '$this->mName'" ); + } + } else { + wfDebugLog( 'CentralAuth', + "Incomplete migration for '$this->mName'" ); + } + + $this->storeGlobalData( + $winner->lu_id, + $winner->lu_password, + $winner->lu_email, + $winner->lu_email_authenticated ); + + foreach( $attach as $row ) { + $this->attach( $row->lu_dbname ); + } + + } + + /** + * Attempt to migrate any remaining unattached accounts by virtue of + * the password check. + * + * @param string $password plaintext password to try matching + * @param $migrated out array of db names for records which were + * successfully migrated by this operation + * @param $remaining out array of db names for records which are still + * unattached after the operation + * @return bool true if all accounts are migrated at the end + */ + function attemptPasswordMigration( $password, &$migrated=null, &$remaining=null ) { + $rows = $this->fetchUnattached(); + + if( count( $rows ) == 0 ) { + wfDebugLog( 'CentralAuth', + "Already fully migrated user '$this->mName'" ); + return true; + } + + $migrated = array(); + $remaining = array(); + + // Look for accounts we can match by password + foreach( $rows as $key => $row ) { + if( $this->matchHash( $password, $row->lu_id, $row->lu_password ) ) { + wfDebugLog( 'CentralAuth', + "Attaching '$this->mName' on $row->lu_dbname by password" ); + $this->attach( $row->lu_dbname ); + $migrated[] = $row->lu_dbname; + } else { + wfDebugLog( 'CentralAuth', + "No password match for '$this->mName' on $row->lu_dbname" ); + $remaining[] = $row->lu_dbname; + } + } + + if( count( $remaining ) == 0 ) { + wfDebugLog( 'CentralAuth', + "Successfull auto migration for '$this->mName'" ); + return true; + } + + wfDebugLog( 'CentralAuth', + "Incomplete migration for '$this->mName'" ); + return false; + } + + /** + * Check if the current username is defined and attached on this wiki yet + * @param $dbname Local database key to look up + * @return ("attached", "unattached", "no local user") + */ + function isAttached( $dbname ) { + $dbr = wfGetDB( DB_MASTER, 'CentralAuth' ); + $row = $dbr->selectRow( 'localuser', + array( 'lu_attached' ), + array( 'lu_name' => $this->mName, 'lu_database' => $dbname ), + __METHOD__ ); + + if( !$row ) { + return "no local user"; + } + + if( $row->lu_attached ) { + return "attached"; + } else { + return "unattached"; + } + } + + /** + * Add a local account record for the given wiki to the central database. + * @param + */ + function addLocal( $dbname, $localid ) { + $dbw = wfGetDB( DB_MASTER, 'CentralAuth' ); + $dbw->insert( 'localuser', + array( + 'lu_dbname' => $dbname, + 'lu_id' => $localid, + 'lu_name' => $this->mName, + 'lu_attached' => 1 ), + __METHOD__ ); + } + + /** + * Declare the local account for a given wiki to be attached + * to the global account for the current username. + * + * @return true on success + */ + public function attach( $dbname ) { + $dbw = wfGetDB( DB_MASTER, 'CentralAuth' ); + $dbw->update( 'localuser', + array( + // Boo-yah! + 'lu_attached' => 1, + + // Local information fields become obsolete + /* + 'lu_email' => NULL, + 'lu_email_authenticated' => NULL, + 'lu_password' => NULL, + */ + ), + array( + 'lu_dbname' => $dbname, + 'lu_name' => $this->mName ), + __METHOD__ ); + + $rows = $dbw->affectedRows(); + if( $rows > 0 ) { + return true; + } else { + wfDebugLog( 'CentralAuth', + "failed to attach \"{$this->mName}@$dbname\", not in localuser\n" ); + return false; + } + } + + /** + * Attempt to authenticate the global user account with the given password + * @param string $password + * @return ("ok", "no user", "locked", "bad password") + */ + public function authenticate( $password ) { + $dbw = wfGetDB( DB_MASTER, 'CentralAuth' ); + $row = $dbw->selectRow( 'globaluser', + array( 'gu_salt', 'gu_password', 'gu_locked' ), + array( 'gu_name' => $this->mName ), + __METHOD__ ); + + if( !$row ) { + return "no user"; + } + + $salt = $row->gu_salt; + $crypt = $row->gu_password; + $locked = $row->gu_locked; + + if( $locked ) { + wfDebugLog( 'CentralAuth', + "authentication for '$this->mName' failed due to lock" ); + return "locked"; + } + + if( $this->matchHash( $password, $salt, $crypt ) ) { + wfDebugLog( 'CentralAuth', + "authentication for '$this->mName' succeeded" ); + return "ok"; + } else { + wfDebugLog( 'CentralAuth', + "authentication for '$this->mName' failed, bad pass" ); + return "bad password"; + } + } + + /** + * @param $plaintext User-provided password plaintext. + * @param $salt The hash "salt", eg a local id for migrated passwords. + * @param $encrypted Fully salted and hashed database crypto text from db. + * @return bool true on match. + */ + private function matchHash( $plaintext, $salt, $encrypted ) { + return md5( $salt . "-" . md5( $plaintext ) ) === $encrypted; + } + + /** + * Fetch a list of databases where this account name is registered, + * but not yet attached to the global account. It would be used for + * an alert or management system to show which accounts have still + * to be dealt with. + * + * @return array of database name strings + */ + function listUnattached() { + $rows = $this->fetchUnattached; + $dbs = array(); + foreach( $rows as $row ) { + $dbs[] = $row->lu_dbname; + } + return $dbs; + } + + function fetchUnattached() { + $dbw = wfGetDB( DB_MASTER, 'CentralAuth' ); + $result = $dbw->select( 'localuser', + array( + 'lu_dbname', + 'lu_id', + 'lu_name', + 'lu_password', + 'lu_email', + 'lu_email_authenticated', + 'lu_editcount', + 'lu_attached', + ), + array( + 'lu_name' => $this->mName, + 'lu_attached' => 0, + ), + __METHOD__ ); + while( $row = $dbw->fetchObject( $result ) ) { + $rows[] = $row; + } + $dbw->freeResult( $result ); + return $rows; + } + + function getEmail() { + $dbr = wfGetDB( DB_MASTER, 'CentralAuth' ); + return $dbr->selectField( 'globaluser', 'gu_email', + array( 'gu_name' => $this->mName ), + __METHOD__ ); + } + + function saltedPassword( $password ) { + $salt = mt_rand( 0, 1000000 ); + $hash = wfEncryptPassword( $salt, $password ); + return array( $salt, $hash ); + } + + /** + * Set the account's password + */ + function setPassword( $password ) { + list( $salt, $hash ) = $this->saltedPassword( $password ); + + $dbw = wfGetDB( DB_MASTER, 'CentralAuth' ); + $result = $dbr->update( 'globaluser', + array( + 'gu_salt' => $salt, + 'gu_password' => $hash, + ), + array( + 'gu_name' => $this->mName, + ), + __METHOD__ ); + + $rows = $dbw->numRows( $result ); + $dbw->freeResult( $result ); + + return $rows > 0; + } + +} + +/** + * Quickie test implementation using local test database + */ +class CentralAuth extends AuthPlugin { + static function factory() { + global $wgCentralAuthState; + switch( $wgCentralAuthState ) { + case 'premigrate': + case 'testing': + // FIXME + return new AuthPlugin(); + case 'migration': + case 'production': + return new CentralAuth(); + default: + die('wtf'); + } + } + + /** + * Check whether there exists a user account with the given name. + * The name will be normalized to MediaWiki's requirements, so + * you might need to munge it (for instance, for lowercase initial + * letters). + * + * @param $username String: username. + * @return bool + * @public + */ + function userExists( $username ) { + $user = new CentralAuthUser( $username ); + return $user->exists(); + } + + /** + * Check if a username+password pair is a valid login. + * The name will be normalized to MediaWiki's requirements, so + * you might need to munge it (for instance, for lowercase initial + * letters). + * + * @param $username String: username. + * @param $password String: user password. + * @return bool + * @public + */ + function authenticate( $username, $password ) { + $user = new CentralAuthUser( $username ); + return $user->authenticate( $password ) == "ok"; + } + + /** + * When a user logs in, optionally fill in preferences and such. + * For instance, you might pull the email address or real name from the + * external user database. + * + * The User object is passed by reference so it can be modified; don't + * forget the & on your function declaration. + * + * @param User $user + * @public + */ + function updateUser( &$user ) { + # Override this and do something + return true; + } + + + /** + * Return true if the wiki should create a new local account automatically + * when asked to login a user who doesn't exist locally but does in the + * external auth database. + * + * If you don't automatically create accounts, you must still create + * accounts in some way. It's not possible to authenticate without + * a local account. + * + * This is just a question, and shouldn't perform any actions. + * + * @return bool + * @public + */ + function autoCreate() { + return true; + } + + /** + * Set the given password in the authentication database. + * Return true if successful. + * + * @param $password String: password. + * @return bool + * @public + */ + function setPassword( $password ) { + // Fixme: password changes should happen through central interface. + $global = CentralAuthUser( $user->getName() ); + return $global->setPassword( $password ); + } + + /** + * Update user information in the external authentication database. + * Return true if successful. + * + * @param $user User object. + * @return bool + * @public + */ + function updateExternalDB( $user ) { + return true; + } + + /** + * Check to see if external accounts can be created. + * Return true if external accounts can be created. + * @return bool + * @public + */ + function canCreateAccounts() { + // Require accounts to be created through the central login interface? + return true; + } + + /** + * Add a user to the external authentication database. + * Return true if successful. + * + * @param User $user + * @param string $password + * @return bool + * @public + */ + function addUser( $user, $password ) { + $global = new CentralAuthUser( $user->getName() ); + return $global->register( $password ); + } + + + /** + * Return true to prevent logins that don't authenticate here from being + * checked against the local database's password fields. + * + * This is just a question, and shouldn't perform any actions. + * + * @return bool + * @public + */ + function strict() { + return true; + } + + /** + * When creating a user account, optionally fill in preferences and such. + * For instance, you might pull the email address or real name from the + * external user database. + * + * The User object is passed by reference so it can be modified; don't + * forget the & on your function declaration. + * + * @param $user User object. + * @public + */ + function initUser( &$user ) { + # Override this to do something. + $global = new CentralAuthUser( $user->getName() ); + $user->setEmail( $global->getEmail() ); + // etc + } +} + +?> diff --git a/evil-plans.txt b/evil-plans.txt new file mode 100644 index 000000000..8df288e05 --- /dev/null +++ b/evil-plans.txt @@ -0,0 +1,251 @@ +Implementation notes... +----------------------- + +== Goals == + +As a reminder, some things we are and aren't trying to accomplish here: + +=== Are trying to achieve: === + +* All new accounts will be valid on all Wikimedia wikis, using a consistent + username and password everywhere. + +* Once migrated, all old accounts will be valid on all Wikimedia wikis, + using a consistent username and password everywhere. + +* Accounts will only have to set and confirm e-mail in one place. + +=== Are not trying to achieve at this time: === + +* Automatic passing of login data between sites +* Integration with non-Wikimedia authentication systems (OpenID etc) +* Total integration of user options, etc across wikis + +=== Are not trying to achieve ever: === + +* Different usernames on each wiki + + +== Migration strategies == + +The system consists of 'local' accounts (the user table entries on each wiki) +and 'global' accounts (the accounts on the central auth server). + +A local account may be in one of two states: + - unattached: old account awaiting migration + - attached: migrated, or newly created under the new system + +An attempt to login with a given name on a given wiki will encounter one of +these possible states: + - no global account: 'no such user' error + - no local account: an attached local account will be transparently created + - attached: login continues + - unattached: login-time migration will be triggered + + +=== First-stage migration === + +This is an automated process which will run when the system is put into +place: + +For each name in use on the various wikis at initial migration time, a +global account is created. + +One account for each name is selected as the 'winner', usually the most +prolific. The winner's password and email address are assigned to the +global account. + +Some accounts can be fully migrated automatically: + - Name occurred only on one wiki + - Multiple instances, but all with the same e-mail address + - Potentially, unused accounts could be subsumed automatically + +Note that passwords cannot be checked at this time due to the hashing +method used in our user table. Matching e-mail addresses can be considered +'password-equivalent' here as whoever owns that address is able to set +the password. + +If there are accounts which do not match the winning e-mail address, the +account will be left in a transitional state: + - Matching local accounts are attached, and can be used to log in. + - Non-matching local accounts are left unattached, for later migration. + + +=== Login-time migration === + +When a user attempts to login to an unattached account, this triggers +login-time migration. + +The account can now be automatically attached if: + - The given password matches both the local and global account + - The local account's email address matches the global account's + confirmed e-mail address + +(We check e-mail again as the global account's email may have been changed +since original migration time.) + + +=== Login-time renaming === + +Some portion of name conflicts really are different people, so they won't +be able to confirm themselves as the global account owner. + +If the login-time migration checks fail, the user is offered the option to +rename the account, either merging it to an existing global account or making +a brand new one. + +* FIXME: We may need to clean up some rename operations to make this safe. + + +=== Cleanup and long-term === + +The presence of a third-party unattached local account on a given wiki means +that the owner of the global account can't use his/her global account to log +in on that wiki. + +Practically speaking, not all conflicting accounts will be resolved by their +owners in a timely fashion. Some will never return; some will be malicious; +some will just forget. + +We'll require a way for unclaimed unattached accounts to be renamed forcefully. +Possibly this can require a bureaucrat's intervention; possibly this can be +done by the conflicting global account's owner after some timeout period. + + +=== Notifications === + +Conflicting accounts should be notified by e-mail where possible. + + +== Implementation: parts! == + +* Core: central database o' fun +* Edge: Wikis + +=== Communication requirements === + +* Full edge<->core connectivity in cases: + - pmtpa: same database cluster + - pmtpa.enwiki: alternate database master + - yaseo: offsite [could be implemented with a ssh tunnel to mysql] + +* Open login sessions should continue to function if core is offline + +* (?) Previously used login sessions should be able to log back in if + core is offline. + +If core is inaccessible from an edge server: + +* Open login sessions should continue to basically function + - some operations such as changing password or email would fail + +* Previously used logins _may_ be able to log back in + - using previously stored password hash? Unsure about this. + +* New account creations, etc obviously would fail + + + +=== Core auth API === + +A few basic operations: + +* register($name) +* setPassword($name, $hashedPass) +* setEmail($name, $email) +* setEmailConfirmed($name) +* attachLocal($name, $dbname) +* attemptAuth($name, $hashedPass) + + + +=== Some crappy login pseudocode === + +On login: +* load local account data + if local user exists: + * coreAuth::attemptAuth($name, $hashedPass) + if passed: + -> update local lock state, email, email confirmation + -> successful login. + else if failed: + -> whine about bad pass + else if no such user: + -> INVALID STATE + else: + * coreAuth::attemptAuth($name, $hashedPass) + if passed: + -> create local account + -> coreAuth::attachLocal($name, $dbname) + -> update local lock state, email, email confirmation + -> successful login. + else if failed: + -> whine about bad pass + else if no such user: + -> whine about no such user + +On registration request: +* load local account data + if local user exists: + -> whine about user already exists + else: + * coreAuth::attemptAuth($name, $hashedPass) + if passed: + -> whine or log in :) + else if failed: + -> whine about user already exists + else if no such user: + -> coreAuth::register($name) + -> coreAuth::setEmail($name, $email) + -> coreAuth::setPassword($name, $hashedPass) + -> create local account + -> coreAuth::attachLocal($name, $dbname) + -> update local lock state, email, email confirmation + -> successful login. + +See the diagramms made using dia ( http://www.gnome.org/projects/dia ): + - login.dia + - registration.dia + +== Edge implementation == + +The AuthPlugin interface was written with the idea of creating local accounts +automatically on login based on external data. + +Probably require some additional work on the login page to work in migration. + +Users logged in to a transitional account should see a notice about the +remaining unattached accounts. (This could be dismissed eg to show only +on the preferences page to minimize annoyance.) From this notice a list +of conflicting wikis can be produced for immediate link-and-login. + + +== Core implementation == + +Either this can be done as local PHP code accessing a database (potentially +via ssh tunnel for yaseo) or hit something over http/https. May want to +examine this a bit. + + +== Migration testing == + +First-stage migration can be tested offline to get some statistics. + + +== Messages and translations == + +Get the UI messages ready with some time before this goes live; we'll want +translations in the various languages ready to go. + + +== Permissions == + +Group memberships (hence on-wiki permissions) remain local; a sysop in +one place is not necessarily in another. + +We will have to make some changes to the way we handle the restricted +wikis however: the simplest thing would be to add a group on those wikis +for approved users, and shift the permissions over from 'user' to 'private' +or whatever. Then add some handy way for local sysops to privatise people, +rather than the cumbersome 'account by email'. + diff --git a/extract-data.sh b/extract-data.sh new file mode 100755 index 000000000..b0ee047a6 --- /dev/null +++ b/extract-data.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# +# Script to extract user data from the databases to csv files +# It takes a bit more than one hour to extract the data. +# +# Author: Brion Vibber + +cluster=`cat /etc/cluster` +for db in `cat /home/wikipedia/common/$cluster.dblist` +do + if [ "$cluster" == "yaseo" ] + then + server=dryas + else + if [ "$db" == "enwiki" ] + then + server=db4 + else + server=samuel + fi + fi + echo "User data from $db..." + echo "SELECT user_id, user_name, user_email, user_email_authenticated FROM user;" | \ + sql $db -h $server > $db-user.csv + echo "Counts from $db..." + echo "SELECT rev_user, count(*) FROM revision GROUP BY rev_user;" | \ + sql $db -h $server > $db-count.csv +done + + +# 1) SELECT user_id, user_name, user_email, user_email_authenticated FROM user; +# 2) SELECT rev_user, count(*) FROM revision GROUP BY rev_user; +# +# SELECT user_id, user_name, user_email, user_email_authenticated, COUNT(rev_user) as editcount +# FROM user LEFT OUTER JOIN revision ON user_id=rev_user GROUP BY user_id, rev_user; diff --git a/login.dia b/login.dia new file mode 100644 index 000000000..bfef0f4be Binary files /dev/null and b/login.dia differ diff --git a/migratePass0.php b/migratePass0.php new file mode 100644 index 000000000..e012b0191 --- /dev/null +++ b/migratePass0.php @@ -0,0 +1,87 @@ + disable account creations, password changes +// pass 0: +// * generate 'localuser' entries for each user on each wiki +// * generate 'globaluser' entries for each username +// --> enable + +require_once 'commandLine.inc'; + + +/** + * Copy user data for this wiki into the localuser table + */ +function migratePassZero() { + global $wgDBname; + $dbr = wfGetDB( DB_SLAVE ); // fixme for large dbs + + $start = microtime( true ); + + // We're going to run two queries side-by-side here. + // The first fetches user data from 'user' + // The second fetches edit counts from 'revision' + // + // We combine these into an unholy chimera and send it to the + // central authentication server, which in theory might be + // on another continent. + + $user = $dbr->tableName( 'user' ); + $revision = $dbr->tableName( 'revision' ); + $result = $dbr->query( + "SELECT + user_id, + user_name, + user_password, + user_newpassword, + user_email, + user_email_authenticated, + COUNT(rev_user) AS user_editcount + FROM $user + LEFT OUTER JOIN $revision ON user_id=rev_user + GROUP BY user_id" ); + + $migrated = 0; + while( $row = $dbr->fetchObject( $result ) ) { + CentralAuthUser::storeLocalData( $wgDBname, $row, $row->user_editcount ); + if( ++$migrated % 100 == 0 ) { + $delta = microtime( true ) - $start; + $rate = ($delta == 0.0) ? 0.0 : $migrated / $delta; + printf( "%d done in %0.1f secs (%0.3f accounts/sec).\n", + $migrated, $delta, $rate ); + } + } + $dbr->freeResult( $result ); + + $delta = microtime( true ) - $start; + $rate = ($delta == 0.0) ? 0.0 : $migrated / $delta; + printf( "%d done in %0.1f secs (%0.3f accounts/sec).\n", + $migrated, $delta, $rate ); +} + +function getEditCount( $userId ) { + return countEdits( $userId, 'revision', 'rev_user' ); +} + +function countEdits( $userId, $table, $field ) { + $dbr = wfGetDB( DB_SLAVE ); + $count = $dbr->selectField( $table, 'COUNT(*)', + array( $field => $userId ), + __METHOD__ ); + return intval( $count ); +} + +if( $wgCentralAuthState != 'premigrate' ) { + if( $wgCentralAuthState == 'testing' ) { + echo "WARNING: \$wgCentralAuthState is set to 'testing', generated data may be corrupt.\n"; + } else { + wfDie( "\$wgCentralAuthState is '$wgCentralAuthState', please set to 'premigrate' to prevent conflicts.\n" ); + } +} + +echo "CentralAuth migration pass 0:\n"; +echo "$wgDBname preparing migration data...\n"; +migratePassZero(); +echo "done.\n"; + +?> \ No newline at end of file diff --git a/migratePass1.php b/migratePass1.php new file mode 100644 index 000000000..d56df2230 --- /dev/null +++ b/migratePass1.php @@ -0,0 +1,39 @@ +select( + 'localuser', + array( 'lu_name' ), + '', + __METHOD__, + array( 'GROUP BY' => 'lu_name' ) ); + while( $row = $dbBackground->fetchObject( $result ) ) { + $name = $row->lu_name; + $central = new CentralAuthUser( $name ); + if( $central->storeAndMigrate() ) { + echo "Migrated '$name'\n"; + } + } + $dbBackground->freeResult( $result ); +} + +if( $wgCentralAuthState != 'premigrate' ) { + if( $wgCentralAuthState == 'testing' ) { + echo "WARNING: \$wgCentralAuthState is set to 'testing', generated data may be corrupt.\n"; + } else { + wfDie( "\$wgCentralAuthState is '$wgCentralAuthState', please set to 'premigrate'.\n" ); + } +} + +echo "CentralAuth migration pass 1:\n"; +echo "Finding accounts which can be migrated without interaction...\n"; +migratePassOne(); +echo "done.\n"; + +?> \ No newline at end of file diff --git a/registration.dia b/registration.dia new file mode 100644 index 000000000..e2fbd55b4 Binary files /dev/null and b/registration.dia differ diff --git a/renaming.txt b/renaming.txt new file mode 100644 index 000000000..93f5a301f --- /dev/null +++ b/renaming.txt @@ -0,0 +1,6 @@ +Renaming model: + +* Never change the established name of a user account +* To "rename", create a new account and migrate the settings. +* -question- -> what about reattribution? + diff --git a/sample-data.sql b/sample-data.sql new file mode 100644 index 000000000..356db9b65 --- /dev/null +++ b/sample-data.sql @@ -0,0 +1,10 @@ +insert +into globaluser + (gu_id,gu_name,gu_email,gu_email_authenticated, + gu_salt,gu_password,gu_locked,gu_hidden, + gu_registration) +values + (1, 'Duderino', 'dude@localhost', '20060719012345', + '34', MD5(CONCAT('34', '-', MD5('mycoolpass'))), 0, 0, + '20060719012345'); + diff --git a/stats.php b/stats.php new file mode 100644 index 000000000..5a75957c6 --- /dev/null +++ b/stats.php @@ -0,0 +1,106 @@ + \ No newline at end of file diff --git a/testpass.php b/testpass.php new file mode 100644 index 000000000..009321aec --- /dev/null +++ b/testpass.php @@ -0,0 +1,166 @@ +lu_name == $lastname ) { + $queue[] = $row; + } else { + firstPassUser( $dbw, $queue ); + $queue = array( $row ); + } + $lastname = $row->lu_name; + } + if( $queue ) { + firstPassUser( $dbw, $queue ); + } + mysql_free_result( $res ); + echo "Done.\n"; +} + +function firstPassUser($dbw, $rows) { + global $stats; + + $stats['all']++; + + $winner = false; + $attach = array(); + $unattach = array(); + $total = count($rows); + + if( $total == 1 ) { + $stats['singleton']++; + $winner = $rows[0]; + $attach[] = $winner; + } else { + // The winner is the one with the most edits, usually + $max = -1; + foreach( $rows as $row ) { + if( $row->lu_editcount > $max ) { + $winner = $row; + $max = $row->lu_editcount; + } + } + + // Do they all match? + $allMatch = true; + $allMatchOrEmpty = true; + $allMatchOrUnused = true; + $isConflict = false; + $winningMail = ($winner->lu_email == '' ? false : $winner->lu_email); + foreach( $rows as $row ) { + if( $row->lu_dbname == $winner->lu_dbname ) { + $attach[] = $row; + } else { + if( $row->lu_email !== $winningMail ) { + $allMatch = false; + if( $row->lu_email !== '' ) { + $allMatchOrEmpty = false; + } + if( $row->lu_editcount == 0 ) { + // Unused accounts are fair game for reclaiming + $attach[] = $row; + } else { + $allMatchOrUnused = false; + $unattach[] = $row; + $isConflict = true; + } + } else { + $attach[] = $row; + } + } + } + + if( $allMatch ) $stats['identical mail']++; + if( $allMatchOrEmpty) $stats['identical or empty mail']++; + if( $allMatchOrUnused) $stats['identical or unused']++; + if( $isConflict ) $stats['potential conflict']++; + } + + if( false === $winner ) { + var_dump( $rows ); + die ('wtf'); + } + + if( $winner->lu_editcount == 0 ) { + $stats['unused']++; + } + + $xname = addQuotes( $winner->lu_name ); + $xemail = addQuotes( $winner->lu_email ); + $xauthenticated = addQuotes( $winner->lu_email_authenticated ); + $sql = "INSERT IGNORE INTO globaluser (gu_name, gu_email, gu_email_authenticated) " . + "VALUES ($xname, $xemail, $xauthenticated)"; + doQuery( $sql, $dbw ); + + foreach( $attach as $row ) { + $xdbname = addQuotes( $row->lu_dbname ); + $sql = "UPDATE localuser SET lu_attached=1 " . + "WHERE lu_dbname=$xdbname AND lu_name=$xname"; + doQuery( $sql, $dbw ); + } + + // fixme: update the attached markers + + $matches = prettyList( $attach ); + $failures = prettyList( $unattach ); + echo "$xname {$total}x winner: [{$winner->lu_dbname}] attached: ($matches) loose: ($failures) $xemail\n"; +} + +function prettyList( $list ) { + return implode( ', ', array_map( 'prettyListItem', $list ) ); +} + +function prettyListItem( $item ) { + return $item->lu_dbname . '.' . $item->lu_editcount; +} + +$dbr = openDb(); +$dbw = openDb(); + +$stats = array( + 'all' => 0, + 'singleton' => 0, + 'identical mail' => 0, + 'identical or empty mail' => 0, + 'identical or unused' => 0, + 'potential conflict' => 0, + 'unused' => 0, +); + +firstPass($dbr, $dbw); + +var_dump( $stats ); + +?> \ No newline at end of file