Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 264
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialListGroupRights
0.00% covered (danger)
0.00%
0 / 264
0.00% covered (danger)
0.00%
0 / 6
1560
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 80
0.00% covered (danger)
0.00%
0 / 1
132
 outputNamespaceProtectionInfo
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 1
56
 outputRestrictedGroupsConfig
0.00% covered (danger)
0.00%
0 / 79
0.00% covered (danger)
0.00%
0 / 1
72
 formatPermissions
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
132
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Specials;
8
9use MediaWiki\Html\Html;
10use MediaWiki\Language\ILanguageConverter;
11use MediaWiki\Language\LanguageConverterFactory;
12use MediaWiki\MainConfigNames;
13use MediaWiki\Message\Message;
14use MediaWiki\Parser\Sanitizer;
15use MediaWiki\Permissions\GroupPermissionsLookup;
16use MediaWiki\SpecialPage\SpecialPage;
17use MediaWiki\Specials\Helpers\UserRequirementsConditionFormatterTrait;
18use MediaWiki\Title\NamespaceInfo;
19use MediaWiki\Title\Title;
20use MediaWiki\Title\TitleValue;
21use MediaWiki\User\RestrictedUserGroupConfigReader;
22use MediaWiki\User\User;
23use MediaWiki\User\UserGroupManager;
24use MediaWiki\User\UserGroupMembership;
25
26/**
27 * List all defined user groups and the associated rights.
28 *
29 * See also @ref $wgGroupPermissions.
30 *
31 * @ingroup SpecialPage
32 * @author Petr Kadlec <mormegil@centrum.cz>
33 */
34class SpecialListGroupRights extends SpecialPage {
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
54    /**
55     * Show the special page
56     * @param string|null $par
57     */
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' )->plaintextParams( $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' ] ) .
178            Html::element(
179                'th',
180                [],
181                $this->msg( 'listgrouprights-namespaceprotection-namespace' )->text()
182            ) .
183            Html::element(
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', [],
268                Html::element(
269                    'th',
270                    [],
271                    $this->msg( 'listgrouprights-group' )->text()
272                ) .
273                Html::element(
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
337    /**
338     * Create a user-readable list of permissions from the given array.
339     *
340     * @param string[] $permissions Array of granted permissions
341     * @param string[] $revoke Array of revoked permissions
342     * @param array $add Array of groups this group is allowed to add or true
343     * @param array $remove Array of groups this group is allowed to remove or true
344     * @param array $addSelf Array of groups this group is allowed to add to self or true
345     * @param array $removeSelf Array of group this group is allowed to remove from self or true
346     * @return string HTML list of all granted permissions
347     */
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
411    /** @inheritDoc */
412    protected function getGroupName() {
413        return 'users';
414    }
415}
416
417// @codeCoverageIgnoreStart
418/** @deprecated class alias since 1.41 */
419class_alias( SpecialListGroupRights::class, 'SpecialListGroupRights' );
420// @codeCoverageIgnoreEnd