<?php

use Composer\Semver\Comparator;
use WP_CLI\Extractor;
use WP_CLI\Iterators\Table as TableIterator;
use WP_CLI\Utils;
use WP_CLI\Formatter;
use WP_CLI\WpOrgApi;

/**
 * Downloads, installs, updates, and manages a WordPress installation.
 *
 * ## EXAMPLES
 *
 *     # Download WordPress core
 *     $ wp core download --locale=nl_NL
 *     Downloading WordPress 4.5.2 (nl_NL)...
 *     md5 hash verified: c5366d05b521831dd0b29dfc386e56a5
 *     Success: WordPress downloaded.
 *
 *     # Install WordPress
 *     $ wp core install --url=example.com --title=Example --admin_user=supervisor --admin_password=strongpassword --admin_email=info@example.com
 *     Success: WordPress installed successfully.
 *
 *     # Display the WordPress version
 *     $ wp core version
 *     4.5.2
 *
 * @package wp-cli
 */
class Core_Command extends WP_CLI_Command {

	/**
	 * Checks for WordPress updates via Version Check API.
	 *
	 * Lists the most recent versions when there are updates available,
	 * or success message when up to date.
	 *
	 * ## OPTIONS
	 *
	 * [--minor]
	 * : Compare only the first two parts of the version number.
	 *
	 * [--major]
	 * : Compare only the first part of the version number.
	 *
	 * [--force-check]
	 * : Bypass the transient cache and force a fresh update check.
	 *
	 * [--field=<field>]
	 * : Prints the value of a single field for each update.
	 *
	 * [--fields=<fields>]
	 * : Limit the output to specific object fields. Defaults to version,update_type,package_url.
	 *
	 * [--format=<format>]
	 * : Render output in a particular format.
	 * ---
	 * default: table
	 * options:
	 *   - table
	 *   - csv
	 *   - count
	 *   - json
	 *   - yaml
	 * ---
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp core check-update
	 *     +---------+-------------+-------------------------------------------------------------+
	 *     | version | update_type | package_url                                                 |
	 *     +---------+-------------+-------------------------------------------------------------+
	 *     | 4.5.2   | major       | https://downloads.wordpress.org/release/wordpress-4.5.2.zip |
	 *     +---------+-------------+-------------------------------------------------------------+
	 *
	 * @subcommand check-update
	 *
	 * @param string[] $args Positional arguments. Unused.
	 * @param array{minor?: bool, major?: bool, 'force-check'?: bool, field?: string, format: string} $assoc_args Associative arguments.
	 */
	public function check_update( $args, $assoc_args ) {
		$format = Utils\get_flag_value( $assoc_args, 'format', 'table' );

		$updates = $this->get_updates( $assoc_args );

		if ( $updates || 'table' !== $format ) {
			$updates   = array_reverse( $updates );
			$formatter = new Formatter(
				$assoc_args,
				[ 'version', 'update_type', 'package_url' ]
			);
			$formatter->display_items( $updates );
		} else {
			WP_CLI::success( 'WordPress is at the latest version.' );
		}
	}

	/**
	 * Downloads core WordPress files.
	 *
	 * Downloads and extracts WordPress core files to the specified path. Uses
	 * current directory when no path is specified. Downloaded build is verified
	 * to have the correct md5 and then cached to the local filesystem.
	 * Subsequent uses of command will use the local cache if it still exists.
	 *
	 * ## OPTIONS
	 *
	 * [<download-url>]
	 * : Download directly from a provided URL instead of fetching the URL from the wordpress.org servers.
	 *
	 * [--path=<path>]
	 * : Specify the path in which to install WordPress. Defaults to current
	 * directory.
	 *
	 * [--locale=<locale>]
	 * : Select which language you want to download.
	 *
	 * [--version=<version>]
	 * : Select which version you want to download. Accepts a version number, 'latest' or 'nightly'.
	 *
	 * [--skip-content]
	 * : Download WP without the default themes and plugins.
	 *
	 * [--force]
	 * : Overwrites existing files, if present.
	 *
	 * [--insecure]
	 * : Retry download without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack.
	 *
	 * [--extract]
	 * : Whether to extract the downloaded file. Defaults to true.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp core download --locale=nl_NL
	 *     Downloading WordPress 4.5.2 (nl_NL)...
	 *     md5 hash verified: c5366d05b521831dd0b29dfc386e56a5
	 *     Success: WordPress downloaded.
	 *
	 * @when before_wp_load
	 *
	 * @param array{0?: string} $args Positional arguments.
	 * @param array{path?: string, locale?: string, version?: string, 'skip-content'?: bool, force?: bool, insecure?: bool, extract?: bool} $assoc_args Associative arguments.
	 */
	public function download( $args, $assoc_args ) {
		/**
		 * @var string $download_dir
		 */
		$download_dir = ! empty( $assoc_args['path'] )
			? ( rtrim( $assoc_args['path'], '/\\' ) . '/' )
			: ABSPATH;

		// Check for files if WordPress already present or not.
		$wordpress_present = is_readable( $download_dir . 'wp-load.php' )
			|| is_readable( $download_dir . 'wp-mail.php' )
			|| is_readable( $download_dir . 'wp-cron.php' )
			|| is_readable( $download_dir . 'wp-links-opml.php' );

		if ( $wordpress_present && ! Utils\get_flag_value( $assoc_args, 'force' ) ) {
			WP_CLI::error( 'WordPress files seem to already be present here.' );
		}

		if ( ! is_dir( $download_dir ) ) {
			if ( ! is_writable( dirname( $download_dir ) ) ) {
				WP_CLI::error( "Insufficient permission to create directory '{$download_dir}'." );
			}

			WP_CLI::log( "Creating directory '{$download_dir}'." );
			if ( ! @mkdir( $download_dir, 0777, true /*recursive*/ ) ) {
				$error = error_get_last();
				if ( $error ) {
					WP_CLI::error( "Failed to create directory '{$download_dir}': {$error['message']}." );
				} else {
					WP_CLI::error( "Failed to create directory '{$download_dir}'." );
				}
			}
		}

		if ( ! is_writable( $download_dir ) ) {
			WP_CLI::error( "'{$download_dir}' is not writable by current user." );
		}

		$locale       = Utils\get_flag_value( $assoc_args, 'locale', 'en_US' );
		$skip_content = Utils\get_flag_value( $assoc_args, 'skip-content', false );
		$insecure     = Utils\get_flag_value( $assoc_args, 'insecure', false );
		$extract      = Utils\get_flag_value( $assoc_args, 'extract', true );

		if ( $skip_content && ! $extract ) {
			WP_CLI::error( 'Cannot use both --skip-content and --no-extract at the same time.' );
		}

		$download_url = array_shift( $args );
		$from_url     = ! empty( $download_url );

		if ( $from_url ) {
			$version = null;
			if ( isset( $assoc_args['version'] ) ) {
				WP_CLI::error( 'Version option is not available for URL downloads.' );
			}
			if ( $skip_content || 'en_US' !== $locale ) {
				WP_CLI::error( 'Skip content and locale options are not available for URL downloads.' );
			}
		} elseif ( isset( $assoc_args['version'] ) && 'latest' !== $assoc_args['version'] ) {
			$version = $assoc_args['version'];
			if ( in_array( strtolower( $version ), [ 'trunk', 'nightly' ], true ) ) {
				$version = 'nightly';
			}

			// Nightly builds and skip content are only available in .zip format.
			$extension = ( ( 'nightly' === $version ) || $skip_content )
				? 'zip'
				: 'tar.gz';

			$download_url = $this->get_download_url( $version, $locale, $extension );
		} else {
			try {
				$offer = ( new WpOrgApi( [ 'insecure' => $insecure ] ) )
					->get_core_download_offer( $locale );
			} catch ( Exception $exception ) {
				WP_CLI::error( $exception );
			}
			if ( ! $offer ) {
				WP_CLI::error( "The requested locale ({$locale}) was not found." );
			}
			$version      = $offer['current'];
			$download_url = $offer['download'];
			if ( ! $skip_content ) {
				$download_url = str_replace( '.zip', '.tar.gz', $download_url );
			}
		}

		if ( 'nightly' === $version && 'en_US' !== $locale ) {
			WP_CLI::error( 'Nightly builds are only available for the en_US locale.' );
		}

		$from_version = '';
		if ( file_exists( $download_dir . 'wp-includes/version.php' ) ) {
			$wp_details   = self::get_wp_details( $download_dir );
			$from_version = $wp_details['wp_version'];
		}

		if ( $from_url ) {
			WP_CLI::log( "Downloading from {$download_url} ..." );
		} else {
			WP_CLI::log( "Downloading WordPress {$version} ({$locale})..." );
		}

		$path_parts = pathinfo( $download_url );
		$extension  = 'tar.gz';
		if ( isset( $path_parts['extension'] ) && 'zip' === $path_parts['extension'] ) {
			$extension = 'zip';
			if ( $extract && ! class_exists( 'ZipArchive' ) ) {
				WP_CLI::error( 'Extracting a zip file requires ZipArchive.' );
			}
		}

		if ( $skip_content && 'zip' !== $extension ) {
			WP_CLI::error( 'Skip content is only available for ZIP files.' );
		}

		$cache = WP_CLI::get_cache();
		if ( $from_url ) {
			$cache_file = null;
		} else {
			$cache_key  = "core/wordpress-{$version}-{$locale}.{$extension}";
			$cache_file = $cache->has( $cache_key );
		}

		$bad_cache = false;

		if ( is_string( $cache_file ) ) {
			WP_CLI::log( "Using cached file '{$cache_file}'..." );
			$skip_content_cache_file = $skip_content ? self::strip_content_dir( $cache_file ) : null;
			if ( $extract ) {
				try {
					Extractor::extract( $skip_content_cache_file ?: $cache_file, $download_dir );
				} catch ( Exception $exception ) {
					WP_CLI::warning( 'Extraction failed, downloading a new copy...' );
					$bad_cache = true;
				}
			} else {
				copy( $cache_file, $download_dir . basename( $cache_file ) );
			}
		}

		if ( ! $cache_file || $bad_cache ) {
			// We need to use a temporary file because piping from cURL to tar is flaky
			// on MinGW (and probably in other environments too).
			$temp = Utils\get_temp_dir() . uniqid( 'wp_' ) . ".{$extension}";
			register_shutdown_function(
				function () use ( $temp ) {
					if ( file_exists( $temp ) ) {
						unlink( $temp );
					}
				}
			);

			$headers = [ 'Accept' => 'application/json' ];
			$options = [
				'timeout'  => 600,  // 10 minutes ought to be enough for everybody
				'filename' => $temp,
				'insecure' => $insecure,
			];

			/** @var \WpOrg\Requests\Response $response */
			$response = Utils\http_request( 'GET', $download_url, null, $headers, $options );

			if ( 404 === (int) $response->status_code ) {
				WP_CLI::error( 'Release not found. Double-check locale or version.' );
			} elseif ( 20 !== (int) substr( (string) $response->status_code, 0, 2 ) ) {
				WP_CLI::error( "Couldn't access download URL (HTTP code {$response->status_code})." );
			}

			if ( 'nightly' !== $version ) {
				unset( $options['filename'] );
				/** @var \WpOrg\Requests\Response $md5_response */
				$md5_response = Utils\http_request( 'GET', $download_url . '.md5', null, [], $options );
				if ( $md5_response->status_code >= 200 && $md5_response->status_code < 300 ) {
					$md5_file = md5_file( $temp );

					if ( $md5_file === $md5_response->body ) {
						WP_CLI::log( 'md5 hash verified: ' . $md5_file );
					} else {
						WP_CLI::error( "md5 hash for download ({$md5_file}) is different than the release hash ({$md5_response->body})." );
					}
				} else {
					WP_CLI::warning( "Couldn't access md5 hash for release ({$download_url}.md5, HTTP code {$md5_response->status_code})." );
				}
			} else {
				WP_CLI::warning( 'md5 hash checks are not available for nightly downloads.' );
			}

			$skip_content_temp = $skip_content ? self::strip_content_dir( $temp ) : null;
			if ( $extract ) {
				try {
					Extractor::extract( $skip_content_temp ?: $temp, $download_dir );
				} catch ( Exception $exception ) {
					WP_CLI::error( "Couldn't extract WordPress archive. {$exception->getMessage()}" );
				}
			} else {
				copy( $temp, $download_dir . basename( $temp ) );
			}

			// Do not use the cache for nightly builds or for downloaded URLs
			// (the URL could be something like "latest.zip" or "nightly.zip").
			if ( ! $from_url && 'nightly' !== $version ) {
				$cache->import( $cache_key, $temp );
			}
		}

		if ( $wordpress_present ) {
			$this->cleanup_extra_files( $from_version, $version, $locale, $insecure );
		}

		WP_CLI::success( 'WordPress downloaded.' );
	}

	/**
	 * Checks if WordPress is installed.
	 *
	 * Determines whether WordPress is installed by checking if the standard
	 * database tables are installed. Doesn't produce output; uses exit codes
	 * to communicate whether WordPress is installed.
	 *
	 * ## OPTIONS
	 *
	 * [--network]
	 * : Check if this is a multisite installation.
	 *
	 * ## EXAMPLES
	 *
	 *     # Bash script for checking if WordPress is not installed.
	 *
	 *     if ! wp core is-installed 2>/dev/null; then
	 *         # WP is not installed. Let's try installing it.
	 *         wp core install
	 *     fi
	 *
	 *     # Bash script for checking if WordPress is installed, with fallback.
	 *
	 *     if wp core is-installed 2>/dev/null; then
	 *         # WP is installed. Let's do some things we should only do in a confirmed WP environment.
	 *         wp core verify-checksums
	 *     else
	 *         # Fallback if WP is not installed.
	 *         echo 'Hey Friend, you are in the wrong spot. Move in to your WordPress directory and try again.'
	 *     fi
	 *
	 * @subcommand is-installed
	 *
	 * @param string[] $args Positional arguments. Unused.
	 * @param array{network?: bool} $assoc_args Associative arguments.
	 */
	public function is_installed( $args, $assoc_args ) {
		if ( is_blog_installed() && ( ! Utils\get_flag_value( $assoc_args, 'network' ) || is_multisite() ) ) {
			WP_CLI::halt( 0 );
		}

		WP_CLI::halt( 1 );
	}

	/**
	 * Runs the standard WordPress installation process.
	 *
	 * Creates the WordPress tables in the database using the URL, title, and
	 * default admin user details provided. Performs the famous 5 minute install
	 * in seconds or less.
	 *
	 * Note: if you've installed WordPress in a subdirectory, then you'll need
	 * to `wp option update siteurl` after `wp core install`. For instance, if
	 * WordPress is installed in the `/wp` directory and your domain is example.com,
	 * then you'll need to run `wp option update siteurl http://example.com/wp` for
	 * your WordPress installation to function properly.
	 *
	 * Note: When using custom user tables (e.g. `CUSTOM_USER_TABLE`), the admin
	 * email and password are ignored if the user_login already exists. If the
	 * user_login doesn't exist, a new user will be created.
	 *
	 * ## OPTIONS
	 *
	 * --url=<url>
	 * : The address of the new site.
	 *
	 * --title=<site-title>
	 * : The title of the new site.
	 *
	 * --admin_user=<username>
	 * : The name of the admin user.
	 *
	 * [--admin_password=<password>]
	 * : The password for the admin user. Defaults to randomly generated string.
	 *
	 * --admin_email=<email>
	 * : The email address for the admin user.
	 *
	 * [--locale=<locale>]
	 * : The locale/language for the installation (e.g. `de_DE`). Default is `en_US`.
	 *
	 * [--skip-email]
	 * : Don't send an email notification to the new admin user.
	 *
	 * ## EXAMPLES
	 *
	 *     # Install WordPress in 5 seconds
	 *     $ wp core install --url=example.com --title=Example --admin_user=supervisor --admin_password=strongpassword --admin_email=info@example.com
	 *     Success: WordPress installed successfully.
	 *
	 *     # Install WordPress without disclosing admin_password to bash history
	 *     $ wp core install --url=example.com --title=Example --admin_user=supervisor --admin_email=info@example.com --prompt=admin_password < admin_password.txt
	 *
	 * @param string[] $args Positional arguments. Unused.
	 * @param array{url: string, title: string, admin_user: string, admin_password?: string, admin_email: string, locale?: string, 'skip-email'?: bool} $assoc_args Associative arguments.
	 */
	public function install( $args, $assoc_args ) {
		if ( $this->do_install( $assoc_args ) ) {
			WP_CLI::success( 'WordPress installed successfully.' );
		} else {
			WP_CLI::log( 'WordPress is already installed.' );
		}
	}

	/**
	 * Transforms an existing single-site installation into a multisite installation.
	 *
	 * Creates the multisite database tables, and adds the multisite constants
	 * to wp-config.php.
	 *
	 * For those using WordPress with Apache, remember to update the `.htaccess`
	 * file with the appropriate multisite rewrite rules.
	 *
	 * [Review the multisite documentation](https://wordpress.org/support/article/create-a-network/)
	 * for more details about how multisite works.
	 *
	 * ## OPTIONS
	 *
	 * [--title=<network-title>]
	 * : The title of the new network.
	 *
	 * [--base=<url-path>]
	 * : Base path after the domain name that each site url will start with.
	 * ---
	 * default: /
	 * ---
	 *
	 * [--subdomains]
	 * : If passed, the network will use subdomains, instead of subdirectories. Doesn't work with 'localhost'.
	 *
	 * [--skip-config]
	 * : Don't add multisite constants to wp-config.php.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp core multisite-convert
	 *     Set up multisite database tables.
	 *     Added multisite constants to wp-config.php.
	 *     Success: Network installed. Don't forget to set up rewrite rules.
	 *
	 * @subcommand multisite-convert
	 * @alias install-network
	 *
	 * @param string[] $args Positional arguments. Unused.
	 * @param array{title?: string, base: string, subdomains?: bool, 'skip-config'?: bool} $assoc_args Associative arguments.
	 */
	public function multisite_convert( $args, $assoc_args ) {
		if ( is_multisite() ) {
			WP_CLI::error( 'This already is a multisite installation.' );
		}

		$assoc_args = self::set_multisite_defaults( $assoc_args );
		if ( ! isset( $assoc_args['title'] ) ) {
			/**
			 * @var string $blogname
			 */
			$blogname = get_option( 'blogname' );

			// translators: placeholder is blog name
			$assoc_args['title'] = sprintf( _x( '%s Sites', 'Default network name' ), $blogname );
		}

		if ( $this->multisite_convert_( $assoc_args ) ) {
			WP_CLI::success( "Network installed. Don't forget to set up rewrite rules (and a .htaccess file, if using Apache)." );
		}
	}

	/**
	 * Installs WordPress multisite from scratch.
	 *
	 * Creates the WordPress tables in the database using the URL, title, and
	 * default admin user details provided. Then, creates the multisite tables
	 * in the database and adds multisite constants to the wp-config.php.
	 *
	 * For those using WordPress with Apache, remember to update the `.htaccess`
	 * file with the appropriate multisite rewrite rules.
	 *
	 * ## OPTIONS
	 *
	 * [--url=<url>]
	 * : The address of the new site.
	 *
	 * [--base=<url-path>]
	 * : Base path after the domain name that each site url in the network will start with.
	 * ---
	 * default: /
	 * ---
	 *
	 * [--subdomains]
	 * : If passed, the network will use subdomains, instead of subdirectories. Doesn't work with 'localhost'.
	 *
	 * --title=<site-title>
	 * : The title of the new site.
	 *
	 * --admin_user=<username>
	 * : The name of the admin user.
	 * ---
	 * default: admin
	 * ---
	 *
	 * [--admin_password=<password>]
	 * : The password for the admin user. Defaults to randomly generated string.
	 *
	 * --admin_email=<email>
	 * : The email address for the admin user.
	 *
	 * [--skip-email]
	 * : Don't send an email notification to the new admin user.
	 *
	 * [--skip-config]
	 * : Don't add multisite constants to wp-config.php.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp core multisite-install --title="Welcome to the WordPress" \
	 *     > --admin_user="admin" --admin_password="password" \
	 *     > --admin_email="user@example.com"
	 *     Single site database tables already present.
	 *     Set up multisite database tables.
	 *     Added multisite constants to wp-config.php.
	 *     Success: Network installed. Don't forget to set up rewrite rules.
	 *
	 * @subcommand multisite-install
	 *
	 * @param string[] $args Positional arguments. Unused.
	 * @param array{url?: string, base: string, subdomains?: bool, title: string, admin_user: string, admin_password?: string, admin_email: string, 'skip-email'?: bool, 'skip-config'?: bool} $assoc_args Associative arguments.
	 */
	public function multisite_install( $args, $assoc_args ) {
		if ( $this->do_install( $assoc_args ) ) {
			WP_CLI::log( 'Created single site database tables.' );
		} else {
			WP_CLI::log( 'Single site database tables already present.' );
		}

		$assoc_args = self::set_multisite_defaults( $assoc_args );
		// translators: placeholder is user supplied title
		$assoc_args['title'] = sprintf( _x( '%s Sites', 'Default network name' ), $assoc_args['title'] );

		// Overwrite runtime args, to avoid mismatches.
		$consts_to_args = [
			'SUBDOMAIN_INSTALL'    => 'subdomains',
			'PATH_CURRENT_SITE'    => 'base',
			'SITE_ID_CURRENT_SITE' => 'site_id',
			'BLOG_ID_CURRENT_SITE' => 'blog_id',
		];

		foreach ( $consts_to_args as $const => $arg ) {
			if ( defined( $const ) ) {
				$assoc_args[ $arg ] = constant( $const );
			}
		}

		if ( ! $this->multisite_convert_( $assoc_args ) ) {
			return;
		}

		// Do the steps that were skipped by populate_network(),
		// which checks is_multisite().
		if ( is_multisite() ) {
			$site_user = get_user_by( 'email', $assoc_args['admin_email'] );
			self::add_site_admins( $site_user );
			$domain = self::get_clean_basedomain();
			self::create_initial_blog(
				$assoc_args['site_id'],
				$assoc_args['blog_id'],
				$domain,
				$assoc_args['base'],
				$assoc_args['subdomains'],
				$site_user
			);
		}

		WP_CLI::success( "Network installed. Don't forget to set up rewrite rules (and a .htaccess file, if using Apache)." );
	}

	private static function set_multisite_defaults( $assoc_args ) {
		$defaults = [
			'subdomains' => false,
			'base'       => '/',
			'site_id'    => 1,
			'blog_id'    => 1,
		];

		return array_merge( $defaults, $assoc_args );
	}

	private function do_install( $assoc_args ) {
		if ( is_blog_installed() ) {
			return false;
		}

		if ( true === Utils\get_flag_value( $assoc_args, 'skip-email' ) ) {
			if ( ! function_exists( 'wp_new_blog_notification' ) ) {
				// @phpstan-ignore function.inner
				function wp_new_blog_notification() {
					// Silence is golden
				}
			}
			// WP 4.9.0 - skip "Notice of Admin Email Change" email as well (https://core.trac.wordpress.org/ticket/39117).
			add_filter( 'send_site_admin_email_change_email', '__return_false' );
		}

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';

		$defaults = [
			'title'          => '',
			'admin_user'     => '',
			'admin_email'    => '',
			'admin_password' => '',
		];

		$defaults['locale'] = '';

		$args = wp_parse_args( $assoc_args, $defaults );

		// Support prompting for the `--url=<url>`,
		// which is normally a runtime argument
		if ( isset( $assoc_args['url'] ) ) {
			WP_CLI::set_url( $assoc_args['url'] );
		}

		$public   = true;
		$password = $args['admin_password'];

		if ( ! is_email( $args['admin_email'] ) ) {
			WP_CLI::error( "The '{$args['admin_email']}' email address is invalid." );
		}

		$result = wp_install(
			$args['title'],
			$args['admin_user'],
			$args['admin_email'],
			$public,
			'',
			$password,
			$args['locale']
		);

		if ( ! empty( $GLOBALS['wpdb']->last_error ) ) {
			WP_CLI::error( 'Installation produced database errors, and may have partially or completely failed.' );
		}

		if ( empty( $args['admin_password'] ) ) {
			WP_CLI::log( "Admin password: {$result['password']}" );
		}

		// Confirm the uploads directory exists
		$upload_dir = wp_upload_dir();
		if ( ! empty( $upload_dir['error'] ) ) {
			WP_CLI::warning( $upload_dir['error'] );
		}

		return true;
	}

	private function multisite_convert_( $assoc_args ) {
		global $wpdb;

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';

		$domain = self::get_clean_basedomain();
		if ( 'localhost' === $domain && ! empty( $assoc_args['subdomains'] ) ) {
			WP_CLI::error( "Multisite with subdomains cannot be configured when domain is 'localhost'." );
		}

		// need to register the multisite tables manually for some reason
		foreach ( $wpdb->tables( 'ms_global' ) as $table => $prefixed_table ) {
			$wpdb->$table = $prefixed_table;
		}

		install_network();

		/**
		 * @var string $admin_email
		 */
		$admin_email = get_option( 'admin_email' );

		$result = populate_network(
			$assoc_args['site_id'],
			$domain,
			$admin_email,
			$assoc_args['title'],
			$assoc_args['base'],
			$assoc_args['subdomains']
		);

		$site_id = $wpdb->get_var( "SELECT id FROM $wpdb->site" );
		$site_id = ( null === $site_id ) ? 1 : (int) $site_id;

		if ( true === $result ) {
			WP_CLI::log( 'Set up multisite database tables.' );
		} else {
			switch ( $result->get_error_code() ) {

				case 'siteid_exists':
					WP_CLI::log( $result->get_error_message() );
					return false;

				case 'no_wildcard_dns':
					WP_CLI::warning( __( 'Wildcard DNS may not be configured correctly.' ) );
					break;

				default:
					WP_CLI::error( $result );
			}
		}

		// delete_site_option() cleans the alloptions cache to prevent dupe option
		delete_site_option( 'upload_space_check_disabled' );
		update_site_option( 'upload_space_check_disabled', 1 );

		if ( ! is_multisite() ) {
			$subdomain_export = Utils\get_flag_value( $assoc_args, 'subdomains' ) ? 'true' : 'false';
			$ms_config        = <<<EOT
define( 'WP_ALLOW_MULTISITE', true );
define( 'MULTISITE', true );
define( 'SUBDOMAIN_INSTALL', {$subdomain_export} );
\$base = '{$assoc_args['base']}';
define( 'DOMAIN_CURRENT_SITE', '{$domain}' );
define( 'PATH_CURRENT_SITE', '{$assoc_args['base']}' );
define( 'SITE_ID_CURRENT_SITE', {$site_id} );
define( 'BLOG_ID_CURRENT_SITE', 1 );
EOT;

			$wp_config_path = Utils\locate_wp_config();
			if ( true === Utils\get_flag_value( $assoc_args, 'skip-config' ) ) {
				WP_CLI::log( "Addition of multisite constants to 'wp-config.php' skipped. You need to add them manually:\n{$ms_config}" );
			} elseif ( is_writable( $wp_config_path ) && self::modify_wp_config( $ms_config ) ) {
				WP_CLI::log( "Added multisite constants to 'wp-config.php'." );
			} else {
				WP_CLI::warning( "Multisite constants could not be written to 'wp-config.php'. You may need to add them manually:\n{$ms_config}" );
			}
		} else {
			/* Multisite constants are defined, therefore we already have an empty site_admins site meta.
			 *
			 * Code based on parts of delete_network_option. */
			$rows = $wpdb->get_results( "SELECT meta_id, site_id FROM {$wpdb->sitemeta} WHERE meta_key = 'site_admins' AND meta_value = ''" );

			foreach ( $rows as $row ) {
				wp_cache_delete( "{$row->site_id}:site_admins", 'site-options' );

				$wpdb->delete(
					$wpdb->sitemeta,
					[ 'meta_id' => $row->meta_id ]
				);
			}
		}

		return true;
	}

	// copied from populate_network()
	private static function create_initial_blog(
		$network_id,
		$blog_id,
		$domain,
		$path,
		$subdomain_install,
		$site_user
	) {
		global $wpdb, $current_site, $wp_rewrite;

		// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- This is meant to replace Core functionality.
		$current_site            = new stdClass();
		$current_site->domain    = $domain;
		$current_site->path      = $path;
		$current_site->site_name = ucfirst( $domain );
		$blog_data               = [
			'site_id'    => $network_id,
			'domain'     => $domain,
			'path'       => $path,
			'registered' => current_time( 'mysql' ),
		];
		$wpdb->insert( $wpdb->blogs, $blog_data );
		$current_site->blog_id = $wpdb->insert_id;
		$blog_id               = $wpdb->insert_id;
		update_user_meta( $site_user->ID, 'source_domain', $domain );
		update_user_meta( $site_user->ID, 'primary_blog', $blog_id );

		if ( $subdomain_install ) {
			$wp_rewrite->set_permalink_structure( '/%year%/%monthnum%/%day%/%postname%/' );
		} else {
			$wp_rewrite->set_permalink_structure( '/blog/%year%/%monthnum%/%day%/%postname%/' );
		}

		flush_rewrite_rules();
	}

	// copied from populate_network()
	private static function add_site_admins( $site_user ) {
		$site_admins = [ $site_user->user_login ];
		$users       = get_users( [ 'fields' => [ 'ID', 'user_login' ] ] );
		if ( $users ) {
			foreach ( $users as $user ) {
				if ( is_super_admin( $user->ID )
					&& ! in_array( $user->user_login, $site_admins, true ) ) {
					$site_admins[] = $user->user_login;
				}
			}
		}

		update_site_option( 'site_admins', $site_admins );
	}

	private static function modify_wp_config( $content ) {
		$wp_config_path = Utils\locate_wp_config();

		$token           = "/* That's all, stop editing!";
		$config_contents = (string) file_get_contents( $wp_config_path );
		if ( false === strpos( $config_contents, $token ) ) {
			return false;
		}

		list( $before, $after ) = explode( $token, $config_contents );

		$content = trim( $content );

		file_put_contents(
			$wp_config_path,
			"{$before}\n\n{$content}\n\n{$token}{$after}"
		);

		return true;
	}

	private static function get_clean_basedomain() {
		/**
		 * @var string $siteurl
		 */
		$siteurl = get_option( 'siteurl' );
		$domain  = (string) preg_replace( '|https?://|', '', $siteurl );
		$slash   = strpos( $domain, '/' );
		if ( false !== $slash ) {
			$domain = substr( $domain, 0, $slash );
		}
		return $domain;
	}

	/**
	 * Displays the WordPress version.
	 *
	 * ## OPTIONS
	 *
	 * [--extra]
	 * : Show extended version information.
	 *
	 * ## EXAMPLES
	 *
	 *     # Display the WordPress version
	 *     $ wp core version
	 *     4.5.2
	 *
	 *     # Display WordPress version along with other information
	 *     $ wp core version --extra
	 *     WordPress version: 4.5.2
	 *     Database revision: 36686
	 *     TinyMCE version:   4.310 (4310-20160418)
	 *     Package language:  en_US
	 *
	 * @when before_wp_load
	 *
	 * @param string[] $args Positional arguments. Unused.
	 * @param array{extra?: bool} $assoc_args Associative arguments.
	 */
	public function version( $args = [], $assoc_args = [] ) {
		$details = self::get_wp_details();

		if ( ! Utils\get_flag_value( $assoc_args, 'extra' ) ) {
			WP_CLI::line( $details['wp_version'] );
			return;
		}

		$match                   = [];
		$found_version           = preg_match( '/(\d)(\d+)-/', $details['tinymce_version'], $match );
		$human_readable_tiny_mce = $found_version ? "{$match[1]}.{$match[2]}" : '';

		echo Utils\mustache_render(
			self::get_template_path( 'versions.mustache' ),
			[
				'wp-version'    => $details['wp_version'],
				'db-version'    => $details['wp_db_version'],
				'local-package' => empty( $details['wp_local_package'] )
					? 'en_US'
					: $details['wp_local_package'],
				'mce-version'   => $human_readable_tiny_mce
					? "{$human_readable_tiny_mce} ({$details['tinymce_version']})"
					: $details['tinymce_version'],
			]
		);
	}

	/**
	 * Gets version information from `wp-includes/version.php`.
	 *
	 * @return array {
	 *     @type string $wp_version The WordPress version.
	 *     @type int $wp_db_version The WordPress DB revision.
	 *     @type string $tinymce_version The TinyMCE version.
	 *     @type string $wp_local_package The TinyMCE version.
	 * }
	 */
	private static function get_wp_details( $abspath = ABSPATH ) {
		$versions_path = $abspath . 'wp-includes/version.php';

		if ( ! is_readable( $versions_path ) ) {
			WP_CLI::error(
				"This does not seem to be a WordPress installation.\n" .
				'Pass --path=`path/to/wordpress` or run `wp core download`.'
			);
		}

		$version_content = (string) file_get_contents( $versions_path, false, null, 6, 2048 );

		$vars   = [ 'wp_version', 'wp_db_version', 'tinymce_version', 'wp_local_package' ];
		$result = [];

		foreach ( $vars as $var_name ) {
			$result[ $var_name ] = self::find_var( $var_name, $version_content );
		}

		return $result;
	}

	/**
	 * Gets the template path based on installation type.
	 */
	private static function get_template_path( $template ) {
		$command_root  = Utils\phar_safe_path( dirname( __DIR__ ) );
		$template_path = "{$command_root}/templates/{$template}";

		if ( ! file_exists( $template_path ) ) {
			WP_CLI::error( "Couldn't find {$template}" );
		}

		return $template_path;
	}

	/**
	 * Searches for the value assigned to variable `$var_name` in PHP code `$code`.
	 *
	 * This is equivalent to matching the `\$VAR_NAME = ([^;]+)` regular expression and returning
	 * the first match either as a `string` or as an `integer` (depending if it's surrounded by
	 * quotes or not).
	 *
	 * @param string $var_name Variable name to search for.
	 * @param string $code PHP code to search in.
	 *
	 * @return string|null
	 */
	private static function find_var( $var_name, $code ) {
		$start = strpos( $code, '$' . $var_name . ' = ' );

		if ( ! $start ) {
			return null;
		}

		$start = $start + strlen( $var_name ) + 3;
		$end   = strpos( $code, ';', $start );

		$value = substr( $code, $start, $end - $start );

		return trim( $value, " '" );
	}

	/**
	 * Security copy of the core function with Requests - Gets the checksums for the given version of WordPress.
	 *
	 * @param string $version  Version string to query.
	 * @param string $locale   Locale to query.
	 * @param bool   $insecure Whether to retry without certificate validation on TLS handshake failure.
	 * @return string|array String message on failure. An array of checksums on success.
	 */
	private static function get_core_checksums( $version, $locale, $insecure ) {
		$wp_org_api = new WpOrgApi( [ 'insecure' => $insecure ] );

		try {
			/**
			 * @var array|false $checksums
			 */
			$checksums = $wp_org_api->get_core_checksums( $version, $locale );
		} catch ( Exception $exception ) {
			return $exception->getMessage();
		}

		if ( false === $checksums ) {
			return "Checksums not available for WordPress {$version}/{$locale}.";
		}

		return $checksums;
	}

	/**
	 * Updates WordPress to a newer version.
	 *
	 * Defaults to updating WordPress to the latest version.
	 *
	 * If you see "Error: Another update is currently in progress.", you may
	 * need to run `wp option delete core_updater.lock` after verifying another
	 * update isn't actually running.
	 *
	 * ## OPTIONS
	 *
	 * [<zip>]
	 * : Path to zip file to use, instead of downloading from wordpress.org.
	 *
	 * [--minor]
	 * : Only perform updates for minor releases (e.g. update from WP 4.3 to 4.3.3 instead of 4.4.2).
	 *
	 * [--version=<version>]
	 * : Update to a specific version, instead of to the latest version. Alternatively accepts 'nightly'.
	 *
	 * [--force]
	 * : Update even when installed WP version is greater than the requested version.
	 *
	 * [--locale=<locale>]
	 * : Select which language you want to download.
	 *
	 * [--insecure]
	 * : Retry download without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack.
	 *
	 * ## EXAMPLES
	 *
	 *     # Update WordPress
	 *     $ wp core update
	 *     Updating to version 4.5.2 (en_US)...
	 *     Downloading update from https://downloads.wordpress.org/release/wordpress-4.5.2-no-content.zip...
	 *     Unpacking the update...
	 *     Cleaning up files...
	 *     No files found that need cleaning up
	 *     Success: WordPress updated successfully.
	 *
	 *     # Update WordPress using zip file.
	 *     $ wp core update ../latest.zip
	 *     Starting update...
	 *     Unpacking the update...
	 *     Success: WordPress updated successfully.
	 *
	 *     # Update WordPress to 3.1 forcefully
	 *     $ wp core update --version=3.1 --force
	 *     Updating to version 3.1 (en_US)...
	 *     Downloading update from https://wordpress.org/wordpress-3.1.zip...
	 *     Unpacking the update...
	 *     Warning: Checksums not available for WordPress 3.1/en_US. Please cleanup files manually.
	 *     Success: WordPress updated successfully.
	 *
	 * @alias upgrade
	 *
	 * @param array{0?: string} $args Positional arguments.
	 * @param array{minor?: bool, version?: string, force?: bool, locale?: string, insecure?: bool} $assoc_args Associative arguments.
	 */
	public function update( $args, $assoc_args ) {
		global $wp_version;

		$update   = null;
		$upgrader = 'WP_CLI\\Core\\CoreUpgrader';

		if ( 'trunk' === Utils\get_flag_value( $assoc_args, 'version' ) ) {
			$assoc_args['version'] = 'nightly';
		}

		if ( ! empty( $args[0] ) ) {

			// ZIP path or URL is given
			$upgrader = 'WP_CLI\\Core\\NonDestructiveCoreUpgrader';
			$version  = Utils\get_flag_value( $assoc_args, 'version' );

			$update = (object) [
				'response' => 'upgrade',
				'current'  => $version,
				'download' => $args[0],
				'packages' => (object) [
					'partial'     => null,
					'new_bundled' => null,
					'no_content'  => null,
					'full'        => $args[0],
				],
				'version'  => $version,
				'locale'   => null,
			];

		} elseif ( empty( $assoc_args['version'] ) ) {

			// Update to next release
			wp_version_check();

			/**
			 * @var object{updates: array<object{version: string, locale: string}>} $from_api
			 */
			$from_api = get_site_transient( 'update_core' );

			if ( Utils\get_flag_value( $assoc_args, 'minor' ) ) {
				foreach ( $from_api->updates as $offer ) {
					$sem_ver = Utils\get_named_sem_ver( $offer->version, $wp_version );
					if ( ! $sem_ver || 'patch' !== $sem_ver ) {
						continue;
					}
					$update = $offer;
					break;
				}
				if ( empty( $update ) ) {
					WP_CLI::success( 'WordPress is at the latest minor release.' );
					return;
				}
			} elseif ( ! empty( $from_api->updates ) ) {
				list( $update ) = $from_api->updates;
			}
		} elseif ( Utils\wp_version_compare( $assoc_args['version'], '<' )
			|| 'nightly' === $assoc_args['version']
			|| Utils\get_flag_value( $assoc_args, 'force' ) ) {

			// Specific version is given
			$version = $assoc_args['version'];

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

			$new_package = $this->get_download_url( $version, $locale );

			$update = (object) [
				'response' => 'upgrade',
				'current'  => $assoc_args['version'],
				'download' => $new_package,
				'packages' => (object) [
					'partial'     => null,
					'new_bundled' => null,
					'no_content'  => null,
					'full'        => $new_package,
				],
				'version'  => $version,
				'locale'   => $locale,
			];

		}

		if ( ! empty( $update )
			&& ( $update->version !== $wp_version
				|| Utils\get_flag_value( $assoc_args, 'force' ) ) ) {

			require_once ABSPATH . 'wp-admin/includes/upgrade.php';

			if ( $update->version ) {
				WP_CLI::log( "Updating to version {$update->version} ({$update->locale})..." );
			} else {
				WP_CLI::log( 'Starting update...' );
			}

			$from_version = $wp_version;
			$insecure     = (bool) Utils\get_flag_value( $assoc_args, 'insecure', false );

			$GLOBALS['wpcli_core_update_obj'] = $update;

			/**
			 * @var \WP_CLI\Core\CoreUpgrader $wp_upgrader
			 */
			$wp_upgrader = Utils\get_upgrader( $upgrader, $insecure );
			$result      = $wp_upgrader->upgrade( $update );
			unset( $GLOBALS['wpcli_core_update_obj'] );

			if ( is_wp_error( $result ) ) {
				$message = WP_CLI::error_to_string( $result );
				if ( 'up_to_date' !== $result->get_error_code() ) {
					WP_CLI::error( $message );
				} else {
					WP_CLI::success( $message );
				}
			} else {

				$to_version = '';
				if ( file_exists( ABSPATH . 'wp-includes/version.php' ) ) {
					$wp_details = self::get_wp_details();
					$to_version = $wp_details['wp_version'];
				}

				/**
				 * @var string $locale
				 */
				$locale = Utils\get_flag_value( $assoc_args, 'locale', get_locale() );
				$this->cleanup_extra_files( $from_version, $to_version, $locale, $insecure );

				WP_CLI::success( 'WordPress updated successfully.' );
			}
		} else {
			WP_CLI::success( 'WordPress is up to date.' );
		}
	}

	/**
	 * Runs the WordPress database update procedure.
	 *
	 * ## OPTIONS
	 *
	 * [--network]
	 * : Update databases for all sites on a network
	 *
	 * [--dry-run]
	 * : Compare database versions without performing the update.
	 *
	 * ## EXAMPLES
	 *
	 *     # Update the WordPress database.
	 *     $ wp core update-db
	 *     Success: WordPress database upgraded successfully from db version 36686 to 35700.
	 *
	 *     # Update databases for all sites on a network.
	 *     $ wp core update-db --network
	 *     WordPress database upgraded successfully from db version 35700 to 29630 on example.com/
	 *     Success: WordPress database upgraded on 123/123 sites.
	 *
	 * @subcommand update-db
	 *
	 * @param string[] $args Positional arguments. Unused.
	 * @param array{network?: bool, 'dry-run'?: bool} $assoc_args Associative arguments.
	 */
	public function update_db( $args, $assoc_args ) {
		global $wpdb, $wp_db_version, $wp_current_db_version;

		$network = Utils\get_flag_value( $assoc_args, 'network' );
		if ( $network && ! is_multisite() ) {
			WP_CLI::error( 'This is not a multisite installation.' );
		}

		$dry_run = Utils\get_flag_value( $assoc_args, 'dry-run' );
		if ( $dry_run ) {
			WP_CLI::log( 'Performing a dry run, with no database modification.' );
		}

		if ( $network ) {
			$iterator_args = [
				'table' => $wpdb->blogs,
				'where' => [
					'spam'     => 0,
					'deleted'  => 0,
					'archived' => 0,
				],
			];
			$it            = new TableIterator( $iterator_args );
			$success       = 0;
			$total         = 0;
			$site_ids      = [];

			/**
			 * @var object{site_id: int, domain: string, path: string} $blog
			 */
			foreach ( $it as $blog ) {
				++$total;
				$site_ids[] = $blog->site_id;
				$url        = $blog->domain . $blog->path;
				$cmd        = "--url={$url} core update-db";
				if ( $dry_run ) {
					$cmd .= ' --dry-run';
				}

				/**
				 * @var object{stdout: string, stderr: string, return_code: int} $process
				 */
				$process = WP_CLI::runcommand(
					$cmd,
					[
						'return'     => 'all',
						'exit_error' => false,
					]
				);
				if ( 0 === (int) $process->return_code ) {
					// See if we can parse the stdout
					if ( preg_match( '#Success: (.+)#', $process->stdout, $matches ) ) {
						$message = rtrim( $matches[1], '.' );
						$message = "{$message} on {$url}";
					} else {
						$message = "Database upgraded successfully on {$url}";
					}
					WP_CLI::log( $message );
					++$success;
				} else {
					WP_CLI::warning( "Database failed to upgrade on {$url}" );
				}
			}
			if ( ! $dry_run && $total && $success === $total ) {
				foreach ( array_unique( $site_ids ) as $site_id ) {
					update_metadata( 'site', $site_id, 'wpmu_upgrade_site', $wp_db_version );
				}
			}
			WP_CLI::success( "WordPress database upgraded on {$success}/{$total} sites." );
		} else {
			require_once ABSPATH . 'wp-admin/includes/upgrade.php';

			/**
			 * @var string $wp_current_db_version
			 */
			// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Replacing WP Core behavior is the goal here.
			$wp_current_db_version = __get_option( 'db_version' );
			// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Replacing WP Core behavior is the goal here.
			$wp_current_db_version = (int) $wp_current_db_version;

			if ( $wp_db_version !== $wp_current_db_version ) {
				if ( $dry_run ) {
					WP_CLI::success( "WordPress database will be upgraded from db version {$wp_current_db_version} to {$wp_db_version}." );
				} else {
					// WP upgrade isn't too fussy about generating MySQL warnings such as "Duplicate key name" during an upgrade so suppress.
					$wpdb->suppress_errors();

					// WP upgrade expects `$_SERVER['HTTP_HOST']` to be set in `wp_guess_url()`, otherwise get PHP notice.
					if ( ! isset( $_SERVER['HTTP_HOST'] ) ) {
						$_SERVER['HTTP_HOST'] = 'example.com';
					}

					wp_upgrade();

					WP_CLI::success( "WordPress database upgraded successfully from db version {$wp_current_db_version} to {$wp_db_version}." );
				}
			} else {
				WP_CLI::success( "WordPress database already at latest db version {$wp_db_version}." );
			}
		}
	}

	/**
	 * Gets download url based on version, locale and desired file type.
	 *
	 * @param $version
	 * @param string $locale
	 * @param string $file_type
	 * @return string
	 */
	private function get_download_url( $version, $locale = 'en_US', $file_type = 'zip' ) {

		if ( 'nightly' === $version ) {
			if ( 'zip' === $file_type ) {
				return 'https://wordpress.org/nightly-builds/wordpress-latest.zip';
			} else {
				WP_CLI::error( 'Nightly builds are only available in .zip format.' );
			}
		}

		$locale_subdomain = 'en_US' === $locale ? '' : substr( $locale, 0, 2 ) . '.';
		$locale_suffix    = 'en_US' === $locale ? '' : "-{$locale}";
		// Match 6.7.0 but not 6.0
		if ( substr_count( $version, '.' ) > 1 && substr( $version, -2 ) === '.0' ) {
			$version = substr( $version, 0, -2 );
		}

		return "https://{$locale_subdomain}wordpress.org/wordpress-{$version}{$locale_suffix}.{$file_type}";
	}

	/**
	 * Returns update information.
	 *
	 * @param array $assoc_args Associative array of arguments.
	 * @return array List of available updates , or an empty array if no updates are available.
	 */
	private function get_updates( $assoc_args ) {
		$force_check = Utils\get_flag_value( $assoc_args, 'force-check' );
		wp_version_check( [], $force_check );

		/**
		 * @var object{updates: array<object{version: string, locale: string, packages: object{partial?: string, full: string}}>}|false $from_api
		 */
		$from_api = get_site_transient( 'update_core' );
		if ( ! $from_api ) {
			return [];
		}

		$compare_version = str_replace( '-src', '', $GLOBALS['wp_version'] );

		$updates = [
			'major' => false,
			'minor' => false,
		];
		foreach ( $from_api->updates as $offer ) {

			$update_type = Utils\get_named_sem_ver( $offer->version, $compare_version );
			if ( ! $update_type ) {
				continue;
			}

			// WordPress follow its own versioning which is roughly equivalent to semver
			if ( 'minor' === $update_type ) {
				$update_type = 'major';
			} elseif ( 'patch' === $update_type ) {
				$update_type = 'minor';
			}

			if ( ! empty( $updates[ $update_type ] ) && ! Comparator::greaterThan( $offer->version, $updates[ $update_type ]['version'] ) ) {
				continue;
			}

			$updates[ $update_type ] = [
				'version'     => $offer->version,
				'update_type' => $update_type,
				'package_url' => ! empty( $offer->packages->partial ) ? $offer->packages->partial : $offer->packages->full,
			];
		}

		foreach ( $updates as $type => $value ) {
			if ( empty( $value ) ) {
				unset( $updates[ $type ] );
			}
		}

		foreach ( [ 'major', 'minor' ] as $type ) {
			if ( true === Utils\get_flag_value( $assoc_args, $type ) ) {
				return ! empty( $updates[ $type ] )
					? [ $updates[ $type ] ]
					: [];
			}
		}
		return array_values( $updates );
	}

	/**
	 * Clean up extra files.
	 *
	 * @param string $version_from Starting version that the installation was updated from.
	 * @param string $version_to   Target version that the installation is updated to.
	 * @param string $locale       Locale of the installation.
	 * @param bool   $insecure     Whether to retry without certificate validation on TLS handshake failure.
	 */
	private function cleanup_extra_files( $version_from, $version_to, $locale, $insecure ) {
		if ( ! $version_from || ! $version_to ) {
			WP_CLI::warning( 'Failed to find WordPress version. Please cleanup files manually.' );
			return;
		}

		$old_checksums = self::get_core_checksums( $version_from, $locale ?: 'en_US', $insecure );
		if ( ! is_array( $old_checksums ) ) {
			WP_CLI::warning( "{$old_checksums} Please cleanup files manually." );
			return;
		}

		$new_checksums = self::get_core_checksums( $version_to, $locale ?: 'en_US', $insecure );
		if ( ! is_array( $new_checksums ) ) {
			WP_CLI::warning( "{$new_checksums} Please cleanup files manually." );

			return;
		}

		// Compare the files from the old version and the new version in a case-insensitive manner,
		// to prevent files being incorrectly deleted on systems with case-insensitive filesystems
		// when core changes the case of filenames.
		// The main logic for this was taken from the Joomla project and adapted for WP.
		// See: https://github.com/joomla/joomla-cms/blob/bb5368c7ef9c20270e6e9fcc4b364cd0849082a5/administrator/components/com_admin/script.php#L8158

		$old_filepaths = array_keys( $old_checksums );
		$new_filepaths = array_keys( $new_checksums );

		$new_filepaths = array_combine( array_map( 'strtolower', $new_filepaths ), $new_filepaths );

		$old_filepaths_to_check = array_diff( $old_filepaths, $new_filepaths );

		foreach ( $old_filepaths_to_check as $old_filepath_to_check ) {
			$old_realpath = realpath( ABSPATH . $old_filepath_to_check );

			// On Unix without incorrectly cased file.
			if ( false === $old_realpath ) {
				continue;
			}

			$lowercase_old_filepath_to_check = strtolower( $old_filepath_to_check );

			if ( ! array_key_exists( $lowercase_old_filepath_to_check, $new_filepaths ) ) {
				$files_to_remove[] = $old_filepath_to_check;
				continue;
			}

			// We are now left with only the files that are similar from old to new except for their case.

			$old_basename      = basename( $old_realpath );
			$new_filepath      = $new_filepaths[ $lowercase_old_filepath_to_check ];
			$expected_basename = basename( $new_filepath );
			$new_realpath      = (string) realpath( ABSPATH . $new_filepath );
			$new_basename      = basename( $new_realpath );

			// On Windows or Unix with only the incorrectly cased file.
			if ( $new_basename !== $expected_basename ) {
				WP_CLI::debug( "Renaming file '{$old_filepath_to_check}' => '{$new_filepath}'", 'core' );

				rename( ABSPATH . $old_filepath_to_check, ABSPATH . $old_filepath_to_check . '.tmp' );
				rename( ABSPATH . $old_filepath_to_check . '.tmp', ABSPATH . $new_filepath );

				continue;
			}

			// There might still be an incorrectly cased file on other OS than Windows.
			if ( basename( $old_filepath_to_check ) === $old_basename ) {
				// Check if case-insensitive file system, eg on OSX.
				if ( fileinode( $old_realpath ) === fileinode( $new_realpath ) ) {
					$files = scandir( dirname( $new_realpath ) ) ?: [];

					// Check deeper because even realpath or glob might not return the actual case.
					if ( ! in_array( $expected_basename, $files, true ) ) {
						WP_CLI::debug( "Renaming file '{$old_filepath_to_check}' => '{$new_filepath}'", 'core' );

						rename( ABSPATH . $old_filepath_to_check, ABSPATH . $old_filepath_to_check . '.tmp' );
						rename( ABSPATH . $old_filepath_to_check . '.tmp', ABSPATH . $new_filepath );
					}
				} else {
					// On Unix with both files: Delete the incorrectly cased file.
					$files_to_remove[] = $old_filepath_to_check;
				}
			}
		}

		if ( ! empty( $files_to_remove ) ) {
			WP_CLI::log( 'Cleaning up files...' );

			$count = 0;
			foreach ( $files_to_remove as $file ) {

				// wp-content should be considered user data
				if ( 0 === stripos( $file, 'wp-content' ) ) {
					continue;
				}

				if ( file_exists( ABSPATH . $file ) ) {
					unlink( ABSPATH . $file );
					WP_CLI::log( "File removed: {$file}" );
					++$count;
				}
			}

			if ( $count ) {
				WP_CLI::log( number_format( $count ) . ' files cleaned up.' );
			} else {
				WP_CLI::log( 'No files found that need cleaning up.' );
			}
		}
	}

	private static function strip_content_dir( $zip_file ) {
		$new_zip_file = Utils\get_temp_dir() . uniqid( 'wp_' ) . '.zip';
		register_shutdown_function(
			function () use ( $new_zip_file ) {
				if ( file_exists( $new_zip_file ) ) {
					unlink( $new_zip_file );
				}
			}
		);
		// Duplicate file to avoid modifying the original, which could be cache.
		if ( ! copy( $zip_file, $new_zip_file ) ) {
			WP_CLI::error( 'Failed to copy ZIP file.' );
		}
		$zip = new ZipArchive();
		$res = $zip->open( $new_zip_file );
		if ( true === $res ) {
			// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
			for ( $i = 0; $i < $zip->numFiles; $i++ ) {
				$info = $zip->statIndex( $i );
				if ( ! $info ) {
					continue;
				}

				// Strip all files in wp-content/themes and wp-content/plugins
				// but leave the directories and index.php files intact.
				if ( in_array(
					$info['name'],
					array(
						'wordpress/wp-content/plugins/',
						'wordpress/wp-content/plugins/index.php',
						'wordpress/wp-content/themes/',
						'wordpress/wp-content/themes/index.php',
					),
					true
				) ) {
					continue;
				}

				if ( 0 === stripos( $info['name'], 'wordpress/wp-content/themes/' ) || 0 === stripos( $info['name'], 'wordpress/wp-content/plugins/' ) ) {
					$zip->deleteIndex( $i );
				}
			}
			$zip->close();
			return $new_zip_file;
		} else {
			WP_CLI::error( 'ZipArchive failed to open ZIP file.' );
		}
	}
}
