thumbnails to be stored in a separate location to the source images.
* If config/ directory is not executable, the command to make it executable
now asks the user to cd to the correct directory
+* Add experimental new external authentication framework, ExternalAuth.
=== Bug fixes in 1.16 ===
'ExternalStoreDB' => 'includes/ExternalStoreDB.php',
'ExternalStoreHttp' => 'includes/ExternalStoreHttp.php',
'ExternalStore' => 'includes/ExternalStore.php',
+ 'ExternalUser' => 'includes/ExternalUser.php',
+ 'ExternalUser_vB' => 'includes/extauth/vB.php',
'FatalError' => 'includes/Exception.php',
'FakeTitle' => 'includes/FakeTitle.php',
'FauxRequest' => 'includes/WebRequest.php',
* );
*/
$wgPoolCounterConf = null;
+
+/**
+ * Use some particular type of external authentication. The specific
+ * authentication module you use will normally require some extra settings to
+ * be specified.
+ *
+ * null indicates no external authentication is to be used. Otherwise,
+ * "ExternalUser_$wgExternalAuthType" must be the name of a non-abstract class
+ * that extends ExternalUser.
+ *
+ * Core authentication modules can be found in includes/extauth/.
+ */
+$wgExternalAuthType = null;
+
+/**
+ * Configuration for the external authentication. This may include arbitrary
+ * keys that depend on the authentication mechanism. For instance,
+ * authentication against another web app might require that the database login
+ * info be provided. Check the file where your auth mechanism is defined for
+ * info on what to put here.
+ */
+$wgExternalAuthConfig = array();
+
+/**
+ * When should we automatically create local accounts when external accounts
+ * already exist, if using ExternalAuth? Can have three values: 'never',
+ * 'login', 'view'. 'view' requires the external database to support cookies,
+ * and implies 'login'.
+ *
+ * TODO: Implement 'view' (currently behaves like 'login').
+ */
+$wgAutocreatePolicy = 'login';
+
+/**
+ * Policies for how each preference is allowed to be changed, in the presence
+ * of external authentication. The keys are preference keys, e.g., 'password'
+ * or 'emailaddress' (see Preferences.php et al.). The value can be one of the
+ * following:
+ *
+ * - local: Allow changes to this pref through the wiki interface but only
+ * apply them locally (default).
+ * - semiglobal: Allow changes through the wiki interface and try to apply them
+ * to the foreign database, but continue on anyway if that fails.
+ * - global: Allow changes through the wiki interface, but only let them go
+ * through if they successfully update the foreign database.
+ * - message: Allow no local changes for linked accounts; replace the change
+ * form with a message provided by the auth plugin, telling the user how to
+ * change the setting externally (maybe providing a link, etc.). If the auth
+ * plugin provides no message for this preference, hide it entirely.
+ *
+ * Accounts that are not linked to an external account are never affected by
+ * this setting. You may want to look at $wgHiddenPrefs instead.
+ * $wgHiddenPrefs supersedes this option.
+ *
+ * TODO: Implement message, global.
+ */
+$wgAllowPrefChange = array();
--- /dev/null
+<?php
+
+# Copyright (C) 2009 Aryeh Gregor
+#
+# 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
+
+/**
+ * A class intended to supplement, and perhaps eventually replace, AuthPlugin.
+ * See: http://www.mediawiki.org/wiki/ExternalAuth
+ *
+ * The class represents a user whose data is in a foreign database. The
+ * database may have entirely different conventions from MediaWiki, but it's
+ * assumed to at least support the concept of a user id (possibly not an
+ * integer), a user name (possibly not meeting MediaWiki's username
+ * requirements), and a password.
+ */
+abstract class ExternalUser {
+ protected function __construct() {}
+
+ /**
+ * Wrappers around initFrom*().
+ */
+
+ /**
+ * @param $name string
+ * @return mixed ExternalUser, or false on failure
+ */
+ public static function newFromName( $name ) {
+ global $wgExternalAuthType;
+ if ( is_null( $wgExternalAuthType ) ) {
+ return false;
+ }
+ $class = "ExternalUser_$wgExternalAuthType";
+ $obj = new $class;
+ if ( !$obj->initFromName( $name ) ) {
+ return false;
+ }
+ return $obj;
+ }
+
+ /**
+ * @param $id string
+ * @return mixed ExternalUser, or false on failure
+ */
+ public static function newFromId( $id ) {
+ global $wgExternalAuthType;
+ if ( is_null( $wgExternalAuthType ) ) {
+ return false;
+ }
+ $class = "ExternalUser_$wgExternalAuthType";
+ $obj = new $class;
+ if ( !$obj->initFromId( $id ) ) {
+ return false;
+ }
+ return $obj;
+ }
+
+ /**
+ * @param $cookie string
+ * @return mixed ExternalUser, or false on failure
+ */
+ public static function newFromCookie( $cookie ) {
+ global $wgExternalAuthType;
+ if ( is_null( $wgExternalAuthType ) ) {
+ return false;
+ }
+ $class = "ExternalUser_$wgExternalAuthType";
+ $obj = new $class;
+ if ( !$obj->initFromCookie( $cookie ) ) {
+ return false;
+ }
+ return $obj;
+ }
+
+ /**
+ * Creates the object corresponding to the given User object, assuming the
+ * user exists on the wiki and is linked to an external account. If either
+ * of these is false, this will return false.
+ *
+ * This is a wrapper around newFromId().
+ *
+ * @param $user User
+ * @return mixed ExternalUser or false
+ */
+ public static function newFromUser( $user ) {
+ global $wgExternalAuthType;
+ if ( is_null( $wgExternalAuthType ) ) {
+ # Short-circuit to avoid database query in common case so no one
+ # kills me
+ return false;
+ }
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $id = $dbr->selectField( 'external_user', 'eu_external_id',
+ array( 'eu_wiki_id' => $user->getId() ), __METHOD__ );
+ if ( $id === false ) {
+ return false;
+ }
+ return self::newFromId( $id );
+ }
+
+ /**
+ * Given a name, which is a string exactly as input by the user in the
+ * login form but with whitespace stripped, initialize this object to be
+ * the corresponding ExternalUser. Return true if successful, otherwise
+ * false.
+ *
+ * @param $name string
+ * @return bool Success?
+ */
+ protected abstract function initFromName( $name );
+
+ /**
+ * Given an id, which was at some previous point in history returned by
+ * getId(), initialize this object to be the corresponding ExternalUser.
+ * Return true if successful, false otherwise.
+ *
+ * @param $id string
+ * @return bool Success?
+ */
+ protected abstract function initFromId( $id );
+
+ /**
+ * Given the user's cookie, initialize this object to the correct user if
+ * the cookie indicates that the user is logged into the external database.
+ * If successful, return true. If the external database doesn't support
+ * cookie-based authentication, or if the cookies don't belong to a
+ * logged-in user, return false.
+ *
+ * TODO: Actually use this.
+ *
+ * @param $cookie string
+ * @return bool Success?
+ */
+ protected function initFromCookie( $cookie ) {
+ return false;
+ }
+
+ /**
+ * This must return some identifier that stably, uniquely identifies the
+ * user. In a typical web application, this could be an integer
+ * representing the "user id". In other cases, it might be a string. In
+ * any event, the return value should be a string between 1 and 255
+ * characters in length; must uniquely identify the user in the foreign
+ * database; and, if at all possible, should be permanent.
+ *
+ * This will only ever be used to reconstruct this ExternalUser object via
+ * newFromId(). The resulting object in that case should correspond to the
+ * same user, even if details have changed in the interim (e.g., renames or
+ * preference changes).
+ *
+ * @return string
+ */
+ abstract public function getId();
+
+ /**
+ * This must return the name that the user would normally use for login to
+ * the external database. It is subject to no particular restrictions
+ * beyond rudimentary sanity, and in particular may be invalid as a
+ * MediaWiki username. It's used to auto-generate an account name that
+ * *is* valid for MediaWiki, either with or without user input, but
+ * basically is only a hint.
+ *
+ * @return string
+ */
+ abstract public function getName();
+
+ /**
+ * Is the given password valid for the external user? The password is
+ * provided in plaintext, with whitespace stripped but not otherwise
+ * modified.
+ *
+ * @param $password string
+ * @return bool
+ */
+ abstract public function authenticate( $password );
+
+ /**
+ * Retrieve the value corresponding to the given preference key. The most
+ * important values are:
+ *
+ * - emailaddress
+ * - language
+ *
+ * The value must meet MediaWiki's requirements for values of this type,
+ * and will be checked for validity before use. If the preference makes no
+ * sense for the backend, or it makes sense but is unset for this user, or
+ * is unrecognized, return null.
+ *
+ * $pref will never equal 'password', since passwords are usually hashed
+ * and cannot be directly retrieved. authenticate() is used for this
+ * instead.
+ *
+ * TODO: Currently this is only called for 'emailaddress'; generalize! Add
+ * some config option to decide which values are grabbed on user
+ * initialization.
+ *
+ * @param $pref string
+ * @return mixed
+ */
+ public function getPref( $pref ) {
+ return null;
+ }
+
+ /**
+ * Return an array of identifiers for all the foreign groups that this user
+ * has. The identifiers are opaque objects that only need to be
+ * specifiable by the administrator in LocalSettings.php when configuring
+ * $wgAutopromote. They may be, for instance, strings or integers.
+ *
+ * TODO: Support this in $wgAutopromote.
+ *
+ * @return array
+ */
+ public function getGroups() {
+ return array();
+ }
+
+ /**
+ * Given a preference key (e.g., 'emailaddress'), provide an HTML message
+ * telling the user how to change it in the external database. The
+ * administrator has specified that this preference cannot be changed on
+ * the wiki, and may only be changed in the foreign database. If no
+ * message is available, such as for an unrecognized preference, return
+ * false.
+ *
+ * TODO: Use this somewhere.
+ *
+ * @param $pref string
+ * @return mixed String or false
+ */
+ public static function prefMessage( $pref ) {
+ return false;
+ }
+
+ /**
+ * Set the given preference key to the given value. Two important
+ * preference keys that you might want to implement are 'password' and
+ * 'emailaddress'. If the set fails, such as because the preference is
+ * unrecognized or because the external database can't be changed right
+ * now, return false. If it succeeds, return true.
+ *
+ * If applicable, you should make sure to validate the new value against
+ * any constraints the external database may have, since MediaWiki may have
+ * more limited constraints (e.g., on password strength).
+ *
+ * TODO: Untested.
+ *
+ * @param $key string
+ * @param $value string
+ * @return bool Success?
+ */
+ public static function setPref( $key, $value ) {
+ return false;
+ }
+
+ /**
+ * Create a link for future reference between this object and the provided
+ * user_id. If the user was already linked, the old link will be
+ * overwritten.
+ *
+ * This is part of the core code and is not overridable by specific
+ * plugins. It's in this class only for convenience.
+ *
+ * @param $id int user_id
+ */
+ public final function link( $id ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->replace( 'external_user',
+ array( 'eu_wiki_id', 'eu_external_id' ),
+ array( 'eu_wiki_id' => $id,
+ 'eu_external_id' => $this->getId() ),
+ __METHOD__ );
+ }
+}
}
protected function saveOptions() {
+ global $wgAllowPrefChange;
+
+ $extuser = ExternalUser::newFromUser( $this );
+
$this->loadOptions();
$dbw = wfGetDB( DB_MASTER );
return;
foreach( $saveOptions as $key => $value ) {
- if ( ( is_null(self::getDefaultOption($key)) &&
+ # Don't bother storing default values
+ if ( ( is_null( self::getDefaultOption( $key ) ) &&
!( $value === false || is_null($value) ) ) ||
$value != self::getDefaultOption( $key ) ) {
$insert_rows[] = array(
'up_value' => $value,
);
}
+ if ( $extuser && isset( $wgAllowPrefChange[$key] ) ) {
+ switch ( $wgAllowPrefChange[$key] ) {
+ case 'local': case 'message':
+ break;
+ case 'semiglobal': case 'global':
+ $extuser->setPref( $key, $value );
+ }
+ }
}
$dbw->begin();
--- /dev/null
+<?php
+
+# Copyright (C) 2009 Aryeh Gregor
+#
+# 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
+
+/**
+ * This class supports the proprietary vBulletin forum system
+ * <http://www.vbulletin.com>, versions 3.5 and up. It calls no functions or
+ * code, only reads from the database. Example lines to put in
+ * LocalSettings.php:
+ *
+ * $wgExternalAuthType = 'vB';
+ * $wgExternalAuthConf = array(
+ * 'server' => 'localhost',
+ * 'username' => 'forum',
+ * 'password' => 'udE,jSqDJ<""p=fI.K9',
+ * 'dbname' => 'forum',
+ * 'tableprefix' => ''
+ * );
+ */
+class ExternalUser_vB extends ExternalUser {
+ private $mDb, $mRow;
+
+ protected function initFromName( $name ) {
+ return $this->initFromCond( array( 'username' => $name ) );
+ }
+
+ protected function initFromId( $id ) {
+ return $this->initFromCond( array( 'userid' => $id ) );
+ }
+
+ # initFromCookie() not yet implemented
+
+ private function initFromCond( $cond ) {
+ global $wgExternalAuthConf;
+
+ $this->mDb = new Database(
+ $wgExternalAuthConf['server'],
+ $wgExternalAuthConf['username'],
+ $wgExternalAuthConf['password'],
+ $wgExternalAuthConf['dbname'],
+ false, 0,
+ $wgExternalAuthConf['tableprefix']
+ );
+
+ $row = $this->mDb->selectRow(
+ 'user',
+ array( 'userid', 'username', 'password', 'salt', 'email', 'usergroupid',
+ 'membergroupids' ),
+ $cond,
+ __METHOD__
+ );
+ if ( !$row ) {
+ return false;
+ }
+ $this->mRow = $row;
+
+ return true;
+ }
+
+ public function getId() { return $this->mRow->userid; }
+ public function getName() { return $this->mRow->username; }
+
+ public function authenticate( $password ) {
+ return $this->mRow->password == md5( md5( $password )
+ . $this->mRow->salt );
+ }
+
+ public function getPref( $pref ) {
+ if ( $pref == 'emailaddress' && $this->mRow->email ) {
+ # TODO: only return if validated?
+ return $this->mRow->email;
+ }
+ return null;
+ }
+
+ public function getGroups() {
+ $groups = array( $this->mRow->usergroupid );
+ $groups = array_merge( $groups, explode( ',', $this->mRow->membergroupids ) );
+ $groups = array_unique( $groups );
+ return $groups;
+ }
+}
var $mLoginattempt, $mRemember, $mEmail, $mDomain, $mLanguage;
var $mSkipCookieCheck, $mReturnToQuery;
+ private $mExtUser = null;
+
/**
* Constructor
* @param WebRequest $request A WebRequest object passed by reference
$wgAuth->initUser( $u, $autocreate );
+ if ( $this->mExtUser ) {
+ $this->mExtUser->link( $u->getId() );
+ $email = $this->mExtUser->getPref( 'emailaddress' );
+ if ( $email && !$this->mEmail ) {
+ $u->setEmail( $email );
+ }
+ }
+
$u->setOption( 'rememberpassword', $this->mRemember ? 1 : 0 );
$u->saveSettings();
wfDebug( __METHOD__.": already logged in as {$this->mName}\n" );
return self::SUCCESS;
}
+
+ $this->mExtUser = ExternalUser::newFromName( $this->mName );
+
+ # TODO: Allow some magic here for invalid external names, e.g., let the
+ # user choose a different wiki name.
$u = User::newFromName( $this->mName );
if( is_null( $u ) || !User::isUsableName( $u->getName() ) ) {
return self::ILLEGAL;
* @return integer Status code
*/
function attemptAutoCreate( $user ) {
- global $wgAuth, $wgUser;
+ global $wgAuth, $wgUser, $wgAutocreatePolicy;
+
+ if ( $wgUser->isBlockedFromCreateAccount() ) {
+ wfDebug( __METHOD__.": user is blocked from account creation\n" );
+ return self::CREATE_BLOCKED;
+ }
+
/**
* If the external authentication plugin allows it, automatically cre-
* ate a new account for users that are externally defined but have not
* yet logged in.
*/
- if ( !$wgAuth->autoCreate() ) {
- return self::NOT_EXISTS;
- }
- if ( !$wgAuth->userExists( $user->getName() ) ) {
- wfDebug( __METHOD__.": user does not exist\n" );
- return self::NOT_EXISTS;
- }
- if ( !$wgAuth->authenticate( $user->getName(), $this->mPassword ) ) {
- wfDebug( __METHOD__.": \$wgAuth->authenticate() returned false, aborting\n" );
- return self::WRONG_PLUGIN_PASS;
- }
- if ( $wgUser->isBlockedFromCreateAccount() ) {
- wfDebug( __METHOD__.": user is blocked from account creation\n" );
- return self::CREATE_BLOCKED;
+ if ( $this->mExtUser ) {
+ # mExtUser is neither null nor false, so use the new ExternalAuth
+ # system.
+ if ( $wgAutocreatePolicy == 'never' ) {
+ return self::NOT_EXISTS;
+ }
+ if ( !$this->mExtUser->authenticate( $this->mPassword ) ) {
+ return self::WRONG_PLUGIN_PASS;
+ }
+ } else {
+ # Old AuthPlugin.
+ if ( !$wgAuth->autoCreate() ) {
+ return self::NOT_EXISTS;
+ }
+ if ( !$wgAuth->userExists( $user->getName() ) ) {
+ wfDebug( __METHOD__.": user does not exist\n" );
+ return self::NOT_EXISTS;
+ }
+ if ( !$wgAuth->authenticate( $user->getName(), $this->mPassword ) ) {
+ wfDebug( __METHOD__.": \$wgAuth->authenticate() returned false, aborting\n" );
+ return self::WRONG_PLUGIN_PASS;
+ }
}
wfDebug( __METHOD__.": creating account\n" );
function processLogin() {
global $wgUser, $wgAuth;
- switch ($this->authenticateUserData())
- {
+ switch ( $this->authenticateUserData() ) {
case self::SUCCESS:
# We've verified now, update the real record
if( (bool)$this->mRemember != (bool)$wgUser->getOption( 'rememberpassword' ) ) {
--- /dev/null
+CREATE TABLE /*_*/external_user (
+ -- Foreign key to user_id
+ eu_wiki_id int unsigned NOT NULL PRIMARY KEY,
+
+ -- Some opaque identifier provided by the external database
+ eu_external_id varchar(255) binary NOT NULL
+) /*$wgDBTableOptions*/;
+
+CREATE UNIQUE INDEX /*i*/eu_external_id ON /*_*/external_user (eu_external_id);
CREATE INDEX /*i*/el_index ON /*_*/externallinks (el_index(60));
+--
+-- Track external user accounts, if ExternalAuth is used
+--
+CREATE TABLE /*_*/external_user (
+ -- Foreign key to user_id
+ eu_wiki_id int unsigned NOT NULL PRIMARY KEY,
+
+ -- Some opaque identifier provided by the external database
+ eu_external_id varchar(255) binary NOT NULL
+) /*$wgDBTableOptions*/;
+
+CREATE UNIQUE INDEX /*i*/eu_external_id ON /*_*/external_user (eu_external_id);
+
+
--
-- Track interlanguage links
--
array( 'do_log_search_population' ),
array( 'add_field', 'logging', 'log_user_text', 'patch-log_user_text.sql' ),
array( 'add_table', 'l10n_cache', 'patch-l10n_cache.sql' ),
+ array( 'add_table', 'external_user', 'patch-external_user.sql' ),
),
'sqlite' => array(