<?php
/**
 * Downloads, installs, updates, and manages a CiviCRM installation.
 *
 * ## EXAMPLES
 *
 *     # Download the latest stable CiviCRM core archive.
 *     $ wp civicrm core download
 *     Checking file to download...
 *     Downloading file...
 *     Success: CiviCRM downloaded to /tmp/
 *
 *     # Install the current stable version of CiviCRM with localization files.
 *     $ wp civicrm core install --l10n
 *     Success: Installed 1 of 1 plugins.
 *     Success: CiviCRM localization downloaded and extracted to: /wp-content/plugins/civicrm
 *
 *     # Check for the latest stable version of CiviCRM.
 *     $ wp civicrm core check-update
 *     +-----------+---------+-------------------------------------------------------------------------------------------+
 *     | Package   | Version | Package URL                                                                               |
 *     +-----------+---------+-------------------------------------------------------------------------------------------+
 *     | WordPress | 5.67.0  | https://storage.googleapis.com/civicrm/civicrm-stable/5.67.0/civicrm-5.67.0-wordpress.zip |
 *     | L10n      | 5.67.0  | https://storage.googleapis.com/civicrm/civicrm-stable/5.67.0/civicrm-5.67.0-l10n.tar.gz   |
 *     +-----------+---------+-------------------------------------------------------------------------------------------+
 *
 * @since 5.69
 */
class CLI_Tools_CiviCRM_Command_Core extends CLI_Tools_CiviCRM_Command {

  /**
   * @var string
   * The URL to check for CiviCRM upgrades.
   * @since 5.69
   * @access private
   */
  private $upgrade_url = 'https://download.civicrm.org/check';

  /**
   * @var string
   * The Google API URL to check for all top-level CiviCRM prefixes.
   * @since 5.69
   * @access private
   */
  private $google_url = 'https://storage.googleapis.com/storage/v1/b/civicrm/o/?delimiter=/';

  /**
   * @var string
   * The Google API query param to append for checking CiviCRM stable versions.
   * @since 5.69
   * @access private
   */
  private $google_prefix_stable = 'prefix=civicrm-stable/';

  /**
   * @var string
   * The common part of the Google API URL for CiviCRM release archive downloads.
   * @since 5.69
   * @access private
   */
  private $google_download_url = 'https://storage.googleapis.com/civicrm/';

  /**
   * Activates the CiviCRM plugin and loads the database.
   *
   * ## OPTIONS
   *
   * [--dbname=<dbname>]
   * : MySQL database name of your CiviCRM database. Defaults to the WordPress database name.
   *
   * [--dbpass=<dbpass>]
   * : MySQL password for your CiviCRM database. Defaults to the WordPress MySQL database password.
   *
   * [--dbuser=<dbuser>]
   * : MySQL username for your CiviCRM database. Defaults to the WordPress MySQL database username.
   *
   * [--dbhost=<dbhost>]
   * : MySQL host for your CiviCRM database. Defaults to the WordPress MySQL host.
   *
   * [--locale=<locale>]
   * : Locale to use for installation. Defaults to "en_US".
   *
   * [--ssl=<ssl>]
   * : The SSL setting for your website, e.g. '--ssl=on'. Defaults to "on".
   *
   * [--site-url=<site-url>]
   * : Domain for your website, e.g. 'mysite.com'.
   *
   * [--yes]
   * : Answer yes to the confirmation message.
   *
   * ## EXAMPLES
   *
   *     # Activate the CiviCRM plugin.
   *     $ wp civicrm core activate
   *     CiviCRM database credentials:
   *     +----------+-----------------------+
   *     | Field    | Value                 |
   *     +----------+-----------------------+
   *     | Database | civicrm_database_name |
   *     | Username | foo                   |
   *     | Password | dbpassword            |
   *     | Host     | localhost             |
   *     | Locale   | en_US                 |
   *     | SSL      | on                    |
   *     +----------+-----------------------+
   *     Do you want to continue? [y/n] y
   *     Creating file /httpdocs/wp-content/uploads/civicrm/civicrm.settings.php
   *     Success: CiviCRM data files initialized.
   *     Creating civicrm_* database tables in civicrm_database_name
   *     Success: CiviCRM database loaded.
   *     Plugin 'civicrm' activated.
   *     Success: Activated 1 of 1 plugins.
   *
   * @since 5.69
   *
   * @param array $args The WP-CLI positional arguments.
   * @param array $assoc_args The WP-CLI associative arguments.
   */
  public function activate($args, $assoc_args) {

    // Only install plugin if not already installed.
    $fetcher = new \WP_CLI\Fetchers\Plugin();
    $plugin_installed = $fetcher->get('civicrm');
    if (!$plugin_installed) {
      WP_CLI::error('You need to install CiviCRM first.');
    }

    // Get the path to the CiviCRM plugin directory.
    $plugin_path = $this->plugin_path_get();

    /*
     * Check for the presence of the CiviCRM core codebase.
     *
     * NOTE: This is *not* the CiviCRM plugin - it is the directory where the common
     * CiviCRM code lives. It always lives in a sub-directory of the plugin directory
     * called "civicrm".
     */
    global $crmPath;
    $crmPath = trailingslashit($plugin_path) . 'civicrm';
    if (!is_dir($crmPath)) {
      WP_CLI::error('CiviCRM core files are missing.');
    }

    // We need the CiviCRM classloader so that we can run `Civi\Setup`.
    $classLoaderPath = "$crmPath/CRM/Core/ClassLoader.php";
    if (!file_exists($classLoaderPath)) {
      WP_CLI::error('CiviCRM installer helper file is missing.');
    }

    // Grab associative arguments.
    $dbuser = (string) \WP_CLI\Utils\get_flag_value($assoc_args, 'dbuser', (defined('DB_USER') ? DB_USER : ''));
    $dbpass = (string) \WP_CLI\Utils\get_flag_value($assoc_args, 'dbpass', (defined('DB_PASSWORD') ? DB_PASSWORD : ''));
    $dbhost = (string) \WP_CLI\Utils\get_flag_value($assoc_args, 'dbhost', (defined('DB_HOST') ? DB_HOST : ''));
    $dbname = (string) \WP_CLI\Utils\get_flag_value($assoc_args, 'dbname', (defined('DB_NAME') ? DB_NAME : ''));
    $locale = (string) \WP_CLI\Utils\get_flag_value($assoc_args, 'locale', 'en_US');
    $ssl = (string) \WP_CLI\Utils\get_flag_value($assoc_args, 'ssl', 'on');
    $base_url = (string) \WP_CLI\Utils\get_flag_value($assoc_args, 'site-url', '');

    // Show database parameters.
    WP_CLI::log(WP_CLI::colorize('%GCiviCRM database credentials:%n'));
    $assoc_args['format'] = 'table';
    $feedback = [
      'Database' => $dbname,
      'Username' => $dbuser,
      'Password' => $dbpass,
      'Host' => $dbhost,
      'Locale' => $locale,
      'SSL' => $ssl,
    ];
    $assoc_args['fields'] = array_keys($feedback);
    $formatter = $this->formatter_get($assoc_args);
    $formatter->display_item($feedback);

    // Let's give folks a chance to exit now.
    WP_CLI::confirm(WP_CLI::colorize('%GDo you want to continue?%n'), $assoc_args);

    // ----------------------------------------------------------------------------
    // Activation and installation.
    // ----------------------------------------------------------------------------

    // Set some constants that CiviCRM requires.
    if (!defined('CIVICRM_PLUGIN_DIR')) {
      define('CIVICRM_PLUGIN_DIR', \WP_CLI\Utils\trailingslashit($plugin_path));
    }
    if (!defined('CIVICRM_PLUGIN_URL')) {
      define('CIVICRM_PLUGIN_URL', plugin_dir_url(CIVICRM_PLUGIN_DIR));
    }

    // Maybe set SSL.
    if ('on' === $ssl) {
      $_SERVER['HTTPS'] = 'on';
    }

    // Initialize civicrm-setup.
    require_once $classLoaderPath;
    CRM_Core_ClassLoader::singleton()->register();
    \Civi\Setup::assertProtocolCompatibility(1.0);
    \Civi\Setup::init(['cms' => 'WordPress', 'srcPath' => $crmPath]);
    $setup = \Civi\Setup::instance();

    // Apply essential arguments.
    $setup->getModel()->db = ['server' => $dbhost, 'username' => $dbuser, 'password' => $dbpass, 'database' => $dbname];
    $setup->getModel()->lang = $locale;

    /*
     * The "base URL" should already be known, either by:
     *
     * * The "site_url()" setting in WordPress standalone
     * * The URL flag in WordPress Multisite: --url=https://my-domain.com
     *
     * TODO: This means that the `--site_url` flag is basically redundant.
     */
    if (!empty($base_url)) {
      $protocol = ('on' === $ssl ? 'https' : 'http');
      $base_url = $protocol . '://' . $base_url;
      $setup->getModel()->cmsBaseUrl = trailingslashit($base_url);
    }

    // Validate system requirements.
    $reqs = $setup->checkRequirements();
    foreach ($reqs->getWarnings() as $msg) {
      WP_CLI::log(sprintf(WP_CLI::colorize('%YWARNING:%n %y(%s) %s:%n %s'), $msg['section'], $msg['name'], $msg['message']));
    }
    $errors = $reqs->getErrors();
    if ($errors) {
      foreach ($errors as $msg) {
        WP_CLI::log(sprintf(WP_CLI::colorize('%RERROR:%n %r(%s) %s:%n %s'), $msg['section'], $msg['name'], $msg['message']));
      }
      WP_CLI::error('Requirements check failed.');
    }

    // Install data files.
    $installed = $setup->checkInstalled();
    if (!$installed->isSettingInstalled()) {
      WP_CLI::log(sprintf(WP_CLI::colorize('%GCreating file%n %Y%s%n'), $setup->getModel()->settingsPath));
      $setup->installFiles();
    }
    else {
      WP_CLI::log(sprintf(WP_CLI::colorize('%gFound existing%n %Y%s%n %Gin%n %Y%s%n'), basename($setup->getModel()->settingsPath), dirname($setup->getModel()->settingsPath)));
      switch ($this->conflict_action_pick('civicrm.settings.php')) {
        case 'abort':
          WP_CLI::log(WP_CLI::colorize('%CAborted%n'));
          WP_CLI::halt(0);

        case 'overwrite':
          WP_CLI::log(sprintf(WP_CLI::colorize('%GRemoving%n %Y%s%n %Gfrom%n %Y%s%n'), basename($setup->getModel()->settingsPath), dirname($setup->getModel()->settingsPath)));
          $setup->uninstallFiles();
          WP_CLI::log(sprintf(WP_CLI::colorize('%GCreating%n %Y%s%n %Gin%n %Y%s%n'), basename($setup->getModel()->settingsPath), dirname($setup->getModel()->settingsPath)));
          $setup->installFiles();
          break;

        case 'keep':
          break;

        default:
          WP_CLI::error('Unrecognized action');
      }
    }

    WP_CLI::success('CiviCRM data files initialized.');

    // Clean the "templates_c" directory to avoid fatal error when overwriting the database.
    if (function_exists('civicrm_initialize')) {
      $this->bootstrap_civicrm();
      $config = CRM_Core_Config::singleton();
      $config->cleanup(1, FALSE);
    }

    // Install database.
    if (!$installed->isDatabaseInstalled()) {
      WP_CLI::log(sprintf(WP_CLI::colorize('%GCreating%n %Ycivicrm_*%n %Gdatabase tables in%n %Y%s%n'), $setup->getModel()->db['database']));
      $setup->installDatabase();
    }
    else {
      WP_CLI::log(sprintf(WP_CLI::colorize('%GFound existing%n %Ycivicrm_*%n database tables in%n %Y%s%n'), $setup->getModel()->db['database']));
      switch ($this->conflict_action_pick('database tables')) {
        case 'abort':
          WP_CLI::log(WP_CLI::colorize('%CAborted%n'));
          WP_CLI::halt(0);

        case 'overwrite':
          WP_CLI::log(sprintf(WP_CLI::colorize('%GRemoving%n %Ycivicrm_*%n database tables in%n %Y%s%n'), $setup->getModel()->db['database']));
          $setup->uninstallDatabase();
          WP_CLI::log(sprintf(WP_CLI::colorize('%GCreating%n %Ycivicrm_*%n database tables in%n %Y%s%n'), $setup->getModel()->db['database']));
          $setup->installDatabase();
          break;

        case 'keep':
          break;

        default:
          WP_CLI::error('Unrecognized action');
      }
    }

    WP_CLI::success('CiviCRM database loaded.');

    // Looking good, let's activate the CiviCRM plugin.
    WP_CLI::run_command(['plugin', 'activate', 'civicrm'], []);

  }

  /**
   * Back up the CiviCRM plugin files and database.
   *
   * ## OPTIONS
   *
   * [--backup-dir=<backup-dir>]
   * : Path to your CiviCRM backup directory. Default is one level above ABSPATH.
   *
   * [--also-include=<also-include>]
   * : Comma separated list of additional tables to back up based on wildcard search.
   *
   * [--yes]
   * : Answer yes to the confirmation message.
   *
   * ## EXAMPLES
   *
   *     # Standard backup.
   *     $ wp civicrm core backup
   *     Gathering system information.
   *     +------------------------+-----------------------------------------------------------------------+
   *     | Field                  | Value                                                                 |
   *     +------------------------+-----------------------------------------------------------------------+
   *     | Backup directory       | /example.com/civicrm-backup                                           |
   *     | Plugin path            | /example.com/httpdocs/wp-content/plugins/civicrm/                     |
   *     | Database name          | civicrm_db                                                            |
   *     | Database username      | dbuser                                                                |
   *     | Database password      | dbpassword                                                            |
   *     | Database host          | localhost                                                             |
   *     | Settings file          | /example.com/httpdocs/wp-content/uploads/civicrm/civicrm.settings.php |
   *     | Config and Log         | /example.com/httpdocs/wp-content/uploads/civicrm/ConfigAndLog/        |
   *     | Custom PHP             | Not found                                                             |
   *     | Custom templates       | Not found                                                             |
   *     | Compiled templates     | /example.com/httpdocs/wp-content/uploads/civicrm/templates_c/         |
   *     | Extensions directory   | /example.com/httpdocs/wp-content/uploads/civicrm/ext/                 |
   *     | Uploads directory      | /example.com/httpdocs/wp-content/uploads/civicrm/upload/              |
   *     | Image upload directory | /example.com/httpdocs/wp-content/uploads/civicrm/persist/contribute/  |
   *     | File upload directory  | /example.com/httpdocs/wp-content/uploads/civicrm/custom/              |
   *     +------------------------+-----------------------------------------------------------------------+
   *     Do you want to continue? [y/n] y
   *
   *     # Also back up tables not registered with CiviCRM.
   *     # In this case, also exports tables for the "Canadian Tax Receipts" extension.
   *     $ wp civicrm core backup --also-include='cdntaxreceipts_*'
   *     ...
   *
   * @since 5.69
   *
   * @param array $args The WP-CLI positional arguments.
   * @param array $assoc_args The WP-CLI associative arguments.
   */
  public function backup($args, $assoc_args) {

    // Grab associative arguments.
    $backup_dir = \WP_CLI\Utils\get_flag_value($assoc_args, 'backup-dir', trailingslashit(dirname(ABSPATH)) . 'civicrm-backup');
    $also_include = (string) \WP_CLI\Utils\get_flag_value($assoc_args, 'also-include', '');

    // ----------------------------------------------------------------------------
    // Build feedback table.
    // ----------------------------------------------------------------------------
    WP_CLI::log(WP_CLI::colorize('%GGathering system information.%n'));

    // Bootstrap CiviCRM.
    $this->bootstrap_civicrm();

    // Let's have a look for some CiviCRM variables.
    $config = CRM_Core_Config::singleton();

    // Build feedback.
    $feedback = [];
    if (!empty($backup_dir)) {
      $feedback['Backup directory'] = $backup_dir;
    }
    if (defined('CIVICRM_PLUGIN_DIR')) {
      $feedback['Plugin path'] = CIVICRM_PLUGIN_DIR;
    }
    else {
      $feedback['Plugin path'] = 'Not found';
    }
    if (defined('CIVICRM_DSN')) {
      $dsn = DB::parseDSN(CIVICRM_DSN);
      $feedback['Database name'] = $dsn['database'];
      $feedback['Database username'] = $dsn['username'];
      $feedback['Database password'] = $dsn['password'];
      $feedback['Database host'] = $dsn['hostspec'];
    }
    else {
      $feedback['Database Settings'] = 'Not found';
    }
    if (defined('CIVICRM_SETTINGS_PATH')) {
      $feedback['Settings file'] = CIVICRM_SETTINGS_PATH;
    }
    else {
      $feedback['Settings file'] = 'Not found';
    }
    if (!empty($config->configAndLogDir)) {
      $feedback['Config and Log'] = $config->configAndLogDir;
    }
    else {
      $feedback['Config and Log'] = 'Not found';
    }
    if (!empty($config->customPHPPathDir)) {
      $feedback['Custom PHP'] = $config->customPHPPathDir;
    }
    else {
      $feedback['Custom PHP'] = 'Not found';
    }
    if (!empty($config->customTemplateDir)) {
      $feedback['Custom templates'] = $config->customTemplateDir;
    }
    else {
      $feedback['Custom templates'] = 'Not found';
    }
    if (!empty($config->templateCompileDir)) {
      $feedback['Compiled templates'] = $config->templateCompileDir;
    }
    else {
      $feedback['Compiled templates'] = 'Not found';
    }
    if (!empty($config->extensionsDir)) {
      $feedback['Extensions directory'] = $config->extensionsDir;
    }
    else {
      $feedback['Extensions directory'] = 'Not found';
    }
    if (!empty($config->uploadDir)) {
      $feedback['Uploads directory'] = $config->uploadDir;
    }
    else {
      $feedback['Uploads directory'] = 'Not found';
    }
    if (!empty($config->imageUploadDir)) {
      $feedback['Image upload directory'] = $config->imageUploadDir;
    }
    else {
      $feedback['Image upload directory'] = 'Not found';
    }
    if (!empty($config->customFileUploadDir)) {
      $feedback['File upload directory'] = $config->customFileUploadDir;
    }
    else {
      $feedback['File upload directory'] = 'Not found';
    }

    // Render feedback.
    $assoc_args['fields'] = array_keys($feedback);
    $formatter = $this->formatter_get($assoc_args);
    $formatter->display_item($feedback);

    // Let's give folks a chance to exit now.
    WP_CLI::confirm(WP_CLI::colorize('%GDo you want to continue?%n'), $assoc_args);

    // ----------------------------------------------------------------------------
    // Validate backup directory.
    // ----------------------------------------------------------------------------
    $backup_dir = untrailingslashit($backup_dir);

    // Maybe create destination directory.
    if (!is_dir($backup_dir)) {
      if (!is_writable(dirname($backup_dir))) {
        WP_CLI::error("Insufficient permission to create directory '{$backup_dir}'.");
      }
      WP_CLI::log("Creating directory '{$backup_dir}'.");
      // Recursively create directory.
      if (!@mkdir($backup_dir, 0777, TRUE)) {
        $error = error_get_last();
        WP_CLI::error("Failed to create directory '{$backup_dir}': {$error['message']}.");
      }
    }

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

    // ----------------------------------------------------------------------------
    // Backup procedure.
    // ----------------------------------------------------------------------------

    // Maybe add extra filters.
    $also_include_args = '';
    if (!empty($also_include)) {
      $also_include_args = " --also-include={$also_include}";
    }

    // Use "wp civicrm db export" to export the CiviCRM database tables.
    WP_CLI::log('');
    WP_CLI::log(WP_CLI::colorize('%GExporting database...%n'));
    $command = 'civicrm db export' . $also_include_args . ' --result-file=' . $backup_dir . '/civicrm-db.sql';
    $options = ['launch' => FALSE, 'return' => FALSE];
    WP_CLI::runcommand($command, $options);
    WP_CLI::success('Database exported.');

    // Back up plugin directory.
    WP_CLI::log('');
    WP_CLI::log(WP_CLI::colorize('%GBacking up plugin directory...%n'));
    $plugin_path = $this->plugin_path_get();
    if (!$this->zip_compress($plugin_path, $backup_dir . '/civicrm.zip')) {
      WP_CLI::error('Could not compress plugin archive.');
    }
    WP_CLI::success('Plugin directory backed up.');

    // Back up "civicrm.settings.php" file.
    if (defined('CIVICRM_SETTINGS_PATH')) {
      WP_CLI::log('');
      WP_CLI::log(WP_CLI::colorize('%GBacking up Settings File...%n'));
      $dest_path = $backup_dir . '/civicrm.settings.php';
      if (file_exists($dest_path) && is_writable($dest_path)) {
        copy(CIVICRM_SETTINGS_PATH, $dest_path);
      }
      elseif (!file_exists($dest_path)) {
        copy(CIVICRM_SETTINGS_PATH, $dest_path);
      }
      else {
        WP_CLI::error("Could not copy '" . CIVICRM_SETTINGS_PATH . "' to backup directory.");
      }
      WP_CLI::success('Settings File backed up.');
    }

    // Back up Config and Log directory.
    if (!empty($config->configAndLogDir)) {
      WP_CLI::log('');
      WP_CLI::log(WP_CLI::colorize('%GBacking up Config and Log directory...%n'));
      if (!$this->zip_compress(untrailingslashit($config->configAndLogDir), $backup_dir . '/civicrm-config-log.zip')) {
        WP_CLI::error('Could not compress Config and Log archive.');
      }
      WP_CLI::success('Config and Log directory backed up.');
    }

    // Back up Custom PHP directory.
    if (!empty($config->customPHPPathDir)) {
      WP_CLI::log('');
      WP_CLI::log(WP_CLI::colorize('%GBacking up Custom PHP directory...%n'));
      if (!$this->zip_compress(untrailingslashit($config->customPHPPathDir), $backup_dir . '/civicrm-custom-php.zip')) {
        WP_CLI::error('Could not compress Custom PHP archive.');
      }
      WP_CLI::success('Custom PHP directory backed up.');
    }

    // Back up Custom templates directory.
    if (!empty($config->customTemplateDir)) {
      WP_CLI::log('');
      WP_CLI::log(WP_CLI::colorize('%GBacking up Custom Templates directory...%n'));
      if (!$this->zip_compress(untrailingslashit($config->customTemplateDir), $backup_dir . '/civicrm-custom-templates.zip')) {
        WP_CLI::error('Could not compress Custom Templates archive.');
      }
      WP_CLI::success('Custom Templates directory backed up.');
    }

    // Back up Compiled templates directory.
    if (!empty($config->templateCompileDir)) {
      WP_CLI::log('');
      WP_CLI::log(WP_CLI::colorize('%GBacking up Compiled Templates directory...%n'));
      if (!$this->zip_compress(untrailingslashit($config->templateCompileDir), $backup_dir . '/civicrm-compiled-templates.zip')) {
        WP_CLI::error('Could not compress Compiled templates archive.');
      }
      WP_CLI::success('Compiled Templates directory backed up.');
    }

    // Back up Extensions directory.
    if (!empty($config->extensionsDir)) {
      WP_CLI::log('');
      WP_CLI::log(WP_CLI::colorize('%GBacking up Extensions directory...%n'));
      if (!$this->zip_compress(untrailingslashit($config->extensionsDir), $backup_dir . '/civicrm-extensions.zip')) {
        WP_CLI::error('Could not compress Extensions archive.');
      }
      WP_CLI::success('Extensions directory backed up.');
    }

    // Back up Uploads directory.
    if (!empty($config->uploadDir)) {
      WP_CLI::log('');
      WP_CLI::log(WP_CLI::colorize('%GBacking up Uploads directory...%n'));
      if (!$this->zip_compress(untrailingslashit($config->uploadDir), $backup_dir . '/civicrm-uploads.zip')) {
        WP_CLI::error('Could not compress Uploads archive.');
      }
      WP_CLI::success('Uploads directory backed up.');
    }

    // Back up Image upload directory.
    if (!empty($config->imageUploadDir)) {
      WP_CLI::log('');
      WP_CLI::log(WP_CLI::colorize('%GBacking up Image Uploads directory...%n'));
      if (!$this->zip_compress(untrailingslashit($config->imageUploadDir), $backup_dir . '/civicrm-image-uploads.zip')) {
        WP_CLI::error('Could not compress Image Uploads archive.');
      }
      WP_CLI::success('Image Uploads directory backed up.');
    }

    // Back up File Uploads directory.
    if (!empty($config->customFileUploadDir)) {
      WP_CLI::log('');
      WP_CLI::log(WP_CLI::colorize('%GBacking up File Uploads directory...%n'));
      if (!$this->zip_compress(untrailingslashit($config->customFileUploadDir), $backup_dir . '/civicrm-file-uploads.zip')) {
        WP_CLI::error('Could not compress File Uploads archive.');
      }
      WP_CLI::success('File Uploads directory backed up.');
    }

  }

  /**
   * Checks for a CiviCRM version or matching localization archive.
   *
   * ## OPTIONS
   *
   * [--version=<version>]
   * : Specify the version to check. Accepts a version number, 'stable', 'rc' or 'nightly'.
   *
   * [--l10n]
   * : Get the localization file data for the specified version.
   *
   * [--format=<format>]
   * : Render output in a particular format.
   * ---
   * default: table
   * options:
   *   - table
   *   - json
   *   - url
   *   - version
   * ---
   *
   * ## EXAMPLES
   *
   *     # Check for a stable version of CiviCRM
   *     $ wp civicrm core check-version --version=5.17.2
   *     +-----------+---------+-------------------------------------------------------------------------------------------+
   *     | Package   | Version | Package URL                                                                               |
   *     +-----------+---------+-------------------------------------------------------------------------------------------+
   *     | WordPress | 5.17.2  | https://storage.googleapis.com/civicrm/civicrm-stable/5.17.2/civicrm-5.17.2-wordpress.zip |
   *     | L10n      | 5.17.2  | https://storage.googleapis.com/civicrm/civicrm-stable/5.17.2/civicrm-5.17.2-l10n.tar.gz   |
   *     +-----------+---------+-------------------------------------------------------------------------------------------+
   *
   *     # Get the URL for a stable version of CiviCRM
   *     $ wp civicrm core check-version --version=5.17.2 --format=url
   *     https://storage.googleapis.com/civicrm/civicrm-stable/5.17.2/civicrm-5.17.2-wordpress.zip
   *
   *     # Get the URL for a stable version of the CiviCRM localisation archive
   *     $ wp civicrm core check-version --version=5.17.2 --format=url --l10n
   *     https://storage.googleapis.com/civicrm/civicrm-stable/5.17.2/civicrm-5.17.2-l10n.tar.gz
   *
   *     # Get the JSON-formatted data for a stable version of CiviCRM
   *     $ wp civicrm core check-version --version=5.17.2 --format=json
   *     {"version":"5.17.2","tar":{"L10n":"civicrm-stable\/5.17.2\/civicrm-5.17.2-l10n.tar.gz","WordPress":"civicrm-stable\/5.17.2\/civicrm-5.17.2-wordpress.zip"}}
   *
   *     # Get the latest nightly version of CiviCRM
   *     $ wp civicrm core check-version --version=nightly --format=version
   *     5.59.alpha1
   *
   * @subcommand check-version
   *
   * @since 5.69
   *
   * @param array $args The WP-CLI positional arguments.
   * @param array $assoc_args The WP-CLI associative arguments.
   */
  public function check_version($args, $assoc_args) {

    // Grab associative arguments.
    $version = (string) \WP_CLI\Utils\get_flag_value($assoc_args, 'version', 'stable');
    $l10n = (bool) \WP_CLI\Utils\get_flag_value($assoc_args, 'l10n', FALSE);
    $format = (string) \WP_CLI\Utils\get_flag_value($assoc_args, 'format', 'table');

    // Pass to "check-update" for "stable", "rc" or "nightly".
    if (in_array($version, ['stable', 'rc', 'nightly'])) {
      $options = ['launch' => FALSE, 'return' => FALSE];
      $command = 'civicrm core check-update --version=' . $version . ' --format=' . $format . (empty($l10n) ? '' : ' --l10n');
      WP_CLI::runcommand($command, $options);
      return;
    }

    // Check for valid release.
    $versions = $this->releases_get();
    if (!in_array($version, $versions)) {
      WP_CLI::error(sprintf(WP_CLI::colorize('Version %Y%s%n is not a valid CiviCRM version.'), $version));
    }

    // Get the release data.
    $data = $this->release_data_get($version);

    switch ($format) {

      // URL-only output.
      case 'url':
        if ($l10n) {
          echo $this->google_download_url . $data['L10n'] . "\n";
        }
        else {
          echo $this->google_download_url . $data['WordPress'] . "\n";
        }
        break;

      // Version-only output.
      case 'version':
        echo $version . "\n";
        break;

      // Display output as json.
      case 'json':
        // Use a similar format to the Version Check API.
        $info = [
          'version' => $version,
          'tar' => $data,
        ];
        $json = json_encode($info);
        if (JSON_ERROR_NONE !== json_last_error()) {
          WP_CLI::error(sprintf(WP_CLI::colorize('Failed to encode JSON: %Y%s.%n'), json_last_error_msg()));
        }
        echo $json . "\n";
        break;

      // Display output as table (default).
      case 'table':
      default:
        // Build the rows.
        $rows = [];
        $fields = ['Package', 'Version', 'Package URL'];
        $rows[] = [
          'Package' => 'WordPress',
          'Version' => $version,
          'Package URL' => $this->google_download_url . $data['WordPress'],
        ];
        $rows[] = [
          'Package'  => 'L10n',
          'Version' => $version,
          'Package URL' => $this->google_download_url . $data['L10n'],
        ];

        // Display the rows.
        $args = ['format' => $format];
        $formatter = new \WP_CLI\Formatter($args, $fields);
        $formatter->display_items($rows);

    }

  }

  /**
   * Checks for CiviCRM updates via Version Check API.
   *
   * ## OPTIONS
   *
   * [--version=<version>]
   * : Specify the version to get.
   * ---
   * default: stable
   * options:
   *   - nightly
   *   - rc
   *   - stable
   * ---
   *
   * [--l10n]
   * : Get the localization file data for the specified version.
   *
   * [--format=<format>]
   * : Render output in a particular format.
   * ---
   * default: table
   * options:
   *   - table
   *   - json
   *   - url
   *   - version
   * ---
   *
   * ## EXAMPLES
   *
   *     # Check for the latest stable version of CiviCRM
   *     $ wp civicrm core check-update
   *     +-----------+---------+-------------------------------------------------------------------------------------------+
   *     | Package   | Version | Package URL                                                                               |
   *     +-----------+---------+-------------------------------------------------------------------------------------------+
   *     | WordPress | 5.67.0  | https://storage.googleapis.com/civicrm/civicrm-stable/5.67.0/civicrm-5.67.0-wordpress.zip |
   *     | L10n      | 5.67.0  | https://storage.googleapis.com/civicrm/civicrm-stable/5.67.0/civicrm-5.67.0-l10n.tar.gz   |
   *     +-----------+---------+-------------------------------------------------------------------------------------------+
   *
   *     # Get the URL for the latest stable version of CiviCRM core
   *     $ wp civicrm core check-update --format=url
   *     https://storage.googleapis.com/civicrm/civicrm-stable/5.67.0/civicrm-5.67.0-wordpress.zip
   *
   *     # Get the URL for the latest stable version of CiviCRM localisation archive
   *     $ wp civicrm core check-update --format=url --l10n
   *     https://storage.googleapis.com/civicrm/civicrm-stable/5.67.0/civicrm-5.67.0-l10n.tar.gz
   *
   *     # Get the complete JSON-formatted data for the latest RC version of CiviCRM core
   *     $ wp civicrm core check-update --version=rc --format=json
   *     {"version":"5.58.beta1","rev":"5.58.beta1-202301260741" [...] "pretty":"Thu, 26 Jan 2023 07:41:00 +0000"}}
   *
   * @subcommand check-update
   *
   * @since 5.69
   *
   * @param array $args The WP-CLI positional arguments.
   * @param array $assoc_args The WP-CLI associative arguments.
   */
  public function check_update($args, $assoc_args) {

    // Grab associative arguments.
    $version = (string) \WP_CLI\Utils\get_flag_value($assoc_args, 'version', 'stable');
    $l10n = (bool) \WP_CLI\Utils\get_flag_value($assoc_args, 'l10n', FALSE);
    $format = (string) \WP_CLI\Utils\get_flag_value($assoc_args, 'format', 'table');

    // Look up the data.
    $url = $this->upgrade_url . '?stability=' . $version;
    $response = $this->http_get_response($url);

    // Try and decode response.
    $lookup = json_decode($response, TRUE);
    if (JSON_ERROR_NONE !== json_last_error()) {
      WP_CLI::error(sprintf(WP_CLI::colorize('Failed to decode JSON: %Y%s.%n'), json_last_error_msg()));
    }

    // Sanity checks.
    if (empty($lookup)) {
      WP_CLI::error(sprintf(WP_CLI::colorize('Version not found at: %Y%s%n'), $url));
    }
    if (empty($lookup['tar']['WordPress'])) {
      WP_CLI::error(sprintf(WP_CLI::colorize('No WordPress version found at: %Y%s%n'), $url));
    }

    switch ($format) {

      // URL-only output.
      case 'url':
        if ($l10n) {
          echo $lookup['tar']['L10n'] . "\n";
        }
        else {
          echo $lookup['tar']['WordPress'] . "\n";
        }
        break;

      // Version-only output.
      case 'version':
        echo $lookup['version'] . "\n";
        break;

      // Display output as json.
      case 'json':
        echo $response . "\n";
        break;

      // Display output as table (default).
      case 'table':
      default:
        // Build the rows.
        $rows = [];
        $fields = ['Package', 'Version', 'Package URL'];
        $rows[] = [
          'Package' => 'WordPress',
          'Version' => $lookup['version'],
          'Package URL' => $lookup['tar']['WordPress'],
        ];
        $rows[] = [
          'Package' => 'L10n',
          'Version' => $lookup['version'],
          'Package URL' => $lookup['tar']['L10n'],
        ];

        // Display the rows.
        $args = ['format' => $format];
        $formatter = new \WP_CLI\Formatter($args, $fields);
        $formatter->display_items($rows);

    }

  }

  /**
   * Downloads CiviCRM core files or localization files.
   *
   * Downloads and extracts CiviCRM core files or localization files to the
   * specified path. Uses the local temp directory when no path is specified.
   *
   * ## OPTIONS
   *
   * [--version=<version>]
   * : Specify the CiviCRM version to get. Accepts a version number, 'stable', 'rc' or 'nightly'. Defaults to latest stable version.
   *
   * [--l10n]
   * : Get the localization file for the specified version.
   *
   * [--destination=<destination>]
   * : Specify the absolute path to put the archive file. Defaults to local temp directory.
   *
   * [--insecure]
   * : Retry 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 false.
   *
   * ## EXAMPLES
   *
   *     # Download the latest stable CiviCRM core archive.
   *     $ wp civicrm core download
   *     Checking file to download...
   *     Downloading file...
   *     Success: CiviCRM downloaded to /tmp/
   *
   *     # Download and extract a stable CiviCRM core archive to a directory.
   *     $ wp civicrm core download --version=5.17.2 --extract --destination=/some/path
   *     Checking file to download...
   *     Downloading file...
   *     Extracting zip archive...
   *     Success: CiviCRM downloaded and extracted to: /some/path/
   *
   *     # Quietly download a stable CiviCRM core archive.
   *     $ wp civicrm core download --version=5.17.2 --quiet
   *     /tmp/civicrm-5.17.2-wordpress.zip
   *
   *     # Download and extract a stable CiviCRM localization archive to a directory.
   *     $ wp civicrm core download --version=5.17.2 --l10n --extract --destination=/some/path
   *     Checking file to download...
   *     Downloading file...
   *     Extracting tar.gz archive...
   *     Success: CiviCRM localization downloaded and extracted to: /some/path/
   *
   *     # Quietly download a stable CiviCRM localization archive.
   *     $ wp civicrm core download --version=5.17.2 --l10n --quiet
   *     /tmp/civicrm-5.17.2-l10n.tar.gz
   *
   * @since 5.69
   *
   * @param array $args The WP-CLI positional arguments.
   * @param array $assoc_args The WP-CLI associative arguments.
   */
  public function download($args, $assoc_args) {

    // Grab associative arguments.
    $version = (string) \WP_CLI\Utils\get_flag_value($assoc_args, 'version', 'stable');
    $l10n = (bool) \WP_CLI\Utils\get_flag_value($assoc_args, 'l10n', FALSE);
    $download_dir = (string) \WP_CLI\Utils\get_flag_value($assoc_args, 'destination', \WP_CLI\Utils\get_temp_dir());
    $insecure = (bool) \WP_CLI\Utils\get_flag_value($assoc_args, 'insecure', FALSE);
    $extract = (bool) \WP_CLI\Utils\get_flag_value($assoc_args, 'extract', FALSE);

    // Maybe create destination directory.
    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}'.");
      // Recursively create directory.
      if (!@mkdir($download_dir, 0777, TRUE)) {
        $error = error_get_last();
        WP_CLI::error("Failed to create directory '{$download_dir}': {$error['message']}.");
      }
    }

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

    // Use "wp civicrm core check-version" to find out which file to download.
    WP_CLI::log(WP_CLI::colorize('%GChecking' . (empty($l10n) ? '' : ' localization') . ' file to download...%n'));
    $options = ['launch' => FALSE, 'return' => TRUE];
    $command = 'civicrm core check-version --version=' . $version . ' --format=url' . (empty($l10n) ? '' : ' --l10n');
    $url = WP_CLI::runcommand($command, $options);

    // Configure the download.
    $headers = [];
    $options = [
      'insecure' => (bool) $insecure,
    ];

    // Do the download now.
    WP_CLI::log(WP_CLI::colorize('%GDownloading file...%n'));
    $archive = $this->file_download($url, $download_dir, $headers, $options);

    // Stop early if not extracting.
    if (empty($extract)) {
      if (empty($l10n)) {
        WP_CLI::success(sprintf(WP_CLI::colorize('CiviCRM downloaded to: %Y%s%n'), $download_dir));
      }
      else {
        WP_CLI::success(sprintf(WP_CLI::colorize('CiviCRM localization downloaded to: %Y%s%n'), $download_dir));
      }
      if (!empty(WP_CLI::get_config('quiet'))) {
        echo $archive . "\n";
      }
      WP_CLI::halt(0);
    }

    // Extract the download.
    if (empty($l10n)) {
      if (!$this->unzip($archive, $download_dir)) {
        WP_CLI::error('Could not extract zipfile.');
      }
      WP_CLI::success(sprintf(WP_CLI::colorize('CiviCRM downloaded and extracted to: %Y%s%n'), $download_dir));
    }
    else {
      if (!$this->untar($archive, $download_dir)) {
        WP_CLI::error('Could not extract tarfile.');
      }
      WP_CLI::success(sprintf(WP_CLI::colorize('CiviCRM localization downloaded and extracted to: %Y%s%n'), $download_dir));
    }

  }

  /**
   * Install the CiviCRM plugin.
   *
   * This command obviously can't be used until the CiviCRM plugin has been installed.
   * It is included here for completeness and in preparation for creating a package.
   * If you want to use this command, you can install the CLI Tools for CiviCRM plugin
   * so that no conflicts occur when you call it.
   *
   * ## OPTIONS
   *
   * [--version=<version>]
   * : Specify the CiviCRM version to get. Accepts a version number, 'stable', 'rc' or 'nightly'. Defaults to latest stable version.
   *
   * [--zipfile=<zipfile>]
   * : Path to your CiviCRM zip file. If specified --version is ignored.
   *
   * [--l10n]
   * : Additionally install the localization files for the specified version.
   *
   * [--l10n-tarfile=<l10n-tarfile>]
   * : Path to your l10n tar.gz file. If specified --l10n is ignored.
   *
   * [--force]
   * : If set, the command will overwrite any installed version of the plugin, without prompting for confirmation.
   *
   * ## EXAMPLES
   *
   *     # Install the current stable version of CiviCRM.
   *     $ wp civicrm core install
   *     Success: Installed 1 of 1 plugins.
   *
   *     # Install the current stable version of CiviCRM with localization files.
   *     $ wp civicrm core install --l10n
   *     Success: Installed 1 of 1 plugins.
   *     Success: CiviCRM localization downloaded and extracted to: /wp-content/plugins/civicrm
   *
   *     # Install a specific version of CiviCRM.
   *     $ wp civicrm core install --version=5.56.2
   *     Success: Installed 1 of 1 plugins.
   *
   * @since 5.69
   *
   * @param array $args The WP-CLI positional arguments.
   * @param array $assoc_args The WP-CLI associative arguments.
   */
  public function install($args, $assoc_args) {

    // Grab associative arguments.
    $version = (string) \WP_CLI\Utils\get_flag_value($assoc_args, 'version', 'stable');
    $zipfile = (string) \WP_CLI\Utils\get_flag_value($assoc_args, 'zipfile', '');
    $l10n = (string) \WP_CLI\Utils\get_flag_value($assoc_args, 'l10n', '');
    $l10n_tarfile = (string) \WP_CLI\Utils\get_flag_value($assoc_args, 'l10n-tarfile', '');
    $force = (bool) \WP_CLI\Utils\get_flag_value($assoc_args, 'force', FALSE);

    // Get the path to the CiviCRM plugin directory.
    $plugin_path = $this->plugin_path_get();

    // Only install plugin if not already installed.
    $fetcher = new \WP_CLI\Fetchers\Plugin();
    $installed = $fetcher->get('civicrm');
    if (!$installed || !empty($force)) {

      // When no zipfile is specified.
      if (!empty($version) && empty($zipfile)) {

        // Use "wp civicrm core check-version" to find out which file to download.
        WP_CLI::log(WP_CLI::colorize('%GChecking plugin file to download...%n'));
        $options = ['launch' => FALSE, 'return' => TRUE];
        $command = 'civicrm core check-version --version=' . $version . ' --format=url';
        $url = WP_CLI::runcommand($command, $options);

        // Use "wp plugin install" to install CiviCRM core.
        $options = ['launch' => FALSE, 'return' => FALSE];
        $command = 'plugin install ' . $url . (empty($force) ? '' : ' --force');
        WP_CLI::runcommand($command, $options);

      }
      elseif (!empty($zipfile)) {

        // Default extraction options.
        $extract_options = [
          'clear_destination' => FALSE,
          'clear_working' => FALSE,
        ];

        // If forcing, overwrite existing CiviCRM plugin directory.
        if (!empty($force)) {
          $extract_options['clear_destination'] = TRUE;
        }

        // Let's do it.
        $this->zip_extract($zipfile, $plugin_path, $extract_options);

      }

    }
    elseif (empty($l10n) && empty($l10n_tarfile)) {

      // Bail when plugin is installed and no localization archive is specified.
      WP_CLI::error('CiviCRM is already installed.');

    }

    // When localization is wanted but no archive is specified.
    if (!empty($l10n) && empty($l10n_tarfile)) {

      // Use "wp civicrm core check-version" to find out which file to download.
      $options = ['launch' => FALSE, 'return' => TRUE];
      $command = 'civicrm core check-version --version=' . $version . ' --l10n --format=url';
      $url = WP_CLI::runcommand($command, $options);

      // Use "wp civicrm core download" to download and extract.
      $options = ['launch' => FALSE, 'return' => FALSE];
      $command = 'civicrm core download --version=' . $version . ' --l10n --extract --destination=' . $plugin_path;
      WP_CLI::runcommand($command, $options);

    }
    elseif (!empty($l10n_tarfile)) {

      // Extract localization archive to plugin directory.
      WP_CLI::log(sprintf(WP_CLI::colorize('Extracting localization archive to: %y%s%n'), $plugin_path));
      if (!$this->untar($l10n_tarfile, $plugin_path)) {
        WP_CLI::error('Could not extract localization archive.');
      }
      WP_CLI::success(sprintf(WP_CLI::colorize('CiviCRM localization files extracted to: %Y%s%n'), $plugin_path));

    }

  }

  /**
   * Restore the CiviCRM plugin files and database from a backup created with `wp civicrm backup`.
   *
   * ## OPTIONS
   *
   * [--backup-dir=<backup-dir>]
   * : Path to your CiviCRM backup directory. Default is one level above ABSPATH.
   *
   * [--also-include=<also-include>]
   * : Comma separated list of additional tables to restore based on wildcard search. Ensures existing tables are cleared.
   *
   * [--yes]
   * : Answer yes to the confirmation message.
   *
   * ## EXAMPLES
   *
   *     # Standard restore.
   *     $ wp civicrm core restore
   *     Gathering system information.
   *     +------------------------+-----------------------------------------------------------------------+
   *     | Field                  | Value                                                                 |
   *     +------------------------+-----------------------------------------------------------------------+
   *     | Backup directory       | /example.com/civicrm-backup                                           |
   *     | Plugin path            | /example.com/httpdocs/wp-content/plugins/civicrm/                     |
   *     | Database name          | civicrm_db                                                            |
   *     | Database username      | dbuser                                                                |
   *     | Database password      | dbpassword                                                            |
   *     | Database host          | localhost                                                             |
   *     | Settings file          | /example.com/httpdocs/wp-content/uploads/civicrm/civicrm.settings.php |
   *     | Config and Log         | /example.com/httpdocs/wp-content/uploads/civicrm/ConfigAndLog/        |
   *     | Custom PHP             | Not found                                                             |
   *     | Custom templates       | Not found                                                             |
   *     | Compiled templates     | /example.com/httpdocs/wp-content/uploads/civicrm/templates_c/         |
   *     | Extensions directory   | /example.com/httpdocs/wp-content/uploads/civicrm/ext/                 |
   *     | Uploads directory      | /example.com/httpdocs/wp-content/uploads/civicrm/upload/              |
   *     | Image upload directory | /example.com/httpdocs/wp-content/uploads/civicrm/persist/contribute/  |
   *     | File upload directory  | /example.com/httpdocs/wp-content/uploads/civicrm/custom/              |
   *     +------------------------+-----------------------------------------------------------------------+
   *     Do you want to continue? [y/n] y
   *
   *     # Also restore tables not registered with CiviCRM.
   *     # In this case, also correctly restores tables for the "Canadian Tax Receipts" extension.
   *     $ wp civicrm core restore --also-include='cdntaxreceipts_*'
   *     ...
   *
   * @since 5.69
   *
   * @param array $args The WP-CLI positional arguments.
   * @param array $assoc_args The WP-CLI associative arguments.
   */
  public function restore($args, $assoc_args) {

    // Grab associative arguments.
    $backup_dir = \WP_CLI\Utils\get_flag_value($assoc_args, 'backup-dir', trailingslashit(dirname(ABSPATH)) . 'civicrm-backup');
    $also_include = (string) \WP_CLI\Utils\get_flag_value($assoc_args, 'also-include', '');

    // ----------------------------------------------------------------------------
    // Build feedback table.
    // ----------------------------------------------------------------------------
    WP_CLI::log(WP_CLI::colorize('%GGathering system information.%n'));

    // Bootstrap CiviCRM.
    $this->bootstrap_civicrm();

    // Let's have a look for some CiviCRM variables.
    $config = CRM_Core_Config::singleton();

    // Build feedback.
    $feedback = [];
    if (!empty($backup_dir)) {
      $feedback['Backup directory'] = $backup_dir;
    }
    if (defined('CIVICRM_PLUGIN_DIR')) {
      $feedback['Plugin path'] = CIVICRM_PLUGIN_DIR;
    }
    else {
      $feedback['Plugin path'] = 'Not found';
    }
    if (defined('CIVICRM_DSN')) {
      $dsn = DB::parseDSN(CIVICRM_DSN);
      $feedback['Database name'] = $dsn['database'];
      $feedback['Database username'] = $dsn['username'];
      $feedback['Database password'] = $dsn['password'];
      $feedback['Database host'] = $dsn['hostspec'];
    }
    else {
      $feedback['Database Settings'] = 'Not found';
    }
    if (defined('CIVICRM_SETTINGS_PATH')) {
      $feedback['Settings file'] = CIVICRM_SETTINGS_PATH;
    }
    else {
      $feedback['Settings file'] = 'Not found';
    }
    if (!empty($config->configAndLogDir)) {
      $feedback['Config and Log'] = $config->configAndLogDir;
    }
    else {
      $feedback['Config and Log'] = 'Not found';
    }
    if (!empty($config->customPHPPathDir)) {
      $feedback['Custom PHP'] = $config->customPHPPathDir;
    }
    else {
      $feedback['Custom PHP'] = 'Not found';
    }
    if (!empty($config->customTemplateDir)) {
      $feedback['Custom templates'] = $config->customTemplateDir;
    }
    else {
      $feedback['Custom templates'] = 'Not found';
    }
    if (!empty($config->templateCompileDir)) {
      $feedback['Compiled templates'] = $config->templateCompileDir;
    }
    else {
      $feedback['Compiled templates'] = 'Not found';
    }
    if (!empty($config->extensionsDir)) {
      $feedback['Extensions directory'] = $config->extensionsDir;
    }
    else {
      $feedback['Extensions directory'] = 'Not found';
    }
    if (!empty($config->uploadDir)) {
      $feedback['Uploads directory'] = $config->uploadDir;
    }
    else {
      $feedback['Uploads directory'] = 'Not found';
    }
    if (!empty($config->imageUploadDir)) {
      $feedback['Image upload directory'] = $config->imageUploadDir;
    }
    else {
      $feedback['Image upload directory'] = 'Not found';
    }
    if (!empty($config->customFileUploadDir)) {
      $feedback['File upload directory'] = $config->customFileUploadDir;
    }
    else {
      $feedback['File upload directory'] = 'Not found';
    }

    // Render feedback.
    $assoc_args['fields'] = array_keys($feedback);
    $formatter = $this->formatter_get($assoc_args);
    $formatter->display_item($feedback);

    // Let's give folks a chance to exit now.
    WP_CLI::confirm(WP_CLI::colorize('%GDo you want to continue?%n'), $assoc_args);

    // Ensure there's no trailing slash.
    $backup_dir = untrailingslashit($backup_dir);

    // ----------------------------------------------------------------------------
    // Restore procedure.
    // ----------------------------------------------------------------------------

    // Restore File Uploads directory.
    if (!empty($config->customFileUploadDir) && file_exists($backup_dir . '/civicrm-file-uploads.zip')) {
      WP_CLI::log('');
      WP_CLI::log(WP_CLI::colorize('%GRestoring File Uploads directory...%n'));
      $zipfile = $backup_dir . '/civicrm-file-uploads.zip';
      $destination = untrailingslashit($config->customFileUploadDir);
      $this->zip_overwrite($zipfile, $destination);
      WP_CLI::success('File Uploads directory restored.');
    }

    // Restore Image upload directory.
    if (!empty($config->imageUploadDir) && file_exists($backup_dir . '/civicrm-image-uploads.zip')) {
      WP_CLI::log('');
      WP_CLI::log(WP_CLI::colorize('%GRestoring Image Uploads directory...%n'));
      $zipfile = $backup_dir . '/civicrm-image-uploads.zip';
      $destination = untrailingslashit($config->imageUploadDir);
      $this->zip_overwrite($zipfile, $destination);
      WP_CLI::success('Image Uploads directory restored.');
    }

    // Restore Uploads directory.
    if (!empty($config->uploadDir) && file_exists($backup_dir . '/civicrm-uploads.zip')) {
      WP_CLI::log('');
      WP_CLI::log(WP_CLI::colorize('%GRestoring Uploads directory...%n'));
      $zipfile = $backup_dir . '/civicrm-uploads.zip';
      $destination = untrailingslashit($config->uploadDir);
      $this->zip_overwrite($zipfile, $destination);
      WP_CLI::success('Uploads directory restored.');
    }

    // Restore Extensions directory.
    if (!empty($config->extensionsDir) && file_exists($backup_dir . '/civicrm-extensions.zip')) {
      WP_CLI::log('');
      WP_CLI::log(WP_CLI::colorize('%GRestoring Extensions directory...%n'));
      $zipfile = $backup_dir . '/civicrm-extensions.zip';
      $destination = untrailingslashit($config->extensionsDir);
      $this->zip_overwrite($zipfile, $destination);
      WP_CLI::success('Extensions directory restored.');
    }

    // Restore Compiled templates directory.
    if (!empty($config->templateCompileDir) && file_exists($backup_dir . '/civicrm-compiled-templates.zip')) {
      WP_CLI::log('');
      WP_CLI::log(WP_CLI::colorize('%GRestoring Compiled Templates directory...%n'));
      $zipfile = $backup_dir . '/civicrm-compiled-templates.zip';
      $destination = untrailingslashit($config->templateCompileDir);
      $this->zip_overwrite($zipfile, $destination);
      WP_CLI::success('Compiled Templates directory restored.');
    }

    // Restore Custom templates directory.
    if (!empty($config->customTemplateDir) && file_exists($backup_dir . '/civicrm-custom-templates.zip')) {
      WP_CLI::log('');
      WP_CLI::log(WP_CLI::colorize('%GRestoring Custom Templates directory...%n'));
      $zipfile = $backup_dir . '/civicrm-custom-templates.zip';
      $destination = untrailingslashit($config->customTemplateDir);
      $this->zip_overwrite($zipfile, $destination);
      WP_CLI::success('Custom Templates directory restored.');
    }

    // Restore Custom PHP directory.
    if (!empty($config->customPHPPathDir) && file_exists($backup_dir . '/civicrm-custom-php.zip')) {
      WP_CLI::log('');
      WP_CLI::log(WP_CLI::colorize('%GRestoring Custom PHP directory...%n'));
      $zipfile = $backup_dir . '/civicrm-custom-php.zip';
      $destination = untrailingslashit($config->customPHPPathDir);
      $this->zip_overwrite($zipfile, $destination);
      WP_CLI::success('Custom PHP directory restored.');
    }

    // Restore Config and Log directory.
    if (!empty($config->configAndLogDir) && file_exists($backup_dir . '/civicrm-config-log.zip')) {
      WP_CLI::log('');
      WP_CLI::log(WP_CLI::colorize('%GRestoring Config and Log directory...%n'));
      $zipfile = $backup_dir . '/civicrm-config-log.zip';
      $destination = untrailingslashit($config->configAndLogDir);
      $this->zip_overwrite($zipfile, $destination);
      WP_CLI::success('Config and Log directory restored.');
    }

    // Restore "civicrm.settings.php" file.
    if (defined('CIVICRM_SETTINGS_PATH') && file_exists($backup_dir . '/civicrm.settings.php')) {
      WP_CLI::log('');
      WP_CLI::log(WP_CLI::colorize('%GRestoring Settings File...%n'));
      $source_path = $backup_dir . '/civicrm.settings.php';
      if (file_exists(CIVICRM_SETTINGS_PATH) && is_writable(CIVICRM_SETTINGS_PATH)) {
        copy(CIVICRM_SETTINGS_PATH, $source_path);
      }
      elseif (!file_exists(CIVICRM_SETTINGS_PATH)) {
        copy($source_path, CIVICRM_SETTINGS_PATH);
      }
      else {
        WP_CLI::error("Could not restore '" . CIVICRM_SETTINGS_PATH . "' from backup directory.");
      }
      WP_CLI::success('Settings File restored.');
    }

    // Use "wp plugin install" to restore plugin directory.
    if (file_exists($backup_dir . '/civicrm.zip')) {
      WP_CLI::log('');
      WP_CLI::log(WP_CLI::colorize('%GRestoring plugin directory...%n'));
      $options = ['launch' => FALSE, 'return' => FALSE];
      $command = 'plugin install ' . $backup_dir . '/civicrm.zip --force';
      WP_CLI::runcommand($command, $options);
      WP_CLI::success('Plugin directory restored.');
    }

    // Use "wp civicrm db clear" and "wp civicrm db import" to restore database.
    if (defined('CIVICRM_DSN') && file_exists($backup_dir . '/civicrm-db.sql')) {

      WP_CLI::log('');
      WP_CLI::log(WP_CLI::colorize('%GRestoring database...%n'));

      // Maybe add extra filters.
      $also_include_args = '';
      if (!empty($also_include)) {
        $also_include_args = " --also-include={$also_include}";
      }

      // Clear existing tables.
      $command = 'civicrm db clear' . $also_include_args;
      $options = ['launch' => FALSE, 'return' => FALSE];
      WP_CLI::runcommand($command, $options);

      // Load backup tables.
      $command = 'civicrm db import --load-file=' . $backup_dir . '/civicrm-db.sql';
      $options = ['launch' => FALSE, 'return' => FALSE];
      WP_CLI::runcommand($command, $options);

      WP_CLI::success('Database restored.');

    }

  }

  /**
   * Upgrade the CiviCRM plugin files and database.
   *
   * ## OPTIONS
   *
   * [--version=<version>]
   * : Specify the CiviCRM version to get. Accepts a version number, 'stable', 'rc' or 'nightly'. Defaults to latest stable version.
   *
   * [--zipfile=<zipfile>]
   * : Path to your CiviCRM zip file. If specified --version is ignored.
   *
   * [--l10n]
   * : Additionally install the localization files for the specified version.
   *
   * [--l10n-tarfile=<l10n-tarfile>]
   * : Path to your l10n tar.gz file. If specified --l10n is ignored.
   *
   * [--yes]
   * : Answer yes to the confirmation message.
   *
   * ## EXAMPLES
   *
   *     # Update to the current stable version of CiviCRM.
   *     $ wp civicrm core update
   *     Success: Installed 1 of 1 plugins.
   *
   * @alias upgrade
   *
   * @since 5.69
   *
   * @param array $args The WP-CLI positional arguments.
   * @param array $assoc_args The WP-CLI associative arguments.
   */
  public function update($args, $assoc_args) {

    // Grab associative arguments.
    $version = (string) \WP_CLI\Utils\get_flag_value($assoc_args, 'version', 'stable');
    $zipfile = (string) \WP_CLI\Utils\get_flag_value($assoc_args, 'zipfile', '');
    $l10n = (string) \WP_CLI\Utils\get_flag_value($assoc_args, 'l10n', '');
    $l10n_tarfile = (string) \WP_CLI\Utils\get_flag_value($assoc_args, 'l10n-tarfile', '');

    WP_CLI::log(WP_CLI::colorize('%GGathering system information.%n'));

    // Bootstrap CiviCRM.
    $this->bootstrap_civicrm();

    // Let's have a look for some CiviCRM variables.
    global $civicrm_root;
    $config = CRM_Core_Config::singleton();

    // ----------------------------------------------------------------------------
    // Build feedback table.
    // ----------------------------------------------------------------------------

    // Build feedback.
    $feedback = [];
    if (!empty($zipfile)) {
      $feedback['Plugin zip archive'] = $zipfile;
    }
    if (!empty($version) && empty($zipfile)) {
      $feedback['Requested version'] = $version;
      $options = ['launch' => FALSE, 'return' => TRUE];
      $command = 'civicrm core check-version --version=' . $version . ' --format=url';
      $archive = WP_CLI::runcommand($command, $options);
      // Maybe strip all the Google authentication stuff if present.
      if (FALSE !== strpos($archive, '?')) {
        $arr = explode('?', $archive);
        $archive = $arr[0];
      }
      $feedback['Requested archive'] = $archive;
    }
    if (defined('CIVICRM_PLUGIN_DIR')) {
      $feedback['Plugin path'] = CIVICRM_PLUGIN_DIR;
    }
    if (!empty($civicrm_root)) {
      $feedback['CiviCRM root'] = $civicrm_root;
    }
    if (defined('CIVICRM_DSN')) {
      $dsn = DB::parseDSN(CIVICRM_DSN);
      $feedback['Database name'] = $dsn['database'];
      $feedback['Database username'] = $dsn['username'];
      $feedback['Database password'] = $dsn['password'];
      $feedback['Database host'] = $dsn['hostspec'];
    }

    // Render feedback.
    $assoc_args['fields'] = array_keys($feedback);
    $formatter = $this->formatter_get($assoc_args);
    $formatter->display_item($feedback);

    // Let's give folks a chance to exit now.
    WP_CLI::confirm(WP_CLI::colorize('%GDo you want to continue?%n'), $assoc_args);

    // ----------------------------------------------------------------------------
    // Start upgrade.
    // ----------------------------------------------------------------------------

    // Enable Maintenance Mode.
    //WP_CLI::runcommand('maintenance-mode activate', ['launch' => FALSE, 'return' => FALSE]);

    // Build install command.
    $command = 'civicrm core install' .
      (empty($version) ? '' : ' --version=' . $version) .
      (empty($zipfile) ? '' : ' --zipfile=' . $zipfile) .
      (empty($l10n) ? '' : ' --l10n') .
      (empty($langtarfile) ? '' : ' --l10n-tarfile=' . $langtarfile) .
      ' --force';

    // Run "wp civicrm core install".
    $options = ['launch' => FALSE, 'return' => FALSE];
    WP_CLI::runcommand($command, $options);

    // Disable Maintenance Mode.
    //WP_CLI::runcommand('maintenance-mode deactivate', ['launch' => FALSE, 'return' => FALSE]);

  }

  /**
   * Upgrade the CiviCRM database schema.
   *
   * ## OPTIONS
   *
   * [--dry-run]
   * : Preview the list of upgrade tasks.
   *
   * [--retry]
   * : Resume a failed upgrade, retrying the last step.
   *
   * [--skip]
   * : Resume a failed upgrade, skipping the last step.
   *
   * [--step]
   * : Run the upgrade queue in steps, pausing before each step.
   *
   * [--v]
   * : Run the upgrade queue with verbose output.
   *
   * [--vv]
   * : Run the upgrade queue with extra verbose output.
   *
   * [--vvv]
   * : An alias of --vv for old timers more used to cv syntax.
   *
   * [--yes]
   * : Answer yes to the confirmation messages. Does not apply to step messages.
   *
   * ## EXAMPLES
   *
   *     $ wp civicrm core update-db --dry-run --v
   *     Found CiviCRM code version: 5.57.1
   *     Found CiviCRM database version: 5.57.0
   *     Checking pre-upgrade messages.
   *     (No messages)
   *     Dropping SQL triggers.
   *     Preparing upgrade.
   *     Executing upgrade.
   *     Cleanup old files
   *     Cleanup old upgrade snapshots
   *     Checking extensions
   *     Finish Upgrade DB to 5.57.1
   *     Update all reserved message templates
   *     Finish core DB updates 5.57.1
   *     Assess extension upgrades
   *     Generate final messages
   *     Finishing upgrade.
   *     Upgrade to 5.57.1 completed.
   *     Checking post-upgrade messages.
   *     (No messages)
   *     Have a nice day.
   *
   * @subcommand update-db
   *
   * @alias upgrade-db
   *
   * @since 5.69
   *
   * @param array $args The WP-CLI positional arguments.
   * @param array $assoc_args The WP-CLI associative arguments.
   */
  public function update_db($args, $assoc_args) {

    // Bootstrap CiviCRM.
    $this->bootstrap_civicrm();

    if (!defined('CIVICRM_UPGRADE_ACTIVE')) {
      define('CIVICRM_UPGRADE_ACTIVE', 1);
    }

    // Check whether an upgrade is necessary.
    $code_version = CRM_Utils_System::version();
    WP_CLI::log(sprintf(WP_CLI::colorize('%GFound CiviCRM code version:%n %Y%s%n'), $code_version));
    $db_version = CRM_Core_BAO_Domain::version();
    WP_CLI::log(sprintf(WP_CLI::colorize('%GFound CiviCRM database version:%n %Y%s%n'), $db_version));
    if (version_compare($code_version, $db_version) === 0) {
      WP_CLI::success(sprintf('You are already upgraded to CiviCRM %s', $code_version));
      WP_CLI::halt(0);
    }

    // Get flags.
    $dry_run = \WP_CLI\Utils\get_flag_value($assoc_args, 'dry-run', FALSE);
    $retry = \WP_CLI\Utils\get_flag_value($assoc_args, 'retry', FALSE);
    $skip = \WP_CLI\Utils\get_flag_value($assoc_args, 'skip', FALSE);
    $step = \WP_CLI\Utils\get_flag_value($assoc_args, 'step', FALSE);
    $first_try = (empty($retry) && empty($skip)) ? TRUE : FALSE;

    // Get verbosity.
    $verbose = \WP_CLI\Utils\get_flag_value($assoc_args, 'v', FALSE);
    $verbose_extra = \WP_CLI\Utils\get_flag_value($assoc_args, 'vv', FALSE);
    $verbose_old_skool = \WP_CLI\Utils\get_flag_value($assoc_args, 'vvv', FALSE);
    if (!empty($verbose_old_skool)) {
      $verbose_extra = TRUE;
    }

    // When stepping, we need at least "verbose".
    if (!empty($step)) {
      if (empty($verbose_extra) && empty($verbose)) {
        $verbose = TRUE;
      }
    }

    // Bail if incomplete upgrade.
    if ($first_try && FALSE !== stripos($db_version, 'upgrade')) {
      WP_CLI::error('Cannot begin upgrade: The database indicates that an incomplete upgrade is pending. If you would like to resume, use --retry or --skip.');
    }

    // Bootstrap upgrader.
    $upgrade = new CRM_Upgrade_Form();
    $error = $upgrade->checkUpgradeableVersion($db_version, $code_version);
    if (!empty($error)) {
      WP_CLI::error($error);
    }

    // Check pre-upgrade messages.
    if ($first_try) {
      WP_CLI::log(WP_CLI::colorize('%gChecking pre-upgrade messages.%n'));
      $preUpgradeMessage = NULL;
      $upgrade->setPreUpgradeMessage($preUpgradeMessage, $db_version, $code_version);
      if ($preUpgradeMessage) {
        WP_CLI::log(CRM_Utils_String::htmlToText($preUpgradeMessage));
        WP_CLI::confirm(WP_CLI::colorize('%GDo you want to continue?%n'), $assoc_args);
      }
      else {
        WP_CLI::log('(No messages)');
      }
    }

    // Why is dropTriggers() hard-coded? Can't we just enqueue this as part of buildQueue()?
    if ($first_try) {
      WP_CLI::log(WP_CLI::colorize('%gDropping SQL triggers.%n'));
      if (empty($dry_run)) {
        CRM_Core_DAO::dropTriggers();
      }
    }

    // Let's create a file for storing upgrade messages.
    $post_upgrade_message_file = CRM_Utils_File::tempnam('civicrm-post-upgrade');

    // Build the queue.
    if ($first_try) {
      WP_CLI::log(WP_CLI::colorize('%gPreparing upgrade.%n'));
      $queue = CRM_Upgrade_Form::buildQueue($db_version, $code_version, $post_upgrade_message_file);
      // Sanity check - only SQL queues can be resumed.
      if (!($queue instanceof CRM_Queue_Queue_Sql)) {
        WP_CLI::error('The "upgrade-db" command only supports SQL-based queues.');
      }
    }
    else {
      WP_CLI::log(WP_CLI::colorize('%Resuming upgrade.%n'));
      $queue = CRM_Queue_Service::singleton()->load([
        'name' => CRM_Upgrade_Form::QUEUE_NAME,
        'type' => 'Sql',
      ]);
      if ($skip) {
        $item = $queue->stealItem();
        if (!empty($item->data->title)) {
          WP_CLI::log(sprintf('Skip task: %s', $item->data->title));
          $queue->deleteItem($item);
        }
      }
    }

    // Start the upgrade.
    WP_CLI::log(WP_CLI::colorize('%gExecuting upgrade.%n'));
    set_time_limit(0);

    // Mimic what "Console Queue Runner" does.
    $task_context = new CRM_Queue_TaskContext();
    $task_context->queue = $queue;

    // Maybe suppress Task Context logger output.
    if (empty($verbose_extra) && empty($verbose)) {
      if (!class_exists('CLI_Tools_CiviCRM_Logger_Dummy')) {
        require_once __DIR__ . '/utilities/class-logger-dummy.php';
      }
      $task_context->log = new CLI_Tools_CiviCRM_Logger_Dummy();
    }
    else {
      $task_context->log = \Log::singleton('display');
    }

    while ($queue->numberOfItems()) {

      // In case we're retrying a failed job.
      $item = $queue->stealItem();
      $task = $item->data;

      // Feedback.
      if (!empty($verbose_extra)) {
        $feedback = self::task_callback_format($task);
        WP_CLI::log(WP_CLI::colorize('%g' . $task->title . '%n') . ' ' . WP_CLI::colorize($feedback));
      }
      elseif (!empty($verbose)) {
        WP_CLI::log(WP_CLI::colorize('%g' . $task->title . '%n'));
      }
      else {
        echo '.';
      }

      // Get action.
      $action = 'y';
      if (!empty($step)) {
        fwrite(STDOUT, 'Execute this step? [ y=yes / s=skip / a=abort ] ');
        $action = strtolower(trim(fgets(STDIN)));
      }

      // Bail if skip action is "abort".
      if ($action === 'a') {
        WP_CLI::halt(1);
      }

      // Run the task when action is "yes".
      if ($action === 'y' && empty($dry_run)) {
        try {
          $success = $task->run($task_context);
          if (!$success) {
            WP_CLI::error('Task returned false');
          }
        }
        catch (\Exception $e) {
          // WISHLIST: For interactive mode, perhaps allow retry/skip?
          WP_CLI::log(sprintf(WP_CLI::colorize('%RError executing task%n %Y"%s"%n'), $task->title));
          WP_CLI::log('');
          WP_CLI::log(WP_CLI::colorize('%RError message:%n'));
          WP_CLI::log(sprintf(WP_CLI::colorize('%r%s%n'), $e->getMessage()));
          WP_CLI::log('');
          WP_CLI::log(WP_CLI::colorize('%RStack trace:%n'));
          WP_CLI::log(sprintf(WP_CLI::colorize('%r%s%n'), $e->getTraceAsString()));
          WP_CLI::log('');
          WP_CLI::error('Could not complete database update.');
        }
      }

      $queue->deleteItem($item);

    }

    // End feedback.
    if (empty($verbose_extra) && empty($verbose)) {
      echo "\n";
    }

    WP_CLI::log(WP_CLI::colorize('%gFinishing upgrade.%n'));
    if (empty($dry_run)) {
      CRM_Upgrade_Form::doFinish();
    }

    WP_CLI::log(sprintf(WP_CLI::colorize('%GUpgrade to%n %Y%s%n %Gcompleted.%n'), $code_version));

    if (version_compare($code_version, '5.26.alpha', '<')) {
      // Work-around for bugs like dev/core#1713.
      WP_CLI::log(WP_CLI::colorize('%GDetected CiviCRM 5.25 or earlier. Force flush.%n'));
      if (empty($dry_run)) {
        \Civi\Cv\Util\Cv::passthru('flush');
      }
    }

    WP_CLI::log(WP_CLI::colorize('%GChecking post-upgrade messages.%n'));
    $message = file_get_contents($post_upgrade_message_file);
    if ($message) {
      WP_CLI::log(CRM_Utils_String::htmlToText($message));
    }
    else {
      WP_CLI::log('(No messages)');
    }

    // Remove file for storing upgrade messages.
    unlink($post_upgrade_message_file);

    WP_CLI::log(WP_CLI::colorize('%GHave a nice day.%n'));

  }

  /**
   * Reset paths to correct config settings.
   *
   * This command can be useful when the CiviCRM site has been cloned or migrated.
   *
   * The old version of this command tried to preserve webserver ownership of "templates_c"
   * and "civicrm/upload" because (when running this command as something other than the
   * web-user) `doSiteMove` clears and recreates these directories. The check took place
   * *after* `doSiteMove` had run, however, so would only report back the current user and
   * group ownership.
   *
   * If you run this command as something other than the web-user, it's up to you to assign
   * correct user and group permissions for these directories.
   *
   * ## EXAMPLES
   *
   *     $ wp civicrm core update-cfg
   *     Beginning site move process...
   *     Template cache and upload directory have been cleared.
   *     Database cache tables cleared.
   *     Session has been reset.
   *     Please make sure the following directories have the correct permissions:
   *     /example.com/httpdocs/wp-content/uploads/civicrm/templates_c/
   *     /example.com/httpdocs/wp-content/uploads/civicrm/upload/
   *     Success: Config successfully updated.
   *
   * @subcommand update-cfg
   *
   * @since 5.69
   *
   * @param array $args The WP-CLI positional arguments.
   * @param array $assoc_args The WP-CLI associative arguments.
   */
  public function update_cfg($args, $assoc_args) {

    // Bootstrap CiviCRM.
    $this->bootstrap_civicrm();

    // Do site move.
    $result = CRM_Core_BAO_ConfigSetting::doSiteMove();

    // Bail on error.
    if (empty($result)) {
      WP_CLI::error('Config update failed.');
    }

    // Result is HTML, so format and show.
    $results = explode('<br />', $result);
    foreach ($results as $result) {
      if (!empty($result)) {
        WP_CLI::log($result);
      }
    }

    // Show permissions reminder.
    $config = CRM_Core_Config::singleton();
    WP_CLI::log('Please make sure the following directories have the correct permissions:');
    if (!empty($config->templateCompileDir)) {
      WP_CLI::log(sprintf(WP_CLI::colorize('%y%s%n'), $config->templateCompileDir));
    }
    if (!empty($config->uploadDir)) {
      WP_CLI::log(sprintf(WP_CLI::colorize('%y%s%n'), $config->uploadDir));
    }

    WP_CLI::success('Config successfully updated.');

  }

  /**
   * Get the current version of the CiviCRM plugin and database.
   *
   * ## OPTIONS
   *
   * [--source=<source>]
   * : Specify the version to get.
   * ---
   * default: all
   * options:
   *   - all
   *   - plugin
   *   - db
   *
   * [--format=<format>]
   * : Render output in a particular format.
   * ---
   * default: table
   * options:
   *   - table
   *   - json
   *   - number
   * ---
   *
   * ## EXAMPLES
   *
   *     # Get all CiviCRM version information.
   *     $ wp civicrm core version
   *     +----------+---------+
   *     | Source   | Version |
   *     +----------+---------+
   *     | Plugin   | 5.57.1  |
   *     | Database | 5.46.3  |
   *     +----------+---------+
   *
   *     # Get just the CiviCRM database version number.
   *     $ wp civicrm core version --source=db --format=number
   *     5.46.3
   *
   *     # Get just the CiviCRM plugin version number.
   *     $ wp civicrm core version --source=plugin --format=number
   *     5.57.1
   *
   *     # Get all CiviCRM version information as JSON-formatted data.
   *     $ wp civicrm core version --format=json
   *     {"plugin":"5.57.1","db":"5.46.3"}
   *
   * @since 5.69
   *
   * @param array $args The WP-CLI positional arguments.
   * @param array $assoc_args The WP-CLI associative arguments.
   */
  public function version($args, $assoc_args) {

    // Grab associative arguments.
    $source = (string) \WP_CLI\Utils\get_flag_value($assoc_args, 'source', 'all');
    $format = (string) \WP_CLI\Utils\get_flag_value($assoc_args, 'format', 'table');

    // Bootstrap CiviCRM.
    $this->bootstrap_civicrm();

    // Get the data we want.
    $plugin_version = CRM_Utils_System::version();
    $db_version = CRM_Core_BAO_Domain::version();

    switch ($format) {

      // Version number-only output.
      case 'number':
        if (!in_array($source, ['db', 'plugin'])) {
          WP_CLI::error(WP_CLI::colorize('You must specify %Y--source=plugin%n or %Y--source=db%n to use this output format.'));
        }
        if ('plugin' === $source) {
          echo $plugin_version . "\n";
        }
        if ('db' === $source) {
          echo $db_version . "\n";
        }
        break;

      // Display output as json.
      case 'json':
        $info = [];
        if (in_array($source, ['all', 'plugin'])) {
          $info['plugin'] = $plugin_version;
        }
        if (in_array($source, ['all', 'db'])) {
          $info['db'] = $db_version;
        }
        $json = json_encode($info);
        if (JSON_ERROR_NONE !== json_last_error()) {
          WP_CLI::error(sprintf(WP_CLI::colorize('Failed to encode JSON: %Y%s.%n'), json_last_error_msg()));
        }
        echo $json . "\n";
        break;

      // Display output as table (default).
      case 'table':
      default:
        // Build the rows.
        $rows = [];
        $fields = ['Source', 'Version'];
        if (in_array($source, ['all', 'plugin'])) {
          $rows[] = [
            'Source' => 'Plugin',
            'Version' => $plugin_version,
          ];
        }
        if (in_array($source, ['all', 'db'])) {
          $rows[] = [
            'Source' => 'Database',
            'Version' => $db_version,
          ];
        }

        // Display the rows.
        $args = ['format' => $format];
        $formatter = new \WP_CLI\Formatter($args, $fields);
        $formatter->display_items($rows);

    }

  }

  // ----------------------------------------------------------------------------
  // Private methods.
  // ----------------------------------------------------------------------------

  /**
   * Determine what action to take to resolve a conflict.
   *
   * @since 5.69
   *
   * @param string $title The thing which had a conflict.
   * @return string One of 'abort', 'keep' or 'overwrite'.
   */
  private function conflict_action_pick($title) {

    WP_CLI::log(sprintf(WP_CLI::colorize('%GThe%n %Y%s%n %Galready exists.%n'), $title));
    WP_CLI::log(WP_CLI::colorize('%G[a]%n %gAbort. (Default.)%n'));
    WP_CLI::log(sprintf(WP_CLI::colorize('%G[k]%n %gKeep existing%n %y%s%n%g.%n %r(%n%RWARNING:%n %rThis may fail if the existing version is out-of-date.)%n'), $title));
    WP_CLI::log(sprintf(WP_CLI::colorize('%G[o]%n %gOverwrite with new%n %y%s%g.%n %r(%n%RWARNING:%n %rThis may destroy data.)%n'), $title));

    fwrite(STDOUT, WP_CLI::colorize('%GWhat you like to do?%n '));
    $action = strtolower(trim(fgets(STDIN)));
    switch ($action) {
      case 'k':
        return 'keep';

      case 'o':
        return 'overwrite';

      case 'a':
      default:
        return 'abort';
    }

  }

  /**
   * Recursively implode an array.
   *
   * @since 5.69
   *
   * @param array $value The array to implode.
   * @param integer $level The current level.
   * @return string
   */
  private static function implode_recursive($value, $level = 0) {

    // Maybe recurse.
    $array = [];
    if (is_array($value)) {
      foreach ($value as $val) {
        if (is_array($val)) {
          $array[] = self::implode_recursive($val, $level + 1);
        }
        else {
          $array[] = $val;
        }
      }
    }
    else {
      $array[] = $value;
    }

    // Wrap sub-arrays but leave top level alone.
    if ($level > 0) {
      $string = '[' . implode(',', $array) . ']';
    }
    else {
      $string = implode(',', $array);
    }

    return $string;

  }

  /**
   * Gets the array of CiviCRM stable release versions.
   *
   * @since 5.69
   *
   * @return array The array of CiviCRM stable release versions.
   */
  private function releases_get() {

    // Get all release versions.
    $url = $this->google_url . '&' . $this->google_prefix_stable . '&maxResults=1000';
    $result = $this->json_get_request($url);
    if (empty($result['prefixes'])) {
      return [];
    }

    // Strip out all but the version.
    array_walk($result['prefixes'], function(&$item) {
      $item = trim(str_replace('civicrm-stable/', '', $item));
      $item = trim(str_replace('/', '', $item));
    });

    // Sort by version.
    usort($result['prefixes'], 'version_compare');

    return $result['prefixes'];

  }

  /**
   * Gets the array of CiviCRM release data.
   *
   * @since 5.69
   *
   * @param string $release The CiviCRM release.
   * @return array $data The array of CiviCRM release data.
   */
  private function release_data_get($release) {

    // Get the release data.
    $url = $this->google_url . '&' . $this->google_prefix_stable . $release . '/';
    $result = $this->json_get_request($url);
    if (empty($result['items'])) {
      return [];
    }

    // Strip out all but the WordPress and l10n data.
    $data = [];
    foreach ($result['items'] as $item) {
      if (!empty($item['name'])) {
        if (FALSE !== strpos($item['name'], 'wordpress.zip')) {
          $data['WordPress'] = $item['name'];
        }
        if (FALSE !== strpos($item['name'], 'l10n.tar.gz')) {
          $data['L10n'] = $item['name'];
        }
      }
    }

    return $data;

  }

  /**
   * Format the task for when run with extra verbosity.
   *
   * This method re-builds the task arguments because some of them may themselves be arrays.
   *
   * @since 5.69
   *
   * @param CRM_Queue_Task $task The CiviCRM task object.
   * @return string $task The CiviCRM task object.
   */
  private static function task_callback_format($task) {

    $callback_info = implode('::', (array) $task->callback);
    $args_info = self::implode_recursive((array) $task->arguments);

    // Build string with colorization tokens.
    $feedback = '%y' . $callback_info . '(' . $args_info . '%n)';

    return $feedback;

  }

}