Skip to content

Commit 347182e

Browse files
Copilotswissspidy
andcommitted
Add wp db users create command implementation
Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com>
1 parent 2e544c4 commit 347182e

5 files changed

Lines changed: 312 additions & 1 deletion

File tree

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@
5050
"db search",
5151
"db tables",
5252
"db size",
53-
"db columns"
53+
"db columns",
54+
"db users",
55+
"db users create"
5456
]
5557
},
5658
"autoload": {

db-command.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
}
1111

1212
WP_CLI::add_command( 'db', 'DB_Command' );
13+
WP_CLI::add_command( 'db users', 'DB_Users_Command' );

features/db-users.feature

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
Feature: Manage database users
2+
3+
Scenario: Create database user without privileges
4+
Given an empty directory
5+
And WP files
6+
And wp-config.php
7+
8+
When I run `wp db create`
9+
Then STDOUT should be:
10+
"""
11+
Success: Database created.
12+
"""
13+
14+
When I run `wp db users create testuser localhost --password=testpass123`
15+
Then STDOUT should contain:
16+
"""
17+
Success: Database user 'testuser'@'localhost' created.
18+
"""
19+
20+
When I run `wp db query "SELECT User, Host FROM mysql.user WHERE User='testuser'"`
21+
Then STDOUT should contain:
22+
"""
23+
testuser
24+
"""
25+
26+
Scenario: Create database user with privileges
27+
Given an empty directory
28+
And WP files
29+
And wp-config.php
30+
31+
When I run `wp db create`
32+
Then STDOUT should be:
33+
"""
34+
Success: Database created.
35+
"""
36+
37+
When I run `wp db users create appuser localhost --password=secret123 --grant-privileges`
38+
Then STDOUT should contain:
39+
"""
40+
created with privileges on database
41+
"""
42+
And STDOUT should contain:
43+
"""
44+
appuser
45+
"""
46+
47+
Scenario: Create database user with custom host
48+
Given an empty directory
49+
And WP files
50+
And wp-config.php
51+
52+
When I run `wp db create`
53+
Then STDOUT should be:
54+
"""
55+
Success: Database created.
56+
"""
57+
58+
When I run `wp db users create remoteuser '%' --password=remote123`
59+
Then STDOUT should contain:
60+
"""
61+
Success: Database user 'remoteuser'@'%' created.
62+
"""
63+
64+
Scenario: Create database user with no password
65+
Given an empty directory
66+
And WP files
67+
And wp-config.php
68+
69+
When I run `wp db create`
70+
Then STDOUT should be:
71+
"""
72+
Success: Database created.
73+
"""
74+
75+
When I run `wp db users create nopassuser localhost`
76+
Then STDOUT should contain:
77+
"""
78+
Success: Database user 'nopassuser'@'localhost' created.
79+
"""

phpcs.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
<!-- Exclude existing classes from the prefix rule as it would break BC to prefix them now. -->
5555
<rule ref="WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound">
5656
<exclude-pattern>*/src/DB_Command\.php$</exclude-pattern>
57+
<exclude-pattern>*/src/DB_Users_Command\.php$</exclude-pattern>
5758
</rule>
5859

5960
<exclude-pattern>/tests/phpstan/scan-files</exclude-pattern>

src/DB_Users_Command.php

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
<?php
2+
3+
use WP_CLI\Utils;
4+
5+
/**
6+
* Manages MySQL database users.
7+
*
8+
* ## EXAMPLES
9+
*
10+
* # Create a new database user with privileges.
11+
* $ wp db users create myuser myhost --password=mypass --grant-privileges
12+
* Success: Database user 'myuser'@'myhost' created with privileges.
13+
*
14+
* @when after_wp_config_load
15+
*/
16+
class DB_Users_Command extends WP_CLI_Command {
17+
18+
/**
19+
* Creates a new database user with optional privileges.
20+
*
21+
* Creates a MySQL database user account and optionally grants full privileges
22+
* to the current database specified in wp-config.php.
23+
*
24+
* ## OPTIONS
25+
*
26+
* <username>
27+
* : MySQL username for the new user account.
28+
*
29+
* [<host>]
30+
* : MySQL host for the new user account.
31+
* ---
32+
* default: localhost
33+
* ---
34+
*
35+
* [--password=<password>]
36+
* : Password for the new user account. If not provided, MySQL will use no password.
37+
*
38+
* [--grant-privileges]
39+
* : Grant full privileges on the current database to the new user.
40+
*
41+
* [--dbuser=<value>]
42+
* : Username to connect as (privileged user). Defaults to DB_USER.
43+
*
44+
* [--dbpass=<value>]
45+
* : Password to connect with (privileged user). Defaults to DB_PASSWORD.
46+
*
47+
* [--defaults]
48+
* : Loads the environment's MySQL option files. Default behavior is to skip loading them to avoid failures due to misconfiguration.
49+
*
50+
* ## EXAMPLES
51+
*
52+
* # Create a user without privileges.
53+
* $ wp db users create myuser localhost --password=mypass
54+
* Success: Database user 'myuser'@'localhost' created.
55+
*
56+
* # Create a user with full privileges on the current database.
57+
* $ wp db users create appuser localhost --password=secret123 --grant-privileges
58+
* Success: Database user 'appuser'@'localhost' created with privileges on database 'wp_database'.
59+
*/
60+
public function create( $args, $assoc_args ) {
61+
list( $username, $host ) = array_pad( $args, 2, 'localhost' );
62+
63+
$password = Utils\get_flag_value( $assoc_args, 'password', '' );
64+
$grant_privileges = Utils\get_flag_value( $assoc_args, 'grant-privileges', false );
65+
66+
// Escape identifiers for SQL
67+
$username_escaped = $this->esc_sql_ident( $username );
68+
$host_escaped = $this->esc_sql_ident( $host );
69+
$user_identifier = "{$username_escaped}@{$host_escaped}";
70+
71+
// Create user
72+
$create_query = "CREATE USER {$user_identifier}";
73+
if ( ! empty( $password ) ) {
74+
$password_escaped = $this->esc_sql_string( $password );
75+
$create_query .= " IDENTIFIED BY {$password_escaped}";
76+
}
77+
$create_query .= ';';
78+
79+
$this->run_query( $create_query, $assoc_args );
80+
81+
// Grant privileges if requested
82+
if ( $grant_privileges ) {
83+
$database = DB_NAME;
84+
$database_escaped = $this->esc_sql_ident( $database );
85+
$grant_query = "GRANT ALL PRIVILEGES ON {$database_escaped}.* TO {$user_identifier};";
86+
$this->run_query( $grant_query, $assoc_args );
87+
88+
// Flush privileges
89+
$this->run_query( 'FLUSH PRIVILEGES;', $assoc_args );
90+
91+
WP_CLI::success( "Database user '{$username}'@'{$host}' created with privileges on database '{$database}'." );
92+
} else {
93+
WP_CLI::success( "Database user '{$username}'@'{$host}' created." );
94+
}
95+
}
96+
97+
/**
98+
* Run a single query via the 'mysql' binary.
99+
*
100+
* @param string $query Query to execute.
101+
* @param array $assoc_args Optional. Associative array of arguments.
102+
*/
103+
private function run_query( $query, $assoc_args = [] ) {
104+
WP_CLI::debug( "Query: {$query}", 'db' );
105+
106+
$mysql_args = array_merge(
107+
$this->get_dbuser_dbpass_args( $assoc_args ),
108+
$this->get_mysql_args( $assoc_args )
109+
);
110+
111+
$this->run(
112+
sprintf(
113+
'mysql%s --no-auto-rehash',
114+
$this->get_defaults_flag_string( $assoc_args )
115+
),
116+
array_merge( [ 'execute' => $query ], $mysql_args )
117+
);
118+
}
119+
120+
/**
121+
* Run a MySQL command.
122+
*
123+
* @param string $cmd Command to run.
124+
* @param array $assoc_args Optional. Associative array of arguments to use.
125+
*
126+
* @return array {
127+
* Associative array containing STDOUT and STDERR output.
128+
*
129+
* @type string $stdout Output that was sent to STDOUT.
130+
* @type string $stderr Output that was sent to STDERR.
131+
* @type int $exit_code Exit code of the process.
132+
* }
133+
*/
134+
private function run( $cmd, $assoc_args = [] ) {
135+
$required = [
136+
'host' => DB_HOST,
137+
'user' => DB_USER,
138+
'pass' => DB_PASSWORD,
139+
];
140+
141+
if ( ! isset( $assoc_args['default-character-set'] )
142+
&& defined( 'DB_CHARSET' ) && constant( 'DB_CHARSET' ) ) {
143+
$required['default-character-set'] = constant( 'DB_CHARSET' );
144+
}
145+
146+
// Using 'dbuser' as option name to workaround clash with WP-CLI's global WP 'user' parameter.
147+
if ( isset( $assoc_args['dbuser'] ) ) {
148+
$required['user'] = $assoc_args['dbuser'];
149+
unset( $assoc_args['dbuser'] );
150+
}
151+
if ( isset( $assoc_args['dbpass'] ) ) {
152+
$required['pass'] = $assoc_args['dbpass'];
153+
unset( $assoc_args['dbpass'], $assoc_args['password'] );
154+
}
155+
156+
$final_args = array_merge( $required, $assoc_args );
157+
158+
return Utils\run_mysql_command( $cmd, $final_args, null, true, false );
159+
}
160+
161+
/**
162+
* Helper to pluck 'dbuser' and 'dbpass' from associative args array.
163+
*
164+
* @param array $assoc_args Associative args array.
165+
* @return array Array with 'dbuser' and 'dbpass' set if in passed-in associative args array.
166+
*/
167+
private function get_dbuser_dbpass_args( $assoc_args ) {
168+
$mysql_args = [];
169+
$dbuser = Utils\get_flag_value( $assoc_args, 'dbuser' );
170+
if ( null !== $dbuser ) {
171+
$mysql_args['dbuser'] = $dbuser;
172+
}
173+
$dbpass = Utils\get_flag_value( $assoc_args, 'dbpass' );
174+
if ( null !== $dbpass ) {
175+
$mysql_args['dbpass'] = $dbpass;
176+
}
177+
return $mysql_args;
178+
}
179+
180+
/**
181+
* Gets the MySQL args from the associative args array.
182+
*
183+
* @param array $assoc_args Associative args array.
184+
* @return array MySQL args.
185+
*/
186+
private function get_mysql_args( $assoc_args ) {
187+
$mysql_args = [];
188+
189+
if ( isset( $assoc_args['host'] ) ) {
190+
$mysql_args['host'] = $assoc_args['host'];
191+
}
192+
193+
return $mysql_args;
194+
}
195+
196+
/**
197+
* Gets the defaults flag string.
198+
*
199+
* @param array $assoc_args Associative args array.
200+
* @return string Defaults flag string.
201+
*/
202+
private function get_defaults_flag_string( $assoc_args ) {
203+
$defaults = Utils\get_flag_value( $assoc_args, 'defaults', false );
204+
return $defaults ? '' : ' --no-defaults';
205+
}
206+
207+
/**
208+
* Escapes a string for use in a SQL query.
209+
*
210+
* @param string $value String to escape.
211+
* @return string Escaped string.
212+
*/
213+
private function esc_sql_string( $value ) {
214+
// Use single quotes and escape single quotes by doubling them.
215+
return "'" . str_replace( "'", "''", $value ) . "'";
216+
}
217+
218+
/**
219+
* Escapes (backticks) MySQL identifiers (aka schema object names).
220+
*
221+
* @param string $ident A single identifier.
222+
* @return string An escaped string.
223+
*/
224+
private function esc_sql_ident( $ident ) {
225+
// Escape any backticks in the identifier by doubling.
226+
return '`' . str_replace( '`', '``', $ident ) . '`';
227+
}
228+
}

0 commit comments

Comments
 (0)