Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 74
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
MysqlCreateUserTask
0.00% covered (danger)
0.00%
0 / 74
0.00% covered (danger)
0.00%
0 / 6
342
0.00% covered (danger)
0.00%
0 / 1
 getName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDependencies
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isSkipped
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 execute
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 1
132
 buildFullUserName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 userDefinitelyExists
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace MediaWiki\Installer\Task;
4
5use MediaWiki\MainConfigNames;
6use MediaWiki\Status\Status;
7use Wikimedia\Rdbms\DatabaseFactory;
8use Wikimedia\Rdbms\DBConnectionError;
9use Wikimedia\Rdbms\DBQueryError;
10use Wikimedia\Rdbms\IMaintainableDatabase;
11
12/**
13 * Create the MySQL/MariaDB user
14 *
15 * @internal For use by the installer
16 */
17class MysqlCreateUserTask extends Task {
18    public function getName() {
19        return 'user';
20    }
21
22    public function getDependencies() {
23        return 'database';
24    }
25
26    public function isSkipped(): bool {
27        $dbUser = $this->getConfigVar( MainConfigNames::DBuser );
28        return $dbUser == $this->getOption( 'InstallUser' )
29            || $this->getOption( 'InstallUser' ) === null;
30    }
31
32    public function execute(): Status {
33        $dbUser = $this->getConfigVar( MainConfigNames::DBuser );
34        $status = $this->getConnection( ITaskContext::CONN_CREATE_DATABASE );
35        if ( !$status->isOK() ) {
36            return $status;
37        }
38
39        $conn = $status->getDB();
40        $server = $this->getConfigVar( MainConfigNames::DBserver );
41        $password = $this->getConfigVar( MainConfigNames::DBpassword );
42        $grantableNames = [];
43
44        if ( $this->getOption( 'CreateDBAccount' ) ) {
45            // Before we blindly try to create a user that already has access,
46            try { // first attempt to connect to the database
47                ( new DatabaseFactory() )->create( 'mysql', [
48                    'host' => $server,
49                    'user' => $dbUser,
50                    'password' => $password,
51                    'ssl' => $this->getConfigVar( MainConfigNames::DBssl ),
52                    'dbname' => null,
53                    'flags' => 0,
54                    'tablePrefix' => $this->getConfigVar( MainConfigNames::DBprefix )
55                ] );
56                $grantableNames[] = $this->buildFullUserName( $conn, $dbUser, $server );
57                $tryToCreate = false;
58            } catch ( DBConnectionError $e ) {
59                $tryToCreate = true;
60            }
61        } else {
62            $grantableNames[] = $this->buildFullUserName( $conn, $dbUser, $server );
63            $tryToCreate = false;
64        }
65
66        if ( $tryToCreate ) {
67            $createHostList = [
68                $server,
69                'localhost',
70                'localhost.localdomain',
71                '%'
72            ];
73
74            $createHostList = array_unique( $createHostList );
75            $escPass = $conn->addQuotes( $password );
76
77            foreach ( $createHostList as $host ) {
78                $fullName = $this->buildFullUserName( $conn, $dbUser, $host );
79                if ( !$this->userDefinitelyExists( $conn, $host, $dbUser ) ) {
80                    try {
81                        $conn->begin( __METHOD__ );
82                        $conn->query( "CREATE USER $fullName IDENTIFIED BY $escPass", __METHOD__ );
83                        $conn->commit( __METHOD__ );
84                        $grantableNames[] = $fullName;
85                    } catch ( DBQueryError $dqe ) {
86                        if ( $conn->lastErrno() == 1396 /* ER_CANNOT_USER */ ) {
87                            // User (probably) already exists
88                            $conn->rollback( __METHOD__ );
89                            $status->warning( 'config-install-user-alreadyexists', $dbUser );
90                            $grantableNames[] = $fullName;
91                            break;
92                        } else {
93                            // If we couldn't create for some bizarre reason and the
94                            // user probably doesn't exist, skip the grant
95                            $conn->rollback( __METHOD__ );
96                            $status->warning( 'config-install-user-create-failed', $dbUser, $dqe->getMessage() );
97                        }
98                    }
99                } else {
100                    $status->warning( 'config-install-user-alreadyexists', $dbUser );
101                    $grantableNames[] = $fullName;
102                    break;
103                }
104            }
105        }
106
107        // Try to grant to all the users we know exist or we were able to create
108        $dbAllTables = $conn->addIdentifierQuotes( $this->getConfigVar( MainConfigNames::DBname ) ) . '.*';
109        foreach ( $grantableNames as $name ) {
110            try {
111                $conn->begin( __METHOD__ );
112                $conn->query( "GRANT ALL PRIVILEGES ON $dbAllTables TO $name", __METHOD__ );
113                $conn->commit( __METHOD__ );
114            } catch ( DBQueryError $dqe ) {
115                $conn->rollback( __METHOD__ );
116                $status->fatal( 'config-install-user-grant-failed', $dbUser, $dqe->getMessage() );
117            }
118        }
119
120        return $status;
121    }
122
123    /**
124     * Return a formal 'User'@'Host' username for use in queries
125     * @param IMaintainableDatabase $conn
126     * @param string $name Username, quotes will be added
127     * @param string $host Hostname, quotes will be added
128     * @return string
129     */
130    private function buildFullUserName( $conn, $name, $host ) {
131        return $conn->addQuotes( $name ) . '@' . $conn->addQuotes( $host );
132    }
133
134    /**
135     * Try to see if the user account exists. Our "superuser" may not have
136     * access to mysql.user, so false means "no" or "maybe"
137     * @param IMaintainableDatabase $conn
138     * @param string $host Hostname to check
139     * @param string $user Username to check
140     * @return bool
141     */
142    private function userDefinitelyExists( $conn, $host, $user ) {
143        try {
144            $res = $conn->newSelectQueryBuilder()
145                ->select( [ 'Host', 'User' ] )
146                ->from( 'mysql.user' )
147                ->where( [ 'Host' => $host, 'User' => $user ] )
148                ->caller( __METHOD__ )->fetchRow();
149
150            return (bool)$res;
151        } catch ( DBQueryError $dqe ) {
152            return false;
153        }
154    }
155
156}