MediaWiki  master
SpecialUserrights.php
Go to the documentation of this file.
1 <?php
31 
37 class UserrightsPage extends SpecialPage {
44  protected $mTarget;
48  protected $mFetchedUser = null;
49  protected $isself = false;
50 
53 
55  private $userNameUtils;
56 
59 
61  private $userFactory;
62 
69  public function __construct(
70  UserGroupManagerFactory $userGroupManagerFactory = null,
74  ) {
75  parent::__construct( 'Userrights' );
76  $services = MediaWikiServices::getInstance();
77  // This class is extended and therefore falls back to global state - T263207
78  $this->userNameUtils = $userNameUtils ?? $services->getUserNameUtils();
79  $this->userNamePrefixSearch = $userNamePrefixSearch ?? $services->getUserNamePrefixSearch();
80  $this->userFactory = $userFactory ?? $services->getUserFactory();
81 
82  // TODO don't hard code false, use interwiki domains. See T14518
83  $this->userGroupManager = ( $userGroupManagerFactory ?? $services->getUserGroupManagerFactory() )
84  ->getUserGroupManager( false );
85  }
86 
87  public function doesWrites() {
88  return true;
89  }
90 
100  public function userCanChangeRights( UserIdentity $targetUser, $checkIfSelf = true ) {
101  $isself = $this->getUser()->equals( $targetUser );
102 
103  $available = $this->changeableGroups();
104  if ( $targetUser->getId() === 0 ) {
105  return false;
106  }
107 
108  if ( $available['add'] || $available['remove'] ) {
109  // can change some rights for any user
110  return true;
111  }
112 
113  if ( ( $available['add-self'] || $available['remove-self'] )
114  && ( $isself || !$checkIfSelf )
115  ) {
116  // can change some rights for self
117  return true;
118  }
119 
120  return false;
121  }
122 
130  public function execute( $par ) {
131  $user = $this->getUser();
132  $request = $this->getRequest();
133  $session = $request->getSession();
134  $out = $this->getOutput();
135 
136  $out->addModules( [ 'mediawiki.special.userrights' ] );
137 
138  $this->mTarget = $par ?? $request->getVal( 'user' );
139 
140  if ( is_string( $this->mTarget ) ) {
141  $this->mTarget = trim( $this->mTarget );
142  }
143 
144  if ( $this->mTarget !== null && $this->userNameUtils->getCanonical( $this->mTarget ) === $user->getName() ) {
145  $this->isself = true;
146  }
147 
148  $fetchedStatus = $this->mTarget === null ? Status::newFatal( 'nouserspecified' ) :
149  $this->fetchUser( $this->mTarget, true );
150  if ( $fetchedStatus->isOK() ) {
151  $this->mFetchedUser = $fetchedStatus->value;
152  if ( $this->mFetchedUser instanceof User ) {
153  // Set the 'relevant user' in the skin, so it displays links like Contributions,
154  // User logs, UserRights, etc.
155  $this->getSkin()->setRelevantUser( $this->mFetchedUser );
156  }
157  }
158 
159  // show a successbox, if the user rights was saved successfully
160  if (
161  $session->get( 'specialUserrightsSaveSuccess' ) &&
162  $this->mFetchedUser !== null
163  ) {
164  // Remove session data for the success message
165  $session->remove( 'specialUserrightsSaveSuccess' );
166 
167  $out->addModuleStyles( 'mediawiki.notification.convertmessagebox.styles' );
168  $out->addHTML(
170  'div',
171  [
172  'class' => 'mw-notify-success successbox',
173  'id' => 'mw-preferences-success',
174  'data-mw-autohide' => 'false',
175  ],
177  'p',
178  [],
179  $this->msg( 'savedrights', $this->mFetchedUser->getName() )->text()
180  )
181  )
182  );
183  }
184 
185  $this->setHeaders();
186  $this->outputHeader();
187 
188  $out->addModuleStyles( 'mediawiki.special' );
189  $this->addHelpLink( 'Help:Assigning permissions' );
190 
191  $this->switchForm();
192 
193  if (
194  $request->wasPosted() &&
195  $request->getCheck( 'saveusergroups' ) &&
196  $this->mTarget !== null &&
197  $user->matchEditToken( $request->getVal( 'wpEditToken' ), $this->mTarget )
198  ) {
199  /*
200  * If the user is blocked and they only have "partial" access
201  * (e.g. they don't have the userrights permission), then don't
202  * allow them to change any user rights.
203  */
204  if ( !$this->getAuthority()->isAllowed( 'userrights' ) ) {
205  $block = $user->getBlock();
206  if ( $block && $block->isSitewide() ) {
207  throw new UserBlockedError(
208  $block,
209  $user,
210  $this->getLanguage(),
211  $request->getIP()
212  );
213  }
214  }
215 
216  $this->checkReadOnly();
217 
218  // save settings
219  if ( !$fetchedStatus->isOK() ) {
220  $this->getOutput()->addWikiTextAsInterface(
221  $fetchedStatus->getWikiText( false, false, $this->getLanguage() )
222  );
223 
224  return;
225  }
226 
227  $targetUser = $this->mFetchedUser;
228  if ( $targetUser instanceof User ) { // UserRightsProxy doesn't have this method (T63252)
229  $targetUser->clearInstanceCache(); // T40989
230  }
231 
232  $conflictCheck = $request->getVal( 'conflictcheck-originalgroups' );
233  $conflictCheck = ( $conflictCheck === '' ) ? [] : explode( ',', $conflictCheck );
234  $userGroups = $targetUser->getGroups();
235 
236  if ( $userGroups !== $conflictCheck ) {
237  $out->addHTML( Html::errorBox(
238  $this->msg( 'userrights-conflict' )->parse()
239  ) );
240  } else {
241  $status = $this->saveUserGroups(
242  $this->mTarget,
243  $request->getVal( 'user-reason' ),
244  $targetUser
245  );
246 
247  if ( $status->isOK() ) {
248  // Set session data for the success message
249  $session->set( 'specialUserrightsSaveSuccess', 1 );
250 
251  $out->redirect( $this->getSuccessURL() );
252  return;
253  } else {
254  // Print an error message and redisplay the form
255  $out->wrapWikiTextAsInterface(
256  'error', $status->getWikiText( false, false, $this->getLanguage() )
257  );
258  }
259  }
260  }
261 
262  // show some more forms
263  if ( $this->mTarget !== null ) {
264  $this->editUserGroupsForm( $this->mTarget );
265  }
266  }
267 
268  private function getSuccessURL() {
269  return $this->getPageTitle( $this->mTarget )->getFullURL();
270  }
271 
278  public function canProcessExpiries() {
279  return true;
280  }
281 
291  public static function expiryToTimestamp( $expiry ) {
292  if ( wfIsInfinity( $expiry ) ) {
293  return null;
294  }
295 
296  $unix = strtotime( $expiry );
297 
298  if ( !$unix || $unix === -1 ) {
299  return false;
300  }
301 
302  // @todo FIXME: Non-qualified absolute times are not in users specified timezone
303  // and there isn't notice about it in the ui (see ProtectionForm::getExpiry)
304  return wfTimestamp( TS_MW, $unix );
305  }
306 
316  protected function saveUserGroups( $username, $reason, $user ) {
317  $allgroups = $this->getAllGroups();
318  $addgroup = [];
319  $groupExpiries = []; // associative array of (group name => expiry)
320  $removegroup = [];
321  $existingUGMs = $user->getGroupMemberships();
322 
323  // This could possibly create a highly unlikely race condition if permissions are changed between
324  // when the form is loaded and when the form is saved. Ignoring it for the moment.
325  foreach ( $allgroups as $group ) {
326  // We'll tell it to remove all unchecked groups, and add all checked groups.
327  // Later on, this gets filtered for what can actually be removed
328  if ( $this->getRequest()->getCheck( "wpGroup-$group" ) ) {
329  $addgroup[] = $group;
330 
331  if ( $this->canProcessExpiries() ) {
332  // read the expiry information from the request
333  $expiryDropdown = $this->getRequest()->getVal( "wpExpiry-$group" );
334  if ( $expiryDropdown === 'existing' ) {
335  continue;
336  }
337 
338  if ( $expiryDropdown === 'other' ) {
339  $expiryValue = $this->getRequest()->getVal( "wpExpiry-$group-other" );
340  } else {
341  $expiryValue = $expiryDropdown;
342  }
343 
344  // validate the expiry
345  $groupExpiries[$group] = self::expiryToTimestamp( $expiryValue );
346 
347  if ( $groupExpiries[$group] === false ) {
348  return Status::newFatal( 'userrights-invalid-expiry', $group );
349  }
350 
351  // not allowed to have things expiring in the past
352  if ( $groupExpiries[$group] && $groupExpiries[$group] < wfTimestampNow() ) {
353  return Status::newFatal( 'userrights-expiry-in-past', $group );
354  }
355 
356  // if the user can only add this group (not remove it), the expiry time
357  // cannot be brought forward (T156784)
358  if ( !$this->canRemove( $group ) &&
359  isset( $existingUGMs[$group] ) &&
360  ( $existingUGMs[$group]->getExpiry() ?: 'infinity' ) >
361  ( $groupExpiries[$group] ?: 'infinity' )
362  ) {
363  return Status::newFatal( 'userrights-cannot-shorten-expiry', $group );
364  }
365  }
366  } else {
367  $removegroup[] = $group;
368  }
369  }
370 
371  $this->doSaveUserGroups( $user, $addgroup, $removegroup, $reason, [], $groupExpiries );
372 
373  return Status::newGood();
374  }
375 
389  public function doSaveUserGroups( $user, array $add, array $remove, $reason = '',
390  array $tags = [], array $groupExpiries = []
391  ) {
392  // Validate input set...
393  $isself = $user->getName() == $this->getUser()->getName();
394  $groups = $user->getGroups();
395  $ugms = $user->getGroupMemberships();
396  $changeable = $this->changeableGroups();
397  $addable = array_merge( $changeable['add'], $isself ? $changeable['add-self'] : [] );
398  $removable = array_merge( $changeable['remove'], $isself ? $changeable['remove-self'] : [] );
399 
400  $remove = array_unique( array_intersect( $remove, $removable, $groups ) );
401  $add = array_intersect( $add, $addable );
402 
403  // add only groups that are not already present or that need their expiry updated,
404  // UNLESS the user can only add this group (not remove it) and the expiry time
405  // is being brought forward (T156784)
406  $add = array_filter( $add,
407  static function ( $group ) use ( $groups, $groupExpiries, $removable, $ugms ) {
408  if ( isset( $groupExpiries[$group] ) &&
409  !in_array( $group, $removable ) &&
410  isset( $ugms[$group] ) &&
411  ( $ugms[$group]->getExpiry() ?: 'infinity' ) >
412  ( $groupExpiries[$group] ?: 'infinity' )
413  ) {
414  return false;
415  }
416  return !in_array( $group, $groups ) || array_key_exists( $group, $groupExpiries );
417  } );
418 
419  $this->getHookRunner()->onChangeUserGroups( $this->getUser(), $user, $add, $remove );
420 
421  $oldGroups = $groups;
422  $oldUGMs = $user->getGroupMemberships();
423  $newGroups = $oldGroups;
424 
425  // Remove groups, then add new ones/update expiries of existing ones
426  if ( $remove ) {
427  foreach ( $remove as $index => $group ) {
428  if ( !$user->removeGroup( $group ) ) {
429  unset( $remove[$index] );
430  }
431  }
432  $newGroups = array_diff( $newGroups, $remove );
433  }
434  if ( $add ) {
435  foreach ( $add as $index => $group ) {
436  $expiry = $groupExpiries[$group] ?? null;
437  if ( !$user->addGroup( $group, $expiry ) ) {
438  unset( $add[$index] );
439  }
440  }
441  $newGroups = array_merge( $newGroups, $add );
442  }
443  $newGroups = array_unique( $newGroups );
444  $newUGMs = $user->getGroupMemberships();
445 
446  // Ensure that caches are cleared
447  $user->invalidateCache();
448 
449  // update groups in external authentication database
450  $this->getHookRunner()->onUserGroupsChanged( $user, $add, $remove,
451  $this->getUser(), $reason, $oldUGMs, $newUGMs );
452 
453  wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) );
454  wfDebug( 'newGroups: ' . print_r( $newGroups, true ) );
455  wfDebug( 'oldUGMs: ' . print_r( $oldUGMs, true ) );
456  wfDebug( 'newUGMs: ' . print_r( $newUGMs, true ) );
457 
458  // Only add a log entry if something actually changed
459  if ( $newGroups != $oldGroups || $newUGMs != $oldUGMs ) {
460  $this->addLogEntry( $user, $oldGroups, $newGroups, $reason, $tags, $oldUGMs, $newUGMs );
461  }
462 
463  return [ $add, $remove ];
464  }
465 
473  protected static function serialiseUgmForLog( $ugm ) {
474  if ( !$ugm instanceof UserGroupMembership ) {
475  return null;
476  }
477  return [ 'expiry' => $ugm->getExpiry() ];
478  }
479 
490  protected function addLogEntry( $user, array $oldGroups, array $newGroups, $reason,
491  array $tags, array $oldUGMs, array $newUGMs
492  ) {
493  // make sure $oldUGMs and $newUGMs are in the same order, and serialise
494  // each UGM object to a simplified array
495  $oldUGMs = array_map( function ( $group ) use ( $oldUGMs ) {
496  return isset( $oldUGMs[$group] ) ?
497  self::serialiseUgmForLog( $oldUGMs[$group] ) :
498  null;
499  }, $oldGroups );
500  $newUGMs = array_map( function ( $group ) use ( $newUGMs ) {
501  return isset( $newUGMs[$group] ) ?
502  self::serialiseUgmForLog( $newUGMs[$group] ) :
503  null;
504  }, $newGroups );
505 
506  $logEntry = new ManualLogEntry( 'rights', 'rights' );
507  $logEntry->setPerformer( $this->getUser() );
508  $logEntry->setTarget( $user->getUserPage() );
509  $logEntry->setComment( $reason );
510  $logEntry->setParameters( [
511  '4::oldgroups' => $oldGroups,
512  '5::newgroups' => $newGroups,
513  'oldmetadata' => $oldUGMs,
514  'newmetadata' => $newUGMs,
515  ] );
516  $logid = $logEntry->insert();
517  if ( count( $tags ) ) {
518  $logEntry->addTags( $tags );
519  }
520  $logEntry->publish( $logid );
521  }
522 
527  private function editUserGroupsForm( $username ) {
528  $status = $this->fetchUser( $username, true );
529  if ( !$status->isOK() ) {
530  $this->getOutput()->addWikiTextAsInterface(
531  $status->getWikiText( false, false, $this->getLanguage() )
532  );
533 
534  return;
535  }
536 
538  $user = $status->value;
539  '@phan-var User $user';
540 
541  $groups = $user->getGroups();
542  $groupMemberships = $user->getGroupMemberships();
543  $this->showEditUserGroupsForm( $user, $groups, $groupMemberships );
544 
545  // This isn't really ideal logging behavior, but let's not hide the
546  // interwiki logs if we're using them as is.
547  $this->showLogFragment( $user, $this->getOutput() );
548  }
549 
559  public function fetchUser( $username, $writing = true ) {
560  $parts = explode( $this->getConfig()->get( 'UserrightsInterwikiDelimiter' ), $username );
561  if ( count( $parts ) < 2 ) {
562  $name = trim( $username );
563  $dbDomain = '';
564  } else {
565  list( $name, $dbDomain ) = array_map( 'trim', $parts );
566 
567  if ( WikiMap::isCurrentWikiId( $dbDomain ) ) {
568  $dbDomain = '';
569  } else {
570  if ( $writing &&
571  !$this->getAuthority()->isAllowed( 'userrights-interwiki' )
572  ) {
573  return Status::newFatal( 'userrights-no-interwiki' );
574  }
575  if ( !UserRightsProxy::validDatabase( $dbDomain ) ) {
576  return Status::newFatal( 'userrights-nodatabase', $dbDomain );
577  }
578  }
579  }
580 
581  if ( $name === '' ) {
582  return Status::newFatal( 'nouserspecified' );
583  }
584 
585  if ( $name[0] == '#' ) {
586  // Numeric ID can be specified...
587  // We'll do a lookup for the name internally.
588  $id = intval( substr( $name, 1 ) );
589 
590  if ( $dbDomain == '' ) {
591  $name = User::whoIs( $id );
592  } else {
593  $name = UserRightsProxy::whoIs( $dbDomain, $id );
594  }
595 
596  if ( !$name ) {
597  return Status::newFatal( 'noname' );
598  }
599  } else {
600  $name = $this->userNameUtils->getCanonical( $name );
601  if ( $name === false ) {
602  // invalid name
603  return Status::newFatal( 'nosuchusershort', $username );
604  }
605  }
606 
607  if ( $dbDomain == '' ) {
608  $user = $this->userFactory->newFromName( $name );
609  } else {
610  $user = UserRightsProxy::newFromName( $dbDomain, $name );
611  }
612 
613  if ( !$user || $user->isAnon() ) {
614  return Status::newFatal( 'nosuchusershort', $username );
615  }
616 
617  if ( $user instanceof User &&
618  $user->isHidden() &&
619  !$this->getAuthority()->isAllowed( 'hideuser' )
620  ) {
621  // Cannot see hidden users, pretend they don't exist
622  return Status::newFatal( 'nosuchusershort', $username );
623  }
624 
625  return Status::newGood( $user );
626  }
627 
635  public function makeGroupNameList( $ids ) {
636  if ( empty( $ids ) ) {
637  return $this->msg( 'rightsnone' )->inContentLanguage()->text();
638  } else {
639  return implode( ', ', $ids );
640  }
641  }
642 
646  protected function switchForm() {
647  $this->getOutput()->addModules( 'mediawiki.userSuggest' );
648 
649  $this->getOutput()->addHTML(
651  'form',
652  [
653  'method' => 'get',
654  'action' => wfScript(),
655  'name' => 'uluser',
656  'id' => 'mw-userrights-form1'
657  ]
658  ) .
659  Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
660  Xml::fieldset( $this->msg( 'userrights-lookup-user' )->text() ) .
662  $this->msg( 'userrights-user-editname' )->text(),
663  'user',
664  'username',
665  30,
666  str_replace( '_', ' ', $this->mTarget ),
667  [
668  'class' => 'mw-autocomplete-user', // used by mediawiki.userSuggest
669  ] + (
670  // Set autofocus on blank input and error input
671  $this->mFetchedUser === null ? [ 'autofocus' => '' ] : []
672  )
673  ) . ' ' .
675  $this->msg( 'editusergroup' )->text()
676  ) .
677  Html::closeElement( 'fieldset' ) .
678  Html::closeElement( 'form' ) . "\n"
679  );
680  }
681 
691  protected function showEditUserGroupsForm( $user, $groups, $groupMemberships ) {
692  $list = $membersList = $tempList = $tempMembersList = [];
693  foreach ( $groupMemberships as $ugm ) {
694  $linkG = UserGroupMembership::getLink( $ugm, $this->getContext(), 'html' );
695  $linkM = UserGroupMembership::getLink( $ugm, $this->getContext(), 'html',
696  $user->getName() );
697  if ( $ugm->getExpiry() ) {
698  $tempList[] = $linkG;
699  $tempMembersList[] = $linkM;
700  } else {
701  $list[] = $linkG;
702  $membersList[] = $linkM;
703 
704  }
705  }
706 
707  $autoList = [];
708  $autoMembersList = [];
709 
710  $isUserInstance = $user instanceof User;
711 
712  if ( $isUserInstance ) {
713  foreach ( $this->userGroupManager->getUserAutopromoteGroups( $user ) as $group ) {
714  $autoList[] = UserGroupMembership::getLink( $group, $this->getContext(), 'html' );
715  $autoMembersList[] = UserGroupMembership::getLink( $group, $this->getContext(),
716  'html', $user->getName() );
717  }
718  }
719 
720  $language = $this->getLanguage();
721  $displayedList = $this->msg( 'userrights-groupsmember-type' )
722  ->rawParams(
723  $language->commaList( array_merge( $tempList, $list ) ),
724  $language->commaList( array_merge( $tempMembersList, $membersList ) )
725  )->escaped();
726  $displayedAutolist = $this->msg( 'userrights-groupsmember-type' )
727  ->rawParams(
728  $language->commaList( $autoList ),
729  $language->commaList( $autoMembersList )
730  )->escaped();
731 
732  $grouplist = '';
733  $count = count( $list ) + count( $tempList );
734  if ( $count > 0 ) {
735  $grouplist = $this->msg( 'userrights-groupsmember' )
736  ->numParams( $count )
737  ->params( $user->getName() )
738  ->parse();
739  $grouplist = '<p>' . $grouplist . ' ' . $displayedList . "</p>\n";
740  }
741 
742  $count = count( $autoList );
743  if ( $count > 0 ) {
744  $autogrouplistintro = $this->msg( 'userrights-groupsmember-auto' )
745  ->numParams( $count )
746  ->params( $user->getName() )
747  ->parse();
748  $grouplist .= '<p>' . $autogrouplistintro . ' ' . $displayedAutolist . "</p>\n";
749  }
750 
751  $systemUser = $isUserInstance && $user->isSystemUser();
752  if ( $systemUser ) {
753  $systemusernote = $this->msg( 'userrights-systemuser' )
754  ->params( $user->getName() )
755  ->parse();
756  $grouplist .= '<p>' . $systemusernote . "</p>\n";
757  }
758 
759  // Only add an email link if the user is not a system user
760  $flags = $systemUser ? 0 : Linker::TOOL_LINKS_EMAIL;
761  $userToolLinks = Linker::userToolLinks(
762  $user->getId(),
763  $user->getName(),
764  false, /* default for redContribsWhenNoEdits */
765  $flags
766  );
767 
768  list( $groupCheckboxes, $canChangeAny ) =
769  $this->groupCheckboxes( $groupMemberships, $user );
770  $this->getOutput()->addHTML(
772  'form',
773  [
774  'method' => 'post',
775  'action' => $this->getPageTitle()->getLocalURL(),
776  'name' => 'editGroup',
777  'id' => 'mw-userrights-form2'
778  ]
779  ) .
780  Html::hidden( 'user', $this->mTarget ) .
781  Html::hidden( 'wpEditToken', $this->getUser()->getEditToken( $this->mTarget ) ) .
782  Html::hidden(
783  'conflictcheck-originalgroups',
784  implode( ',', $user->getGroups() )
785  ) . // Conflict detection
786  Xml::openElement( 'fieldset' ) .
787  Xml::element(
788  'legend',
789  [],
790  $this->msg(
791  $canChangeAny ? 'userrights-editusergroup' : 'userrights-viewusergroup',
792  $user->getName()
793  )->text()
794  ) .
795  $this->msg(
796  $canChangeAny ? 'editinguser' : 'viewinguserrights'
797  )->params( wfEscapeWikiText( $user->getName() ) )
798  ->rawParams( $userToolLinks )->parse()
799  );
800  if ( $canChangeAny ) {
801  $this->getOutput()->addHTML(
802  $this->msg( 'userrights-groups-help', $user->getName() )->parse() .
803  $grouplist .
804  $groupCheckboxes .
805  Xml::openElement( 'table', [ 'id' => 'mw-userrights-table-outer' ] ) .
806  "<tr>
807  <td class='mw-label'>" .
808  Xml::label( $this->msg( 'userrights-reason' )->text(), 'wpReason' ) .
809  "</td>
810  <td class='mw-input'>" .
811  Xml::input( 'user-reason', 60, $this->getRequest()->getVal( 'user-reason' ) ?? false, [
812  'id' => 'wpReason',
813  // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
814  // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
815  // Unicode codepoints.
817  ] ) .
818  "</td>
819  </tr>
820  <tr>
821  <td></td>
822  <td class='mw-submit'>" .
823  Xml::submitButton( $this->msg( 'saveusergroups', $user->getName() )->text(),
824  [ 'name' => 'saveusergroups' ] +
825  Linker::tooltipAndAccesskeyAttribs( 'userrights-set' )
826  ) .
827  "</td>
828  </tr>" .
829  Xml::closeElement( 'table' ) . "\n"
830  );
831  } else {
832  $this->getOutput()->addHTML( $grouplist );
833  }
834  $this->getOutput()->addHTML(
835  Xml::closeElement( 'fieldset' ) .
836  Xml::closeElement( 'form' ) . "\n"
837  );
838  }
839 
843  protected static function getAllGroups() {
844  // TODO don't hard code false here (refers to local domain). See T14518
845  return MediaWikiServices::getInstance()
846  ->getUserGroupManagerFactory()
847  ->getUserGroupManager( false )
848  ->listAllGroups();
849  }
850 
860  private function groupCheckboxes( $usergroups, $user ) {
861  $allgroups = $this->getAllGroups();
862  $ret = '';
863 
864  // Get the list of preset expiry times from the system message
865  $expiryOptionsMsg = $this->msg( 'userrights-expiry-options' )->inContentLanguage();
866  $expiryOptions = $expiryOptionsMsg->isDisabled()
867  ? []
868  : XmlSelect::parseOptionsMessage( $expiryOptionsMsg->text() );
869 
870  // Put all column info into an associative array so that extensions can
871  // more easily manage it.
872  $columns = [ 'unchangeable' => [], 'changeable' => [] ];
873 
874  foreach ( $allgroups as $group ) {
875  $set = isset( $usergroups[$group] );
876  // Users who can add the group, but not remove it, can only lengthen
877  // expiries, not shorten them. So they should only see the expiry
878  // dropdown if the group currently has a finite expiry
879  $canOnlyLengthenExpiry = ( $set && $this->canAdd( $group ) &&
880  !$this->canRemove( $group ) && $usergroups[$group]->getExpiry() );
881  // Should the checkbox be disabled?
882  $disabledCheckbox = !(
883  ( $set && $this->canRemove( $group ) ) ||
884  ( !$set && $this->canAdd( $group ) ) );
885  // Should the expiry elements be disabled?
886  $disabledExpiry = $disabledCheckbox && !$canOnlyLengthenExpiry;
887  // Do we need to point out that this action is irreversible?
888  $irreversible = !$disabledCheckbox && (
889  ( $set && !$this->canAdd( $group ) ) ||
890  ( !$set && !$this->canRemove( $group ) ) );
891 
892  $checkbox = [
893  'set' => $set,
894  'disabled' => $disabledCheckbox,
895  'disabled-expiry' => $disabledExpiry,
896  'irreversible' => $irreversible
897  ];
898 
899  if ( $disabledCheckbox && $disabledExpiry ) {
900  $columns['unchangeable'][$group] = $checkbox;
901  } else {
902  $columns['changeable'][$group] = $checkbox;
903  }
904  }
905 
906  // Build the HTML table
907  $ret .= Xml::openElement( 'table', [ 'class' => 'mw-userrights-groups' ] ) .
908  "<tr>\n";
909  foreach ( $columns as $name => $column ) {
910  if ( $column === [] ) {
911  continue;
912  }
913  // Messages: userrights-changeable-col, userrights-unchangeable-col
914  $ret .= Xml::element(
915  'th',
916  null,
917  $this->msg( 'userrights-' . $name . '-col', count( $column ) )->text()
918  );
919  }
920 
921  $ret .= "</tr>\n<tr>\n";
922  foreach ( $columns as $column ) {
923  if ( $column === [] ) {
924  continue;
925  }
926  $ret .= "\t<td style='vertical-align:top;'>\n";
927  foreach ( $column as $group => $checkbox ) {
928  $attr = [ 'class' => 'mw-userrights-groupcheckbox' ];
929  if ( $checkbox['disabled'] ) {
930  $attr['disabled'] = 'disabled';
931  }
932 
933  $member = UserGroupMembership::getGroupMemberName( $group, $user->getName() );
934  if ( $checkbox['irreversible'] ) {
935  $text = $this->msg( 'userrights-irreversible-marker', $member )->text();
936  } elseif ( $checkbox['disabled'] && !$checkbox['disabled-expiry'] ) {
937  $text = $this->msg( 'userrights-no-shorten-expiry-marker', $member )->text();
938  } else {
939  $text = $member;
940  }
941  $checkboxHtml = Xml::checkLabel( $text, "wpGroup-" . $group,
942  "wpGroup-" . $group, $checkbox['set'], $attr );
943 
944  if ( $this->canProcessExpiries() ) {
945  $uiUser = $this->getUser();
946  $uiLanguage = $this->getLanguage();
947 
948  $currentExpiry = isset( $usergroups[$group] ) ?
949  $usergroups[$group]->getExpiry() :
950  null;
951 
952  // If the user can't modify the expiry, print the current expiry below
953  // it in plain text. Otherwise provide UI to set/change the expiry
954  if ( $checkbox['set'] &&
955  ( $checkbox['irreversible'] || $checkbox['disabled-expiry'] )
956  ) {
957  if ( $currentExpiry ) {
958  $expiryFormatted = $uiLanguage->userTimeAndDate( $currentExpiry, $uiUser );
959  $expiryFormattedD = $uiLanguage->userDate( $currentExpiry, $uiUser );
960  $expiryFormattedT = $uiLanguage->userTime( $currentExpiry, $uiUser );
961  $expiryHtml = Xml::element( 'span', null,
962  $this->msg( 'userrights-expiry-current' )->params(
963  $expiryFormatted, $expiryFormattedD, $expiryFormattedT )->text() );
964  } else {
965  $expiryHtml = Xml::element( 'span', null,
966  $this->msg( 'userrights-expiry-none' )->text() );
967  }
968  // T171345: Add a hidden form element so that other groups can still be manipulated,
969  // otherwise saving errors out with an invalid expiry time for this group.
970  $expiryHtml .= Html::hidden( "wpExpiry-$group",
971  $currentExpiry ? 'existing' : 'infinite' );
972  $expiryHtml .= "<br />\n";
973  } else {
974  $expiryHtml = Xml::element( 'span', null,
975  $this->msg( 'userrights-expiry' )->text() );
976  $expiryHtml .= Xml::openElement( 'span' );
977 
978  // add a form element to set the expiry date
979  $expiryFormOptions = new XmlSelect(
980  "wpExpiry-$group",
981  "mw-input-wpExpiry-$group", // forward compatibility with HTMLForm
982  $currentExpiry ? 'existing' : 'infinite'
983  );
984  if ( $checkbox['disabled-expiry'] ) {
985  $expiryFormOptions->setAttribute( 'disabled', 'disabled' );
986  }
987 
988  if ( $currentExpiry ) {
989  $timestamp = $uiLanguage->userTimeAndDate( $currentExpiry, $uiUser );
990  $d = $uiLanguage->userDate( $currentExpiry, $uiUser );
991  $t = $uiLanguage->userTime( $currentExpiry, $uiUser );
992  $existingExpiryMessage = $this->msg( 'userrights-expiry-existing',
993  $timestamp, $d, $t );
994  $expiryFormOptions->addOption( $existingExpiryMessage->text(), 'existing' );
995  }
996 
997  $expiryFormOptions->addOption(
998  $this->msg( 'userrights-expiry-none' )->text(),
999  'infinite'
1000  );
1001  $expiryFormOptions->addOption(
1002  $this->msg( 'userrights-expiry-othertime' )->text(),
1003  'other'
1004  );
1005 
1006  $expiryFormOptions->addOptions( $expiryOptions );
1007 
1008  // Add expiry dropdown
1009  $expiryHtml .= $expiryFormOptions->getHTML() . '<br />';
1010 
1011  // Add custom expiry field
1012  $attribs = [
1013  'id' => "mw-input-wpExpiry-$group-other",
1014  'class' => 'mw-userrights-expiryfield',
1015  ];
1016  if ( $checkbox['disabled-expiry'] ) {
1017  $attribs['disabled'] = 'disabled';
1018  }
1019  $expiryHtml .= Xml::input( "wpExpiry-$group-other", 30, '', $attribs );
1020 
1021  // If the user group is set but the checkbox is disabled, mimic a
1022  // checked checkbox in the form submission
1023  if ( $checkbox['set'] && $checkbox['disabled'] ) {
1024  $expiryHtml .= Html::hidden( "wpGroup-$group", 1 );
1025  }
1026 
1027  $expiryHtml .= Xml::closeElement( 'span' );
1028  }
1029 
1030  $divAttribs = [
1031  'id' => "mw-userrights-nested-wpGroup-$group",
1032  'class' => 'mw-userrights-nested',
1033  ];
1034  $checkboxHtml .= "\t\t\t" . Xml::tags( 'div', $divAttribs, $expiryHtml ) . "\n";
1035  }
1036  $ret .= "\t\t" . ( ( $checkbox['disabled'] && $checkbox['disabled-expiry'] )
1037  ? Xml::tags( 'div', [ 'class' => 'mw-userrights-disabled' ], $checkboxHtml )
1038  : Xml::tags( 'div', [], $checkboxHtml )
1039  ) . "\n";
1040  }
1041  $ret .= "\t</td>\n";
1042  }
1043  $ret .= Xml::closeElement( 'tr' ) . Xml::closeElement( 'table' );
1044 
1045  return [ $ret, (bool)$columns['changeable'] ];
1046  }
1047 
1052  private function canRemove( $group ) {
1053  $groups = $this->changeableGroups();
1054 
1055  return in_array(
1056  $group,
1057  $groups['remove'] ) || ( $this->isself && in_array( $group, $groups['remove-self'] )
1058  );
1059  }
1060 
1065  private function canAdd( $group ) {
1066  $groups = $this->changeableGroups();
1067 
1068  return in_array(
1069  $group,
1070  $groups['add'] ) || ( $this->isself && in_array( $group, $groups['add-self'] )
1071  );
1072  }
1073 
1082  protected function changeableGroups() {
1083  return $this->userGroupManager->getGroupsChangeableBy( $this->getContext()->getAuthority() );
1084  }
1085 
1092  protected function showLogFragment( $user, $output ) {
1093  $rightsLogPage = new LogPage( 'rights' );
1094  $output->addHTML( Xml::element( 'h2', null, $rightsLogPage->getName()->text() ) );
1095  LogEventsList::showLogExtract( $output, 'rights', $user->getUserPage() );
1096  }
1097 
1106  public function prefixSearchSubpages( $search, $limit, $offset ) {
1107  $search = $this->userNameUtils->getCanonical( $search );
1108  if ( !$search ) {
1109  // No prefix suggestion for invalid user
1110  return [];
1111  }
1112  // Autocomplete subpage as user list - public to allow caching
1113  return $this->userNamePrefixSearch
1114  ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
1115  }
1116 
1117  protected function getGroupName() {
1118  return 'users';
1119  }
1120 }
SpecialPage\getPageTitle
getPageTitle( $subpage=false)
Get a self-referential title object.
Definition: SpecialPage.php:768
SpecialPage\msg
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
Definition: SpecialPage.php:936
UserrightsPage\canRemove
canRemove( $group)
Definition: SpecialUserrights.php:1052
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
UserBlockedError
Show an error when the user tries to do something whilst blocked.
Definition: UserBlockedError.php:32
SpecialPage\getOutput
getOutput()
Get the OutputPage being used for this instance.
Definition: SpecialPage.php:814
UserrightsPage\editUserGroupsForm
editUserGroupsForm( $username)
Edit user groups membership.
Definition: SpecialUserrights.php:527
WikiMap\isCurrentWikiId
static isCurrentWikiId( $wikiId)
Definition: WikiMap.php:321
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:203
Xml\label
static label( $label, $id, $attribs=[])
Convenience function to build an HTML form label.
Definition: Xml.php:365
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1649
Linker\userToolLinks
static userToolLinks( $userId, $userText, $redContribsWhenNoEdits=false, $flags=0, $edits=null, $useParentheses=true)
Generate standard user tool links (talk, contributions, block link, etc.)
Definition: Linker.php:1125
UserRightsProxy\validDatabase
static validDatabase( $dbDomain)
Confirm the selected database name is a valid local interwiki database name.
Definition: UserRightsProxy.php:74
UserrightsPage\switchForm
switchForm()
Output a form to allow searching for a user.
Definition: SpecialUserrights.php:646
SpecialPage\getSkin
getSkin()
Shortcut to get the skin being used for this instance.
Definition: SpecialPage.php:844
UserrightsPage\execute
execute( $par)
Manage forms to be shown according to posted data.
Definition: SpecialUserrights.php:130
SpecialPage\getAuthority
getAuthority()
Shortcut to get the Authority executing this instance.
Definition: SpecialPage.php:834
MediaWiki\User\UserIdentity\getId
getId( $wikiId=self::LOCAL)
SpecialPage\getLanguage
getLanguage()
Shortcut to get user's language.
Definition: SpecialPage.php:854
UserrightsPage\userCanChangeRights
userCanChangeRights(UserIdentity $targetUser, $checkIfSelf=true)
Check whether the current user (from context) can change the target user's rights.
Definition: SpecialUserrights.php:100
XmlSelect\parseOptionsMessage
static parseOptionsMessage(string $msg)
Parse labels and values out of a comma- and colon-separated list of options, such as is used for expi...
Definition: XmlSelect.php:146
Xml\openElement
static openElement( $element, $attribs=null)
This opens an XML element.
Definition: Xml.php:111
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:39
UserrightsPage\showLogFragment
showLogFragment( $user, $output)
Show a rights log fragment for the specified user.
Definition: SpecialUserrights.php:1092
Linker\tooltipAndAccesskeyAttribs
static tooltipAndAccesskeyAttribs( $name, array $msgParams=[], $options=null)
Returns the attributes for the tooltip and access key.
Definition: Linker.php:2222
XmlSelect
Class for generating HTML <select> or <datalist> elements.
Definition: XmlSelect.php:26
UserrightsPage\getGroupName
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
Definition: SpecialUserrights.php:1117
Xml\fieldset
static fieldset( $legend=false, $content=false, $attribs=[])
Shortcut for creating fieldsets.
Definition: Xml.php:624
Html\closeElement
static closeElement( $element)
Returns "</$element>".
Definition: Html.php:319
UserrightsPage\expiryToTimestamp
static expiryToTimestamp( $expiry)
Converts a user group membership expiry string into a timestamp.
Definition: SpecialUserrights.php:291
UserrightsPage\prefixSearchSubpages
prefixSearchSubpages( $search, $limit, $offset)
Return an array of subpages beginning with $search that this special page will accept.
Definition: SpecialUserrights.php:1106
MediaWiki\User\UserGroupManager
Managers user groups.
Definition: UserGroupManager.php:52
UserrightsPage\getAllGroups
static getAllGroups()
Definition: SpecialUserrights.php:843
UserrightsPage
Special page to allow managing user group membership.
Definition: SpecialUserrights.php:37
SpecialPage\addHelpLink
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Definition: SpecialPage.php:972
SpecialPage\getHookRunner
getHookRunner()
Definition: SpecialPage.php:1119
UserrightsPage\showEditUserGroupsForm
showEditUserGroupsForm( $user, $groups, $groupMemberships)
Show the form to edit group memberships.
Definition: SpecialUserrights.php:691
SpecialPage\getConfig
getConfig()
Shortcut to get main config object.
Definition: SpecialPage.php:902
UserrightsPage\serialiseUgmForLog
static serialiseUgmForLog( $ugm)
Serialise a UserGroupMembership object for storage in the log_params section of the logging table.
Definition: SpecialUserrights.php:473
wfScript
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
Definition: GlobalFunctions.php:2268
UserGroupMembership\getLink
static getLink( $ugm, IContextSource $context, $format, $userName=null)
Gets a link for a user group, possibly including the expiry date if relevant.
Definition: UserGroupMembership.php:104
UserrightsPage\fetchUser
fetchUser( $username, $writing=true)
Normalize the input username, which may be local or remote, and return a user (or proxy) object for m...
Definition: SpecialUserrights.php:559
MediaWiki\User\UserNamePrefixSearch
Handles searching prefixes of user names.
Definition: UserNamePrefixSearch.php:39
User\isHidden
isHidden()
Check if user account is hidden.
Definition: User.php:1866
LogPage
Class to simplify the use of log pages.
Definition: LogPage.php:38
Xml\element
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:42
UserrightsPage\addLogEntry
addLogEntry( $user, array $oldGroups, array $newGroups, $reason, array $tags, array $oldUGMs, array $newUGMs)
Add a rights log entry for an action.
Definition: SpecialUserrights.php:490
UserrightsPage\canProcessExpiries
canProcessExpiries()
Returns true if this user rights form can set and change user group expiries.
Definition: SpecialUserrights.php:278
SpecialPage\setHeaders
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
Definition: SpecialPage.php:642
UserrightsPage\$userFactory
UserFactory $userFactory
Definition: SpecialUserrights.php:61
SpecialPage\getUser
getUser()
Shortcut to get the User executing this instance.
Definition: SpecialPage.php:824
User\clearInstanceCache
clearInstanceCache( $reloadFrom=false)
Clear various cached data stored in this object.
Definition: User.php:1366
LogEventsList\showLogExtract
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
Definition: LogEventsList.php:597
wfTimestampNow
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
Definition: GlobalFunctions.php:1678
UserrightsPage\getSuccessURL
getSuccessURL()
Definition: SpecialUserrights.php:268
UserrightsPage\canAdd
canAdd( $group)
Definition: SpecialUserrights.php:1065
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:894
Html\errorBox
static errorBox( $html, $heading='', $className='')
Return an error box.
Definition: Html.php:771
SpecialPage\getContext
getContext()
Gets the context this SpecialPage is executed in.
Definition: SpecialPage.php:788
Html\hidden
static hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:834
MediaWiki\User\UserGroupManagerFactory
Factory service for UserGroupManager instances.
Definition: UserGroupManagerFactory.php:37
User\whoIs
static whoIs( $id)
Get the username corresponding to a given user ID.
Definition: User.php:910
Linker\TOOL_LINKS_EMAIL
const TOOL_LINKS_EMAIL
Definition: Linker.php:44
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
SpecialPage
Parent class for all special pages.
Definition: SpecialPage.php:43
Xml\tags
static tags( $element, $attribs, $contents)
Same as Xml::element(), but does not escape contents.
Definition: Xml.php:133
UserrightsPage\groupCheckboxes
groupCheckboxes( $usergroups, $user)
Adds a table with checkboxes where you can select what groups to add/remove.
Definition: SpecialUserrights.php:860
wfIsInfinity
wfIsInfinity( $str)
Determine input string is represents as infinity.
Definition: GlobalFunctions.php:2514
SpecialPage\getRequest
getRequest()
Get the WebRequest being used for this instance.
Definition: SpecialPage.php:804
UserRightsProxy\newFromName
static newFromName( $dbDomain, $name, $ignoreInvalidDB=false)
Factory function; get a remote user entry by name.
Definition: UserRightsProxy.php:116
wfEscapeWikiText
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Definition: GlobalFunctions.php:1440
UserrightsPage\doesWrites
doesWrites()
Indicates whether this special page may perform database writes.
Definition: SpecialUserrights.php:87
UserrightsPage\__construct
__construct(UserGroupManagerFactory $userGroupManagerFactory=null, UserNameUtils $userNameUtils=null, UserNamePrefixSearch $userNamePrefixSearch=null, UserFactory $userFactory=null)
Definition: SpecialUserrights.php:69
UserRightsProxy\whoIs
static whoIs( $dbDomain, $id, $ignoreInvalidDB=false)
Same as User::whoIs()
Definition: UserRightsProxy.php:87
UserrightsPage\saveUserGroups
saveUserGroups( $username, $reason, $user)
Save user groups changes in the database.
Definition: SpecialUserrights.php:316
CommentStore\COMMENT_CHARACTER_LIMIT
const COMMENT_CHARACTER_LIMIT
Maximum length of a comment in UTF-8 characters.
Definition: CommentStore.php:48
UserrightsPage\$mFetchedUser
null User $mFetchedUser
The user object of the target username or null.
Definition: SpecialUserrights.php:48
UserrightsPage\$userGroupManager
UserGroupManager $userGroupManager
Definition: SpecialUserrights.php:52
Xml\closeElement
static closeElement( $element)
Shortcut to close an XML element.
Definition: Xml.php:120
MediaWiki\User\UserNameUtils
UserNameUtils service.
Definition: UserNameUtils.php:42
UserrightsPage\makeGroupNameList
makeGroupNameList( $ids)
Definition: SpecialUserrights.php:635
Html\openElement
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
Definition: Html.php:255
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:213
UserrightsPage\doSaveUserGroups
doSaveUserGroups( $user, array $add, array $remove, $reason='', array $tags=[], array $groupExpiries=[])
Save user groups changes in the database.
Definition: SpecialUserrights.php:389
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:45
UserrightsPage\$userNamePrefixSearch
UserNamePrefixSearch $userNamePrefixSearch
Definition: SpecialUserrights.php:58
UserrightsPage\$mTarget
null string $mTarget
The target of the local right-adjuster's interest.
Definition: SpecialUserrights.php:44
UserGroupMembership\getGroupMemberName
static getGroupMemberName( $group, $member)
Gets the localized name for a member of a group, if it exists.
Definition: UserGroupMembership.php:182
Xml\input
static input( $name, $size=false, $value=false, $attribs=[])
Convenience function to build an HTML text input field.
Definition: Xml.php:281
UserrightsPage\$userNameUtils
UserNameUtils $userNameUtils
Definition: SpecialUserrights.php:55
SpecialPage\checkReadOnly
checkReadOnly()
If the wiki is currently in readonly mode, throws a ReadOnlyError.
Definition: SpecialPage.php:395
$t
$t
Definition: testCompression.php:74
UserrightsPage\changeableGroups
changeableGroups()
Definition: SpecialUserrights.php:1082
Html\element
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:235
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:67
Xml\inputLabel
static inputLabel( $label, $name, $id, $size=false, $value=false, $attribs=[])
Convenience function to build an HTML text input field with a label.
Definition: Xml.php:387
MediaWiki\User\UserFactory
Creates User objects.
Definition: UserFactory.php:41
SpecialPage\outputHeader
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
Definition: SpecialPage.php:733
UserGroupMembership
Represents a "user group membership" – a specific instance of a user belonging to a group.
Definition: UserGroupMembership.php:35
UserrightsPage\$isself
$isself
Definition: SpecialUserrights.php:49
Xml\submitButton
static submitButton( $value, $attribs=[])
Convenience function to build an HTML submit button When $wgUseMediaWikiUIEverywhere is true it will ...
Definition: Xml.php:466
Xml\checkLabel
static checkLabel( $label, $name, $id, $checked=false, $attribs=[])
Convenience function to build an HTML checkbox with a label.
Definition: Xml.php:426