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