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