<?php
//
// Config Wizard Helper Functions
// Copyright (c) 2008-2018 Nagios Enterprises, LLC and others.
//

if (!defined('SUBSYSTEM')) {
	require_once(dirname(__FILE__) . '/../common.inc.php');
}

/**
 * Builds a hidden overlay div for selecting hosts or services. 
 * Show the overlay with show_overlay() as can be seen in the eventhandler notify wizards.
 *
 * @param string $type        nagios object type (host, service, command, etc)
 * @param string $nodelist    json encoded list of hosts or services (use get_xml_host_objects() or get_xml_service_objects())
 * @return string returns populated overlay html to select lists for the $type object
 */
function construct_overlay($type, $nodelist) {
    global $unique;

    if($type == 'host') {
        $display_display_name = grab_request_var("display_host_display_name", get_user_meta(0, "display_host_display_name"));
    } else if($type == 'service') {
        $display_display_name = grab_request_var("display_service_display_name", get_user_meta(0, "display_service_display_name"));
    }

    $Title = ucfirst($type);
    $Titles = ucfirst($type).'s';
    
    $html = "<div class='overlay wiz-overlay' id='{$type}Box'>

    <div class='overlay-title'>
        <h2>{$Titles}</h2>
        <div class='overlay-close ccm-tt-bind' data-placement='left' title='"._('Close')."' onclick='killOverlay(\"{$type}Box\")'><i class='fa fa-times'></i></div>
        <div class='clear'></div>
    </div>

    <div class='left'>
        <div class='listDiv'>
            <div class='filter'>
                <span class='clear-filter ccm-tt-bind' title='"._('Clear')."'><i class='fa fa-times fa-14'></i></span>
                <input type='text' id='filter{$Titles}' class='form-control fc-fl' style='border-bottom: 0;' placeholder='"._('Filter')."...'>
            </div>
            <select name='sel{$Titles}[]' class='form-control fc-m lists' multiple='multiple' id='sel{$Titles}' ondblclick='transferMembers(\"sel{$Titles}\", \"tbl{$Titles}\", \"{$type}s\")'>
                <!-- option value is tbl ID -->
    ";

    $nodelist = json_decode(json_encode($nodelist), true);
    if (key_exists("recordcount", $nodelist) && $nodelist["recordcount"] > 1) {
        $nodelist = $nodelist[$type];
    } else {
        $nodelist = array($nodelist[$type]);
    }
    $unique++;

    foreach($nodelist as $key => $node){

        if($type == 'host'){
            $id = $node['@attributes']['id'];
            $host_name = $node['host_name'];
            if($display_display_name) {
                $display_name = $node['display_name'];
            } else {
                $display_name = $node['alias'];
            }

            $disabled = '';
            if(!$node['is_active']){
                $disabled = 'disabled';
            }

            $node = json_encode($node);
            $html .= "<option id='".$unique++."' name='{$host_name}' value='{$node}' title='{$host_name}' ".$disabled.">{$display_name}</option>";
        } else if($type == 'service'){
            $id = $node['@attributes']['id'];
            $host_name = $node['host_name'];
            if($display_display_name) {
                $display_name = $node['display_name'];
            } else {
                $display_name = $node['service_description'];
            }

            $disabled = '';
            if(!$node['is_active']){
                $disabled = 'disabled';
            }

            $node = json_encode($node);
            $html .= "<option id='".$unique++."' name='{$host_name} - {$display_name}' value='{$node}' title='{$host_name}' ".$disabled.">{$host_name} - {$display_name}</option>";
        }

    }
    $html .= "  </select>
                <div class='overlay-left-bottom'>
                    <button type='button' class='btn btn-sm btn-primary fl' onclick=\"transferMembers('sel{$Titles}', 'tbl{$Titles}', '{$type}s')\">"._("Add Selected")." <i class='fa fa-chevron-right'></i></button>
                    <div class='clear'></div>
                </div>
            <div class='closeOverlay'>
                <button type='button' class='btn btn-sm btn-default' onclick='killOverlay(\"{$type}Box\")'>"._("Close")."</button>
            </div>
        </div>
    </div>
    <!-- end leftBox -->

    <div class='right'>
        <div class='right-container'>
            <table class='table table-no-margin table-small-border-bottom table-x-condensed'>
                <thead>
                    <tr>
                        <th colspan='2'>
                            <span class='thMember'>"._("Assigned")."</span>
                            <a class='fr' title='Remove All' href='javascript:void(0)' onclick=\"removeAll('tbl{$Titles}')\">"._("Remove All")."</a>
                            <div class='clear'></div>
                        </th>
                    </tr>
                </thead>
            </table>
            <div class='assigned-container lists'>
                <table class='table table-x-condensed table-hover table-assigned' id='tbl{$Titles}'>
                    <tbody>
                        <!-- insert selected items here -->
                    </tbody>
                </table>
            </div>
        </div>
    </div>

    <!-- $type radio buttons -->

    </div> 

    <script type=\"text/javascript\" src=\"../includes/components/ccm/javascript/form_js.js?".get_build_id()."\"></script>

    <!-- end {$type}box --> ";

    return $html;

}

/**
 * Summary of wizard_input_text
 * @param string $name                   The name of the input. Will be used as id as well
 * @param string $label                  The label of the input
 * @param string $value                  The default value of the input
 * @param string $tooltip                The text for the tooltip
 * @param string $required_feedback      The feedback message when nothing is entered. Providing a value here means
 *                                      the field is now required
 * @param boolean $disabled               Whether or not the text box is disabled
 * @return bool|string
 */

function wizard_input_text($name, $label, $value = "", $tooltip = "", $required_feedback = "", $disabled = false) {
    ob_start();
?>
    <div class="row mb-2">
        <div class="col-sm-6">
            <label for="<?= $name ?>" class="form-label <?php if(!empty($required_feedback)) echo "form-item-required"?>"><?= $label ?> <?php if (!empty($tooltip)) echo xi6_info_tooltip($tooltip); ?></label>
            <div class="input-group position-relative">
                <input type="text" class="form-control monitor rounded" id="<?= $name ?>" name="<?= $name ?>" value="<?= encode_form_val($value); ?>" <?php if(!empty($required_feedback)) echo "required"?> <?php if($disabled) echo "disabled"?>>
<?php
                if(!empty($required_feedback)) {
?>
                    <div class="invalid-feedback">
                        <?= $required_feedback ?>
                    </div>
                    <i id="<?= $name ?>_Alert" class="visually-hidden position-absolute top-0 start-100 translate-middle icon icon-circle color-ok icon-size-status"></i>
<?php
                }
?>
            </div>
        </div>
    </div>
<?php
    return ob_get_clean();
}

/**
 * Summary of wizard_select
 * @param string $name                   The name of the select. Will be used as id as well
 * @param string $label                  The label of the select
 * @param array $options                 The options to use. Option Display pairs, the option is used as the value, and the display is used in the display
 *                                       $option => $display
 * @param string $value                  The default value of the select
 * @param string $tooltip                The text for the tooltip
 * @param string $required_feedback      The feedback message when nothing is entered. Providing a value here means
 *                                       the field is now required
 * @return bool|string
 */
function wizard_select($name, $label, $options, $value = '', $tooltip = '', $required_feedback = '') {
    ob_start();
?>
    <div class="row mb-2">
        <div class="col-sm-6">
            <label for="<?= $name ?>" class="form-label <?php if(!empty($required_feedback)) echo "form-item-required"?>"><?= $label ?> <?php if(!empty($tooltip)) echo xi6_info_tooltip($tooltip) ?></label>
            <div class="input-group position-relative">
                <select name="<?= $name ?>" id="<?= $name ?>" class="form-select form-select-sm form-control-sm monitor rounded" placeholder="<?= _("Select Host Name") ?>" <?php if(!empty($required_feedback)) echo "required" ?>>
                    <option value=null></option>
<?php

                    foreach ($options as $option => $display) {
?>
                        <option value="<?= $option ?>" <?= is_selected($option, $value) ?>><?= $display ?></option>
<?php
                    }
?>
                </select>
<?php
                if(!empty($required_feedback)) {
?>
                    <div class="invalid-feedback">
                        <?= $required_feedback ?>
                    </div>
                    <i id="<?= $name ?>_Alert" class="visually-hidden position-absolute top-0 start-100 translate-middle icon icon-circle color-ok icon-size-status"></i>
<?php
                }
?>
            </div>
        </div>
    </div>
<?php
    return ob_get_clean();
}

class NCPAConnectionException extends Exception {};
class NCPAEmptyDataException extends Exception {};
class NCPAInvalidDataException extends Exception {};
class NCPAErrorException extends Exception {};

/**
 * Send a request to the NCPA API and get the returned JSON as an array
 *
 * @param   string  $endpoint       API endpoint
 * @param   string  $api_url        API URL
 * @param   string  $token          Security token
 * @param   boolean $no_ssl_verify  If true, do not verify SSL certificate
 * @param   array   $params         Optional HTTP parameters passed via the URL
 *
 * @return  array
 */
function get_data_from_ncpa_api($endpoint, $api_url, $token, $no_ssl_verify, $params = array())
{
    $exceptions = [
        E_ERROR => "E_ERROR",
        E_WARNING => "E_WARNING",
        E_PARSE => "E_PARSE",
        E_NOTICE => "E_NOTICE",
        E_CORE_ERROR => "E_CORE_ERROR",
        E_CORE_WARNING => "E_CORE_WARNING",
        E_COMPILE_ERROR => "E_COMPILE_ERROR",
        E_COMPILE_WARNING => "E_COMPILE_WARNING",
        E_USER_ERROR => "E_USER_ERROR",
        E_USER_WARNING => "E_USER_WARNING",
        E_USER_NOTICE => "E_USER_NOTICE",
        E_STRICT => "E_STRICT",
        E_RECOVERABLE_ERROR => "E_RECOVERABLE_ERROR",
        E_DEPRECATED => "E_DEPRECATED",
        E_USER_DEPRECATED => "E_USER_DEPRECATED",
        E_ALL => "E_ALL"
    ];

    #
    # Setup
    #

    # Remove SSL verification, if appropriate
    $context = array("ssl" => array("verify_peer" => true, "verify_peer_name" => true));

    if ($no_ssl_verify) {
        $context['ssl']['verify_peer'] = false;
        $context['ssl']['verify_peer_name'] = false;
    }

    # Setup API url
    $params['token'] = $token;
    $full_api_url = $api_url."/".$endpoint."?".http_build_query($params);

    # Setup API url for error messages.  This will be displayed, so hide the token, for "security"
    $safe_url = str_replace('token='.urlencode($token), "token=&lt;your_token&gt;", $full_api_url);

    #
    # Connect and get data.
    #

    # Use the error control operator '@' and error_get_last() to catch and handle errors.
    # TODO: If this does not do enough, may need to use set_error_handler, etc.
    $data = @file_get_contents($full_api_url, false, stream_context_create($context));

    #
    # Check for errors and invalid data conditions.
    #

    if ($data === false) {
        $error = error_get_last();  // TODO: Is this thread safe?  PHP may not be multi-threaded, but Apache is.
        error_clear_last();         // Seems like a good idea.  May not be necessary.

        #
        # Clean up the message for display as an error on the page.
        #

        # Hide the token
        $safe_message = str_replace('token='.urlencode($token), "token=&lt;your_token&gt;", $error['message']);

        # Strip "file_get_contents()"
        $pattern = '/file_get_contents\((.*)\)(.*)/i';
        $safe_message = preg_replace($pattern, '$1$2', $safe_message);

        # Add line feeds for readability
        $safe_message = str_replace(': ', "\r", $safe_message);

        # Final error message
        $error_msg = _("Connection timed out. Failure connecting to NCPA")." &nbsp; [GDFNA:001]<pre class=\"errmsg\">{$exceptions[$error['type']]}: {$safe_message}</pre>";

        throw new NCPAConnectionException($error_msg);

    } else if (empty($data)) {
        $error_msg = _("Connection timed out. No data returned from NCPA")." &nbsp; [GDFNA:002]";
        throw new NCPAEmptyDataException($error_msg);
    } else if (!is_json($data)) {
        $error_msg = _("Invalid data format from NCPA")." &nbsp; [GDFNA:003]<pre class=\"errmsg\">ERROR: [{$safe_url}]\r".var_export($data, true)."</pre>";
        throw new NCPAInvalidDataException($error_msg);
    } else if (is_json($data)) {
        // Check for error
        $error = json_decode($data, true);

        if (array_key_exists('error', $error)) {
            $error_msg = _("Error from NCPA")." &nbsp; [GDFNA:004]<pre class=\"errmsg\">ERROR: {$safe_url}\r{$error['error']}</pre>";
            throw new NCPAErrorException($error_msg);
        }
    }

    #
    # Received valid data.  Do a bit of final formatting and return.
    #

    $data = json_decode($data, true);

    if (is_array($data) && array_key_exists('value', $data)) {
        $data = array_pop($data['value']);
    } elseif (is_array($data)) {
        $data = array_pop($data);
    }

    return $data;
}

/*
 * Test for properly formatted json data.
 */
function is_json($string) {
    json_decode($string);
    return json_last_error() === JSON_ERROR_NONE;
}

/**
 * Escapes password/token values for use in check command line
 * Specifically escapes "!" characters to prevent breaking in the CCM
 * wraps in double quotes to support escaping while preventing command injection
 *
 * @param string $password          password string provided by user in a wizard form
 * @return string $escaped          returns escaped password string wrapped in quotes
 */
function escape_password($password) {
    $escaped = str_replace('!', '\!', $password);

    return "'" . $escaped . "'";
}

/**
 * Invalidate host-specific keys in a preserved $services array when the target
 * NCPA host credentials have changed (e.g., user navigated back to stage 1 and
 * entered a different address/port/token). Keeps generic thresholds intact.
 *
 * @param array        $services      The preserved $services array (by reference).
 * @param array|null   $inargs        Current wizard $inargs with ip_address/port/token.
 * @param array        $host_keys     Subkeys of $services to clear on mismatch
 *                                    (e.g. ['disk','interface','services','process','processes','plugins']).
 * @return bool                       True if the fingerprint mismatched and keys were cleared.
 */
function configwizard_ncpa_invalidate_host_keys(&$services, $inargs, $host_keys) {
    if (!is_array($services)) {
        $services = array();
    }
    $current = array(
        'ip_address' => is_array($inargs) ? (string) grab_array_var($inargs, 'ip_address', '') : '',
        'port'       => is_array($inargs) ? (string) grab_array_var($inargs, 'port', '') : '',
        'token'      => is_array($inargs) ? (string) grab_array_var($inargs, 'token', '') : '',
    );
    $stored = (isset($services['_host_fingerprint']) && is_array($services['_host_fingerprint']))
        ? $services['_host_fingerprint'] : null;

    $changed = false;
    if ($stored === null) {
        // First time through; just record fingerprint, don't clear.
    } elseif ($stored !== $current) {
        foreach ($host_keys as $k) {
            unset($services[$k]);
        }
        $changed = true;
    }
    $services['_host_fingerprint'] = $current;
    return $changed;
}