Add userrights-text message to display at all times on Special:Userrights, like makes...
[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 // If the visitor doesn't have permissions to assign or remove
46 // any groups, it's a bit silly to give them the user search prompt.
47 $available = $this->changeableGroups();
48 if( empty( $available['add'] ) && empty( $available['remove'] ) ) {
49 // fixme... there may be intermediate groups we can mention.
50 global $wgOut, $wgUser;
51 $wgOut->showPermissionsErrorPage( array(
52 $wgUser->isAnon()
53 ? 'userrights-nologin'
54 : 'userrights-notallowed' ) );
55 return;
56 }
57
58 // show the general form
59 $this->switchForm();
60 if( $this->mPosted ) {
61 // show some more forms
62 if( $this->mRequest->getCheck( 'ssearchuser' ) ) {
63 $this->editUserGroupsForm( $this->mRequest->getVal( 'user-editname' ) );
64 }
65
66 // save settings
67 if( $this->mRequest->getCheck( 'saveusergroups' ) ) {
68 global $wgUser;
69 $username = $this->mRequest->getVal( 'user-editname' );
70 $reason = $this->mRequest->getVal( 'user-reason' );
71 if( $wgUser->matchEditToken( $this->mRequest->getVal( 'wpEditToken' ), $username ) ) {
72 $this->saveUserGroups( $username,
73 $this->mRequest->getArray( 'removable' ),
74 $this->mRequest->getArray( 'available' ),
75 $reason );
76 }
77 }
78 }
79 }
80
81
82 /**
83 * Save user groups changes in the database.
84 * Data comes from the editUserGroupsForm() form function
85 *
86 * @param string $username Username to apply changes to.
87 * @param array $removegroup id of groups to be removed.
88 * @param array $addgroup id of groups to be added.
89 * @param string $reason Reason for group change
90 *
91 */
92 function saveUserGroups( $username, $removegroup, $addgroup, $reason = '') {
93 $user = $this->fetchUser( $username );
94 if( !$user ) {
95 return;
96 }
97
98 // Validate input set...
99 $changeable = $this->changeableGroups();
100 $removegroup = array_unique(
101 array_intersect( (array)$removegroup, $changeable['remove'] ) );
102 $addgroup = array_unique(
103 array_intersect( (array)$addgroup, $changeable['add'] ) );
104
105 $oldGroups = $user->getGroups();
106 $newGroups = $oldGroups;
107 // remove then add groups
108 if( $removegroup ) {
109 $newGroups = array_diff($newGroups, $removegroup);
110 foreach( $removegroup as $group ) {
111 $user->removeGroup( $group );
112 }
113 }
114 if( $addgroup ) {
115 $newGroups = array_merge($newGroups, $addgroup);
116 foreach( $addgroup as $group ) {
117 $user->addGroup( $group );
118 }
119 }
120 $newGroups = array_unique( $newGroups );
121
122 // Ensure that caches are cleared
123 $user->invalidateCache();
124
125 wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) );
126 wfDebug( 'newGroups: ' . print_r( $newGroups, true ) );
127 if( $user instanceof User ) {
128 // hmmm
129 wfRunHooks( 'UserRights', array( &$user, $addgroup, $removegroup ) );
130 }
131
132 $log = new LogPage( 'rights' );
133 $log->addEntry( 'rights',
134 $user->getUserPage(),
135 $this->mReason,
136 array(
137 $this->makeGroupNameList( $oldGroups ),
138 $this->makeGroupNameList( $newGroups ) ) );
139 }
140
141 /**
142 * Edit user groups membership
143 * @param string $username Name of the user.
144 */
145 function editUserGroupsForm( $username ) {
146 global $wgOut;
147
148 $user = $this->fetchUser( $username );
149 if( !$user ) {
150 return;
151 }
152
153 $groups = $user->getGroups();
154
155 $this->showEditUserGroupsForm( $user, $groups );
156
157 // This isn't really ideal logging behavior,
158 // but let's not hide the interwiki logs if
159 // we're using them as is.
160 $this->showLogFragment( $user, $wgOut );
161 }
162
163 /**
164 * Normalize the input username, which may be local or remote, and
165 * return a user (or proxy) object for manipulating it.
166 *
167 * Side effects: error output for invalid access
168 * @return mixed User, UserRightsProxy, or null
169 */
170 function fetchUser( $username ) {
171 global $wgOut, $wgUser;
172
173 $parts = explode( '@', $username );
174 if( count( $parts ) < 2 ) {
175 $name = trim( $username );
176 $database = '';
177 } else {
178 list( $name, $database ) = array_map( 'trim', $parts );
179
180 if( !$wgUser->isAllowed( 'userrights-interwiki' ) ) {
181 $wgOut->addWikiText( wfMsg( 'userrights-no-interwiki' ) );
182 return null;
183 }
184 if( !UserRightsProxy::validDatabase( $database ) ) {
185 $wgOut->addWikiText( wfMsg( 'userrights-nodatabase', $database ) );
186 return null;
187 }
188 }
189
190 if( $name == '' ) {
191 $wgOut->addWikiText( wfMsg( 'nouserspecified' ) );
192 return false;
193 }
194
195 if( $name{0} == '#' ) {
196 // Numeric ID can be specified...
197 // We'll do a lookup for the name internally.
198 $id = intval( substr( $name, 1 ) );
199
200 if( $database == '' ) {
201 $name = User::whoIs( $id );
202 } else {
203 $name = UserRightsProxy::whoIs( $database, $id );
204 }
205
206 if( !$name ) {
207 $wgOut->addWikiText( wfMsg( 'noname' ) );
208 return null;
209 }
210 }
211
212 if( $database == '' ) {
213 $user = User::newFromName( $name );
214 } else {
215 $user = UserRightsProxy::newFromName( $database, $name );
216 }
217
218 if( !$user || $user->isAnon() ) {
219 $wgOut->addWikiText( wfMsg( 'nosuchusershort', wfEscapeWikiText( $username ) ) );
220 return null;
221 }
222
223 return $user;
224 }
225
226 function makeGroupNameList( $ids ) {
227 return implode( ', ', $ids );
228 }
229
230 /**
231 * Output a form to allow searching for a user
232 */
233 function switchForm() {
234 global $wgOut, $wgRequest;
235 $wgOut->addWikiText( wfMsg( 'userrights-text' ) );
236
237 $username = $wgRequest->getText( 'user-editname' );
238 $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->action, 'name' => 'uluser' ) );
239 $form .= '<fieldset><legend>' . wfMsgHtml( 'userrights-lookup-user' ) . '</legend>';
240 $form .= '<p>' . Xml::inputLabel( wfMsg( 'userrights-user-editname' ), 'user-editname', 'username', 30, $username ) . '</p>';
241 $form .= '<p>' . Xml::submitButton( wfMsg( 'editusergroup' ), array( 'name' => 'ssearchuser' ) ) . '</p>';
242 $form .= '</fieldset>';
243 $form .= '</form>';
244 $wgOut->addHTML( $form );
245 }
246
247 /**
248 * Go through used and available groups and return the ones that this
249 * form will be able to manipulate based on the current user's system
250 * permissions.
251 *
252 * @param $groups Array: list of groups the given user is in
253 * @return Array: Tuple of addable, then removable groups
254 */
255 protected function splitGroups( $groups ) {
256 list($addable, $removable) = array_values( $this->changeableGroups() );
257 $removable = array_intersect($removable, $groups ); // Can't remove groups the user doesn't have
258 $addable = array_diff( $addable, $groups ); // Can't add groups the user does have
259
260 return array( $addable, $removable );
261 }
262
263 /**
264 * Show the form to edit group memberships.
265 *
266 * @todo make all CSS-y and semantic
267 * @param $user User or UserRightsProxy you're editing
268 * @param $groups Array: Array of groups the user is in
269 */
270 protected function showEditUserGroupsForm( $user, $groups ) {
271 global $wgOut, $wgUser;
272
273 list( $addable, $removable ) = $this->splitGroups( $groups );
274
275 $list = array();
276 foreach( $user->getGroups() as $group )
277 $list[] = self::buildGroupLink( $group );
278 $grouplist = implode( ', ', $list );
279
280 $wgOut->addHTML(
281 Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->action, 'name' => 'editGroup' ) ) .
282 Xml::hidden( 'user-editname', $user->getName() ) .
283 Xml::hidden( 'wpEditToken', $wgUser->editToken( $user->getName() ) ) .
284 Xml::openElement( 'fieldset' ) .
285 Xml::element( 'legend', array(), wfMsg( 'userrights-editusergroup' ) ) .
286 wfMsgExt( 'editinguser', array( 'parse' ),
287 wfEscapeWikiText( $user->getName() ) ) .
288 '<p>' . wfMsgHtml('userrights-groupsmember') . ' ' . $grouplist . '</p>' .
289 $this->explainRights() .
290 "<table border='0'>
291 <tr>
292 <td></td>
293 <td>
294 <table width='400'>
295 <tr>
296 <td width='50%'>" . $this->removeSelect( $removable ) . "</td>
297 <td width='50%'>" . $this->addSelect( $addable ) . "</td>
298 </tr>
299 </table>
300 </tr>
301 <tr>
302 <td colspan='2'>" .
303 $wgOut->parse( wfMsg('userrights-groupshelp') ) .
304 "</td>
305 </tr>
306 <tr>
307 <td>" .
308 Xml::label( wfMsg( 'userrights-reason' ), 'wpReason' ) .
309 "</td>
310 <td>" .
311 Xml::input( 'user-reason', 60, false, array( 'id' => 'wpReason', 'maxlength' => 255 ) ) .
312 "</td>
313 </tr>
314 <tr>
315 <td></td>
316 <td>" .
317 Xml::submitButton( wfMsg( 'saveusergroups' ), array( 'name' => 'saveusergroups' ) ) .
318 "</td>
319 </tr>
320 </table>\n" .
321 Xml::closeElement( 'fieldset' ) .
322 Xml::closeElement( 'form' ) . "\n"
323 );
324 }
325
326 /**
327 * Format a link to a group description page
328 *
329 * @param string $group
330 * @return string
331 */
332 private static function buildGroupLink( $group ) {
333 static $cache = array();
334 if( !isset( $cache[$group] ) )
335 $cache[$group] = User::makeGroupLinkHtml( $group, User::getGroupMember( $group ) );
336 return $cache[$group];
337 }
338
339 /**
340 * Prepare a list of groups the user is able to add and remove
341 *
342 * @return string
343 */
344 private function explainRights() {
345 global $wgUser, $wgLang;
346
347 $out = array();
348 list( $add, $remove ) = array_values( $this->changeableGroups() );
349
350 if( count( $add ) > 0 )
351 $out[] = wfMsgExt( 'userrights-available-add', 'parseinline', $wgLang->listToText( $add ) );
352 if( count( $remove ) > 0 )
353 $out[] = wfMsgExt( 'userrights-available-remove', 'parseinline', $wgLang->listToText( $remove ) );
354
355 return count( $out ) > 0
356 ? implode( ' ', $out )
357 : wfMsgExt( 'userrights-available-none', 'parseinline' );
358 }
359
360 /**
361 * Adds the <select> thingie where you can select what groups to remove
362 *
363 * @param array $groups The groups that can be removed
364 * @return string XHTML <select> element
365 */
366 private function removeSelect( $groups ) {
367 return $this->doSelect( $groups, 'removable' );
368 }
369
370 /**
371 * Adds the <select> thingie where you can select what groups to add
372 *
373 * @param array $groups The groups that can be added
374 * @return string XHTML <select> element
375 */
376 private function addSelect( $groups ) {
377 return $this->doSelect( $groups, 'available' );
378 }
379
380 /**
381 * Adds the <select> thingie where you can select what groups to add/remove
382 *
383 * @param array $groups The groups that can be added/removed
384 * @param string $name 'removable' or 'available'
385 * @return string XHTML <select> element
386 */
387 private function doSelect( $groups, $name ) {
388 $ret = wfMsgHtml( "{$this->mName}-groups$name" ) .
389 Xml::openElement( 'select', array(
390 'name' => "{$name}[]",
391 'multiple' => 'multiple',
392 'size' => '6',
393 'style' => 'width: 100%;'
394 )
395 );
396 foreach ($groups as $group) {
397 $ret .= Xml::element( 'option', array( 'value' => $group ), User::getGroupName( $group ) );
398 }
399 $ret .= Xml::closeElement( 'select' );
400 return $ret;
401 }
402
403 /**
404 * @param string $group The name of the group to check
405 * @return bool Can we remove the group?
406 */
407 private function canRemove( $group ) {
408 // $this->changeableGroups()['remove'] doesn't work, of course. Thanks,
409 // PHP.
410 $groups = $this->changeableGroups();
411 return in_array( $group, $groups['remove'] );
412 }
413
414 /**
415 * @param string $group The name of the group to check
416 * @return bool Can we add the group?
417 */
418 private function canAdd( $group ) {
419 $groups = $this->changeableGroups();
420 return in_array( $group, $groups['add'] );
421 }
422
423 /**
424 * Returns an array of the groups that the user can add/remove.
425 *
426 * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) )
427 */
428 function changeableGroups() {
429 global $wgUser;
430
431 if( $wgUser->isAllowed( 'userrights' ) ) {
432 // This group gives the right to modify everything (reverse-
433 // compatibility with old "userrights lets you change
434 // everything")
435 // Using array_merge to make the groups reindexed
436 $all = array_merge( User::getAllGroups() );
437 return array(
438 'add' => $all,
439 'remove' => $all
440 );
441 }
442
443 // Okay, it's not so simple, we will have to go through the arrays
444 $groups = array( 'add' => array(), 'remove' => array() );
445 $addergroups = $wgUser->getEffectiveGroups();
446
447 foreach ($addergroups as $addergroup) {
448 $groups = array_merge_recursive(
449 $groups, $this->changeableByGroup($addergroup)
450 );
451 $groups['add'] = array_unique( $groups['add'] );
452 $groups['remove'] = array_unique( $groups['remove'] );
453 }
454 return $groups;
455 }
456
457 /**
458 * Returns an array of the groups that a particular group can add/remove.
459 *
460 * @param String $group The group to check for whether it can add/remove
461 * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) )
462 */
463 private function changeableByGroup( $group ) {
464 global $wgAddGroups, $wgRemoveGroups;
465
466 $groups = array( 'add' => array(), 'remove' => array() );
467 if( empty($wgAddGroups[$group]) ) {
468 // Don't add anything to $groups
469 } elseif( $wgAddGroups[$group] === true ) {
470 // You get everything
471 $groups['add'] = User::getAllGroups();
472 } elseif( is_array($wgAddGroups[$group]) ) {
473 $groups['add'] = $wgAddGroups[$group];
474 }
475
476 // Same thing for remove
477 if( empty($wgRemoveGroups[$group]) ) {
478 } elseif($wgRemoveGroups[$group] === true ) {
479 $groups['remove'] = User::getAllGroups();
480 } elseif( is_array($wgRemoveGroups[$group]) ) {
481 $groups['remove'] = $wgRemoveGroups[$group];
482 }
483 return $groups;
484 }
485
486 /**
487 * Show a rights log fragment for the specified user
488 *
489 * @param User $user User to show log for
490 * @param OutputPage $output OutputPage to use
491 */
492 protected function showLogFragment( $user, $output ) {
493 $viewer = new LogViewer(
494 new LogReader(
495 new FauxRequest(
496 array(
497 'type' => 'rights',
498 'page' => $user->getUserPage()->getPrefixedText(),
499 )
500 )
501 )
502 );
503 $output->addHtml( "<h2>" . htmlspecialchars( LogPage::logName( 'rights' ) ) . "</h2>\n" );
504 $viewer->showList( $output );
505 }
506
507 }