<?php

/*
 * VNagios - Nagios Framework for PHP
 * Developed by Daniel Marschall, ViaThinkSoft <www.viathinksoft.com>
 * Licensed under the terms of GPLv3
 */

define('VNAGIOS_VERSION''2014-02-16 WIP');

/****************************************************************************************************

    VNagios is a small framework for Nagios Plugin Developers who use PHP CLI scripts.
    The main purpose of VNagios is to make the development of plugins as easy as possible, so that
    the developer can concentrate on the actual work. VNagios will try to automate as much
    as possible.

    Please note that your script should include the +x chmod flag:
        chmod +x myscript.php

    A Nagios plugin should begin with following header:

        #!/usr/bin/php
        <?php

        declare(ticks=1);
        require_once __DIR__ . '/nagios-framework.inc.php';

        $vnag = new VNagios();

        // It is recommended that you define these values, so that the basic help functionalities
        // -V/--version, -? (usage), and -h/--help, which are already implemented by VNagios
        // will show accurate information about your plugin.
        // If you are only writing a small internal quick-n-dity plugin, you can leave this part away.
        $vnag->getHelpManager()->setPluginName('foobar_plugin');
        $vnag->getHelpManager()->setVersion('1.0');
        $vnag->getHelpManager()->setShortDescription('This plugin checks the status of foobar.');
        $vnag->getHelpManager()->setCopyright('Copyright (C) 2011-$CURYEAR$ ACME Inc.'); # $CURYEAR$ will be replaced by the current year
        $vnag->getHelpManager()->setSyntax('$SCRIPTNAME$ -w <range> -c <range> [-h <host>] [-s|--silent]'); // $SCRIPTNAME$ will be replaced by the actual script name
        $vnag->getHelpManager()->setFootNotes('.......'); // list and explain each option, and can output examples as well as foot notes (contact information etc.)

        // Please define all arguments you are expecting.
        // This part will ensure that --help will list the parameters your plugin is expecting,
        // and in future versions of VNagios, it might be necessary that VNagios knows all valid arguments (see note below).
        // VNagios has already inserted the argument which it does implement itself.
        # In this example, we create an two arguments -s|--silent and -H|--host .
        $vnag->addExpectedArgument($argSilent = new VNagiosArgument('s', 'silent', VNagiosArgumentHandler::VALUE_FORBIDDEN, 'Description for the --help output'));
        $vnag->addExpectedArgument($argHost   = new VNagiosArgument('H', 'host',   VNagiosArgumentHandler::VALUE_REQUIRED,  'Description for the --help output'));

        $vnag->run();

        // <now you can finally begin to insert your code!>

    In the example above, the two argument objects $argSilent and $argHost were created.
    With these objects of the type VNagiosArgument, you can query the argument's value,
    how often the argument was passed and if it is set:

        $argSilent->count();      // 1 if "-s" is passed, 2 if "-s -s" is passed etc.
        $argSilent->available();  // true if "-s" is passed, false otherwise
        $argHost->getValue();     // "example.com" if "-h example.com" is passed

    It is recommended that you pass every argument to $vnag->addExpectedArgument() .
    Using this way, VNagios can generate a --help page for you, which lists all your arguments.
    Future version of VNagios may also require to have a complete list of all valid arguments,
    since the Nagios Development Guidelines recommend to output the usage information if an illegal
    argument is passed. Due to PHP's horrible bad implementation of GNU's getopt(), this check for
    unknown arguments is currently not possible, and the developer of VNagios does not want to use
    dirty hacks/workarounds, which would not match to all argument notation variations/styles.
    See: https://bugs.php.net/bug.php?id=68806
         https://bugs.php.net/bug.php?id=65673
         https://bugs.php.net/bug.php?id=26818

# TODO: also arguments after "--"



    VNagios will prevent that the script is executed using a web service like Apache,
    but it is recommended that you do not put the script inside the docroot of your web service.
# TODO: vnagios over apache

    You can set the status with:
        $vnag->setStatus(VNagios::STATUS_OK);
    If you don't set a status, the script will return Unknown instead.
    setStatus($status) will keep the most severe status, e.g.
        $vnag->setStatus(VNagios::STATUS_CRITICAL);
        $vnag->setStatus(VNagios::STATUS_OK);
    will result in a status "Critical".
    If you want to completely overwrite the status, use $force=true:
        $vnag->setStatus(VNagios::STATUS_CRITICAL);
        $vnag->setStatus(VNagios::STATUS_OK, true);
    The status will now be "OK".

    Possible status codes are:
        (For service plugins:)
        VNagios::STATUS_OK       = 0;
        VNagios::STATUS_WARN     = 1;
        VNagios::STATUS_CRITICAL = 2;
        VNagios::STATUS_UNKNOWN  = 3;

        (For host plugins:)
        VNagios::STATUS_UP       = 0;
        VNagios::STATUS_DOWN     = 1;

    As soon as $vnag gets freed (or the script terminates),
    the output of the plugin will be presented in the Nagios output format.

    The output format will be:
        <Service status text>: <Comma separates messages> | <whitespace separated primary performance data>
            "Verbose information:"
        <Multiline verbose output> | <Multiline secondary performance data>

    "Service status text" will be automatically created by VNagios.

    Verbose information are printed below the first line. Most Nagios clients will only print the first line.
    If you have important output, use $vnag->setMessage() instead.
    You can add verbose information with following method:
        $vnag->addVerboseMessage('foobar', $verbosity);

    Following verbosity levels are defined:
        VNagios::VERBOSITY_SUMMARY                = 0; // always printed
        VNagios::VERBOSITY_ADDITIONAL_INFORMATION = 1; // requires at least -v
        VNagios::VERBOSITY_CONFIGURATION_DEBUG    = 2; // requiers at least -vv
        VNagios::VERBOSITY_PLUGIN_DEBUG           = 3; // requiers at least -vvv

    All STDOUT outputs of your script (e.g. by echo) will be interpreted as "verbose" output
    and is automatically collected, so
        echo "foobar";
    has the same functionality as
        $vnag->addVerboseMessage('foobar', VNagios::VERBOSITY_SUMMARY);

    You can set messages (which will be added into the first line, which is preferred for plugin outputs)
    using
        $vnag->setMessage($msg, $append, $verbosity);
    Using the flag $append, you can choose if you want to append or replace the message.

    VNagios will catch Exceptions of your script and will automatically end the plugin,
    return a valid Nagios output.
  # TODO: ERROR or CRITICAL?

    VNagios will automatic handle of following CLI arguments:
        -?
        -V --version
        -h --help
        -v --verbose
        -t --timeout   (only works if you set declare(ticks=1) at the beginning of your main script)
                    ## TODO: auch in der jeder unit?
        -w --warning
        -c --critical

    You can performe range checking by using:
        $example_value = '10MB';
        $this->checkAgainstWarningRange($example_value);
    this is the same as:
        $example_value = '10MB';
        $wr = $vnag->getWarningRange()
        if (isset($wr) && $wr->checkAlert($example_value)) {
            $vnag->setStatus(VNagios::STATUS_WARNING);
        }

    In case that your script allows ranges which can be relative and absolute, you can provide multiple arguments;
    $wr->checkAlert() will be true, as soon as one of the arguments is in the warning range.
    The check will be done in this way:
        $example_values = array('10MB', '5%');
        $this->checkAgainstWarningRange($example_values);
    this is the same as:
        $example_values = array('10MB', '5%');
        $wr = $vnag->getWarningRange()
        if (isset($wr) && $wr->checkAlert($example_values)) {
            $vnag->setStatus(VNagios::STATUS_WARNING);
        }

    Note that VNagios will automatically detect the UOM (Unit of Measurement) and is also able to convert them,
    e.g. if you use the range "-w 20MB:40MB", your script will be able to use $wr->checkAlert('3000KB')

    Please note that only following UOMs are accepted (as defined in the Plugin Development Guidelines):
    - no unit specified: assume a number (int or float) of things (eg, users, processes, load averages)
    - s, us, ms: seconds
    - %: percentage
    - B, KB, MB, TB, EB, PB: bytes
    - c: a continous counter (such as bytes transmitted on an interface)
# TODO: weitere erlauben?

    # TODO: exit(3) im hauptscript abfangen?

    You can add performance data using
        $vnag->addPerformanceData(new VNagiosPerformanceData(...));
        # TODO: performance data löschen?

    ---

    This framework currently supports meets following guidelines:
    - https://nagios-plugins.org/doc/guidelines.html#PLUGOUTPUT (Plugin Output for Nagios)
    - https://nagios-plugins.org/doc/guidelines.html#AEN33 (Print only one line of text)
    - https://nagios-plugins.org/doc/guidelines.html#AEN41 (Verbose output)
    - https://nagios-plugins.org/doc/guidelines.html#AEN78 (Plugin Return Codes)
    - https://nagios-plugins.org/doc/guidelines.html#THRESHOLDFORMAT (Threshold and ranges)
    - https://nagios-plugins.org/doc/guidelines.html#AEN200 (Performance data)
    - https://nagios-plugins.org/doc/guidelines.html#PLUGOPTIONS (Plugin Options)
    - https://nagios-plugins.org/doc/guidelines.html#AEN302 (Option Processing)
        // TODO: The -? option, or any other unparsable set of options, should print out a short usage statement. Character width should be 80 and less and no more that 23 lines should be printed (it should display cleanly on a dumb terminal in a server room).

    This framework does currently NOT support following guidelines:
    - https://nagios-plugins.org/doc/guidelines.html#AEN74 (Screen Output)
    - https://nagios-plugins.org/doc/guidelines.html#AEN239 (Translations)
    - https://nagios-plugins.org/doc/guidelines.html#AEN293 (Use DEFAULT_SOCKET_TIMEOUT)
    - https://nagios-plugins.org/doc/guidelines.html#AEN296 (Add alarms to network plugins)
    - https://nagios-plugins.org/doc/guidelines.html#AEN245 (Don't execute system commands without specifying their full path)
    - https://nagios-plugins.org/doc/guidelines.html#AEN249 (Use spopen() if external commands must be executed)
    - https://nagios-plugins.org/doc/guidelines.html#AEN253 (Don't make temp files unless absolutely required)
    - https://nagios-plugins.org/doc/guidelines.html#AEN259 (Validate all input)
    - https://nagios-plugins.org/doc/guidelines.html#AEN317 (Plugins with more than one type of threshold, or with threshold ranges)

    We will intentionally NOT follow the following guidelines:
    - https://nagios-plugins.org/doc/guidelines.html#AEN256 (Don't be tricked into following symlinks)
      Reason: We believe that this guideline is contraproductive.
              Nagios plugins usually run as user 'nagios'. It is the task of the system administrator
              to ensure that the user 'nagios' must not read/write to files which are not intended
              for access by the Nagios service. Instead, symlinks are useful for several tasks.
              See also http://stackoverflow.com/questions/27112949/nagios-plugins-why-not-following-symlinks

****************************************************************************************************/

# If you want to use -t/--timeout with your module, you must add following line in your module code:
declare(ticks=1);

# Attention: The -t/--timeout parameter does not respect the built-in set_time_limit() of PHP.
# PHP should set this time limit to infinite.
set_time_limit(0);

# TODO: exception info alles in die titelzeile

class VNagiosException extends Exception {
}

class 
VNagiosTimeoutException extends VNagiosException {
}

class 
VNagiosInvalidArgumentException extends VNagiosException {
}

class 
VNagiosInvalidCLIArgumentException extends VNagiosInvalidArgumentException {
    
// Use this class to signal that the user made an error because of invalid CLI parameters
    // The exception handler will then return UNKNOWN instead of ERROR
    // Please use VNagiosInvalidArgumentException if the input data comes from a differen source
}

class 
VNagiosInvalidRangeException extends VNagiosInvalidArgumentException {
    public function 
__construct($msg) {
        
$e_msg 'Range is invalid';
        if (!empty(
$msg)) $e_msg .= ': '.$msg;
        
super($e_msg);
    }
}

class 
VNagiosInvalidTimeoutException extends VNagiosInvalidArgumentException {
}

class 
VNagiosInvalidShortOpt extends VNagiosInvalidArgumentException {
}

class 
VNagiosInvalidLongOpt extends VNagiosInvalidArgumentException {
}

class 
VNagiosInvalidValuePolicy extends VNagiosInvalidArgumentException {
}

class 
VNagiosInvalidPerformanceDataException extends VNagiosInvalidArgumentException {
    public function 
__construct($msg) {
        
$e_msg 'Performance data invalid';
        if (!empty(
$msg)) $e_msg .= ': '.$msg;
        
super($e_msg);
    }
}

class 
Timeouter {
    
// based on http://stackoverflow.com/questions/7493676/detecting-a-timeout-for-a-block-of-code-in-php

    
private static $start_time false;
    private static 
$timeout;
    private static 
$fired      false;
    private static 
$registered false;

    private function 
__construct() {
    }

    public static function 
start($timeout) {
        if (!
is_numeric($timeout) || ($timeout <= 0)) {
            throw new 
VNagiosInvalidTimeoutException('Timeout value invalid');
        }

        
self::$start_time microtime(true);
        
self::$timeout    = (float) $timeout;
        
self::$fired      false;
        if (!
self::$registered) {
            
self::$registered true;
            
register_tick_function(array('Timeouter''tick'));
        }
    }

    public static function 
end() {
        if (
self::$registered) {
            
unregister_tick_function(array('Timeouter''tick'));
            
self::$registered false;
        }
    }

    public static function 
tick() {
        if ((!
self::$fired) && ((microtime(true) - self::$start_time) > self::$timeout)) {
            
self::$fired true// do not fire again
            
throw new VNagiosTimeoutException('Timeout!');
        }
    }
}

# TODO: multiple longopts ... also entweder string oder array
# may there be multiple shortopts?
class VNagiosArgument {
    const 
VALUE_FORBIDDEN 0;
    const 
VALUE_REQUIRED  1;
    const 
VALUE_OPTIONAL  2;

    protected 
$shortopt;
    protected 
$longopt;
    protected 
$valuePolicy;
    protected 
$helpText;

    public function 
getShortOpt() {
        return 
$this->shortopt;
    }

    public function 
getLongOpt() {
        return 
$this->longopt;
    }

    public function 
getValuePolicy() {
        return 
$this->valuePolicy;
    }

    public function 
getHelpText() {
        return 
$this->helpText;
    }

# TODO: more stuff which could be described for the help
#        - multiple $longops?
#        - how many times may it be defined (e.g. -v -vv -vvv)?
#        - important: description of the value!, e.g. --host=<hostname>
    
public function __construct($shortopt$longopt$valuePolicy$helpText) {
        if (!empty(
$shortopt)) {
            
// Perform 2 checks:
            // 1. Is the shortopt only 1 character long?
            // 2. Does the shortopt contain illegal characters?
            //    http://stackoverflow.com/questions/28522387/which-chars-are-valid-shortopts-for-gnu-getopt
            // We do not filter +, - and ?, since we might need it for other methods, e.g. VNagiosArgumentHandler::_addExpectedArgument
            
if (!preg_match('@^[a-zA-Z0-9\\+\\-\\?]$@'$shortopt$m)) {
                throw new 
VNagiosInvalidShortOpt("Illegal shortopt '-$shortopt'");
            }
        }

        if (!empty(
$longopt)) {
            
// Not sure... (TODO)
            
if (!preg_match('@^[a-zA-Z0-9\\+\\-\\?]+$@'$longopt$m)) {
                throw new 
VNagiosInvalidLongOpt("Illegal longopt '--$longopt'");
            }
        }

        switch (
$valuePolicy) {
            case 
VNagiosArgument::VALUE_FORBIDDEN:
                break;
            case 
VNagiosArgument::VALUE_REQUIRED:
                break;
            case 
VNagiosArgument::VALUE_OPTIONAL:
                break;
            default:
                throw new 
VNagiosInvalidValuePolicy("valuePolicy has illegal value '$valuePolicy'");
        }

        
$this->shortopt    $shortopt;
        
$this->longopt     $longopt;
        
$this->valuePolicy $valuePolicy;
        
$this->helpText    $helpText;

        
# TODO: valuePolicy must be between 0..2 and being int
        
if (strlen($this->shortopt) > 1) {
            throw new 
VNagiosInvalidShortOpt("Illegal shortopt '--$shortopt'");
        }
    }

    protected function 
getOptions() {
        
$policyApx str_repeat(':'$this->valuePolicy);

        
$shortopts $this->shortopt.$policyApx;
        
$longopts  = (empty($this->longopt)) ? array() : array($this->longopt.$policyApx);

        return 
getopt($shortopts$longopts);
    }

    public function 
count() {
        
$options $this->getOptions();

        
$count 0;

        if (isset(
$options[$this->shortopt])) {
            if (
is_array($options[$this->shortopt])) {
                
// e.g. -vvv
                
$count += count($options[$this->shortopt]);
            } else {
                
// e.g. -v
                
$count += 1;
            }
        }

        if (isset(
$options[$this->longopt])) {
            if (
is_array($options[$this->longopt])) {
                
// e.g. --verbose --verbose --verbose
                
$count += count($options[$this->longopt]);
            } else {
                
// e.g. --verbose
                
$count += 1;
            }
        }

        return 
$count;
    }

    public function 
available() {
        
$options $this->getOptions();

        return isset(
$options[$this->shortopt]) || isset($options[$this->longopt]);
    }

    public function 
getValue() {
        
$options $this->getOptions();

        if (isset(
$options[$this->shortopt])) {
            if (
is_array($options[$this->shortopt])) $options[$this->shortopt] = $options[$this->shortopt][0];
            return 
$options[$this->shortopt];
        }

        if (isset(
$options[$this->longopt])) {
            if (
is_array($options[$this->longopt])) $options[$this->longopt] = $options[$this->longopt][0];
            return 
$options[$this->longopt];
        }

        return 
null;
    }
}

class 
VNagiosArgumentHandler {
    protected 
$expectedArgs = array();

    
// Will be called by VNagios via ReflectionMethod (like C++ style friends), because it should not be called manually.
    // Use VNagios's function instead (since it adds to the helpObj too)
    
protected function _addExpectedArgument($argObj) {
        
// -? is always illegal, so it will trigger illegalUsage(). So we don't add it to the list of
        // expected arguments, otherwise illegalUsage() would be true.
        
if ($argObj->getShortOpt() == '?') return false;

        
// GNU extensions with a special meaning
        
if ($argObj->getShortOpt() == '-') return false// cancel parsing
        
if ($argObj->getShortOpt() == '+') return false// enable POSIXLY_CORRECT

        
$this->expectedArgs[] = $argObj;
        return 
true;
    }

    public function 
illegalUsage() {
        
// In this function, we should check if $argv (resp. getopts) contains stuff which is not expected or illegal,
        // so the script can show an usage information and quit the program.

        // WONTFIX: PHP's horrible implementation of GNU's getopt does not allow following intended tasks:
        // - check for illegal values/arguments (e.g. the argument -? which is always illegal)
        // - check for missing values (e.g. -H instead of -H localhost )
        // - check for unexpected arguments (e.g. -x if only -a -b -c are defined in $expectedArgs as expected arguments)
        // - Of course, everything behind "--" may not be evaluated

        // So the only way is to do this stupid hard coded check for '-?'
        // PHP sucks...
        
global $argv;
        return (isset(
$argv[1])) && (($argv[1] == '-?') || ($argv[1] == '/?'));
    }

}

class 
VNagios {
    const 
STATUS_OK       0;
    const 
STATUS_WARN     1;
    const 
STATUS_CRITICAL 2;
    const 
STATUS_UNKNOWN  3;

    
# "Higher-level errors (such as name resolution errors, socket timeouts, etc) are outside of the control of plugins and should generally NOT be reported as UNKNOWN states."
    # -- We choose 4 as exitcode. The developer is free to return any other status.
    # TODO: is this OK?
    
const STATUS_ERROR    4;

    
// The page
    // https://blog.centreon.com/good-practices-how-to-develop-monitoring-plugin-nagios/
    // states, that host plugins may return following status codes:
    // 0=UP, 1=DOWN, Other=Maintains last known state
    
const STATUS_UP       0;
    const 
STATUS_DOWN     1;

    const 
VERBOSITY_SUMMARY                0;
    const 
VERBOSITY_ADDITIONAL_INFORMATION 1;
    const 
VERBOSITY_CONFIGURATION_DEBUG    2;
    const 
VERBOSITY_PLUGIN_DEBUG           3;

    protected 
$statusSet false;
    protected 
$status;
    protected 
$messages;
    protected 
$warning;
    protected 
$critical;
    protected 
$performanceDataObjects = array();
    protected 
$initialized false;

    private 
$helpObj;
    private 
$argHandler;

    public function 
getHelpManager() {
        return 
$this->helpObj;
    }

    public function 
getArgumentHandler() {
        return 
$this->argHandler;
    }

    public function 
addExpectedArgument($argObj) {
        
// Emulate C++ "friend" access to hidden functions

        // $this->helpObj->_addOption($argObj);
        
$helpObjAddEntryMethod = new ReflectionMethod($this->helpObj'_addOption');
        
$helpObjAddEntryMethod->setAccessible(true);
        
$helpObjAddEntryMethod->invoke($this->helpObj$argObj);

        
// $this->argHandler->_addExpectedArgument($argObj);
        
$argHandlerAddEntryMethod = new ReflectionMethod($this->argHandler'_addExpectedArgument');
        
$argHandlerAddEntryMethod->setAccessible(true);
        
$argHandlerAddEntryMethod->invoke($this->argHandler$argObj);
    }

    protected function 
createArgumentHandler() {
        
$this->argHandler = new VNagiosArgumentHandler();

    }

    protected function 
createHelpObject() {
        
$this->helpObj = new VNagiosHelp();
    }

    protected function 
getVerbosityLevel() {
        
$level $this->argVerbosity->count();
        if (
$level 3$level 3;
        return 
$level;
    }

    public function 
getWarningRange() {
        return 
$this->warning;
    }

    public function 
getCriticalRange() {
        return 
$this->critical;
    }

    public function 
checkAgainstWarningRange($value) {
        
$wr $this->getWarningRange();
        if (isset(
$wr) && $wr->checkAlert($example_value)) {
            
$this->setStatus(VNagios::STATUS_WARNING);
        }
    }

    public function 
checkAgainstCriticalRange($value) {
        
$wr $this->getCriticalRange();
        if (isset(
$wr) && $wr->checkAlert($example_value)) {
            
$this->setStatus(VNagios::STATUS_CRITICAL);
        }
    }

    protected 
$argWarning;
    protected 
$argCritical;
    protected 
$argVersion;
    protected 
$argVerbosity;
    protected 
$argTimeout;
    protected 
$argHelp;
    protected 
$argUsage;

    public function 
__construct() {
        
$this->createHelpObject();
        
$this->createArgumentHandler();

        
$this->addExpectedArgument($this->argWarning   = new VNagiosArgument('w''warning',  VNagiosArgument::VALUE_FORBIDDEN'Warning range')); // not every plugin needs it TODO
        
$this->addExpectedArgument($this->argCritical  = new VNagiosArgument('c''critical'VNagiosArgument::VALUE_FORBIDDEN'Critical range')); // not every plugin needs it
        
$this->addExpectedArgument($this->argVersion   = new VNagiosArgument('V''version',  VNagiosArgument::VALUE_FORBIDDEN'Prints version'));
        
$this->addExpectedArgument($this->argVerbosity = new VNagiosArgument('v''verbose',  VNagiosArgument::VALUE_FORBIDDEN'Verbosity -v -vv -vvv')); // TODO: --verbose --verbose ? oder =4 ?
        
$this->addExpectedArgument($this->argTimeout   = new VNagiosArgument('t''timeout',  VNagiosArgument::VALUE_REQUIRED,  'Sets timeout in seconds')); // not every plugin supports it because of declare()
        
$this->addExpectedArgument($this->argHelp      = new VNagiosArgument('h''help',     VNagiosArgument::VALUE_FORBIDDEN'Prints help page'));
        
$this->addExpectedArgument($this->argUsage     = new VNagiosArgument('?''',         VNagiosArgument::VALUE_FORBIDDEN'Prints usage'));
    }

    
# TODO: ATTENTION! WE MAY NOT ALLOW STUFF TO RUN, IF run() was not called before. usually, this stuff should be in the __construct(), be we want to have the help shit
    
public function run() { # TODO ist das gut?
        
if (php_sapi_name() !== 'cli') {
            
# TODO: auch apache zulassen (verschlüsselte remote ausgabe)
            
throw new VNagiosException('VNagios should only be used in a CLI script.');
        }

        if (
$this->argHandler->illegalUsage()) {
            
$this->helpObj->printUsagePage();
            exit(
0); # TODO: we should allow the user to do the same, to output something as non-nagios output
        
}

        if (
$this->argVersion->available()) {
            
$this->helpObj->printVersionPage();
            exit(
0);
        }

        if (
$this->argHelp->available()) {
            
$this->helpObj->printHelpPage();
            exit(
0);
        }

        
ob_start();

        
$this->status      self::STATUS_UNKNOWN;
        
$this->messages    = array();
        
set_exception_handler(array(&$this'_exceptionHandler'));
        
$this->initialized true// to prevent that __destruct() will be called when querying --help only

        
try {
            
$warning $this->argWarning->getValue();
            if (!
is_null($warning)) {
                
$this->warning = new VNagiosRange($warning);
            }

            
$critical $this->argCritical->getValue();
            if (!
is_null($critical)) {
                
$this->critical = new VNagiosRange($critical);
            }

            
$timeout $this->argTimeout->getValue();
            if (!
is_null($timeout)) {
                
Timeouter::start($timeout);
            }
        } catch (
VNagiosInvalidArgumentException $e) {
            throw new 
VNagiosInvalidCLIArgumentException($e->getMessage());
        }
    }

    public function 
addPerformanceData($prefDataObj) { // TODO: ob in erster oder letzter
        
$this->performanceDataObjects[] = $prefDataObj;
    }

    public function 
getPerformanceData() {
        
// see https://nagios-plugins.org/doc/guidelines.html#AEN200
        // 1. space separated list of label/value pairs
        
return $this->performanceDataObjects;
    }

    public function 
__destruct() {
        
// see https://nagios-plugins.org/doc/guidelines.html#AEN33

        
if (!$this->initialized) {
            return;
        } else {
            
$this->initialized false;
        }

        
restore_exception_handler();

        
$verbose ob_get_contents();
        
ob_end_clean();

        
// see https://nagios-plugins.org/doc/guidelines.html#AEN200
        // 1. space separated list of label/value pairs
        
$ary_perfdata $this->getPerformanceData();
        
$performancedata_first array_shift($ary_perfdata);
        
$performancedata_rest  implode(' '$ary_perfdata);

        echo 
trim($this->getMessage());
        if (!empty(
$performancedata_first)) echo '| '.trim($performancedata_first);
        if (!empty(
$verbose)) {
            echo 
"\n\nVerbose information:\n\n".trim($verbose); # TODO: translate
        
}
        if (!empty(
$performancedata_rest)) echo '| '.trim($performancedata_rest);
        echo 
"\n";

        exit(
$this->status);
    }

    protected function 
appendMessage($msg) {
        if (empty(
$msg)) return;
        
$this->messages[] = $msg;
    }

    protected function 
changeMessage($msg) {
        if (empty(
$msg)) {
            
$this->messages = array();
        } else {
            
$this->messages = array($msg);
        }
    }

    public function 
setMessage($msg$append$verbosityLevel=VNagios::VERBOSITY_SUMMARY) {
        if (
self::getVerbosityLevel() < $verbosityLevel$msg '';
        if (
$append) {
            return 
$this->appendMessage($msg);
        } else {
            return 
$this->changeMessage($msg);
        }
    }

    public function 
getStatusText() {
        switch (
$this->status) { # TODO: translate?
            
case self::STATUS_OK:
                return 
'OK';
            case 
self::STATUS_WARN:
                return 
'Warning';
            case 
self::STATUS_CRITICAL:
                return 
'Critical';
            case 
self::STATUS_UNKNOWN:
                return 
'Unknown';
            default:
                return 
'Error';
        }
    }

    public function 
getMessage() {
        
$statText $this->getStatusText();
        
$msgText  implode(', '$this->messages);

        
$ary = array();
        if (!empty(
$statText)) $ary[] = $statText;
        if (!empty(
$msgText))  $ary[] = $msgText;

        return 
implode(': '$ary);
    }

    public function 
addVerboseMessage($msg$verbosityLevel) {
        if (
self::getVerbosityLevel() >= $verbosityLevel) {
            
// will be catched by OB at _shutdownFunction()
            
echo $msg;
        }
    }

    public function 
setStatus($status$force=false) {
        if (!
$this->statusSet) {
            
$this->status    $status;
            
$this->statusSet true;
        } else {
            if ((
$force) || ($this->status $status)) {
                
$this->status $status;
            }
        }
    }

    
# DO NOT CALL MANUALLY
    # Unfortunately, this function has to be public, otherwise set_exception_handler() wouldn't work
    
public function _exceptionHandler($exception) {
        
Timeouter::end();
        if (
is_a($exception'VNagiosInvalidCLIArgumentException')) {
            
$this->setStatus(self::STATUS_UNKNOWN);
            
$this->setMessage('Invalid CLI arguments'false);
        } else {
            
$this->setStatus(self::STATUS_ERROR);
            
$this->setMessage('Unhandled Exception'false);
        }
        
$this->addVerboseMessage($exception->getMessage(), self::VERBOSITY_SUMMARY);
        
$this->__destruct(); // This is required, since the object won't be freed otherwise (and we require the call of __destruct() to send the correct exit code etc.
    
}
}

class 
VNagiosUOMHandler {
    private function 
__construct() {
    }

// TODO: also allow other UOMs?
    
public static function isKnownUOM($uom) {
        
// see https://nagios-plugins.org/doc/guidelines.html#AEN200
        // 10. UOM (unit of measurement) is one of:
        
return (
                
// no unit specified - assume a number (int or float) of things (eg, users, processes, load averages)
                
($uom === '') ||
                
// s - seconds (also us, ms)
                
($uom === 's') || ($uom === 'ms') || ($uom === 'us') ||
                
// % - percentage
                
($uom === '%') ||
                
// B - bytes (also KB, MB, TB)
                // Should we also allow KiB, MiB, etc.? or Ko Mo (octet)?
                
($uom === 'B') || ($uom === 'KB') || ($uom === 'MB') || ($uom === 'TB') || ($uom === 'EB') || ($uom === 'PB') ||
                
// c - a continous counter (such as bytes transmitted on an interface)
                
($uom === 'c')
            );
    }

    public static function 
compare($a$b) {
        
// TODO: -1, 0, 1, false(uoms incompatible)
    
}

}

// TODO: Rang should also accept UOMs, e.g. -w "30%:60%"
# TODO: allow UOM in range
# TODO: mixed UOMs in range
// TODO: attention: how to mix % and MB? in checkAlert() we have to give both information
class VNagiosRange {
    
// see https://nagios-plugins.org/doc/guidelines.html#THRESHOLDFORMAT

    
protected $start;
    protected 
$end;
    protected 
$warnInsideRange;

    public function 
__construct($rangeDef) {
        if (!
preg_match('|(@){0,1}(\d+)(:){0,1}(\d+){0,1}|'$rangeDef$m)) {
            throw new 
VNagiosInvalidRangeException('Syntax error');
        }

        
$this->warnInsideRange $m[1] === '@';

        if (
$m[3] === ':') {
            if (
$m[2] === '~') {
                
$this->start 'inf';
            } else {
                
$this->start $m[2];
            }

            if (empty(
$m[4])) {
                
$this->end 'inf';
            } else {
                
$this->end $m[4];
            }
        } else {
            
assert(empty($m[4]));
            if (empty(
$m[2])) throw new VNagiosInvalidRangeException('End value required');
            
$this->start 0;
            
$this->end   $m[2];
        }

        
// Check if range is valid
        
if (($this->start != 'inf') && (!is_numeric($this->start))) {
            throw new 
VNagiosInvalidRangeException('Invalid start value');
        }
        if ((
$this->end   != 'inf') && (!is_numeric($this->end))) {
            throw new 
VNagiosInvalidRangeException('Invalid end value');
        }
        if ((
$this->start != 'inf') && ($this->end != 'inf') && ($this->start $this->end)) {
            throw new 
VNagiosInvalidRangeException('Start is greater than end value');
        }
    }

    public function 
__tostring() {
        
// Attention:
        // - this function assumes that $start and $end are valid.
        // - not the shortest result will be chosen

        
$ret '';
        if (
$this->warnInsideRange) {
            
$ret '@';
        }

        if (
$this->start === 'inf') {
            
$ret .= '~';
        } else {
            
$ret .= $this->start;
        }

        
$ret .= ':';

        if (
$this->end != 'inf') {
            
$ret .= $this->end;
        }
    }

    public function 
checkAlert($value) {
        if (
$this->warnInsideRange) {
            return ((
$this->start === 'inf') || ($value >= $this->start)) &&
                   ((
$this->end   === 'inf') || ($value <= $this->end  ));
        } else {
            return ((
$this->start !== 'inf') && ($value <  $this->start)) ||
                   ((
$this->end   !== 'inf') && ($value >  $this->end  ));
        }
    }
}

class 
VNagiosPerformanceData {
    
// see https://nagios-plugins.org/doc/guidelines.html#AEN200

    
protected $label;
    protected 
$value;
    protected 
$uom;
    protected 
$warn;
    protected 
$crit;
    protected 
$min;
    protected 
$max;

    public function 
__construct($label$value$uom$warn$crit$min$max$strict=true) {
        
// Not checked resp. nothing to check:
        // 4. label length is arbitrary, but ideally the first 19 characters are unique (due to a limitation in RRD). Be aware of a limitation in the amount of data that NRPE returns to Nagios
        // 6. warn, crit, min or max may be null (for example, if the threshold is not defined or min and max do not apply). Trailing unfilled semicolons can be dropped
        // 9. warn and crit are in the range format (see the Section called Threshold and ranges). Must be the same UOM

        // 2. label can contain any characters except the equals sign or single quote (')
        
if (strpos($label'=') !== false) throw new VNagiosPerformanceDataInvalid('Label may not contain an equal sign');
        
// if (strpos($label, "'") !== false) throw new VNagiosPerformanceDataInvalid('Label may not contain a single quote sign');

        // 5. to specify a quote character, use two single quotes
        
$label str_replace("'""''"$label);

        if (
$strict) {
            
// 7. min and max are not required if UOM=%
            
if (($uom != '%') && (empty($min) || empty($max))) {
                throw new 
VNagiosPerformanceDataInvalid('Min and Max need to be present at performance data if UOM is not %');
            }
        }

        
// 8. value, min and max in class [-0-9.]. Must all be the same UOM. value may be a literal "U" instead, this would indicate that the actual value couldn't be determined
        
if (($value != 'U') && (!preg_match('|^[-0-9\\.]+$|'$value$m))) {
            throw new 
VNagiosPerformanceDataInvalid('Value must be in class [-0-9.] or be \'U\' if the actual value can\'t be determined');
        }
        if ((!empty(
$min)) && (!preg_match('|^[-0-9\\.]+$|'$min$m))) {
            throw new 
VNagiosPerformanceDataInvalid('Min must be in class [-0-9.] or empty');
        }
        if ((!empty(
$max)) && (!preg_match('|^[-0-9\\.]+$|'$max$m))) {
            throw new 
VNagiosPerformanceDataInvalid('Max must be in class [-0-9.] or empty');
        }

        if (
$strict) {
            
// 10. UOM (unit of measurement) is one of ....
            
if (!VNagiosUOMHandler::isKnownUOM($uom)) {
                throw new 
VNagiosPerformanceDataInvalid('UOM (unit of measurement) is not recognized');
            }
        }

        
// TODO: darf warn,crit,min,max den UOM als suffix beinhalten?
        
$this->label $label;
        
$this->value $value;
        
$this->uom   $uom;
        
$this->warn  $warn;
        
$this->crit  $crit;
        
$this->min   $min;
        
$this->max   $max;
    }

    public function 
__tostring() {
        
$label $this->label;
        
$value $this->value;
        
$uom   $this->uom;
        
$warn  $this->warn;
        
$crit  $this->crit;
        
$min   $this->min;
        
$max   $this->max;

        
// 5. to specify a quote character, use two single quotes
        
$label str_replace("''""'"$label);

        
// 'label'=value[UOM];[warn];[crit];[min];[max]
        // 3. the single quotes for the label are optional. Required if spaces are in the label
        
return "'$label'=$value$uom;$warn;$crit;$min;$max";
    }
}

class 
VNagiosHelp {
    
// TODO: enforce screen sized output

    
public function printUsagePage() {
        echo 
trim($this->getUsage())."\n";
    }

    public function 
printVersionPage() {
        echo 
trim($this->getNameAndVersion())."\n";
    }

    public function 
printHelpPage() {
        echo 
trim($this->getNameAndVersion())."\n";
        echo 
trim($this->getCopyright())."\n";
        echo 
"\n";
        echo 
trim($this->getShortDescription())."\n";
        echo 
"\n";
        echo 
"\n";
        echo 
trim($this->getUsage())."\n";
        echo 
"\n";
        foreach (
$this->options as $argObj)  {
            
$this->printArgumentHelp($argObj);
        }
        echo 
"\n";
        
$footNotes $this->getFootNotes();
        if (!empty(
$footNotes)) {
            echo 
trim($footNotes)."\n";
        }
    }

    protected 
/* VNagiosArgument[] */ $options = array();

    
// Will be called by VNagios via ReflectionMethod (like C++ style friends), because it should not be called manually.
    // Use VNagios's function instead (since it adds to the argHandler too)
    
protected function _addOption($argObj) {
        
$this->options[] = $argObj;
    }

# automatic creation of usage page? which argument is necessary?
# TODO: auch value description?
    
protected static function printArgumentHelp($argObj) {
        
$identifiers = array();

        
$shortopt $argObj->getShortopt();
        if (!empty(
$shortopt)) $identifiers[] = '-'.$shortopt;

        
$longopt $argObj->getLongopt();
        if (!empty(
$longopt)) $identifiers[] = '--'.$longopt;

        if (
count($identifiers) == 0) return; // TODO: exception?

        
$arginfo '';
        switch (
$argObj->getValuePolicy()) {
            case 
VNagiosArgument::VALUE_FORBIDDEN:
                
$arginfo ' (no values are permitted)';
                break;
            case 
VNagiosArgument::VALUE_REQUIRED:
                
$arginfo ' (a value is required)';
                break;
            case 
VNagiosArgument::VALUE_OPTIONAL:
                
$arginfo ' (a value is optional)';
                break;
            default:
                
// ?
        
}

        echo 
"  ".implode(', '$identifiers)."$arginfo\n";
        
# TODO: automatischer zeilenumbruch
        
echo "    ".trim($argObj->getHelpText())."\n";
    }

    
// $pluginName should contain the name of the plugin, without version.
    
protected $pluginName;
    public function 
setPluginName($pluginName) {
        
$this->pluginName $this->replaceStuff($pluginName);
    }
    public function 
getPluginName() {
        if (empty(
$this->pluginName)) {
            global 
$argv;
            return 
basename($argv[0]);
        } else {
            return 
$this->pluginName;
        }
    }

    
// $version should contain the version, not the program name or copyright.
    
protected $version;
    public function 
setVersion($version) {
        
$this->version $this->replaceStuff($version);
    }
    public function 
getVersion() {
        return 
$this->version;
    }
    public function 
getNameAndVersion() {
        
$ret $this->getPluginName();
        
$ver $this->getVersion();
        if (!empty(
$ver)) {
            
$ret .= ' version '.$ver;
        }
        
$ret .= ' (powered by VNagios Framework '.VNAGIOS_VERSION.')';
        return 
$ret;
    }

    
// $copyright should contain the copyright only, no program name or version.
    // $CURYEAR$ will be replaced by the current year
    
protected $copyright;
    public function 
setCopyright($copyright) {
        
$this->copyright $this->replaceStuff($copyright);
    }
    public function 
getCopyright() {
        if (empty(
$this->copyright)) {
            
$ret 'The author of this plugin has not defined a copyright.';
        } else {
            
$ret trim($this->copyright);
        }
        
$ret .= "\nVNagios Framework (C) 2014-".max(2015date('Y'))." ViaThinkSoft <info&viathinksoft.de>";
        return 
$ret;
    }

    
// $shortDescription should describe what this plugin does.
    
protected $shortDescription;
    public function 
setShortDescription($shortDescription) {
        
$this->shortDescription $this->replaceStuff($shortDescription);
    }
    public function 
getShortDescription() {
        if (empty(
$this->shortDescription)) {
            return 
'The author of this plugin has not defined a short description of this plugin.';
        } else {
            return 
$this->shortDescription;
        }
    }


# todo: translate everything

    
protected function replaceStuff($text) {
        global 
$argv;
        
$text str_replace('$SCRIPTNAME$'$argv[0], $text);
        
$text str_replace('$CURYEAR$'date('Y'), $text);
    }

    
// $syntax should contain the option syntax only, no explanations.
    // $SCRIPTNAME$ will be replaced by the actual script name TODO
# TODO: automatically generate syntax?
    
protected $syntax;
    public function 
setSyntax($syntax) {
        
$syntax $this->replaceStuff($syntax);
        
$this->syntax $syntax;
    }
    public function 
getUsage() {
        if (empty(
$this->syntax)) {
            return 
'The author of this plugin has not defined a syntax for this plugin.';
        } else {
            return 
"Usage:\n".$this->syntax;
        }
    }

    
// $footNotes can be contact information or other notes which should appear in --help
    
protected $footNotes;
    public function 
setFootNotes($footNotes) {
        
$this->footNotes $this->replaceStuff($footNotes);
    }
    public function 
getFootNotes($footNotes) {
        return 
$this->footNotes;
    }
}