Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
1.67% covered (danger)
1.67%
7 / 418
8.33% covered (danger)
8.33%
2 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialGlobalGroupPermissions
1.67% covered (danger)
1.67%
7 / 418
8.33% covered (danger)
8.33%
2 / 24
7618.68
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 doesWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 userCanEdit
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 execute
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
56
 buildMainView
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
12
 getGlobalGroupsTable
0.00% covered (danger)
0.00%
0 / 58
0.00% covered (danger)
0.00%
0 / 1
42
 getGroupInfo
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 buildGroupView
0.00% covered (danger)
0.00%
0 / 66
0.00% covered (danger)
0.00%
0 / 1
132
 buildWikiSetSelector
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 buildCheckboxes
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
30
 formatRight
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 getAssignedRights
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 doSubmit
0.00% covered (danger)
0.00%
0 / 83
0.00% covered (danger)
0.00%
0 / 1
600
 revokeRightsFromGroup
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 grantRightsToGroup
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 showLogFragment
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 addPermissionLog
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 addRenameLog
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 addWikiSetLog
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 setRestrictions
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 getWikiSetName
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 invalidateRightsCache
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 validateGroupName
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2# This file is part of MediaWiki.
3
4# MediaWiki is free software: you can redistribute it and/or modify
5# it under the terms of version 2 of the GNU General Public License
6# as published by the Free Software Foundation.
7
8# MediaWiki is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12
13namespace MediaWiki\Extension\CentralAuth\Special;
14
15use Exception;
16use InvalidArgumentException;
17use LogEventsList;
18use LogPage;
19use ManualLogEntry;
20use MediaWiki\Extension\CentralAuth\CentralAuthDatabaseManager;
21use MediaWiki\Extension\CentralAuth\GlobalGroup\GlobalGroupLookup;
22use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
23use MediaWiki\Extension\CentralAuth\WikiSet;
24use MediaWiki\Html\Html;
25use MediaWiki\MainConfigNames;
26use MediaWiki\Output\OutputPage;
27use MediaWiki\Permissions\PermissionManager;
28use MediaWiki\SpecialPage\SpecialPage;
29use MediaWiki\Status\Status;
30use MediaWiki\Title\Title;
31use MediaWiki\User\User;
32use MediaWiki\User\UserGroupMembership;
33use MediaWiki\Xml\Xml;
34use MediaWiki\Xml\XmlSelect;
35
36/**
37 * Special page to allow managing global groups
38 * Prototype for a similar system in core.
39 *
40 * @file
41 * @ingroup Extensions
42 */
43
44class SpecialGlobalGroupPermissions extends SpecialPage {
45
46    private PermissionManager $permissionManager;
47    private CentralAuthDatabaseManager $databaseManager;
48    private GlobalGroupLookup $globalGroupLookup;
49
50    public function __construct(
51        PermissionManager $permissionManager,
52        CentralAuthDatabaseManager $databaseManager,
53        GlobalGroupLookup $globalGroupLookup
54    ) {
55        parent::__construct( 'GlobalGroupPermissions' );
56        $this->permissionManager = $permissionManager;
57        $this->databaseManager = $databaseManager;
58        $this->globalGroupLookup = $globalGroupLookup;
59    }
60
61    public function doesWrites() {
62        return true;
63    }
64
65    /**
66     * @param User $user
67     * @return bool
68     */
69    public function userCanEdit( $user ) {
70        $globalUser = CentralAuthUser::getInstance( $user );
71
72        # Should be a global user
73        if ( !$globalUser->exists() || !$globalUser->isAttached() ) {
74            return false;
75        }
76
77        return $user->isAllowed( 'globalgrouppermissions' );
78    }
79
80    /** @inheritDoc */
81    public function execute( $subpage ) {
82        $this->addHelpLink( 'Extension:CentralAuth' );
83        if ( !$this->userCanExecute( $this->getUser() ) ) {
84            $this->displayRestrictionError();
85        }
86
87        $this->getOutput()->setPageTitleMsg( $this->msg( 'globalgrouppermissions' ) );
88
89        $this->getOutput()->addModuleStyles( 'ext.centralauth.misc.styles' );
90        $this->getOutput()->setRobotPolicy( "noindex,nofollow" );
91        $this->getOutput()->setArticleRelated( false );
92        $this->getOutput()->disableClientCache();
93
94        if ( $subpage == '' ) {
95            $subpage = $this->getRequest()->getVal( 'wpGroup' );
96        }
97
98        if (
99            $subpage != ''
100            && $this->getUser()->matchEditToken( $this->getRequest()->getVal( 'wpEditToken' ) )
101            && $this->getRequest()->wasPosted()
102        ) {
103            $this->doSubmit( $subpage );
104        } elseif ( $subpage != '' ) {
105            $this->buildGroupView( $subpage );
106        } else {
107            $this->buildMainView();
108        }
109    }
110
111    private function buildMainView() {
112        $out = $this->getOutput();
113        $groups = $this->globalGroupLookup->getDefinedGroups();
114
115        if ( count( $groups ) ) {
116            $out->addHTML(
117                $this->msg( 'centralauth-globalgroupperms-groups-intro' )->parseAsBlock()
118                    . $this->getGlobalGroupsTable( $groups )
119            );
120        } else {
121            $out->addWikiMsg( 'centralauth-globalgroupperms-nogroups' );
122        }
123
124        if ( $this->userCanEdit( $this->getUser() ) ) {
125            // "Create a group" prompt
126            // @todo Move this out of main view to a separate page
127            $html = Xml::fieldset( $this->msg( 'centralauth-newgroup-legend' )->text() );
128            $html .= $this->msg( 'centralauth-newgroup-intro' )->parseAsBlock();
129            $html .= Xml::openElement( 'form', [
130                'method' => 'post',
131                'action' => $this->getConfig()->get( MainConfigNames::Script ),
132                'name' => 'centralauth-globalgroups-newgroup'
133            ] );
134            $html .= Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() );
135
136            $fields = [ 'centralauth-globalgroupperms-newgroupname' => Xml::input( 'wpGroup' ) ];
137
138            $html .= Xml::buildForm( $fields, 'centralauth-globalgroupperms-creategroup-submit' );
139            $html .= Xml::closeElement( 'form' );
140            $html .= Xml::closeElement( 'fieldset' );
141
142            $out->addHTML( $html );
143        }
144    }
145
146    /**
147     * @param array $groups
148     * @return string HTML for the group permissions table
149     */
150    protected function getGlobalGroupsTable( $groups ) {
151        $table = Html::openElement( 'table',
152            [ 'class' => 'mw-centralauth-groups-table wikitable' ] );
153
154        // Header stuff
155        $table .= Html::openElement( 'tr' );
156        $table .= Html::element( 'th', [],
157            $this->msg( 'centralauth-globalgroupperms-group' )->text()
158        );
159        $table .= Html::element( 'th', [],
160            $this->msg( 'centralauth-globalgroupperms-rights' )->text()
161        );
162        $table .= Html::closeElement( 'tr' );
163
164        foreach ( $groups as $groupName ) {
165            $groupInfo = $this->getGroupInfo( $groupName );
166            $wikiset = $groupInfo['wikiset'];
167
168            $table .= Html::openElement( 'tr' );
169
170            // Column with group name, links and local disabled status
171            $table .= Html::openElement( 'td' );
172            $table .= $this->getOutput()->parseInlineAsInterface(
173                UserGroupMembership::getLinkWiki( $groupName, $this->getContext() ) ) . '<br />';
174
175            $groupWithParentheses = $this->msg( 'parentheses' )->params( $groupName )->escaped();
176            $table .= Html::openElement( 'code' );
177            $table .= $groupWithParentheses;
178            $table .= Html::closeElement( 'code' ) . Html::element( 'br' );
179
180            $linkRenderer = $this->getLinkRenderer();
181            $links = [
182                $linkRenderer->makeKnownLink(
183                    $this->getPageTitle( $groupName ),
184                    $this->msg( 'centralauth-globalgroupperms-management' )->text()
185                ),
186                $linkRenderer->makeKnownLink(
187                    SpecialPage::getTitleFor( 'GlobalUsers', $groupName ),
188                    $this->msg( 'centralauth-globalgroupperms-group-listmembers' )->text()
189                ),
190            ];
191            $table .= $this->msg( 'parentheses' )
192                ->rawParams( $this->getLanguage()->pipeList( $links ) )->escaped();
193
194            if ( $wikiset !== null && !$wikiset['enabledHere'] ) {
195                $table .= '<br /><small>';
196                $table .= $this->msg( 'centralauth-globalgroupperms-group-disabled' )->escaped() .
197                    '</small>';
198            }
199            $table .= Html::closeElement( 'td' );
200
201            // Column for wikiset info and group rights list
202            $table .= Html::openElement( 'td' );
203            if ( $wikiset === null ) {
204                $table .= $this->msg( 'centralauth-globalgroupperms-wikiset-none' )->escaped();
205            } else {
206                $table .= $this->msg( 'centralauth-globalgroupperms-group-wikiset' )
207                    ->rawParams(
208                        $linkRenderer->makeKnownLink(
209                            SpecialPage::getTitleFor( 'WikiSets', $wikiset['id'] ),
210                            $wikiset['name']
211                        )
212                    )->escaped();
213            }
214
215            $table .= '<hr />';
216
217            $rightsList = '';
218            foreach ( $groupInfo['rights'] as $right ) {
219                $rightsList .= Html::rawElement( 'li', [], $this->formatRight( $right ) );
220            }
221            $table .= '<ul>' . $rightsList . '</ul>';
222            $table .= Html::closeElement( 'td' );
223
224            $table .= Html::closeElement( 'tr' );
225        }
226
227        $table .= Html::closeElement( 'table' );
228
229        return $table;
230    }
231
232    /**
233     * @param string $group The group's name
234     * @return array
235     *      - rights: string The list of rights assigned to the group
236     *   - wikiset: array|null Either array with id, name, enabledHere or
237     *      null if the group is not associated to any wikiset
238     * @throws Exception
239     */
240    protected function getGroupInfo( $group ) {
241        $info = [ 'rights' => $this->getAssignedRights( $group ) ];
242
243        $wikiset = WikiSet::getWikiSetForGroup( $group );
244        if ( $wikiset !== 0 ) {
245            $wikiset = WikiSet::newFromID( $wikiset );
246            if ( !$wikiset ) {
247                throw new InvalidArgumentException( "__METHOD__: $group with unknown wikiset." );
248            }
249            $info['wikiset'] = [
250                'id' => $wikiset->getId(),
251                'name' => $wikiset->getName(),
252                'enabledHere' => $wikiset->inSet(),
253            ];
254        } else {
255            $info['wikiset'] = null;
256        }
257
258        return $info;
259    }
260
261    /**
262     * @param string $group
263     */
264    private function buildGroupView( $group ) {
265        $editable = $this->userCanEdit( $this->getUser() );
266        $assignedRights = $this->getAssignedRights( $group );
267        $this->getOutput()->addBacklinkSubtitle( $this->getPageTitle() );
268
269        if ( !$assignedRights ) {
270            // if the group doesn't exist and the user can not manage the global groups,
271            // an error message should be shown instead of the permission list box.
272            if ( !$editable ) {
273                $this->getOutput()->wrapWikiMsg( '<div class="error">$1</div>',
274                    [ 'centralauth-editgroup-nonexistent', $group ] );
275                $this->showLogFragment( $group, $this->getOutput() );
276                return;
277            }
278
279            $nameValidationResult = $this->validateGroupName( $group );
280            if ( !$nameValidationResult->isGood() ) {
281                $this->getOutput()->wrapWikiMsg( '<div class="error">$1</div>',
282                    $nameValidationResult->getMessage() );
283                $this->showLogFragment( $group, $this->getOutput() );
284                return;
285            }
286        }
287
288        $fieldsetClass = $editable
289            ? 'mw-centralauth-editgroup'
290            : 'mw-centralauth-editgroup-readonly';
291        $html = Xml::fieldset(
292            $this->msg( 'centralauth-editgroup-fieldset', $group )->text(),
293            false,
294            [ 'class' => $fieldsetClass ]
295        );
296
297        if ( $editable ) {
298            $html .= Xml::openElement( 'form', [
299                'method' => 'post',
300                'action' => $this->getPageTitle( $group )->getLocalUrl(),
301                'name' => 'centralauth-globalgroups-newgroup'
302            ] );
303            $html .= Html::hidden( 'wpGroup', $group );
304            $html .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() );
305        }
306
307        $fields = [];
308
309        if ( $editable ) {
310            $fields['centralauth-editgroup-name'] = Xml::input( 'wpGlobalGroupName', 50, $group );
311        } else {
312            $fields['centralauth-editgroup-name'] = htmlspecialchars( $group );
313        }
314
315        $lang = $this->getLanguage();
316        if ( $this->getContext()->getAuthority()->isAllowed( 'editinterface' ) ) {
317            # Show edit link only to user with the editinterface right
318            $fields['centralauth-editgroup-display'] = $this->msg(
319                'centralauth-editgroup-display-edit',
320                $group,
321                $lang->getGroupName( $group )
322            )->parse();
323            $fields['centralauth-editgroup-member'] = $this->msg(
324                'centralauth-editgroup-member-edit',
325                $group,
326                $lang->getGroupMemberName( $group, '#' )
327            )->parse();
328        } else {
329            $fields['centralauth-editgroup-display'] =
330                htmlspecialchars( $lang->getGroupName( $group ) );
331            $fields['centralauth-editgroup-member'] =
332                htmlspecialchars( $lang->getGroupMemberName( $group, '#' ) );
333        }
334        $fields['centralauth-editgroup-members'] = $this->msg(
335            'centralauth-editgroup-members-link',
336            $group,
337            $lang->getGroupMemberName( $group, '#' )
338        )->parse();
339        $fields['centralauth-editgroup-restrictions'] = $this->buildWikiSetSelector( $group );
340        $fields['centralauth-editgroup-perms'] = $this->buildCheckboxes( $group );
341
342        if ( $editable ) {
343            $fields['centralauth-editgroup-reason'] = Xml::input( 'wpReason', 60 );
344        }
345
346        $html .= Xml::buildForm( $fields, $editable ? 'centralauth-editgroup-submit' : null );
347
348        if ( $editable ) {
349            $html .= Xml::closeElement( 'form' );
350        }
351
352        $html .= Xml::closeElement( 'fieldset' );
353
354        $this->getOutput()->addHTML( $html );
355
356        $this->showLogFragment( $group, $this->getOutput() );
357    }
358
359    /**
360     * @param string $group
361     * @return string
362     */
363    private function buildWikiSetSelector( $group ) {
364        $sets = WikiSet::getAllWikiSets();
365        $default = WikiSet::getWikiSetForGroup( $group );
366
367        if ( !$this->userCanEdit( $this->getUser() ) ) {
368            $set = WikiSet::newFromID( $default );
369            if ( $set ) {
370                return $this->getLinkRenderer()->makeLink(
371                    SpecialPage::getTitleFor( 'WikiSets', (string)$set->getId() ),
372                    $set->getName()
373                );
374            } else {
375                return $this->msg( 'centralauth-editgroup-nowikiset' )->parse();
376            }
377        }
378
379        $select = new XmlSelect( 'set', 'wikiset', (string)$default );
380        $select->addOption( $this->msg( 'centralauth-editgroup-noset' )->text(), '0' );
381        foreach ( $sets as $set ) {
382            $select->addOption( $set->getName(), (string)$set->getID() );
383        }
384
385        $editlink = $this->msg( 'centralauth-editgroup-editsets' )->parse();
386        return $select->getHTML() . "&#160;{$editlink}";
387    }
388
389    /**
390     * @param string $group
391     * @return string
392     */
393    private function buildCheckboxes( $group ) {
394        $editable = $this->userCanEdit( $this->getUser() );
395
396        $assignedRights = $this->getAssignedRights( $group );
397
398        $checkboxes = [];
399        $attribs = [];
400
401        if ( !$editable ) {
402            $attribs['disabled'] = 'disabled';
403        }
404
405        $rights = array_unique(
406            array_merge(
407                $this->permissionManager->getAllPermissions(),
408                $assignedRights
409            )
410        );
411        sort( $rights );
412
413        foreach ( $rights as $right ) {
414            // Build a checkbox
415            $checked = in_array( $right, $assignedRights );
416
417            $desc = $this->formatRight( $right );
418
419            $checkbox = Xml::check( "wpRightAssigned-$right", $checked,
420                array_merge( $attribs, [ 'id' => "wpRightAssigned-$right" ] ) );
421            $label = Xml::tags( 'label', [ 'for' => "wpRightAssigned-$right" ],
422                    $desc );
423
424            $liClass = $checked
425                ? 'mw-centralauth-editgroup-checked'
426                : 'mw-centralauth-editgroup-unchecked';
427            $checkboxes[] = Html::rawElement(
428                'li', [ 'class' => $liClass ], "$checkbox&#160;$label" );
429        }
430
431        $count = count( $checkboxes );
432
433        $html = Html::openElement( 'div', [ 'class' => 'mw-centralauth-rights' ] )
434            . '<ul>';
435
436        foreach ( $checkboxes as $cb ) {
437            $html .= $cb;
438        }
439
440        $html .= '</ul>'
441            . Html::closeElement( 'div' );
442
443        return $html;
444    }
445
446    /**
447     * Given a user right name, return HTML with the description
448     * of the right and it's name for displaying to the user
449     * @param string $right
450     * @return string escaped html
451     */
452    protected function formatRight( $right ) {
453        return $this->msg( 'listgrouprights-right-display' )
454            ->params( User::getRightDescription( $right ) )
455            ->rawParams( Html::element(
456                'span',
457                [ 'class' => 'mw-listgrouprights-right-name' ],
458                $right
459            ) )
460            ->parse();
461    }
462
463    /**
464     * @param string $group
465     * @return string[]
466     */
467    private function getAssignedRights( $group ) {
468        return $this->globalGroupLookup->getRightsForGroup( $group );
469    }
470
471    /**
472     * @param string $group
473     */
474    private function doSubmit( $group ) {
475        // It is important to check userCanEdit, as otherwise an
476        // unauthorized user could manually construct a POST request.
477        if ( !$this->userCanEdit( $this->getUser() ) ) {
478            return;
479        }
480        $reason = $this->getRequest()->getVal( 'wpReason', '' );
481
482        // Current name of the group
483        // XXX This is a horrible hack. We should not use Title for normalization. We need to prefix
484        // the group name so that the first letter doesn't get uppercased.
485        $group = Title::newFromText( "A/$group" );
486        if ( !$group ) {
487            $this->getOutput()->addWikiMsg( 'centralauth-editgroup-invalid-name' );
488            return;
489        }
490        $group = ltrim( substr( $group->getDBkey(), 2 ), '_' );
491
492        // (Potentially) New name of the group
493        $newname = $this->getRequest()->getVal( 'wpGlobalGroupName', $group );
494
495        $newname = Title::newFromText( "A/$newname" );
496        if ( !$newname ) {
497            $this->getOutput()->addWikiMsg( 'centralauth-editgroup-invalid-name' );
498            return;
499        }
500        $newname = ltrim( substr( $newname->getDBkey(), 2 ), '_' );
501
502        // all new group names should be lowercase: check all new and changed group names (T202095)
503        if (
504            !in_array( $group, $this->globalGroupLookup->getDefinedGroups( DB_PRIMARY ) )
505            || ( $group !== $newname )
506        ) {
507            $nameValidationResult = $this->validateGroupName( $newname );
508            if ( !$nameValidationResult->isGood() ) {
509                $this->getOutput()->wrapWikiMsg( '<div class="error">$1</div>',
510                    $nameValidationResult->getMessage() );
511                return;
512            }
513        }
514
515        // Calculate permission changes already! We'll only save any changes
516        // here after processing a possible group rename, but want to add
517        // validation logic before that.
518        $addRights = [];
519        $removeRights = [];
520        $oldRights = $this->getAssignedRights( $group );
521        $allRights = array_unique(
522            array_merge(
523                $this->permissionManager->getAllPermissions(),
524                $oldRights
525            )
526        );
527
528        foreach ( $allRights as $right ) {
529            $alreadyAssigned = in_array( $right, $oldRights );
530            $checked = $this->getRequest()->getCheck( "wpRightAssigned-$right" );
531
532            if ( !$alreadyAssigned && $checked ) {
533                $addRights[] = $right;
534            } elseif ( $alreadyAssigned && !$checked ) {
535                $removeRights[] = $right;
536            }
537        }
538
539        // Disallow deleting existing groups with members in them
540        if (
541            count( $oldRights ) !== 0
542            && count( $addRights ) === 0
543            && count( $removeRights ) === count( $oldRights )
544        ) {
545            $dbr = $this->databaseManager->getCentralReplicaDB();
546            $memberCount = $dbr->newSelectQueryBuilder()
547                ->select( 'gug_group' )
548                ->from( 'global_user_groups' )
549                ->where( [ 'gug_group' => $group ] )
550                ->caller( __METHOD__ )
551                ->fetchRow();
552
553            if ( $memberCount ) {
554                $this->getOutput()->addWikiMsg( 'centralauth-editgroup-delete-removemembers' );
555                return;
556            }
557        }
558
559        // Check if we need to rename the group
560        if ( $group != $newname ) {
561            if ( in_array( $newname, $this->globalGroupLookup->getDefinedGroups( DB_PRIMARY ) ) ) {
562                $this->getOutput()->addWikiMsg( 'centralauth-editgroup-rename-taken', $newname );
563                return;
564            }
565
566            $dbw = $this->databaseManager->getCentralPrimaryDB();
567            $updates = [
568                'global_group_permissions' => 'ggp_group',
569                'global_group_restrictions' => 'ggr_group',
570                'global_user_groups' => 'gug_group'
571            ];
572
573            foreach ( $updates as $table => $field ) {
574                $dbw->newUpdateQueryBuilder()
575                    ->update( $table )
576                    ->set( [ $field => $newname ] )
577                    ->where( [ $field => $group ] )
578                    ->caller( __METHOD__ )
579                    ->execute();
580            }
581            $this->addRenameLog( $group, $newname, $reason );
582
583            // The rest of the changes here will be performed on the "new" group
584            $group = $newname;
585        }
586
587        // Assign the rights.
588        if ( count( $addRights ) > 0 ) {
589            $this->grantRightsToGroup( $group, $addRights );
590        }
591        if ( count( $removeRights ) > 0 ) {
592            $this->revokeRightsFromGroup( $group, $removeRights );
593        }
594
595        // Log it
596        if ( !( count( $addRights ) == 0 && count( $removeRights ) == 0 ) ) {
597            $this->addPermissionLog( $group, $addRights, $removeRights, $reason );
598        }
599
600        // Change set
601        $current = WikiSet::getWikiSetForGroup( $group );
602        $new = $this->getRequest()->getInt( 'set' );
603        if ( $current != $new ) {
604            $this->setRestrictions( $group, $new );
605            $this->addWikiSetLog( $group, $current, $new, $reason );
606        }
607
608        $this->invalidateRightsCache( $group );
609
610        // Display success
611        $this->getOutput()->setSubTitle( $this->msg( 'centralauth-editgroup-success' ) );
612        $this->getOutput()->addWikiMsg( 'centralauth-editgroup-success-text', $group );
613    }
614
615    /**
616     * @param string $group
617     * @param string[] $rights
618     */
619    private function revokeRightsFromGroup( $group, $rights ) {
620        $dbw = $this->databaseManager->getCentralPrimaryDB();
621
622        # Delete from the DB
623        $dbw->newDeleteQueryBuilder()
624            ->deleteFrom( 'global_group_permissions' )
625            ->where( [ 'ggp_group' => $group, 'ggp_permission' => $rights ] )
626            ->caller( __METHOD__ )
627            ->execute();
628    }
629
630    /**
631     * @param string $group
632     * @param string[]|string $rights
633     */
634    private function grantRightsToGroup( $group, $rights ) {
635        $dbw = $this->databaseManager->getCentralPrimaryDB();
636
637        if ( !is_array( $rights ) ) {
638            $rights = [ $rights ];
639        }
640
641        $insertRows = [];
642        foreach ( $rights as $right ) {
643            $insertRows[] = [ 'ggp_group' => $group, 'ggp_permission' => $right ];
644        }
645
646        # Replace into the DB
647        $dbw->newReplaceQueryBuilder()
648            ->replaceInto( 'global_group_permissions' )
649            ->uniqueIndexFields( [ 'ggp_group', 'ggp_permission' ] )
650            ->rows( $insertRows )
651            ->caller( __METHOD__ )
652            ->execute();
653    }
654
655    /**
656     * @param string $group
657     * @param OutputPage $output
658     */
659    protected function showLogFragment( $group, $output ) {
660        $title = SpecialPage::getTitleFor( 'GlobalUsers', $group );
661        $logPage = new LogPage( 'gblrights' );
662        $output->addHTML( Xml::element( 'h2', null, $logPage->getName()->text() . "\n" ) );
663        LogEventsList::showLogExtract( $output, 'gblrights', $title->getPrefixedText() );
664    }
665
666    /**
667     * Log permission changes
668     *
669     * @param string $group
670     * @param string[] $addRights
671     * @param string[] $removeRights
672     * @param string $reason
673     */
674    private function addPermissionLog( $group, $addRights, $removeRights, $reason ) {
675        $entry = new ManualLogEntry( 'gblrights', 'groupprms2' );
676        $entry->setTarget( SpecialPage::getTitleFor( 'GlobalUsers', $group ) );
677        $entry->setPerformer( $this->getUser() );
678        $entry->setComment( $reason );
679        $entry->setParameters( [
680            'addRights' => $addRights,
681            'removeRights' => $removeRights,
682        ] );
683        $logid = $entry->insert();
684        $entry->publish( $logid );
685    }
686
687    /**
688     * Log the renaming of a global group
689     *
690     * @param string $oldName
691     * @param string $newName
692     * @param string $reason
693     */
694    private function addRenameLog( $oldName, $newName, $reason ) {
695        $entry = new ManualLogEntry( 'gblrights', 'grouprename' );
696        // This has to point to 'Special:GlobalUsers so that self::showLogFragment can find it
697        $entry->setTarget( SpecialPage::getTitleFor( 'GlobalUsers', $newName ) );
698        $entry->setPerformer( $this->getUser() );
699        $entry->setComment( $reason );
700        $entry->setParameters( [
701            'newName' => $newName,
702            'oldName' => $oldName,
703        ] );
704        $logid = $entry->insert();
705        $entry->publish( $logid );
706    }
707
708    /**
709     * Log wikiset changes
710     *
711     * @param string $group
712     * @param int $old
713     * @param int $new
714     * @param string $reason
715     */
716    private function addWikiSetLog( $group, $old, $new, $reason ) {
717        $entry = new ManualLogEntry( 'gblrights', 'groupprms3' );
718        $entry->setTarget( SpecialPage::getTitleFor( 'GlobalUsers', $group ) );
719        $entry->setPerformer( $this->getUser() );
720        $entry->setComment( $reason );
721        $params = [];
722        $mapping = [
723            'old' => [ 4, $old ],
724            'new' => [ 5, $new ],
725        ];
726        foreach ( $mapping as $param => [ $id, $set ] ) {
727            $name = $this->getWikiSetName( $set );
728            if ( $name !== null ) {
729                $params["$id::$param"] = $name;
730            } else {
731                $params["$id:msg:$param"] = 'centralauth-editgroup-noset';
732            }
733        }
734        $entry->setParameters( $params );
735        $logid = $entry->insert();
736        $entry->publish( $logid );
737    }
738
739    /**
740     * @param string $group
741     * @param int $set
742     * @return bool
743     */
744    private function setRestrictions( $group, $set ) {
745        $dbw = $this->databaseManager->getCentralPrimaryDB();
746        if ( $set == 0 ) {
747            $dbw->newDeleteQueryBuilder()
748                ->deleteFrom( 'global_group_restrictions' )
749                ->where( [ 'ggr_group' => $group ] )
750                ->caller( __METHOD__ )
751                ->execute();
752        } else {
753            $dbw->newReplaceQueryBuilder()
754                ->replaceInto( 'global_group_restrictions' )
755                ->uniqueIndexFields( 'ggr_group' )
756                ->row( [ 'ggr_group' => $group, 'ggr_set' => $set, ] )
757                ->caller( __METHOD__ )
758                ->execute();
759        }
760        return (bool)$dbw->affectedRows();
761    }
762
763    /**
764     * @param string|int $id
765     * @return string|null
766     */
767    private function getWikiSetName( $id ) {
768        $wikiset = WikiSet::newFromID( $id );
769        if ( $wikiset !== null ) {
770            return $wikiset->getName();
771        }
772        return null;
773    }
774
775    /**
776     * @param string $group
777     */
778    private function invalidateRightsCache( $group ) {
779        // Figure out all the users in this group.
780        // Use the primary database over here as this could go horribly wrong with newly created or just
781        // renamed groups
782        $dbr = $this->databaseManager->getCentralPrimaryDB();
783
784        $res = $dbr->newSelectQueryBuilder()
785            ->select( 'gu_name' )
786            ->from( 'globaluser' )
787            ->join( 'global_user_groups', null, 'gu_id=gug_user' )
788            ->where( [ 'gug_group' => $group ] )
789            ->caller( __METHOD__ )
790            ->fetchFieldValues();
791
792        // Invalidate their rights cache.
793        foreach ( $res as $name ) {
794            // Use READ_LATEST for paranoia, though the DB isn't used in this method
795            $cu = CentralAuthUser::getPrimaryInstanceByName( $name );
796            $cu->quickInvalidateCache();
797        }
798    }
799
800    /** @inheritDoc */
801    protected function getGroupName() {
802        return 'users';
803    }
804
805    private function validateGroupName( string $name ): Status {
806        // all new group names should be lowercase (T202095)
807        if ( $name !== strtolower( $name ) ) {
808            return Status::newFatal( 'centralauth-editgroup-invalid-name-lowercase' );
809        }
810
811        return Status::newGood();
812    }
813}