Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
78 / 78
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
CreateBotPassword
100.00% covered (success)
100.00%
78 / 78
100.00% covered (success)
100.00%
3 / 3
16
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
49 / 49
100.00% covered (success)
100.00%
1 / 1
13
 showGrants
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Creates a bot password for an existing user account.
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 * @ingroup Maintenance
8 * @author Alex Dean <wikimedia@mostlyalex.com>
9 */
10
11use MediaWiki\Maintenance\Maintenance;
12use MediaWiki\User\BotPassword;
13use MediaWiki\User\User;
14
15// @codeCoverageIgnoreStart
16require_once __DIR__ . '/Maintenance.php';
17// @codeCoverageIgnoreEnd
18
19class CreateBotPassword extends Maintenance {
20    /**
21     * Width of initial column of --showgrants output
22     */
23    private const SHOWGRANTS_COLUMN_WIDTH = 20;
24
25    public function __construct() {
26        parent::__construct();
27        $this->addDescription(
28            'Create a bot password for a user. ' .
29            'See https://www.mediawiki.org/wiki/Manual:Bot_passwords for more information.'
30        );
31
32        $this->addOption( "showgrants",
33            "Prints a description of available grants and exits."
34        );
35        $this->addOption( "appid",
36            "App id for the new bot password.", false, true
37        );
38        $this->addOption( "grants",
39            "CSV list of permissions to grant.", false, true
40        );
41        $this->addArg( "user",
42            "The username to create a bot password for.", false
43        );
44        $this->addArg( "password",
45            "A password will be generated if this is omitted." .
46            " If supplied, it must be exactly 32 characters.", false
47        );
48    }
49
50    public function execute() {
51        if ( $this->hasOption( 'showgrants' ) ) {
52            $this->showGrants();
53            return;
54        }
55
56        $username = $this->getArg( 0 );
57        $password = $this->getArg( 1 );
58        $appId = $this->getOption( 'appid' );
59        $grants = explode( ',', $this->getOption( 'grants', '' ) );
60
61        $errors = [];
62        if ( $username === null ) {
63            $errors[] = "Argument <user> required!";
64        }
65        if ( $appId == null ) {
66            $errors[] = "Param appid required!";
67        }
68        if ( $this->getOption( 'grants' ) === null ) {
69            $errors[] = "Param grants required!";
70        }
71        if ( count( $errors ) > 0 ) {
72            $this->fatalError( implode( "\n", $errors ) );
73        }
74
75        $services = $this->getServiceContainer();
76        $grantsInfo = $services->getGrantsInfo();
77        $invalidGrants = array_diff( $grants, $grantsInfo->getValidGrants() );
78        if ( count( $invalidGrants ) > 0 ) {
79            $this->fatalError(
80                "These grants are invalid: " . implode( ', ', $invalidGrants ) . "\n" .
81                "Use the --showgrants option for a full list of valid grant names."
82            );
83        }
84
85        $passwordFactory = $services->getPasswordFactory();
86
87        $userIdentity = $services->getUserIdentityLookup()->getUserIdentityByName( $username );
88        if ( !$userIdentity || !$userIdentity->isRegistered() ) {
89            $this->fatalError( "Cannot create bot password for non-existent user '$username'." );
90        }
91
92        if ( $password === null ) {
93            $password = BotPassword::generatePassword();
94        } else {
95            $passwordLength = strlen( $password );
96            if ( $passwordLength < BotPassword::PASSWORD_MINLENGTH ) {
97                $message = "Bot passwords must have at least " . BotPassword::PASSWORD_MINLENGTH .
98                    " characters. Given password is $passwordLength characters.";
99                $this->fatalError( $message );
100            }
101        }
102
103        $bp = BotPassword::newUnsaved( [
104            'username' => $username,
105            'appId' => $appId,
106            'grants' => $grants
107        ] );
108
109        if ( $bp === null ) {
110            $this->fatalError( "Bot password creation failed." );
111        }
112
113        $passwordInstance = $passwordFactory->newFromPlaintext( $password );
114        $status = $bp->save( 'insert', $passwordInstance );
115
116        if ( $status->isGood() ) {
117            $this->output( "Success.\n" );
118            $this->output( "Log in using username:'{$username}@{$appId}' and password:'{$password}'.\n" );
119        } else {
120            $this->error( "Bot password creation failed. Does this appid already exist for the user perhaps?" );
121            $this->fatalError( $status );
122        }
123    }
124
125    public function showGrants() {
126        $permissions = $this->getServiceContainer()->getGrantsInfo()->getValidGrants();
127        sort( $permissions );
128
129        $this->output( str_pad( 'GRANT', self::SHOWGRANTS_COLUMN_WIDTH ) . " DESCRIPTION\n" );
130        foreach ( $permissions as $permission ) {
131            $this->output(
132                str_pad( $permission, self::SHOWGRANTS_COLUMN_WIDTH ) . " " .
133                User::getRightDescription( $permission ) . "\n"
134            );
135        }
136    }
137}
138
139// @codeCoverageIgnoreStart
140$maintClass = CreateBotPassword::class;
141require_once RUN_MAINTENANCE_IF_MAIN;
142// @codeCoverageIgnoreEnd