PageRenderTime 48ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/includes/specials/SpecialUserrights.php

https://bitbucket.org/brunodefraine/mediawiki
PHP | 608 lines | 378 code | 75 blank | 155 comment | 75 complexity | f71af9a587291cc7dd3a920aafa406cc MD5 | raw file
Possible License(s): GPL-2.0, Apache-2.0, LGPL-3.0
  1. <?php
  2. /**
  3. * Implements Special:Userrights
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. * @ingroup SpecialPage
  22. */
  23. /**
  24. * Special page to allow managing user group membership
  25. *
  26. * @ingroup SpecialPage
  27. */
  28. class UserrightsPage extends SpecialPage {
  29. # The target of the local right-adjuster's interest. Can be gotten from
  30. # either a GET parameter or a subpage-style parameter, so have a member
  31. # variable for it.
  32. protected $mTarget;
  33. protected $isself = false;
  34. public function __construct() {
  35. parent::__construct( 'Userrights' );
  36. }
  37. public function isRestricted() {
  38. return true;
  39. }
  40. public function userCanExecute( User $user ) {
  41. return $this->userCanChangeRights( $user, false );
  42. }
  43. public function userCanChangeRights( $user, $checkIfSelf = true ) {
  44. $available = $this->changeableGroups();
  45. return !empty( $available['add'] )
  46. || !empty( $available['remove'] )
  47. || ( ( $this->isself || !$checkIfSelf ) &&
  48. ( !empty( $available['add-self'] )
  49. || !empty( $available['remove-self'] ) ) );
  50. }
  51. /**
  52. * Manage forms to be shown according to posted data.
  53. * Depending on the submit button used, call a form or a save function.
  54. *
  55. * @param $par Mixed: string if any subpage provided, else null
  56. */
  57. public function execute( $par ) {
  58. // If the visitor doesn't have permissions to assign or remove
  59. // any groups, it's a bit silly to give them the user search prompt.
  60. $user = $this->getUser();
  61. /*
  62. * If the user is blocked and they only have "partial" access
  63. * (e.g. they don't have the userrights permission), then don't
  64. * allow them to use Special:UserRights.
  65. */
  66. if( $user->isBlocked() && !$user->isAllowed( 'userrights' ) ) {
  67. throw new UserBlockedError( $user->mBlock );
  68. }
  69. $request = $this->getRequest();
  70. if( $par !== null ) {
  71. $this->mTarget = $par;
  72. } else {
  73. $this->mTarget = $request->getVal( 'user' );
  74. }
  75. $available = $this->changeableGroups();
  76. if ( $this->mTarget === null ) {
  77. /*
  78. * If the user specified no target, and they can only
  79. * edit their own groups, automatically set them as the
  80. * target.
  81. */
  82. if ( !count( $available['add'] ) && !count( $available['remove'] ) )
  83. $this->mTarget = $user->getName();
  84. }
  85. if ( User::getCanonicalName( $this->mTarget ) == $user->getName() ) {
  86. $this->isself = true;
  87. }
  88. if( !$this->userCanChangeRights( $user, true ) ) {
  89. // @todo FIXME: There may be intermediate groups we can mention.
  90. $msg = $user->isAnon() ? 'userrights-nologin' : 'userrights-notallowed';
  91. throw new PermissionsError( null, array( array( $msg ) ) );
  92. }
  93. $this->checkReadOnly();
  94. $this->setHeaders();
  95. $this->outputHeader();
  96. $out = $this->getOutput();
  97. $out->addModuleStyles( 'mediawiki.special' );
  98. // show the general form
  99. if ( count( $available['add'] ) || count( $available['remove'] ) ) {
  100. $this->switchForm();
  101. }
  102. if( $request->wasPosted() ) {
  103. // save settings
  104. if( $request->getCheck( 'saveusergroups' ) ) {
  105. $reason = $request->getVal( 'user-reason' );
  106. $tok = $request->getVal( 'wpEditToken' );
  107. if( $user->matchEditToken( $tok, $this->mTarget ) ) {
  108. $this->saveUserGroups(
  109. $this->mTarget,
  110. $reason
  111. );
  112. $out->redirect( $this->getSuccessURL() );
  113. return;
  114. }
  115. }
  116. }
  117. // show some more forms
  118. if( $this->mTarget !== null ) {
  119. $this->editUserGroupsForm( $this->mTarget );
  120. }
  121. }
  122. function getSuccessURL() {
  123. return $this->getTitle( $this->mTarget )->getFullURL();
  124. }
  125. /**
  126. * Save user groups changes in the database.
  127. * Data comes from the editUserGroupsForm() form function
  128. *
  129. * @param $username String: username to apply changes to.
  130. * @param $reason String: reason for group change
  131. * @return null
  132. */
  133. function saveUserGroups( $username, $reason = '' ) {
  134. $status = $this->fetchUser( $username );
  135. if( !$status->isOK() ) {
  136. $this->getOutput()->addWikiText( $status->getWikiText() );
  137. return;
  138. } else {
  139. $user = $status->value;
  140. }
  141. $allgroups = $this->getAllGroups();
  142. $addgroup = array();
  143. $removegroup = array();
  144. // This could possibly create a highly unlikely race condition if permissions are changed between
  145. // when the form is loaded and when the form is saved. Ignoring it for the moment.
  146. foreach ( $allgroups as $group ) {
  147. // We'll tell it to remove all unchecked groups, and add all checked groups.
  148. // Later on, this gets filtered for what can actually be removed
  149. if ( $this->getRequest()->getCheck( "wpGroup-$group" ) ) {
  150. $addgroup[] = $group;
  151. } else {
  152. $removegroup[] = $group;
  153. }
  154. }
  155. $this->doSaveUserGroups( $user, $addgroup, $removegroup, $reason );
  156. }
  157. /**
  158. * Save user groups changes in the database.
  159. *
  160. * @param $user User object
  161. * @param $add Array of groups to add
  162. * @param $remove Array of groups to remove
  163. * @param $reason String: reason for group change
  164. * @return Array: Tuple of added, then removed groups
  165. */
  166. function doSaveUserGroups( $user, $add, $remove, $reason = '' ) {
  167. // Validate input set...
  168. $isself = ( $user->getName() == $this->getUser()->getName() );
  169. $groups = $user->getGroups();
  170. $changeable = $this->changeableGroups();
  171. $addable = array_merge( $changeable['add'], $isself ? $changeable['add-self'] : array() );
  172. $removable = array_merge( $changeable['remove'], $isself ? $changeable['remove-self'] : array() );
  173. $remove = array_unique(
  174. array_intersect( (array)$remove, $removable, $groups ) );
  175. $add = array_unique( array_diff(
  176. array_intersect( (array)$add, $addable ),
  177. $groups )
  178. );
  179. $oldGroups = $user->getGroups();
  180. $newGroups = $oldGroups;
  181. // remove then add groups
  182. if( $remove ) {
  183. $newGroups = array_diff( $newGroups, $remove );
  184. foreach( $remove as $group ) {
  185. $user->removeGroup( $group );
  186. }
  187. }
  188. if( $add ) {
  189. $newGroups = array_merge( $newGroups, $add );
  190. foreach( $add as $group ) {
  191. $user->addGroup( $group );
  192. }
  193. }
  194. $newGroups = array_unique( $newGroups );
  195. // Ensure that caches are cleared
  196. $user->invalidateCache();
  197. wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) );
  198. wfDebug( 'newGroups: ' . print_r( $newGroups, true ) );
  199. wfRunHooks( 'UserRights', array( &$user, $add, $remove ) );
  200. if( $newGroups != $oldGroups ) {
  201. $this->addLogEntry( $user, $oldGroups, $newGroups, $reason );
  202. }
  203. return array( $add, $remove );
  204. }
  205. /**
  206. * Add a rights log entry for an action.
  207. */
  208. function addLogEntry( $user, $oldGroups, $newGroups, $reason ) {
  209. $log = new LogPage( 'rights' );
  210. $log->addEntry( 'rights',
  211. $user->getUserPage(),
  212. $reason,
  213. array(
  214. $this->makeGroupNameListForLog( $oldGroups ),
  215. $this->makeGroupNameListForLog( $newGroups )
  216. )
  217. );
  218. }
  219. /**
  220. * Edit user groups membership
  221. * @param $username String: name of the user.
  222. */
  223. function editUserGroupsForm( $username ) {
  224. $status = $this->fetchUser( $username );
  225. if( !$status->isOK() ) {
  226. $this->getOutput()->addWikiText( $status->getWikiText() );
  227. return;
  228. } else {
  229. $user = $status->value;
  230. }
  231. $groups = $user->getGroups();
  232. $this->showEditUserGroupsForm( $user, $groups );
  233. // This isn't really ideal logging behavior, but let's not hide the
  234. // interwiki logs if we're using them as is.
  235. $this->showLogFragment( $user, $this->getOutput() );
  236. }
  237. /**
  238. * Normalize the input username, which may be local or remote, and
  239. * return a user (or proxy) object for manipulating it.
  240. *
  241. * Side effects: error output for invalid access
  242. * @return Status object
  243. */
  244. public function fetchUser( $username ) {
  245. global $wgUserrightsInterwikiDelimiter;
  246. $parts = explode( $wgUserrightsInterwikiDelimiter, $username );
  247. if( count( $parts ) < 2 ) {
  248. $name = trim( $username );
  249. $database = '';
  250. } else {
  251. list( $name, $database ) = array_map( 'trim', $parts );
  252. if( $database == wfWikiID() ) {
  253. $database = '';
  254. } else {
  255. if( !$this->getUser()->isAllowed( 'userrights-interwiki' ) ) {
  256. return Status::newFatal( 'userrights-no-interwiki' );
  257. }
  258. if( !UserRightsProxy::validDatabase( $database ) ) {
  259. return Status::newFatal( 'userrights-nodatabase', $database );
  260. }
  261. }
  262. }
  263. if( $name === '' ) {
  264. return Status::newFatal( 'nouserspecified' );
  265. }
  266. if( $name[0] == '#' ) {
  267. // Numeric ID can be specified...
  268. // We'll do a lookup for the name internally.
  269. $id = intval( substr( $name, 1 ) );
  270. if( $database == '' ) {
  271. $name = User::whoIs( $id );
  272. } else {
  273. $name = UserRightsProxy::whoIs( $database, $id );
  274. }
  275. if( !$name ) {
  276. return Status::newFatal( 'noname' );
  277. }
  278. } else {
  279. $name = User::getCanonicalName( $name );
  280. if( $name === false ) {
  281. // invalid name
  282. return Status::newFatal( 'nosuchusershort', $username );
  283. }
  284. }
  285. if( $database == '' ) {
  286. $user = User::newFromName( $name );
  287. } else {
  288. $user = UserRightsProxy::newFromName( $database, $name );
  289. }
  290. if( !$user || $user->isAnon() ) {
  291. return Status::newFatal( 'nosuchusershort', $username );
  292. }
  293. return Status::newGood( $user );
  294. }
  295. function makeGroupNameList( $ids ) {
  296. if( empty( $ids ) ) {
  297. return wfMsgForContent( 'rightsnone' );
  298. } else {
  299. return implode( ', ', $ids );
  300. }
  301. }
  302. function makeGroupNameListForLog( $ids ) {
  303. if( empty( $ids ) ) {
  304. return '';
  305. } else {
  306. return $this->makeGroupNameList( $ids );
  307. }
  308. }
  309. /**
  310. * Output a form to allow searching for a user
  311. */
  312. function switchForm() {
  313. global $wgScript;
  314. $this->getOutput()->addHTML(
  315. Html::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'name' => 'uluser', 'id' => 'mw-userrights-form1' ) ) .
  316. Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) .
  317. Xml::fieldset( wfMsg( 'userrights-lookup-user' ) ) .
  318. Xml::inputLabel( wfMsg( 'userrights-user-editname' ), 'user', 'username', 30, str_replace( '_', ' ', $this->mTarget ) ) . ' ' .
  319. Xml::submitButton( wfMsg( 'editusergroup' ) ) .
  320. Html::closeElement( 'fieldset' ) .
  321. Html::closeElement( 'form' ) . "\n"
  322. );
  323. }
  324. /**
  325. * Go through used and available groups and return the ones that this
  326. * form will be able to manipulate based on the current user's system
  327. * permissions.
  328. *
  329. * @param $groups Array: list of groups the given user is in
  330. * @return Array: Tuple of addable, then removable groups
  331. */
  332. protected function splitGroups( $groups ) {
  333. list( $addable, $removable, $addself, $removeself ) = array_values( $this->changeableGroups() );
  334. $removable = array_intersect(
  335. array_merge( $this->isself ? $removeself : array(), $removable ),
  336. $groups
  337. ); // Can't remove groups the user doesn't have
  338. $addable = array_diff(
  339. array_merge( $this->isself ? $addself : array(), $addable ),
  340. $groups
  341. ); // Can't add groups the user does have
  342. return array( $addable, $removable );
  343. }
  344. /**
  345. * Show the form to edit group memberships.
  346. *
  347. * @param $user User or UserRightsProxy you're editing
  348. * @param $groups Array: Array of groups the user is in
  349. */
  350. protected function showEditUserGroupsForm( $user, $groups ) {
  351. $list = array();
  352. foreach( $groups as $group ) {
  353. $list[] = self::buildGroupLink( $group );
  354. }
  355. $autolist = array();
  356. if ( $user instanceof User ) {
  357. foreach( Autopromote::getAutopromoteGroups( $user ) as $group ) {
  358. $autolist[] = self::buildGroupLink( $group );
  359. }
  360. }
  361. $grouplist = '';
  362. $count = count( $list );
  363. if( $count > 0 ) {
  364. $grouplist = wfMessage( 'userrights-groupsmember', $count)->parse();
  365. $grouplist = '<p>' . $grouplist . ' ' . $this->getLanguage()->listToText( $list ) . "</p>\n";
  366. }
  367. $count = count( $autolist );
  368. if( $count > 0 ) {
  369. $autogrouplistintro = wfMessage( 'userrights-groupsmember-auto', $count)->parse();
  370. $grouplist .= '<p>' . $autogrouplistintro . ' ' . $this->getLanguage()->listToText( $autolist ) . "</p>\n";
  371. }
  372. $userToolLinks = Linker::userToolLinks(
  373. $user->getId(),
  374. $user->getName(),
  375. false, /* default for redContribsWhenNoEdits */
  376. Linker::TOOL_LINKS_EMAIL /* Add "send e-mail" link */
  377. );
  378. $this->getOutput()->addHTML(
  379. Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->getLocalURL(), 'name' => 'editGroup', 'id' => 'mw-userrights-form2' ) ) .
  380. Html::hidden( 'user', $this->mTarget ) .
  381. Html::hidden( 'wpEditToken', $this->getUser()->getEditToken( $this->mTarget ) ) .
  382. Xml::openElement( 'fieldset' ) .
  383. Xml::element( 'legend', array(), wfMessage( 'userrights-editusergroup', $user->getName() )->text() ) .
  384. wfMessage( 'editinguser' )->params( wfEscapeWikiText( $user->getName() ) )->rawParams( $userToolLinks )->parse() .
  385. wfMessage( 'userrights-groups-help', $user->getName() )->parse() .
  386. $grouplist .
  387. Xml::tags( 'p', null, $this->groupCheckboxes( $groups, $user ) ) .
  388. Xml::openElement( 'table', array( 'border' => '0', 'id' => 'mw-userrights-table-outer' ) ) .
  389. "<tr>
  390. <td class='mw-label'>" .
  391. Xml::label( wfMsg( 'userrights-reason' ), 'wpReason' ) .
  392. "</td>
  393. <td class='mw-input'>" .
  394. Xml::input( 'user-reason', 60, $this->getRequest()->getVal( 'user-reason', false ),
  395. array( 'id' => 'wpReason', 'maxlength' => 255 ) ) .
  396. "</td>
  397. </tr>
  398. <tr>
  399. <td></td>
  400. <td class='mw-submit'>" .
  401. Xml::submitButton( wfMsg( 'saveusergroups' ),
  402. array( 'name' => 'saveusergroups' ) + Linker::tooltipAndAccesskeyAttribs( 'userrights-set' ) ) .
  403. "</td>
  404. </tr>" .
  405. Xml::closeElement( 'table' ) . "\n" .
  406. Xml::closeElement( 'fieldset' ) .
  407. Xml::closeElement( 'form' ) . "\n"
  408. );
  409. }
  410. /**
  411. * Format a link to a group description page
  412. *
  413. * @param $group string
  414. * @return string
  415. */
  416. private static function buildGroupLink( $group ) {
  417. static $cache = array();
  418. if( !isset( $cache[$group] ) )
  419. $cache[$group] = User::makeGroupLinkHtml( $group, htmlspecialchars( User::getGroupName( $group ) ) );
  420. return $cache[$group];
  421. }
  422. /**
  423. * Returns an array of all groups that may be edited
  424. * @return array Array of groups that may be edited.
  425. */
  426. protected static function getAllGroups() {
  427. return User::getAllGroups();
  428. }
  429. /**
  430. * Adds a table with checkboxes where you can select what groups to add/remove
  431. *
  432. * @todo Just pass the username string?
  433. * @param $usergroups Array: groups the user belongs to
  434. * @param $user User a user object
  435. * @return string XHTML table element with checkboxes
  436. */
  437. private function groupCheckboxes( $usergroups, $user ) {
  438. $allgroups = $this->getAllGroups();
  439. $ret = '';
  440. # Put all column info into an associative array so that extensions can
  441. # more easily manage it.
  442. $columns = array( 'unchangeable' => array(), 'changeable' => array() );
  443. foreach( $allgroups as $group ) {
  444. $set = in_array( $group, $usergroups );
  445. # Should the checkbox be disabled?
  446. $disabled = !(
  447. ( $set && $this->canRemove( $group ) ) ||
  448. ( !$set && $this->canAdd( $group ) ) );
  449. # Do we need to point out that this action is irreversible?
  450. $irreversible = !$disabled && (
  451. ( $set && !$this->canAdd( $group ) ) ||
  452. ( !$set && !$this->canRemove( $group ) ) );
  453. $checkbox = array(
  454. 'set' => $set,
  455. 'disabled' => $disabled,
  456. 'irreversible' => $irreversible
  457. );
  458. if( $disabled ) {
  459. $columns['unchangeable'][$group] = $checkbox;
  460. } else {
  461. $columns['changeable'][$group] = $checkbox;
  462. }
  463. }
  464. # Build the HTML table
  465. $ret .= Xml::openElement( 'table', array( 'border' => '0', 'class' => 'mw-userrights-groups' ) ) .
  466. "<tr>\n";
  467. foreach( $columns as $name => $column ) {
  468. if( $column === array() )
  469. continue;
  470. $ret .= Xml::element( 'th', null, wfMessage( 'userrights-' . $name . '-col', count( $column ) )->text() );
  471. }
  472. $ret.= "</tr>\n<tr>\n";
  473. foreach( $columns as $column ) {
  474. if( $column === array() )
  475. continue;
  476. $ret .= "\t<td style='vertical-align:top;'>\n";
  477. foreach( $column as $group => $checkbox ) {
  478. $attr = $checkbox['disabled'] ? array( 'disabled' => 'disabled' ) : array();
  479. $member = User::getGroupMember( $group, $user->getName() );
  480. if ( $checkbox['irreversible'] ) {
  481. $text = wfMessage( 'userrights-irreversible-marker', $member )->escaped();
  482. } else {
  483. $text = htmlspecialchars( $member );
  484. }
  485. $checkboxHtml = Xml::checkLabel( $text, "wpGroup-" . $group,
  486. "wpGroup-" . $group, $checkbox['set'], $attr );
  487. $ret .= "\t\t" . ( $checkbox['disabled']
  488. ? Xml::tags( 'span', array( 'class' => 'mw-userrights-disabled' ), $checkboxHtml )
  489. : $checkboxHtml
  490. ) . "<br />\n";
  491. }
  492. $ret .= "\t</td>\n";
  493. }
  494. $ret .= Xml::closeElement( 'tr' ) . Xml::closeElement( 'table' );
  495. return $ret;
  496. }
  497. /**
  498. * @param $group String: the name of the group to check
  499. * @return bool Can we remove the group?
  500. */
  501. private function canRemove( $group ) {
  502. // $this->changeableGroups()['remove'] doesn't work, of course. Thanks,
  503. // PHP.
  504. $groups = $this->changeableGroups();
  505. return in_array( $group, $groups['remove'] ) || ( $this->isself && in_array( $group, $groups['remove-self'] ) );
  506. }
  507. /**
  508. * @param $group string: the name of the group to check
  509. * @return bool Can we add the group?
  510. */
  511. private function canAdd( $group ) {
  512. $groups = $this->changeableGroups();
  513. return in_array( $group, $groups['add'] ) || ( $this->isself && in_array( $group, $groups['add-self'] ) );
  514. }
  515. /**
  516. * Returns $this->getUser()->changeableGroups()
  517. *
  518. * @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) , 'add-self' => array( addablegroups to self), 'remove-self' => array( removable groups from self) )
  519. */
  520. function changeableGroups() {
  521. return $this->getUser()->changeableGroups();
  522. }
  523. /**
  524. * Show a rights log fragment for the specified user
  525. *
  526. * @param $user User to show log for
  527. * @param $output OutputPage to use
  528. */
  529. protected function showLogFragment( $user, $output ) {
  530. $output->addHTML( Xml::element( 'h2', null, LogPage::logName( 'rights' ) . "\n" ) );
  531. LogEventsList::showLogExtract( $output, 'rights', $user->getUserPage() );
  532. }
  533. }