#!/usr/bin/php
<?php
//
// Command Subsystem
// Copyright (c) 2017-2018 Nagios Enterprises, LLC. All rights reserved.
//
define('SUBSYSTEM', 1);
require_once(dirname(__FILE__) . '/../html/includes/base.inc.php');

subsys_get_options($default_time = 60);
cmd_subsys();

function cmd_subsys() {

	global $max_time;

	$start_time = time();
	$total_cmds_processed = 0;

	log_debug(LOG_TYPE_SYSTEM, "cmd_subsys() called at {$start_time}");

	while ((time() - $start_time) <= $max_time) {

		$cmds_processed = 0;

		$cmds_processed += process_commands();
		$total_cmds_processed += $cmds_processed;

		if ($cmds_processed == 0)
			sleep(1);
	}

	log_debug(LOG_TYPE_SYSTEM, "Processed {$total_cmds_processed} commands");

	// update sysstat for cmd subsys
	set_sysstat_data('cmd_subsys', SUBSYS_OK);

	// TODO: this, but name it something else
	// do_uloop_jobs();
}

// process commands
// returns number of commands processed
function process_commands() {

	global $db;

	$bind_array = array(':status_code' => COMMAND_STATUS_QUEUED);
	$commands = $db->exec_query('SELECT * FROM commands WHERE status_code = :status_code AND scheduled_time <= NOW() ORDER BY submission_time ASC LIMIT 1', $bind_array);

	if ($commands === false)
		subsys_exit('process_commands() error');

	foreach ($commands as $command) {

		$processed = process_command_row($command);
		return $processed;
	}

	return 0;
}

// process a command row
// command row is an array containing the command information
// return 0 if command wasn't processed, 1 if it was
function process_command_row($command) {

	if (empty($command) || !is_array($command))
		return 0;

	if (empty($command['command_id']) || !isset($command['command']))
		return 0;

	global $db;

	// keep track of processing time to update sql later
	$start_time = time();

	log_info(LOG_TYPE_SYSTEM, 'Processing command: ' . one_line_print_r($command) . " at {$start_time}");

	// update command with real start time and processing status code
	$db->query('UPDATE commands 
		SET 
		start_time = NOW(), 
		status_code = :status_code 
		WHERE command_id = :command_id LIMIT 1');
	$db->bind(':status_code', COMMAND_STATUS_PROCESSING);
	$db->bind(':command_id', $command['command_id']);

	if ($db->exec() === false)
		subsys_exit('process_command_row() failure on first update ' . one_line_print_r($db->get_errors()));

	log_debug(LOG_TYPE_SYSTEM, "Got command {$command['command']} with data {$command['command_data']}");

	//TODO: audit logging that a command is about to be processed?
	//TODO: in xi, the log is sent during the command process. i propose it is auditted during submission AND completion
	process_command($command['command'], $command['command_data'], $result_code, $result);

	log_debug(LOG_TYPE_SYSTEM, "Got result_code {$result_code} and result {$result}");

	// how long did all this take?
	$processing_time = time() - $start_time;

	// update command with result, result_code, status_code [finished], and the processing time
	$db->query('UPDATE commands 
		SET 
		result = :result, 
		result_code = :result_code, 
		status_code = :status_code, 
		processing_time = :processing_time 
		WHERE command_id = :command_id LIMIT 1');
	$db->bind(':result', $result);
	$db->bind(':result_code', $result_code);
	$db->bind(':status_code', COMMAND_STATUS_COMPLETED);
	$db->bind(':processing_time', $processing_time);
	$db->bind(':command_id', $command['command_id']);

	if ($db->exec() === false)
		subsys_exit('process_command_row() failure on second update ' . one_line_print_r($db->get_errors()));

	return 1;
}

// the actual process command function
// result_code and result are values by reference
// these are updated with the appropriate values on failure/success
function process_command($command, $data, &$result_code, &$result) {

    // we may need these
    require_once(get_base_dir() . '/includes/utils/dashlets.inc.php');
    init_dashlets();
	global $components;
	global $dashlets;

	// these are the variables you set in the individual case statements
	// in the big switch below
	// script name/data will be converted to a cmd_line
	// and then the cmd_line will be executed
	// the output from the command will be set to $result
	// and the return code from the command will be set to $result_code
	// you can obviously set these yourself and bypass the cmd execution
	// if so required
	$cmd_line = '';
	$script_name = '';
	$script_data = '';

	// if you execute a command that restarts mysql, make sure you set this to true
	$restarted_mysql = false;

	// these variables are post function callbacks that you may want to execute
	// these will be executed AFTER cmd_line is executed if it was successful
	// the result_code will become the return code from the post func command
	$post_func = '';
	$post_func_args = array();

	// some variables that will be able to be used without having to 
	// call them all the time
	$root_dir = get_root_dir();
	$base_dir = get_base_dir();
	$tmp_dir = get_tmp_dir();
	$scripts_dir = get_scripts_dir();

	// commands present in this array won't log sensitive information
	// unless debugging is enabled (then everything gets printed)
	$commands_with_sensitive_data = array();

	log_info(LOG_TYPE_SYSTEM, "Processing command: {$command}");
	log_info(LOG_TYPE_SYSTEM, "Command data: {$data}");

	// default values
	$result_code = COMMAND_RESULT_ERROR;
	$result = '';

/////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////

	// this is where the magic happens
	switch ($command) {

		case COMMAND_CHANGE_TIMEZONE:
			$restarted_mysql = true;
			$timezone = escapeshellarg($data);
			$cmd_line = "sudo {$root_dir}/scripts/change_timezone.sh -z '{$timezone}'";
			break;

		case COMMAND_INSTALL_COMPONENT:
		    $file = sanitize_filename($data);

            if (empty($file))
                return COMMAND_RESULT_ERROR;

            // create a tmp dir for holding the component while we check it
            $tmp_component_dir = $tmp_dir . '/' . random_string(5);
            log_debug(LOG_TYPE_SYSTEM, "INSTALL_COMPONENT: tmp_component_dir: {$tmp_component_dir}");

            system("rm -rf {$tmp_component_dir}");
            mkdir($tmp_component_dir);
            $safe_component_file = escapeshellarg("{$tmp_dir}/component-{$file}");
            $cmd_line = "cd {$tmp_component_dir} && unzip -o $safe_component_file";
            system($cmd_line);
            
            // determine component directory/file name
            // if the file name length is over 40, we assume it came from gitlab or github
            // so we check, and get rid of words like master and the 40 char len string
            // so that the component has a sane name
            $component_dir = system("ls -1 {$tmp_component_dir}/");
            log_debug(LOG_TYPE_SYSTEM, "INSTALL_COMPONENT: component_dir: {$component_dir}");
            if (strlen($component_dir) > MAGIC_NUMBER_LENGTH_TO_DETERMINE_ZIP_CAME_FROM_GITLAB) {
                $a = explode('-', $component_dir);
                if (strlen(end($a)) == MAGIC_NUMBER_LENGTH_TO_DETERMINE_ZIP_CAME_FROM_GITLAB) {
                    $i = count($a);
                    unset($a[$i-1]);
                    unset($a[$i-2]);
                    $component_name = implode('-', $a);
                }
            } else {
                $component_name = $component_dir;
            }
            log_debug(LOG_TYPE_SYSTEM, "INSTALL_COMPONENT: component_name: {$component_name}");

            // make sure this is at least trying to be a component
            $full_component_path = escapeshellarg("{$tmp_component_dir}/{$component_dir}/{$component_name}.inc.php");
            $cmd_line = "grep -q register_component $full_component_path";
			log_debug(LOG_TYPE_SYSTEM, "INSTALL_COMPONENT: cmd_line: {$cmd_line}");
            system($cmd_line, $rc);
            if ($rc != 0) {

            	system("rm -rf {$tmp_component_dir}");
            	$result = _('Uploaded file is not a component.');
            	log_debug(LOG_TYPE_SYSTEM, "INSTALL_COMPONENT: {$result}");
            	return COMMAND_RESULT_ERROR;
            }
            
            // make new component directory (might exist already)
            @mkdir("{$base_dir}/includes/components/{$component_name}");
            
            // move component to production directory and delete temp directory
            // and added permissions fix to make sure all new components are executable
            $cmd_line = 
            	". {$root_dir}/var/fusion-sys.cfg " .
            	"&& chmod -R 755 {$tmp_component_dir} " .
            	"&& chown -R \$nagiosuser:\$nagiosgroup {$tmp_component_dir} " . 
            	"&& cp -rf {$tmp_component_dir}/{$component_dir}/* {$base_dir}/includes/components/{$component_name} " .
            	"&& rm -rf {$tmp_component_dir}";

            // we run this here so that we can use our sanity tester
            system($cmd_line);

            // now we check to make sure everything is fine via our sanity test
            // so that we can roll back immediately if not
            $cmd_line = "cd {$scripts_dir} && ./sanity_test.php";
            system($cmd_line, $rc);
            if ($rc != 0) {

                system("rm -rf {$base_dir}/includes/components/{$component_name}");
                $result = _("It looks like this component wasn't built for this version of Fusion.");
                log_debug(LOG_TYPE_SYSTEM, "INSTALL_COMPONENT: {$result}");
                return COMMAND_RESULT_ERROR;
            }

            // empty out the cmd line
            $cmd_line = '';

            $post_func = 'install_component';
            $post_func_args = array(
                'component_name' => $component_name,
                'component_dir' => "{$base_dir}/includes/components/{$component_name}",
            );
			break;

		case COMMAND_DELETE_COMPONENT:
            $name = $data;

            // cant delete blank data, or if there is no component directory specified
            if (empty($name) || empty($components[$name]))
                return COMMAND_RESULT_ERROR;
            if (empty($components[$name][COMPONENT_DIRECTORY]))
                return COMMAND_RESULT_ERROR;

            // can't delete core components!
            if (!empty($components[$name][COMPONENT_TYPE]) && $components[$name][COMPONENT_TYPE] == COMPONENT_TYPE_CORE)
            	return COMMAND_RESULT_ERROR;

            $dir = sanitize_filename($components[$name][COMPONENT_DIRECTORY]);
            $safe_path = escapeshellarg("{$base_dir}/includes/components/{$dir}");
            $cmd_line = "rm -rf $safe_path";
            break;

        case COMMAND_PACKAGE_COMPONENT:
            $dir = sanitize_filename($data);

            if (empty($dir))
                return COMMAND_RESULT_ERROR;

            $safe_zip_path = escapeshellarg("{$tmp_dir}/component-{$dir}.zip");
            $safe_dir = escapeshellarg($dir);
            
            $cmd_line = "cd {$base_dir}/includes/components && zip -r $safe_zip_path $safe_dir";
        	break;

        case COMMAND_INSTALL_DASHLET:
            $file = sanitize_filename($data);

            if (empty($file))
                return COMMAND_RESULT_ERROR;

            // create a tmp dir for holding the dashlet while we check it
            $tmp_dashlet_dir = $tmp_dir . '/' . random_string(5);
            log_debug(LOG_TYPE_SYSTEM, "INSTALL_DASHLET: tmp_dashlet_dir: {$tmp_dashlet_dir}");

            system("rm -rf {$tmp_dashlet_dir}");
            mkdir($tmp_dashlet_dir);
            $cmd_line = "cd {$tmp_dashlet_dir} && unzip -o {$tmp_dir}/dashlet-{$file}";
            system($cmd_line);
            
            // determine dashlet directory/file name
            // if the file name length is over 40, we assume it came from gitlab or github
            // so we check, and get rid of words like master and the 40 char len string
            // so that the dashlet has a sane name
            $dashlet_dir = system("ls -1 {$tmp_dashlet_dir}/");
            log_debug(LOG_TYPE_SYSTEM, "INSTALL_DASHLET: dashlet_dir: {$dashlet_dir}");
            if (strlen($dashlet_dir) > MAGIC_NUMBER_LENGTH_TO_DETERMINE_ZIP_CAME_FROM_GITLAB) {
                $a = explode('-', $dashlet_dir);
                if (strlen(end($a)) == MAGIC_NUMBER_LENGTH_TO_DETERMINE_ZIP_CAME_FROM_GITLAB) {
                    $i = count($a);
                    unset($a[$i-1]);
                    unset($a[$i-2]);
                    $dashlet_name = implode('-', $a);
                }
            } else {
                $dashlet_name = $dashlet_dir;
            }
            log_debug(LOG_TYPE_SYSTEM, "INSTALL_DASHLET: dashlet_name: {$dashlet_name}");

            // make sure this is at least trying to be a dashlet
            $cmd_line = 
                "grep -q register_dashlet {$tmp_dashlet_dir}/{$dashlet_dir}/{$dashlet_name}.inc.php" .
                "&& grep -q DASHLET_LOAD_URL {$tmp_dashlet_dir}/{$dashlet_dir}/{$dashlet_name}.inc.php";
            log_debug(LOG_TYPE_SYSTEM, "INSTALL_DASHLET: cmd_line: {$cmd_line}");
            system($cmd_line, $rc);
            if ($rc != 0) {

                system("rm -rf {$tmp_dashlet_dir}");
                $result = _('Uploaded file is not a dashlet.');
                log_debug(LOG_TYPE_SYSTEM, "INSTALL_DASHLET: {$result}");
                return COMMAND_RESULT_ERROR;
            }
            
            // make new dashlet directory (might exist already)
            @mkdir("{$base_dir}/includes/dashlets/{$dashlet_name}");
            
            // move dashlet to production directory and delete temp directory
            // and added permissions fix to make sure all new dashlets are executable
            $cmd_line = 
                ". {$root_dir}/var/fusion-sys.cfg " .
                "&& chmod -R 755 {$tmp_dashlet_dir} " .
                "&& chown -R \$nagiosuser:\$nagiosgroup {$tmp_dashlet_dir} " . 
                "&& cp -rf {$tmp_dashlet_dir}/{$dashlet_dir}/* {$base_dir}/includes/dashlets/{$dashlet_name} " .
                "&& rm -rf {$tmp_dashlet_dir}";

            // we run this here so that we can use our sanity tester(s)
            system($cmd_line);

            // now we check to make sure everything is fine via our sanity test
            // so that we can roll back immediately if not
            $cmd_line = "cd {$scripts_dir} && ./sanity_test.php";
            system($cmd_line, $rc);
            if ($rc != 0) {

                system("rm -rf {$base_dir}/includes/dashlets/{$dashlet_name}");
                $result = _("It looks like this dashlet wasn't built for this version of Fusion.");
                log_debug(LOG_TYPE_SYSTEM, "INSTALL_DASHLET: {$result}");
                return COMMAND_RESULT_ERROR;
            }

            // empty out the cmd line
            $cmd_line = '';

            break;

        case COMMAND_DELETE_DASHLET:
            $name = $data;

            // cant delete blank data, or if there is no dashlet directory specified
            if (empty($name) || empty($dashlets[$name]))
                return COMMAND_RESULT_ERROR;
            if (empty($dashlets[$name][DASHLET_DIRECTORY]))
                return COMMAND_RESULT_ERROR;

            // can't delete core dashlets!
            if (!empty($dashlets[$name][DASHLET_TYPE]) && $dashlets[$name][DASHLET_TYPE] == DASHLET_TYPE_CORE)
                return COMMAND_RESULT_ERROR;

            $dir = sanitize_filename($dashlets[$name][DASHLET_DIRECTORY]);
            $safe_dir = escapeshellarg("{$base_dir}/includes/dashlets/{$dir}");
            $cmd_line = "rm -rf $safe_dir";
            break;

        case COMMAND_PACKAGE_DASHLET:
            $dir = sanitize_filename($data);

            if (empty($dir))
                return COMMAND_RESULT_ERROR;
            $safe_zip_path = escapeshellarg("{$tmp_dir}/dashlet-{$dir}.zip");
            $safe_dir = escapeshellarg($dir);
            $cmd_line = "cd {$base_dir}/includes/dashlets && zip -r $safe_zip_path $safe_dir";
            break;

        case COMMAND_RESTART_HTTPD:
            $cmd_line = "sudo {$root_dir}/scripts/manage_services.sh restart httpd";
            break;

        // Upgrade from GUI
        case COMMAND_UPGRADE_TO_LATEST:
            $restarted_mysql = true;
            $tmpdir = get_tmp_dir();
            file_put_contents($tmpdir."/upgrade.log", "");

            // Download latest version of Fusion
            $file = "https://assets.nagios.com/downloads/nagiosfusion/fusion-latest.tar.gz";
            if (is_dev_mode()) {
                $file = "https://assets.nagios.com/downloads/nagiosfusion/revision/get_latest_rev.php";
            }

            $proxy = (bool) get_option('use_proxy', 0);
            $options = array(
                'return_info' => true,
                'method' => 'get',
                'timeout' => 300,
                'verifypeer' => true,
            );

            // Fetch the url
            $res = load_url($file, $options, $proxy);
            if (empty($res["body"])) {
                return COMMAND_RESULT_ERROR;
            }

            // Download the new latest version (or dev version)
            if (file_exists($tmpdir."/fusion-latest.tar.gz")) {
                unlink($tmpdir."/fusion-latest.tar.gz");
            }

            file_put_contents($tmpdir."/fusion-latest.tar.gz", $res["body"]);

            $cmd_line = "sudo {$root_dir}/scripts/upgrade_to_latest.sh";
            break;

		default:
			log_error(LOG_TYPE_SYSTEM, "INVALID COMMAND!");
			return;
	}

/////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////

	// if we're running a script, generate the cmd_line
	if (!empty($script_name)) {

		if (empty($script_data))
			$script_data = ' ';

		$cmd_line = rtrim("cd {$scripts_dir} && ./{$script_name} {$script_data}");
	}

	// attempt to execute the command line
	if (!empty($cmd_line)) {

		// we want to collect error output as well
		$cmd_line .= ' 2>&1';

		// output some information regarding the cmd_line
		log_info(LOG_TYPE_SYSTEM, "Command line: {$cmd_line}");

		exec($cmd_line, $output, $result_code);
		$result = implode(" ", $output);
	} else {

        $result_code = 0;
        $result = '[Empty]';
    }

	// if we restarted mysql
	if ($restarted_mysql) {

		// we need to give this for sure enough time before we try and setup the database again
		// 2 was too low
		sleep(4);
		setup_database();
	}

	log_info(LOG_TYPE_SYSTEM, "Result code: {$result_code}");
	log_info(LOG_TYPE_SYSTEM, "Result: {$result}");

	// run the post function call
	if ($result_code == 0 && !empty($post_func) && function_exists($post_func)) {

		log_info(LOG_TYPE_SYSTEM, "Running post function call {$post_func}");
		log_debug(LOG_TYPE_SYSTEM, "postfunc args " . one_line_print_r($post_func_args));

		$result_code = $post_func($post_func_args);
		log_info(LOG_TYPE_SYSTEM, "postfunc Result code: {$result_code}");
	}

	// TODO: callbacks
}
