From c698f2f24328937da474e0eb0c1e0be5b0558622 Mon Sep 17 00:00:00 2001
From: hahn <axel.hahn@iml.unibe.ch>
Date: Wed, 29 May 2019 11:20:36 +0200
Subject: [PATCH] Update components font-awesome (change icon syntax from
 fontawesome 4 --> 5), jquery action button (build, deploy, ...) have the
 color of the target phase Update appmonitor class files

---
 .../classes/appmonitor-checks.class.php       | 414 +++++++++++++++---
 .../classes/appmonitor-client.class.php       | 180 +++++++-
 2 files changed, 517 insertions(+), 77 deletions(-)

diff --git a/public_html/appmonitor/classes/appmonitor-checks.class.php b/public_html/appmonitor/classes/appmonitor-checks.class.php
index 82df3271..66d2375e 100644
--- a/public_html/appmonitor/classes/appmonitor-checks.class.php
+++ b/public_html/appmonitor/classes/appmonitor-checks.class.php
@@ -1,12 +1,28 @@
 <?php
 
-define("RESULT_OK", 0);
-define("RESULT_UNKNOWN", 1);
-define("RESULT_WARNING", 2);
-define("RESULT_ERROR", 3);
+if(!defined('RESULT_OK')){
+    define("RESULT_OK", 0);
+    define("RESULT_UNKNOWN", 1);
+    define("RESULT_WARNING", 2);
+    define("RESULT_ERROR", 3);
+}
 
 /**
- * APPMONITOR CLIENT CHECKS<br>
+ * ____________________________________________________________________________
+ * 
+ *  _____ _____ __                   _____         _ _           
+ * |     |     |  |      ___ ___ ___|     |___ ___|_| |_ ___ ___ 
+ * |-   -| | | |  |__   | .'| . | . | | | | . |   | |  _| . |  _|
+ * |_____|_|_|_|_____|  |__,|  _|  _|_|_|_|___|_|_|_|_| |___|_|  
+ *                          |_| |_|                              
+ *                           _ _         _                                            
+ *                       ___| |_|___ ___| |_                                          
+ *                      |  _| | | -_|   |  _|                                         
+ *                      |___|_|_|___|_|_|_|   
+ *                                                               
+ * ____________________________________________________________________________
+ * 
+ * APPMONITOR :: CLASS FOR CLIENT TEST FUNCTIONS<br>
  * <br>
  * THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE <br>
  * LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR <br>
@@ -24,8 +40,14 @@ define("RESULT_ERROR", 3);
  * 2015-04-08  0.9   axel.hahn@iml.unibe.ch  added sochket test: checkPortTcp<br>
  * 2018-06-29  0.24  axel.hahn@iml.unibe.ch  add file and directory checks<br>
  * 2018-07-17  0.42  axel.hahn@iml.unibe.ch  add port on mysqli check<br>
+ * 2018-07-26  0.46  axel.hahn@iml.unibe.ch  fix mysql connection check with empty port param<br>
+ * 2018-08-14  0.47  axel.hahn@iml.unibe.ch  appmonitor client: use timeout of 5 sec for tcp socket connections<br>
+ * 2018-08-15  0.49  axel.hahn@iml.unibe.ch  cert check: added flag to skip verification<br>
+ * 2018-08-23  0.50  axel.hahn@iml.unibe.ch  replace mysqli connect with mysqli real connect (to use a timeout)<br>
+ * 2018-08-27  0.52  axel.hahn@iml.unibe.ch  add pdo connect (starting with mysql)<br>
+ * 2018-11-05  0.58  axel.hahn@iml.unibe.ch  additional flag in http check to show content<br>
  * --------------------------------------------------------------------------------<br>
- * @version 0.9
+ * @version 0.77
  * @author Axel Hahn
  * @link TODO
  * @license GPL
@@ -36,22 +58,31 @@ class appmonitorcheck {
     // ----------------------------------------------------------------------
     // CONFIG
     // ----------------------------------------------------------------------
-
+    
     /**
      * config container
      * @var array
      */
-    private $_aConfig = array();
+    protected $_aConfig = array();
 
     /**
      * data of all checks
      * @var array
      */
-    private $_aData = array();
+    protected $_aData = array();
     
-    
-    // protected $_units = array( 'B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB');
+
+    /**
+     * flat array with units for sizes
+     * @var array
+     */
     protected $_units = array( 'B', 'KB', 'MB', 'GB', 'TB');
+    
+    /**
+     * timeout in sec for tcp socket connections
+     * @var type 
+     */
+    protected $_iTimeoutTcp=5;
 
     // ----------------------------------------------------------------------
     // CONSTRUCTOR
@@ -72,13 +103,15 @@ class appmonitorcheck {
      * create basic array values for metadata
      * @return boolean
      */
-    private function _createDefaultMetadata() {
+    protected function _createDefaultMetadata() {
 
         $this->_aData = array(
             "name" => $this->_aConfig["name"],
             "description" => $this->_aConfig["description"],
             "result" => RESULT_UNKNOWN,
             "value" => false,
+            "type" => false,
+            "time" => false,
         );
         return true;
     }
@@ -88,7 +121,7 @@ class appmonitorcheck {
      * @param type $iResult
      * @return type
      */
-    private function _setResult($iResult) {
+    protected function _setResult($iResult) {
         return $this->_aData["result"] = (int) $iResult;
     }
 
@@ -97,7 +130,7 @@ class appmonitorcheck {
      * @param type $iResult
      * @return type
      */
-    private function _setOutput($s) {
+    protected function _setOutput($s) {
         return $this->_aData["value"] = (string) $s;
     }
 
@@ -107,13 +140,13 @@ class appmonitorcheck {
      * @param type $s
      * @return boolean
      */
-    private function _setReturn($iResult, $s) {
+    protected function _setReturn($iResult, $s) {
         $this->_setResult($iResult);
         $this->_setOutput($s);
         return true;
     }
 
-    private function _checkArrayKeys($aConfig, $sKeyList) {
+    protected function _checkArrayKeys($aConfig, $sKeyList) {
         foreach (explode(",", $sKeyList) as $sKey) {
             if (!array_key_exists($sKey, $aConfig)) {
                 header('HTTP/1.0 503 Service Unavailable');
@@ -203,17 +236,22 @@ class appmonitorcheck {
     /**
      * helper function: read certificate data
      * called in checkCert()
-     * @param string $sUrl  url to connect
+     * @param string  $sUrl         url to connect
+     * @param boolean $bVerifyCert  flag: verify certificate; default: no check
      * @return array
      */
-    protected function _certGetInfos($sUrl) {
+    protected function _certGetInfos($sUrl, $bVerifyCert) {
         $iTimeout=10;
         $aUrldata=parse_url($sUrl);
         $sHost = isset($aUrldata['host']) ? $aUrldata['host'] : false;
         $iPort = isset($aUrldata['port']) ? $aUrldata['port'] : ((isset($aUrldata['scheme']) && $aUrldata['scheme'] === 'https') ? 443 : false);
 
-        
-        $get = stream_context_create(array("ssl" => array("capture_peer_cert" => TRUE)));
+        $aSsl=array('capture_peer_cert' => true);
+        if($bVerifyCert){
+            $aSsl['verify_peer']=false;
+            $aSsl['verify_peer_name']=false;
+        };
+        $get = stream_context_create(array('ssl' => $aSsl));
         if(!$get){
             return array('_error' => 'Error: Cannot create stream_context');
         }
@@ -221,7 +259,7 @@ class appmonitorcheck {
         $errstr="stream_socket_client failed.";
         $read = stream_socket_client("ssl://$sHost:$iPort", $errno, $errstr, $iTimeout, STREAM_CLIENT_CONNECT, $get);
         if(!$read){
-            return array('_error' => "Error $errno: $errstr; cannot create stream_context to ssl://$sHost:$iPort");
+            return array('_error' => "Error $errno: $errstr; cannot create stream_socket_client with given stream_context to ssl://$sHost:$iPort; you can try to set the flag [verify] to false to check expiration date only.");
         }
         $cert = stream_context_get_params($read);
         if(!$cert){
@@ -238,26 +276,28 @@ class appmonitorcheck {
      * @param array $aParams
      * array(
      *     "url"       optional: url to connect check; default: own protocol + server
+     *     "verify"    optional: flag for verification of certificate or check for expiration only; default=true (=verification is on)
      *     "warning"   optional: count of days to warn; default=30
      * )
      * @return boolean
      */
-    private function checkCert($aParams) {
+    protected function checkCert($aParams) {
         $sUrl = isset($aParams["url"]) 
                 ? $aParams["url"] 
                 : 'http'. ($_SERVER['HTTPS'] ? 's' : '') . '://' . $_SERVER['SERVER_NAME'] .':' . $_SERVER['SERVER_PORT']
                 ;
-        $iWarn = isset($aParams["warning"]) ? (int)($aParams["warning"]) : 30;
+        $bVerify = isset($aParams["verify"])  ? !!$aParams["verify"] : true;
+        $iWarn   = isset($aParams["warning"]) ? (int)($aParams["warning"]) : 30;
 
         $sMessage="Checked url: $sUrl ... ";
-        $certinfo=$this->_certGetInfos($sUrl);
+        $certinfo=$this->_certGetInfos($sUrl, $bVerify);
         if(isset($certinfo['_error'])){
             $this->_setReturn(RESULT_ERROR, $certinfo['_error'] . $sMessage);
             return true;
         }
         
         $sDNS=isset($certinfo['extensions']['subjectAltName']) ? $certinfo['extensions']['subjectAltName'] : false;
-        $sHost=parse_url($url,PHP_URL_HOST);
+        $sHost=parse_url($sUrl,PHP_URL_HOST);
         if(strstr($sDNS, 'DNS:'.$sHost)===false){
             $this->_setReturn(RESULT_ERROR, 'Wrong certificate: '.$sHost.' is not listed as DNS alias in ['.$sDNS.']  ' . $sMessage);
             return true;
@@ -278,7 +318,9 @@ class appmonitorcheck {
             return true;
         }
         // echo '<pre>';
-        $this->_setReturn(RESULT_OK, 'OK, is valid. ' . $sMessage);
+        $this->_setReturn(RESULT_OK, 'OK. ' 
+                .($bVerify ? 'Certificate is valid. ' : '(Verification is disabled; Check for expiration only.) ' )
+                . $sMessage);
         return true;
     }
 
@@ -302,7 +344,7 @@ class appmonitorcheck {
         }
         $power=0;
         foreach($this->_units as $sUnit){
-            if (preg_match('/^[0-9\.]*'.$sUnit.'/', $sValue)){
+            if (preg_match('/^[0-9\.\ ]*'.$sUnit.'/', $sValue)){
                 $i=preg_replace('/([0-9\.]*).*/', '$1', $sValue);
                 $iReal=$i*pow(1024, $power);
                 // die("FOUND: $sValue with unit ${sUnit} - 1024^$power * $i = $iReal");
@@ -325,7 +367,7 @@ class appmonitorcheck {
      * )
      * @return boolean
      */
-    private function checkDiskfree($aParams) {
+    protected function checkDiskfree($aParams) {
         $this->_checkArrayKeys($aParams, "directory", "critical");
         
         $sDirectory = $aParams["directory"];
@@ -378,7 +420,7 @@ class appmonitorcheck {
      * )
      * @return boolean
      */
-    private function checkFile($aParams) {
+    protected function checkFile($aParams) {
         $aOK = array();
         $aErrors = array();
         $this->_checkArrayKeys($aParams, "filename");
@@ -413,42 +455,235 @@ class appmonitorcheck {
         return true;
     }
 
+    
+    /**
+     * compare function for 2 values
+     * @param any      $verifyValue  search value
+     * @param string   $sCompare     compare function; it is one of
+     *                               IS - 
+     *                               GE - greater or equal
+     *                               GT - greater
+     * @param any      $value        existing value
+     * @return boolean
+     */
+    protected function _compare($value, $sCompare, $verifyValue){
+        switch ($sCompare){
+            case "IS":
+                return $value===$verifyValue;
+                break;
+            case "GE":
+                return $value>=$verifyValue;
+                break;
+            case "GT":
+                return $value>$verifyValue;
+                break;
+            case "HAS":
+                return !!(strstr($value, $verifyValue)!==false);
+                break;
+            default:
+                die("FATAL ERROR: a compare function [$sCompare] is not implemented (yet).");
+                break;
+        }
+        return false;
+    }
+
+
     /**
      * make http request and test response body
      * @param array $aParams
      * array(
-     *     "url"       url to fetch
-     *     "contains"  string that must exist in response body
+     *     url                 string   url to fetch
+     *     headeronly          boolean  optional flag to fetch http response herader only; default: false = returns header and body
+     *     follow              boolean  optional flag to follow a location; default: false = do not follow
+     *     status              integer  test for an expected http status code; if none is given then test fails on status 400 and greater
+     *     headercontains      string   test for a string in the http response header; it returns OK if the text was found
+     *     headernotcontains   string   test for a string in the http response header; it returns OK if the text was not found
+     *     headerregex         string   test for a regex in the http response header; it returns OK if the regex matches; example: "headerregex"=>"/lowercasematch/i"
+     *     bodycontains        string   test for a string in the http response body; it returns OK if the text was found
+     *     bodynotcontains     string   test for a string in the http response body; it returns OK if the text was not found
+     *     bodyregex           string   test for a regex in the http response body; it returns OK if the regex matches; example: "headerregex"=>"/lowercasematch/i"
      * )
      * @param integer $iTimeout  value in sec; default: 5sec
      */
-    private function checkHttpContent($aParams, $iTimeout = 5) {
-        $this->_checkArrayKeys($aParams, "url,contains");
+    protected function checkHttpContent($aParams, $iTimeout = 5) {
+        $this->_checkArrayKeys($aParams, "url");
         if (!function_exists("curl_init")) {
             header('HTTP/1.0 503 Service Unavailable');
             die("ERROR: PHP CURL module is not installed.");
         }
+        $bShowContent = (isset($aParams["content"]) && $aParams["content"]) ? true : false;
         $ch = curl_init($aParams["url"]);
+        
+        curl_setopt($ch, CURLOPT_HEADER, 1);
+        curl_setopt($ch, CURLOPT_NOBODY, isset($aParams["headeronly"]) && $aParams["headeronly"]);
+        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, isset($aParams["follow"]) && $aParams["follow"]);
         curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
         curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
         curl_setopt($ch, CURLOPT_TIMEOUT, $iTimeout);
         $res = curl_exec($ch);
-        curl_close($ch);
 
         if (!$res) {
             $this->_setReturn(RESULT_ERROR, 'ERROR: failed to fetch ' . $aParams["url"] . '.');
+            return false;
+        } 
+        $sOut='';
+        $bError=false;
+        
+        $aInfos = curl_getinfo($ch);
+        /*
+            Array
+            (
+                [url] => https://www.iml.unibe.ch/
+                [content_type] => text/html; charset=utf-8
+                [http_code] => 200
+                [header_size] => 926
+                [request_size] => 55
+                [filetime] => -1
+                [ssl_verify_result] => 20
+                [redirect_count] => 0
+                [total_time] => 1.812
+                [namelookup_time] => 0
+                [connect_time] => 0
+                [pretransfer_time] => 0.015
+                [size_upload] => 0
+                [size_download] => 94654
+                [speed_download] => 52237
+                [speed_upload] => 0
+                [download_content_length] => -1
+                [upload_content_length] => -1
+                [starttransfer_time] => 1.812
+                [redirect_time] => 0
+                [redirect_url] => 
+                [primary_ip] => 130.92.30.80
+                [certinfo] => Array
+                    (
+                    )
+
+                [primary_port] => 443
+                [local_ip] => 130.92.79.49
+                [local_port] => 63597
+            )
+         */
+        
+        curl_close($ch);
+        
+        $aTmp=explode("\r\n\r\n", $res, 2);
+        $sHttpHeader=$aTmp[0];
+        $sHttpBody=isset($aTmp[1]) ? $aTmp[1] : false;
+        
+        // ---------- check functions
+        
+        // --- http status code
+        $sOut.="Http status: ".$aInfos['http_code']." - ";
+        if(isset($aParams["status"])){
+            if($aInfos['http_code'] === $aParams["status"]){
+                $sOut.="compare OK<br>";
+            } else {
+                $sOut.="compare failed<br>";
+                $bError=true;
+            }
         } else {
-            if (!strpos($res, $aParams["contains"]) === false) {
-                $this->_setReturn(RESULT_OK, 'OK: The text "' . $aParams["contains"] . '" was found in response of ' . $aParams["url"] . '.');
+            if($aInfos['http_code'] >= 400){
+                $sOut.="Error page detected<br>";
+                $bError=true;
             } else {
-                $this->_setReturn(RESULT_ERROR, 'ERROR: The text ' . $aParams["contains"] . ' was NOT found in response of ' . $aParams["url"] . '.');
+                $sOut.="request successful<br>";
+            }
+        }
+        // --- http header
+        if(isset($aParams["headercontains"]) && $aParams["headercontains"]){
+            $sOut.="Http header contains &quot;".$aParams["headercontains"]."&quot; - ";
+            if(!strstr($sHttpHeader, $aParams["headercontains"])===false){
+                $sOut.="compare OK<br>";
+            } else {
+                $sOut.="compare failed<br>";
+                $bError=true;
+            }
+        }
+        if(isset($aParams["headernotcontains"]) && $aParams["headernotcontains"]){
+            $sOut.="Http header does not contain &quot;".$aParams["headernotcontains"]."&quot; - ";
+            if(strstr($sHttpHeader, $aParams["headernotcontains"])===false){
+                $sOut.="compare OK<br>";
+            } else {
+                $sOut.="compare failed<br>";
+                $bError=true;
+            }
+        }
+        if(isset($aParams["headerregex"]) && $aParams["headerregex"]){
+            $sOut.="Http header regex test &quot;".$aParams["headerregex"]."&quot; - ";
+            try{
+                $bRegex=preg_match($aParams["headerregex"], $sHttpHeader);
+                if($bRegex){
+                    $sOut.="compare OK<br>";
+                } else {
+                    $sOut.="compare failed<br>";
+                    $bError=true;
+                }
+            } 
+            catch(Exception $e){
+                $sOut.="Wrong REGEX<br>" . print_r($e, 1).'<br>';
+                $bError=true;
             }
         }
-        return $res;
+        // --- http body
+        if(isset($aParams["bodycontains"]) && $aParams["bodycontains"]){
+            $sOut.="Http body contains &quot;".$aParams["bodycontains"]."&quot; - ";
+            if(!strstr($sHttpBody, $aParams["bodycontains"])===false){
+                $sOut.="compare OK<br>";
+            } else {
+                $sOut.="compare failed<br>";
+                $bError=true;
+            }
+        }
+        if(isset($aParams["bodynotcontains"]) && $aParams["bodynotcontains"]){
+            $sOut.="Http body does not contain &quot;".$aParams["bodynotcontains"]."&quot; - ";
+            if(strstr($sHttpBody, $aParams["bodynotcontains"])===false){
+                $sOut.="compare OK<br>";
+            } else {
+                $sOut.="compare failed<br>";
+                $bError=true;
+            }
+        }
+        if(isset($aParams["bodyregex"]) && $aParams["bodyregex"]){
+            $sOut.="Http body regex test &quot;".$aParams["bodyregex"]."&quot; - ";
+            try{
+                $bRegex=preg_match($aParams["bodyregex"], $sHttpBody);
+                if($bRegex){
+                    $sOut.="compare OK<br>";
+                } else {
+                    $sOut.="compare failed<br>";
+                    $bError=true;
+                }
+            } 
+            catch(Exception $e){
+                $sOut.="Wrong REGEX<br>" . print_r($e, 1).'<br>';
+                $bError=true;
+            }
+        }
+        
+        if (!$bError) {
+            $this->_setReturn(RESULT_OK, 
+                    'OK: http check "' . $aParams["url"] . '".<br>'.$sOut
+                    );
+        } else {
+            $this->_setReturn(RESULT_ERROR, 
+                    'ERROR: http check "' . $aParams["url"] . '".<br>'.$sOut
+                    );
+        }
+
+        /*
+        echo '<pre>'; 
+        echo $sOut."<hr>";
+        echo "<hr>HEADER: ".htmlentities($sHttpHeader)."<hr>";
+        print_r($aParams); print_r($aInfos); 
+        // echo htmlentities($sHttpBody);
+        die();
+         */
+        return true;
     }
 
     /**
-     * check mysql connection to a database using mysqli
+     * check mysql connection to a database using mysqli realconnect
      * @param array $aParams
      * array(
      *     "server" 
@@ -458,18 +693,65 @@ class appmonitorcheck {
      *     "port"     <<< optional
      * )
      */
-    private function checkMysqlConnect($aParams) {
+    protected function checkMysqlConnect($aParams) {
         $this->_checkArrayKeys($aParams, "server,user,password,db");
+        $mysqli=mysqli_init();
+        if(!$mysqli){
+            $this->_setReturn(RESULT_ERROR, 'ERROR: mysqli_init failed.');
+            return false;
+        }
+        if (!$mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, $this->_iTimeoutTcp)) {
+            $this->_setReturn(RESULT_ERROR, 'ERROR: setting mysqli_init failed.');
+            return false;
+        }
+
         $db = (isset($aParams["port"]) && $aParams["port"]) 
-                ? mysqli_connect($aParams["server"], $aParams["user"], $aParams["password"], $aParams["db"], $aParams["port"])
-                : mysqli_connect($aParams["server"], $aParams["user"], $aParams["password"], $aParams["db"])
+                ? $mysqli->real_connect($aParams["server"], $aParams["user"], $aParams["password"], $aParams["db"], $aParams["port"])
+                : $mysqli->real_connect($aParams["server"], $aParams["user"], $aParams["password"], $aParams["db"])
                 ;
         if ($db) {
             $this->_setReturn(RESULT_OK, "OK: Mysql database " . $aParams["db"] . " was connected");
-            mysqli_close($db);
+            $mysqli->close();
             return true;
         } else {
-            $this->_setReturn(RESULT_ERROR, "ERROR: Mysql database " . $aParams["db"] . " was not connected. " . mysqli_connect_error());
+            $this->_setReturn(RESULT_ERROR, "ERROR: Mysql database " . $aParams["db"] . " was not connected. Error ".mysqli_connect_errno() .": ". mysqli_connect_error());
+            return false;
+        }
+    }
+    /**
+     * check connection to a database using pdo
+     * see http://php.net/manual/en/pdo.drivers.php
+     * 
+     * @param array $aParams
+     * array(
+     *     "connect" 
+     *     "password" 
+     *     "user" 
+     * )
+     */
+    protected function checkPdoConnect($aParams) {
+        $this->_checkArrayKeys($aParams, "connect,user,password");
+
+        try{
+            $db = new PDO(
+                $aParams['connect'], 
+                $aParams['user'], 
+                $aParams['password'], 
+                array(
+                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+                    
+                    // timeout
+                    // Not all drivers support this option; mysqli does
+                    PDO::ATTR_TIMEOUT => $this->_iTimeoutTcp,                  
+                    // mssql
+                    // PDO::SQLSRV_ATTR_QUERY_TIMEOUT => $this->_iTimeoutTcp,  
+                )
+            );
+            $this->_setReturn(RESULT_OK, "OK: Database was connected with PDO " . $aParams['connect']);
+            $db=null;
+            return true;
+        } catch(PDOException $e) {
+            $this->_setReturn(RESULT_ERROR, "ERROR: Database was not connected " . $aParams['connect'] . " was not connected. Error ".$e->getMessage());
             return false;
         }
     }
@@ -483,7 +765,7 @@ class appmonitorcheck {
      * )
      * @return boolean
      */
-    private function checkPortTcp($aParams) {
+    protected function checkPortTcp($aParams) {
         $this->_checkArrayKeys($aParams, "port");
 
         $sHost = array_key_exists('host', $aParams) ? $aParams['host'] : '127.0.0.1';
@@ -496,6 +778,16 @@ class appmonitorcheck {
             $this->_setReturn(RESULT_UNKNOWN, "ERROR: $sHost:$iPort was not checked. socket_create() failed: " . socket_strerror(socket_last_error()));
             return false;
         }
+        // set socket timeout
+        socket_set_option(
+            $socket,
+            SOL_SOCKET,  // socket level
+            SO_SNDTIMEO, // timeout option
+            array(
+              "sec"=>$this->_iTimeoutTcp, // timeout in seconds
+              "usec"=>0
+              )
+            );
 
         $result = socket_connect($socket, $sHost, $iPort);
         if ($result === false) {
@@ -510,16 +802,32 @@ class appmonitorcheck {
     }
 
     /**
-     * most simple check: set values
+     * most simple check: set given values
+     * Use this function to add a counter
+     * 
      * @param array $aParams
-     * array(
-     *     "result" integer; RESUL_nn
-     *     "value"  description text
-     * )
+     * array keys:
+     *     "result"   integer; RESULT_nn
+     *     "value"    description text
+     * 
+     *     brainstorming for a future release
+     * 
+     *     "counter"  optioal: array of counter values
+     *         - "label"  string: a label
+     *         - "value"  a number
+     *         - "unit"   string: unit, i.e. ms, MB/s
+     *         - "type"   one of counter | bars | bars-stacked | lines | ... ??
+     * 
      */
-    private function checkSimple($aParams) {
+    protected function checkSimple($aParams) {
         $this->_checkArrayKeys($aParams, "result,value");
-        return $this->_setReturn((int) $aParams["result"], $aParams["value"]);
+        $this->_setReturn((int) $aParams["result"], $aParams["value"]);
+        foreach(array('type', 'count', 'visual') as $sMyKey){
+            if(isset($aParams[$sMyKey])){
+                $this->_aData[$sMyKey]=$aParams[$sMyKey];
+            }
+        }
+        return true;
     }
 
     /**
@@ -530,7 +838,7 @@ class appmonitorcheck {
      * )
      * @return boolean
      */
-    private function checkSqliteConnect($aParams) {
+    protected function checkSqliteConnect($aParams) {
         $this->_checkArrayKeys($aParams, "db");
         if (!file_exists($aParams["db"])) {
             $this->_setReturn(RESULT_ERROR, "ERROR: Sqlite database file " . $aParams["db"] . " does not exist.");
diff --git a/public_html/appmonitor/classes/appmonitor-client.class.php b/public_html/appmonitor/classes/appmonitor-client.class.php
index 235b2217..a73a52a3 100644
--- a/public_html/appmonitor/classes/appmonitor-client.class.php
+++ b/public_html/appmonitor/classes/appmonitor-client.class.php
@@ -1,7 +1,21 @@
 <?php
 
 /**
- * APPMONITOR CLIENT<br>
+ * ____________________________________________________________________________
+ * 
+ *  _____ _____ __                   _____         _ _           
+ * |     |     |  |      ___ ___ ___|     |___ ___|_| |_ ___ ___ 
+ * |-   -| | | |  |__   | .'| . | . | | | | . |   | |  _| . |  _|
+ * |_____|_|_|_|_____|  |__,|  _|  _|_|_|_|___|_|_|_|_| |___|_|  
+ *                          |_| |_|                              
+ *                           _ _         _                                            
+ *                       ___| |_|___ ___| |_                                          
+ *                      |  _| | | -_|   |  _|                                         
+ *                      |___|_|_|___|_|_|_|   
+ *                                                               
+ * ____________________________________________________________________________
+ * 
+ * APPMONITOR :: CLASS FOR CLIENT CHECKS<br>
  * <br>
  * THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE <br>
  * LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR <br>
@@ -15,10 +29,14 @@
  * --------------------------------------------------------------------------------<br>
  * <br>
  * --- HISTORY:<br>
- * 2014-10-24  0.5  axel.hahn@iml.unibe.ch<br>
- * 2014-11-21  0.6  axel.hahn@iml.unibe.ch  removed meta::ts <br>
+ * 2014-10-24  0.5   axel.hahn@iml.unibe.ch<br>
+ * 2014-11-21  0.6   axel.hahn@iml.unibe.ch  removed meta::ts <br>
+ * 2018-08-23  0.50  axel.hahn@iml.unibe.ch  show version<br>
+ * 2018-08-24  0.51  axel.hahn@iml.unibe.ch  method to show local status page<br>
+ * 2018-08-27  0.52  axel.hahn@iml.unibe.ch  add pdo connect (starting with mysql)<br>
+ * 2018-11-05  0.58  axel.hahn@iml.unibe.ch  additional flag in http check to show content<br>
  * --------------------------------------------------------------------------------<br>
- * @version 0.6
+ * @version 0.85
  * @author Axel Hahn
  * @link TODO
  * @license GPL
@@ -32,29 +50,41 @@ class appmonitor {
      * value is in seconds
      * @var int
      */
-    private $_iDefaultTtl = 300;
+    protected $_sVersion = 'php-client-v0.85';
+
+    /**
+     * config: default ttl for server before requesting the client check again
+     * value is in seconds
+     * @var int
+     */
+    protected $_iDefaultTtl = 300;
 
     /**
      * internal counter: greatest return value of all checks
      * @var type 
      */
-    private $_iMaxResult = false;
+    protected $_iMaxResult = false;
 
     /**
      * responded metadata of a website
      * @see _createDefaultMetadata()
      * @var array
      */
-    private $_aMeta = array();
+    protected $_aMeta = array();
 
     /**
      * repended array of all checks
      * @see addCheck()
      * @var array
      */
-    private $_aChecks = array();
+    protected $_aChecks = array();
+    
+    /**
+     * for time measurements: start time
+     * @var type 
+     */
     protected $_iStart = false;
-
+    
     /**
      * constructor: init data
      */
@@ -63,21 +93,22 @@ class appmonitor {
     }
 
     // ----------------------------------------------------------------------
-    // private function
+    // protected function
     // ----------------------------------------------------------------------
 
     /**
      * create basic array values for metadata
      * @return boolean
      */
-    private function _createDefaultMetadata() {
+    protected function _createDefaultMetadata() {
         $this->_iStart = microtime(true);
         $this->_aMeta = array(
             "host" => false,
             "website" => false,
             "ttl" => false,
             "result" => false,
-            "time" => false
+            "time" => false,
+            "version" => $this->_sVersion,
         );
 
         // fill with default values
@@ -92,7 +123,9 @@ class appmonitor {
     // ----------------------------------------------------------------------
 
     /**
-     * set a host for metadata
+     * set the physical hostname for metadata; if no host is given then
+     * the php_uname("n") will be used to set one.
+     * 
      * @param string $s  hostname
      * @return bool
      */
@@ -104,7 +137,14 @@ class appmonitor {
     }
 
     /**
-     * set a vhost for metadata
+     * set a name for this website or application and its environment 
+     * (dev, test, prod); 
+     * 
+     * If you have several application in subdirectories, i.e. /blog,  /shop...
+     * then you should the path or any description to identify them too
+     * 
+     * if no argument is given the name of HTTP_HOST will be used
+     * 
      * @param string $sNewHost  hostname
      * @return bool
      */
@@ -116,7 +156,9 @@ class appmonitor {
     }
 
     /**
-     * set a ttl value for 
+     * set a ttl value in seconds to define how long a server should not
+     * ask again for a new status of this instance
+     * 
      * @param int $iTTl TTL value in sec
      * @return boolean
      */
@@ -151,9 +193,16 @@ class appmonitor {
         $oCheck = new appmonitorcheck();
         // print_r($aJob); die();
         $aCheck = $oCheck->makecheck($aJob);
-
-        if (!$this->_iMaxResult || $aCheck["result"] > $this->_iMaxResult) {
-            $this->_iMaxResult = $aCheck["result"];
+        
+        // limit error code
+        // echo "DEBUG ".$aCheck["name"].": ".$aCheck["result"]."\n";
+        $iMyResult=isset($aJob['worstresult']) 
+                ? min($aCheck["result"], $aJob['worstresult'])
+                : $aCheck["result"]
+                ;
+
+        if (!$this->_iMaxResult || $iMyResult > $this->_iMaxResult) {
+            $this->_iMaxResult = $iMyResult;
         }
         return $this->_aChecks[] = $aCheck;
     }
@@ -282,7 +331,7 @@ class appmonitor {
     /**
      * verify array values and abort with all found errors
      */
-    private function _checkData() {
+    protected function _checkData() {
         $aErrors = array();
 
         if (!count($this->_aChecks)) {
@@ -293,19 +342,28 @@ class appmonitor {
             $aErrors[] = "method setResult was not used to set a final result for all checks.";
         }
 
-
         if (count($aErrors)) {
-            header('HTTP/1.0 503 Service Unavailable');
-            echo "<h1>Errors detected</h1><ol><li>" . implode("<li>", $aErrors) . "</ol><hr>";
-            echo "<pre>" . print_r($this->getResults(), true) . "</pre><hr>";
-            die("ABORT");
+            $this->abort(
+                '<h2>Error: client check is not complete</h2><p>Found errors:</p><ol><li>' . implode('<li>', $aErrors) . '</ol><br><br>'
+                // .'Dump of your data so far:<pre>' . json_encode($this->getResults(), JSON_PRETTY_PRINT) . '</pre><hr>'
+            );
         }
+        return true;
     }
 
     // ----------------------------------------------------------------------
     // output
     // ----------------------------------------------------------------------
 
+    /**
+     * stop processing the client checks and abort with an error
+     * @param string $sMessage
+     */
+    public function abort($sMessage){
+        header('HTTP/1.0 503 Service Unavailable');
+        die('<h1>503 Service Unavailable</h1>'.$sMessage);
+    }
+    
     /**
      * get full array for response with metadata and Checks
      * @return type
@@ -384,5 +442,79 @@ class appmonitor {
         echo $sOut;
         return $sOut;
     }
+    /**
+     * output appmonitor client status as single html page
+     * 
+     * @example <code>
+     * ob_start();<br>
+     * require __DIR__ . '/appmonitor/client/index.php';
+     * $sJson=ob_get_contents();
+     * ob_end_clean();
+     * $oMonitor->renderHtmloutput($sJson);
+     * </code>
+     * 
+     * @param string  $sJson  JSON of client output
+     */
+    public function renderHtmloutput($sJson) {
+
+
+        header('Content-type: text/html');
+        header('Cache-Control: cache');
+        header('max-age: ' . $this->_aMeta["ttl"]);
+        $aMsg = array(
+            0 => "OK",
+            1 => "UNKNOWN",
+            2 => "WARNING",
+            3 => "ERROR"
+        );
+
+        // $sOut = print_r($sJson, 1);
+        $aData= json_decode($sJson, 1);
+
+        // ----- Ausgabe human readable
+        $sOut='';
+        $sOut.='<h2>Metadata</h2>'
+                . '<div class="meta'.(isset($aData['meta']['result'])  ? ' result'.$aData['meta']['result'] : '' ) .'">'
+                . 'Status: '  . (isset($aData['meta']['result'])  ? $aMsg[$aData['meta']['result']] : '?').'<br>'
+                . '</div>'
+                . 'Host: '    . (isset($aData['meta']['host'])    ? '<span class="string">' . $aData['meta']['host']   .'</span>'    : '?').'<br>'
+                . 'Website: ' . (isset($aData['meta']['website']) ? '<span class="string">' . $aData['meta']['website'].'</span>'  : '?').'<br>'
+                // . 'Status: '  . (isset($aData['meta']['result'])  ? '<span class="result'.$aData['meta']['result'].'">'. $aMsg[$aData['meta']['result']].'</span>'  : '?').'<br>'
+                . 'Execution time: '    . (isset($aData['meta']['time'])    ? '<span class="float">'  . $aData['meta']['time']  .'</span>'  : '?').'<br>'
+
+                .'<h2>Checks</h2>'
+                ;
+        if (isset($aData['checks'][0]) && count($aData['checks'])){
+            foreach($aData['checks'] as $aCheck){
+               $sOut.= '<span class="result'.$aCheck['result'].'"> <strong>'.$aCheck['name'].'</strong></span> <br>'
+                   . $aCheck['description'].'<br>'
+                   // . '<span class="result'.$aCheck['result'].'">'.$aCheck['value'].'</span><br>'
+                   . $aCheck['value'].'<br>'
+                   . 'Execution time: ' . $aCheck['time'].'<br>'
+                   . 'Status: '  . $aMsg[$aCheck['result']].'<br>'
+                   . '<br>'
+                   ;
+            }
+        }
+        $sOut.= '<hr>List of farbcodes: ';
+        foreach ($aMsg as $i=>$sText){
+            $sOut.= '<span class="result'.$i.'">'. $sText.'</span> ';
+        }
+        $sOut = '<!DOCTYPE html><html><head>'
+                . '<style>'
+                . 'body{background:#fff; color:#444; font-family: verdana,arial; margin: 3em;}'
+                . '.result0{background:#aca; border-left: 1em solid #080; padding: 0 0.5em; }'
+                . '.result1{background:#ccc; border-left: 1em solid #aaa; padding: 0 0.5em; }'
+                . '.result2{background:#fc9; border-left: 1em solid #860; padding: 0 0.5em; }'
+                . '.result3{background:#f88; border-left: 1em solid #f00; padding: 0 0.5em; }'
+                . '</style>'
+                . '<title>' . __CLASS__ . '</title>'
+                . '</head><body>'
+                . '<h1>' . __CLASS__ . ' :: client status</h1>'
+                . $sOut
+                . '</body></html>';
+        echo $sOut;
+        return $sOut;
+    }
 
 }
-- 
GitLab