Index: trunk/phase3/includes/User.php |
— | — | @@ -3231,8 +3231,116 @@ |
3232 | 3232 | return $text; |
3233 | 3233 | } |
3234 | 3234 | } |
| 3235 | + |
| 3236 | + /** |
| 3237 | + * Returns an array of the groups that a particular group can add/remove. |
| 3238 | + * |
| 3239 | + * @param $group String: the group to check for whether it can add/remove |
| 3240 | + * @return Array array( 'add' => array( addablegroups ), |
| 3241 | + * 'remove' => array( removablegroups ), |
| 3242 | + * 'add-self' => array( addablegroups to self), |
| 3243 | + * 'remove-self' => array( removable groups from self) ) |
| 3244 | + */ |
| 3245 | + static function changeableByGroup( $group ) { |
| 3246 | + global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf; |
3235 | 3247 | |
| 3248 | + $groups = array( 'add' => array(), 'remove' => array(), 'add-self' => array(), 'remove-self' => array() ); |
| 3249 | + if( empty($wgAddGroups[$group]) ) { |
| 3250 | + // Don't add anything to $groups |
| 3251 | + } elseif( $wgAddGroups[$group] === true ) { |
| 3252 | + // You get everything |
| 3253 | + $groups['add'] = self::getAllGroups(); |
| 3254 | + } elseif( is_array($wgAddGroups[$group]) ) { |
| 3255 | + $groups['add'] = $wgAddGroups[$group]; |
| 3256 | + } |
| 3257 | + |
| 3258 | + // Same thing for remove |
| 3259 | + if( empty($wgRemoveGroups[$group]) ) { |
| 3260 | + } elseif($wgRemoveGroups[$group] === true ) { |
| 3261 | + $groups['remove'] = self::getAllGroups(); |
| 3262 | + } elseif( is_array($wgRemoveGroups[$group]) ) { |
| 3263 | + $groups['remove'] = $wgRemoveGroups[$group]; |
| 3264 | + } |
| 3265 | + |
| 3266 | + // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility |
| 3267 | + if( empty($wgGroupsAddToSelf['user']) || $wgGroupsAddToSelf['user'] !== true ) { |
| 3268 | + foreach($wgGroupsAddToSelf as $key => $value) { |
| 3269 | + if( is_int($key) ) { |
| 3270 | + $wgGroupsAddToSelf['user'][] = $value; |
| 3271 | + } |
| 3272 | + } |
| 3273 | + } |
| 3274 | + |
| 3275 | + if( empty($wgGroupsRemoveFromSelf['user']) || $wgGroupsRemoveFromSelf['user'] !== true ) { |
| 3276 | + foreach($wgGroupsRemoveFromSelf as $key => $value) { |
| 3277 | + if( is_int($key) ) { |
| 3278 | + $wgGroupsRemoveFromSelf['user'][] = $value; |
| 3279 | + } |
| 3280 | + } |
| 3281 | + } |
| 3282 | + |
| 3283 | + // Now figure out what groups the user can add to him/herself |
| 3284 | + if( empty($wgGroupsAddToSelf[$group]) ) { |
| 3285 | + } elseif( $wgGroupsAddToSelf[$group] === true ) { |
| 3286 | + // No idea WHY this would be used, but it's there |
| 3287 | + $groups['add-self'] = User::getAllGroups(); |
| 3288 | + } elseif( is_array($wgGroupsAddToSelf[$group]) ) { |
| 3289 | + $groups['add-self'] = $wgGroupsAddToSelf[$group]; |
| 3290 | + } |
| 3291 | + |
| 3292 | + if( empty($wgGroupsRemoveFromSelf[$group]) ) { |
| 3293 | + } elseif( $wgGroupsRemoveFromSelf[$group] === true ) { |
| 3294 | + $groups['remove-self'] = User::getAllGroups(); |
| 3295 | + } elseif( is_array($wgGroupsRemoveFromSelf[$group]) ) { |
| 3296 | + $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group]; |
| 3297 | + } |
| 3298 | + |
| 3299 | + return $groups; |
| 3300 | + } |
| 3301 | + |
3236 | 3302 | /** |
| 3303 | + * Returns an array of groups that this user can add and remove |
| 3304 | + * @return Array array( 'add' => array( addablegroups ), |
| 3305 | + * 'remove' => array( removablegroups ), |
| 3306 | + * 'add-self' => array( addablegroups to self), |
| 3307 | + * 'remove-self' => array( removable groups from self) ) |
| 3308 | + */ |
| 3309 | + function changeableGroups() { |
| 3310 | + if( $this->isAllowed( 'userrights' ) ) { |
| 3311 | + // This group gives the right to modify everything (reverse- |
| 3312 | + // compatibility with old "userrights lets you change |
| 3313 | + // everything") |
| 3314 | + // Using array_merge to make the groups reindexed |
| 3315 | + $all = array_merge( User::getAllGroups() ); |
| 3316 | + return array( |
| 3317 | + 'add' => $all, |
| 3318 | + 'remove' => $all, |
| 3319 | + 'add-self' => array(), |
| 3320 | + 'remove-self' => array() |
| 3321 | + ); |
| 3322 | + } |
| 3323 | + |
| 3324 | + // Okay, it's not so simple, we will have to go through the arrays |
| 3325 | + $groups = array( |
| 3326 | + 'add' => array(), |
| 3327 | + 'remove' => array(), |
| 3328 | + 'add-self' => array(), |
| 3329 | + 'remove-self' => array() ); |
| 3330 | + $addergroups = $this->getEffectiveGroups(); |
| 3331 | + |
| 3332 | + foreach ($addergroups as $addergroup) { |
| 3333 | + $groups = array_merge_recursive( |
| 3334 | + $groups, $this->changeableByGroup($addergroup) |
| 3335 | + ); |
| 3336 | + $groups['add'] = array_unique( $groups['add'] ); |
| 3337 | + $groups['remove'] = array_unique( $groups['remove'] ); |
| 3338 | + $groups['add-self'] = array_unique( $groups['add-self'] ); |
| 3339 | + $groups['remove-self'] = array_unique( $groups['remove-self'] ); |
| 3340 | + } |
| 3341 | + return $groups; |
| 3342 | + } |
| 3343 | + |
| 3344 | + /** |
3237 | 3345 | * Increment the user's edit-count field. |
3238 | 3346 | * Will have no effect for anonymous users. |
3239 | 3347 | */ |
Index: trunk/phase3/includes/api/ApiQueryRecentChanges.php |
— | — | @@ -43,12 +43,13 @@ |
44 | 44 | private $fld_comment = false, $fld_user = false, $fld_flags = false, |
45 | 45 | $fld_timestamp = false, $fld_title = false, $fld_ids = false, |
46 | 46 | $fld_sizes = false; |
47 | | - |
| 47 | + /** |
| 48 | + * Get an array mapping token names to their handler functions. |
| 49 | + * The prototype for a token function is func($pageid, $title, $rc) |
| 50 | + * it should return a token or false (permission denied) |
| 51 | + * @return array(tokenname => function) |
| 52 | + */ |
48 | 53 | protected function getTokenFunctions() { |
49 | | - // tokenname => function |
50 | | - // function prototype is func($pageid, $title, $rev) |
51 | | - // should return token or false |
52 | | - |
53 | 54 | // Don't call the hooks twice |
54 | 55 | if(isset($this->tokenFunctions)) |
55 | 56 | return $this->tokenFunctions; |
Index: trunk/phase3/includes/api/ApiMain.php |
— | — | @@ -80,6 +80,7 @@ |
81 | 81 | 'watch' => 'ApiWatch', |
82 | 82 | 'patrol' => 'ApiPatrol', |
83 | 83 | 'import' => 'ApiImport', |
| 84 | + 'userrights' => 'ApiUserrights', |
84 | 85 | ); |
85 | 86 | |
86 | 87 | /** |
Index: trunk/phase3/includes/api/ApiQueryUsers.php |
— | — | @@ -39,7 +39,37 @@ |
40 | 40 | public function __construct($query, $moduleName) { |
41 | 41 | parent :: __construct($query, $moduleName, 'us'); |
42 | 42 | } |
| 43 | + |
| 44 | + /** |
| 45 | + * Get an array mapping token names to their handler functions. |
| 46 | + * The prototype for a token function is func($user) |
| 47 | + * it should return a token or false (permission denied) |
| 48 | + * @return array(tokenname => function) |
| 49 | + */ |
| 50 | + protected function getTokenFunctions() { |
| 51 | + // Don't call the hooks twice |
| 52 | + if(isset($this->tokenFunctions)) |
| 53 | + return $this->tokenFunctions; |
43 | 54 | |
| 55 | + // If we're in JSON callback mode, no tokens can be obtained |
| 56 | + if(!is_null($this->getMain()->getRequest()->getVal('callback'))) |
| 57 | + return array(); |
| 58 | + |
| 59 | + $this->tokenFunctions = array( |
| 60 | + 'userrights' => array( 'ApiQueryUsers', 'getUserrightsToken' ), |
| 61 | + ); |
| 62 | + wfRunHooks('APIQueryUsersTokens', array(&$this->tokenFunctions)); |
| 63 | + return $this->tokenFunctions; |
| 64 | + } |
| 65 | + |
| 66 | + public static function getUserrightsToken($user) |
| 67 | + { |
| 68 | + global $wgUser; |
| 69 | + // Since the permissions check for userrights is non-trivial, |
| 70 | + // don't bother with it here |
| 71 | + return $wgUser->editToken($user->getName()); |
| 72 | + } |
| 73 | + |
44 | 74 | public function execute() { |
45 | 75 | $params = $this->extractRequestParams(); |
46 | 76 | $result = $this->getResult(); |
— | — | @@ -115,6 +145,18 @@ |
116 | 146 | } |
117 | 147 | if(isset($this->prop['emailable']) && $user->canReceiveEmail()) |
118 | 148 | $data[$name]['emailable'] = ''; |
| 149 | + if(!is_null($params['token'])) |
| 150 | + { |
| 151 | + $tokenFunctions = $this->getTokenFunctions(); |
| 152 | + foreach($params['token'] as $t) |
| 153 | + { |
| 154 | + $val = call_user_func($tokenFunctions[$t], $user); |
| 155 | + if($val === false) |
| 156 | + $this->setWarning("Action '$t' is not allowed for the current user"); |
| 157 | + else |
| 158 | + $data[$name][$t . 'token'] = $val; |
| 159 | + } |
| 160 | + } |
119 | 161 | } |
120 | 162 | } |
121 | 163 | // Second pass: add result data to $retval |
— | — | @@ -153,7 +195,11 @@ |
154 | 196 | ), |
155 | 197 | 'users' => array( |
156 | 198 | ApiBase :: PARAM_ISMULTI => true |
157 | | - ) |
| 199 | + ), |
| 200 | + 'token' => array( |
| 201 | + ApiBase :: PARAM_TYPE => array_keys($this->getTokenFunctions()), |
| 202 | + ApiBase :: PARAM_ISMULTI => true |
| 203 | + ), |
158 | 204 | ); |
159 | 205 | } |
160 | 206 | |
— | — | @@ -167,7 +213,8 @@ |
168 | 214 | ' registration - adds the user\'s registration timestamp', |
169 | 215 | ' emailable - tags if the user can and wants to receive e-mail through [[Special:Emailuser]]', |
170 | 216 | ), |
171 | | - 'users' => 'A list of users to obtain the same information for' |
| 217 | + 'users' => 'A list of users to obtain the same information for', |
| 218 | + 'token' => 'Which tokens to obtain for each user', |
172 | 219 | ); |
173 | 220 | } |
174 | 221 | |
Index: trunk/phase3/includes/AutoLoader.php |
— | — | @@ -290,6 +290,7 @@ |
291 | 291 | 'ApiRollback' => 'includes/api/ApiRollback.php', |
292 | 292 | 'ApiUnblock' => 'includes/api/ApiUnblock.php', |
293 | 293 | 'ApiUndelete' => 'includes/api/ApiUndelete.php', |
| 294 | + 'ApiUserrights' => 'includes/api/ApiUserrights.php', |
294 | 295 | 'ApiWatch' => 'includes/api/ApiWatch.php', |
295 | 296 | 'Services_JSON' => 'includes/api/ApiFormatJson_json.php', |
296 | 297 | 'Services_JSON_Error' => 'includes/api/ApiFormatJson_json.php', |
Index: trunk/phase3/includes/specials/SpecialUserrights.php |
— | — | @@ -149,29 +149,46 @@ |
150 | 150 | $removegroup[] = $group; |
151 | 151 | } |
152 | 152 | } |
| 153 | + self::doSaveUserGroups( $user, $addgroup, $removegroup, $reason ); |
| 154 | + } |
| 155 | + |
| 156 | + /** |
| 157 | + * Save user groups changes in the database. |
| 158 | + * |
| 159 | + * @param $user User object |
| 160 | + * @param $add Array of groups to add |
| 161 | + * @param $remove Array of groups to remove |
| 162 | + * @param $reason String: reason for group change |
| 163 | + * @return Array: Tuple of added, then removed groups |
| 164 | + */ |
| 165 | + static function doSaveUserGroups( User $user, $add, $remove, $reason = '' ) { |
| 166 | + global $wgUser; |
153 | 167 | |
154 | 168 | // Validate input set... |
155 | | - $changeable = $this->changeableGroups(); |
156 | | - $addable = array_merge( $changeable['add'], $this->isself ? $changeable['add-self'] : array() ); |
157 | | - $removable = array_merge( $changeable['remove'], $this->isself ? $changeable['remove-self'] : array() ); |
| 169 | + $isself = ($user->getName() == $wgUser->getName()); |
| 170 | + $groups = $user->getGroups(); |
| 171 | + $changeable = $wgUser->changeableGroups(); |
| 172 | + $addable = array_merge( $changeable['add'], $isself ? $changeable['add-self'] : array() ); |
| 173 | + $removable = array_merge( $changeable['remove'], $isself ? $changeable['remove-self'] : array() ); |
158 | 174 | |
159 | | - $removegroup = array_unique( |
160 | | - array_intersect( (array)$removegroup, $removable ) ); |
161 | | - $addgroup = array_unique( |
162 | | - array_intersect( (array)$addgroup, $addable ) ); |
| 175 | + $remove = array_unique( |
| 176 | + array_intersect( (array)$remove, $removable, $groups ) ); |
| 177 | + $add = array_unique( array_diff( |
| 178 | + array_intersect( (array)$add, $addable ), |
| 179 | + $groups ) ); |
163 | 180 | |
164 | 181 | $oldGroups = $user->getGroups(); |
165 | 182 | $newGroups = $oldGroups; |
166 | 183 | // remove then add groups |
167 | | - if( $removegroup ) { |
168 | | - $newGroups = array_diff($newGroups, $removegroup); |
169 | | - foreach( $removegroup as $group ) { |
| 184 | + if( $remove ) { |
| 185 | + $newGroups = array_diff($newGroups, $remove); |
| 186 | + foreach( $remove as $group ) { |
170 | 187 | $user->removeGroup( $group ); |
171 | 188 | } |
172 | 189 | } |
173 | | - if( $addgroup ) { |
174 | | - $newGroups = array_merge($newGroups, $addgroup); |
175 | | - foreach( $addgroup as $group ) { |
| 190 | + if( $add ) { |
| 191 | + $newGroups = array_merge($newGroups, $add); |
| 192 | + foreach( $add as $group ) { |
176 | 193 | $user->addGroup( $group ); |
177 | 194 | } |
178 | 195 | } |
— | — | @@ -182,29 +199,27 @@ |
183 | 200 | |
184 | 201 | wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) ); |
185 | 202 | wfDebug( 'newGroups: ' . print_r( $newGroups, true ) ); |
186 | | - if( $user instanceof User ) { |
187 | | - // hmmm |
188 | | - wfRunHooks( 'UserRights', array( &$user, $addgroup, $removegroup ) ); |
189 | | - } |
| 203 | + wfRunHooks( 'UserRights', array( &$user, $add, $remove ) ); |
190 | 204 | |
191 | 205 | if( $newGroups != $oldGroups ) { |
192 | | - $this->addLogEntry( $user, $oldGroups, $newGroups ); |
| 206 | + self::addLogEntry( $user, $oldGroups, $newGroups, $reason ); |
193 | 207 | } |
| 208 | + return array( $add, $remove ); |
194 | 209 | } |
| 210 | + |
195 | 211 | |
196 | 212 | /** |
197 | 213 | * Add a rights log entry for an action. |
198 | 214 | */ |
199 | | - function addLogEntry( $user, $oldGroups, $newGroups ) { |
200 | | - global $wgRequest; |
| 215 | + static function addLogEntry( $user, $oldGroups, $newGroups, $reason ) { |
201 | 216 | $log = new LogPage( 'rights' ); |
202 | 217 | |
203 | 218 | $log->addEntry( 'rights', |
204 | 219 | $user->getUserPage(), |
205 | | - $wgRequest->getText( 'user-reason' ), |
| 220 | + $reason, |
206 | 221 | array( |
207 | | - $this->makeGroupNameListForLog( $oldGroups ), |
208 | | - $this->makeGroupNameListForLog( $newGroups ) |
| 222 | + self::makeGroupNameListForLog( $oldGroups ), |
| 223 | + self::makeGroupNameListForLog( $newGroups ) |
209 | 224 | ) |
210 | 225 | ); |
211 | 226 | } |
— | — | @@ -293,7 +308,7 @@ |
294 | 309 | return $user; |
295 | 310 | } |
296 | 311 | |
297 | | - function makeGroupNameList( $ids ) { |
| 312 | + static function makeGroupNameList( $ids ) { |
298 | 313 | if( empty( $ids ) ) { |
299 | 314 | return wfMsgForContent( 'rightsnone' ); |
300 | 315 | } else { |
— | — | @@ -301,11 +316,11 @@ |
302 | 317 | } |
303 | 318 | } |
304 | 319 | |
305 | | - function makeGroupNameListForLog( $ids ) { |
| 320 | + static function makeGroupNameListForLog( $ids ) { |
306 | 321 | if( empty( $ids ) ) { |
307 | 322 | return ''; |
308 | 323 | } else { |
309 | | - return $this->makeGroupNameList( $ids ); |
| 324 | + return self::makeGroupNameList( $ids ); |
310 | 325 | } |
311 | 326 | } |
312 | 327 | |
— | — | @@ -511,115 +526,20 @@ |
512 | 527 | } |
513 | 528 | |
514 | 529 | /** |
515 | | - * Returns an array of the groups that the user can add/remove. |
| 530 | + * Returns $wgUser->changeableGroups() |
516 | 531 | * |
517 | 532 | * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) , 'add-self' => array( addablegroups to self), 'remove-self' => array( removable groups from self) ) |
518 | 533 | */ |
519 | 534 | function changeableGroups() { |
520 | 535 | global $wgUser; |
521 | | - |
522 | | - if( $wgUser->isAllowed( 'userrights' ) ) { |
523 | | - // This group gives the right to modify everything (reverse- |
524 | | - // compatibility with old "userrights lets you change |
525 | | - // everything") |
526 | | - // Using array_merge to make the groups reindexed |
527 | | - $all = array_merge( User::getAllGroups() ); |
528 | | - return array( |
529 | | - 'add' => $all, |
530 | | - 'remove' => $all, |
531 | | - 'add-self' => array(), |
532 | | - 'remove-self' => array() |
533 | | - ); |
534 | | - } |
535 | | - |
536 | | - // Okay, it's not so simple, we will have to go through the arrays |
537 | | - $groups = array( |
538 | | - 'add' => array(), |
539 | | - 'remove' => array(), |
540 | | - 'add-self' => array(), |
541 | | - 'remove-self' => array() ); |
542 | | - $addergroups = $wgUser->getEffectiveGroups(); |
543 | | - |
544 | | - foreach ($addergroups as $addergroup) { |
545 | | - $groups = array_merge_recursive( |
546 | | - $groups, $this->changeableByGroup($addergroup) |
547 | | - ); |
548 | | - $groups['add'] = array_unique( $groups['add'] ); |
549 | | - $groups['remove'] = array_unique( $groups['remove'] ); |
550 | | - $groups['add-self'] = array_unique( $groups['add-self'] ); |
551 | | - $groups['remove-self'] = array_unique( $groups['remove-self'] ); |
552 | | - } |
553 | | - |
| 536 | + $groups = $wgUser->changeableGroups(); |
554 | 537 | // Run a hook because we can |
555 | | - wfRunHooks( 'UserrightsChangeableGroups', array( $this, $wgUser, $addergroups, &$groups ) ); |
556 | | - |
| 538 | + wfRunHooks( 'UserrightsChangeableGroups', array( $this, |
| 539 | + $wgUser, $wgUser->getEffectiveGroups(), &$groups ) ); |
557 | 540 | return $groups; |
558 | 541 | } |
559 | 542 | |
560 | 543 | /** |
561 | | - * Returns an array of the groups that a particular group can add/remove. |
562 | | - * |
563 | | - * @param $group String: the group to check for whether it can add/remove |
564 | | - * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) , 'add-self' => array( addablegroups to self), 'remove-self' => array( removable groups from self) ) |
565 | | - */ |
566 | | - private function changeableByGroup( $group ) { |
567 | | - global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf; |
568 | | - |
569 | | - $groups = array( 'add' => array(), 'remove' => array(), 'add-self' => array(), 'remove-self' => array() ); |
570 | | - if( empty($wgAddGroups[$group]) ) { |
571 | | - // Don't add anything to $groups |
572 | | - } elseif( $wgAddGroups[$group] === true ) { |
573 | | - // You get everything |
574 | | - $groups['add'] = User::getAllGroups(); |
575 | | - } elseif( is_array($wgAddGroups[$group]) ) { |
576 | | - $groups['add'] = $wgAddGroups[$group]; |
577 | | - } |
578 | | - |
579 | | - // Same thing for remove |
580 | | - if( empty($wgRemoveGroups[$group]) ) { |
581 | | - } elseif($wgRemoveGroups[$group] === true ) { |
582 | | - $groups['remove'] = User::getAllGroups(); |
583 | | - } elseif( is_array($wgRemoveGroups[$group]) ) { |
584 | | - $groups['remove'] = $wgRemoveGroups[$group]; |
585 | | - } |
586 | | - |
587 | | - // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility |
588 | | - if( empty($wgGroupsAddToSelf['user']) || $wgGroupsAddToSelf['user'] !== true ) { |
589 | | - foreach($wgGroupsAddToSelf as $key => $value) { |
590 | | - if( is_int($key) ) { |
591 | | - $wgGroupsAddToSelf['user'][] = $value; |
592 | | - } |
593 | | - } |
594 | | - } |
595 | | - |
596 | | - if( empty($wgGroupsRemoveFromSelf['user']) || $wgGroupsRemoveFromSelf['user'] !== true ) { |
597 | | - foreach($wgGroupsRemoveFromSelf as $key => $value) { |
598 | | - if( is_int($key) ) { |
599 | | - $wgGroupsRemoveFromSelf['user'][] = $value; |
600 | | - } |
601 | | - } |
602 | | - } |
603 | | - |
604 | | - // Now figure out what groups the user can add to him/herself |
605 | | - if( empty($wgGroupsAddToSelf[$group]) ) { |
606 | | - } elseif( $wgGroupsAddToSelf[$group] === true ) { |
607 | | - // No idea WHY this would be used, but it's there |
608 | | - $groups['add-self'] = User::getAllGroups(); |
609 | | - } elseif( is_array($wgGroupsAddToSelf[$group]) ) { |
610 | | - $groups['add-self'] = $wgGroupsAddToSelf[$group]; |
611 | | - } |
612 | | - |
613 | | - if( empty($wgGroupsRemoveFromSelf[$group]) ) { |
614 | | - } elseif( $wgGroupsRemoveFromSelf[$group] === true ) { |
615 | | - $groups['remove-self'] = User::getAllGroups(); |
616 | | - } elseif( is_array($wgGroupsRemoveFromSelf[$group]) ) { |
617 | | - $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group]; |
618 | | - } |
619 | | - |
620 | | - return $groups; |
621 | | - } |
622 | | - |
623 | | - /** |
624 | 544 | * Show a rights log fragment for the specified user |
625 | 545 | * |
626 | 546 | * @param $user User to show log for |
Index: trunk/phase3/RELEASE-NOTES |
— | — | @@ -330,6 +330,7 @@ |
331 | 331 | when someone tries to use them |
332 | 332 | * BREAKING CHANGE: action=purge requires write rights and, for anonymous users, |
333 | 333 | a POST request |
| 334 | +* (bug 15935) Added action=userrights to add/remove users to/from groups |
334 | 335 | |
335 | 336 | === Languages updated in 1.15 === |
336 | 337 | |