* Tweak userrights messages a bit and add PLURAL for better localization
[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 $username = $wgRequest->getText( 'user-editname' );
236 $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->action, 'name' => 'uluser' ) );
237 $form .= '<fieldset><legend>' . wfMsgHtml( 'userrights-lookup-user' ) . '</legend>';
238 $form .= '<p>' . Xml::inputLabel( wfMsg( 'userrights-user-editname' ), 'user-editname', 'username', 30, $username ) . '</p>';
239 $form .= '<p>' . Xml::submitButton( wfMsg( 'editusergroup' ), array( 'name' => 'ssearchuser' ) ) . '</p>';
240 $form .= '</fieldset>';
241 $form .= '</form>';
242 $wgOut->addHTML( $form );
243 }
244
245 /**
246 * Go through used and available groups and return the ones that this
247 * form will be able to manipulate based on the current user's system
248 * permissions.
249 *
250 * @param $groups Array: list of groups the given user is in
251 * @return Array: Tuple of addable, then removable groups
252 */
253 protected function splitGroups( $groups ) {
254 list($addable, $removable) = array_values( $this->changeableGroups() );
255 $removable = array_intersect($removable, $groups ); // Can't remove groups the user doesn't have
256 $addable = array_diff( $addable, $groups ); // Can't add groups the user does have
257
258 return array( $addable, $removable );
259 }
260
261 /**
262 * Show the form to edit group memberships.
263 *
264 * @todo make all CSS-y and semantic
265 * @param $user User or UserRightsProxy you're editing
266 * @param $groups Array: Array of groups the user is in
267 */
268 protected function showEditUserGroupsForm( $user, $groups ) {
269 global $wgOut, $wgUser;
270
271 list( $addable, $removable ) = $this->splitGroups( $groups );
272
273 $list = array();
274 foreach( $user->getGroups() as $group )
275 $list[] = self::buildGroupLink( $group );
276
277 $grouplist = '';
278 if( count( $list ) > 0 ) {
279 $grouplist = '<p>' . wfMsgHtml( 'userrights-groupsmember' ) . ' ' . implode( ', ', $list ) . '</p>';
280 }
281 $wgOut->addHTML(
282 Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->action, 'name' => 'editGroup' ) ) .
283 Xml::hidden( 'user-editname', $user->getName() ) .
284 Xml::hidden( 'wpEditToken', $wgUser->editToken( $user->getName() ) ) .
285 Xml::openElement( 'fieldset' ) .
286 Xml::element( 'legend', array(), wfMsg( 'userrights-editusergroup' ) ) .
287 wfMsgExt( 'editinguser', array( 'parse' ),
288 wfEscapeWikiText( $user->getName() ) ) .
289 $grouplist .
290 $this->explainRights() .
291 "<table border='0'>
292 <tr>
293 <td></td>
294 <td>
295 <table width='400'>
296 <tr>
297 <td width='50%'>" . $this->removeSelect( $removable ) . "</td>
298 <td width='50%'>" . $this->addSelect( $addable ) . "</td>
299 </tr>
300 </table>
301 </tr>
302 <tr>
303 <td colspan='2'>" .
304 $wgOut->parse( wfMsg('userrights-groupshelp') ) .
305 "</td>
306 </tr>
307 <tr>
308 <td>" .
309 Xml::label( wfMsg( 'userrights-reason' ), 'wpReason' ) .
310 "</td>
311 <td>" .
312 Xml::input( 'user-reason', 60, false, array( 'id' => 'wpReason', 'maxlength' => 255 ) ) .
313 "</td>
314 </tr>
315 <tr>
316 <td></td>
317 <td>" .
318 Xml::submitButton( wfMsg( 'saveusergroups' ), array( 'name' => 'saveusergroups' ) ) .
319 "</td>
320 </tr>
321 </table>\n" .
322 Xml::closeElement( 'fieldset' ) .
323 Xml::closeElement( 'form' ) . "\n"
324 );
325 }
326
327 /**
328 * Format a link to a group description page
329 *
330 * @param string $group
331 * @return string
332 */
333 private static function buildGroupLink( $group ) {
334 static $cache = array();
335 if( !isset( $cache[$group] ) )
336 $cache[$group] = User::makeGroupLinkHtml( $group, User::getGroupMember( $group ) );
337 return $cache[$group];
338 }
339
340 /**
341 * Prepare a list of groups the user is able to add and remove
342 *
343 * @return string
344 */
345 private function explainRights() {
346 global $wgUser, $wgLang;
347
348 $out = array();
349 list( $add, $remove ) = array_values( $this->changeableGroups() );
350
351 if( count( $add ) > 0 )
352 $out[] = wfMsgExt( 'userrights-available-add', 'parseinline', $wgLang->listToText( $add ), count( $add ) );
353 if( count( $remove ) > 0 )
354 $out[] = wfMsgExt( 'userrights-available-remove', 'parseinline', $wgLang->listToText( $remove ), count( $add ) );
355
356 return count( $out ) > 0
357 ? implode( '<br />', $out )
358 : wfMsgExt( 'userrights-available-none', 'parseinline' );
359 }
360
361 /**
362 * Adds the <select> thingie where you can select what groups to remove
363 *
364 * @param array $groups The groups that can be removed
365 * @return string XHTML <select> element
366 */
367 private function removeSelect( $groups ) {
368 return $this->doSelect( $groups, 'removable' );
369 }
370
371 /**
372 * Adds the <select> thingie where you can select what groups to add
373 *
374 * @param array $groups The groups that can be added
375 * @return string XHTML <select> element
376 */
377 private function addSelect( $groups ) {
378 return $this->doSelect( $groups, 'available' );
379 }
380
381 /**
382 * Adds the <select> thingie where you can select what groups to add/remove
383 *
384 * @param array $groups The groups that can be added/removed
385 * @param string $name 'removable' or 'available'
386 * @return string XHTML <select> element
387 */
388 private function doSelect( $groups, $name ) {
389 $ret = wfMsgHtml( "{$this->mName}-groups$name" ) .
390 Xml::openElement( 'select', array(
391 'name' => "{$name}[]",
392 'multiple' => 'multiple',
393 'size' => '6',
394 'style' => 'width: 100%;'
395 )
396 );
397 foreach ($groups as $group) {
398 $ret .= Xml::element( 'option', array( 'value' => $group ), User::getGroupName( $group ) );
399 }
400 $ret .= Xml::closeElement( 'select' );
401 return $ret;
402 }
403
404 /**
405 * @param string $group The name of the group to check
406 * @return bool Can we remove the group?
407 */
408 private function canRemove( $group ) {
409 // $this->changeableGroups()['remove'] doesn't work, of course. Thanks,
410 // PHP.
411 $groups = $this->changeableGroups();
412 return in_array( $group, $groups['remove'] );
413 }
414
415 /**
416 * @param string $group The name of the group to check
417 * @return bool Can we add the group?
418 */
419 private function canAdd( $group ) {
420 $groups = $this->changeableGroups();
421 return in_array( $group, $groups['add'] );
422 }
423
424 /**
425 * Returns an array of the groups that the user can add/remove.
426 *
427 * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) )
428 */
429 function changeableGroups() {
430 global $wgUser;
431
432 if( $wgUser->isAllowed( 'userrights' ) ) {
433 // This group gives the right to modify everything (reverse-
434 // compatibility with old "userrights lets you change
435 // everything")
436 // Using array_merge to make the groups reindexed
437 $all = array_merge( User::getAllGroups() );
438 return array(
439 'add' => $all,
440 'remove' => $all
441 );
442 }
443
444 // Okay, it's not so simple, we will have to go through the arrays
445 $groups = array( 'add' => array(), 'remove' => array() );
446 $addergroups = $wgUser->getEffectiveGroups();
447
448 foreach ($addergroups as $addergroup) {
449 $groups = array_merge_recursive(
450 $groups, $this->changeableByGroup($addergroup)
451 );
452 $groups['add'] = array_unique( $groups['add'] );
453 $groups['remove'] = array_unique( $groups['remove'] );
454 }
455 return $groups;
456 }
457
458 /**
459 * Returns an array of the groups that a particular group can add/remove.
460 *
461 * @param String $group The group to check for whether it can add/remove
462 * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) )
463 */
464 private function changeableByGroup( $group ) {
465 global $wgAddGroups, $wgRemoveGroups;
466
467 $groups = array( 'add' => array(), 'remove' => array() );
468 if( empty($wgAddGroups[$group]) ) {
469 // Don't add anything to $groups
470 } elseif( $wgAddGroups[$group] === true ) {
471 // You get everything
472 $groups['add'] = User::getAllGroups();
473 } elseif( is_array($wgAddGroups[$group]) ) {
474 $groups['add'] = $wgAddGroups[$group];
475 }
476
477 // Same thing for remove
478 if( empty($wgRemoveGroups[$group]) ) {
479 } elseif($wgRemoveGroups[$group] === true ) {
480 $groups['remove'] = User::getAllGroups();
481 } elseif( is_array($wgRemoveGroups[$group]) ) {
482 $groups['remove'] = $wgRemoveGroups[$group];
483 }
484 return $groups;
485 }
486
487 /**
488 * Show a rights log fragment for the specified user
489 *
490 * @param User $user User to show log for
491 * @param OutputPage $output OutputPage to use
492 */
493 protected function showLogFragment( $user, $output ) {
494 $viewer = new LogViewer(
495 new LogReader(
496 new FauxRequest(
497 array(
498 'type' => 'rights',
499 'page' => $user->getUserPage()->getPrefixedText(),
500 )
501 )
502 )
503 );
504 $output->addHtml( "<h2>" . htmlspecialchars( LogPage::logName( 'rights' ) ) . "</h2>\n" );
505 $viewer->showList( $output );
506 }
507
508 }