Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
42 changes: 42 additions & 0 deletions features/config-create.feature
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,32 @@ Feature: Create a wp-config file
define( 'AUTH_SALT',
"""

Scenario: wp core config uses parameters from wp-cli.yml
Given an empty directory
And WP files
And a wp-cli.yml file:
"""
core config:
dbname: wordpress
dbuser: root
extra-php: |
define( 'WP_DEBUG', true );
define( 'WP_POST_REVISIONS', 50 );
"""

When I run `wp core config --skip-check`
Then the wp-config.php file should contain:
"""
define( 'DB_NAME', 'wordpress' )
"""
And the wp-config.php file should contain:
"""
define( 'DB_USER', 'root' )
"""

When I run `grep WP_POST_REVISIONS wp-config.php`
Then STDOUT should not be empty

Scenario: Configure with invalid table prefix
Given an empty directory
And WP files
Expand Down Expand Up @@ -324,6 +350,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
181 changes: 143 additions & 38 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,156 @@ 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' );
$extra_php = Utils\get_flag_value( $assoc_args, 'extra-php' );
if ( true === $extra_php ) {
$template_args['extra-php'] = file_get_contents( 'php://stdin' );
} elseif ( is_string( $extra_php ) ) {
$template_args['extra-php'] = $extra_php;
}

$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'] );
$bytes_written = file_put_contents( $assoc_args['config-file'], $out );
$wp_config_file_name = basename( $assoc_args['config-file'] );
$created_config_file = ! file_exists( $assoc_args['config-file'] );
$original_config_contents = $created_config_file ? null : file_get_contents( $assoc_args['config-file'] );
if ( false === $original_config_contents ) {
$original_config_contents = null;
}
$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." );
} 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.";
}
} elseif ( null !== $original_config_contents ) {
if ( false === file_put_contents( $assoc_args['config-file'], $original_config_contents ) ) {
$cleanup_error = "\nCleanup: Could not restore the original '{$wp_config_file_name}' after failure.";
}
}
Comment thread
swissspidy marked this conversation as resolved.
Comment thread
Copilot 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 +1653,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;
}
}