Skip to content
16 changes: 16 additions & 0 deletions features/config-create.feature
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,22 @@ Feature: Create a wp-config file
my\password
"""

Scenario: DB charset values with special characters are escaped
Given an empty directory
And WP files

When I run `wp config create --skip-check --dbname=somedb --dbuser=someuser --dbpass=somepassword --dbcharset="utf8mb4'latin1\legacy"`
Then the wp-config.php file should contain:
"""
define( 'DB_CHARSET', 'utf8mb4\'latin1\\legacy' )
"""

When I run `wp config get DB_CHARSET`
Then STDOUT should be:
"""
utf8mb4'latin1\legacy
"""

Scenario: wp-config.php in parent folder should not prevent config create in subfolder
Given an empty directory
And a wp-config.php file:
Expand Down
164 changes: 129 additions & 35 deletions src/Config_Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ private static function get_initial_locale() {
* Success: Generated 'wp-config.php' file.
*/
public function create( $_, $assoc_args ) {
$provided_assoc_args = $assoc_args;

if ( ! Utils\get_flag_value( $assoc_args, 'force' ) ) {
if ( isset( $assoc_args['config-file'] ) && file_exists( $assoc_args['config-file'] ) ) {
$this->config_file_already_exist_error( basename( $assoc_args['config-file'] ) );
Expand Down Expand Up @@ -304,26 +306,145 @@ public function create( $_, $assoc_args ) {
}
}

foreach ( $assoc_args as $key => $value ) {
$assoc_args[ $key ] = $this->escape_config_value( $key, $value );
$template_args = array_merge(
$defaults,
[
'keys-and-salts' => false,
'keys-and-salts-alt' => '',
'extra-php' => '',
]
);

if ( ! empty( $assoc_args['keys-and-salts'] ) ) {
$template_args['keys-and-salts'] = true;
$template_args['auth-key'] = '';
$template_args['secure-auth-key'] = '';
$template_args['logged-in-key'] = '';
$template_args['nonce-key'] = '';
$template_args['auth-salt'] = '';
$template_args['secure-auth-salt'] = '';
$template_args['logged-in-salt'] = '';
$template_args['nonce-salt'] = '';
$template_args['wp-cache-key-salt'] = '';
} elseif ( ! empty( $assoc_args['keys-and-salts-alt'] ) ) {
$template_args['keys-and-salts-alt'] = $assoc_args['keys-and-salts-alt'];
}

// 'extra-php' from STDIN is retrieved after escaping to avoid breaking
// the PHP code.
if ( Utils\get_flag_value( $assoc_args, 'extra-php' ) === true ) {
$assoc_args['extra-php'] = file_get_contents( 'php://stdin' );
$template_args['extra-php'] = file_get_contents( 'php://stdin' );
}

$command_root = Path::phar_safe( dirname( __DIR__ ) );
$out = Utils\mustache_render( "{$command_root}/templates/wp-config.mustache", $assoc_args );
$out = Utils\mustache_render( "{$command_root}/templates/wp-config.mustache", $template_args );

$wp_config_file_name = basename( $assoc_args['config-file'] );
$created_config_file = ! file_exists( $assoc_args['config-file'] );
$bytes_written = file_put_contents( $assoc_args['config-file'], $out );
if ( ! $bytes_written ) {
WP_CLI::error( "Could not create new '{$wp_config_file_name}' file." );
Comment on lines 340 to 344
} else {
WP_CLI::success( "Generated '{$wp_config_file_name}' file." );
}

try {
$config_transformer = new WPConfigTransformer( $assoc_args['config-file'] );

$value_map = [
'dbname' => [
'type' => 'constant',
'name' => 'DB_NAME',
'anchor' => '/** Database username */',
],
'dbuser' => [
'type' => 'constant',
'name' => 'DB_USER',
'anchor' => '/** Database password */',
],
'dbpass' => [
'type' => 'constant',
'name' => 'DB_PASSWORD',
'anchor' => '/** Database hostname */',
],
'dbhost' => [
'type' => 'constant',
'name' => 'DB_HOST',
'anchor' => '/** Database charset to use in creating database tables. */',
],
'dbcharset' => [
'type' => 'constant',
'name' => 'DB_CHARSET',
'anchor' => '/** The database collate type. Don\'t change this if in doubt. */',
],
'dbcollate' => [
'type' => 'constant',
'name' => 'DB_COLLATE',
'anchor' => '/**#@+',
],
'dbprefix' => [
'type' => 'variable',
'name' => 'table_prefix',
'anchor' => '/* Add any custom values between this line and the "stop editing" line. */',
],
];

foreach ( $value_map as $arg_name => $entry ) {
if ( ! array_key_exists( $arg_name, $provided_assoc_args ) ) {
continue;
}

if ( is_string( $provided_assoc_args[ $arg_name ] ) && false !== strpos( $provided_assoc_args[ $arg_name ], '\\' ) ) {
// Use remove+update to preserve backslash escaping while keeping insertion behavior aligned with the rest of this class.
$config_transformer->remove( $entry['type'], $entry['name'] );
$config_transformer->update(
$entry['type'],
$entry['name'],
$provided_assoc_args[ $arg_name ],
[
'add' => true,
'anchor' => $entry['anchor'],
'placement' => 'before',
]
);
continue;
}

$config_transformer->update(
$entry['type'],
$entry['name'],
$provided_assoc_args[ $arg_name ],
[ 'add' => true ]
);
}

if ( ! empty( $assoc_args['keys-and-salts'] ) ) {
$salt_map = [
'AUTH_KEY' => 'auth-key',
'SECURE_AUTH_KEY' => 'secure-auth-key',
'LOGGED_IN_KEY' => 'logged-in-key',
'NONCE_KEY' => 'nonce-key',
'AUTH_SALT' => 'auth-salt',
'SECURE_AUTH_SALT' => 'secure-auth-salt',
'LOGGED_IN_SALT' => 'logged-in-salt',
'NONCE_SALT' => 'nonce-salt',
'WP_CACHE_KEY_SALT' => 'wp-cache-key-salt',
];

foreach ( $salt_map as $name => $arg_name ) {
if ( ! array_key_exists( $arg_name, $assoc_args ) ) {
continue;
}
$config_transformer->update( 'constant', $name, $assoc_args[ $arg_name ], [ 'add' => true ] );
}
}
} catch ( Throwable $exception ) {
$cleanup_error = '';
if ( $created_config_file && file_exists( $assoc_args['config-file'] ) ) {
if ( ! unlink( $assoc_args['config-file'] ) ) {
$cleanup_error = "\nCleanup: Could not remove '{$wp_config_file_name}' after failure.";
}
}
Comment thread
swissspidy marked this conversation as resolved.
WP_CLI::error( "Could not process the '{$wp_config_file_name}' transformation.\nReason: {$exception->getMessage()}{$cleanup_error}" );
}

WP_CLI::success( "Generated '{$wp_config_file_name}' file." );
}

/**
Expand Down Expand Up @@ -1521,31 +1642,4 @@ private static function is_sqlite_integration_active() {
$db_dropin_contents
);
}

/**
* Escape a config value so it can be safely used within single quotes.
*
* @param string $key Key into the arguments array.
* @param mixed $value Value to escape.
* @return mixed Escaped value.
*/
private function escape_config_value( $key, $value ) {
// Skip 'extra-php', it mustn't be escaped.
if ( 'extra-php' === $key ) {
return $value;
}

// Skip 'keys-and-salts-alt' and assume they are safe.
if ( 'keys-and-salts-alt' === $key && ! empty( $value ) ) {
return $value;
}

if ( is_string( $value ) ) {
$value = str_replace( '\\', '\\\\', $value ); // Escape backslashes first
$value = str_replace( "'", "\\'", $value ); // Then escape single quotes
return $value;
}

return $value;
}
}
Loading