<?php
/**
 * File: installer.php
 * Author: Ryan Prather <ryan.prather@cyberperspectives.com>
 * Purpose: This script runs the installer processes
 * Created: Nov 28, 2017
 *
 * Copyright 2017-2018: Cyber Perspective, LLC, All rights reserved
 * Released under the Apache v2.0 License
 *
 * See license.txt for details
 *
 * Change Log:
 *  - Nov 28, 2017 - File created
 *  - Dec 27, 2017 - Fixed bug with SCG showing empty, and added download progress meta keys
 *  - Jan 2, 2018 - Add sleep to fix bug #357 race condition
 *  - Jan 10, 2018 - Formatting
 *  - Apr 29, 2018 - Removed settings to move to .sql file. Also, changed to pull CVEs from NVD instead of Mitre repo
 */
include_once 'helper.inc';
set_time_limit(0);

$params       = [
    'filter' => FILTER_SANITIZE_STRING,
    'flag'   => FILTER_NULL_ON_FAILURE
];
$db_step      = [
    'doc-root'      => $params,
    'pwd-file'      => $params,
    'tmp-path'      => $params,
    'log-path'      => $params,
    'log-level'     => $params,
    'db-server'     => $params,
    'root-uname'    => $params,
    'root-pwd'      => $params,
    'conf-root-pwd' => $params,
    'web-pwd'       => $params,
    'local-path'    => $params,
    'action'        => $params,
    'sample-data'   => ['filter' => FILTER_VALIDATE_BOOLEAN],
    'cpe'           => ['filter' => FILTER_VALIDATE_BOOLEAN],
    'cve'           => ['filter' => FILTER_VALIDATE_BOOLEAN],
    'stig'          => ['filter' => FILTER_VALIDATE_BOOLEAN],
    'update-freq'   => ['filter' => FILTER_VALIDATE_FLOAT, 'flag' => FILTER_NULL_ON_FAILURE]
];
$company_step = [
    'company'       => $params,
    'comp-add'      => $params,
    'last-modified' => $params,
    'creator'       => $params,
    'system-class'  => $params,
    'classified-by' => $params,
    'scg'           => $params,
    'derived-on'    => $params,
    'declassify-on' => $params
];
$options_step = [
    'flatten'       => ['filter' => FILTER_VALIDATE_BOOLEAN],
    'wrap-text'     => ['filter' => FILTER_VALIDATE_BOOLEAN],
    'notifications' => ['filter' => FILTER_VALIDATE_BOOLEAN],
    'port-limit'    => [
        'filter'  => FILTER_VALIDATE_INT,
        'flag'    => FILTER_REQUIRE_ARRAY,
        'options' => ['max_range' => 10000]
    ],
    'max-results'   => [
        'filter'  => FILTER_VALIDATE_INT,
        'flag'    => FILTER_REQUIRE_ARRAY,
        'options' => ['min_range' => 1, 'max_range' => 20]
    ],
    'output-format' => [
        'filter'  => FILTER_VALIDATE_REGEXP,
        'flag'    => FILTER_NULL_ON_FAILURE,
        'options' => ['regexp' => "/xlsx|xls|html|csv|pdf|ods/"]
    ]
];

$step = filter_input(INPUT_POST, 'step', FILTER_VALIDATE_INT);

if ($step == 0) {
    $fields = filter_input_array(INPUT_POST, $db_step);
    save_Database($fields);
}
elseif ($step == 1) {
    $fields = filter_input_array(INPUT_POST, $company_step);
    save_Company($fields);
}
elseif ($step == 2) {
    $fields = filter_input_array(INPUT_POST, $options_step);
    save_Options($fields);
}

/**
 * Function to save database details and load data
 *
 * @param array $params
 */
function save_Database($params)
{
    $config = file_get_contents("config.inc", FILE_USE_INCLUDE_PATH);

    $php   = null;
    $mysql = null;
    if (strtolower(substr(PHP_OS, 0, 3)) == 'lin') {
		$res = [];
		exec("which php", $res);
        if (file_exists('/bin/php')) {
            $php = realpath("/bin/php");
        }
		elseif (is_array($res) && isset($res[0]) && file_exists($res[0])) {
			$php = realpath($res[0]);
		}
        else {
            die(json_encode(['error' => 'Cannot find the PHP executable']));
        }

		$res = [];
		exec("which mysql", $res);
        if (file_exists('/bin/mysql')) {
            $mysql = realpath('/bin/mysql');
        }
		elseif (is_array($res) && isset($res[0]) && file_exists($res[0])) {
			$mysql = realpath($res[0]);
		}
        else {
            die(json_encode(['error' => 'Cannot find the MySQL executable']));
        }
    }
    else {
        if (file_exists("c:/xampp/php/php.exe")) {
            $php = realpath("c:/xampp/php/php.exe");
        }
        else {
            die(json_encode(['error' => 'Cannot find the PHP executable']));
        }

        if (file_exists("c:/xampp/mysql/bin/mysql.exe")) {
            $mysql = realpath("c:/xampp/mysql/bin/mysql.exe");
        }
        else {
            die(json_encode(['error' => 'Cannot find the MySQL executable']));
        }
    }

    my_str_replace("{DOC_ROOT}", realpath($params['doc-root']), $config);
    my_str_replace("{PWD_FILE}", $params['pwd-file'], $config);
    my_str_replace("'{E_ERROR}'", "E_{$params['log-level']}", $config);
    my_str_replace("{PHP_BIN}", $php, $config);
    my_str_replace("{PHP_CONF}", realpath(php_ini_loaded_file()), $config);
    my_str_replace("{DB_SERVER}", $params['db-server'], $config);
    my_str_replace("{DB_BIN}", $mysql, $config);
    my_str_replace("'{UPDATE_FREQ}'", $params['update-freq'], $config);
    my_str_replace("@new", "@step1", $config);

    if (!file_exists($params['tmp-path'])) {
        if (!mkdir($params['tmp-path'])) {
            die(json_encode(['error' => 'Temporary path is not available. Please create and give Apache user write permissions']));
        }
    }
    elseif (!is_dir($params['tmp-path']) || !is_writable($params['tmp-path'])) {
        die(json_encode(['error' => 'TMP path is not a writable directory to Apache']));
    }
    my_str_replace("{TMP_PATH}", $params['tmp-path'], $config);

    if (!file_exists($params['log-path'])) {
        if (!mkdir($params['log-path'])) {
            die(json_encode(['error' => 'Log path is not available. Please create and give Apache user write permissions']));
        }
    }
    elseif (!is_dir($params['log-path']) || !is_writable($params['log-path'])) {
        die(json_encode(['error' => 'Log path is not a writable directory by Apache']));
    }
    my_str_replace("{LOG_PATH}", $params['log-path'], $config);

    file_put_contents("{$params['doc-root']}/config.inc", $config);

    include_once 'config.inc';
    include_once 'database.inc';

    /* ---------------------------------
     * 	CREATE DB PASSWORD FILE
     * --------------------------------- */
    $enc_pwd = my_encrypt($params['web-pwd']);
    if(!file_put_contents(DOC_ROOT . "/" . PWD_FILE, $enc_pwd)) {
        die(json_encode(['error' => "Could not create the password file"]));
    }

    if (isset($params['conf-root-pwd']) && $params['conf-root-pwd'] == $params['root-pwd']) {
        $db = new mysqli(DB_SERVER, $params['root-uname'], '', 'mysql');
        if (!$db->real_query("UPDATE user SET Password=PASSWORD('{$db->real_escape_string($params['root-pwd'])}') WHERE User='root'")) {
            error_log($db->error);
            die(json_encode(['error' => "Could not set the root users password, manually set it and try this again"]));
        }

        $db->real_query("FLUSH PRIVILEGES");
        unset($db);
    }

    $zip        = new ZipArchive();
    $db         = new mysqli(DB_SERVER, $params['root-uname'], $params['root-pwd'], 'mysql');
    if ($db->connect_errno && $db->connect_errno == 1045) {
        die(json_encode(['error' => 'There was a problem with the user/password combination, please go back and try again']));
    }
    elseif ($db->connect_errno) {
        die(json_encode(['error' => "There was an error connecting to the database on " . DB_SERVER . " with user {$params['root-uname']} and {$params['root-pwd']}"]));
    }
    $help = new db_helper($db);

    $svr_ver = (int) $db->server_version;
    $maj     = (int) ($svr_ver / 10000);
    $svr_ver -= ($maj * 10000);
    $min     = (int) ($svr_ver / 100);
    $svr_ver -= ($min * 100);
    $update  = $svr_ver;

    if (version_compare("{$maj}.{$min}.{$update}", "5.5", "<=")) {
        die(json_encode(['error' => "The current version of MySQL needs to be at least 5.5"]));
    }

    // set the character set and default database
    $db->set_charset("utf8");

    /* --------------------------------
     *    USER MANAGEMENT
     * -------------------------------- */
    $help->delete("mysql.user", null, [
        [
            'field' => 'User',
            'op'    => '=',
            'value' => ''
        ]
    ]);
    $help->execute();

    $errors = [];

    /* --------------------------------
     *    SCHEMA MANAGEMENT
     * -------------------------------- */
    if (!$db->real_query("CREATE DATABASE IF NOT EXISTS `rmf`")) {
        $errors[] = $db->error;
    }
    if (!$db->real_query("CREATE DATABASE IF NOT EXISTS `sagacity`")) {
        $errors[] = $db->error;
    }
    $db->real_query("DROP DATABASE IF EXISTS cdcol");
    $db->real_query("DROP DATABASE IF EXISTS phpmyadmin");
    $db->real_query("DROP DATABASE IF EXISTS test");

    /* --------------------------------
     *    SET SCHEMA PERMISSIONS
     * -------------------------------- */
    $host = '%';
    if (in_array(strtolower(DB_SERVER), ["localhost", "127.0.0.1"])) {
        $host = 'localhost';
    }

    $help->select("mysql.user", ["COALESCE(COUNT(1), 0) AS 'count'"], [
        [
            'field' => 'User',
            'op'    => '=',
            'value' => 'web'
        ]
    ]);
    if (!$help->execute()['count']) {
        if (!$db->real_query("CREATE USER 'web'@'$host' IDENTIFIED BY '{$db->real_escape_string($params['web-pwd'])}'")) {
            $errors[] = $db->error;
        }
    }
    else {
        if (!$db->real_query("SET PASSWORD FOR 'web'@'$host' = PASSWORD('{$db->real_escape_string($params['web-pwd'])}')")) {
            $errors[] = $db->error;
        }
    }

    if (!$db->real_query("GRANT ALL ON `rmf`.* TO 'web'@'$host'")) {
        $errors[] = $db->error;
    }
    if (!$db->real_query("GRANT ALL ON `sagacity`.* TO 'web'@'$host'")) {
        $errors[] = $db->error;
    }

    if (count($errors)) {
        die(json_encode(['errors' => implode("<br />", $errors)]));
    }

    $db->real_query("FLUSH PRIVILEGES");
    chdir(realpath(DOC_ROOT));

    $json = json_decode(file_get_contents("db_schema.json"));

    foreach ($json->tables as $table) {
        Sagacity_Error::err_handler("Creating {$table->schema}.{$table->name}");
        $help->create_table_json($table);

        if (isset($table->triggers)) {
            // see if the first entry is a drop statement, run it and remove for subsequent statements
            if (substr($table->triggers[0], 0, 4) == 'DROP') {
                $db->real_query($table->triggers[0]);
                unset($table->triggers[0]);
            }
            // concatenate the trigger into one string
            $trig = implode(" ", $table->triggers);
            if (!$db->real_query(str_replace("{host}", $host, $trig))) {
                die($db->error);
            }
        }

        $help->insert("sagacity.settings", [
            'meta_key' => "{$table->schema}.{$table->name}",
            'db_data'  => json_encode($table)
            ], true);

        if (!$help->execute()) {
            $help->debug(E_WARNING, "JSON for {$table->schema}.{$table->name} table was not pushed to database");
        }
    }

    /*
     * ***********************************************************
     * Load table data
     * ***********************************************************
     */
    chdir(DOC_ROOT);
    $zip->open("Database_Baseline.zip");
    $zip->extractTo("Database_Baseline");
    chdir("Database_Baseline");
    $sql_files = glob("*.sql");
    $zip->close();

    if (!$params['sample-data']) {
        if (($key = array_search("sample_data.sql", $sql_files)) !== false) {
            unset($sql_files[$key]);
            unlink("sample_data.sql");
        }
    }

    $defaults = <<<EOO
[client]
password="{$params['root-pwd']}"
port=3306

EOO;
    file_put_contents(realpath(TMP) . "/defaults.tmp", $defaults);

    $routines = glob("*routines.sql");
    foreach ($routines as $file) {
        if (($key = array_search($file, $sql_files)) !== false) {
            unset($sql_files[$key]);
        }
    }

    if (count($sql_files)) {
        sort($sql_files);
        foreach ($sql_files as $file) {
            $output = [];
            $cmd    = realpath(DB_BIN) . " --defaults-file=\"" . realpath(TMP . "/defaults.tmp") . "\"" .
                " --user={$params['root-uname']}" .
                " --host=" . DB_SERVER .
                " --default-character-set=utf8 < \"$file\"";
            exec($cmd, $output);

            if (preg_grep("/Access Denied/i", $output)) {
                $errors[]   = $output;
            }
            else {
                unlink($file);
            }
        }

        foreach ($routines as $file) {
            $str = file_get_contents($file);
            my_str_replace("{host}", $host, $str);
            file_put_contents($file, $str);

            $cmd = realpath(DB_BIN) . " --defaults-file=\"" . realpath(TMP . "/defaults.tmp") . "\"" .
                " --user={$params['root-uname']}" .
                " --host=" . DB_SERVER .
                " --default-character-set=utf8 < \"$file\"";

            exec($cmd);
            unlink($file);
            flush();
        }
    }

    if (count($errors)) {
        print json_encode(['errors' => implode("<br />", $errors)]);
        return;
    }

    unlink(realpath(TMP . "/defaults.tmp"));
    rmdir(realpath(DOC_ROOT . "/Database_Baseline"));

    $cpe    = null;
    $cve    = null;
    $stig   = null;
    $action = null;
    if ($params['cpe']) {
        $cpe = " --cpe";
    }

    if ($params['cve']) {
        $cve = " --nvd";
    }

    if ($params['stig']) {
        $stig = " --stig";
    }

    $msg = null;
    if ($params['action'] == 'do' || $params['action'] == 'po') {
        $action = " --{$params['action']}";
        $msg    = "Files need to be placed in {doc_root}/tmp for parsing to work correctly";
    }

    print json_encode(['success' => true, 'msg' => $msg]);

    if (!is_null($cpe) || !is_null($cve) || !is_null($stig)) {
        include_once 'vendor/autoload.php';
        $script  = realpath(PHP_BIN) .
            " -c " . realpath(PHP_CONF) .
            " -f " . realpath(DOC_ROOT . "/exec/update_db.php") .
            " --{$cpe}{$cve}{$stig}{$action}";
        $process = new Cocur\BackgroundProcess\BackgroundProcess($script);
        $process->run();
    }
}

/**
 * Function to save company information
 *
 * @param array $fields
 */
function save_Company($fields)
{
    $config = file_get_contents("config.inc", FILE_USE_INCLUDE_PATH);

    $scg_date     = new DateTime($fields['derived-on']);
    $declass_date = new DateTime($fields['declassify-on']);

    if (!is_a($scg_date, "DateTime") || !is_a($declass_date, "DateTime")) {
        print json_encode(['error' => 'Error parsing the dates']);
        return;
    }

    my_str_replace("{COMPANY}", $fields['company'], $config);
    my_str_replace("{COMP_ADD}", $fields['comp-add'], $config);
    my_str_replace("{LAST_MODIFIED_BY}", $fields['last-modified'], $config);
    my_str_replace("{CREATOR}", $fields['creator'], $config);
    my_str_replace("{SYSTEM_CLASS}", $fields['system-class'], $config);
    my_str_replace("{CLASSIFIED_BY}", $fields['classified-by'], $config);
    my_str_replace("{SCG}", $fields['scg'], $config);
    my_str_replace("{DERIVED_ON}", $scg_date->format("Y-m-d"), $config);
    my_str_replace("{DECLASSIFY_ON}", $declass_date->format("Y-m-d"), $config);
    my_str_replace("@step1", "@step2", $config);

    file_put_contents(dirname(dirname(__FILE__)) . "/config.inc", $config);

    print json_encode(['success' => true]);
}

/**
 * Function to save Sagacity options
 *
 * @param array $fields
 */
function save_Options($fields)
{
    $config = file_get_contents("config.inc", FILE_USE_INCLUDE_PATH);

    my_str_replace("'{FLATTEN}'", ($fields['flatten'] ? 'true' : 'false'), $config);
    my_str_replace("'{WRAP_TEXT}'", ($fields['wrap-text'] ? 'true' : 'false'), $config);
    my_str_replace("'{NOTIFICATIONS}'", ($fields['notifications'] ? 'true' : 'false'), $config);
    my_str_replace("'{PORT_LIMIT}'", $fields['port-limit'], $config);
    my_str_replace("'{MAX_RESULTS}'", $fields['max-results'], $config);
    my_str_replace("{ECHECKLIST_FORMAT}", $fields['output-format'], $config);
    my_str_replace("@step2", "", $config);

    file_put_contents(dirname(dirname(__FILE__)) . "/config.inc", $config);

    print json_encode(['success' => true]);
}
