r24113 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r24112‎ | r24113 | r24114 >
Date:07:38, 15 July 2007
Author:aaron
Status:old
Tags:
Comment:
*Commit this extension (Extension:ConfirmAccount). New accounts are made through a request and confirmation queue. Passwords confirmed and emailed to requesters. Direct account creation off by default, but can still be granted to some users. Not yet stable.
Modified paths:
  • /trunk/extensions/ConfirmAccount (added) (history)
  • /trunk/extensions/ConfirmAccount/ConfirmAccount.i18n.php (added) (history)
  • /trunk/extensions/ConfirmAccount/ConfirmAccount.pg.sql (added) (history)
  • /trunk/extensions/ConfirmAccount/ConfirmAccount.sql (added) (history)
  • /trunk/extensions/ConfirmAccount/ConfirmAccount_body.php (added) (history)
  • /trunk/extensions/ConfirmAccount/SpecialConfirmAccount.php (added) (history)

Diff [purge]

Index: trunk/extensions/ConfirmAccount/SpecialConfirmAccount.php
@@ -0,0 +1,79 @@
 2+<?php
 3+#(c) Aaron Schulz
 4+
 5+if ( !defined( 'MEDIAWIKI' ) ) {
 6+ echo "ConfirmAccount extension\n";
 7+ exit( 1 ) ;
 8+}
 9+
 10+# This extension needs email enabled!
 11+# Otherwise users can't get their passwords...
 12+if( !$wgEnableEmail ) {
 13+ echo "ConfirmAccount extension requeires \$wgEnableEmail set to true \n";
 14+ exit( 1 ) ;
 15+}
 16+
 17+$wgExtensionCredits['specialpage'][] = array(
 18+ 'name' => 'Confirm user accounts',
 19+ 'description' => 'Gives bureaucrats the ability to confirm account requests',
 20+ 'author' => 'Aaron Schulz'
 21+);
 22+
 23+# Set the person's bio as their userpage?
 24+$wgMakeUserPageFromBio = true;
 25+
 26+$wgGroupPermissions['*']['createaccount'] = false;
 27+$wgGroupPermissions['sysop']['createaccount'] = false;
 28+$wgGroupPermissions['bureaucrat']['confirmaccount'] = true;
 29+
 30+$wgAccountRequestThrottle = 1;
 31+
 32+# Internationalisation
 33+require_once( 'ConfirmAccount.i18n.php' );
 34+
 35+function efLoadConfirmAccountsMessages() {
 36+ global $wgMessageCache, $wgConfirmAccountMessages;
 37+
 38+ foreach( $wgConfirmAccountMessages as $key => $value ) {
 39+ $wgMessageCache->addMessages( $wgConfirmAccountMessages[$key], $key );
 40+ }
 41+}
 42+
 43+function efAddRequestLoginText( &$template ) {
 44+ efLoadConfirmAccountsMessages();
 45+
 46+ $template->set( 'header', wfMsgExt('requestacount-loginnotice', array('parse') ) );
 47+
 48+ return true;
 49+}
 50+
 51+function efCheckIfAccountNameIsPending( &$user, &$abortError ) {
 52+ efLoadConfirmAccountsMessages();
 53+ # If an account is made with name X, and one is pending with name X
 54+ # we will have problems if the pending one is later confirmed
 55+ $dbw = wfGetDB( DB_MASTER );
 56+ $dup = $dbw->selectField( 'account_requests', '1',
 57+ array( 'acr_name' => $user->getName() ),
 58+ __METHOD__ );
 59+
 60+ if ( $dup ) {
 61+ $abortError = wfMsgHtml('requestaccount-inuse');
 62+ }
 63+
 64+ return true;
 65+}
 66+
 67+# Register special page
 68+if ( !function_exists( 'extAddSpecialPage' ) ) {
 69+ require( dirname(__FILE__) . '/../ExtensionFunctions.php' );
 70+}
 71+# Request an account
 72+extAddSpecialPage( dirname(__FILE__) . '/ConfirmAccount_body.php', 'RequestAccount', 'RequestAccountPage' );
 73+# Confirm accounts
 74+extAddSpecialPage( dirname(__FILE__) . '/ConfirmAccount_body.php', 'ConfirmAccounts', 'ConfirmAccountsPage' );
 75+
 76+# Add notice of where to request an account
 77+$wgHooks['UserCreateForm'][] = 'efAddRequestLoginText';
 78+$wgHooks['UserLoginForm'][] = 'efAddRequestLoginText';
 79+# Check for collisions
 80+$wgHooks['AbortNewAccount'][] = 'efCheckIfAccountNameIsPending';
\ No newline at end of file
Index: trunk/extensions/ConfirmAccount/ConfirmAccount.sql
@@ -0,0 +1,46 @@
 2+-- (c) Aaron Schulz, 2007
 3+
 4+-- Table structure for table `Confirm account`
 5+-- Replace /*$wgDBprefix*/ with the proper prefix
 6+
 7+-- This stores all of our reviews,
 8+-- the corresponding tags are stored in the tag table
 9+CREATE TABLE /*$wgDBprefix*/account_requests (
 10+ acr_id int unsigned NOT NULL auto_increment,
 11+ -- Usernames must be unique, must not be in the form of
 12+ -- an IP address. _Shouldn't_ allow slashes or case
 13+ -- conflicts. Spaces are allowed, and are _not_ converted
 14+ -- to underscores like titles. See the User::newFromName() for
 15+ -- the specific tests that usernames have to pass.
 16+ acr_name varchar(255) binary NOT NULL default '',
 17+ -- Optional 'real name' to be displayed in credit listings
 18+ acr_real_name varchar(255) binary NOT NULL default '',
 19+ -- Note: email should be restricted, not public info.
 20+ -- Same with passwords.
 21+ acr_email tinytext NOT NULL,
 22+ -- Initially NULL; when a user's e-mail address has been
 23+ -- validated by returning with a mailed token, this is
 24+ -- set to the current timestamp.
 25+ acr_email_authenticated binary(14) default NULL,
 26+ -- Randomly generated token created when the e-mail address
 27+ -- is set and a confirmation test mail sent.
 28+ acr_email_token binary(32),
 29+ -- Expiration date for the user_email_token
 30+ acr_email_token_expires binary(14),
 31+ -- Timestamp of account registration.
 32+ -- Accounts predating this schema addition may contain NULL.
 33+ acr_registration char(14) NOT NULL,
 34+ -- A little about this user
 35+ acr_bio mediumblob default '',
 36+ -- Private info for reviewers to look at when considering request
 37+ acr_notes mediumblob default '',
 38+ -- Links to recognize/identify this user, CSV, may not be public
 39+ acr_urls mediumblob default '',
 40+ -- IP address
 41+ acr_ip VARCHAR(255) NULL default '',
 42+
 43+ PRIMARY KEY (acr_id),
 44+ UNIQUE KEY (acr_name),
 45+ INDEX (acr_registration),
 46+ INDEX (acr_email_token)
 47+) TYPE=InnoDB;
\ No newline at end of file
Index: trunk/extensions/ConfirmAccount/ConfirmAccount.pg.sql
@@ -0,0 +1,27 @@
 2+-- (c) Aaron Schulz, 2007
 3+
 4+-- Table structure for table `Confirm account`
 5+-- Replace /*$wgDBprefix*/ with the proper prefix
 6+
 7+BEGIN;
 8+
 9+CREATE SEQUENCE account_requests_acr_id_seq;
 10+CREATE TABLE account_requests (
 11+ acr_id INTEGER NOT NULL DEFAULT nextval('account_requests_acr_id_seq'),
 12+ acr_name TEXT NOT NULL UNIQUE,
 13+ acr_real_name TEXT,
 14+ acr_email TEXT,
 15+ acr_email_token CHAR(32),
 16+ acr_email_token_expires TIMESTAMPTZ,
 17+ acr_email_authenticated TIMESTAMPTZ,
 18+ acr_registration TIMESTAMPTZ,
 19+ acr_bio TEXT,
 20+ acr_notes TEXT,
 21+ acr_url TEXT,
 22+ acr_ip CIDR
 23+);
 24+
 25+CREATE INDEX acr_registration ON account_requests (acr_registration),
 26+CREATE INDEX acr_email_token ON account_requests (acr_email_token);
 27+
 28+COMMIT;
Index: trunk/extensions/ConfirmAccount/ConfirmAccount_body.php
@@ -0,0 +1,612 @@
 2+<?php
 3+
 4+if ( !defined( 'MEDIAWIKI' ) ) {
 5+ echo "ConfirmAccount extension\n";
 6+ exit( 1 );
 7+}
 8+
 9+# Add messages
 10+efLoadConfirmAccountsMessages();
 11+
 12+class RequestAccountPage extends SpecialPage {
 13+ function __construct() {
 14+ parent::__construct( 'RequestAccount' );
 15+ }
 16+
 17+ function execute( $subpage ) {
 18+ global $wgUser, $wgOut, $wgRequest, $action ;
 19+
 20+ if( $wgUser->isBlocked() ) {
 21+ $wgOut->blockedPage();
 22+ return;
 23+ }
 24+ if ( wfReadOnly() ) {
 25+ $wgOut->readOnlyPage();
 26+ return;
 27+ }
 28+
 29+ $this->setHeaders();
 30+
 31+ $this->mUsername = $wgRequest->getText( 'wpUsername' );
 32+ $this->mRealName = $wgRequest->getText( 'wpRealName' );
 33+ $this->mEmail = $wgRequest->getText( 'wpEmail' );
 34+ $this->mBio = $wgRequest->getText( 'wpBio' );
 35+ $this->mNotes = $wgRequest->getText( 'wpNotes' );
 36+ $this->mUrls = $wgRequest->getText( 'wpUrls' );
 37+ $emailCode = $wgRequest->getText( 'wpEmailToken' );
 38+
 39+ if ( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
 40+ $this->doSubmit();
 41+ } else if( $action == 'confirmemail' ) {
 42+ $this->confirmEmailToken( $emailCode );
 43+ } else {
 44+ $this->showForm();
 45+ }
 46+ }
 47+
 48+ function showForm( $msg='' ) {
 49+ global $wgOut, $wgUser, $wgTitle, $wgAuth;
 50+
 51+ $wgOut->setPagetitle( wfMsg( "requestaccount" ) );
 52+ # Output failure message
 53+ if( $msg ) {
 54+ $wgOut->addHTML( '<div class="errorbox">' . $msg . '</div><div class="visualClear"></div>' );
 55+ }
 56+ # Give notice to users that are logged in
 57+ if( $wgUser->getID() ) {
 58+ $wgOut->addWikiText( wfMsg( "requestaccount-dup" ) );
 59+ }
 60+
 61+ $wgOut->addWikiText( wfMsg( "requestacount-text" ) );
 62+
 63+ $action = $wgTitle->escapeLocalUrl( 'action=submit' );
 64+ $form = "<form name='accountrequest' action='$action' method='post'><fieldset>";
 65+ $form .= '<legend>' . wfMsg('requestacount-legend1') . '</legend>';
 66+ $form .= "<p>".wfMsgExt( 'requestacount-acc-text', array('parse') )."</p>\n";
 67+ $form .= '<table cellpadding=\'4\'>';
 68+ $form .= "<tr><td>".Xml::label( wfMsgHtml('username'), 'wpUsername' )."</td>";
 69+ $form .= "<td>".Xml::input( 'wpUsername', 30, $this->mUsername, array('id' => 'wpUsername') )."</td></tr>\n";
 70+ $form .= "<tr><td>".Xml::label( wfMsgHtml('requestaccount-email'), 'wpEmail' )."</td>";
 71+ $form .= "<td>".Xml::input( 'wpEmail', 30, $this->mEmail, array('id' => 'wpEmail') )."</td></tr>\n";
 72+ $form .= '</table></fieldset>';
 73+
 74+ $form .= '<fieldset>';
 75+ $form .= '<legend>' . wfMsg('requestacount-legend2') . '</legend>';
 76+ $form .= "<p>".wfMsgExt( 'requestaccount-bio-text', array('parse') )."</p>\n";
 77+ $form .= '<table cellpadding=\'4\'>';
 78+ $form .= "<tr><td>".Xml::label( wfMsgHtml('requestaccount-real'), 'wpRealName' )."</td>";
 79+ $form .= "<td>".Xml::input( 'wpRealName', 35, $this->mRealName, array('id' => 'wpRealName') )."</td></tr>\n";
 80+ $form .= '</table cellpadding=\'4\'>';
 81+ $form .= "<p>".wfMsgHtml('requestaccount-bio')."</p>";
 82+ $form .= "<p><textarea tabindex='1' name='wpBio' id='wpBio' rows='10' cols='80' style='width:100%'>" .
 83+ $this->mBio .
 84+ "</textarea></p>";
 85+ $form .= '</fieldset>';
 86+
 87+ $form .= '<fieldset>';
 88+ $form .= '<legend>' . wfMsg('requestacount-legend3') . '</legend>';
 89+ $form .= "<p>".wfMsgExt( 'requestacount-ext-text', array('parse') )."</p>\n";
 90+
 91+ $form .= "<p>".wfMsgHtml('requestaccount-notes')."</p>\n";
 92+ $form .= "<p><textarea tabindex='1' name='wpNotes' id='wpNotes' rows='3' cols='80' style='width:100%'>" .
 93+ $this->mNotes .
 94+ "</textarea></p>";
 95+ $form .= "<p>".wfMsgHtml('requestaccount-urls')."</p>\n";
 96+ $form .= "<p><textarea tabindex='1' name='wpUrls' id='wpUrls' rows='2' cols='80' style='width:100%'>" .
 97+ $this->mUrls .
 98+ "</textarea></p>";
 99+
 100+ $form .= Xml::hidden( 'title', $wgTitle->getPrefixedText() )."\n";
 101+ $form .= Xml::hidden( 'wpEditToken', $wgUser->editToken() )."\n";
 102+ $form .= '</fieldset>';
 103+
 104+ # Pseudo template for extensions
 105+ global $wgCaptcha;
 106+ if( isset($wgCaptcha) ) {
 107+ $template = new UsercreateTemplate();
 108+ $template->set( 'header', '' );
 109+ # Hook point to add captchas
 110+ $wgCaptcha->injectUserCreate( $template );
 111+ if( $template->data['header'] ) {
 112+ $form .= '<fieldset>';
 113+ $form .= $template->data['header'];
 114+ $form .= '</fieldset>';
 115+ }
 116+ }
 117+
 118+ $form .= "<p>".wfMsgExt( 'requestacount-confirm', array('parse') )."</p>\n";
 119+ $form .= "<p>".Xml::submitButton( wfMsgHtml( 'requestacount-submit') ) . "</p></fieldset>";
 120+ $form .= '</form>';
 121+
 122+ $wgOut->addHTML( $form );
 123+ }
 124+
 125+ function doSubmit() {
 126+ global $wgOut, $wgUser, $wgAuth, $wgAccountRequestThrottle, $wgMemc;
 127+
 128+ # Now create a dummy user ($u) and check if it is valid
 129+ $name = trim( $this->mUsername );
 130+ $u = User::newFromName( $name, 'creatable' );
 131+ if( is_null( $u ) ) {
 132+ $this->showForm( wfMsgHtml('noname') );
 133+ return;
 134+ }
 135+ # Check if already in use
 136+ if( 0 != $u->idForName() || $wgAuth->userExists( $u->getName() ) ) {
 137+ $this->showForm( wfMsgHtml('userexists') );
 138+ return;
 139+ }
 140+ # Check pending accounts for name use
 141+ $dbw = wfGetDB( DB_MASTER );
 142+ $dup = $dbw->selectField( 'account_requests', '1',
 143+ array( 'acr_name' => $u->getName() ),
 144+ __METHOD__ );
 145+ if( $dup ) {
 146+ $this->showForm( wfMsgHtml('requestaccount-inuse') );
 147+ return;
 148+ }
 149+ # Validate email address
 150+ if( !$u->isValidEmailAddr( $this->mEmail ) ) {
 151+ $this->showForm( wfMsgHtml('invalidemailaddress') );
 152+ return;
 153+ }
 154+ # Set some additional data so the AbortNewAccount hook can be
 155+ # used for more than just username validation
 156+ $u->setEmail( $this->mEmail );
 157+ $u->setRealName( $this->mRealName );
 158+ # Let captchas confirm
 159+ global $wgCaptcha;
 160+ if( isset($wgCaptcha) ) {
 161+ $abortError = '';
 162+ $wgCaptcha->confirmUserCreate( $u, &$abortError );
 163+ if( $abortError ) {
 164+ $this->showForm( $abortError );
 165+ return false;
 166+ }
 167+ }
 168+ # Insert into pending requests...
 169+ $dbw->begin();
 170+ $dbw->insert( 'account_requests',
 171+ array(
 172+ 'acr_name' => $u->mName,
 173+ 'acr_email' => $u->mEmail,
 174+ 'acr_real_name' => $u->mRealName,
 175+ 'acr_registration' => wfTimestampNow(),
 176+ 'acr_bio' => $this->mBio,
 177+ 'acr_notes' => $this->mNotes,
 178+ 'acr_urls' => $this->mUrls,
 179+ 'acr_ip' => wfGetIP() // Possible use for spam blocking
 180+ ),
 181+ __METHOD__
 182+ );
 183+ # Send confirmation, required!
 184+ $result = $this->sendConfirmationMail( $u );
 185+ if( WikiError::isError( $result ) ) {
 186+ $error = wfMsg( 'mailerror', htmlspecialchars( $result->getMessage() ) );
 187+ $this->showForm( $error );
 188+ $dbw->rollback(); // Nevermind
 189+ return false;
 190+ }
 191+ $dbw->commit();
 192+ # Now request spamming!
 193+ if( $wgAccountRequestThrottle && $wgUser->isPingLimitable() ) {
 194+ $key = wfMemcKey( 'acctrequest', 'ip', wfGetIP() );
 195+ $value = $wgMemc->incr( $key );
 196+ if( !$value ) {
 197+ $wgMemc->set( $key, 1, 86400 );
 198+ }
 199+ if( $value > $wgAccountRequestThrottle ) {
 200+ $this->throttleHit( $wgAccountRequestThrottle );
 201+ return false;
 202+ }
 203+ }
 204+ # Done!
 205+ $this->showSuccess();
 206+ }
 207+
 208+ function showSuccess() {
 209+ global $wgOut;
 210+
 211+ $wgOut->setPagetitle( wfMsg( "requestaccount" ) );
 212+ $wgOut->addWikiText( wfMsg( "requestaccount-sent" ) );
 213+
 214+ $wgOut->returnToMain();
 215+ }
 216+
 217+ /**
 218+ * @private
 219+ */
 220+ function throttleHit( $limit ) {
 221+ global $wgOut;
 222+
 223+ $wgOut->addWikiText( wfMsg( 'acct_request_throttle_hit', $limit ) );
 224+ }
 225+
 226+ function confirmEmailToken( $code ) {
 227+ global $wgUser, $wgOut;
 228+ # Confirm if this token is in the pending requests
 229+ $name = $this->requestFromEmailToken( $code );
 230+ if( $name !== false ) {
 231+ $this->confirmEmail( $name );
 232+ $message = $wgUser->isLoggedIn() ? 'confirmemail_loggedin' : 'confirmemail_success';
 233+ $wgOut->addWikiText( wfMsg( $message ) );
 234+ $wgOut->returnToMain();
 235+ return;
 236+ }
 237+ # Maybe the user confirmed after account was created...
 238+ $user = User::newFromConfirmationCode( $code );
 239+ if( is_object( $user ) ) {
 240+ if( $user->confirmEmail() ) {
 241+ $message = $wgUser->isLoggedIn() ? 'confirmemail_loggedin' : 'confirmemail_success';
 242+ $wgOut->addWikiText( wfMsg( $message ) );
 243+ if( !$wgUser->isLoggedIn() ) {
 244+ $title = SpecialPage::getTitleFor( 'Userlogin' );
 245+ $wgOut->returnToMain( true, $title->getPrefixedText() );
 246+ }
 247+ } else {
 248+ $wgOut->addWikiText( wfMsg( 'confirmemail_error' ) );
 249+ }
 250+ } else {
 251+ $wgOut->addWikiText( wfMsg( 'confirmemail_invalid' ) );
 252+ }
 253+ }
 254+
 255+ /**
 256+ * Get a request ID from an emailconfirm token
 257+ *
 258+ * @param Sring $code
 259+ * @returns Integer $reqID
 260+ */
 261+ function requestFromEmailToken( $code ) {
 262+ $dbr = wfGetDB( DB_SLAVE );
 263+ $reqID = $dbr->selectField( 'account_requests', 'acr_name',
 264+ array( 'acr_email_token' => md5( $code ),
 265+ 'acr_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
 266+ )
 267+ );
 268+ return $reqID;
 269+ }
 270+
 271+ /**
 272+ * Flag a user's email as confirmed in the db
 273+ *
 274+ * @param Sring $name
 275+ */
 276+ function confirmEmail( $name ) {
 277+ $dbw = wfGetDB( DB_MASTER );
 278+ $dbw->update( 'account_requests',
 279+ array( 'acr_email_authenticated' => wfTimestampNow() ),
 280+ array( 'acr_name' => $name ),
 281+ __METHOD__ );
 282+ }
 283+
 284+ /**
 285+ * Generate a new e-mail confirmation token and send a confirmation
 286+ * mail to the user's given address.
 287+ *
 288+ * @param User $user
 289+ * @return mixed True on success, a WikiError object on failure.
 290+ */
 291+ function sendConfirmationMail( $user ) {
 292+ global $wgContLang;
 293+ $expiration = null; // gets passed-by-ref and defined in next line.
 294+ $url = $this->confirmationTokenUrl( $user, $expiration );
 295+ return $user->sendMail( wfMsg( 'requestaccount-email-subj' ),
 296+ wfMsg( 'requestaccount-email-body',
 297+ wfGetIP(),
 298+ $user->getName(),
 299+ $url,
 300+ $wgContLang->timeanddate( $expiration, false ) ) );
 301+ }
 302+
 303+ /**
 304+ * Generate and store a new e-mail confirmation token, and return
 305+ * the URL the user can use to confirm.
 306+ * @param User $user
 307+ * @return string
 308+ * @private
 309+ */
 310+ function confirmationTokenUrl( $user, &$expiration ) {
 311+ $token = $this->confirmationToken( $user, $expiration );
 312+ $title = Title::makeTitle( NS_SPECIAL, 'RequestAccount' );
 313+ return $title->getFullUrl( 'action=confirmemail&wpEmailToken='.$token );
 314+ }
 315+
 316+ /**
 317+ * Generate, store, and return a new e-mail confirmation code.
 318+ * A hash (unsalted since it's used as a key) is stored.
 319+ * @param User $user
 320+ * @return string
 321+ * @private
 322+ */
 323+ function confirmationToken( $user, &$expiration ) {
 324+ $now = time();
 325+ $expires = $now + 7 * 24 * 60 * 60;
 326+ $expiration = wfTimestamp( TS_MW, $expires );
 327+
 328+ $token = $user->generateToken( $user->getName() . $user->getEmail() . $expires );
 329+ $hash = md5( $token );
 330+
 331+ $dbw = wfGetDB( DB_MASTER );
 332+ $dbw->update( 'account_requests',
 333+ array( 'acr_email_token' => $hash,
 334+ 'acr_email_token_expires' => $dbw->timestamp( $expires ) ),
 335+ array( 'acr_name' => $user->getName() ),
 336+ __METHOD__ );
 337+
 338+ return $token;
 339+ }
 340+
 341+}
 342+
 343+class ConfirmAccountsPage extends SpecialPage
 344+{
 345+
 346+ function __construct() {
 347+ SpecialPage::SpecialPage('ConfirmAccounts','confirmaccount');
 348+ }
 349+
 350+ function execute( $par ) {
 351+ global $wgRequest, $wgOut, $wgUser;
 352+
 353+ if( !$wgUser->isAllowed( 'confirmaccount' ) ) {
 354+ $wgOut->permissionRequired( 'confirmaccount' );
 355+ return;
 356+ }
 357+
 358+ $this->setHeaders();
 359+ # A target user
 360+ $this->acrID = $wgRequest->getIntOrNull( 'acrid' );
 361+ # For renaming to alot for collisions with other local requests
 362+ # that were added to some global $wgAuth system first.
 363+ $this->mUsername = $wgRequest->getIntOrNull( 'wpNewName' );
 364+
 365+ $this->skin = $wgUser->getSkin();
 366+
 367+ if ( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
 368+ $this->doSubmit();
 369+ } else if( $this->acrID ) {
 370+ $this->showForm();
 371+ } else {
 372+ $this->showList();
 373+ }
 374+ }
 375+
 376+ function doSubmit() {
 377+ global $wgOut, $wgTitle, $wgAuth, $action;
 378+
 379+ $row = $this->getRequest();
 380+ if( !$row ) {
 381+ $wgOut->addHTML( wfMsg('confirmaccount-badid') );
 382+ $wgOut->returnToMain( null, $wgTitle );
 383+ return;
 384+ }
 385+
 386+ if( $action == 'reject' ) {
 387+ $dbw = wfGetDB( DB_MASTER );
 388+ $dbw->delete( 'account_requests', array('acr_id' => $this->acrID), __METHOD__ );
 389+
 390+ $this->showSuccess( $action );
 391+ } else if( $action == 'accept' ) {
 392+ global $wgMakeUserPageFromBio;
 393+ # Check if the name is to be overridden
 394+ $name = $this->mUsername ? trim($this->mUsername) : $row->acr_name;
 395+ # Now create a dummy user ($u) and check if it is valid
 396+ $u = User::newFromName( $name, 'creatable' );
 397+ if( is_null( $u ) ) {
 398+ $this->showForm( wfMsgHtml('noname') );
 399+ return;
 400+ }
 401+ # Check if already in use
 402+ if( 0 != $u->idForName() || $wgAuth->userExists( $u->getName() ) ) {
 403+ $this->showForm( wfMsgHtml('userexists') );
 404+ return;
 405+ }
 406+
 407+ $pass = User::randomPassword();
 408+ if( !$wgAuth->addUser( $u, $pass, $row->acr_email, $row->acr_real_name ) ) {
 409+ $this->showForm( wfMsg( 'externaldberror' ) );
 410+ return false;
 411+ }
 412+ # Now that name is validated, create the stub account
 413+ $user = User::createNew( $name );
 414+ # VERY important to set email now. Otherwise user will have to request
 415+ # a new password at the login screen...
 416+ $user->setEmail( $row->acr_email );
 417+ $user->setRealName( $row->acr_real_name );
 418+ $user->setPassword( $pass );
 419+ $user->saveSettings(); // Save this stuff now
 420+ # Email this password
 421+ $user->sendMail( wfMsg( 'confirmaccount-email-subj' ),
 422+ wfMsg( 'confirmaccount-email-body',
 423+ $user->getName(),
 424+ $pass ) );
 425+ # Check if the user already confirmed email address
 426+ $dbw = wfGetDB( DB_MASTER );
 427+ $dbw->update( 'user',
 428+ array( 'user_email_authenticated' => $row->acr_email_authenticated ),
 429+ array( 'user_id' => $user->getID() ),
 430+ __METHOD__ );
 431+
 432+ # OK, now remove the request
 433+ $dbw->delete( 'account_requests', array('acr_id' => $this->acrID), __METHOD__ );
 434+
 435+ wfRunHooks( 'AddNewAccount', array( $user ) );
 436+ # Start up the user's brand new userpage
 437+ if( $wgMakeUserPageFromBio ) {
 438+ $userpage = new Article( $user->getUserPage() );
 439+ $userpage->doEdit( $row->acr_bio, wfMsg('confirmaccount-summary'), EDIT_NEW );
 440+ }
 441+
 442+ $this->showSuccess( $action, $user->getName() );
 443+ }
 444+ }
 445+
 446+ function showForm( $msg='' ) {
 447+ global $wgOut, $wgTitle, $wgUser;
 448+
 449+ # Output failure message
 450+ if( $msg ) {
 451+ $wgOut->addHTML( '<div class="errorbox">' . $msg . '</div><div class="visualClear"></div>' );
 452+ }
 453+
 454+ $listLink = $this->skin->makeKnownLinkObj( $wgTitle, wfMsgHtml( 'confirmaccount-back' ) );
 455+ $wgOut->setSubtitle( '<p>'.$listLink.'</p>' );
 456+
 457+ $row = $this->getRequest();
 458+ if( !$row ) {
 459+ $wgOut->addHTML( wfMsg('confirmaccount-badid') );
 460+ $wgOut->returnToMain( null, $wgTitle );
 461+ return;
 462+ }
 463+
 464+ $wgOut->addWikiText( wfMsg( "confirmacount-text" ) );
 465+
 466+ $action = $wgTitle->escapeLocalUrl( 'action=submit' );
 467+ $form = "<form name='accountconfirm' action='$action' method='post'><fieldset>";
 468+ $form .= '<legend>' . wfMsg('requestacount-legend1') . '</legend>';
 469+ $form .= '<table cellpadding=\'4\'>';
 470+ $form .= "<tr><td>".Xml::label( wfMsgHtml('username'), 'wpUsername' )."</td>";
 471+ $form .= "<td>".Xml::input( 'wpUsername', 30, $row->acr_name, array('id' => 'wpUsername') )."</td></tr>\n";
 472+
 473+ $econf = $row->acr_email_authenticated ? ' <strong>'.wfMsg('confirmaccount-econf').'</strong>' : '';
 474+ $form .= "<tr><td>".wfMsgHtml('requestaccount-email')."</td>";
 475+ $form .= "<td>".$row->acr_email.$econf."</td></tr>\n";
 476+ $form .= '</table></fieldset>';
 477+
 478+ $form .= '<fieldset>';
 479+ $form .= '<legend>' . wfMsg('requestacount-legend2') . '</legend>';
 480+ $form .= '<table cellpadding=\'4\'>';
 481+ $form .= "<tr><td>".wfMsgHtml('requestaccount-real')."</td>";
 482+ $form .= "<td>".$row->acr_real_name."</td></tr>\n";
 483+ $form .= '</table cellpadding=\'4\'>';
 484+ $form .= "<p>".wfMsgHtml('requestaccount-bio')."</p>";
 485+ $form .= "<p><textarea tabindex='1' readonly name='wpBio' id='wpBio' rows='10' cols='80' style='width:100%'>" .
 486+ $row->acr_bio .
 487+ "</textarea></p>";
 488+ $form .= '</fieldset>';
 489+
 490+ $form .= '<fieldset>';
 491+ $form .= '<legend>' . wfMsg('requestacount-legend3') . '</legend>';
 492+ $form .= "<p>".wfMsgHtml('requestaccount-notes')."</p>\n";
 493+ $form .= "<p><textarea tabindex='1' readonly name='wpNotes' id='wpNotes' rows='3' cols='80' style='width:100%'>" .
 494+ $row->acr_notes .
 495+ "</textarea></p>";
 496+ $form .= "<p>".wfMsgHtml('requestaccount-urls')."</p>\n";
 497+ $form .= "<p><textarea tabindex='1' readonly name='wpUrls' id='wpUrls' rows='2' cols='80' style='width:100%'>" .
 498+ $row->acr_urls .
 499+ "</textarea></p>";
 500+ $form .= '</fieldset>';
 501+
 502+ $form .= Xml::hidden( 'title', $wgTitle->getPrefixedText() )."\n";
 503+ $form .= Xml::hidden( 'action', 'accept' );
 504+ $form .= Xml::hidden( 'acrid', $row->acr_id );
 505+ $form .= Xml::hidden( 'wpEditToken', $wgUser->editToken() )."\n";
 506+
 507+ $form .= "<p>".wfMsgExt( 'confirmacount-confirm', array('parse') )."</p>\n";
 508+ $form .= '<div style="float: left">'.Xml::submitButton( wfMsgHtml( 'confirmacount-create') ).'</div>';
 509+ $form .= '</form>';
 510+ # Make deny use a separate form to avoid problems with people pressing enter
 511+ $form .= "<form name='accountreject' action='$action' method='post'>";
 512+ $form .= '<div style="float: right">'.Xml::submitButton( wfMsgHtml( 'confirmacount-deny') ) . "</div>";
 513+ $form .= Xml::hidden( 'title', $wgTitle->getPrefixedText() )."\n";
 514+ $form .= Xml::hidden( 'action', 'reject' );
 515+ $form .= Xml::hidden( 'acrid', $row->acr_id );
 516+ $form .= Xml::hidden( 'wpEditToken', $wgUser->editToken() )."\n";
 517+ $form .= '</form>';
 518+
 519+ $wgOut->addHTML( $form );
 520+ }
 521+
 522+ function getRequest() {
 523+ if( !$this->acrID )
 524+ return false;
 525+
 526+ $dbw = wfGetDB( DB_SLAVE );
 527+ $row = $dbw->selectRow( 'account_requests', '*', array('acr_id' => $this->acrID ), __METHOD__ );
 528+
 529+ return $row;
 530+ }
 531+
 532+ function showSuccess( $action, $name = NULL ) {
 533+ global $wgOut, $wgTitle;
 534+
 535+ $wgOut->setPagetitle( wfMsg( "requestaccount" ) );
 536+ if( $action == 'accept' )
 537+ $wgOut->addWikiText( wfMsg( "confirmaccount-acc", $name ) );
 538+ else
 539+ $wgOut->addWikiText( wfMsg( "confirmaccount-del" ) );
 540+
 541+ $wgOut->returnToMain( null, $wgTitle );
 542+ }
 543+
 544+ function showList() {
 545+ global $wgOut, $wgUser, $wgLang;
 546+
 547+ $pager = new ConfirmAccountsPager( $this, array() );
 548+ if ( $pager->getNumRows() ) {
 549+ $wgOut->addHTML( wfMsgExt('confirmacount-list', array('parse') ) );
 550+ $wgOut->addHTML( $pager->getNavigationBar() );
 551+ $wgOut->addHTML( "<ul>" . $pager->getBody() . "</ul>" );
 552+ $wgOut->addHTML( $pager->getNavigationBar() );
 553+ } else {
 554+ $wgOut->addHTML( wfMsgExt('confirmacount-none', array('parse')) );
 555+ }
 556+ }
 557+
 558+ function formatRow( $row ) {
 559+ global $wgLang, $wgUser;
 560+
 561+ $title = SpecialPage::getTitleFor( 'ConfirmAccounts' );
 562+ $link = $this->skin->makeKnownLinkObj( $title, wfMsg('confirmaccount-review'), 'acrid='.$row->acr_id );
 563+ $time = $wgLang->timeanddate( wfTimestamp(TS_MW, $row->acr_registration), true );
 564+
 565+ $r = '<li>';
 566+ $r .= $time." ($link)".'<br/>';
 567+ $r .= '<table cellspacing=\'1\' cellpadding=\'3\' border=\'1\' width=\'100%\'>';
 568+ $r .= '<tr><td><strong>'.wfMsg('confirmaccount-name').'</strong></td><td width=\'100%\'>'.$row->acr_name.'</td></tr>';
 569+ $r .= '<tr><td><strong>'.wfMsg('confirmaccount-real').'</strong></td><td width=\'100%\'>'.$row->acr_real_name.'</td></tr>';
 570+ $econf = $row->acr_email_authenticated ? ' <strong>'.wfMsg('confirmaccount-econf').'</strong>' : '';
 571+ $r .= '<tr><td><strong>'.wfMsg('confirmaccount-email').'</strong></td><td width=\'100%\'>'.$row->acr_email.$econf.'</td></tr>';
 572+ # Truncate this, blah blah...
 573+ $bio = substr( $row->acr_bio, 0, 500 );
 574+ $bio = strlen($bio) < strlen($row->acr_bio) ? "$bio . . ." : $bio;
 575+
 576+ $r .= '<tr><td><strong>'.wfMsg('confirmaccount-bio').'</strong></td><td width=\'100%\'><i>'.$bio.'</i></td></tr>';
 577+ $r .= '</table></li>';
 578+
 579+ return $r;
 580+ }
 581+}
 582+
 583+/**
 584+ * Query to list out stable versions for a page
 585+ */
 586+class ConfirmAccountsPager extends ReverseChronologicalPager {
 587+ public $mForm, $mConds;
 588+
 589+ function __construct( $form, $conds = array() ) {
 590+ $this->mForm = $form;
 591+ $this->mConds = $conds;
 592+ parent::__construct();
 593+ }
 594+
 595+ function formatRow( $row ) {
 596+ $block = new Block;
 597+ return $this->mForm->formatRow( $row );
 598+ }
 599+
 600+ function getQueryInfo() {
 601+ $conds = $this->mConds;
 602+ return array(
 603+ 'tables' => array('account_requests'),
 604+ 'fields' => 'acr_id,acr_name,acr_real_name,acr_registration,acr_email,acr_email_authenticated,
 605+ acr_bio,acr_notes,acr_urls',
 606+ 'conds' => $conds
 607+ );
 608+ }
 609+
 610+ function getIndexField() {
 611+ return 'acr_registration';
 612+ }
 613+}
Index: trunk/extensions/ConfirmAccount/ConfirmAccount.i18n.php
@@ -0,0 +1,88 @@
 2+<?php
 3+/**
 4+ * Internationalisation file for ConfirmAccount extension.
 5+ *
 6+ * @addtogroup Extensions
 7+*/
 8+
 9+$wgConfirmAccountMessages = array();
 10+
 11+$wgConfirmAccountMessages['en'] = array(
 12+ # Request account page
 13+ 'requestaccount' => 'Request account',
 14+ 'requestacount-text' => '\'\'\'Complete and submit the following form to request a user account\'\'\'.
 15+
 16+ Your email address will be sent a confirmation message once this request is submited. Please respond by clicking
 17+ on the confirmation link provided by the the email.
 18+
 19+ Once the account is approved, you will be emailed a notification message and the account will be usable at
 20+ [[Special:Userlogin]].',
 21+ 'requestaccount-dup' => '\'\'\'Note: You already are logged in with a registered account.\'\'\'',
 22+ 'requestacount-legend1' => 'User account:',
 23+ 'requestacount-legend2' => 'Personal information:',
 24+ 'requestacount-legend3' => 'Other information:',
 25+ 'requestacount-acc-text' => 'Your password will be emailed to you when your account is confirmed.',
 26+ 'requestacount-ext-text' => 'The following information is kept private and will only be used for this request.
 27+ You may want to list contacts such as fax/phone numbers to aid in identify confirmation.',
 28+ 'requestaccount-bio-text' => "Your biography will be set as the default content for your userpage. Try to include
 29+ any credentials. Make sure you are comfortable publishing such information. Your name can be changed via [[Special:Preferences]].",
 30+ 'requestaccount-real' => 'Real name:',
 31+ 'requestaccount-email' => 'Email address:',
 32+ 'requestaccount-bio' => 'Personal biography:',
 33+ 'requestaccount-notes' => 'Additional notes:',
 34+ 'requestaccount-urls' => 'Websites:',
 35+ 'requestaccount-inuse' => 'Username is already in use in a pending account request.',
 36+ 'requestacount-confirm' => 'Press the submit button below once you have confirmed that all the above is correct.',
 37+ 'requestacount-submit' => 'Request account',
 38+ 'requestaccount-sent' => 'Your account request has successfully been sent and is now pending review.',
 39+ 'requestaccount-email-subj' => '{{SITENAME}} e-mail address confirmation',
 40+ 'requestaccount-email-body' => 'Someone, probably you from IP address $1, has requested an
 41+account "$2" with this e-mail address on {{SITENAME}}.
 42+
 43+To confirm that this account really does belong to you on {{SITENAME}}, open this link in your browser:
 44+
 45+$3
 46+
 47+If the account is created, only you will be emailed the password. If this is *not* you, don\'t follow the link.
 48+This confirmation code will expire at $4.',
 49+
 50+ 'acct_request_throttle_hit' => "Sorry, you have already requested $1 accounts. You can't make any more requests.",
 51+
 52+ # Add to Special:Login
 53+ 'requestacount-loginnotice' => 'To obtain a user account, you must \'\'\'[[Special:RequestAccount|request one]].\'\'\'',
 54+
 55+ # Confirm account page
 56+ 'confirmaccounts' => 'Confirm account requests',
 57+ 'confirmacount-list' => 'Below is a list of account requests awaiting approval.
 58+ Approved accounts will be created and removed from this list. Rejected accounts will simply be deleted from this
 59+ list.',
 60+ 'confirmacount-text' => 'This is a pending request for a user account at \'\'\'{{SITENAME}}\'\'\'. Carefully
 61+ review and if needed, confirm, all the below information. Note that you can choose to create the account under a
 62+ different username. Use this only to avoid collisions with other names.
 63+
 64+ If you simply leave this page without confirming or denying this request, it will remain pending.',
 65+ 'confirmacount-none' => 'There are currently no pending account requests.',
 66+ 'confirmaccount-badid' => 'There is no pending request corresponding to the given ID. It may have already been handled.',
 67+ 'confirmaccount-back' => 'View pending account list',
 68+ 'confirmaccount-name' => 'Username',
 69+ 'confirmaccount-real' => 'Name',
 70+ 'confirmaccount-email' => 'Email',
 71+ 'confirmaccount-bio' => 'Biography',
 72+ 'confirmaccount-review' => 'Review this request in detail',
 73+ 'confirmacount-confirm' => 'Use the buttons below to irreversibly confirm this request and create the account or deny it.',
 74+ 'confirmaccount-econf' => '(confirmed)',
 75+ 'confirmacount-create' => 'Confirm (create account)',
 76+ 'confirmacount-deny' => 'Reject',
 77+ 'confirmaccount-acc' => 'Account request confirmed successfully; created new user account [[User:$1]].',
 78+ 'confirmaccount-del' => 'Account request rejected and successfully deleted.',
 79+ 'confirmaccount-summary' => 'Creating user page with biography of new user.',
 80+ 'confirmaccount-email-subj' => '{{SITENAME}} account request',
 81+ 'confirmaccount-email-body' => 'Your request for an account has been approved on {{SITENAME}}.
 82+
 83+Account name: $1
 84+
 85+Password: $2
 86+
 87+You may have been granted a slightly different name than requested. This could be due to name collisions
 88+or policy reasons. Also, please immediatly login, go to your preferences options, and set a new password.',
 89+);

Status & tagging log