<?php
/*
 +--------------------------------------------------------------------+
 | Copyright CiviCRM LLC. All rights reserved.                        |
 |                                                                    |
 | This work is published under the GNU AGPLv3 license with some      |
 | permitted exceptions and without any warranty. For full license    |
 | and copyright information, see https://civicrm.org/licensing       |
 +--------------------------------------------------------------------+
 */

/**
 *
 * @package CRM
 * @copyright CiviCRM LLC https://civicrm.org/licensing
 *
 */

if (!defined('CIVICRM_WPCLI_LOADED')) {
  define('CIVICRM_WPCLI_LOADED', 1);

  /**
   * WP-CLI port of drush-civicrm integration.
   *
   * @see https://github.com/andy-walker/wp-cli-civicrm
   *
   * @since 4.5
   */
  class CiviCRM_Command extends WP_CLI_Command {

    private $args;
    private $assoc_args;

    /**
     * WP-CLI integration with CiviCRM.
     *
     * wp civicrm api
     * ==============
     * Command for accessing the CiviCRM API. Syntax is identical to `drush cvap`.
     *
     * wp civicrm cache-clear
     * ======================
     * Command for accessing clearing cache.  Equivilant of running `civicrm/admin/setting/updateConfigBackend&reset=1`.
     *
     * wp civicrm enable-debug
     * =======================
     * Command for to turn debug on.
     *
     * wp civicrm disable-debug
     * ========================
     * Command for to turn debug off.
     *
     * wp civicrm member-records
     * =========================
     * Run the CiviMember UpdateMembershipRecord cron (civicrm member-records).
     *
     * wp civicrm pipe <connection-flags>
     * ==================
     * Start a Civi::pipe session (JSON-RPC 2.0)
     * See https://docs.civicrm.org/dev/en/latest/framework/pipe#flags
     *
     * wp civicrm process-mail-queue
     * =============================
     * Process pending CiviMail mailing jobs.
     * Example:
     * wp civicrm process-mail-queue -u admin
     *
     * wp civicrm rest
     * ===============
     * Rest interface for accessing CiviCRM APIs. It can return xml or json formatted data.
     *
     * wp civicrm restore
     * ==================
     * Restore CiviCRM codebase and database back from the specified backup directory.
     *
     * wp civicrm sql-conf
     * ===================
     * Show CiviCRM database connection details.
     *
     * wp civicrm sql-connect
     * ======================
     * A string which connects to the CiviCRM database.
     *
     * wp civicrm sql-cli
     * ==================
     * Quickly enter the mysql command line.
     *
     * wp civicrm sql-dump
     * ===================
     * Prints the whole CiviCRM database to STDOUT or save to a file.
     *
     * wp civicrm sql-query
     * ====================
     * Usage: wp civicrm sql-query <query> <options>...
     * <query> is a SQL statement, which can alternatively be passed via STDIN. Any additional arguments are passed to the mysql command directly.
     *
     * wp civicrm update-cfg
     * =====================
     * Update config_backend to correct config settings, especially when the CiviCRM site has been cloned or migrated.
     *
     * wp civicrm upgrade
     * ==================
     * Take backups, replace CiviCRM codebase with new specified tarfile and upgrade database by executing the CiviCRM upgrade process - civicrm/upgrade?reset=1. Use civicrm-restore to revert to previous state in case anything goes wrong.
     *
     * wp civicrm upgrade-db
     * =====================
     * Run civicrm/upgrade?reset=1 just as a web browser would.
     *
     * wp civicrm install
     * ==================
     * Command for to install CiviCRM.  The install command requires that you have downloaded a tarball or zip file first.
     * Options:
     * --dbhost            MySQL host for your WordPress/CiviCRM database. Defaults to localhost.
     * --dbname            MySQL database name of your WordPress/CiviCRM database.
     * --dbpass            MySQL password for your WordPress/CiviCRM database.
     * --dbuser            MySQL username for your WordPress/CiviCRM database.
     * --lang              Default language to use for installation.
     * --langtarfile       Path to your l10n tar.gz file.
     * --site_url          Base Url for your WordPress/CiviCRM website without http (e.g. mysite.com)
     * --ssl               Using ssl for your WordPress/CiviCRM website if set to on (e.g. --ssl=on)
     * --tarfile           Path to your CiviCRM tar.gz file.
     * --zipfile           Path to your CiviCRM zip file.
     *
     */
    public function __invoke($args, $assoc_args) {

      $this->args       = $args;
      $this->assoc_args = $assoc_args;

      // Define command router.
      $command_router = [
        'api'                => 'api',
        'cache-clear'        => 'cacheClear',
        'enable-debug'       => 'enableDebug',
        'disable-debug'      => 'disableDebug',
        'install'            => 'install',
        'member-records'     => 'memberRecords',
        'pipe'               => 'pipe',
        'process-mail-queue' => 'processMailQueue',
        'rest'               => 'rest',
        'restore'            => 'restore',
        'sql-cli'            => 'sqlCLI',
        'sql-conf'           => 'sqlConf',
        'sql-connect'        => 'sqlConnect',
        'sql-dump'           => 'sqlDump',
        'sql-query'          => 'sqlQuery',
        'update-cfg'         => 'updateConfig',
        'upgrade'            => 'upgrade',
        'upgrade-db'         => 'upgradeDB',
      ];

      // Get the command.
      $command = array_shift($args);

      // Allow help to pass.
      if ('help' === $command) {
        return;
      }

      // Check for existence of CiviCRM (except for command 'install').
      if (!function_exists('civicrm_initialize') && 'install' != $command) {
        return WP_CLI::error('Unable to find CiviCRM install.');
      }

      // Check existence of router entry / handler method.
      if (!isset($command_router[$command]) || !method_exists($this, $command_router[$command])) {
        return WP_CLI::error(sprintf('Unrecognized command: %s', $command));
      }

      // Run command.
      return $this->{$command_router[$command]}();

    }

    /**
     * Implementation of command 'api'.
     *
     * @since 4.5
     */
    private function api() {

      $defaults = ['version' => 3];

      array_shift($this->args);
      list($entity, $action) = explode('.', $this->args[0]);
      array_shift($this->args);

      //  Parse params.
      $format = $this->getOption('in', 'args');
      switch ($format) {

        //  Input params supplied via args.
        case 'args':
          $params = $defaults;
          foreach ($this->args as $arg) {
            preg_match('/^([^=]+)=(.*)$/', $arg, $matches);
            $params[$matches[1]] = $matches[2];
          }
          break;

        //  Input params supplied via json.
        case 'json':
          $json   = stream_get_contents(STDIN);
          $params = (empty($json) ? $defaults : array_merge($defaults, json_decode($json, TRUE)));
          break;

        default:
          WP_CLI::error(sprintf('Unknown format: %s', $format));
          break;

      }

      civicrm_initialize();

      // CRM-18062: Set CiviCRM timezone if any.
      $wp_base_timezone = date_default_timezone_get();
      $wp_user_timezone = $this->getOption('timezone', get_option('timezone_string'));
      if ($wp_user_timezone) {
        date_default_timezone_set($wp_user_timezone);
        CRM_Core_Config::singleton()->userSystem->setMySQLTimeZone();
      }

      $result = civicrm_api($entity, $action, $params);

      // Restore WordPress's timezone.
      if ($wp_base_timezone) {
        date_default_timezone_set($wp_base_timezone);
      }

      switch ($this->getOption('out', 'pretty')) {

        // Pretty-print output (default).
        case 'pretty':
          WP_CLI::line(print_r($result, TRUE));
          break;

        // Display output as json.
        case 'json':
          WP_CLI::line(json_encode($result));
          break;

        default:
          return WP_CLI::error(sprintf('Unknown format: %s', $format));

      }

    }

    /**
     * Implementation of command 'cache-clear'.
     *
     * @since 4.5
     */
    private function cacheClear() {

      civicrm_initialize();
      require_once 'CRM/Core/Config.php';
      $config = CRM_Core_Config::singleton();

      // Clear db caching.
      $config->clearDBCache();

      // Also cleanup the templates_c directory.
      $config->cleanup(1, FALSE);

      // Also cleanup the session object.
      $session = CRM_Core_Session::singleton();
      $session->reset(1);

    }

    /**
     * Implementation of command 'enable-debug'.
     *
     * @since 4.5
     */
    private function enableDebug() {
      civicrm_initialize();
      Civi::settings()->add([
        'debug_enabled' => 1,
        'backtrace' => 1,
      ]);
      WP_CLI::success('Debug setting enabled.');
    }

    /**
     * Implementation of command 'disable-debug'.
     *
     * @since 4.7
     */
    private function disableDebug() {
      civicrm_initialize();
      Civi::settings()->add([
        'debug_enabled' => 0,
        'backtrace' => 0,
      ]);
      WP_CLI::success('Debug setting disabled.');
    }

    /**
     * Implementation of command 'install'.
     *
     * @since 4.5
     */
    private function install() {
      if ('on' === $this->getOption('ssl', FALSE)) {
        $_SERVER['HTTPS'] = 'on';
      }

      // Identify the destination.

      if ($plugin_path = $this->getOption('destination', FALSE)) {
        $plugin_path = ABSPATH . $plugin_path;
      }
      else {
        $plugin_path = WP_PLUGIN_DIR . '/civicrm';
      }

      global $crmPath;
      $crmPath = "$plugin_path/civicrm";
      $crm_files_present = is_dir($crmPath);

      // Validate install parameters.

      if (!$dbuser = $this->getOption('dbuser', FALSE)) {
        return WP_CLI::error('CiviCRM database username not specified.');
      }
      if (!$dbpass = $this->getOption('dbpass', FALSE)) {
        return WP_CLI::error('CiviCRM database password not specified.');
      }
      if (!$dbhost = $this->getOption('dbhost', FALSE)) {
        return WP_CLI::error('CiviCRM database host not specified.');
      }
      if (!$dbname = $this->getOption('dbname', FALSE)) {
        return WP_CLI::error('CiviCRM database name not specified.');
      }
      if ($lang = $this->getOption('lang', FALSE)) {
        $moPath = "$crmPath/l10n/$lang/LC_MESSAGES/civicrm.mo";

        if (!($langtarfile = $this->getOption('langtarfile', FALSE)) && !file_exists($moPath)) {
          return WP_CLI::error("Failed to find data for language ($lang). Please download valid language data with --langtarfile=<path/to/tarfile>.");
        }
      }

      // Extract the archive.
      if ($this->getOption('tarfile', FALSE)) {
        // Should probably never get to here as Wordpress Civi comes in a zip file.
        // Check anyway just in case that ever changes.
        if ($crm_files_present) {
          return WP_CLI::error('Existing CiviCRM found. No action taken.');
        }

        if (!$this->untar(dirname($plugin_path))) {
          return WP_CLI::error('Error extracting tarfile.');
        }
      }
      elseif ($this->getOption('zipfile', FALSE)) {
        if ($crm_files_present) {
          return WP_CLI::error('Existing CiviCRM found. No action taken.');
        }

        if (!$this->unzip(dirname($plugin_path))) {
          return WP_CLI::error('Error extracting zipfile.');
        }
      }
      elseif ($crm_files_present) {
        // Site is already extracted - which is how we're running this script.
        // We just need to run the installer.
      }
      else {
        return WP_CLI::error('No zipfile specified. Use "--zipfile=path/to/zipfile" or extract file ahead of time.');
      }

      // Include CiviCRM classloader - so that we can run `Civi\Setup`.
      $classLoaderPath = "$crmPath/CRM/Core/ClassLoader.php";

      if (!file_exists($classLoaderPath)) {
        return WP_CLI::error('Archive could not be unpacked or CiviCRM installer helper file is missing.');
      }

      if ($crm_files_present) {
        // We were using a directory that was already there.
        WP_CLI::success('Using installer files found on the site.');
      }
      else {
        // We must've just unpacked the archive because it wasn't there
        // before.
        WP_CLI::success('Archive unpacked.');
      }

      if ($this->getOption('langtarfile', FALSE)) {
        if (!$this->untar($plugin_path, 'langtarfile')) {
          return WP_CLI::error('Error downloading langtarfile');
        }
      }

      if (!empty($lang) && !file_exists($moPath)) {
        return WP_CLI::error("Failed to find data for language ($lang). Please download valid language data with \"--langtarfile=<path/to/tarfile>\".");
      }

      // Initialize civicrm-setup
      @WP_CLI::run_command(['plugin', 'activate', 'civicrm'], []);
      require_once $classLoaderPath;
      CRM_Core_ClassLoader::singleton()->register();
      \Civi\Setup::assertProtocolCompatibility(1.0);
      \Civi\Setup::init(['cms' => 'WordPress', 'srcPath' => $crmPath]);
      $setup = \Civi\Setup::instance();
      $setup->getModel()->db = ['server' => $dbhost, 'username' => $dbuser, 'password' => $dbpass, 'database' => $dbname];
      $setup->getModel()->lang = (empty($lang) ? 'en_US' : $lang);
      if ($base_url = $this->getOption('site_url', FALSE)) {
        $ssl = $this->getOption('ssl', FALSE);
        $protocol = ('on' == $ssl ? 'https' : 'http');
        $base_url = $protocol . '://' . $base_url;
        if (substr($base_url, -1) != '/') {
          $base_url .= '/';
        }
        $setup->getModel()->cmsBaseUrl = $base_url;
      }

      // Check system requirements
      $reqs = $setup->checkRequirements();
      array_map('WP_CLI::print_value', $this->formatRequirements(array_merge($reqs->getErrors(), $reqs->getWarnings())));
      if ($reqs->getErrors()) {
        WP_CLI::error(sprintf("Cannot install. Please check requirements and resolve errors.", count($reqs->getErrors()), count($reqs->getWarnings())));
      }

      $installed = $setup->checkInstalled();
      if ($installed->isSettingInstalled() || $installed->isDatabaseInstalled()) {
        WP_CLI::error("Cannot install. CiviCRM has already been installed.");
      }

      // Go time
      $setup->installFiles();
      WP_CLI::success('CiviCRM data files initialized successfully.');
      $setup->installDatabase();
      WP_CLI::success('CiviCRM database loaded successfully.');
      WP_CLI::success('CiviCRM installed.');
    }

    private function formatRequirements(array $messages): array {
      $formatted = [];
      foreach ($messages as $message) {
        $formatted[] = sprintf("[%s] %s: %s", $message['severity'], $message['section'], $message['message']);
      }
      return array_unique($formatted);
    }

    /**
     * Implementation of command 'member-records'.
     *
     * @since 4.5
     */
    private function memberRecords() {

      civicrm_initialize();

      if (substr(CRM_Utils_System::version(), 0, 3) >= '4.3') {

        $job = new CRM_Core_JobManager();
        $job->executeJobByAction('job', 'process_membership');
        WP_CLI::success('Executed "process_membership" job.');

      }
      else {

        $_REQUEST['name'] = $this->getOption('civicrm_cron_username', NULL);
        $_REQUEST['pass'] = $this->getOption('civicrm_cron_password', NULL);
        $_REQUEST['key']  = $this->getOption('civicrm_sitekey', NULL);

        global $argv;
        $argv = [
          0 => 'drush',
          1 => '-u' . $_REQUEST['name'],
          2 => '-p' . $_REQUEST['pass'],
          3 => '-s' . $this->getOption('uri', FALSE),
        ];

        # if (!defined('CIVICRM_CONFDIR')) {
        # $plugins_dir = plugin_dir_path(__FILE__);
        #     define('CIVICRM_CONFDIR', $plugins_dir);
        # }

        include 'bin/UpdateMembershipRecord.php';

      }

    }

    private function pipe() {
      civicrm_initialize();
      if (!is_callable(['Civi', 'pipe'])) {
        return WP_CLI::error('This version of CiviCRM does not include Civi::pipe() support.');
      }

      if (!empty($this->args[1])) {
        Civi::pipe($this->args[1]);
      }
      else {
        Civi::pipe();
      }
    }

    /**
     * Implementation of command 'process-mail-queue'.
     *
     * @since 4.5
     */
    private function processMailQueue() {

      civicrm_initialize();

      if (substr(CRM_Utils_System::version(), 0, 3) >= '4.3') {

        $job = new CRM_Core_JobManager();
        $job->executeJobByAction('job', 'process_mailing');
        WP_CLI::success("Executed 'process_mailing' job.");

      }
      else {

        $result = civicrm_api('Mailing', 'Process', ['version' => 3]);
        if ($result['is_error']) {
          WP_CLI::error($result['error_message']);
        }
      }

    }

    /**
     * Implementation of command 'rest'.
     *
     * @since 4.5
     */
    private function rest() {

      civicrm_initialize();

      if (!$query = $this->getOption('query', FALSE)) {
        return WP_CLI::error('query not specified.');
      }

      $query     = explode('&', $query);
      $_GET['q'] = array_shift($query);

      foreach ($query as $key_val) {
        list($key, $val) = explode('=', $key_val);
        $_REQUEST[$key]  = $val;
        $_GET[$key]      = $val;
      }

      require_once 'CRM/Utils/REST.php';
      $rest = new CRM_Utils_REST();

      require_once 'CRM/Core/Config.php';
      $config = CRM_Core_Config::singleton();

      global $civicrm_root;
      // Adding dummy script, since based on this api file path is computed.
      $_SERVER['SCRIPT_FILENAME'] = "$civicrm_root/extern/rest.php";

      if (isset($_GET['json']) && $_GET['json']) {
        header('Content-Type: text/javascript');
      }
      else {
        header('Content-Type: text/xml');
      }

      echo $rest->run($config);

    }

    /**
     * Implementation of command 'restore'.
     *
     * @since 4.5
     */
    private function restore() {

      // Validate.
      $restore_dir = $this->getOption('restore-dir', FALSE);
      $restore_dir = rtrim($restore_dir, '/');
      if (!$restore_dir) {
        return WP_CLI::error('"restore-dir" not specified.');
      }

      $sql_file = $restore_dir . '/civicrm.sql';
      if (!file_exists($sql_file)) {
        return WP_CLI::error('Could not locate "civicrm.sql" file in the restore directory.');
      }

      $code_dir = $restore_dir . '/civicrm';
      if (!is_dir($code_dir)) {
        return WP_CLI::error('Could not locate the CiviCRM directory inside "restore-dir".');
      }
      elseif (!file_exists("$code_dir/civicrm/civicrm-version.txt") && !file_exists("$code_dir/civicrm/civicrm-version.php")) {
        return WP_CLI::error('The CiviCRM directory inside "restore-dir" does not seem to be a valid CiviCRM codebase.');
      }

      // Prepare to restore.
      $date = date('YmdHis');

      civicrm_initialize();
      global $civicrm_root;

      $civicrm_root_base = explode('/', $civicrm_root);
      array_pop($civicrm_root_base);
      $civicrm_root_base = implode('/', $civicrm_root_base) . '/';

      $basepath = explode('/', $civicrm_root);

      if (!end($basepath)) {
        array_pop($basepath);
      }

      array_pop($basepath);
      $project_path = implode('/', $basepath) . '/';

      $wp_root = ABSPATH;
      $restore_backup_dir = $this->getOption('backup-dir', $wp_root . '/../backup');
      $restore_backup_dir = rtrim($restore_backup_dir, '/');

      // Get confirmation from user.

      if (!defined('CIVICRM_DSN')) {
        WP_CLI::error('CIVICRM_DSN is not defined.');
      }

      $db_spec = DB::parseDSN(CIVICRM_DSN);
      WP_CLI::line('');
      WP_CLI::line('Process involves:');
      WP_CLI::line(sprintf("1. Restoring '\$restore-dir/civicrm' directory to '%s'.", $civicrm_root_base));
      WP_CLI::line(sprintf("2. Dropping and creating '%s' database.", $db_spec['database']));
      WP_CLI::line("3. Loading '\$restore-dir/civicrm.sql' file into the database.");
      WP_CLI::line('');
      WP_CLI::line(sprintf("Note: Before restoring, a backup will be taken in '%s' directory.", "$restore_backup_dir/plugins/restore"));
      WP_CLI::line('');

      WP_CLI::confirm('Do you really want to continue?');

      $restore_backup_dir .= '/plugins/restore/' . $date;

      if (!mkdir($restore_backup_dir, 0755, TRUE)) {
        return WP_CLI::error(sprintf('Failed to create directory: %s', $restore_backup_dir));
      }

      // 1. Backup and restore codebase.
      WP_CLI::line('Restoring CiviCRM codebase...');
      if (is_dir($project_path) && !rename($project_path, $restore_backup_dir . '/civicrm')) {
        return WP_CLI::error(sprintf("Failed to take backup for '%s' directory", $project_path));
      }

      if (!rename($code_dir, $project_path)) {
        return WP_CLI::error(sprintf("Failed to restore CiviCRM directory '%s' to '%s'", $code_dir, $project_path));
      }

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

      // 2. Backup, drop and create database.
      WP_CLI::run_command(
        ['civicrm', 'sql-dump'],
        ['result-file' => $restore_backup_dir . '/civicrm.sql']
      );

      WP_CLI::success('Database backed up.');

      // Prepare a mysql command-line string for issuing db drop/create commands.
      $command = sprintf(
        'mysql --user=%s --password=%s',
        $db_spec['username'],
        $db_spec['password']
      );

      if (isset($db_spec['hostspec'])) {
        $command .= ' --host=' . $db_spec['hostspec'];
      }

      if (isset($dsn['port']) && !mpty($dsn['port'])) {
        $command .= ' --port=' . $db_spec['port'];
      }

      // Attempt to drop old database.
      if (system($command . sprintf(' --execute="DROP DATABASE IF EXISTS %s"', $db_spec['database']))) {
        return WP_CLI::error(sprintf('Could not drop database: %s', $db_spec['database']));
      }

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

      // Attempt to create new database.
      if (system($command . sprintf(' --execute="CREATE DATABASE %s"', $db_spec['database']))) {
        WP_CLI::error(sprintf('Could not create new database: %s', $db_spec['database']));
      }

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

      // 3. Restore database.
      WP_CLI::line('Loading "civicrm.sql" file from "restore-dir"...');
      system($command . ' ' . $db_spec['database'] . ' < ' . $sql_file);

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

      WP_CLI::line('Clearing caches...');
      WP_CLI::run_command(['civicrm', 'cache-clear']);

      WP_CLI::success('Restore process completed.');

    }

    /**
     * Implementation of command 'sql-conf'.
     *
     * @since 4.5
     */
    private function sqlConf() {

      civicrm_initialize();
      if (!defined('CIVICRM_DSN')) {
        WP_CLI::error('CIVICRM_DSN is not defined.');
      }

      WP_CLI::line(print_r(DB::parseDSN(CIVICRM_DSN), TRUE));

    }

    /**
     * Implementation of command 'sql-connect'.
     *
     * @since 4.5
     */
    private function sqlConnect() {

      civicrm_initialize();
      if (!defined('CIVICRM_DSN')) {
        return WP_CLI::error('CIVICRM_DSN is not defined.');
      }

      $dsn = DB::parseDSN(CIVICRM_DSN);

      $command = sprintf(
        'mysql --database=%s --host=%s --user=%s --password=%s',
        $dsn['database'],
        $dsn['hostspec'],
        $dsn['username'],
        $dsn['password']
      );

      if (isset($dsn['port']) && !empty($dsn['port'])) {
        $command .= ' --port=' . $dsn['port'];
      }

      return WP_CLI::line($command);

    }

    /**
     * Implementation of command 'sql-dump'.
     *
     * @since 4.5
     */
    private function sqlDump() {

      // Bootstrap CiviCRM when we're not being called as part of an upgrade.
      if (!defined('CIVICRM_UPGRADE_ACTIVE')) {
        civicrm_initialize();
      }

      if (!defined('CIVICRM_DSN') && !defined('CIVICRM_OLD_DSN')) {
        WP_CLI::error('DSN is not defined.');
      }

      $dsn = self::parseDSN(defined('CIVICRM_DSN') ? CIVICRM_DSN : CIVICRM_OLD_DSN);

      $assoc_args       = $this->assoc_args;
      $stdout           = !isset($assoc_args['result-file']);
      $command          = "mysqldump --no-defaults --host={$dsn['hostspec']} --user={$dsn['username']} --password='{$dsn['password']}' %s";
      $command_esc_args = [$dsn['database']];

      if (isset($assoc_args['tables'])) {
        $tables = explode(',', $assoc_args['tables']);
        unset($assoc_args['tables']);
        $command .= ' --tables';
        foreach ($tables as $table) {
          $command .= ' %s';
          $command_esc_args[] = trim($table);
        }
      }

      $escaped_command = call_user_func_array(
        '\WP_CLI\Utils\esc_cmd',
        array_merge(
          [$command],
          $command_esc_args
        )
      );

      \WP_CLI\Utils\run_mysql_command($escaped_command, $assoc_args);

      if (!$stdout) {
        WP_CLI::success(sprintf('Exported to %s', $assoc_args['result-file']));
      }

    }

    /**
     * Implementation of command 'sql-query'.
     *
     * @since 4.5
     */
    private function sqlQuery() {

      if (!isset($this->args[0])) {
        WP_CLI::error('No query specified.');
        return;
      }

      $query = $this->args[0];

      civicrm_initialize();
      if (!defined('CIVICRM_DSN')) {
        WP_CLI::error('CIVICRM_DSN is not defined.');
      }

      $dsn = DB::parseDSN(CIVICRM_DSN);

      $mysql_args = [
        'host'     => $dsn['hostspec'],
        'database' => $dsn['database'],
        'user'     => $dsn['username'],
        'password' => $dsn['password'],
        'execute'  => $query,
      ];

      \WP_CLI\Utils\run_mysql_command('mysql --no-defaults', $mysql_args);

    }

    /**
     * Implementation of command 'sql-cli'.
     *
     * @since 4.5
     */
    private function sqlCLI() {

      civicrm_initialize();
      if (!defined('CIVICRM_DSN')) {
        WP_CLI::error('CIVICRM_DSN is not defined.');
      }

      $dsn = DB::parseDSN(CIVICRM_DSN);

      $mysql_args = [
        'host'     => $dsn['hostspec'],
        'database' => $dsn['database'],
        'user'     => $dsn['username'],
        'password' => $dsn['password'],
      ];

      \WP_CLI\Utils\run_mysql_command('mysql --no-defaults', $mysql_args);

    }

    /**
     * Implementation of command 'update-cfg'.
     *
     * @since 4.5
     */
    private function updateConfig() {

      civicrm_initialize();

      $default_values = [];
      $states         = ['old', 'new'];

      for ($i = 1; $i <= 3; $i++) {
        foreach ($states as $state) {
          $name = "{$state}Val_{$i}";
          $value = $this->getOption($name, NULL);
          if ($value) {
            $default_values[$name] = $value;
          }
        }
      }

      $webserver_user  = $this->getWebServerUser();
      $webserver_group = $this->getWebServerGroup();

      require_once 'CRM/Core/I18n.php';
      require_once 'CRM/Core/BAO/ConfigSetting.php';
      $result = CRM_Core_BAO_ConfigSetting::doSiteMove($default_values);

      if ($result) {

        // Attempt to preserve webserver ownership of templates_c, civicrm/upload.
        if ($webserver_user && $webserver_group) {
          $upload_dir      = wp_upload_dir();
          $civicrm_files_dir      = $upload_dir['basedir'] . DIRECTORY_SEPARATOR . 'civicrm' . DIRECTORY_SEPARATOR;
          system(sprintf('chown -R %s:%s %s/templates_c', $webserver_user, $webserver_group, $civicrm_files_dir));
          system(sprintf('chown -R %s:%s %s/upload', $webserver_user, $webserver_group, $civicrm_files_dir));
        }

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

      }
      else {
        WP_CLI::error('Config update failed.');
      }

    }

    /**
     * Implementation of command 'upgrade'.
     *
     * @since 4.5
     */
    private function upgrade() {

      // TODO: Use wp-cli to download tarfile.
      // TODO: If tarfile is not specified, see if the code already exists and use that instead.
      if (!$this->getOption('tarfile', FALSE) && !$this->getOption('zipfile', FALSE)) {
        return WP_CLI::error('Must specify either --tarfile or --zipfile');
      }

      // FIXME: Throw error if tarfile is not in a valid format.
      if (!defined('CIVICRM_UPGRADE_ACTIVE')) {
        define('CIVICRM_UPGRADE_ACTIVE', 1);
      }

      $wp_root       = ABSPATH;
      $plugins_dir = plugin_dir_path(__FILE__);
      $legacy_settings_file = $plugins_dir . '/civicrm.settings.php';
      $upload_dir      = wp_upload_dir();
      $settings_file     = $upload_dir['basedir'] . DIRECTORY_SEPARATOR . 'civicrm' . DIRECTORY_SEPARATOR . 'civicrm.settings.php';
      if (!file_exists($legacy_settings_file) && !file_exists($settings_file)) {
        return WP_CLI::error(sprintf('Unable to locate settings file at "%s" or at "%s"', $legacy_settings_file, $settings_file));
      }

      /*
       * We don't want to require "civicrm.settings.php" here, because:
       *
       * a) This is the old environment we're going to replace.
       * b) upgrade-db needs to bootstrap the new environment, so requiring the file
       *    now will create multiple inclusion problems later on.
       *
       * However, all we're really after is $civicrm_root and CIVICRM_DSN, so we're going to
       * pull out the lines we need using a regex and run them - yes, it's pretty silly.
       * Don't try this at home, kids.
       */

      $legacy_settings = file_get_contents($legacy_settings_file);
      $legacy_settings = str_replace("\r", '', $legacy_settings);
      $legacy_settings = explode("\n", $legacy_settings);
      $settings = file_get_contents($settings_file);
      $settings = str_replace("\r", '', $settings);
      $settings = explode("\n", $settings);

      if ($civicrm_root_code = reset(preg_grep('/^\s*\$civicrm_root\s*=.*$/', $legacy_settings))) {
        // phpcs:disable
        eval($civicrm_root_code);
        // phpcs:enable
      }
      elseif ($civicrm_root_code = reset(preg_grep('/^\s*\$civicrm_root\s*=.*$/', $settings))) {
        // phpcs:disable
        eval($civicrm_root_code);
        // phpcs:enable
      }
      else {
        return WP_CLI::error('Unable to read $civicrm_root from civicrm.settings.php');
      }

      if ($civicrm_dsn_code = reset(preg_grep('/^\s*define.*CIVICRM_DSN.*$/', $settings))) {
        $civicrm_dsn_code = str_replace('CIVICRM_DSN', 'CIVICRM_OLD_DSN', $civicrm_dsn_code);
        // phpcs:disable
        eval($civicrm_dsn_code);
        // phpcs:enable
      }
      else {
        return WP_CLI::error('Unable to read CIVICRM_DSN from civicrm.settings.php');
      }

      if (!defined('CIVICRM_OLD_DSN')) {
        return WP_CLI::error('Unable to set CIVICRM_OLD_DSN.');
      }

      $date        = date('YmdHis');
      $backup_file = 'civicrm';

      $basepath = explode('/', $civicrm_root);

      if (!end($basepath)) {
        array_pop($basepath);
      }

      array_pop($basepath);
      $project_path = implode('/', $basepath) . '/';
      array_pop($basepath);
      $plugin_path = implode('/', $basepath) . '/';

      $backup_dir = $this->getOption('backup-dir', $wp_root . '../backup');
      $backup_dir = rtrim($backup_dir, '/');

      WP_CLI::line();
      WP_CLI::line('The upgrade process involves:');
      WP_CLI::line(sprintf('1. Backing up current CiviCRM code as => %s', "$backup_dir/plugins/$date/$backup_file"));
      WP_CLI::line(sprintf('2. Backing up database as => %s', "$backup_dir/plugins/$date/$backup_file.sql"));
      WP_CLI::line(sprintf('3. Unpacking tarfile to => %s', $plugin_path));
      WP_CLI::line('4. Executing "civicrm/upgrade?reset=1" just as a browser would.');
      WP_CLI::line();

      WP_CLI::confirm('Do you really want to continue?');

      // Begin upgrade.
      $backup_dir .= '/plugins/' . $date;
      if (!mkdir($backup_dir, 0755, TRUE)) {
        return WP_CLI::error(sprintf('Failed to create directory: %s', $backup_dir));
      }

      $backup_target = $backup_dir . '/' . $backup_file;

      if (!rename($project_path, $backup_target)) {
        return WP_CLI::error(sprintf('Failed to backup CiviCRM project directory %s to %s', $project_path, $backup_target));
      }

      WP_CLI::line();
      WP_CLI::success('1. Code backed up.');

      WP_CLI::run_command(
        ['civicrm', 'sql-dump'],
        ['result-file' => $backup_target . '.sql']
      );

      WP_CLI::success('2. Database backed up.');

      // Decompress.
      if ($this->getOption('tarfile', FALSE)) {
        // Should probably never get to here, because WordPress CiviCRM comes in a zip file.
        if (!$this->untar($plugin_path)) {
          return WP_CLI::error('Error extracting tarfile');
        }
      }
      elseif ($this->getOption('zipfile', FALSE)) {
        if (!$this->unzip($plugin_path)) {
          return WP_CLI::error('Error extracting zipfile');
        }
      }
      else {
        return WP_CLI::error('No zipfile specified, use --zipfile=path/to/zipfile');
      }

      WP_CLI::success('3. Archive unpacked.');

      WP_CLI::line('Copying civicrm.settings.php to ' . $project_path . '..');
      define('CIVICRM_SETTINGS_PATH', $project_path . 'civicrm.settings.php');

      if (!copy($backup_dir . '/civicrm/civicrm.settings.php', CIVICRM_SETTINGS_PATH)) {
        return WP_CLI::error('Failed to copy file.');
      }

      WP_CLI::success('4. Settings file copied.');

      WP_CLI::run_command(['civicrm', 'upgrade-db'], []);

      WP_CLI::success('Process completed.');

    }

    /**
     * Implementation of command 'upgrade-db'.
     *
     * @since 4.5
     */
    private function upgradeDB() {

      civicrm_initialize();

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

      if (class_exists('CRM_Upgrade_Headless')) {

        // CRM_Upgrade_Headless introduced in 4.2 - at the same time as class auto-loading.
        try {
          $upgrade_headless = new CRM_Upgrade_Headless();
          $result = $upgrade_headless->run();
          WP_CLI::line(sprintf('Upgrade outputs: "%s"', $result['message']));
        }
        catch (Exception $e) {
          WP_CLI::error($e->getMessage());
        }

      }
      else {

        require_once 'CRM/Core/Smarty.php';
        $template = CRM_Core_Smarty::singleton();

        require_once 'CRM/Upgrade/Page/Upgrade.php';
        $upgrade = new CRM_Upgrade_Page_Upgrade();

        // New since CiviCRM 4.1.
        if (is_callable([$upgrade, 'setPrint'])) {
          $upgrade->setPrint(TRUE);
        }

        // To suppress HTML output with source code.
        ob_start();
        $upgrade->run();
        // Capture the required message.
        $result = $template->get_template_vars('message');
        ob_end_clean();
        WP_CLI::line(sprintf('Upgrade outputs: "%s"', $result));

      }

    }

    /**
     * DSN parser - this has been stolen from PEAR DB since we don't always have a
     * bootstrapped environment we can access this from, eg: when doing an upgrade.
     *
     * @since 4.5
     *
     * @param string|array $dsn
     * @return array $parsed The arry containing db connection details.
     */
    private static function parseDSN($dsn) {

      $parsed = [
        'phptype'  => FALSE,
        'dbsyntax' => FALSE,
        'username' => FALSE,
        'password' => FALSE,
        'protocol' => FALSE,
        'hostspec' => FALSE,
        'port'     => FALSE,
        'socket'   => FALSE,
        'database' => FALSE,
      ];

      if (is_array($dsn)) {
        $dsn = array_merge($parsed, $dsn);
        if (!$dsn['dbsyntax']) {
          $dsn['dbsyntax'] = $dsn['phptype'];
        }
        return $dsn;
      }

      // Find phptype and dbsyntax.
      if (($pos = strpos($dsn, '://')) !== FALSE) {
        $str = substr($dsn, 0, $pos);
        $dsn = substr($dsn, $pos + 3);
      }
      else {
        $str = $dsn;
        $dsn = NULL;
      }

      // Get phptype and dbsyntax.
      // $str => phptype(dbsyntax)
      if (preg_match('|^(.+?)\((.*?)\)$|', $str, $arr)) {
        $parsed['phptype']  = $arr[1];
        $parsed['dbsyntax'] = !$arr[2] ? $arr[1] : $arr[2];
      }
      else {
        $parsed['phptype']  = $str;
        $parsed['dbsyntax'] = $str;
      }

      if (empty($dsn)) {
        return $parsed;
      }

      // Get (if found): username and password.
      // $dsn => username:password@protocol+hostspec/database
      if (($at = strrpos($dsn, '@')) !== FALSE) {
        $str = substr($dsn, 0, $at);
        $dsn = substr($dsn, $at + 1);
        if (($pos = strpos($str, ':')) !== FALSE) {
          $parsed['username'] = rawurldecode(substr($str, 0, $pos));
          $parsed['password'] = rawurldecode(substr($str, $pos + 1));
        }
        else {
          $parsed['username'] = rawurldecode($str);
        }
      }

      // Find protocol and hostspec.

      if (preg_match('|^([^(]+)\((.*?)\)/?(.*?)$|', $dsn, $match)) {
        // $dsn => proto(proto_opts)/database
        $proto       = $match[1];
        $proto_opts  = $match[2] ? $match[2] : FALSE;
        $dsn         = $match[3];

      }
      else {
        // $dsn => protocol+hostspec/database (old format)
        if (strpos($dsn, '+') !== FALSE) {
          list($proto, $dsn) = explode('+', $dsn, 2);
        }
        if (strpos($dsn, '/') !== FALSE) {
          list($proto_opts, $dsn) = explode('/', $dsn, 2);
        }
        else {
          $proto_opts = $dsn;
          $dsn = NULL;
        }
      }

      // Process the different protocol options.
      $parsed['protocol'] = (!empty($proto)) ? $proto : 'tcp';
      $proto_opts = rawurldecode($proto_opts);
      if (strpos($proto_opts, ':') !== FALSE) {
        list($proto_opts, $parsed['port']) = explode(':', $proto_opts);
      }
      if ('tcp' == $parsed['protocol']) {
        $parsed['hostspec'] = $proto_opts;
      }
      elseif ('unix' == $parsed['protocol']) {
        $parsed['socket'] = $proto_opts;
      }

      // Get dabase if any.
      // $dsn => database
      if ($dsn) {
        if (($pos = strpos($dsn, '?')) === FALSE) {
          // /database
          $parsed['database'] = rawurldecode($dsn);
        }
        else {
          // /database?param1=value1&param2=value2
          $parsed['database'] = rawurldecode(substr($dsn, 0, $pos));
          $dsn = substr($dsn, $pos + 1);
          if (strpos($dsn, '&') !== FALSE) {
            $opts = explode('&', $dsn);
          }
          else {
            // database?param1=value1
            $opts = [$dsn];
          }
          foreach ($opts as $opt) {
            list($key, $value) = explode('=', $opt);
            if (!isset($parsed[$key])) {
              // Don't allow params overwrite.
              $parsed[$key] = rawurldecode($value);
            }
          }
        }
      }

      return $parsed;

    }

    /**
     * Helper function to replicate functionality of 'drush_get_option'.
     *
     * @since 4.5
     *
     * @param string $name
     * @param string $default
     * @return mixed The value if found or default if not.
     */
    private function getOption($name, $default) {
      return isset($this->assoc_args[$name]) ? $this->assoc_args[$name] : $default;
    }

    /**
     * Get the user the web server runs as - used to preserve file permissions on
     * templates_c, civicrm/upload etc when running as root. This is not a very
     * good check, but is good enough for what we want to do, which is to preserve
     * file permissions.
     *
     * @since 4.5
     *
     * @return string The user which owns templates_c. Empty string if not found.
     */
    private function getWebServerUser() {

      $plugins_dir = plugin_dir_path(__FILE__);
      $plugins_dir_root = WP_PLUGIN_DIR;
      $upload_dir      = wp_upload_dir();
      $tpl_path     = $upload_dir['basedir'] . DIRECTORY_SEPARATOR . 'civicrm' . DIRECTORY_SEPARATOR . 'templates_c';
      $legacy_tpl_path = $plugins_dir_root . '/files/civicrm/templates_c';

      if (is_dir($legacy_tpl_path)) {
        $owner = posix_getpwuid(fileowner($legacy_tpl_path));
        if (isset($owner['name'])) {
          return $owner['name'];
        }
      }
      elseif (is_dir($tpl_path)) {
        $owner = posix_getpwuid(fileowner($tpl_path));
        if (isset($owner['name'])) {
          return $owner['name'];
        }
      }

      return '';

    }

    /**
     * Get the group the webserver runs as - as above, but for group.
     *
     * @since 4.5
     *
     * @return string The group the webserver runs as. Empty string if not found.
     */
    private function getWebServerGroup() {

      $plugins_dir = plugin_dir_path(__FILE__);
      $plugins_dir_root = WP_PLUGIN_DIR;
      $upload_dir      = wp_upload_dir();
      $tpl_path     = $upload_dir['basedir'] . DIRECTORY_SEPARATOR . 'civicrm' . DIRECTORY_SEPARATOR . 'templates_c';
      $legacy_tpl_path = $plugins_dir_root . '/files/civicrm/templates_c';

      if (is_dir($legacy_tpl_path)) {
        $group = posix_getgrgid(filegroup($legacy_tpl_path));
        if (isset($group['name'])) {
          return $group['name'];
        }
      }
      elseif (is_dir($tpl_path)) {
        $group = posix_getgrgid(filegroup($tpl_path));
        if (isset($group['name'])) {
          return $group['name'];
        }
      }

      return '';

    }

    /**
     * Extracts a tar.gz archive.
     *
     * @since 4.5
     *
     * @param string $destination_path The path to extract to.
     * @param string $option The command line option to get input filename from, defaults to 'tarfile'.
     * @return bool True if successful, false otherwise.
     */
    private function untar($destination_path, $option = 'tarfile') {

      if ($tarfile = $this->getOption($option, FALSE)) {
        WP_CLI::line('Extracting tar.gz archive...');
        WP_CLI::launch("gzip -d $tarfile");
        $tarfile = substr($tarfile, 0, strlen($tarfile) - 3);
        WP_CLI::launch("tar -xf $tarfile -C \"$destination_path\"");
        return TRUE;
      }
      else {
        return FALSE;
      }

    }

    /**
     * Extracts a zip archive.
     *
     * @since 4.5
     *
     * @param string $destination_path The path to extract to.
     * @param string $option The command line option to get zip filename from, defaults to 'zipfile'.
     * @return bool True if successful, false otherwise.
     */
    private function unzip($destination_path, $option = 'zipfile') {

      if ($zipfile = $this->getOption($option, FALSE)) {
        WP_CLI::line('Extracting zip archive...');
        WP_CLI::launch("unzip -q $zipfile -d $destination_path");
        return TRUE;
      }
      else {
        return FALSE;
      }

    }

  }

  WP_CLI::add_command('civicrm', 'CiviCRM_Command');
  WP_CLI::add_command('cv', 'CiviCRM_Command');

  // Set path early.
  WP_CLI::add_hook('before_wp_load', function() {

    global $civicrm_paths;
    $wp_cli_config = WP_CLI::get_config();

    // If --path is set, save for later use by CiviCRM.
    if (!empty($wp_cli_config['path'])) {
      $civicrm_paths['cms.root']['path'] = $wp_cli_config['path'];
    }

    // If --url is set, save for later use by CiviCRM.
    if (!empty($wp_cli_config['url'])) {
      $civicrm_paths['cms.root']['url'] = $wp_cli_config['url'];
    }

  });

}