Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@
"db search",
"db tables",
"db size",
"db columns"
"db columns",
"db users",
"db users create"
]
},
"autoload": {
Expand Down
1 change: 1 addition & 0 deletions db-command.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
}

WP_CLI::add_command( 'db', 'DB_Command' );
WP_CLI::add_command( 'db users', 'DB_Users_Command' );
79 changes: 79 additions & 0 deletions features/db-users.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
Feature: Manage database users

Scenario: Create database user without privileges
Given an empty directory
And WP files
And wp-config.php

When I run `wp db create`
Then STDOUT should be:
"""
Success: Database created.
"""

When I run `wp db users create testuser localhost --password=testpass123`
Then STDOUT should contain:
"""
Success: Database user 'testuser'@'localhost' created.
"""

When I run `wp db query "SELECT User, Host FROM mysql.user WHERE User='testuser'"`
Then STDOUT should contain:
"""
testuser
"""

Scenario: Create database user with privileges
Given an empty directory
And WP files
And wp-config.php

When I run `wp db create`
Then STDOUT should be:
"""
Success: Database created.
"""

When I run `wp db users create appuser localhost --password=secret123 --grant-privileges`
Then STDOUT should contain:
"""
created with privileges on database
"""
And STDOUT should contain:
"""
appuser
"""

Scenario: Create database user with custom host
Given an empty directory
And WP files
And wp-config.php

When I run `wp db create`
Then STDOUT should be:
"""
Success: Database created.
"""

When I run `wp db users create remoteuser '%' --password=remote123`
Then STDOUT should contain:
"""
Success: Database user 'remoteuser'@'%' created.
"""

Scenario: Create database user with no password
Given an empty directory
And WP files
And wp-config.php

When I run `wp db create`
Then STDOUT should be:
"""
Success: Database created.
"""

When I run `wp db users create nopassuser localhost`
Then STDOUT should contain:
"""
Success: Database user 'nopassuser'@'localhost' created.
"""
1 change: 1 addition & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
<!-- Exclude existing classes from the prefix rule as it would break BC to prefix them now. -->
<rule ref="WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound">
<exclude-pattern>*/src/DB_Command\.php$</exclude-pattern>
<exclude-pattern>*/src/DB_Users_Command\.php$</exclude-pattern>
Comment thread
swissspidy marked this conversation as resolved.
Outdated
</rule>

<exclude-pattern>/tests/phpstan/scan-files</exclude-pattern>
Expand Down
123 changes: 123 additions & 0 deletions src/DB_Users_Command.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

use WP_CLI\Utils;

/**
* Manages MySQL database users.
*
* ## EXAMPLES
*
* # Create a new database user with privileges.
* $ wp db users create myuser myhost --password=mypass --grant-privileges
* Success: Database user 'myuser'@'myhost' created with privileges.
Comment thread
swissspidy marked this conversation as resolved.
Outdated
*
* @when after_wp_config_load
*/
class DB_Users_Command extends DB_Command {

/**
* Creates a new database user with optional privileges.
*
* Creates a MySQL database user account and optionally grants full privileges
* to the current database specified in wp-config.php.
*
* ## OPTIONS
*
* <username>
* : MySQL username for the new user account.
*
* [<host>]
* : MySQL host for the new user account.
* ---
* default: localhost
* ---
*
* [--password=<password>]
* : Password for the new user account. If not provided, MySQL will use no password.
*
* [--grant-privileges]
* : Grant full privileges on the current database to the new user.
*
* [--dbuser=<value>]
* : Username to connect as (privileged user). Defaults to DB_USER.
*
* [--dbpass=<value>]
* : Password to connect with (privileged user). Defaults to DB_PASSWORD.
*
* [--defaults]
* : Loads the environment's MySQL option files. Default behavior is to skip loading them to avoid failures due to misconfiguration.
*
* ## EXAMPLES
*
* # Create a user without privileges.
* $ wp db users create myuser localhost --password=mypass
* Success: Database user 'myuser'@'localhost' created.
*
* # Create a user with full privileges on the current database.
* $ wp db users create appuser localhost --password=secret123 --grant-privileges
* Success: Database user 'appuser'@'localhost' created with privileges on database 'wp_database'.
*/
public function create( $args, $assoc_args ) {
list( $username, $host ) = array_pad( $args, 2, 'localhost' );

$password = Utils\get_flag_value( $assoc_args, 'password', '' );
$grant_privileges = Utils\get_flag_value( $assoc_args, 'grant-privileges', false );

// Escape identifiers for SQL
$username_escaped = $this->esc_sql_ident( $username );

Check failure on line 67 in src/DB_Users_Command.php

View workflow job for this annotation

GitHub Actions / copilot

Cannot cast array|string to string.
$host_escaped = $this->esc_sql_ident( $host );

Check failure on line 68 in src/DB_Users_Command.php

View workflow job for this annotation

GitHub Actions / copilot

Cannot cast array|string to string.
$user_identifier = "{$username_escaped}@{$host_escaped}";

Check failure on line 69 in src/DB_Users_Command.php

View workflow job for this annotation

GitHub Actions / copilot

Part $username_escaped (array|string) of encapsed string cannot be cast to string.

Check failure on line 69 in src/DB_Users_Command.php

View workflow job for this annotation

GitHub Actions / copilot

Part $host_escaped (array|string) of encapsed string cannot be cast to string.

// Create user
$create_query = "CREATE USER {$user_identifier}";
if ( ! empty( $password ) ) {
$password_escaped = $this->esc_sql_string( $password );
Comment thread
swissspidy marked this conversation as resolved.
$create_query .= " IDENTIFIED BY {$password_escaped}";
}
$create_query .= ';';

parent::run_query( $create_query, $assoc_args );

// Grant privileges if requested
if ( $grant_privileges ) {
$database = DB_NAME;
$database_escaped = $this->esc_sql_ident( $database );
$grant_query = "GRANT ALL PRIVILEGES ON {$database_escaped}.* TO {$user_identifier};";
parent::run_query( $grant_query, $assoc_args );

// Flush privileges
parent::run_query( 'FLUSH PRIVILEGES;', $assoc_args );

WP_CLI::success( "Database user '{$username}'@'{$host}' created with privileges on database '{$database}'." );
} else {
WP_CLI::success( "Database user '{$username}'@'{$host}' created." );
}
}

/**
* Escapes a string for use in a SQL query.
*
* @param string $value String to escape.
* @return string Escaped string.
*/
private function esc_sql_string( $value ) {
// Escape backslashes first, then single quotes.
$value = str_replace( '\\', '\\\\', $value );
$value = str_replace( "'", "''", $value );
Comment thread
swissspidy marked this conversation as resolved.
Outdated
return "'" . $value . "'";
}

/**
* Escapes (backticks) MySQL identifiers (aka schema object names).
*
* Note: This duplicates functionality from parent DB_Command::esc_sql_ident()
* which is private static and cannot be accessed from child classes.
*
* @param string $ident A single identifier.
* @return string An escaped string.
*/
private function esc_sql_ident( $ident ) {
Comment thread
swissspidy marked this conversation as resolved.
Outdated
// Escape any backticks in the identifier by doubling.
return '`' . str_replace( '`', '``', $ident ) . '`';
}
}
Loading