<?php

use WP_CLI\ExitException;
use WP_CLI\Utils;
use WP_CLI\WpOrgApi;

/**
 * Generates and reads the wp-config.php file.
 *
 * ## EXAMPLES
 *
 *     # Create standard wp-config.php file.
 *     $ wp config create --dbname=testing --dbuser=wp --dbpass=securepswd --locale=ro_RO
 *     Success: Generated 'wp-config.php' file.
 *
 *     # List constants and variables defined in wp-config.php file.
 *     $ wp config list
 *     +------------------+------------------------------------------------------------------+----------+
 *     | key              | value                                                            | type     |
 *     +------------------+------------------------------------------------------------------+----------+
 *     | table_prefix     | wp_                                                              | variable |
 *     | DB_NAME          | wp_cli_test                                                      | constant |
 *     | DB_USER          | root                                                             | constant |
 *     | DB_PASSWORD      | root                                                             | constant |
 *     | AUTH_KEY         | r6+@shP1yO&$)1gdu.hl[/j;7Zrvmt~o;#WxSsa0mlQOi24j2cR,7i+QM/#7S:o^ | constant |
 *     | SECURE_AUTH_KEY  | iO-z!_m--YH$Tx2tf/&V,YW*13Z_HiRLqi)d?$o-tMdY+82pK$`T.NYW~iTLW;xp | constant |
 *     +------------------+------------------------------------------------------------------+----------+
 *
 *     # Get wp-config.php file path.
 *     $ wp config path
 *     /home/person/htdocs/project/wp-config.php
 *
 *     # Get the table_prefix as defined in wp-config.php file.
 *     $ wp config get table_prefix
 *     wp_
 *
 *     # Set the WP_DEBUG constant to true.
 *     $ wp config set WP_DEBUG true --raw
 *     Success: Updated the constant 'WP_DEBUG' in the 'wp-config.php' file with the raw value 'true'.
 *
 *     # Delete the COOKIE_DOMAIN constant from the wp-config.php file.
 *     $ wp config delete COOKIE_DOMAIN
 *     Success: Deleted the constant 'COOKIE_DOMAIN' from the 'wp-config.php' file.
 *
 *     # Launch system editor to edit wp-config.php file.
 *     $ wp config edit
 *
 *     # Check whether the DB_PASSWORD constant exists in the wp-config.php file.
 *     $ wp config has DB_PASSWORD
 *     $ echo $?
 *     0
 *
 *     # Assert if MULTISITE is true.
 *     $ wp config is-true MULTISITE
 *     $ echo $?
 *     0
 *
 *     # Get new salts for your wp-config.php file.
 *     $ wp config shuffle-salts
 *     Success: Shuffled the salt keys.
 *
 * @package wp-cli
 */
class Config_Command extends WP_CLI_Command {

	/**
	 * List of characters that are valid for a key name.
	 *
	 * @var string
	 */
	const VALID_KEY_CHARACTERS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|';

	/**
	 * List of default constants that are generated by WordPress Core.
	 *
	 * @var string[]
	 */
	const DEFAULT_SALT_CONSTANTS = [
		'AUTH_KEY',
		'SECURE_AUTH_KEY',
		'LOGGED_IN_KEY',
		'NONCE_KEY',
		'AUTH_SALT',
		'SECURE_AUTH_SALT',
		'LOGGED_IN_SALT',
		'NONCE_SALT',
	];

	/**
	 * Retrieve the initial locale from the WordPress version file.
	 *
	 * @return string Initial locale if present, or an empty string if not.
	 */
	private static function get_initial_locale() {
		global $wp_local_package;

		include ABSPATH . '/wp-includes/version.php';

		if ( ! empty( $wp_local_package ) ) {
			return $wp_local_package;
		}

		return '';
	}

	/**
	 * Generates a wp-config.php file.
	 *
	 * Creates a new wp-config.php with database constants, and verifies that
	 * the database constants are correct.
	 *
	 * ## OPTIONS
	 *
	 * --dbname=<dbname>
	 * : Set the database name.
	 *
	 * --dbuser=<dbuser>
	 * : Set the database user.
	 *
	 * [--dbpass=<dbpass>]
	 * : Set the database user password.
	 *
	 * [--dbhost=<dbhost>]
	 * : Set the database host.
	 * ---
	 * default: localhost
	 * ---
	 *
	 * [--dbprefix=<dbprefix>]
	 * : Set the database table prefix.
	 * ---
	 * default: wp_
	 * ---
	 *
	 * [--dbcharset=<dbcharset>]
	 * : Set the database charset.
	 * ---
	 * default: utf8
	 * ---
	 *
	 * [--dbcollate=<dbcollate>]
	 * : Set the database collation.
	 * ---
	 * default:
	 * ---
	 *
	 * [--locale=<locale>]
	 * : Set the WPLANG constant. Defaults to $wp_local_package variable.
	 *
	 * [--extra-php]
	 * : If set, the command copies additional PHP code into wp-config.php from STDIN.
	 *
	 * [--skip-salts]
	 * : If set, keys and salts won't be generated, but should instead be passed via `--extra-php`.
	 *
	 * [--skip-check]
	 * : If set, the database connection is not checked.
	 *
	 * [--force]
	 * : Overwrites existing files, if present.
	 *
	 * [--config-file=<path>]
	 * : Specify the file path to the config file to be created. Defaults to the root of the
	 * WordPress installation and the filename "wp-config.php".
	 *
	 * [--insecure]
	 * : Retry API download without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack.
	 *
	 * [--ssl]
	 * : Use SSL when checking the database connection.
	 *
	 * ## EXAMPLES
	 *
	 *     # Standard wp-config.php file
	 *     $ wp config create --dbname=testing --dbuser=wp --dbpass=securepswd --locale=ro_RO
	 *     Success: Generated 'wp-config.php' file.
	 *
	 *     # Enable WP_DEBUG and WP_DEBUG_LOG
	 *     $ wp config create --dbname=testing --dbuser=wp --dbpass=securepswd --extra-php <<PHP
	 *     define( 'WP_DEBUG', true );
	 *     define( 'WP_DEBUG_LOG', true );
	 *     PHP
	 *     Success: Generated 'wp-config.php' file.
	 *
	 *     # Avoid disclosing password to bash history by reading from password.txt
	 *     # Using --prompt=dbpass will prompt for the 'dbpass' argument
	 *     $ wp config create --dbname=testing --dbuser=wp --prompt=dbpass < password.txt
	 *     Success: Generated 'wp-config.php' file.
	 */
	public function create( $_, $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'] ) );
			} elseif ( ! isset( $assoc_args['config-file'] ) && Utils\locate_wp_config() ) {
				$this->config_file_already_exist_error( 'wp-config.php' );
			}
		}

		$defaults   = [
			'dbhost'      => 'localhost',
			'dbpass'      => '',
			'dbprefix'    => 'wp_',
			'dbcharset'   => 'utf8',
			'dbcollate'   => '',
			'locale'      => self::get_initial_locale(),
			'config-file' => rtrim( ABSPATH, '/\\' ) . '/wp-config.php',
			'ssl'         => false,
		];
		$assoc_args = array_merge( $defaults, $assoc_args );
		if ( empty( $assoc_args['dbprefix'] ) ) {
			WP_CLI::error( '--dbprefix cannot be empty' );
		}
		if ( preg_match( '|[^a-z0-9_]|i', $assoc_args['dbprefix'] ) ) {
			WP_CLI::error( '--dbprefix can only contain numbers, letters, and underscores.' );
		}

		// Check DB connection. To make command more portable, we are not using MySQL CLI and using
		// mysqli directly instead, as $wpdb is not accessible in this context.
		if ( ! Utils\get_flag_value( $assoc_args, 'skip-check' ) ) {
			// phpcs:disable WordPress.DB.RestrictedFunctions
			$mysql = mysqli_init();

			if ( ! $mysql ) {
				WP_CLI::error( 'Database connection error. Could not initialize MySQLi.' );
			}

			mysqli_report( MYSQLI_REPORT_STRICT );
			try {
				// Accept similar format to one used by parse_db_host() e.g. 'localhost:/tmp/mysql.sock'
				$socket     = '';
				$host       = $assoc_args['dbhost'];
				$socket_pos = strpos( $host, ':/' );
				if ( false !== $socket_pos ) {
					$socket = substr( $host, $socket_pos + 1 );
					$host   = substr( $host, 0, $socket_pos );
				}

				$flags = 0;

				if ( $assoc_args['ssl'] ) {
					$flags = MYSQLI_CLIENT_SSL;
				}

				if ( file_exists( $socket ) ) {
					// If dbhost is a path to a socket
					mysqli_real_connect( $mysql, null, $assoc_args['dbuser'], $assoc_args['dbpass'], null, null, $socket, $flags );
				} else {
					// If dbhost is a hostname or IP address
					mysqli_real_connect( $mysql, $host, $assoc_args['dbuser'], $assoc_args['dbpass'], null, null, null, $flags );
				}
			} catch ( mysqli_sql_exception $exception ) {
				WP_CLI::error( 'Database connection error (' . $exception->getCode() . ') ' . $exception->getMessage() );
			}
			// phpcs:enable WordPress.DB.RestrictedFunctions
		}

		if ( ! Utils\get_flag_value( $assoc_args, 'skip-salts' ) ) {
			try {
				$assoc_args['keys-and-salts']    = true;
				$assoc_args['auth-key']          = self::unique_key();
				$assoc_args['secure-auth-key']   = self::unique_key();
				$assoc_args['logged-in-key']     = self::unique_key();
				$assoc_args['nonce-key']         = self::unique_key();
				$assoc_args['auth-salt']         = self::unique_key();
				$assoc_args['secure-auth-salt']  = self::unique_key();
				$assoc_args['logged-in-salt']    = self::unique_key();
				$assoc_args['nonce-salt']        = self::unique_key();
				$assoc_args['wp-cache-key-salt'] = self::unique_key();
			} catch ( Exception $e ) {
				$assoc_args['keys-and-salts']     = false;
				$assoc_args['keys-and-salts-alt'] = self::fetch_remote_salts(
					(bool) Utils\get_flag_value( $assoc_args, 'insecure', false )
				);
			}
		}

		foreach ( $assoc_args as $key => $value ) {
			$assoc_args[ $key ] = $this->escape_config_value( $key, $value );
		}

		// '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' );
		}

		$command_root = Utils\phar_safe_path( dirname( __DIR__ ) );
		$out          = Utils\mustache_render( "{$command_root}/templates/wp-config.mustache", $assoc_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." );
		}
	}

	/**
	 * Gives error when wp-config already exist and try to create it.
	 *
	 * @param string $wp_config_file_name Config file name.
	 * @return void
	 */
	private function config_file_already_exist_error( $wp_config_file_name ) {
		WP_CLI::error( "The '{$wp_config_file_name}' file already exists." );
	}

	/**
	 * Launches system editor to edit the wp-config.php file.
	 *
	 * ## OPTIONS
	 *
	 * [--config-file=<path>]
	 * : Specify the file path to the config file to be edited. Defaults to the root of the
	 * WordPress installation and the filename "wp-config.php".
	 *
	 * ## EXAMPLES
	 *
	 *     # Launch system editor to edit wp-config.php file
	 *     $ wp config edit
	 *
	 *     # Edit wp-config.php file in a specific editor
	 *     $ EDITOR=vim wp config edit
	 *
	 * @when before_wp_load
	 */
	public function edit( $_, $assoc_args ) {
		$path                = $this->get_config_path( $assoc_args );
		$wp_config_file_name = basename( $path );
		$contents            = (string) file_get_contents( $path );
		$r                   = Utils\launch_editor_for_input( $contents, $wp_config_file_name, 'php' );
		if ( false === $r ) {
			WP_CLI::warning( "No changes made to {$wp_config_file_name}, aborted." );
		} else {
			file_put_contents( $path, $r );
		}
	}

	/**
	 * Gets the path to wp-config.php file.
	 *
	 * ## EXAMPLES
	 *
	 *     # Get wp-config.php file path
	 *     $ wp config path
	 *     /home/person/htdocs/project/wp-config.php
	 *
	 * @when before_wp_load
	 */
	public function path() {
		WP_CLI::line( $this->get_config_path( array() ) );
	}

	/**
	 * Lists variables, constants, and file includes defined in wp-config.php file.
	 *
	 * ## OPTIONS
	 *
	 * [<filter>...]
	 * : Name or partial name to filter the list by.
	 *
	 * [--fields=<fields>]
	 * : Limit the output to specific fields. Defaults to all fields.
	 *
	 * [--format=<format>]
	 * : Render output in a particular format.
	 * Dotenv is limited to non-object values.
	 * ---
	 * default: table
	 * options:
	 *   - table
	 *   - csv
	 *   - json
	 *   - yaml
	 *   - dotenv
	 * ---
	 *
	 * [--strict]
	 * : Enforce strict matching when a filter is provided.
	 *
	 * [--config-file=<path>]
	 * : Specify the file path to the config file to be read. Defaults to the root of the
	 * WordPress installation and the filename "wp-config.php".
	 *
	 * ## EXAMPLES
	 *
	 *     # List constants and variables defined in wp-config.php file.
	 *     $ wp config list
	 *     +------------------+------------------------------------------------------------------+----------+
	 *     | key              | value                                                            | type     |
	 *     +------------------+------------------------------------------------------------------+----------+
	 *     | table_prefix     | wp_                                                              | variable |
	 *     | DB_NAME          | wp_cli_test                                                      | constant |
	 *     | DB_USER          | root                                                             | constant |
	 *     | DB_PASSWORD      | root                                                             | constant |
	 *     | AUTH_KEY         | r6+@shP1yO&$)1gdu.hl[/j;7Zrvmt~o;#WxSsa0mlQOi24j2cR,7i+QM/#7S:o^ | constant |
	 *     | SECURE_AUTH_KEY  | iO-z!_m--YH$Tx2tf/&V,YW*13Z_HiRLqi)d?$o-tMdY+82pK$`T.NYW~iTLW;xp | constant |
	 *     +------------------+------------------------------------------------------------------+----------+
	 *
	 *     # List only database user and password from wp-config.php file.
	 *     $ wp config list DB_USER DB_PASSWORD --strict
	 *     +------------------+-------+----------+
	 *     | key              | value | type     |
	 *     +------------------+-------+----------+
	 *     | DB_USER          | root  | constant |
	 *     | DB_PASSWORD      | root  | constant |
	 *     +------------------+-------+----------+
	 *
	 *     # List all salts from wp-config.php file.
	 *     $ wp config list _SALT
	 *     +------------------+------------------------------------------------------------------+----------+
	 *     | key              | value                                                            | type     |
	 *     +------------------+------------------------------------------------------------------+----------+
	 *     | AUTH_SALT        | n:]Xditk+_7>Qi=>BmtZHiH-6/Ecrvl(V5ceeGP:{>?;BT^=[B3-0>,~F5z$(+Q$ | constant |
	 *     | SECURE_AUTH_SALT | ?Z/p|XhDw3w}?c.z%|+BAr|(Iv*H%%U+Du&kKR y?cJOYyRVRBeB[2zF-`(>+LCC | constant |
	 *     | LOGGED_IN_SALT   | +$@(1{b~Z~s}Cs>8Y]6[m6~TnoCDpE>O%e75u}&6kUH!>q:7uM4lxbB6[1pa_X,q | constant |
	 *     | NONCE_SALT       | _x+F li|QL?0OSQns1_JZ{|Ix3Jleox-71km/gifnyz8kmo=w-;@AE8W,(fP<N}2 | constant |
	 *     +------------------+------------------------------------------------------------------+----------+
	 *
	 * @when before_wp_load
	 * @subcommand list
	 */
	public function list_( $args, $assoc_args ) {
		$path                = $this->get_config_path( $assoc_args );
		$wp_config_file_name = basename( $path );
		$strict              = (bool) Utils\get_flag_value( $assoc_args, 'strict' );
		if ( $strict && empty( $args ) ) {
			WP_CLI::error( 'The --strict option can only be used in combination with a filter.' );
		}

		$default_fields = [
			'name',
			'value',
			'type',
		];

		$defaults = [
			'fields' => implode( ',', $default_fields ),
			'format' => 'table',
		];

		$assoc_args = array_merge( $defaults, $assoc_args );

		$values = self::get_wp_config_vars( $path );

		if ( ! empty( $args ) ) {
			$values = $this->filter_values( $values, $args, $strict );
		}

		if ( empty( $values ) ) {
			WP_CLI::error( "No matching entries found in '{$wp_config_file_name}'." );
		}

		if ( 'dotenv' === $assoc_args['format'] ) {
			return array_walk( $values, array( $this, 'print_dotenv' ) );
		}

		Utils\format_items( $assoc_args['format'], $values, $assoc_args['fields'] );
	}

	/**
	 * Gets the value of a specific constant or variable defined in wp-config.php file.
	 *
	 * ## OPTIONS
	 *
	 * <name>
	 * : Name of the wp-config.php constant or variable.
	 *
	 * [--type=<type>]
	 * : Type of config value to retrieve. Defaults to 'all'.
	 * ---
	 * default: all
	 * options:
	 *   - constant
	 *   - variable
	 *   - all
	 * ---
	 *
	 * [--format=<format>]
	 * : Get value in a particular format.
	 * Dotenv is limited to non-object values.
	 * ---
	 * default: var_export
	 * options:
	 *   - var_export
	 *   - json
	 *   - yaml
	 *   - dotenv
	 * ---
	 *
	 * [--config-file=<path>]
	 * : Specify the file path to the config file to be read. Defaults to the root of the
	 * WordPress installation and the filename "wp-config.php".
	 *
	 * ## EXAMPLES
	 *
	 *     # Get the table_prefix as defined in wp-config.php file.
	 *     $ wp config get table_prefix
	 *     wp_
	 *
	 * @when before_wp_load
	 */
	public function get( $args, $assoc_args ) {
		$value = $this->get_value( $assoc_args, $args );
		WP_CLI::print_value( $value, $assoc_args );
	}

	/**
	 * Determines whether value of a specific defined constant or variable is truthy.
	 *
	 * This determination is made by evaluating the retrieved value via boolval().
	 *
	 * ## OPTIONS
	 *
	 * <name>
	 * : Name of the wp-config.php constant or variable.
	 *
	 * [--type=<type>]
	 * : Type of config value to retrieve. Defaults to 'all'.
	 * ---
	 * default: all
	 * options:
	 *   - constant
	 *   - variable
	 *   - all
	 * ---
	 *
	 * [--config-file=<path>]
	 * : Specify the file path to the config file to be read. Defaults to the root of the
	 * WordPress installation and the filename "wp-config.php".
	 *
	 * ## EXAMPLES
	 *
	 *     # Assert if MULTISITE is true
	 *     $ wp config is-true MULTISITE
	 *     $ echo $?
	 *     0
	 *
	 * @subcommand is-true
	 * @when before_wp_load
	 */
	public function is_true( $args, $assoc_args ) {
		$value = $this->get_value( $assoc_args, $args );

		if ( boolval( $value ) ) {
			WP_CLI::halt( 0 );
		}
		WP_CLI::halt( 1 );
	}

	/**
	 * Get the array of wp-config.php constants and variables.
	 *
	 * @param string $wp_config_path Config file path
	 *
	 * @return array
	 */
	private static function get_wp_config_vars( $wp_config_path = '' ) {
		$wp_cli_original_defined_constants = get_defined_constants();
		$wp_cli_original_defined_vars      = get_defined_vars();
		$wp_cli_original_includes          = get_included_files();

		// Output buffering prevents warnings or notices while parsing wp-config
		// from printing twice. See https://github.com/wp-cli/wp-cli/issues/4944
		ob_start();
		// phpcs:ignore Squiz.PHP.Eval.Discouraged -- Don't have another way.
		eval( WP_CLI::get_runner()->get_wp_config_code( $wp_config_path ) );
		ob_end_clean();

		$wp_config_vars      = self::get_wp_config_diff( get_defined_vars(), $wp_cli_original_defined_vars, 'variable', [ 'wp_cli_original_defined_vars' ] );
		$wp_config_constants = self::get_wp_config_diff( get_defined_constants(), $wp_cli_original_defined_constants, 'constant' );

		foreach ( $wp_config_vars as $name => $value ) {
			if ( 'wp_cli_original_includes' === $value['name'] ) {
				$name_backup = $name;
				break;
			}
		}

		if ( isset( $name_backup ) ) {
			unset( $wp_config_vars[ $name_backup ] );
		}
		$wp_config_vars           = array_values( $wp_config_vars );
		$wp_config_includes       = array_diff( get_included_files(), $wp_cli_original_includes );
		$wp_config_includes_array = [];

		foreach ( $wp_config_includes as $name => $value ) {
			$wp_config_includes_array[] = [
				'name'  => basename( $value ),
				'value' => $value,
				'type'  => 'includes',
			];
		}

		return array_merge( $wp_config_vars, $wp_config_constants, $wp_config_includes_array );
	}

	/**
	 * Sets the value of a specific constant or variable defined in wp-config.php file.
	 *
	 * ## OPTIONS
	 *
	 * <name>
	 * : Name of the wp-config.php constant or variable.
	 *
	 * <value>
	 * : Value to set the wp-config.php constant or variable to.
	 *
	 * [--add]
	 * : Add the value if it doesn't exist yet.
	 * This is the default behavior, override with --no-add.
	 *
	 * [--raw]
	 * : Place the value into the wp-config.php file as is, instead of as a quoted string.
	 *
	 * [--anchor=<anchor>]
	 * : Anchor string where additions of new values are anchored around.
	 * Defaults to "/* That's all, stop editing!".
	 * The special case "EOF" string uses the end of the file as the anchor.
	 *
	 * [--placement=<placement>]
	 * : Where to place the new values in relation to the anchor string.
	 * ---
	 * default: 'before'
	 * options:
	 *   - before
	 *   - after
	 * ---
	 *
	 * [--separator=<separator>]
	 * : Separator string to put between an added value and its anchor string.
	 * The following escape sequences will be recognized and properly interpreted: '\n' => newline, '\r' => carriage return, '\t' => tab.
	 * Defaults to a single EOL ("\n" on *nix and "\r\n" on Windows).
	 *
	 * [--type=<type>]
	 * : Type of the config value to set. Defaults to 'all'.
	 * ---
	 * default: all
	 * options:
	 *   - constant
	 *   - variable
	 *   - all
	 * ---
	 *
	 * [--config-file=<path>]
	 * : Specify the file path to the config file to be modified. Defaults to the root of the
	 * WordPress installation and the filename "wp-config.php".
	 *
	 * ## EXAMPLES
	 *
	 *     # Set the WP_DEBUG constant to true.
	 *     $ wp config set WP_DEBUG true --raw
	 *     Success: Updated the constant 'WP_DEBUG' in the 'wp-config.php' file with the raw value 'true'.
	 *
	 * @when before_wp_load
	 */
	public function set( $args, $assoc_args ) {
		$path                 = $this->get_config_path( $assoc_args );
		$wp_config_file_name  = basename( $path );
		list( $name, $value ) = $args;

		/**
		 * @var string $type
		 */
		$type = Utils\get_flag_value( $assoc_args, 'type' );

		$options = [];

		$option_flags = [
			'raw'       => false,
			'add'       => true,
			'anchor'    => null,
			'placement' => null,
			'separator' => null,
		];

		foreach ( $option_flags as $option => $default ) {
			/**
			 * @var string|null $option_value
			 */
			$option_value = Utils\get_flag_value( $assoc_args, $option, $default );
			if ( null !== $option_value ) {
				/**
				 * @var array<string, string> $options
				 */
				$options[ $option ] = $option_value;
				if ( 'separator' === $option ) {
					$options['separator'] = $this->parse_separator( $options['separator'] );
				}
			}
		}

		$adding = false;
		try {
			$config_transformer = new WPConfigTransformer( $path );

			switch ( $type ) {
				case 'all':
					$has_constant = $config_transformer->exists( 'constant', $name );
					$has_variable = $config_transformer->exists( 'variable', $name );
					if ( $has_constant && $has_variable ) {
						WP_CLI::error( "Found both a constant and a variable '{$name}' in the '{$wp_config_file_name}' file. Use --type=<type> to disambiguate." );
					}
					if ( ! $has_constant && ! $has_variable ) {
						if ( ! $options['add'] ) {
							$message = "The constant or variable '{$name}' is not defined in the '{$wp_config_file_name}' file.";
							WP_CLI::error( $message );
						}
						$type   = 'constant';
						$adding = true;
					} else {
						$type = $has_constant ? 'constant' : 'variable';
					}
					break;
				case 'constant':
				case 'variable':
					if ( ! $config_transformer->exists( $type, $name ) ) {
						if ( ! $options['add'] ) {
							WP_CLI::error( "The {$type} '{$name}' is not defined in the '{$wp_config_file_name}' file." );
						}
						$adding = true;
					}
			}

			$config_transformer->update( $type, $name, $value, $options );

		} catch ( Exception $exception ) {
			WP_CLI::error( "Could not process the '{$wp_config_file_name}' transformation.\nReason: {$exception->getMessage()}" );
		}

		$raw = $options['raw'] ? 'raw ' : '';
		if ( $adding ) {
			$message = "Added the {$type} '{$name}' to the '{$wp_config_file_name}' file with the {$raw}value '{$value}'.";
		} else {
			$message = "Updated the {$type} '{$name}' in the '{$wp_config_file_name}' file with the {$raw}value '{$value}'.";
		}

		WP_CLI::success( $message );
	}

	/**
	 * Deletes a specific constant or variable from the wp-config.php file.
	 *
	 * ## OPTIONS
	 *
	 * <name>
	 * : Name of the wp-config.php constant or variable.
	 *
	 * [--type=<type>]
	 * : Type of the config value to delete. Defaults to 'all'.
	 * ---
	 * default: all
	 * options:
	 *   - constant
	 *   - variable
	 *   - all
	 * ---
	 *
	 * [--config-file=<path>]
	 * : Specify the file path to the config file to be modified. Defaults to the root of the
	 * WordPress installation and the filename "wp-config.php".
	 *
	 * ## EXAMPLES
	 *
	 *     # Delete the COOKIE_DOMAIN constant from the wp-config.php file.
	 *     $ wp config delete COOKIE_DOMAIN
	 *     Success: Deleted the constant 'COOKIE_DOMAIN' from the 'wp-config.php' file.
	 *
	 * @when before_wp_load
	 */
	public function delete( $args, $assoc_args ) {
		$path                = $this->get_config_path( $assoc_args );
		$wp_config_file_name = basename( $path );
		list( $name )        = $args;

		/**
		 * @var string $type
		 */
		$type = Utils\get_flag_value( $assoc_args, 'type' );

		try {
			$config_transformer = new WPConfigTransformer( $path );

			switch ( $type ) {
				case 'all':
					$has_constant = $config_transformer->exists( 'constant', $name );
					$has_variable = $config_transformer->exists( 'variable', $name );
					if ( $has_constant && $has_variable ) {
						WP_CLI::error( "Found both a constant and a variable '{$name}' in the '{$wp_config_file_name}' file. Use --type=<type> to disambiguate." );
					}
					if ( ! $has_constant && ! $has_variable ) {
						WP_CLI::error( "The constant or variable '{$name}' is not defined in the '{$wp_config_file_name}' file." );
					} else {
						$type = $has_constant ? 'constant' : 'variable';
					}
					break;
				case 'constant':
				case 'variable':
					if ( ! $config_transformer->exists( $type, $name ) ) {
						WP_CLI::error( "The {$type} '{$name}' is not defined in the '{$wp_config_file_name}' file." );
					}
			}

			$config_transformer->remove( $type, $name );

		} catch ( Exception $exception ) {
			WP_CLI::error( "Could not process the '{$wp_config_file_name}' transformation.\nReason: {$exception->getMessage()}" );
		}

		WP_CLI::success( "Deleted the {$type} '{$name}' from the '{$wp_config_file_name}' file." );
	}

	/**
	 * Checks whether a specific constant or variable exists in the wp-config.php file.
	 *
	 * ## OPTIONS
	 *
	 * <name>
	 * : Name of the wp-config.php constant or variable.
	 *
	 * [--type=<type>]
	 * : Type of the config value to set. Defaults to 'all'.
	 * ---
	 * default: all
	 * options:
	 *   - constant
	 *   - variable
	 *   - all
	 * ---
	 *
	 * [--config-file=<path>]
	 * : Specify the file path to the config file to be checked. Defaults to the root of the
	 * WordPress installation and the filename "wp-config.php".
	 *
	 * ## EXAMPLES
	 *
	 *     # Check whether the DB_PASSWORD constant exists in the wp-config.php file.
	 *     $ wp config has DB_PASSWORD
	 *
	 * @when before_wp_load
	 */
	public function has( $args, $assoc_args ) {
		$path                = $this->get_config_path( $assoc_args );
		$wp_config_file_name = basename( $path );
		list( $name )        = $args;

		/**
		 * @var string $type
		 */
		$type = Utils\get_flag_value( $assoc_args, 'type' );

		try {
			$config_transformer = new WPConfigTransformer( $path, true );

			switch ( $type ) {
				case 'all':
					$has_constant = $config_transformer->exists( 'constant', $name );
					$has_variable = $config_transformer->exists( 'variable', $name );
					if ( $has_constant && $has_variable ) {
						WP_CLI::error( "Found both a constant and a variable '{$name}' in the '{$wp_config_file_name}' file. Use --type=<type> to disambiguate." );
					}
					if ( ! $has_constant && ! $has_variable ) {
						WP_CLI::halt( 1 );
					} else {
						WP_CLI::halt( 0 );
					}
					// @phpstan-ignore deadCode.unreachable
					break;
				case 'constant':
				case 'variable':
					if ( ! $config_transformer->exists( $type, $name ) ) {
						WP_CLI::halt( 1 );
					}
					WP_CLI::halt( 0 );
			}
		} catch ( Exception $exception ) {
			WP_CLI::error( "Could not process the '{$wp_config_file_name}' transformation.\nReason: {$exception->getMessage()}" );
		}
	}

	/**
	 * Refreshes the salts defined in the wp-config.php file.
	 *
	 * ## OPTIONS
	 *
	 * [<keys>...]
	 * : One ore more keys to shuffle. If none are provided, this falls back to the default WordPress Core salt keys.
	 *
	 * [--force]
	 * : If an unknown key is requested to be shuffled, add it instead of throwing a warning.
	 *
	 * [--config-file=<path>]
	 * : Specify the file path to the config file to be modified. Defaults to the root of the
	 * WordPress installation and the filename "wp-config.php".
	 *
	 * [--insecure]
	 * : Retry API download without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack.
	 *
	 * ## EXAMPLES
	 *
	 *     # Get new salts for your wp-config.php file
	 *     $ wp config shuffle-salts
	 *     Success: Shuffled the salt keys.
	 *
	 *     # Add a cache key salt to the wp-config.php file
	 *     $ wp config shuffle-salts WP_CACHE_KEY_SALT --force
	 *     Success: Shuffled the salt keys.
	 *
	 * @subcommand shuffle-salts
	 * @when before_wp_load
	 */
	public function shuffle_salts( $args, $assoc_args ) {
		$keys  = $args;
		$force = Utils\get_flag_value( $assoc_args, 'force', false );

		$has_keys = ( ! empty( $keys ) ) ? true : false;

		if ( empty( $keys ) ) {
			$keys = self::DEFAULT_SALT_CONSTANTS;
		}

		$successes = 0;
		$errors    = 0;
		$skipped   = 0;

		$secret_keys = [];

		try {
			foreach ( $keys as $key ) {
				$unique_key = self::unique_key();
				if ( ! $force && ! in_array( $key, self::DEFAULT_SALT_CONSTANTS, true ) ) {
					WP_CLI::warning( "Could not shuffle the unknown key '{$key}'." );
					++$skipped;
					continue;
				}
				$secret_keys[ $key ] = trim( $unique_key );
			}
		} catch ( Exception $ex ) {
			foreach ( $keys as $key ) {
				if ( ! in_array( $key, self::DEFAULT_SALT_CONSTANTS, true ) ) {
					if ( $force ) {
						WP_CLI::warning( "Could not add the key '{$key}' because 'random_int()' is not supported." );
					} else {
						WP_CLI::warning( "Could not shuffle the unknown key '{$key}'." );
					}
					++$skipped;
				}
			}

			$remote_salts = self::fetch_remote_salts( (bool) Utils\get_flag_value( $assoc_args, 'insecure', false ) );
			$remote_salts = explode( "\n", $remote_salts );
			foreach ( $remote_salts as $k => $salt ) {
				if ( ! empty( $salt ) ) {
					$key = self::DEFAULT_SALT_CONSTANTS[ $k ];
					if ( in_array( $key, $keys, true ) ) {
						$secret_keys[ $key ] = trim( substr( $salt, 28, 64 ) );
					}
				}
			}
		}

		$path = $this->get_config_path( $assoc_args );

		try {
			$config_transformer = new WPConfigTransformer( $path );
			foreach ( $secret_keys as $key => $value ) {
				$is_updated = $config_transformer->update( 'constant', $key, (string) $value );
				if ( $is_updated ) {
					++$successes;
				} else {
					++$errors;
				}
			}
		} catch ( Exception $exception ) {
			$wp_config_file_name = basename( $path );
			WP_CLI::error( "Could not process the '{$wp_config_file_name}' transformation.\nReason: {$exception->getMessage()}" );
		}

		if ( $has_keys ) {
			Utils\report_batch_operation_results( 'salt', 'shuffle', count( $keys ), $successes, $errors, $skipped );
		} else {
			WP_CLI::success( 'Shuffled the salt keys.' );
		}
	}

	/**
	 * Filters wp-config.php file configurations.
	 *
	 * @param array $vars
	 * @param array $previous_list
	 * @param string $type
	 * @param array $exclude_list
	 * @return array
	 */
	private static function get_wp_config_diff( $vars, $previous_list, $type, $exclude_list = [] ) {
		$result = [];
		foreach ( $vars as $name => $val ) {
			if ( array_key_exists( $name, $previous_list ) || in_array( $name, $exclude_list, true ) ) {
				continue;
			}
			$out          = [];
			$out['name']  = $name;
			$out['value'] = $val;
			$out['type']  = $type;
			$result[]     = $out;
		}
		return $result;
	}

	/**
	 * Read the salts from the WordPress.org API.
	 *
	 * @param bool   $insecure Optional. Whether to retry without certificate validation on TLS handshake failure.
	 * @return string String with a set of PHP define() statements to define the salts.
	 * @throws ExitException If the remote request failed.
	 */
	private static function fetch_remote_salts( $insecure = false ) {
		try {
			$salts = (string) ( new WpOrgApi( [ 'insecure' => $insecure ] ) )->get_salts();
		} catch ( Exception $exception ) {
			WP_CLI::error( $exception );
		}

		// Adapt whitespace to adhere to WPCS.
		$salts = (string) preg_replace( '/define\(\'(.*?)\'\);/', 'define( \'$1\' );', $salts );

		return $salts;
	}

	/**
	 * Prints the value of a constant or variable defined in the wp-config.php file.
	 *
	 * If the constant or variable is not defined in the wp-config file then an error will be returned.
	 *
	 * @param string $name
	 * @param string $type
	 * @param string $type
	 * @param string $wp_config_file_name Config file name
	 * @return string The value of the requested constant or variable as defined in the wp-config.php file; if the
	 *                requested constant or variable is not defined then the function will print an error and exit.
	 */
	private function return_value( $name, $type, $values, $wp_config_file_name ) {
		$results = [];
		foreach ( $values as $value ) {
			if ( $name === $value['name'] && ( 'all' === $type || $type === $value['type'] ) ) {
				$results[] = $value;
			}
		}

		if ( count( $results ) > 1 ) {
			WP_CLI::error( "Found both a constant and a variable '{$name}' in the '{$wp_config_file_name}' file. Use --type=<type> to disambiguate." );
		}

		if ( ! empty( $results ) ) {
			return $results[0]['value'];
		}

		$type      = 'all' === $type ? 'constant or variable' : $type;
		$names     = array_column( $values, 'name' );
		$candidate = Utils\get_suggestion( $name, $names );

		if ( ! empty( $candidate ) && $candidate !== $name ) {
			WP_CLI::error( "The {$type} '{$name}' is not defined in the '{$wp_config_file_name}' file.\nDid you mean '{$candidate}'?" );
		}

		WP_CLI::error( "The {$type} '{$name}' is not defined in the '{$wp_config_file_name}' file." );
	}

	/**
	 * Generates a unique key/salt for the wp-config file.
	 *
	 * @throws Exception
	 *
	 * @return string
	 */
	private static function unique_key() {
		if ( ! function_exists( 'random_int' ) ) {
			throw new Exception( "'random_int' does not exist" );
		}

		$chars = self::VALID_KEY_CHARACTERS;
		$key   = '';

		for ( $i = 0; $i < 64; $i++ ) {
			// phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.random_intFound -- Will be called only if function exists.
			$key .= substr( $chars, random_int( 0, strlen( $chars ) - 1 ), 1 );
		}

		return $key;
	}

	/**
	 * Filters the values based on a provider filter key.
	 *
	 * @param array $values
	 * @param array $filters
	 * @param bool $strict
	 *
	 * @return array
	 */
	private function filter_values( $values, $filters, $strict ) {
		$result = [];

		foreach ( $values as $value ) {
			foreach ( $filters as $filter ) {
				if ( $strict && $filter !== $value['name'] ) {
					continue;
				}

				if ( false === strpos( $value['name'], $filter ) ) {
					continue;
				}

				$result[] = $value;
			}
		}

		return $result;
	}

	/**
	 * Gets the path to the wp-config.php file or gives a helpful error if none found.
	 *
	 * @param array $assoc_args associative arguments given while calling wp config subcommand
	 * @return string Path to wp-config.php file.
	 */
	private function get_config_path( $assoc_args ) {
		if ( isset( $assoc_args['config-file'] ) ) {
			$path = $assoc_args['config-file'];
			if ( ! file_exists( $path ) ) {
				$this->config_file_not_found_error( basename( $assoc_args['config-file'] ) );
			}
		} else {
			$path = Utils\locate_wp_config();
			if ( ! $path ) {
				$this->config_file_not_found_error( 'wp-config.php' );
			}
		}
		return $path;
	}

	/**
	 * Gives error the wp-config file not found
	 *
	 * @param string $wp_config_file_name Config file name.
	 * @return void
	 */
	private function config_file_not_found_error( $wp_config_file_name ) {
		WP_CLI::error( "'{$wp_config_file_name}' not found.\nEither create one manually or use `wp config create`." );
	}
	/**
	 * Parses the separator argument, to allow for special character handling.
	 *
	 * Does the following transformations:
	 * - '\n' => "\n" (newline)
	 * - '\r' => "\r" (carriage return)
	 * - '\t' => "\t" (tab)
	 *
	 * @param string $separator Separator string to parse.
	 *
	 * @return mixed Parsed separator string.
	 */
	private function parse_separator( $separator ) {
		$separator = str_replace(
			[ '\n', '\r', '\t' ],
			[ "\n", "\r", "\t" ],
			$separator
		);

		return $separator;
	}

	/**
	 * Gets the value of a specific constant or variable defined in wp-config.php file.
	 *
	 * @param $assoc_args
	 * @param $args
	 *
	 * @return string
	 */
	protected function get_value( $assoc_args, $args ) {
		$path                = $this->get_config_path( $assoc_args );
		$wp_config_file_name = basename( $path );
		list( $name )        = $args;

		/**
		 * @var string $type
		 */
		$type = Utils\get_flag_value( $assoc_args, 'type' );

		$value = $this->return_value(
			$name,
			$type,
			self::get_wp_config_vars( $path ),
			$wp_config_file_name
		);

		return $value;
	}

	/**
	 * Writes a provided variable's key and value to stdout, in dotenv format.
	 *
	 * @param array $value
	 */
	private function print_dotenv( array $value ) {
		if ( ! isset( $value['name'] ) || ! isset( $value['type'] ) || 'constant' !== $value['type'] ) {
			return;
		}

		$name           = strtoupper( $value['name'] );
		$variable_value = isset( $value['value'] ) ? $value['value'] : '';

		$variable_value = str_replace( "'", "\'", $variable_value );

		if ( ! is_numeric( $variable_value ) ) {
			$variable_value = "'{$variable_value}'";
		}

		WP_CLI::line( "{$name}={$variable_value}" );
	}

	/**
	 * 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;
	}
}
