MediaWiki master
SpecialListGroupRights.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\Specials;
8
17use MediaWiki\Specials\Helpers\UserRequirementsConditionFormatterTrait;
25
35
36 use UserRequirementsConditionFormatterTrait;
37
38 public const RESTRICTED_GROUPS_SECTION_ID = 'restricted_groups';
39 private const RESTRICTED_GROUPS_ID_PREFIX = 'group_restrictions-';
40
41 private readonly ILanguageConverter $languageConverter;
42
43 public function __construct(
44 private readonly NamespaceInfo $nsInfo,
45 private readonly UserGroupManager $userGroupManager,
46 LanguageConverterFactory $languageConverterFactory,
47 private readonly GroupPermissionsLookup $groupPermissionsLookup,
48 private readonly RestrictedUserGroupConfigReader $restrictedUserGroupConfigReader,
49 ) {
50 parent::__construct( 'Listgrouprights' );
51 $this->languageConverter = $languageConverterFactory->getLanguageConverter( $this->getContentLanguage() );
52 }
53
58 public function execute( $par ) {
59 $this->setHeaders();
60 $this->outputHeader();
61
62 $out = $this->getOutput();
63 $out->addModuleStyles( 'mediawiki.special' );
64 $this->addHelpLink( 'Help:User_rights_and_groups' );
65
66 $out->wrapWikiMsg( "<div class=\"mw-listgrouprights-key\">\n$1\n</div>", 'listgrouprights-key' );
67
68 $out->addHTML(
69 Html::openElement( 'table', [ 'class' => [ 'wikitable', 'mw-listgrouprights-table' ] ] ) .
70 '<tr>' .
71 Html::element( 'th', [], $this->msg( 'listgrouprights-group' )->text() ) .
72 Html::element( 'th', [], $this->msg( 'listgrouprights-rights' )->text() ) .
73 '</tr>'
74 );
75
76 $config = $this->getConfig();
77 $addGroups = $config->get( MainConfigNames::AddGroups );
78 $removeGroups = $config->get( MainConfigNames::RemoveGroups );
79 $groupsAddToSelf = $config->get( MainConfigNames::GroupsAddToSelf );
80 $groupsRemoveFromSelf = $config->get( MainConfigNames::GroupsRemoveFromSelf );
81 $allGroups = array_merge(
82 $this->userGroupManager->listAllGroups(),
83 $this->userGroupManager->listAllImplicitGroups()
84 );
85 asort( $allGroups );
86
87 $linkRenderer = $this->getLinkRenderer();
88 $lang = $this->getLanguage();
89 $restrictedGroups = $this->restrictedUserGroupConfigReader->getConfig();
90
91 foreach ( $allGroups as $group ) {
92 $permissions = $this->groupPermissionsLookup->getGrantedPermissions( $group );
93 $groupname = ( $group == '*' ) // Replace * with a more descriptive groupname
94 ? 'all'
95 : $group;
96
97 $groupnameLocalized = $lang->getGroupName( $groupname );
98
99 $grouppageLocalizedTitle = UserGroupMembership::getGroupPage( $groupname )
100 ?: Title::makeTitleSafe( NS_PROJECT, $groupname );
101
102 if ( $group == '*' || !$grouppageLocalizedTitle ) {
103 // Do not make a link for the generic * group or group with invalid group page
104 $grouppage = htmlspecialchars( $groupnameLocalized );
105 } else {
106 $grouppage = $linkRenderer->makeLink(
107 $grouppageLocalizedTitle,
108 $groupnameLocalized
109 );
110 }
111
112 $groupWithParentheses = $this->msg( 'parentheses' )->rawParams( $group )->escaped();
113 $groupname = "<br /><code>$groupWithParentheses</code>";
114
115 if ( $group === 'user' ) {
116 // Link to Special:listusers for implicit group 'user'
117 $grouplink = '<br />' . $linkRenderer->makeKnownLink(
118 SpecialPage::getTitleFor( 'Listusers' ),
119 $this->msg( 'listgrouprights-members' )->text()
120 );
121 } elseif ( !in_array( $group, $config->get( MainConfigNames::ImplicitGroups ) ) ) {
122 $grouplink = '<br />' . $linkRenderer->makeKnownLink(
123 SpecialPage::getTitleFor( 'Listusers' ),
124 $this->msg( 'listgrouprights-members' )->text(),
125 [],
126 [ 'group' => $group ]
127 );
128 } else {
129 // No link to Special:listusers for other implicit groups as they are unlistable
130 $grouplink = '';
131 }
132
133 $restrictionsLink = '';
134 if ( array_key_exists( $group, $restrictedGroups ) && $restrictedGroups[$group]->hasAnyConditions() ) {
135 $restrictionsSection = Sanitizer::escapeIdForAttribute( self::RESTRICTED_GROUPS_ID_PREFIX . $group );
136 $restrictionsLink = Html::rawElement( 'p', [],
137 $this->msg( 'listgrouprights-restricted' )
138 ->params( '#' . $restrictionsSection )
139 ->parse()
140 );
141 }
142
143 $revoke = $this->groupPermissionsLookup->getRevokedPermissions( $group );
144 $addgroups = $addGroups[$group] ?? [];
145 $removegroups = $removeGroups[$group] ?? [];
146 $addgroupsSelf = $groupsAddToSelf[$group] ?? [];
147 $removegroupsSelf = $groupsRemoveFromSelf[$group] ?? [];
148
149 $id = $group == '*' ? false : Sanitizer::escapeIdForAttribute( $group );
150 $out->addHTML( Html::rawElement( 'tr', [ 'id' => $id ], "
151 <td>$grouppage$groupname$grouplink$restrictionsLink</td>
152 <td>" .
153 $this->formatPermissions( $permissions, $revoke, $addgroups, $removegroups,
154 $addgroupsSelf, $removegroupsSelf ) .
155 '</td>
156 '
157 ) );
158 }
159 $out->addHTML( Html::closeElement( 'table' ) );
160 $this->outputRestrictedGroupsConfig();
161 $this->outputNamespaceProtectionInfo();
162 }
163
164 private function outputNamespaceProtectionInfo() {
165 $out = $this->getOutput();
166 $namespaceProtection = $this->getConfig()->get( MainConfigNames::NamespaceProtection );
167
168 if ( count( $namespaceProtection ) == 0 ) {
169 return;
170 }
171
172 $header = $this->msg( 'listgrouprights-namespaceprotection-header' )->text();
173 $out->addHTML(
174 Html::element( 'h2', [
175 'id' => Sanitizer::escapeIdForAttribute( $header )
176 ], $header ) .
177 Html::openElement( 'table', [ 'class' => 'wikitable' ] ) .
179 'th',
180 [],
181 $this->msg( 'listgrouprights-namespaceprotection-namespace' )->text()
182 ) .
184 'th',
185 [],
186 $this->msg( 'listgrouprights-namespaceprotection-restrictedto' )->text()
187 )
188 );
189 $linkRenderer = $this->getLinkRenderer();
190 ksort( $namespaceProtection );
191 $validNamespaces = $this->nsInfo->getValidNamespaces();
192 foreach ( $namespaceProtection as $namespace => $rights ) {
193 if ( !in_array( $namespace, $validNamespaces ) ) {
194 continue;
195 }
196
197 if ( $namespace == NS_MAIN ) {
198 $namespaceText = $this->msg( 'blanknamespace' )->text();
199 } else {
200 $namespaceText = $this->languageConverter->convertNamespace( $namespace );
201 }
202
203 $out->addHTML(
204 Html::openElement( 'tr' ) .
205 Html::rawElement(
206 'td',
207 [],
208 $linkRenderer->makeLink(
209 SpecialPage::getTitleFor( 'Allpages' ),
210 $namespaceText,
211 [],
212 [ 'namespace' => $namespace ]
213 )
214 ) .
215 Html::openElement( 'td' ) . Html::openElement( 'ul' )
216 );
217
218 if ( !is_array( $rights ) ) {
219 $rights = [ $rights ];
220 }
221
222 foreach ( $rights as $right ) {
223 $out->addHTML( Html::rawElement( 'li', [],
224 $this->msg( 'listgrouprights-right-display' )
225 ->params( User::getRightDescription( $right ) )
226 ->rawParams( Html::element(
227 'span',
228 [ 'class' => 'mw-listgrouprights-right-name' ],
229 $right
230 ) )->parse()
231 ) );
232 }
233
234 $out->addHTML(
235 Html::closeElement( 'ul' ) .
236 Html::closeElement( 'td' ) .
237 Html::closeElement( 'tr' )
238 );
239 }
240 $out->addHTML( Html::closeElement( 'table' ) );
241 }
242
243 private function outputRestrictedGroupsConfig() {
244 $out = $this->getOutput();
245 $restrictedGroups = $this->restrictedUserGroupConfigReader->getConfig();
246
247 $allGroups = array_merge(
248 $this->userGroupManager->listAllGroups(),
249 $this->userGroupManager->listAllImplicitGroups()
250 );
251 $restrictedGroups = array_filter(
252 $restrictedGroups,
253 static fn ( $restriction, $group ) => in_array( $group, $allGroups ) && $restriction->hasAnyConditions(),
254 ARRAY_FILTER_USE_BOTH
255 );
256
257 if ( !$restrictedGroups ) {
258 return;
259 }
260
261 $header = $this->msg( 'listgrouprights-restrictedgroups-header' )->text();
262 $out->addHTML(
263 Html::element( 'h2', [
264 'id' => Sanitizer::escapeIdForAttribute( self::RESTRICTED_GROUPS_SECTION_ID )
265 ], $header ) .
266 Html::openElement( 'table', [ 'class' => 'wikitable mw-listgrouprights-table' ] ) .
267 Html::rawElement( 'tr', [],
269 'th',
270 [],
271 $this->msg( 'listgrouprights-group' )->text()
272 ) .
274 'th',
275 [],
276 $this->msg( 'listgrouprights-restrictedgroups-config' )->text()
277 )
278 )
279 );
280 ksort( $restrictedGroups );
281
282 $lang = $this->getLanguage();
283 $linkRenderer = $this->getLinkRenderer();
284 foreach ( $restrictedGroups as $group => $groupConfig ) {
285 $out->addHTML(
286 Html::openElement(
287 'tr',
288 [ 'id' => Sanitizer::escapeIdForAttribute( self::RESTRICTED_GROUPS_ID_PREFIX . $group ) ]
289 ) .
290 Html::rawElement(
291 'td',
292 [],
293 $linkRenderer->makeKnownLink(
294 new TitleValue( NS_SPECIAL, $this->getLocalName(), $group ),
295 $lang->getGroupName( $group )
296 )
297 ) .
298 Html::openElement( 'td' )
299 );
300
301 $conditionsParts = [];
302 $memberConditions = $groupConfig->getMemberConditions();
303 if ( $memberConditions ) {
304 $memberHtml = $this->msg( 'listgrouprights-restrictedgroups-memberconditions' )->parse();
305 $memberHtml .= Html::rawElement( 'ul', [],
306 Html::rawElement( 'li', [], $this->formatCondition( $memberConditions ) )
307 );
308 $conditionsParts[] = $memberHtml;
309 }
310 $updaterConditions = $groupConfig->getUpdaterConditions();
311 if ( $updaterConditions ) {
312 $updaterHtml = $this->msg( 'listgrouprights-restrictedgroups-updaterconditions' )->parse();
313 $updaterHtml .= Html::rawElement( 'ul', [],
314 Html::rawElement( 'li', [], $this->formatCondition( $updaterConditions ) )
315 );
316 $conditionsParts[] = $updaterHtml;
317 }
318 if ( $groupConfig->canBeIgnored() ) {
319 $conditionsParts[] = $this->msg( 'listgrouprights-restrictedgroups-bypassable' )
320 ->params( User::getRightDescription( 'ignore-restricted-groups' ) )
321 ->rawParams( Html::element( 'code', [], 'ignore-restricted-groups' ) )
322 ->parse();
323 }
324 if ( $groupConfig->allowsAutomaticDemotion() ) {
325 $conditionsParts[] = $this->msg( 'listgrouprights-restrictedgroups-autodemotion' )->parse();
326 }
327 $out->addHTML( implode( '', $conditionsParts ) );
328
329 $out->addHTML(
330 Html::closeElement( 'td' ) .
331 Html::closeElement( 'tr' )
332 );
333 }
334 $out->addHTML( Html::closeElement( 'table' ) );
335 }
336
348 private function formatPermissions( $permissions, $revoke, $add, $remove, $addSelf, $removeSelf ) {
349 $r = [];
350 foreach ( $permissions as $permission ) {
351 // show as granted only if it isn't revoked to prevent duplicate display of permissions
352 if ( !isset( $revoke[$permission] ) || !$revoke[$permission] ) {
353 $r[] = $this->msg( 'listgrouprights-right-display' )
354 ->params( User::getRightDescription( $permission ) )
355 ->rawParams( Html::element(
356 'span',
357 [ 'class' => 'mw-listgrouprights-right-name' ],
358 $permission
359 ) )->parse();
360 }
361 }
362 foreach ( $revoke as $permission ) {
363 $r[] = $this->msg( 'listgrouprights-right-revoked' )
364 ->params( User::getRightDescription( $permission ) )
365 ->rawParams( Html::element(
366 'span',
367 [ 'class' => 'mw-listgrouprights-right-name' ],
368 $permission
369 ) )->parse();
370 }
371
372 sort( $r );
373
374 $allGroups = $this->userGroupManager->listAllGroups();
375
376 $changeGroups = [
377 'addgroup' => $add,
378 'removegroup' => $remove,
379 'addgroup-self' => $addSelf,
380 'removegroup-self' => $removeSelf
381 ];
382
383 foreach ( $changeGroups as $messageKey => $changeGroup ) {
384 // @phan-suppress-next-line PhanTypeComparisonFromArray
385 if ( $changeGroup === true ) {
386 // For grep: listgrouprights-addgroup-all, listgrouprights-removegroup-all,
387 // listgrouprights-addgroup-self-all, listgrouprights-removegroup-self-all
388 $r[] = $this->msg( 'listgrouprights-' . $messageKey . '-all' )->escaped();
389 } elseif ( is_array( $changeGroup ) ) {
390 $changeGroup = array_intersect( array_values( array_unique( $changeGroup ) ), $allGroups );
391 if ( count( $changeGroup ) ) {
392 $groupLinks = [];
393 foreach ( $changeGroup as $group ) {
394 $groupLinks[] = UserGroupMembership::getLinkWiki( $group, $this->getContext() );
395 }
396 // For grep: listgrouprights-addgroup, listgrouprights-removegroup,
397 // listgrouprights-addgroup-self, listgrouprights-removegroup-self
398 $r[] = $this->msg( 'listgrouprights-' . $messageKey,
399 Message::listParam( $groupLinks ), count( $changeGroup ) )->parse();
400 }
401 }
402 }
403
404 if ( !$r ) {
405 return '';
406 } else {
407 return '<ul><li>' . implode( "</li>\n<li>", $r ) . '</li></ul>';
408 }
409 }
410
412 protected function getGroupName() {
413 return 'users';
414 }
415}
416
418class_alias( SpecialListGroupRights::class, 'SpecialListGroupRights' );
const NS_MAIN
Definition Defines.php:51
const NS_SPECIAL
Definition Defines.php:40
const NS_PROJECT
Definition Defines.php:55
This class is a collection of static functions that serve two purposes:
Definition Html.php:44
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 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 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()
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:144
static listParam(array $list, $type=ListType::AND)
Definition Message.php:1355
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:34
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)