MediaWiki REL1_34
SpecialUserrights.php
Go to the documentation of this file.
1<?php
25
38 protected $mTarget;
39 /*
40 * @var null|User $mFetchedUser The user object of the target username or null.
41 */
42 protected $mFetchedUser = null;
43 protected $isself = false;
44
45 public function __construct() {
46 parent::__construct( 'Userrights' );
47 }
48
49 public function doesWrites() {
50 return true;
51 }
52
62 public function userCanChangeRights( $targetUser, $checkIfSelf = true ) {
63 $isself = $this->getUser()->equals( $targetUser );
64
65 $available = $this->changeableGroups();
66 if ( $targetUser->getId() === 0 ) {
67 return false;
68 }
69
70 if ( $available['add'] || $available['remove'] ) {
71 // can change some rights for any user
72 return true;
73 }
74
75 if ( ( $available['add-self'] || $available['remove-self'] )
76 && ( $isself || !$checkIfSelf )
77 ) {
78 // can change some rights for self
79 return true;
80 }
81
82 return false;
83 }
84
93 public function execute( $par ) {
94 $user = $this->getUser();
95 $request = $this->getRequest();
96 $session = $request->getSession();
97 $out = $this->getOutput();
98
99 $out->addModules( [ 'mediawiki.special.userrights' ] );
100
101 $this->mTarget = $par ?? $request->getVal( 'user' );
102
103 if ( is_string( $this->mTarget ) ) {
104 $this->mTarget = trim( $this->mTarget );
105 }
106
107 if ( $this->mTarget !== null && User::getCanonicalName( $this->mTarget ) === $user->getName() ) {
108 $this->isself = true;
109 }
110
111 $fetchedStatus = $this->fetchUser( $this->mTarget, true );
112 if ( $fetchedStatus->isOK() ) {
113 $this->mFetchedUser = $fetchedStatus->value;
114 if ( $this->mFetchedUser instanceof User ) {
115 // Set the 'relevant user' in the skin, so it displays links like Contributions,
116 // User logs, UserRights, etc.
117 $this->getSkin()->setRelevantUser( $this->mFetchedUser );
118 }
119 }
120
121 // show a successbox, if the user rights was saved successfully
122 if (
123 $session->get( 'specialUserrightsSaveSuccess' ) &&
124 $this->mFetchedUser !== null
125 ) {
126 // Remove session data for the success message
127 $session->remove( 'specialUserrightsSaveSuccess' );
128
129 $out->addModuleStyles( 'mediawiki.notification.convertmessagebox.styles' );
130 $out->addHTML(
131 Html::rawElement(
132 'div',
133 [
134 'class' => 'mw-notify-success successbox',
135 'id' => 'mw-preferences-success',
136 'data-mw-autohide' => 'false',
137 ],
138 Html::element(
139 'p',
140 [],
141 $this->msg( 'savedrights', $this->mFetchedUser->getName() )->text()
142 )
143 )
144 );
145 }
146
147 $this->setHeaders();
148 $this->outputHeader();
149
150 $out->addModuleStyles( 'mediawiki.special' );
151 $this->addHelpLink( 'Help:Assigning permissions' );
152
153 $this->switchForm();
154
155 if (
156 $request->wasPosted() &&
157 $request->getCheck( 'saveusergroups' ) &&
158 $this->mTarget !== null &&
159 $user->matchEditToken( $request->getVal( 'wpEditToken' ), $this->mTarget )
160 ) {
161 /*
162 * If the user is blocked and they only have "partial" access
163 * (e.g. they don't have the userrights permission), then don't
164 * allow them to change any user rights.
165 */
166 if ( !MediaWikiServices::getInstance()
168 ->userHasRight( $user, 'userrights' )
169 ) {
170 $block = $user->getBlock();
171 if ( $block && $block->isSitewide() ) {
172 throw new UserBlockedError( $block );
173 }
174 }
175
176 $this->checkReadOnly();
177
178 // save settings
179 if ( !$fetchedStatus->isOK() ) {
180 $this->getOutput()->addWikiTextAsInterface(
181 $fetchedStatus->getWikiText( false, false, $this->getLanguage() )
182 );
183
184 return;
185 }
186
187 $targetUser = $this->mFetchedUser;
188 if ( $targetUser instanceof User ) { // UserRightsProxy doesn't have this method (T63252)
189 $targetUser->clearInstanceCache(); // T40989
190 }
191
192 $conflictCheck = $request->getVal( 'conflictcheck-originalgroups' );
193 $conflictCheck = ( $conflictCheck === '' ) ? [] : explode( ',', $conflictCheck );
194 $userGroups = $targetUser->getGroups();
195
196 if ( $userGroups !== $conflictCheck ) {
197 $out->wrapWikiMsg( '<span class="error">$1</span>', 'userrights-conflict' );
198 } else {
199 $status = $this->saveUserGroups(
200 $this->mTarget,
201 $request->getVal( 'user-reason' ),
202 $targetUser
203 );
204
205 if ( $status->isOK() ) {
206 // Set session data for the success message
207 $session->set( 'specialUserrightsSaveSuccess', 1 );
208
209 $out->redirect( $this->getSuccessURL() );
210 return;
211 } else {
212 // Print an error message and redisplay the form
213 $out->wrapWikiTextAsInterface(
214 'error', $status->getWikiText( false, false, $this->getLanguage() )
215 );
216 }
217 }
218 }
219
220 // show some more forms
221 if ( $this->mTarget !== null ) {
222 $this->editUserGroupsForm( $this->mTarget );
223 }
224 }
225
226 function getSuccessURL() {
227 return $this->getPageTitle( $this->mTarget )->getFullURL();
228 }
229
236 public function canProcessExpiries() {
237 return true;
238 }
239
249 public static function expiryToTimestamp( $expiry ) {
250 if ( wfIsInfinity( $expiry ) ) {
251 return null;
252 }
253
254 $unix = strtotime( $expiry );
255
256 if ( !$unix || $unix === -1 ) {
257 return false;
258 }
259
260 // @todo FIXME: Non-qualified absolute times are not in users specified timezone
261 // and there isn't notice about it in the ui (see ProtectionForm::getExpiry)
262 return wfTimestamp( TS_MW, $unix );
263 }
264
274 protected function saveUserGroups( $username, $reason, $user ) {
275 $allgroups = $this->getAllGroups();
276 $addgroup = [];
277 $groupExpiries = []; // associative array of (group name => expiry)
278 $removegroup = [];
279 $existingUGMs = $user->getGroupMemberships();
280
281 // This could possibly create a highly unlikely race condition if permissions are changed between
282 // when the form is loaded and when the form is saved. Ignoring it for the moment.
283 foreach ( $allgroups as $group ) {
284 // We'll tell it to remove all unchecked groups, and add all checked groups.
285 // Later on, this gets filtered for what can actually be removed
286 if ( $this->getRequest()->getCheck( "wpGroup-$group" ) ) {
287 $addgroup[] = $group;
288
289 if ( $this->canProcessExpiries() ) {
290 // read the expiry information from the request
291 $expiryDropdown = $this->getRequest()->getVal( "wpExpiry-$group" );
292 if ( $expiryDropdown === 'existing' ) {
293 continue;
294 }
295
296 if ( $expiryDropdown === 'other' ) {
297 $expiryValue = $this->getRequest()->getVal( "wpExpiry-$group-other" );
298 } else {
299 $expiryValue = $expiryDropdown;
300 }
301
302 // validate the expiry
303 $groupExpiries[$group] = self::expiryToTimestamp( $expiryValue );
304
305 if ( $groupExpiries[$group] === false ) {
306 return Status::newFatal( 'userrights-invalid-expiry', $group );
307 }
308
309 // not allowed to have things expiring in the past
310 if ( $groupExpiries[$group] && $groupExpiries[$group] < wfTimestampNow() ) {
311 return Status::newFatal( 'userrights-expiry-in-past', $group );
312 }
313
314 // if the user can only add this group (not remove it), the expiry time
315 // cannot be brought forward (T156784)
316 if ( !$this->canRemove( $group ) &&
317 isset( $existingUGMs[$group] ) &&
318 ( $existingUGMs[$group]->getExpiry() ?: 'infinity' ) >
319 ( $groupExpiries[$group] ?: 'infinity' )
320 ) {
321 return Status::newFatal( 'userrights-cannot-shorten-expiry', $group );
322 }
323 }
324 } else {
325 $removegroup[] = $group;
326 }
327 }
328
329 $this->doSaveUserGroups( $user, $addgroup, $removegroup, $reason, [], $groupExpiries );
330
331 return Status::newGood();
332 }
333
347 function doSaveUserGroups( $user, array $add, array $remove, $reason = '',
348 array $tags = [], array $groupExpiries = []
349 ) {
350 // Validate input set...
351 $isself = $user->getName() == $this->getUser()->getName();
352 $groups = $user->getGroups();
353 $ugms = $user->getGroupMemberships();
354 $changeable = $this->changeableGroups();
355 $addable = array_merge( $changeable['add'], $isself ? $changeable['add-self'] : [] );
356 $removable = array_merge( $changeable['remove'], $isself ? $changeable['remove-self'] : [] );
357
358 $remove = array_unique(
359 array_intersect( (array)$remove, $removable, $groups ) );
360 $add = array_intersect( (array)$add, $addable );
361
362 // add only groups that are not already present or that need their expiry updated,
363 // UNLESS the user can only add this group (not remove it) and the expiry time
364 // is being brought forward (T156784)
365 $add = array_filter( $add,
366 function ( $group ) use ( $groups, $groupExpiries, $removable, $ugms ) {
367 if ( isset( $groupExpiries[$group] ) &&
368 !in_array( $group, $removable ) &&
369 isset( $ugms[$group] ) &&
370 ( $ugms[$group]->getExpiry() ?: 'infinity' ) >
371 ( $groupExpiries[$group] ?: 'infinity' )
372 ) {
373 return false;
374 }
375 return !in_array( $group, $groups ) || array_key_exists( $group, $groupExpiries );
376 } );
377
378 Hooks::run( 'ChangeUserGroups', [ $this->getUser(), $user, &$add, &$remove ] );
379
380 $oldGroups = $groups;
381 $oldUGMs = $user->getGroupMemberships();
382 $newGroups = $oldGroups;
383
384 // Remove groups, then add new ones/update expiries of existing ones
385 if ( $remove ) {
386 foreach ( $remove as $index => $group ) {
387 if ( !$user->removeGroup( $group ) ) {
388 unset( $remove[$index] );
389 }
390 }
391 $newGroups = array_diff( $newGroups, $remove );
392 }
393 if ( $add ) {
394 foreach ( $add as $index => $group ) {
395 $expiry = $groupExpiries[$group] ?? null;
396 if ( !$user->addGroup( $group, $expiry ) ) {
397 unset( $add[$index] );
398 }
399 }
400 $newGroups = array_merge( $newGroups, $add );
401 }
402 $newGroups = array_unique( $newGroups );
403 $newUGMs = $user->getGroupMemberships();
404
405 // Ensure that caches are cleared
406 $user->invalidateCache();
407
408 // update groups in external authentication database
409 Hooks::run( 'UserGroupsChanged', [ $user, $add, $remove, $this->getUser(),
410 $reason, $oldUGMs, $newUGMs ] );
411
412 wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) . "\n" );
413 wfDebug( 'newGroups: ' . print_r( $newGroups, true ) . "\n" );
414 wfDebug( 'oldUGMs: ' . print_r( $oldUGMs, true ) . "\n" );
415 wfDebug( 'newUGMs: ' . print_r( $newUGMs, true ) . "\n" );
416
417 // Only add a log entry if something actually changed
418 if ( $newGroups != $oldGroups || $newUGMs != $oldUGMs ) {
419 $this->addLogEntry( $user, $oldGroups, $newGroups, $reason, $tags, $oldUGMs, $newUGMs );
420 }
421
422 return [ $add, $remove ];
423 }
424
432 protected static function serialiseUgmForLog( $ugm ) {
433 if ( !$ugm instanceof UserGroupMembership ) {
434 return null;
435 }
436 return [ 'expiry' => $ugm->getExpiry() ];
437 }
438
449 protected function addLogEntry( $user, array $oldGroups, array $newGroups, $reason,
450 array $tags, array $oldUGMs, array $newUGMs
451 ) {
452 // make sure $oldUGMs and $newUGMs are in the same order, and serialise
453 // each UGM object to a simplified array
454 $oldUGMs = array_map( function ( $group ) use ( $oldUGMs ) {
455 return isset( $oldUGMs[$group] ) ?
456 self::serialiseUgmForLog( $oldUGMs[$group] ) :
457 null;
458 }, $oldGroups );
459 $newUGMs = array_map( function ( $group ) use ( $newUGMs ) {
460 return isset( $newUGMs[$group] ) ?
461 self::serialiseUgmForLog( $newUGMs[$group] ) :
462 null;
463 }, $newGroups );
464
465 $logEntry = new ManualLogEntry( 'rights', 'rights' );
466 $logEntry->setPerformer( $this->getUser() );
467 $logEntry->setTarget( $user->getUserPage() );
468 $logEntry->setComment( $reason );
469 $logEntry->setParameters( [
470 '4::oldgroups' => $oldGroups,
471 '5::newgroups' => $newGroups,
472 'oldmetadata' => $oldUGMs,
473 'newmetadata' => $newUGMs,
474 ] );
475 $logid = $logEntry->insert();
476 if ( count( $tags ) ) {
477 $logEntry->addTags( $tags );
478 }
479 $logEntry->publish( $logid );
480 }
481
486 function editUserGroupsForm( $username ) {
487 $status = $this->fetchUser( $username, true );
488 if ( !$status->isOK() ) {
489 $this->getOutput()->addWikiTextAsInterface(
490 $status->getWikiText( false, false, $this->getLanguage() )
491 );
492
493 return;
494 }
495
497 $user = $status->value;
498 '@phan-var User $user';
499
500 $groups = $user->getGroups();
501 $groupMemberships = $user->getGroupMemberships();
502 $this->showEditUserGroupsForm( $user, $groups, $groupMemberships );
503
504 // This isn't really ideal logging behavior, but let's not hide the
505 // interwiki logs if we're using them as is.
506 $this->showLogFragment( $user, $this->getOutput() );
507 }
508
518 public function fetchUser( $username, $writing = true ) {
519 $parts = explode( $this->getConfig()->get( 'UserrightsInterwikiDelimiter' ), $username );
520 if ( count( $parts ) < 2 ) {
521 $name = trim( $username );
522 $dbDomain = '';
523 } else {
524 list( $name, $dbDomain ) = array_map( 'trim', $parts );
525
526 if ( WikiMap::isCurrentWikiId( $dbDomain ) ) {
527 $dbDomain = '';
528 } else {
529 if ( $writing && !MediaWikiServices::getInstance()
531 ->userHasRight( $this->getUser(), 'userrights-interwiki' )
532 ) {
533 return Status::newFatal( 'userrights-no-interwiki' );
534 }
535 if ( !UserRightsProxy::validDatabase( $dbDomain ) ) {
536 return Status::newFatal( 'userrights-nodatabase', $dbDomain );
537 }
538 }
539 }
540
541 if ( $name === '' ) {
542 return Status::newFatal( 'nouserspecified' );
543 }
544
545 if ( $name[0] == '#' ) {
546 // Numeric ID can be specified...
547 // We'll do a lookup for the name internally.
548 $id = intval( substr( $name, 1 ) );
549
550 if ( $dbDomain == '' ) {
551 $name = User::whoIs( $id );
552 } else {
553 $name = UserRightsProxy::whoIs( $dbDomain, $id );
554 }
555
556 if ( !$name ) {
557 return Status::newFatal( 'noname' );
558 }
559 } else {
560 $name = User::getCanonicalName( $name );
561 if ( $name === false ) {
562 // invalid name
563 return Status::newFatal( 'nosuchusershort', $username );
564 }
565 }
566
567 if ( $dbDomain == '' ) {
568 $user = User::newFromName( $name );
569 } else {
570 $user = UserRightsProxy::newFromName( $dbDomain, $name );
571 }
572
573 if ( !$user || $user->isAnon() ) {
574 return Status::newFatal( 'nosuchusershort', $username );
575 }
576
577 if ( $user instanceof User &&
578 $user->isHidden() &&
579 !MediaWikiServices::getInstance()
580 ->getPermissionManager()
581 ->userHasRight( $this->getUser(), 'hideuser' )
582 ) {
583 // Cannot see hidden users, pretend they don't exist
584 return Status::newFatal( 'nosuchusershort', $username );
585 }
586
587 return Status::newGood( $user );
588 }
589
597 public function makeGroupNameList( $ids ) {
598 if ( empty( $ids ) ) {
599 return $this->msg( 'rightsnone' )->inContentLanguage()->text();
600 } else {
601 return implode( ', ', $ids );
602 }
603 }
604
608 function switchForm() {
609 $this->getOutput()->addModules( 'mediawiki.userSuggest' );
610
611 $this->getOutput()->addHTML(
612 Html::openElement(
613 'form',
614 [
615 'method' => 'get',
616 'action' => wfScript(),
617 'name' => 'uluser',
618 'id' => 'mw-userrights-form1'
619 ]
620 ) .
621 Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) .
622 Xml::fieldset( $this->msg( 'userrights-lookup-user' )->text() ) .
623 Xml::inputLabel(
624 $this->msg( 'userrights-user-editname' )->text(),
625 'user',
626 'username',
627 30,
628 str_replace( '_', ' ', $this->mTarget ),
629 [
630 'class' => 'mw-autocomplete-user', // used by mediawiki.userSuggest
631 ] + (
632 // Set autofocus on blank input and error input
633 $this->mFetchedUser === null ? [ 'autofocus' => '' ] : []
634 )
635 ) . ' ' .
636 Xml::submitButton(
637 $this->msg( 'editusergroup' )->text()
638 ) .
639 Html::closeElement( 'fieldset' ) .
640 Html::closeElement( 'form' ) . "\n"
641 );
642 }
643
653 protected function showEditUserGroupsForm( $user, $groups, $groupMemberships ) {
654 $list = $membersList = $tempList = $tempMembersList = [];
655 foreach ( $groupMemberships as $ugm ) {
656 $linkG = UserGroupMembership::getLink( $ugm, $this->getContext(), 'html' );
657 $linkM = UserGroupMembership::getLink( $ugm, $this->getContext(), 'html',
658 $user->getName() );
659 if ( $ugm->getExpiry() ) {
660 $tempList[] = $linkG;
661 $tempMembersList[] = $linkM;
662 } else {
663 $list[] = $linkG;
664 $membersList[] = $linkM;
665
666 }
667 }
668
669 $autoList = [];
670 $autoMembersList = [];
671 if ( $user instanceof User ) {
672 foreach ( Autopromote::getAutopromoteGroups( $user ) as $group ) {
673 $autoList[] = UserGroupMembership::getLink( $group, $this->getContext(), 'html' );
674 $autoMembersList[] = UserGroupMembership::getLink( $group, $this->getContext(),
675 'html', $user->getName() );
676 }
677 }
678
679 $language = $this->getLanguage();
680 $displayedList = $this->msg( 'userrights-groupsmember-type' )
681 ->rawParams(
682 $language->commaList( array_merge( $tempList, $list ) ),
683 $language->commaList( array_merge( $tempMembersList, $membersList ) )
684 )->escaped();
685 $displayedAutolist = $this->msg( 'userrights-groupsmember-type' )
686 ->rawParams(
687 $language->commaList( $autoList ),
688 $language->commaList( $autoMembersList )
689 )->escaped();
690
691 $grouplist = '';
692 $count = count( $list ) + count( $tempList );
693 if ( $count > 0 ) {
694 $grouplist = $this->msg( 'userrights-groupsmember' )
695 ->numParams( $count )
696 ->params( $user->getName() )
697 ->parse();
698 $grouplist = '<p>' . $grouplist . ' ' . $displayedList . "</p>\n";
699 }
700
701 $count = count( $autoList );
702 if ( $count > 0 ) {
703 $autogrouplistintro = $this->msg( 'userrights-groupsmember-auto' )
704 ->numParams( $count )
705 ->params( $user->getName() )
706 ->parse();
707 $grouplist .= '<p>' . $autogrouplistintro . ' ' . $displayedAutolist . "</p>\n";
708 }
709
710 $userToolLinks = Linker::userToolLinks(
711 $user->getId(),
712 $user->getName(),
713 false, /* default for redContribsWhenNoEdits */
714 Linker::TOOL_LINKS_EMAIL /* Add "send e-mail" link */
715 );
716
717 list( $groupCheckboxes, $canChangeAny ) =
718 $this->groupCheckboxes( $groupMemberships, $user );
719 $this->getOutput()->addHTML(
720 Xml::openElement(
721 'form',
722 [
723 'method' => 'post',
724 'action' => $this->getPageTitle()->getLocalURL(),
725 'name' => 'editGroup',
726 'id' => 'mw-userrights-form2'
727 ]
728 ) .
729 Html::hidden( 'user', $this->mTarget ) .
730 Html::hidden( 'wpEditToken', $this->getUser()->getEditToken( $this->mTarget ) ) .
731 Html::hidden(
732 'conflictcheck-originalgroups',
733 implode( ',', $user->getGroups() )
734 ) . // Conflict detection
735 Xml::openElement( 'fieldset' ) .
736 Xml::element(
737 'legend',
738 [],
739 $this->msg(
740 $canChangeAny ? 'userrights-editusergroup' : 'userrights-viewusergroup',
741 $user->getName()
742 )->text()
743 ) .
744 $this->msg(
745 $canChangeAny ? 'editinguser' : 'viewinguserrights'
746 )->params( wfEscapeWikiText( $user->getName() ) )
747 ->rawParams( $userToolLinks )->parse()
748 );
749 if ( $canChangeAny ) {
750 $this->getOutput()->addHTML(
751 $this->msg( 'userrights-groups-help', $user->getName() )->parse() .
752 $grouplist .
753 $groupCheckboxes .
754 Xml::openElement( 'table', [ 'id' => 'mw-userrights-table-outer' ] ) .
755 "<tr>
756 <td class='mw-label'>" .
757 Xml::label( $this->msg( 'userrights-reason' )->text(), 'wpReason' ) .
758 "</td>
759 <td class='mw-input'>" .
760 Xml::input( 'user-reason', 60, $this->getRequest()->getVal( 'user-reason', false ), [
761 'id' => 'wpReason',
762 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
763 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
764 // Unicode codepoints.
765 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
766 ] ) .
767 "</td>
768 </tr>
769 <tr>
770 <td></td>
771 <td class='mw-submit'>" .
772 Xml::submitButton( $this->msg( 'saveusergroups', $user->getName() )->text(),
773 [ 'name' => 'saveusergroups' ] +
774 Linker::tooltipAndAccesskeyAttribs( 'userrights-set' )
775 ) .
776 "</td>
777 </tr>" .
778 Xml::closeElement( 'table' ) . "\n"
779 );
780 } else {
781 $this->getOutput()->addHTML( $grouplist );
782 }
783 $this->getOutput()->addHTML(
784 Xml::closeElement( 'fieldset' ) .
785 Xml::closeElement( 'form' ) . "\n"
786 );
787 }
788
793 protected static function getAllGroups() {
794 return User::getAllGroups();
795 }
796
806 private function groupCheckboxes( $usergroups, $user ) {
807 $allgroups = $this->getAllGroups();
808 $ret = '';
809
810 // Get the list of preset expiry times from the system message
811 $expiryOptionsMsg = $this->msg( 'userrights-expiry-options' )->inContentLanguage();
812 $expiryOptions = $expiryOptionsMsg->isDisabled() ?
813 [] :
814 explode( ',', $expiryOptionsMsg->text() );
815
816 // Put all column info into an associative array so that extensions can
817 // more easily manage it.
818 $columns = [ 'unchangeable' => [], 'changeable' => [] ];
819
820 foreach ( $allgroups as $group ) {
821 $set = isset( $usergroups[$group] );
822 // Users who can add the group, but not remove it, can only lengthen
823 // expiries, not shorten them. So they should only see the expiry
824 // dropdown if the group currently has a finite expiry
825 $canOnlyLengthenExpiry = ( $set && $this->canAdd( $group ) &&
826 !$this->canRemove( $group ) && $usergroups[$group]->getExpiry() );
827 // Should the checkbox be disabled?
828 $disabledCheckbox = !(
829 ( $set && $this->canRemove( $group ) ) ||
830 ( !$set && $this->canAdd( $group ) ) );
831 // Should the expiry elements be disabled?
832 $disabledExpiry = $disabledCheckbox && !$canOnlyLengthenExpiry;
833 // Do we need to point out that this action is irreversible?
834 $irreversible = !$disabledCheckbox && (
835 ( $set && !$this->canAdd( $group ) ) ||
836 ( !$set && !$this->canRemove( $group ) ) );
837
838 $checkbox = [
839 'set' => $set,
840 'disabled' => $disabledCheckbox,
841 'disabled-expiry' => $disabledExpiry,
842 'irreversible' => $irreversible
843 ];
844
845 if ( $disabledCheckbox && $disabledExpiry ) {
846 $columns['unchangeable'][$group] = $checkbox;
847 } else {
848 $columns['changeable'][$group] = $checkbox;
849 }
850 }
851
852 // Build the HTML table
853 $ret .= Xml::openElement( 'table', [ 'class' => 'mw-userrights-groups' ] ) .
854 "<tr>\n";
855 foreach ( $columns as $name => $column ) {
856 if ( $column === [] ) {
857 continue;
858 }
859 // Messages: userrights-changeable-col, userrights-unchangeable-col
860 $ret .= Xml::element(
861 'th',
862 null,
863 $this->msg( 'userrights-' . $name . '-col', count( $column ) )->text()
864 );
865 }
866
867 $ret .= "</tr>\n<tr>\n";
868 foreach ( $columns as $column ) {
869 if ( $column === [] ) {
870 continue;
871 }
872 $ret .= "\t<td style='vertical-align:top;'>\n";
873 foreach ( $column as $group => $checkbox ) {
874 $attr = [ 'class' => 'mw-userrights-groupcheckbox' ];
875 if ( $checkbox['disabled'] ) {
876 $attr['disabled'] = 'disabled';
877 }
878
879 $member = UserGroupMembership::getGroupMemberName( $group, $user->getName() );
880 if ( $checkbox['irreversible'] ) {
881 $text = $this->msg( 'userrights-irreversible-marker', $member )->text();
882 } elseif ( $checkbox['disabled'] && !$checkbox['disabled-expiry'] ) {
883 $text = $this->msg( 'userrights-no-shorten-expiry-marker', $member )->text();
884 } else {
885 $text = $member;
886 }
887 $checkboxHtml = Xml::checkLabel( $text, "wpGroup-" . $group,
888 "wpGroup-" . $group, $checkbox['set'], $attr );
889
890 if ( $this->canProcessExpiries() ) {
891 $uiUser = $this->getUser();
892 $uiLanguage = $this->getLanguage();
893
894 $currentExpiry = isset( $usergroups[$group] ) ?
895 $usergroups[$group]->getExpiry() :
896 null;
897
898 // If the user can't modify the expiry, print the current expiry below
899 // it in plain text. Otherwise provide UI to set/change the expiry
900 if ( $checkbox['set'] &&
901 ( $checkbox['irreversible'] || $checkbox['disabled-expiry'] )
902 ) {
903 if ( $currentExpiry ) {
904 $expiryFormatted = $uiLanguage->userTimeAndDate( $currentExpiry, $uiUser );
905 $expiryFormattedD = $uiLanguage->userDate( $currentExpiry, $uiUser );
906 $expiryFormattedT = $uiLanguage->userTime( $currentExpiry, $uiUser );
907 $expiryHtml = $this->msg( 'userrights-expiry-current' )->params(
908 $expiryFormatted, $expiryFormattedD, $expiryFormattedT )->text();
909 } else {
910 $expiryHtml = $this->msg( 'userrights-expiry-none' )->text();
911 }
912 // T171345: Add a hidden form element so that other groups can still be manipulated,
913 // otherwise saving errors out with an invalid expiry time for this group.
914 $expiryHtml .= Html::hidden( "wpExpiry-$group",
915 $currentExpiry ? 'existing' : 'infinite' );
916 $expiryHtml .= "<br />\n";
917 } else {
918 $expiryHtml = Xml::element( 'span', null,
919 $this->msg( 'userrights-expiry' )->text() );
920 $expiryHtml .= Xml::openElement( 'span' );
921
922 // add a form element to set the expiry date
923 $expiryFormOptions = new XmlSelect(
924 "wpExpiry-$group",
925 "mw-input-wpExpiry-$group", // forward compatibility with HTMLForm
926 $currentExpiry ? 'existing' : 'infinite'
927 );
928 if ( $checkbox['disabled-expiry'] ) {
929 $expiryFormOptions->setAttribute( 'disabled', 'disabled' );
930 }
931
932 if ( $currentExpiry ) {
933 $timestamp = $uiLanguage->userTimeAndDate( $currentExpiry, $uiUser );
934 $d = $uiLanguage->userDate( $currentExpiry, $uiUser );
935 $t = $uiLanguage->userTime( $currentExpiry, $uiUser );
936 $existingExpiryMessage = $this->msg( 'userrights-expiry-existing',
937 $timestamp, $d, $t );
938 $expiryFormOptions->addOption( $existingExpiryMessage->text(), 'existing' );
939 }
940
941 $expiryFormOptions->addOption(
942 $this->msg( 'userrights-expiry-none' )->text(),
943 'infinite'
944 );
945 $expiryFormOptions->addOption(
946 $this->msg( 'userrights-expiry-othertime' )->text(),
947 'other'
948 );
949 foreach ( $expiryOptions as $option ) {
950 if ( strpos( $option, ":" ) === false ) {
951 $displayText = $value = $option;
952 } else {
953 list( $displayText, $value ) = explode( ":", $option );
954 }
955 $expiryFormOptions->addOption( $displayText, htmlspecialchars( $value ) );
956 }
957
958 // Add expiry dropdown
959 $expiryHtml .= $expiryFormOptions->getHTML() . '<br />';
960
961 // Add custom expiry field
962 $attribs = [
963 'id' => "mw-input-wpExpiry-$group-other",
964 'class' => 'mw-userrights-expiryfield',
965 ];
966 if ( $checkbox['disabled-expiry'] ) {
967 $attribs['disabled'] = 'disabled';
968 }
969 $expiryHtml .= Xml::input( "wpExpiry-$group-other", 30, '', $attribs );
970
971 // If the user group is set but the checkbox is disabled, mimic a
972 // checked checkbox in the form submission
973 if ( $checkbox['set'] && $checkbox['disabled'] ) {
974 $expiryHtml .= Html::hidden( "wpGroup-$group", 1 );
975 }
976
977 $expiryHtml .= Xml::closeElement( 'span' );
978 }
979
980 $divAttribs = [
981 'id' => "mw-userrights-nested-wpGroup-$group",
982 'class' => 'mw-userrights-nested',
983 ];
984 $checkboxHtml .= "\t\t\t" . Xml::tags( 'div', $divAttribs, $expiryHtml ) . "\n";
985 }
986 $ret .= "\t\t" . ( ( $checkbox['disabled'] && $checkbox['disabled-expiry'] )
987 ? Xml::tags( 'div', [ 'class' => 'mw-userrights-disabled' ], $checkboxHtml )
988 : Xml::tags( 'div', [], $checkboxHtml )
989 ) . "\n";
990 }
991 $ret .= "\t</td>\n";
992 }
993 $ret .= Xml::closeElement( 'tr' ) . Xml::closeElement( 'table' );
994
995 return [ $ret, (bool)$columns['changeable'] ];
996 }
997
1002 private function canRemove( $group ) {
1003 $groups = $this->changeableGroups();
1004
1005 return in_array(
1006 $group,
1007 $groups['remove'] ) || ( $this->isself && in_array( $group, $groups['remove-self'] )
1008 );
1009 }
1010
1015 private function canAdd( $group ) {
1016 $groups = $this->changeableGroups();
1017
1018 return in_array(
1019 $group,
1020 $groups['add'] ) || ( $this->isself && in_array( $group, $groups['add-self'] )
1021 );
1022 }
1023
1034 function changeableGroups() {
1035 return $this->getUser()->changeableGroups();
1036 }
1037
1044 protected function showLogFragment( $user, $output ) {
1045 $rightsLogPage = new LogPage( 'rights' );
1046 $output->addHTML( Xml::element( 'h2', null, $rightsLogPage->getName()->text() ) );
1047 LogEventsList::showLogExtract( $output, 'rights', $user->getUserPage() );
1048 }
1049
1058 public function prefixSearchSubpages( $search, $limit, $offset ) {
1059 $user = User::newFromName( $search );
1060 if ( !$user ) {
1061 // No prefix suggestion for invalid user
1062 return [];
1063 }
1064 // Autocomplete subpage as user list - public to allow caching
1065 return UserNamePrefixSearch::search( 'public', $search, $limit, $offset );
1066 }
1067
1068 protected function getGroupName() {
1069 return 'users';
1070 }
1071}
getPermissionManager()
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,...
static getAutopromoteGroups(User $user)
Get the groups for the given user based on $wgAutopromote.
const TOOL_LINKS_EMAIL
Definition Linker.php:40
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:943
static tooltipAndAccesskeyAttribs( $name, array $msgParams=[], $options=null)
Returns the attributes for the tooltip and access key.
Definition Linker.php:2195
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
Class to simplify the use of log pages.
Definition LogPage.php:33
Class for creating new log entries and inserting them into the database.
MediaWikiServices is the service locator for the application scope of MediaWiki.
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.
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 search( $audience, $search, $limit, $offset=0)
Do a prefix search of user names and return a list of matching user names.
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:51
isHidden()
Check if user account is hidden.
Definition User.php:2318
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.
static getAllGroups()
Returns an array of all groups that may be edited.
switchForm()
Output a form to allow searching for a user.
$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.
userCanChangeRights( $targetUser, $checkIfSelf=true)
Check whether the current user (from context) can change the target user's rights.
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.
fetchUser( $username, $writing=true)
Normalize the input username, which may be local or remote, and return a user (or proxy) object for m...
showLogFragment( $user, $output)
Show a rights log fragment for the specified user.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
execute( $par)
Manage forms to be shown according to posted data.
saveUserGroups( $username, $reason, $user)
Save user groups changes in the database.
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()
Class for generating HTML <select> or <datalist> elements.
Definition XmlSelect.php:26
setAttribute( $name, $value)
Definition XmlSelect.php:64