From 549b1688cc1e87dc0711392b56e0c71f1e8615c2 Mon Sep 17 00:00:00 2001 From: Roan Kattouw Date: Sat, 28 Mar 2009 19:08:47 +0000 Subject: [PATCH] Redo r48746 (API userrights, reverted in r48909 and r48910) in a way that doesn't break CentralAuth. Basically, this works around PHP's inability (at least in < 5.3) to override static methods by adding a hook. Changes to CentralAuth in next commit. --- RELEASE-NOTES | 1 + docs/hooks.txt | 7 + includes/AutoLoader.php | 1 + includes/User.php | 108 +++++++++++++++ includes/api/ApiMain.php | 1 + includes/api/ApiQueryRecentChanges.php | 11 +- includes/api/ApiQueryUsers.php | 51 ++++++- includes/api/ApiUserrights.php | 117 ++++++++++++++++ includes/specials/SpecialUserrights.php | 177 +++++++----------------- 9 files changed, 342 insertions(+), 132 deletions(-) create mode 100644 includes/api/ApiUserrights.php diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 7ddd8f81b6..9f51e2e99a 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -345,6 +345,7 @@ it from source control: http://www.mediawiki.org/wiki/Download_from_SVN when someone tries to use them * BREAKING CHANGE: action=purge requires write rights and, for anonymous users, a POST request +* (bug 15935) Added action=userrights to add/remove users to/from groups * (bug 18099) Using appendtext to edit a non-existent page causes an interface message to be included in the page text * Added uiprop=changeablegroups to meta=userinfo diff --git a/docs/hooks.txt b/docs/hooks.txt index f973d6b847..286ed904d5 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -1439,6 +1439,13 @@ $addergroups : Array of groups that the user is in 'add-self' => array( addablegroups to self ), 'remove-self' => array( removable groups from self ) ) + +'UserRightsLogEntry': before a log entry is added by UserrightsPage::addLogEntry(). If you use this hook to add your own log entries, you can return false to prevent the usual log entry from being added +$user: User or UserrightsProxy object +$oldGroups: Array of groups the user was a member of before the change +$newGroups: Array of groups the user is a member of after the change +$reason: User-provided summary + 'UserRetrieveNewTalks': called when retrieving "You have new messages!" message(s) $user: user retrieving new talks messages $talks: array of new talks page(s) diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 9404024c8d..8edefe54f5 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -292,6 +292,7 @@ $wgAutoloadLocalClasses = array( 'ApiRollback' => 'includes/api/ApiRollback.php', 'ApiUnblock' => 'includes/api/ApiUnblock.php', 'ApiUndelete' => 'includes/api/ApiUndelete.php', + 'ApiUserrights' => 'includes/api/ApiUserrights.php', 'ApiWatch' => 'includes/api/ApiWatch.php', 'Services_JSON' => 'includes/api/ApiFormatJson_json.php', 'Services_JSON_Error' => 'includes/api/ApiFormatJson_json.php', diff --git a/includes/User.php b/includes/User.php index cc861ad450..4798294079 100644 --- a/includes/User.php +++ b/includes/User.php @@ -3231,6 +3231,114 @@ class User { return $text; } } + + /** + * Returns an array of the groups that a particular group can add/remove. + * + * @param $group String: the group to check for whether it can add/remove + * @return Array array( 'add' => array( addablegroups ), + * 'remove' => array( removablegroups ), + * 'add-self' => array( addablegroups to self), + * 'remove-self' => array( removable groups from self) ) + */ + static function changeableByGroup( $group ) { + global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf; + + $groups = array( 'add' => array(), 'remove' => array(), 'add-self' => array(), 'remove-self' => array() ); + if( empty($wgAddGroups[$group]) ) { + // Don't add anything to $groups + } elseif( $wgAddGroups[$group] === true ) { + // You get everything + $groups['add'] = self::getAllGroups(); + } elseif( is_array($wgAddGroups[$group]) ) { + $groups['add'] = $wgAddGroups[$group]; + } + + // Same thing for remove + if( empty($wgRemoveGroups[$group]) ) { + } elseif($wgRemoveGroups[$group] === true ) { + $groups['remove'] = self::getAllGroups(); + } elseif( is_array($wgRemoveGroups[$group]) ) { + $groups['remove'] = $wgRemoveGroups[$group]; + } + + // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility + if( empty($wgGroupsAddToSelf['user']) || $wgGroupsAddToSelf['user'] !== true ) { + foreach($wgGroupsAddToSelf as $key => $value) { + if( is_int($key) ) { + $wgGroupsAddToSelf['user'][] = $value; + } + } + } + + if( empty($wgGroupsRemoveFromSelf['user']) || $wgGroupsRemoveFromSelf['user'] !== true ) { + foreach($wgGroupsRemoveFromSelf as $key => $value) { + if( is_int($key) ) { + $wgGroupsRemoveFromSelf['user'][] = $value; + } + } + } + + // Now figure out what groups the user can add to him/herself + if( empty($wgGroupsAddToSelf[$group]) ) { + } elseif( $wgGroupsAddToSelf[$group] === true ) { + // No idea WHY this would be used, but it's there + $groups['add-self'] = User::getAllGroups(); + } elseif( is_array($wgGroupsAddToSelf[$group]) ) { + $groups['add-self'] = $wgGroupsAddToSelf[$group]; + } + + if( empty($wgGroupsRemoveFromSelf[$group]) ) { + } elseif( $wgGroupsRemoveFromSelf[$group] === true ) { + $groups['remove-self'] = User::getAllGroups(); + } elseif( is_array($wgGroupsRemoveFromSelf[$group]) ) { + $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group]; + } + + return $groups; + } + + /** + * Returns an array of groups that this user can add and remove + * @return Array array( 'add' => array( addablegroups ), + * 'remove' => array( removablegroups ), + * 'add-self' => array( addablegroups to self), + * 'remove-self' => array( removable groups from self) ) + */ + function changeableGroups() { + if( $this->isAllowed( 'userrights' ) ) { + // This group gives the right to modify everything (reverse- + // compatibility with old "userrights lets you change + // everything") + // Using array_merge to make the groups reindexed + $all = array_merge( User::getAllGroups() ); + return array( + 'add' => $all, + 'remove' => $all, + 'add-self' => array(), + 'remove-self' => array() + ); + } + + // Okay, it's not so simple, we will have to go through the arrays + $groups = array( + 'add' => array(), + 'remove' => array(), + 'add-self' => array(), + 'remove-self' => array() ); + $addergroups = $this->getEffectiveGroups(); + + foreach ($addergroups as $addergroup) { + $groups = array_merge_recursive( + $groups, $this->changeableByGroup($addergroup) + ); + $groups['add'] = array_unique( $groups['add'] ); + $groups['remove'] = array_unique( $groups['remove'] ); + $groups['add-self'] = array_unique( $groups['add-self'] ); + $groups['remove-self'] = array_unique( $groups['remove-self'] ); + } + return $groups; + } /** * Increment the user's edit-count field. diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index 3fa0d72f2b..42aaacc8c0 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -80,6 +80,7 @@ class ApiMain extends ApiBase { 'watch' => 'ApiWatch', 'patrol' => 'ApiPatrol', 'import' => 'ApiImport', + 'userrights' => 'ApiUserrights', ); /** diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php index 9dacb86c8e..94b1feeb52 100644 --- a/includes/api/ApiQueryRecentChanges.php +++ b/includes/api/ApiQueryRecentChanges.php @@ -43,12 +43,13 @@ class ApiQueryRecentChanges extends ApiQueryBase { private $fld_comment = false, $fld_user = false, $fld_flags = false, $fld_timestamp = false, $fld_title = false, $fld_ids = false, $fld_sizes = false; - + /** + * Get an array mapping token names to their handler functions. + * The prototype for a token function is func($pageid, $title, $rc) + * it should return a token or false (permission denied) + * @return array(tokenname => function) + */ protected function getTokenFunctions() { - // tokenname => function - // function prototype is func($pageid, $title, $rev) - // should return token or false - // Don't call the hooks twice if(isset($this->tokenFunctions)) return $this->tokenFunctions; diff --git a/includes/api/ApiQueryUsers.php b/includes/api/ApiQueryUsers.php index 51fa44e368..9f51c75705 100644 --- a/includes/api/ApiQueryUsers.php +++ b/includes/api/ApiQueryUsers.php @@ -39,6 +39,36 @@ if (!defined('MEDIAWIKI')) { public function __construct($query, $moduleName) { parent :: __construct($query, $moduleName, 'us'); } + + /** + * Get an array mapping token names to their handler functions. + * The prototype for a token function is func($user) + * it should return a token or false (permission denied) + * @return array(tokenname => function) + */ + protected function getTokenFunctions() { + // Don't call the hooks twice + if(isset($this->tokenFunctions)) + return $this->tokenFunctions; + + // If we're in JSON callback mode, no tokens can be obtained + if(!is_null($this->getMain()->getRequest()->getVal('callback'))) + return array(); + + $this->tokenFunctions = array( + 'userrights' => array( 'ApiQueryUsers', 'getUserrightsToken' ), + ); + wfRunHooks('APIQueryUsersTokens', array(&$this->tokenFunctions)); + return $this->tokenFunctions; + } + + public static function getUserrightsToken($user) + { + global $wgUser; + // Since the permissions check for userrights is non-trivial, + // don't bother with it here + return $wgUser->editToken($user->getName()); + } public function execute() { $params = $this->extractRequestParams(); @@ -115,6 +145,18 @@ if (!defined('MEDIAWIKI')) { } if(isset($this->prop['emailable']) && $user->canReceiveEmail()) $data[$name]['emailable'] = ''; + if(!is_null($params['token'])) + { + $tokenFunctions = $this->getTokenFunctions(); + foreach($params['token'] as $t) + { + $val = call_user_func($tokenFunctions[$t], $user); + if($val === false) + $this->setWarning("Action '$t' is not allowed for the current user"); + else + $data[$name][$t . 'token'] = $val; + } + } } } // Second pass: add result data to $retval @@ -153,7 +195,11 @@ if (!defined('MEDIAWIKI')) { ), 'users' => array( ApiBase :: PARAM_ISMULTI => true - ) + ), + 'token' => array( + ApiBase :: PARAM_TYPE => array_keys($this->getTokenFunctions()), + ApiBase :: PARAM_ISMULTI => true + ), ); } @@ -167,7 +213,8 @@ if (!defined('MEDIAWIKI')) { ' registration - adds the user\'s registration timestamp', ' emailable - tags if the user can and wants to receive e-mail through [[Special:Emailuser]]', ), - 'users' => 'A list of users to obtain the same information for' + 'users' => 'A list of users to obtain the same information for', + 'token' => 'Which tokens to obtain for each user', ); } diff --git a/includes/api/ApiUserrights.php b/includes/api/ApiUserrights.php new file mode 100644 index 0000000000..9e6c9f16f3 --- /dev/null +++ b/includes/api/ApiUserrights.php @@ -0,0 +1,117 @@ +.@home.nl + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiBase.php"); +} + + +/** + * @ingroup API + */ +class ApiUserrights extends ApiBase { + + public function __construct($main, $action) { + parent :: __construct($main, $action); + } + + public function execute() { + global $wgUser; + $params = $this->extractRequestParams(); + if(is_null($params['user'])) + $this->dieUsageMsg(array('missingparam', 'user')); + $user = User::newFromName($params['user']); + if($user->isAnon()) + $this->dieUsageMsg(array('nosuchuser', $params['user'])); + if(is_null($params['token'])) + $this->dieUsageMsg(array('missingparam', 'token')); + if(!$wgUser->matchEditToken($params['token'], $user->getName())) + $this->dieUsageMsg(array('sessionfailure')); + + $r['user'] = $user->getName(); + list($r['added'], $r['removed']) = + UserrightsPage::doSaveUserGroups( + $user, (array)$params['add'], + (array)$params['remove'], $params['reason']); + + $this->getResult()->setIndexedTagName($r['added'], 'group'); + $this->getResult()->setIndexedTagName($r['removed'], 'group'); + $this->getResult()->addValue(null, $this->getModuleName(), $r); + } + + public function mustBePosted() { + return true; + } + + public function isWriteMode() { + return true; + } + + public function getAllowedParams() { + return array ( + 'user' => array( + ApiBase :: PARAM_TYPE => 'user' + ), + 'add' => array( + ApiBase :: PARAM_TYPE => User::getAllGroups(), + ApiBase :: PARAM_ISMULTI => true + ), + 'remove' => array( + ApiBase :: PARAM_TYPE => User::getAllGroups(), + ApiBase :: PARAM_ISMULTI => true + ), + 'token' => null, + 'reason' => array( + ApiBase :: PARAM_DFLT => '' + ) + ); + } + + public function getParamDescription() { + return array ( + 'user' => 'User name', + 'add' => 'Add the user to these groups', + 'remove' => 'Remove the user from these groups', + 'token' => 'A userrights token previously retrieved through list=users', + 'reason' => 'Reason for the change', + ); + } + + public function getDescription() { + return array( + 'Add/remove a user to/from groups', + ); + } + + protected function getExamples() { + return array ( + 'api.php?action=userrights&user=FooBot&add=bot&remove=sysop|bureaucrat&token=123ABC' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id$'; + } +} diff --git a/includes/specials/SpecialUserrights.php b/includes/specials/SpecialUserrights.php index 5e8d42ad0f..bee4320e05 100644 --- a/includes/specials/SpecialUserrights.php +++ b/includes/specials/SpecialUserrights.php @@ -149,29 +149,46 @@ class UserrightsPage extends SpecialPage { $removegroup[] = $group; } } + self::doSaveUserGroups( $user, $addgroup, $removegroup, $reason ); + } + + /** + * Save user groups changes in the database. + * + * @param $user User object + * @param $add Array of groups to add + * @param $remove Array of groups to remove + * @param $reason String: reason for group change + * @return Array: Tuple of added, then removed groups + */ + static function doSaveUserGroups( $user, $add, $remove, $reason = '' ) { + global $wgUser; // Validate input set... - $changeable = $this->changeableGroups(); - $addable = array_merge( $changeable['add'], $this->isself ? $changeable['add-self'] : array() ); - $removable = array_merge( $changeable['remove'], $this->isself ? $changeable['remove-self'] : array() ); + $isself = ($user->getName() == $wgUser->getName()); + $groups = $user->getGroups(); + $changeable = $wgUser->changeableGroups(); + $addable = array_merge( $changeable['add'], $isself ? $changeable['add-self'] : array() ); + $removable = array_merge( $changeable['remove'], $isself ? $changeable['remove-self'] : array() ); - $removegroup = array_unique( - array_intersect( (array)$removegroup, $removable ) ); - $addgroup = array_unique( - array_intersect( (array)$addgroup, $addable ) ); + $remove = array_unique( + array_intersect( (array)$remove, $removable, $groups ) ); + $add = array_unique( array_diff( + array_intersect( (array)$add, $addable ), + $groups ) ); $oldGroups = $user->getGroups(); $newGroups = $oldGroups; // remove then add groups - if( $removegroup ) { - $newGroups = array_diff($newGroups, $removegroup); - foreach( $removegroup as $group ) { + if( $remove ) { + $newGroups = array_diff($newGroups, $remove); + foreach( $remove as $group ) { $user->removeGroup( $group ); } } - if( $addgroup ) { - $newGroups = array_merge($newGroups, $addgroup); - foreach( $addgroup as $group ) { + if( $add ) { + $newGroups = array_merge($newGroups, $add); + foreach( $add as $group ) { $user->addGroup( $group ); } } @@ -182,29 +199,34 @@ class UserrightsPage extends SpecialPage { wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) ); wfDebug( 'newGroups: ' . print_r( $newGroups, true ) ); - if( $user instanceof User ) { - // hmmm - wfRunHooks( 'UserRights', array( &$user, $addgroup, $removegroup ) ); - } + wfRunHooks( 'UserRights', array( &$user, $add, $remove ) ); if( $newGroups != $oldGroups ) { - $this->addLogEntry( $user, $oldGroups, $newGroups ); + self::addLogEntry( $user, $oldGroups, $newGroups, $reason ); } + return array( $add, $remove ); } + /** * Add a rights log entry for an action. */ - function addLogEntry( $user, $oldGroups, $newGroups ) { - global $wgRequest; + static function addLogEntry( $user, $oldGroups, $newGroups, $reason ) { + // Just overriding addLogEntry in a subclass would be cleaner, + // but that requires PHP 5.3 (late static bindings) + if( !wfRunHooks( 'UserRightsLogEntry', array( $user, $oldGroups, + $newGroups, $reason ) ) ) { + return; + } + $log = new LogPage( 'rights' ); $log->addEntry( 'rights', $user->getUserPage(), - $wgRequest->getText( 'user-reason' ), + $reason, array( - $this->makeGroupNameListForLog( $oldGroups ), - $this->makeGroupNameListForLog( $newGroups ) + self::makeGroupNameListForLog( $oldGroups ), + self::makeGroupNameListForLog( $newGroups ) ) ); } @@ -293,7 +315,7 @@ class UserrightsPage extends SpecialPage { return $user; } - function makeGroupNameList( $ids ) { + static function makeGroupNameList( $ids ) { if( empty( $ids ) ) { return wfMsgForContent( 'rightsnone' ); } else { @@ -301,11 +323,11 @@ class UserrightsPage extends SpecialPage { } } - function makeGroupNameListForLog( $ids ) { + static function makeGroupNameListForLog( $ids ) { if( empty( $ids ) ) { return ''; } else { - return $this->makeGroupNameList( $ids ); + return self::makeGroupNameList( $ids ); } } @@ -511,111 +533,16 @@ class UserrightsPage extends SpecialPage { } /** - * Returns an array of the groups that the user can add/remove. + * Returns $wgUser->changeableGroups() * * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) , 'add-self' => array( addablegroups to self), 'remove-self' => array( removable groups from self) ) */ function changeableGroups() { global $wgUser; - - if( $wgUser->isAllowed( 'userrights' ) ) { - // This group gives the right to modify everything (reverse- - // compatibility with old "userrights lets you change - // everything") - // Using array_merge to make the groups reindexed - $all = array_merge( User::getAllGroups() ); - return array( - 'add' => $all, - 'remove' => $all, - 'add-self' => array(), - 'remove-self' => array() - ); - } - - // Okay, it's not so simple, we will have to go through the arrays - $groups = array( - 'add' => array(), - 'remove' => array(), - 'add-self' => array(), - 'remove-self' => array() ); - $addergroups = $wgUser->getEffectiveGroups(); - - foreach ($addergroups as $addergroup) { - $groups = array_merge_recursive( - $groups, $this->changeableByGroup($addergroup) - ); - $groups['add'] = array_unique( $groups['add'] ); - $groups['remove'] = array_unique( $groups['remove'] ); - $groups['add-self'] = array_unique( $groups['add-self'] ); - $groups['remove-self'] = array_unique( $groups['remove-self'] ); - } - + $groups = $wgUser->changeableGroups(); // Run a hook because we can - wfRunHooks( 'UserrightsChangeableGroups', array( $this, $wgUser, $addergroups, &$groups ) ); - - return $groups; - } - - /** - * Returns an array of the groups that a particular group can add/remove. - * - * @param $group String: the group to check for whether it can add/remove - * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) , 'add-self' => array( addablegroups to self), 'remove-self' => array( removable groups from self) ) - */ - private function changeableByGroup( $group ) { - global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf; - - $groups = array( 'add' => array(), 'remove' => array(), 'add-self' => array(), 'remove-self' => array() ); - if( empty($wgAddGroups[$group]) ) { - // Don't add anything to $groups - } elseif( $wgAddGroups[$group] === true ) { - // You get everything - $groups['add'] = User::getAllGroups(); - } elseif( is_array($wgAddGroups[$group]) ) { - $groups['add'] = $wgAddGroups[$group]; - } - - // Same thing for remove - if( empty($wgRemoveGroups[$group]) ) { - } elseif($wgRemoveGroups[$group] === true ) { - $groups['remove'] = User::getAllGroups(); - } elseif( is_array($wgRemoveGroups[$group]) ) { - $groups['remove'] = $wgRemoveGroups[$group]; - } - - // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility - if( empty($wgGroupsAddToSelf['user']) || $wgGroupsAddToSelf['user'] !== true ) { - foreach($wgGroupsAddToSelf as $key => $value) { - if( is_int($key) ) { - $wgGroupsAddToSelf['user'][] = $value; - } - } - } - - if( empty($wgGroupsRemoveFromSelf['user']) || $wgGroupsRemoveFromSelf['user'] !== true ) { - foreach($wgGroupsRemoveFromSelf as $key => $value) { - if( is_int($key) ) { - $wgGroupsRemoveFromSelf['user'][] = $value; - } - } - } - - // Now figure out what groups the user can add to him/herself - if( empty($wgGroupsAddToSelf[$group]) ) { - } elseif( $wgGroupsAddToSelf[$group] === true ) { - // No idea WHY this would be used, but it's there - $groups['add-self'] = User::getAllGroups(); - } elseif( is_array($wgGroupsAddToSelf[$group]) ) { - $groups['add-self'] = $wgGroupsAddToSelf[$group]; - } - - if( empty($wgGroupsRemoveFromSelf[$group]) ) { - } elseif( $wgGroupsRemoveFromSelf[$group] === true ) { - $groups['remove-self'] = User::getAllGroups(); - } elseif( is_array($wgGroupsRemoveFromSelf[$group]) ) { - $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group]; - } - + wfRunHooks( 'UserrightsChangeableGroups', array( $this, + $wgUser, $wgUser->getEffectiveGroups(), &$groups ) ); return $groups; } -- 2.20.1