Userrights work. Changes:
[lhc/web/wiklou.git] / includes / SpecialUserrights.php
1 <?php
2
3 /**
4 * Special page to allow managing user group membership
5 *
6 * @addtogroup SpecialPage
7 * @todo This code is disgusting and needs a total rewrite
8 */
9
10 /** */
11 require_once( dirname(__FILE__) . '/HTMLForm.php');
12
13 /** Entry point */
14 function wfSpecialUserrights() {
15 global $wgRequest;
16 $form = new UserrightsForm($wgRequest);
17 $form->execute();
18 }
19
20 /**
21 * A class to manage user levels rights.
22 * @addtogroup SpecialPage
23 */
24 class UserrightsForm extends HTMLForm {
25 var $mPosted, $mRequest, $mSaveprefs;
26 /** Escaped local url name*/
27 var $action;
28
29 /** Constructor*/
30 public function __construct( &$request ) {
31 $this->mPosted = $request->wasPosted();
32 $this->mRequest =& $request;
33 $this->mName = 'userrights';
34 $this->mReason = $request->getText( 'user-reason' );
35
36 $titleObj = SpecialPage::getTitleFor( 'Userrights' );
37 $this->action = $titleObj->escapeLocalURL();
38 }
39
40 /**
41 * Manage forms to be shown according to posted data.
42 * Depending on the submit button used, call a form or a save function.
43 */
44 function execute() {
45 // show the general form
46 $this->switchForm();
47 if( $this->mPosted ) {
48 // show some more forms
49 if( $this->mRequest->getCheck( 'ssearchuser' ) ) {
50 $this->editUserGroupsForm( $this->mRequest->getVal( 'user-editname' ) );
51 }
52
53 // save settings
54 if( $this->mRequest->getCheck( 'saveusergroups' ) ) {
55 global $wgUser;
56 $username = $this->mRequest->getVal( 'user-editname' );
57 $reason = $this->mRequest->getVal( 'user-reason' );
58 if( $wgUser->matchEditToken( $this->mRequest->getVal( 'wpEditToken' ), $username ) ) {
59 $this->saveUserGroups( $username,
60 $this->mRequest->getArray( 'member' ),
61 $this->mRequest->getArray( 'available' ),
62 $reason );
63 }
64 }
65 }
66 }
67
68
69 /**
70 * Save user groups changes in the database.
71 * Data comes from the editUserGroupsForm() form function
72 *
73 * @param string $username Username to apply changes to.
74 * @param array $removegroup id of groups to be removed.
75 * @param array $addgroup id of groups to be added.
76 * @param string $reason Reason for group change
77 *
78 */
79 function saveUserGroups( $username, $removegroup, $addgroup, $reason = '') {
80 $split = $this->splitUsername( $username );
81 if( WikiError::isError( $split ) ) {
82 $wgOut->addWikiText( wfMsg( 'userrights-nodatabase', $split->getMessage() ) );
83 return;
84 }
85
86 list( $database, $name ) = $split;
87 $this->db =& $this->getDB( $database );
88 $userid = $this->getUserId( $database, $name );
89
90 if( $userid == 0) {
91 $wgOut->addWikiText( wfMsg( 'nosuchusershort', wfEscapeWikiText( $username ) ) );
92 return;
93 }
94
95 global $wgUser;
96 if ($database != '' && !$wgUser->isAllowed('userrights-interwiki')) {
97 $wgOut->addWikiText( wfMsg( 'userrights-no-interwiki' ) );
98 return;
99 }
100
101 $oldGroups = $this->getUserGroups( $database, $userid );
102 $newGroups = $oldGroups;
103 // remove then add groups
104 if(isset($removegroup)) {
105 $newGroups = array_diff($newGroups, $removegroup);
106 foreach( $removegroup as $group ) {
107 $this->removeUserGroup( $database, $userid, $group );
108 }
109 }
110 if(isset($addgroup)) {
111 $newGroups = array_merge($newGroups, $addgroup);
112 foreach( $addgroup as $group ) {
113 $this->addUserGroup( $database, $userid, $group );
114 }
115 }
116 $newGroups = array_unique( $newGroups );
117
118 // Ensure that caches are cleared
119 $this->touchUser( $database, $userid );
120
121 wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) );
122 wfDebug( 'newGroups: ' . print_r( $newGroups, true ) );
123 wfRunHooks( 'UserRights', array( &$u, $addgroup, $removegroup ) );
124
125 $log = new LogPage( 'rights' );
126 $log->addEntry( 'rights', Title::makeTitle( NS_USER, $username ), $this->mReason, array( $this->makeGroupNameList( $oldGroups ),
127 $this->makeGroupNameList( $newGroups ) ) );
128 }
129
130 /**
131 * Edit user groups membership
132 * @param string $username Name of the user.
133 */
134 function editUserGroupsForm($username) {
135 global $wgOut, $wgUser;
136
137 $split = $this->splitUsername( $username );
138 if( WikiError::isError( $split ) ) {
139 $wgOut->addWikiText( wfMsg( 'userrights-nodatabase', $split->getMessage() ) );
140 return;
141 }
142
143 list( $database, $name ) = $split;
144 $this->db =& $this->getDB( $database );
145 $userid = $this->getUserId( $database, $name );
146
147 if( $name == '' ) {
148 $wgOut->addWikiText( wfMsg( 'nouserspecified' ) );
149 return;
150 } elseif( $userid == 0) {
151 $wgOut->addWikiText( wfMsg( 'nosuchusershort', wfEscapeWikiText( $username ) ) );
152 return;
153 }
154
155 global $wgUser;
156 if ($database != '' && !$wgUser->isAllowed('userrights-interwiki')) {
157 $wgOut->addWikiText( wfMsg( 'userrights-no-interwiki' ) );
158 return;
159 }
160
161 $groups = $this->getUserGroups( $database, $userid );
162
163 $this->showEditUserGroupsForm( $username, $groups );
164
165 if ($database == '') {
166 $this->showLogFragment( User::newFromName($username), $wgOut );
167 }
168 }
169
170 function splitUsername( $username ) {
171 $parts = explode( '@', $username );
172 if( count( $parts ) < 2 ) {
173 return array( '', $username );
174 }
175 list( $name, $database ) = $parts;
176
177 global $wgLocalDatabases;
178 return array_search( $database, $wgLocalDatabases ) !== false
179 ? array( $database, $name )
180 : new WikiError( 'Bogus database suffix "' . wfEscapeWikiText( $database ) . '"' );
181 }
182
183 /**
184 * Open a database connection to work on for the requested user.
185 * This may be a new connection to another database for remote users.
186 * @param string $database
187 * @return Database
188 */
189 function &getDB( $database ) {
190 if( $database == '' ) {
191 $db =& wfGetDB( DB_MASTER );
192 } else {
193 global $wgDBuser, $wgDBpassword;
194 $server = $this->getMaster( $database );
195 $db = new Database( $server, $wgDBuser, $wgDBpassword, $database );
196 }
197 return $db;
198 }
199
200 /**
201 * Return the master server to connect to for the requested database.
202 */
203 function getMaster( $database ) {
204 global $wgDBserver, $wgAlternateMaster;
205 if( isset( $wgAlternateMaster[$database] ) ) {
206 return $wgAlternateMaster[$database];
207 }
208 return $wgDBserver;
209 }
210
211 function getUserId( $database, $name ) {
212 if( $name === '' )
213 return 0;
214 return ( $name{0} == "#" )
215 ? IntVal( substr( $name, 1 ) )
216 : IntVal( $this->db->selectField( 'user',
217 'user_id',
218 array( 'user_name' => $name ),
219 'MakesysopStewardForm::getUserId' ) );
220 }
221
222 function getUserGroups( $database, $userid ) {
223 $res = $this->db->select( 'user_groups',
224 array( 'ug_group' ),
225 array( 'ug_user' => $userid ),
226 'MakesysopStewardForm::getUserGroups' );
227 $groups = array();
228 while( $row = $this->db->fetchObject( $res ) ) {
229 $groups[] = $row->ug_group;
230 }
231 return $groups;
232 }
233
234 function addUserGroup( $database, $userid, $group ) {
235 $this->db->insert( 'user_groups',
236 array(
237 'ug_user' => $userid,
238 'ug_group' => $group,
239 ),
240 'MakesysopStewardForm::addUserGroup',
241 array( 'IGNORE' ) );
242 }
243
244 function removeUserGroup( $database, $userid, $group ) {
245 $this->db->delete( 'user_groups',
246 array(
247 'ug_user' => $userid,
248 'ug_group' => $group,
249 ),
250 'MakesysopStewardForm::addUserGroup' );
251 }
252
253 function touchUser( $database, $userid ) {
254 $this->db->update( 'user',
255 array( 'user_touched' => $this->db->timestamp() ),
256 array( 'user_id' => $userid ),
257 'MakesysopStewardForm::touchUser' );
258
259 global $wgMemc;
260 if ( function_exists( 'wfForeignMemcKey' ) ) {
261 $key = wfForeignMemcKey( $database, false, 'user', 'id', $userid );
262 } else {
263 $key = "$database:user:id:$userid";
264 }
265 $wgMemc->delete( $key );
266 }
267
268 function makeGroupNameList( $ids ) {
269 return implode( ', ', $ids );
270 }
271
272 /**
273 * Output a form to allow searching for a user
274 */
275 function switchForm() {
276 global $wgOut, $wgRequest;
277 $username = $wgRequest->getText( 'user-editname' );
278 $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->action, 'name' => 'uluser' ) );
279 $form .= '<fieldset><legend>' . wfMsgHtml( 'userrights-lookup-user' ) . '</legend>';
280 $form .= '<p>' . Xml::inputLabel( wfMsg( 'userrights-user-editname' ), 'user-editname', 'username', 30, $username ) . '</p>';
281 $form .= '<p>' . Xml::submitButton( wfMsg( 'editusergroup' ), array( 'name' => 'ssearchuser' ) ) . '</p>';
282 $form .= '</fieldset>';
283 $form .= '</form>';
284 $wgOut->addHTML( $form );
285 }
286
287 /**
288 * Go through used and available groups and return the ones that this
289 * form will be able to manipulate based on the current user's system
290 * permissions.
291 *
292 * @param $groups Array: list of groups the given user is in
293 * @return Array: Tuple of addable, then removable groups
294 */
295 protected function splitGroups( $groups ) {
296 list($addable, $removable) = array_values( $this->changeableGroups() );
297 $removable = array_intersect($removable, $groups ); // Can't remove groups the user doesn't have
298 $addable = array_diff( $addable, $groups ); // Can't add groups the user does have
299
300 return array( $addable, $removable );
301 }
302
303 /**
304 * Show the form to edit group memberships.
305 *
306 * @todo make all CSS-y and semantic
307 * @param $username String: Name of user you're editing
308 * @param $groups Array: Array of groups the user is in
309 */
310 protected function showEditUserGroupsForm( $username, $groups ) {
311 global $wgOut, $wgUser;
312
313 list( $addable, $removable ) = $this->splitGroups( $groups );
314
315 $wgOut->addHTML(
316 Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->action, 'name' => 'editGroup' ) ) .
317 Xml::hidden( 'user-editname', $username ) .
318 Xml::hidden( 'wpEditToken', $wgUser->editToken( $username ) ) .
319 Xml::openElement( 'fieldset' ) .
320 Xml::element( 'legend', array(), wfMsg( 'userrights-editusergroup' ) ) .
321 $wgOut->parse( wfMsg( 'editinguser', $username ) ) .
322 $this->explainRights() .
323 "<table border='0'>
324 <tr>
325 <td></td>
326 <td>
327 <table width='400'>
328 <tr>
329 <td width='50%'>" . $this->removeSelect( $removable ) . "</td>
330 <td width='50%'>" . $this->addSelect( $addable ) . "</td>
331 </tr>
332 </table>
333 </tr>
334 <tr>
335 <td colspan='2'>" .
336 $wgOut->parse( wfMsg('userrights-groupshelp') ) .
337 "</td>
338 </tr>
339 <tr>
340 <td>" .
341 Xml::label( wfMsg( 'userrights-reason' ), 'wpReason' ) .
342 "</td>
343 <td>" .
344 Xml::input( 'user-reason', 60, false, array( 'id' => 'wpReason', 'maxlength' => 255 ) ) .
345 "</td>
346 </tr>
347 <tr>
348 <td></td>
349 <td>" .
350 Xml::submitButton( wfMsg( 'saveusergroups' ), array( 'name' => 'saveusergroups' ) ) .
351 "</td>
352 </tr>
353 </table>\n" .
354 Xml::closeElement( 'fieldset' ) .
355 Xml::closeElement( 'form' ) . "\n"
356 );
357 }
358
359 /**
360 * Prepare a list of groups the user is able to add and remove
361 *
362 * @return string
363 */
364 private function explainRights() {
365 global $wgUser, $wgLang;
366
367 $out = array();
368 list( $add, $remove ) = array_values( $this->changeableGroups() );
369
370 if( count( $add ) > 0 )
371 $out[] = wfMsgExt( 'userrights-available-add', 'parseinline', $wgLang->listToText( $add ) );
372 if( count( $remove ) > 0 )
373 $out[] = wfMsgExt( 'userrights-available-remove', 'parseinline', $wgLang->listToText( $remove ) );
374
375 return count( $out ) > 0
376 ? implode( ' ', $out )
377 : wfMsgExt( 'userrights-available-none', 'parseinline' );
378 }
379
380 /**
381 * Adds the <select> thingie where you can select what groups to remove
382 *
383 * @param array $groups The groups that can be removed
384 * @return string XHTML <select> element
385 */
386 private function removeSelect( $groups ) {
387 return $this->doSelect( $groups, 'member' );
388 }
389
390 /**
391 * Adds the <select> thingie where you can select what groups to add
392 *
393 * @param array $groups The groups that can be added
394 * @return string XHTML <select> element
395 */
396 private function addSelect( $groups ) {
397 return $this->doSelect( $groups, 'available' );
398 }
399
400 /**
401 * Adds the <select> thingie where you can select what groups to add/remove
402 *
403 * @param array $groups The groups that can be added/removed
404 * @param string $name 'member' or 'available'
405 * @return string XHTML <select> element
406 */
407 private function doSelect( $groups, $name ) {
408 $ret = wfMsgHtml( "{$this->mName}-groups$name" ) .
409 Xml::openElement( 'select', array(
410 'name' => "{$name}[]",
411 'multiple' => 'multiple',
412 'size' => '6',
413 'style' => 'width: 100%;'
414 )
415 );
416 foreach ($groups as $group) {
417 $ret .= Xml::element( 'option', array( 'value' => $group ), User::getGroupName( $group ) );
418 }
419 $ret .= Xml::closeElement( 'select' );
420 return $ret;
421 }
422
423 /**
424 * @param string $group The name of the group to check
425 * @return bool Can we remove the group?
426 */
427 private function canRemove( $group ) {
428 // $this->changeableGroups()['remove'] doesn't work, of course. Thanks,
429 // PHP.
430 $groups = $this->changeableGroups();
431 return in_array( $group, $groups['remove'] );
432 }
433
434 /**
435 * @param string $group The name of the group to check
436 * @return bool Can we add the group?
437 */
438 private function canAdd( $group ) {
439 $groups = $this->changeableGroups();
440 return in_array( $group, $groups['add'] );
441 }
442
443 /**
444 * Returns an array of the groups that the user can add/remove.
445 *
446 * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) )
447 */
448 function changeableGroups() {
449 global $wgUser;
450
451 $groups = array( 'add' => array(), 'remove' => array() );
452 $addergroups = $wgUser->getEffectiveGroups();
453
454 foreach ($addergroups as $addergroup) {
455 $groups = array_merge_recursive(
456 $groups, $this->changeableByGroup($addergroup)
457 );
458 $groups['add'] = array_unique( $groups['add'] );
459 $groups['remove'] = array_unique( $groups['remove'] );
460 }
461 return $groups;
462 }
463
464 /**
465 * Returns an array of the groups that a particular group can add/remove.
466 *
467 * @param String $group The group to check for whether it can add/remove
468 * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) )
469 */
470 private function changeableByGroup( $group ) {
471 global $wgGroupPermissions, $wgAddGroups, $wgRemoveGroups;
472
473 if( $wgGroupPermissions[$group]['userrights'] == true ) {
474 // This group gives the right to modify everything (reverse-
475 // compatibility with old "userrights lets you change
476 // everything")
477 return array(
478 'add' => User::getAllGroups(),
479 'remove' => User::getAllGroups()
480 );
481 }
482
483 // Okay, it's not so simple, we have to go through the arrays
484 $groups = array( 'add' => array(), 'remove' => array() );
485 if( empty($wgAddGroups[$group]) ) {
486 // Don't add anything to $groups
487 } elseif( $wgAddGroups[$group] === true ) {
488 // You get everything
489 $groups['add'] = User::getAllGroups();
490 } elseif( is_array($wgAddGroups[$group]) ) {
491 $groups['add'] = $wgAddGroups[$group];
492 }
493
494 // Same thing for remove
495 if( empty($wgRemoveGroups[$group]) ) {
496 } elseif($wgRemoveGroups[$group] === true ) {
497 $groups['remove'] = User::getAllGroups();
498 } elseif( is_array($wgRemoveGroups[$group]) ) {
499 $groups['remove'] = $wgRemoveGroups[$group];
500 }
501 return $groups;
502 }
503
504 /**
505 * Show a rights log fragment for the specified user
506 *
507 * @param User $user User to show log for
508 * @param OutputPage $output OutputPage to use
509 */
510 protected function showLogFragment( $user, $output ) {
511 $viewer = new LogViewer(
512 new LogReader(
513 new FauxRequest(
514 array(
515 'type' => 'rights',
516 'page' => $user->getUserPage()->getPrefixedText(),
517 )
518 )
519 )
520 );
521 $output->addHtml( "<h2>" . htmlspecialchars( LogPage::logName( 'rights' ) ) . "</h2>\n" );
522 $viewer->showList( $output );
523 }
524
525 }