From 29dfd5bbcbec658f506123b0dd2dbee98e8f2eae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 09:14:48 +0000 Subject: [PATCH 1/8] Initial plan From 5385a927e8ae21355f453e3f6dd2413af1491622 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 09:24:03 +0000 Subject: [PATCH 2/8] refactor config create to apply values via transformer Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/config-create.feature | 16 ++++ src/Config_Command.php | 131 ++++++++++++++++++++++++--------- 2 files changed, 112 insertions(+), 35 deletions(-) diff --git a/features/config-create.feature b/features/config-create.feature index 36dd4a70..156fecfe 100644 --- a/features/config-create.feature +++ b/features/config-create.feature @@ -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: diff --git a/src/Config_Command.php b/src/Config_Command.php index 02ddde8e..ec05271f 100644 --- a/src/Config_Command.php +++ b/src/Config_Command.php @@ -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'] ) ); @@ -304,26 +306,112 @@ 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'] ); $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', + ], + 'dbuser' => [ + 'type' => 'constant', + 'name' => 'DB_USER', + ], + 'dbpass' => [ + 'type' => 'constant', + 'name' => 'DB_PASSWORD', + ], + 'dbhost' => [ + 'type' => 'constant', + 'name' => 'DB_HOST', + ], + 'dbcharset' => [ + 'type' => 'constant', + 'name' => 'DB_CHARSET', + ], + 'dbcollate' => [ + 'type' => 'constant', + 'name' => 'DB_COLLATE', + ], + 'dbprefix' => [ + 'type' => 'variable', + 'name' => 'table_prefix', + ], + ]; + + foreach ( $value_map as $arg_name => $entry ) { + if ( ! array_key_exists( $arg_name, $provided_assoc_args ) ) { + continue; + } + + $config_transformer->update( + $entry['type'], + $entry['name'], + $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 ) { + $config_transformer->update( 'constant', $name, $assoc_args[ $arg_name ], [ 'add' => true ] ); + } + } + } catch ( Exception $exception ) { + WP_CLI::error( "Could not process the '{$wp_config_file_name}' transformation.\nReason: {$exception->getMessage()}" ); + } + + WP_CLI::success( "Generated '{$wp_config_file_name}' file." ); } /** @@ -1521,31 +1609,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; - } } From 5c2abef1813d0fe160eb003e71bd2db36647ee8c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 11:58:35 +0000 Subject: [PATCH 3/8] fix create backslash handling in transformer write path Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Config_Command.php | 48 ++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/src/Config_Command.php b/src/Config_Command.php index ec05271f..f6fe6e66 100644 --- a/src/Config_Command.php +++ b/src/Config_Command.php @@ -348,32 +348,39 @@ public function create( $_, $assoc_args ) { $value_map = [ 'dbname' => [ - 'type' => 'constant', - 'name' => 'DB_NAME', + 'type' => 'constant', + 'name' => 'DB_NAME', + 'anchor' => '/** Database username */', ], 'dbuser' => [ - 'type' => 'constant', - 'name' => 'DB_USER', + 'type' => 'constant', + 'name' => 'DB_USER', + 'anchor' => '/** Database password */', ], 'dbpass' => [ - 'type' => 'constant', - 'name' => 'DB_PASSWORD', + 'type' => 'constant', + 'name' => 'DB_PASSWORD', + 'anchor' => '/** Database hostname */', ], 'dbhost' => [ - 'type' => 'constant', - 'name' => 'DB_HOST', + 'type' => 'constant', + 'name' => 'DB_HOST', + 'anchor' => '/** Database charset to use in creating database tables. */', ], 'dbcharset' => [ - 'type' => 'constant', - 'name' => 'DB_CHARSET', + 'type' => 'constant', + 'name' => 'DB_CHARSET', + 'anchor' => '/** The database collate type. Don\'t change this if in doubt. */', ], 'dbcollate' => [ - 'type' => 'constant', - 'name' => 'DB_COLLATE', + 'type' => 'constant', + 'name' => 'DB_COLLATE', + 'anchor' => '/**#@+', ], 'dbprefix' => [ - 'type' => 'variable', - 'name' => 'table_prefix', + 'type' => 'variable', + 'name' => 'table_prefix', + 'anchor' => '/* Add any custom values between this line and the "stop editing" line. */', ], ]; @@ -388,6 +395,19 @@ public function create( $_, $assoc_args ) { $assoc_args[ $arg_name ], [ 'add' => true ] ); + + if ( false !== strpos( $assoc_args[ $arg_name ], '\\' ) ) { + $config_transformer->remove( $entry['type'], $entry['name'] ); + $config_transformer->add( + $entry['type'], + $entry['name'], + $assoc_args[ $arg_name ], + [ + 'anchor' => $entry['anchor'], + 'placement' => 'before', + ] + ); + } } if ( ! empty( $assoc_args['keys-and-salts'] ) ) { From 7e583a04ababa457332f72288748f7ab9bbf160e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 18:08:44 +0000 Subject: [PATCH 4/8] Address review comments in config create transformer flow Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Config_Command.php | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Config_Command.php b/src/Config_Command.php index f6fe6e66..d032ecb1 100644 --- a/src/Config_Command.php +++ b/src/Config_Command.php @@ -389,14 +389,8 @@ public function create( $_, $assoc_args ) { continue; } - $config_transformer->update( - $entry['type'], - $entry['name'], - $assoc_args[ $arg_name ], - [ 'add' => true ] - ); - if ( false !== strpos( $assoc_args[ $arg_name ], '\\' ) ) { + // Backslashes need the same remove+add path used by `wp config set` to preserve escaping. $config_transformer->remove( $entry['type'], $entry['name'] ); $config_transformer->add( $entry['type'], @@ -407,7 +401,15 @@ public function create( $_, $assoc_args ) { 'placement' => 'before', ] ); + continue; } + + $config_transformer->update( + $entry['type'], + $entry['name'], + $assoc_args[ $arg_name ], + [ 'add' => true ] + ); } if ( ! empty( $assoc_args['keys-and-salts'] ) ) { @@ -428,6 +430,9 @@ public function create( $_, $assoc_args ) { } } } catch ( Exception $exception ) { + if ( file_exists( $assoc_args['config-file'] ) ) { + unlink( $assoc_args['config-file'] ); + } WP_CLI::error( "Could not process the '{$wp_config_file_name}' transformation.\nReason: {$exception->getMessage()}" ); } From b94fb2044e3e391ab979b3235b59f8bd7ee5ddd1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 18:09:43 +0000 Subject: [PATCH 5/8] Harden transformer value checks and failure cleanup Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Config_Command.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Config_Command.php b/src/Config_Command.php index d032ecb1..621d4a09 100644 --- a/src/Config_Command.php +++ b/src/Config_Command.php @@ -389,7 +389,7 @@ public function create( $_, $assoc_args ) { continue; } - if ( false !== strpos( $assoc_args[ $arg_name ], '\\' ) ) { + if ( is_string( $provided_assoc_args[ $arg_name ] ) && false !== strpos( $provided_assoc_args[ $arg_name ], '\\' ) ) { // Backslashes need the same remove+add path used by `wp config set` to preserve escaping. $config_transformer->remove( $entry['type'], $entry['name'] ); $config_transformer->add( @@ -426,10 +426,13 @@ public function create( $_, $assoc_args ) { ]; 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 ( Exception $exception ) { + } catch ( Throwable $exception ) { if ( file_exists( $assoc_args['config-file'] ) ) { unlink( $assoc_args['config-file'] ); } From 7e47a5bdd4d4e083a507e90f410e341b3bcf426b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 18:10:20 +0000 Subject: [PATCH 6/8] Use original provided values in transformer writes Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Config_Command.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Config_Command.php b/src/Config_Command.php index 621d4a09..ddbb2add 100644 --- a/src/Config_Command.php +++ b/src/Config_Command.php @@ -395,7 +395,7 @@ public function create( $_, $assoc_args ) { $config_transformer->add( $entry['type'], $entry['name'], - $assoc_args[ $arg_name ], + $provided_assoc_args[ $arg_name ], [ 'anchor' => $entry['anchor'], 'placement' => 'before', @@ -407,7 +407,7 @@ public function create( $_, $assoc_args ) { $config_transformer->update( $entry['type'], $entry['name'], - $assoc_args[ $arg_name ], + $provided_assoc_args[ $arg_name ], [ 'add' => true ] ); } From 419ad65f83ededd9eaa2adda1ed87f691009dd4d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 18:11:08 +0000 Subject: [PATCH 7/8] Improve cleanup failure reporting in config create Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Config_Command.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Config_Command.php b/src/Config_Command.php index ddbb2add..66ac83bc 100644 --- a/src/Config_Command.php +++ b/src/Config_Command.php @@ -390,7 +390,7 @@ public function create( $_, $assoc_args ) { } if ( is_string( $provided_assoc_args[ $arg_name ] ) && false !== strpos( $provided_assoc_args[ $arg_name ], '\\' ) ) { - // Backslashes need the same remove+add path used by `wp config set` to preserve escaping. + // Use remove+add to preserve backslash escaping when writing with WPConfigTransformer. $config_transformer->remove( $entry['type'], $entry['name'] ); $config_transformer->add( $entry['type'], @@ -433,10 +433,13 @@ public function create( $_, $assoc_args ) { } } } catch ( Throwable $exception ) { + $cleanup_error = ''; if ( file_exists( $assoc_args['config-file'] ) ) { - unlink( $assoc_args['config-file'] ); + if ( ! unlink( $assoc_args['config-file'] ) ) { + $cleanup_error = "\nCleanup: Could not remove '{$wp_config_file_name}' after failure."; + } } - WP_CLI::error( "Could not process the '{$wp_config_file_name}' transformation.\nReason: {$exception->getMessage()}" ); + 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." ); From 59c62aeaa4933e955c53ed909d4985a12da8e907 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 May 2026 18:08:55 +0000 Subject: [PATCH 8/8] Address PR review feedback in config create Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Config_Command.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Config_Command.php b/src/Config_Command.php index 66ac83bc..45447002 100644 --- a/src/Config_Command.php +++ b/src/Config_Command.php @@ -338,6 +338,7 @@ public function create( $_, $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." ); @@ -434,7 +435,7 @@ public function create( $_, $assoc_args ) { } } catch ( Throwable $exception ) { $cleanup_error = ''; - if ( file_exists( $assoc_args['config-file'] ) ) { + 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."; }