Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.14% covered (success)
97.14%
102 / 105
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
CreateAndPromote
97.14% covered (success)
97.14%
102 / 105
66.67% covered (warning)
66.67%
2 / 3
30
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
2
 execute
95.89% covered (success)
95.89%
70 / 73
0.00% covered (danger)
0.00%
0 / 1
27
 addLogEntry
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Creates an account and grants it rights.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Maintenance
22 * @author Rob Church <robchur@gmail.com>
23 * @author Pablo Castellano <pablo@anche.no>
24 */
25
26// @codeCoverageIgnoreStart
27require_once __DIR__ . '/Maintenance.php';
28// @codeCoverageIgnoreEnd
29
30use MediaWiki\Auth\AuthManager;
31use MediaWiki\Deferred\SiteStatsUpdate;
32use MediaWiki\Maintenance\Maintenance;
33use MediaWiki\Password\PasswordError;
34use MediaWiki\User\User;
35use MediaWiki\WikiMap\WikiMap;
36
37/**
38 * Maintenance script to create an account and grant it rights.
39 *
40 * @ingroup Maintenance
41 */
42class CreateAndPromote extends Maintenance {
43    private const PERMIT_ROLES = [ 'sysop', 'bureaucrat', 'interface-admin', 'bot' ];
44
45    public function __construct() {
46        parent::__construct();
47        $this->addDescription( 'Create a new user account and/or grant it additional rights' );
48        $this->addOption(
49            'force',
50            'If account exists already, just grant it rights or change password.'
51        );
52        foreach ( self::PERMIT_ROLES as $role ) {
53            $this->addOption( $role, "Add the account to the {$role} group" );
54        }
55
56        $this->addOption(
57            'custom-groups',
58            'Comma-separated list of groups to add the user to',
59            false,
60            true
61        );
62
63        $this->addOption(
64            'reason',
65            'Reason for account creation and user rights assignment to log to wiki',
66            false,
67            true
68        );
69
70        $this->addArg( 'username', 'Username of new user' );
71        $this->addArg( 'password', 'Password to set', false );
72    }
73
74    public function execute() {
75        $username = $this->getArg( 0 );
76        $password = $this->getArg( 1 );
77        $force = $this->hasOption( 'force' );
78        $inGroups = [];
79        $services = $this->getServiceContainer();
80
81        $user = $services->getUserFactory()->newFromName( $username );
82        if ( !is_object( $user ) ) {
83            $this->fatalError( 'invalid username.' );
84        }
85
86        $exists = ( $user->idForName() !== 0 );
87
88        if ( $exists && !$force ) {
89            $this->fatalError( 'Account exists. Perhaps you want the --force option?' );
90        } elseif ( !$exists && !$password ) {
91            $this->error( 'Argument <password> required!' );
92            $this->maybeHelp( true );
93        } elseif ( $exists ) {
94            $inGroups = $services->getUserGroupManager()->getUserGroups( $user );
95        }
96
97        $groups = array_filter( self::PERMIT_ROLES, [ $this, 'hasOption' ] );
98        if ( $this->hasOption( 'custom-groups' ) ) {
99            $allGroups = array_fill_keys( $services->getUserGroupManager()->listAllGroups(), true );
100            $customGroupsText = $this->getOption( 'custom-groups' );
101            if ( $customGroupsText !== '' ) {
102                $customGroups = explode( ',', $customGroupsText );
103                foreach ( $customGroups as $customGroup ) {
104                    if ( isset( $allGroups[$customGroup] ) ) {
105                        $groups[] = trim( $customGroup );
106                    } else {
107                        $this->output( "$customGroup is not a valid group, ignoring!\n" );
108                    }
109                }
110            }
111        }
112
113        $promotions = array_diff(
114            $groups,
115            $inGroups
116        );
117
118        if ( $exists && !$password && count( $promotions ) === 0 ) {
119            $this->output( "Account exists and nothing to do.\n" );
120
121            return;
122        } elseif ( count( $promotions ) !== 0 ) {
123            $dbDomain = WikiMap::getCurrentWikiDbDomain()->getId();
124            $promoText = "User:{$username} into " . implode( ', ', $promotions ) . "...\n";
125            if ( $exists ) {
126                $this->output( "$dbDomain: Promoting $promoText" );
127            } else {
128                $this->output( "$dbDomain: Creating and promoting $promoText" );
129            }
130        }
131
132        if ( !$exists ) {
133            // Verify the password meets the password requirements before creating.
134            // This check is repeated below to account for differences between
135            // the password policy for regular users and for users in certain groups.
136            if ( $password ) {
137                $status = $user->checkPasswordValidity( $password );
138
139                if ( !$status->isGood() ) {
140                    $this->fatalError( $status );
141                }
142            }
143
144            // Create the user via AuthManager as there may be various side
145            // effects that are performed by the configured AuthManager chain.
146            $status = $this->getServiceContainer()->getAuthManager()->autoCreateUser(
147                $user,
148                AuthManager::AUTOCREATE_SOURCE_MAINT,
149                false
150            );
151            if ( !$status->isGood() ) {
152                $this->fatalError( $status );
153            }
154        }
155
156        if ( $promotions ) {
157            // Add groups before changing password, as the password policy for certain groups has
158            // stricter requirements.
159            $userGroupManager = $services->getUserGroupManager();
160            $userGroupManager->addUserToMultipleGroups( $user, $promotions );
161            $reason = $this->getOption( 'reason' ) ?: '';
162            $this->addLogEntry( $user, $inGroups, array_merge( $inGroups, $promotions ), $reason );
163        }
164
165        if ( $password ) {
166            # Try to set the password
167            try {
168                $status = $user->changeAuthenticationData( [
169                    'username' => $user->getName(),
170                    'password' => $password,
171                    'retype' => $password,
172                ] );
173                if ( !$status->isGood() ) {
174                    throw new PasswordError( $status->getMessage( false, false, 'en' )->text() );
175                }
176                if ( $exists ) {
177                    $this->output( "Password set.\n" );
178                    $user->saveSettings();
179                }
180            } catch ( PasswordError $pwe ) {
181                $this->fatalError( 'Setting the password failed: ' . $pwe->getMessage() );
182            }
183        }
184
185        if ( !$exists ) {
186            # Increment site_stats.ss_users
187            $ssu = SiteStatsUpdate::factory( [ 'users' => 1 ] );
188            $ssu->doUpdate();
189        }
190
191        $this->output( "done.\n" );
192    }
193
194    /**
195     * Add a rights log entry for an action.
196     *
197     * @param User $user
198     * @param array $oldGroups
199     * @param array $newGroups
200     * @param string $reason
201     */
202    private function addLogEntry( $user, array $oldGroups, array $newGroups, string $reason ) {
203        $logEntry = new ManualLogEntry( 'rights', 'rights' );
204        $logEntry->setPerformer( User::newSystemUser( User::MAINTENANCE_SCRIPT_USER, [ 'steal' => true ] ) );
205        $logEntry->setTarget( $user->getUserPage() );
206        $logEntry->setComment( $reason );
207        $logEntry->setParameters( [
208            '4::oldgroups' => $oldGroups,
209            '5::newgroups' => $newGroups
210        ] );
211        $logid = $logEntry->insert();
212        $logEntry->publish( $logid );
213    }
214}
215
216// @codeCoverageIgnoreStart
217$maintClass = CreateAndPromote::class;
218require_once RUN_MAINTENANCE_IF_MAIN;
219// @codeCoverageIgnoreEnd