MediaWiki master
SpecialListGroupRights.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\Specials;
8
24
34
35 public const RESTRICTED_GROUPS_SECTION_ID = 'restricted_groups';
36 private const RESTRICTED_GROUPS_ID_PREFIX = 'group_restrictions-';
37
38 private readonly ILanguageConverter $languageConverter;
39
40 public function __construct(
41 private readonly NamespaceInfo $nsInfo,
42 private readonly UserGroupManager $userGroupManager,
43 LanguageConverterFactory $languageConverterFactory,
44 private readonly GroupPermissionsLookup $groupPermissionsLookup,
45 private readonly RestrictedUserGroupConfigReader $restrictedUserGroupConfigReader,
46 ) {
47 parent::__construct( 'Listgrouprights' );
48 $this->languageConverter = $languageConverterFactory->getLanguageConverter( $this->getContentLanguage() );
49 }
50
55 public function execute( $par ) {
56 $this->setHeaders();
57 $this->outputHeader();
58
59 $out = $this->getOutput();
60 $out->addModuleStyles( 'mediawiki.special' );
61 $this->addHelpLink( 'Help:User_rights_and_groups' );
62
63 $out->wrapWikiMsg( "<div class=\"mw-listgrouprights-key\">\n$1\n</div>", 'listgrouprights-key' );
64
65 $out->addHTML(
66 Html::openElement( 'table', [ 'class' => [ 'wikitable', 'mw-listgrouprights-table' ] ] ) .
67 '<tr>' .
68 Html::element( 'th', [], $this->msg( 'listgrouprights-group' )->text() ) .
69 Html::element( 'th', [], $this->msg( 'listgrouprights-rights' )->text() ) .
70 '</tr>'
71 );
72
73 $config = $this->getConfig();
74 $addGroups = $config->get( MainConfigNames::AddGroups );
75 $removeGroups = $config->get( MainConfigNames::RemoveGroups );
76 $groupsAddToSelf = $config->get( MainConfigNames::GroupsAddToSelf );
77 $groupsRemoveFromSelf = $config->get( MainConfigNames::GroupsRemoveFromSelf );
78 $allGroups = array_merge(
79 $this->userGroupManager->listAllGroups(),
80 $this->userGroupManager->listAllImplicitGroups()
81 );
82 asort( $allGroups );
83
84 $linkRenderer = $this->getLinkRenderer();
85 $lang = $this->getLanguage();
86 $restrictedGroups = $this->restrictedUserGroupConfigReader->getConfig();
87
88 foreach ( $allGroups as $group ) {
89 $permissions = $this->groupPermissionsLookup->getGrantedPermissions( $group );
90 $groupname = ( $group == '*' ) // Replace * with a more descriptive groupname
91 ? 'all'
92 : $group;
93
94 $groupnameLocalized = $lang->getGroupName( $groupname );
95
96 $grouppageLocalizedTitle = UserGroupMembership::getGroupPage( $groupname )
97 ?: Title::makeTitleSafe( NS_PROJECT, $groupname );
98
99 if ( $group == '*' || !$grouppageLocalizedTitle ) {
100 // Do not make a link for the generic * group or group with invalid group page
101 $grouppage = htmlspecialchars( $groupnameLocalized );
102 } else {
103 $grouppage = $linkRenderer->makeLink(
104 $grouppageLocalizedTitle,
105 $groupnameLocalized
106 );
107 }
108
109 $groupWithParentheses = $this->msg( 'parentheses' )->rawParams( $group )->escaped();
110 $groupname = "<br /><code>$groupWithParentheses</code>";
111
112 if ( $group === 'user' ) {
113 // Link to Special:listusers for implicit group 'user'
114 $grouplink = '<br />' . $linkRenderer->makeKnownLink(
115 SpecialPage::getTitleFor( 'Listusers' ),
116 $this->msg( 'listgrouprights-members' )->text()
117 );
118 } elseif ( !in_array( $group, $config->get( MainConfigNames::ImplicitGroups ) ) ) {
119 $grouplink = '<br />' . $linkRenderer->makeKnownLink(
120 SpecialPage::getTitleFor( 'Listusers' ),
121 $this->msg( 'listgrouprights-members' )->text(),
122 [],
123 [ 'group' => $group ]
124 );
125 } else {
126 // No link to Special:listusers for other implicit groups as they are unlistable
127 $grouplink = '';
128 }
129
130 $restrictionsLink = '';
131 if ( array_key_exists( $group, $restrictedGroups ) && $restrictedGroups[$group]->hasAnyConditions() ) {
132 $restrictionsSection = Sanitizer::escapeIdForAttribute( self::RESTRICTED_GROUPS_ID_PREFIX . $group );
133 $restrictionsLink = Html::rawElement( 'p', [],
134 $this->msg( 'listgrouprights-restricted' )
135 ->params( '#' . $restrictionsSection )
136 ->parse()
137 );
138 }
139
140 $revoke = $this->groupPermissionsLookup->getRevokedPermissions( $group );
141 $addgroups = $addGroups[$group] ?? [];
142 $removegroups = $removeGroups[$group] ?? [];
143 $addgroupsSelf = $groupsAddToSelf[$group] ?? [];
144 $removegroupsSelf = $groupsRemoveFromSelf[$group] ?? [];
145
146 $id = $group == '*' ? false : Sanitizer::escapeIdForAttribute( $group );
147 $out->addHTML( Html::rawElement( 'tr', [ 'id' => $id ], "
148 <td>$grouppage$groupname$grouplink$restrictionsLink</td>
149 <td>" .
150 $this->formatPermissions( $permissions, $revoke, $addgroups, $removegroups,
151 $addgroupsSelf, $removegroupsSelf ) .
152 '</td>
153 '
154 ) );
155 }
156 $out->addHTML( Html::closeElement( 'table' ) );
157 $this->outputRestrictedGroupsConfig();
158 $this->outputNamespaceProtectionInfo();
159 }
160
161 private function outputNamespaceProtectionInfo() {
162 $out = $this->getOutput();
163 $namespaceProtection = $this->getConfig()->get( MainConfigNames::NamespaceProtection );
164
165 if ( count( $namespaceProtection ) == 0 ) {
166 return;
167 }
168
169 $header = $this->msg( 'listgrouprights-namespaceprotection-header' )->text();
170 $out->addHTML(
171 Html::element( 'h2', [
172 'id' => Sanitizer::escapeIdForAttribute( $header )
173 ], $header ) .
174 Html::openElement( 'table', [ 'class' => 'wikitable' ] ) .
176 'th',
177 [],
178 $this->msg( 'listgrouprights-namespaceprotection-namespace' )->text()
179 ) .
181 'th',
182 [],
183 $this->msg( 'listgrouprights-namespaceprotection-restrictedto' )->text()
184 )
185 );
186 $linkRenderer = $this->getLinkRenderer();
187 ksort( $namespaceProtection );
188 $validNamespaces = $this->nsInfo->getValidNamespaces();
189 foreach ( $namespaceProtection as $namespace => $rights ) {
190 if ( !in_array( $namespace, $validNamespaces ) ) {
191 continue;
192 }
193
194 if ( $namespace == NS_MAIN ) {
195 $namespaceText = $this->msg( 'blanknamespace' )->text();
196 } else {
197 $namespaceText = $this->languageConverter->convertNamespace( $namespace );
198 }
199
200 $out->addHTML(
201 Html::openElement( 'tr' ) .
202 Html::rawElement(
203 'td',
204 [],
205 $linkRenderer->makeLink(
206 SpecialPage::getTitleFor( 'Allpages' ),
207 $namespaceText,
208 [],
209 [ 'namespace' => $namespace ]
210 )
211 ) .
212 Html::openElement( 'td' ) . Html::openElement( 'ul' )
213 );
214
215 if ( !is_array( $rights ) ) {
216 $rights = [ $rights ];
217 }
218
219 foreach ( $rights as $right ) {
220 $out->addHTML( Html::rawElement( 'li', [],
221 $this->msg( 'listgrouprights-right-display' )
222 ->params( User::getRightDescription( $right ) )
223 ->rawParams( Html::element(
224 'span',
225 [ 'class' => 'mw-listgrouprights-right-name' ],
226 $right
227 ) )->parse()
228 ) );
229 }
230
231 $out->addHTML(
232 Html::closeElement( 'ul' ) .
233 Html::closeElement( 'td' ) .
234 Html::closeElement( 'tr' )
235 );
236 }
237 $out->addHTML( Html::closeElement( 'table' ) );
238 }
239
240 private function outputRestrictedGroupsConfig() {
241 $out = $this->getOutput();
242 $restrictedGroups = $this->restrictedUserGroupConfigReader->getConfig();
243
244 if ( !$restrictedGroups ) {
245 return;
246 }
247
248 $header = $this->msg( 'listgrouprights-restrictedgroups-header' )->text();
249 $out->addHTML(
250 Html::element( 'h2', [
251 'id' => Sanitizer::escapeIdForAttribute( self::RESTRICTED_GROUPS_SECTION_ID )
252 ], $header ) .
253 Html::openElement( 'table', [ 'class' => 'wikitable mw-listgrouprights-table' ] ) .
255 'th',
256 [],
257 $this->msg( 'listgrouprights-group' )->text()
258 ) .
260 'th',
261 [],
262 $this->msg( 'listgrouprights-restrictedgroups-config' )->text()
263 )
264 );
265 ksort( $restrictedGroups );
266
267 $lang = $this->getLanguage();
268 $linkRenderer = $this->getLinkRenderer();
269 $allGroups = array_merge(
270 $this->userGroupManager->listAllGroups(),
271 $this->userGroupManager->listAllImplicitGroups()
272 );
273 foreach ( $restrictedGroups as $group => $groupConfig ) {
274 if ( !$groupConfig->hasAnyConditions() || !in_array( $group, $allGroups ) ) {
275 continue;
276 }
277
278 $out->addHTML(
279 Html::openElement(
280 'tr',
281 [ 'id' => Sanitizer::escapeIdForAttribute( self::RESTRICTED_GROUPS_ID_PREFIX . $group ) ]
282 ) .
283 Html::rawElement(
284 'td',
285 [],
286 $linkRenderer->makeKnownLink(
287 new TitleValue( NS_SPECIAL, $this->getLocalName(), $group ),
288 $lang->getGroupName( $group )
289 )
290 ) .
291 Html::openElement( 'td' )
292 );
293
294 $conditionsParts = [];
295 $memberConditions = $groupConfig->getMemberConditions();
296 if ( $memberConditions ) {
297 $memberHtml = $this->msg( 'listgrouprights-restrictedgroups-memberconditions' )->parse();
298 $memberHtml .= Html::rawElement( 'ul', [],
299 Html::rawElement( 'li', [], $this->formatCondition( $memberConditions ) )
300 );
301 $conditionsParts[] = $memberHtml;
302 }
303 $updaterConditions = $groupConfig->getUpdaterConditions();
304 if ( $updaterConditions ) {
305 $updaterHtml = $this->msg( 'listgrouprights-restrictedgroups-updaterconditions' )->parse();
306 $updaterHtml .= Html::rawElement( 'ul', [],
307 Html::rawElement( 'li', [], $this->formatCondition( $updaterConditions ) )
308 );
309 $conditionsParts[] = $updaterHtml;
310 }
311 if ( $groupConfig->canBeIgnored() ) {
312 $conditionsParts[] = $this->msg( 'listgrouprights-restrictedgroups-bypassable' )
313 ->params( User::getRightDescription( 'ignore-restricted-groups' ) )
314 ->rawParams( Html::element( 'code', [], 'ignore-restricted-groups' ) )
315 ->parse();
316 }
317 if ( $groupConfig->allowsAutomaticDemotion() ) {
318 $conditionsParts[] = $this->msg( 'listgrouprights-restrictedgroups-autodemotion' )->parse();
319 }
320 $out->addHTML( implode( '', $conditionsParts ) );
321
322 $out->addHTML(
323 Html::closeElement( 'td' ) .
324 Html::closeElement( 'tr' )
325 );
326 }
327 $out->addHTML( Html::closeElement( 'table' ) );
328 }
329
341 private function formatPermissions( $permissions, $revoke, $add, $remove, $addSelf, $removeSelf ) {
342 $r = [];
343 foreach ( $permissions as $permission ) {
344 // show as granted only if it isn't revoked to prevent duplicate display of permissions
345 if ( !isset( $revoke[$permission] ) || !$revoke[$permission] ) {
346 $r[] = $this->msg( 'listgrouprights-right-display' )
347 ->params( User::getRightDescription( $permission ) )
348 ->rawParams( Html::element(
349 'span',
350 [ 'class' => 'mw-listgrouprights-right-name' ],
351 $permission
352 ) )->parse();
353 }
354 }
355 foreach ( $revoke as $permission ) {
356 $r[] = $this->msg( 'listgrouprights-right-revoked' )
357 ->params( User::getRightDescription( $permission ) )
358 ->rawParams( Html::element(
359 'span',
360 [ 'class' => 'mw-listgrouprights-right-name' ],
361 $permission
362 ) )->parse();
363 }
364
365 sort( $r );
366
367 $lang = $this->getLanguage();
368 $allGroups = $this->userGroupManager->listAllGroups();
369
370 $changeGroups = [
371 'addgroup' => $add,
372 'removegroup' => $remove,
373 'addgroup-self' => $addSelf,
374 'removegroup-self' => $removeSelf
375 ];
376
377 foreach ( $changeGroups as $messageKey => $changeGroup ) {
378 // @phan-suppress-next-line PhanTypeComparisonFromArray
379 if ( $changeGroup === true ) {
380 // For grep: listgrouprights-addgroup-all, listgrouprights-removegroup-all,
381 // listgrouprights-addgroup-self-all, listgrouprights-removegroup-self-all
382 $r[] = $this->msg( 'listgrouprights-' . $messageKey . '-all' )->escaped();
383 } elseif ( is_array( $changeGroup ) ) {
384 $changeGroup = array_intersect( array_values( array_unique( $changeGroup ) ), $allGroups );
385 if ( count( $changeGroup ) ) {
386 $groupLinks = [];
387 foreach ( $changeGroup as $group ) {
388 $groupLinks[] = UserGroupMembership::getLinkWiki( $group, $this->getContext() );
389 }
390 // For grep: listgrouprights-addgroup, listgrouprights-removegroup,
391 // listgrouprights-addgroup-self, listgrouprights-removegroup-self
392 $r[] = $this->msg( 'listgrouprights-' . $messageKey,
393 $lang->listToText( $groupLinks ), count( $changeGroup ) )->parse();
394 }
395 }
396 }
397
398 if ( !$r ) {
399 return '';
400 } else {
401 return '<ul><li>' . implode( "</li>\n<li>", $r ) . '</li></ul>';
402 }
403 }
404
410 private function formatCondition( $condition ): string {
411 if ( is_array( $condition ) && count( $condition ) > 0 ) {
412 $condName = array_shift( $condition );
413 if ( in_array( $condName, UserRequirementsConditionChecker::VALID_OPS ) ) {
414 $listItems = '';
415 foreach ( $condition as $subcond ) {
416 $listItems .= Html::rawElement( 'li', [], $this->formatCondition( $subcond ) );
417 }
418 $htmlList = Html::rawElement( 'ul', [], $listItems );
419 $condName = match ( $condName ) {
420 '&' => 'listgrouprights-restrictedgroups-op-and',
421 '|' => 'listgrouprights-restrictedgroups-op-or',
422 '^' => 'listgrouprights-restrictedgroups-op-xor',
423 // Even though '!' is usually understood as 'NOT', in fact it's 'NAND' as it can accept
424 // multiple arguments
425 '!' => 'listgrouprights-restrictedgroups-op-nand',
426 };
427 return $this->msg( $condName )
428 ->rawParams( $htmlList )
429 ->parse();
430 } else {
431 return $this->formatAtomicCondition( $condName, $condition );
432 }
433 } elseif ( is_array( $condition ) ) {
434 return '';
435 } else {
436 return $this->formatAtomicCondition( $condition, [] );
437 }
438 }
439
446 private function formatAtomicCondition( $condName, array $args ): string {
447 $msgKey = match ( $condName ) {
448 APCOND_EDITCOUNT => 'listgrouprights-restrictedgroups-cond-editcount',
449 APCOND_AGE => 'listgrouprights-restrictedgroups-cond-age',
450 APCOND_EMAILCONFIRMED => 'listgrouprights-restrictedgroups-cond-emailconfirmed',
451 APCOND_INGROUPS => 'listgrouprights-restrictedgroups-cond-ingroups',
452 APCOND_ISIP => 'listgrouprights-restrictedgroups-cond-isip',
453 APCOND_IPINRANGE => 'listgrouprights-restrictedgroups-cond-ipinrange',
454 APCOND_AGE_FROM_EDIT => 'listgrouprights-restrictedgroups-cond-age-from-edit',
455 APCOND_BLOCKED => 'listgrouprights-restrictedgroups-cond-blocked',
456 APCOND_ISBOT => 'listgrouprights-restrictedgroups-cond-isbot',
457 default => null,
458 };
459
460 if ( $msgKey === null ) {
461 $messageSpec = null;
462 $context = $this->getContext();
463 $this->getHookRunner()->onUserRequirementsConditionDisplay( $condName, $args, $context, $messageSpec );
464 if ( $messageSpec !== null ) {
465 return $this->msg( $messageSpec )->parse();
466 } else {
467 $msgKey = 'listgrouprights-restrictedgroups-cond-' . $condName;
468 }
469 }
470 $msg = $this->msg( $msgKey );
471
472 if ( $condName === APCOND_AGE || $condName === APCOND_AGE_FROM_EDIT ) {
473 $minAge = $args[0] ?? $this->getConfig()->get( MainConfigNames::AutoConfirmAge );
474 $msg->durationParams( $minAge );
475 } elseif ( $condName === APCOND_INGROUPS ) {
476 $groupNames = [];
477 foreach ( $args as $group ) {
478 $groupNames[] = $this->getLanguage()->getGroupName( $group );
479 }
480 $msg->params( count( $args ), $this->getLanguage()->listToText( $groupNames ) );
481 } elseif ( $condName === APCOND_EDITCOUNT ) {
482 $minEdits = $args[0] ?? $this->getConfig()->get( MainConfigNames::AutoConfirmCount );
483 $msg->numParams( $minEdits );
484 } else {
485 $msg->params( ...$args );
486 }
487
488 return $msg->parse();
489 }
490
492 protected function getGroupName() {
493 return 'users';
494 }
495}
496
498class_alias( SpecialListGroupRights::class, 'SpecialListGroupRights' );
const APCOND_BLOCKED
Definition Defines.php:197
const APCOND_AGE
Definition Defines.php:191
const APCOND_EMAILCONFIRMED
Definition Defines.php:192
const APCOND_IPINRANGE
Definition Defines.php:195
const APCOND_INGROUPS
Definition Defines.php:193
const NS_MAIN
Definition Defines.php:51
const APCOND_ISBOT
Definition Defines.php:198
const APCOND_AGE_FROM_EDIT
Definition Defines.php:196
const NS_SPECIAL
Definition Defines.php:40
const APCOND_ISIP
Definition Defines.php:194
const APCOND_EDITCOUNT
Definition Defines.php:190
const NS_PROJECT
Definition Defines.php:55
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:69
This class is a collection of static functions that serve two purposes:
Definition Html.php:43
An interface for creating language converters.
getLanguageConverter( $language=null)
Provide a LanguageConverter for given language.
A class containing constants representing the names of configuration variables.
const AutoConfirmCount
Name constant for the AutoConfirmCount setting, for use with Config::get()
const ImplicitGroups
Name constant for the ImplicitGroups setting, for use with Config::get()
const NamespaceProtection
Name constant for the NamespaceProtection setting, for use with Config::get()
const GroupsRemoveFromSelf
Name constant for the GroupsRemoveFromSelf setting, for use with Config::get()
const AddGroups
Name constant for the AddGroups setting, for use with Config::get()
const AutoConfirmAge
Name constant for the AutoConfirmAge setting, for use with Config::get()
const GroupsAddToSelf
Name constant for the GroupsAddToSelf setting, for use with Config::get()
const RemoveGroups
Name constant for the RemoveGroups setting, for use with Config::get()
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:32
Parent class for all special pages.
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
getConfig()
Shortcut to get main config object.
getContext()
Gets the context this SpecialPage is executed in.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getOutput()
Get the OutputPage being used for this instance.
getContentLanguage()
Shortcut to get content language.
getLocalName()
Get the localised name of the special page.
getLanguage()
Shortcut to get user's language.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages By default the message key is the canonical name of...
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
List all defined user groups and the associated rights.
__construct(private readonly NamespaceInfo $nsInfo, private readonly UserGroupManager $userGroupManager, LanguageConverterFactory $languageConverterFactory, private readonly GroupPermissionsLookup $groupPermissionsLookup, private readonly RestrictedUserGroupConfigReader $restrictedUserGroupConfigReader,)
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Represents the target of a wiki link.
Represents a title within MediaWiki.
Definition Title.php:69
A helper class to read the restricted user groups configuration for a given wiki.
Manage user group memberships.
Represents the membership of one user in one user group.
User class for the MediaWiki software.
Definition User.php:130
The shared interface for all language converters.
element(SerializerNode $parent, SerializerNode $node, $contents)
msg( $key,... $params)