<?php
/*
 * This file is part of MODX Revolution.
 *
 * Copyright (c) MODX, LLC. All Rights Reserved.
 *
 * For complete copyright and license information, see the COPYRIGHT and LICENSE
 * files found in the top-level directory of this distribution.
 */
require_once strtr(realpath(MODX_SETUP_PATH.'includes/request/modinstallrequest.class.php'),'\\','/');

use xPDO\xPDO;

/**
 * modInstallCLIRequest
 *
 * @package setup
*/

/**
 * Handles CLI installs.
 *
 * @package setup
 */
class modInstallCLIRequest extends modInstallRequest {
    /** @var modInstall $install */
    public $install;
    /** @var int $timeStart */
    public $timeStart = 0;
    /** @var string $timeTotal */
    public $timeTotal = '';

    /**
     * Constructor for modInstallConnector object.
     *
     * @constructor
     * @param modInstall &$modInstall A reference to the modInstall object.
     */
    function __construct(modInstall &$modInstall) {
        $this->install =& $modInstall;
        $this->install->loadSettings();
    }

    /**
     * Parse the install mode from a string or int
     * @param string|int $mode
     * @return int
     */
    public function getInstallMode($mode) {
        if (!is_int($mode)) {
            switch ($mode) {
                case 'new':
                    $mode = modInstall::MODE_NEW;
                    break;
                case 'upgrade-evo':
                    $mode = modInstall::MODE_UPGRADE_EVO;
                    break;
                case 'upgrade-revo-advanced':
                case 'upgrade-advanced':
                    $mode = modInstall::MODE_UPGRADE_REVO_ADVANCED;
                    break;
                case 'upgrade-revo':
                case 'upgrade':
                default:
                    $mode = modInstall::MODE_UPGRADE_REVO;
                    break;
            }
        }
        return $mode;
    }

    /**
     * Handles connector requests.
     *
     * @param string $action
     */
    public function handle($action = '') {
        $this->beginTimer();
        /* prepare the settings */
        $settings = $_REQUEST;
        if (empty($settings['installmode'])) $settings['installmode'] = modInstall::MODE_NEW;
        $settings['installmode'] = $this->getInstallMode($settings['installmode']);
        $this->install->settings->fromArray($settings); /* load CLI args into settings */

        /* load the config.xml file */
        $config = $this->getConfig($settings['installmode']);
        if (empty($config)) {
            $this->end($this->install->lexicon('cli_no_config_file'));
        }
        $this->install->settings->fromArray($config);
        $this->install->settings->fromArray($settings); /* do again to allow CLI-based overrides of config.xml */

        /* load the driver */
        $this->install->loadDriver();

        /* Run tests */
        $mode = (int)$this->install->settings->get('installmode');
        $results= $this->install->test($mode);
        if (!$this->install->test->success) {
            $msg = "\n";
            foreach ($results['fail'] as $field => $result) {
                $msg .= $field.': '.$result['title'].' - '.$result['message'];
            }
            $msg = $this->install->lexicon('cli_tests_failed', [
                'errors' => $msg,
            ]);
            $this->end($msg);
        }

        /** Attempt to create the database */
        $this->checkDatabase();

        /* Run installer */
        $this->install->getService('runner','runner.modInstallRunnerWeb');
        $failed = true;
        $errors = [];
        if ($this->install->runner) {
            $success = $this->install->runner->run($mode);
            $results = $this->install->runner->getResults();

            $failed= false;
            foreach ($results as $item) {
                if ($item['class'] === 'failed') {
                    $failed= true;
                    $this->install->xpdo->log(xPDO::LOG_LEVEL_ERROR,$item['msg']);
                    $errors[] = $item;
                    break;
                }
            }
        }

        if ($failed) {
            $msg = "\n";
            foreach ($errors as $field => $result) {
                $msg .= $result['msg'];
            }
            $msg = $this->install->lexicon('cli_install_failed', [
                'errors' => $msg,
            ]);
            $this->end($msg);
        }

        /* cleanup */
        $errors= $this->install->verify();
        foreach ($errors as $error) {
            $this->install->xpdo->log(xPDO::LOG_LEVEL_ERROR,$error);
        }
        $cleanupErrors = $this->install->cleanup();
        foreach ($cleanupErrors as $key => $error) {
            $this->install->xpdo->log(xPDO::LOG_LEVEL_ERROR,$error);
        }

        if ($this->install->settings->get('remove_setup_directory')) {
            $this->install->removeSetupDirectory();
        }
        $this->endTimer();
        $this->end(''.$this->install->lexicon('installation_finished', [
            'time' => $this->timeTotal,
            ]));
    }

    /**
     * {@inheritDoc}
     * @param int $mode
     * @param array $config
     * @return array
     */
    public function getConfig($mode = 0, array $config = []) {
        /* load the config file */
        $config = array_merge($this->loadConfigFile(), $config);
        $config = parent::getConfig($mode, $config);
        $this->prepareSettings($config);
        return $config;
    }

    /**
     * Attempt to load the config.xml (or other config file) to use when installing. One must be present to run
     * MODX Setup in CLI mode.
     *
     * @return array
     */
    public function loadConfigFile() {
        $settings = [];
        $configFile = $this->install->settings->get('config');
        if (empty($configFile)) $configFile = MODX_INSTALL_PATH.'setup/config.xml';
        if (!empty($configFile)) {
            if (!file_exists($configFile) && file_exists(MODX_SETUP_PATH.$configFile)) {
                $configFile = MODX_SETUP_PATH.$configFile;
            }
        } elseif (file_exists(MODX_SETUP_PATH.'config.xml')) {
            $configFile = MODX_SETUP_PATH.'config.xml';
        }
        if (!empty($configFile) && file_exists($configFile)) {
            $settings = $this->parseConfigFile($configFile);
        }
        return $settings;
    }

    /**
     * Prepares settings for installation, including setting of defaults
     *
     * @param array $settings
     * @return void
     */
    public function prepareSettings(array &$settings) {
        if (empty($settings['site_sessionname'])) {
            $settings['site_sessionname'] = 'SN' . uniqid('');
        }
        if (empty($settings['config_options'])) {
            $settings['config_options'] = [];
        }
        if (empty($settings['database']) && !empty($settings['dbase'])) {
            $settings['database'] = $settings['dbase'];
        }
        $settings['database_dsn'] = $this->getDatabaseDSN($settings['database_type'],$settings['database_server'],$settings['database'],$settings['database_connection_charset']);
        if (!empty($settings['database'])) {
            $settings['dbase'] = $settings['database'];
        }
        $this->install->settings->fromArray($settings);

        $this->setDefaultSetting('processors_path',$this->install->settings->get('core_path').'src/Revolution/Processors/');
        $this->setDefaultSetting('connectors_path',$this->install->settings->get('context_connectors_path'));
        $this->setDefaultSetting('connectors_url',$this->install->settings->get('context_connectors_url'));
        $this->setDefaultSetting('mgr_path',$this->install->settings->get('context_mgr_path'));
        $this->setDefaultSetting('mgr_url',$this->install->settings->get('context_mgr_url'));
        $this->setDefaultSetting('web_path',$this->install->settings->get('context_web_path'));
        $this->setDefaultSetting('web_url',$this->install->settings->get('context_web_url'));
        $this->setDefaultSetting('assets_path',$this->install->settings->get('context_assets_path',$this->install->settings->get('context_web_path').'assets/'));
        $this->setDefaultSetting('assets_url',$this->install->settings->get('context_assets_url',$this->install->settings->get('context_web_url').'assets/'));
    }

    /**
     * Sets a default for a setting if not set
     * @param string $key
     * @param mixed $default
     * @return void
     */
    public function setDefaultSetting($key,$default) {
        $value = $this->install->settings->get($key,null);
        if ($value === null) {
            $this->install->settings->set($key,$default);
        }
    }

    /**
     * Parse the config XML file
     *
     * @param string $file
     * @return array
     */
    public function parseConfigFile($file) {
        $contents = file_get_contents($file);
        $xml = new SimpleXMLElement($contents);

        $settings = [];
        foreach ($xml as $k => $v) {
            $settings[(string)$k] = (string)$v;
        }

        return $settings;
    }

    /**
     * Check database settings
     * @return void
     */
    public function checkDatabase() {
        $mode = $this->install->settings->get('installmode');
        if ($mode == modInstall::MODE_NEW) {
            $results = $this->install->driver->verifyServerVersion();
            if ($results['result'] == 'failure') {
                $this->end($results['message']);
            }

            $this->install->xpdo = null;
        }

        /* get an instance of xPDO using the install settings */
        $xpdo = $this->install->getConnection($mode);
        if (!is_object($xpdo) || !($xpdo instanceof xPDO)) {
            $this->end($this->install->lexicon('xpdo_err_ins'));
        }

        /* try to get a connection to the actual database */
        $dbExists = $xpdo->connect();
        if (!$dbExists) {
            if ($mode == modInstall::MODE_NEW && $xpdo->getManager()) {
                /* otherwise try to create the database */
                $dbExists = $xpdo->manager->createSourceContainer(
                    [
                        'dbname' => $this->install->settings->get('dbase')
                        ,'host' => $this->install->settings->get('database_server')
                    ]
                    ,$this->install->settings->get('database_user')
                    ,$this->install->settings->get('database_password')
                    , [
                        'charset' => $this->install->settings->get('database_connection_charset')
                        ,'collation' => $this->install->settings->get('database_collation')
                    ]
                );
                if (!$dbExists) {
                    $this->end($this->install->lexicon('db_err_create_database'));
                } else {
                    $xpdo = $this->install->getConnection($mode);
                    if (!is_object($xpdo) || !($xpdo instanceof xPDO)) {
                        $this->end($this->install->lexicon('xpdo_err_ins'));
                    }
                }
            } elseif ($mode == modInstall::MODE_NEW) {
                $this->end($this->install->lexicon('db_err_connect_server'));
            }
        }
        if (!$xpdo->connect()) {
            $this->end($this->install->lexicon('db_err_connect'));
        }

        /* test table prefix */
        if ($mode == modInstall::MODE_NEW || $mode == modInstall::MODE_UPGRADE_REVO_ADVANCED) {
            $count = null;
            $database = $this->install->settings->get('dbase');
            $prefix = $this->install->settings->get('table_prefix');
            $stmt = $xpdo->query($this->install->driver->testTablePrefix($database,$prefix));
            if ($stmt) {
                $row = $stmt->fetch(PDO::FETCH_ASSOC);
                if ($row) {
                    $count = (int) $row['ct'];
                }
                $stmt->closeCursor();
            }
            if ($mode == modInstall::MODE_NEW && $count !== null) {
                $this->end($this->install->lexicon('test_table_prefix_inuse'));
            } elseif ($mode == modInstall::MODE_UPGRADE_REVO_ADVANCED && $count === null) {
                $this->end($this->install->lexicon('test_table_prefix_nf'));
            }
        }
    }

    /**
     * End the PHP session and output a message
     *
     * @param string $message
     * @return void
     */
    public function end($message = '') {
        @session_write_close();
        die($message."\n");
    }

    /**
     * Start the debugging timer
     * @return int
     */
    protected function beginTimer() {
        $mtime = microtime();
        $mtime = explode(" ", $mtime);
        $mtime = $mtime[1] + $mtime[0];
        $this->timeStart = $mtime;
        return $this->timeStart;
    }
    /**
     * End the debugging timer
     * @return string
     */
    protected function endTimer() {
        $mtime = microtime();
        $mtime = explode(" ", $mtime);
        $mtime = $mtime[1] + $mtime[0];
        $tend = $mtime;
        $this->timeTotal = ($tend - $this->timeStart);
        $this->timeTotal = sprintf("%2.4f s", $this->timeTotal);
        return $this->timeTotal;
    }
}
