5dfe4dacb6bc9c175c2a1558e8d79896e637ad2e
[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( 'member' ),
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 $wgOut->addHTML(
274 Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->action, 'name' => 'editGroup' ) ) .
275 Xml::hidden( 'user-editname', $user->getName() ) .
276 Xml::hidden( 'wpEditToken', $wgUser->editToken( $user->getName() ) ) .
277 Xml::openElement( 'fieldset' ) .
278 Xml::element( 'legend', array(), wfMsg( 'userrights-editusergroup' ) ) .
279 wfMsgExt( 'editinguser', array( 'parse' ),
280 wfEscapeWikiText( $user->getName() ) ) .
281 $this->explainRights() .
282 "<table border='0'>
283 <tr>
284 <td></td>
285 <td>
286 <table width='400'>
287 <tr>
288 <td width='50%'>" . $this->removeSelect( $removable ) . "</td>
289 <td width='50%'>" . $this->addSelect( $addable ) . "</td>
290 </tr>
291 </table>
292 </tr>
293 <tr>
294 <td colspan='2'>" .
295 $wgOut->parse( wfMsg('userrights-groupshelp') ) .
296 "</td>
297 </tr>
298 <tr>
299 <td>" .
300 Xml::label( wfMsg( 'userrights-reason' ), 'wpReason' ) .
301 "</td>
302 <td>" .
303 Xml::input( 'user-reason', 60, false, array( 'id' => 'wpReason', 'maxlength' => 255 ) ) .
304 "</td>
305 </tr>
306 <tr>
307 <td></td>
308 <td>" .
309 Xml::submitButton( wfMsg( 'saveusergroups' ), array( 'name' => 'saveusergroups' ) ) .
310 "</td>
311 </tr>
312 </table>\n" .
313 Xml::closeElement( 'fieldset' ) .
314 Xml::closeElement( 'form' ) . "\n"
315 );
316 }
317
318 /**
319 * Prepare a list of groups the user is able to add and remove
320 *
321 * @return string
322 */
323 private function explainRights() {
324 global $wgUser, $wgLang;
325
326 $out = array();
327 list( $add, $remove ) = array_values( $this->changeableGroups() );
328
329 if( count( $add ) > 0 )
330 $out[] = wfMsgExt( 'userrights-available-add', 'parseinline', $wgLang->listToText( $add ) );
331 if( count( $remove ) > 0 )
332 $out[] = wfMsgExt( 'userrights-available-remove', 'parseinline', $wgLang->listToText( $remove ) );
333
334 return count( $out ) > 0
335 ? implode( ' ', $out )
336 : wfMsgExt( 'userrights-available-none', 'parseinline' );
337 }
338
339 /**
340 * Adds the <select> thingie where you can select what groups to remove
341 *
342 * @param array $groups The groups that can be removed
343 * @return string XHTML <select> element
344 */
345 private function removeSelect( $groups ) {
346 return $this->doSelect( $groups, 'member' );
347 }
348
349 /**
350 * Adds the <select> thingie where you can select what groups to add
351 *
352 * @param array $groups The groups that can be added
353 * @return string XHTML <select> element
354 */
355 private function addSelect( $groups ) {
356 return $this->doSelect( $groups, 'available' );
357 }
358
359 /**
360 * Adds the <select> thingie where you can select what groups to add/remove
361 *
362 * @param array $groups The groups that can be added/removed
363 * @param string $name 'member' or 'available'
364 * @return string XHTML <select> element
365 */
366 private function doSelect( $groups, $name ) {
367 $ret = wfMsgHtml( "{$this->mName}-groups$name" ) .
368 Xml::openElement( 'select', array(
369 'name' => "{$name}[]",
370 'multiple' => 'multiple',
371 'size' => '6',
372 'style' => 'width: 100%;'
373 )
374 );
375 foreach ($groups as $group) {
376 $ret .= Xml::element( 'option', array( 'value' => $group ), User::getGroupName( $group ) );
377 }
378 $ret .= Xml::closeElement( 'select' );
379 return $ret;
380 }
381
382 /**
383 * @param string $group The name of the group to check
384 * @return bool Can we remove the group?
385 */
386 private function canRemove( $group ) {
387 // $this->changeableGroups()['remove'] doesn't work, of course. Thanks,
388 // PHP.
389 $groups = $this->changeableGroups();
390 return in_array( $group, $groups['remove'] );
391 }
392
393 /**
394 * @param string $group The name of the group to check
395 * @return bool Can we add the group?
396 */
397 private function canAdd( $group ) {
398 $groups = $this->changeableGroups();
399 return in_array( $group, $groups['add'] );
400 }
401
402 /**
403 * Returns an array of the groups that the user can add/remove.
404 *
405 * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) )
406 */
407 function changeableGroups() {
408 global $wgUser;
409
410 if( $wgUser->isAllowed( 'userrights' ) ) {
411 // This group gives the right to modify everything (reverse-
412 // compatibility with old "userrights lets you change
413 // everything")
414 // Using array_merge to make the groups reindexed
415 $all = array_merge( User::getAllGroups() );
416 return array(
417 'add' => $all,
418 'remove' => $all
419 );
420 }
421
422 // Okay, it's not so simple, we will have to go through the arrays
423 $groups = array( 'add' => array(), 'remove' => array() );
424 $addergroups = $wgUser->getEffectiveGroups();
425
426 foreach ($addergroups as $addergroup) {
427 $groups = array_merge_recursive(
428 $groups, $this->changeableByGroup($addergroup)
429 );
430 $groups['add'] = array_unique( $groups['add'] );
431 $groups['remove'] = array_unique( $groups['remove'] );
432 }
433 return $groups;
434 }
435
436 /**
437 * Returns an array of the groups that a particular group can add/remove.
438 *
439 * @param String $group The group to check for whether it can add/remove
440 * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) )
441 */
442 private function changeableByGroup( $group ) {
443 global $wgAddGroups, $wgRemoveGroups;
444
445 $groups = array( 'add' => array(), 'remove' => array() );
446 if( empty($wgAddGroups[$group]) ) {
447 // Don't add anything to $groups
448 } elseif( $wgAddGroups[$group] === true ) {
449 // You get everything
450 $groups['add'] = User::getAllGroups();
451 } elseif( is_array($wgAddGroups[$group]) ) {
452 $groups['add'] = $wgAddGroups[$group];
453 }
454
455 // Same thing for remove
456 if( empty($wgRemoveGroups[$group]) ) {
457 } elseif($wgRemoveGroups[$group] === true ) {
458 $groups['remove'] = User::getAllGroups();
459 } elseif( is_array($wgRemoveGroups[$group]) ) {
460 $groups['remove'] = $wgRemoveGroups[$group];
461 }
462 return $groups;
463 }
464
465 /**
466 * Show a rights log fragment for the specified user
467 *
468 * @param User $user User to show log for
469 * @param OutputPage $output OutputPage to use
470 */
471 protected function showLogFragment( $user, $output ) {
472 $viewer = new LogViewer(
473 new LogReader(
474 new FauxRequest(
475 array(
476 'type' => 'rights',
477 'page' => $user->getUserPage()->getPrefixedText(),
478 )
479 )
480 )
481 );
482 $output->addHtml( "<h2>" . htmlspecialchars( LogPage::logName( 'rights' ) ) . "</h2>\n" );
483 $viewer->showList( $output );
484 }
485
486 }