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