<?php
/**
 * ForemanApi
 * 
 * foreman access to API
 * 
 * @example
 * in project class
 * $oForeman=new ForemanApi($this->_aConfig['foreman']);
 * 
 * // enable debugging
 * $oForeman->setDebug(1);
 * 
 * // self check
 * $oForeman->selfcheck(); die(__FUNCTION__);
 *
 * // read operating systems and get id and title only
 * $aForemanHostgroups=$oForeman->read(array(
 *    'request'=>array(
 *        array('operatingsystems'),
 *    ),
 *    'response'=>array(
 *        'id','title'
 *   ),
 * ));
 * 
 * // read details for operating systems #4
 * $aForemanHostgroups=$oForeman->read(array(
 *    'request'=>array(
 *        array('operatingsystems', 4),
 *    ),
 * ));
 *
 * 
 * $aOptions ... can contain optional subkeys
 * - request
 *      [] list of array(keyword [,id])
 * - filter (array)
 *      - search (string)
 *      - page (string)
 *      - per_page (string)
 * - response (array)
 *      - list of keys, i.e. array('id', 'title')

 * @author hahn
 */
class ForemanApi {

    protected $_aCfg=array();
    protected $_bDebug = false;

    protected $_aAllowedUrls=array(
        'api'=>array(
            ''=>array(),
            'architectures'=>array(),
            'audits'=>array('methods'=>array('GET')),
            'auth_source_ldaps'=>array(),
            'bookmarks'=>array(),
            'common_parameters'=>array(),
            'compliance'=>array(),
            'compute_attributes'=>array(),
            'compute_profiles'=>array(),
            'compute_resources'=>array(),
            'config_groups'=>array(),
            'config_reports'=>array(),
            'config_templates'=>array(),
            'dashboard'=>array('methods'=>array('GET')),
            'domains'=>array(),
            'environments'=>array(),
            'fact_values'=>array(),
            'filters'=>array(),
            'hosts'=>array(),
            'hostgroups'=>array(),
            'job_invocations'=>array(),
            'job_templates'=>array(),
            'locations'=>array(),
            'mail_notifications'=>array(),
            'media'=>array(),
            'models'=>array(),
            'operatingsystems'=>array('methods'=>array('GET')),
            'orchestration'=>array(),
            'organizations'=>array(),
            'permissions'=>array(),
            'plugins'=>array(),
            'provisioning_templates'=>array(),
            'ptables'=>array(),
            'puppetclasses'=>array(),
            'realms'=>array(),
            'remote_execution_features'=>array(),
            'reports'=>array(),
            'roles'=>array(),
            'settings'=>array(),
            'smart_class_parameters'=>array(),
            'smart_proxies'=>array(),
            'smart_variables'=>array(),
            'statistics'=>array('methods'=>array('GET')),
            'status'=>array('methods'=>array('GET')),
            'subnets'=>array(),
            'template_combinations'=>array(),
            'template_kinds'=>array('methods'=>array('GET')),
            'templates'=>array(),
            'usergroups'=>array(),
            'users'=>array(),
            // ...
        ),
        'api/v2'=>array(
            'discovered_hosts'=>array(),
            'discovery_rules'=>array(),
        ),
        'foreman_tasks/api'=>array(
            'recurring_logics'=>array(),
            'tasks'=>array(),
        ),
    );
    
    
    protected $_aRequest=array();
    
    
    // ----------------------------------------------------------------------
    // constructor
    // ----------------------------------------------------------------------
    
    
    public function __construct($aCfg) {
        if(!is_array($aCfg) || !count($aCfg) || !array_key_exists('api', $aCfg)){
            die("ERROR: class ".__CLASS__." must be initialized with an array containing api config for foreman.");
            return false;
        }
        $this->_aCfg=$aCfg;
        
        return true;
    }
    
    // ----------------------------------------------------------------------
    // private functions
    // ----------------------------------------------------------------------
    /**
     * add a log messsage
     * @global object $oLog
     * @param  string $sMessage  messeage text
     * @param  string $sLevel    warnlevel of the given message
     * @return bool
     */
    protected function log($sMessage, $sLevel = "info") {
        global $oCLog;
        return $oCLog->add(basename(__FILE__) . " class " . __CLASS__ . " - " . $sMessage, $sLevel);
    }
    
    /**
     * search url prefix in $this->_aAllowedUrls by given key
     * @param type $sFunction
     * @return type
     */
    protected function _guessPrefixUrl($sFunction=false){
        $sReturn='';
        /*
        if (!$sFunction){
            $sFunction=$this->_aRequest['request'][0][0];
        }
         * 
         */
        foreach($this->_aAllowedUrls as $sPrefix=>$aUrls){
            foreach(array_keys($aUrls) as $sKeyword){
                if ($sFunction==$sKeyword){
                    $sReturn=$sPrefix;
                    break;
                }
            }
        }
        return $sReturn;
    }

    /**
     * generate an url for foreman API request based on option keys
     * @return string
     */
    protected function _generateUrl(){
        if(!array_key_exists('request', $this->_aRequest)){
            die('ERROR: missing key [request]');
        }
        $sReturn=$this->_aCfg['api'];
        
        $sPrefix=$this->_guessPrefixUrl();
        $sReturn.=$sPrefix.'/';
        
        foreach($this->_aRequest['request'] as $aReqItem){
            if (!array_key_exists($aReqItem[0], $this->_aAllowedUrls[$sPrefix])){
                echo 'WARNING: wrong item: [' . $aReqItem[0]."]<br>\n";
            }
            $sReturn.=$aReqItem[0] ? $aReqItem[0].'/' : '';
            if(count($aReqItem)>1){
                $sReturn.=(int)$aReqItem[1].'/';
            }
        }
        return $sReturn;
    }
    
    /**
     * add parameter for search and paging in an foreman API URL
     * 
     * @return string
     */
    protected function _generateParams(){
        if (!array_key_exists('filter', $this->_aRequest)){
            return '';
        }
        $sReturn='?';
        
        foreach ($this->_aRequest['filter'] as $sKey=>$value){
            $sReturn.='&'.$sKey.'='.urlencode($value);
        }
        return $sReturn;
    }

    /**
     * make an http get request and return the response body
     * it is called by _makeRequest
     * $aRequest contains subkeys
     * - url
     * - method; one of GET|POST|PUT|DELETE
     * - postdata; for POST only
     * 
     * @param array   $aRequest   arrayurl for Foreman API
     * @return string
     */
    protected function _httpCall($aRequest=false, $iTimeout = 15) {
        if ($aRequest){
            $this->_aRequest=$aRequest;
        }
        $this->log(__FUNCTION__ . " start <pre>".print_r($this->_aRequest,1)."</pre>");
        if (!function_exists("curl_init")) {
            die("ERROR: PHP CURL module is not installed.");
        }
        
        $sApiUser=array_key_exists('user', $this->_aCfg) ? $this->_aCfg['user'] : false;
        $sApiPassword=array_key_exists('password', $this->_aCfg) ? $this->_aCfg['password'] : false;

        $sFullUrl=$sApiUrl.$this->_aRequest['url'];
        $this->log(__FUNCTION__ . " ".$this->_aRequest['method']." " . $this->_aRequest['url']);
        $ch = curl_init($this->_aRequest['url']);

        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->_aRequest['method']);
        if ($this->_aRequest['method']==='POST'){
            curl_setopt($ch, CURLOPT_POSTFIELDS, $this->_aRequest['postdata']);
        }
        curl_setopt($ch, CURLOPT_HEADER, 1);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        
        if (array_key_exists('ignore-ssl-error', $this->_aCfg) && $this->_aCfg['ignore-ssl-error']){
            $this->log(__FUNCTION__ . " WARNING: SSL errors will be ignored by config.", 'warning');
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
        }
        curl_setopt($ch, CURLOPT_TIMEOUT, $iTimeout);
        curl_setopt($ch, CURLOPT_USERAGENT, 'IML Deployment GUI :: ' . __CLASS__);
        if ($sApiUser){
            curl_setopt($ch, CURLOPT_USERPWD, $sApiUser.":".$sApiPassword);
        }

        $res = curl_exec($ch);
        $aReturn=array('info'=>curl_getinfo($ch), 'error'=>curl_error($ch));
        curl_close($ch);
        $this->log(__FUNCTION__ . " status ".$aReturn['info']['http_code'].' '.$this->_aRequest['method']." $sFullUrl");

        $sHeader=substr($res, 0, $aReturn['info']['header_size']);
        $aReturn['header']=explode("\n", $sHeader);
        $aReturn['body']=str_replace($sHeader, "", $res);
                
        return $aReturn;
    }

    /**
     * write debug infos if enabled
     * @param string $sMessage
     * @return boolean
     */
    protected function _writeDebug($sMessage){
        if ($this->_bDebug){
            echo "DEBUG :: ".__CLASS__." :: $sMessage<br>\n";
        }
        return true;
    }

    // ----------------------------------------------------------------------
    // public functions :: low level
    // ----------------------------------------------------------------------
    
    /**
     * make an http(s) request to foreman and scan result for http code and
     * content in response json; method returns an array with subkeys
     * - info: curl info array
     * - error: curl error message
     * - header: http response headers
     * - body: http response body
     * - _json: parsed json data from response body
     * - _OK: flag if result is OK and complete
     * - _status: info
     * 
     *      * $aRequest contains subkeys
     * - function --> to extract method and generate url
     * - method; one of GET|POST|PUT|DELETE
     * - postdata; for POST only

     * @param array   $aRequest   arrayurl for Foreman API
     * @return array
     */
    public function makeRequest($aRequest=false) {
        if ($aRequest){
            $this->_aRequest=$aRequest;
        }
        $sStatus='unknown';
        $bOk=false;
        
        
        // prevent missing data because of paging
        if ($this->_aRequest['method']==='GET' && !array_key_exists('per_page', $this->_aRequest['filter'])){
            $this->_aRequest['filter']['per_page']=1000;
        }
        // TODO check postdata
        if ($this->_aRequest['method']==='POST' && (!array_key_exists('postdata',$this->_aRequest) || !count($this->_aRequest['postdata']))){
            die("ERROR in ".__CLASS__."::".__FUNCTION__.": missing data to make a POST request");
        }
        if (!array_key_exists('url', $this->_aRequest)){
            $this->_aRequest['url']=$this->_generateUrl().$this->_generateParams();
        }

        // ----- request
        $this->_writeDebug(__FUNCTION__ . ' start request <pre>'.print_r($this->_aRequest,1).'</pre>');
        $aReturn=$this->_httpCall();
        
        // ----- check result
        // check status
        $iStatuscode=$aReturn['info']['http_code'];
        if ($iStatuscode===0){
            $sStatus='wrong host';
        }
        if ($iStatuscode>=200 && $iStatuscode<400){
            $sStatus='OK';
            $bOk=true;
        }
        if ($iStatuscode>=400 && $iStatuscode<500){
            $sStatus='error';
        }
        if ($iStatuscode===404){
            $sStatus='wrong url';
        }
        
        // check result json
        if($bOk){
            $aJson=json_decode($aReturn['body'], 1);
            if (is_array($aJson)){
                if (array_key_exists('total', $aJson) && $aJson['total'] > $aJson['per_page']){
                    $bOk=false;
                    $sStatus='Http OK, but incomplete results (paging)';
                }
                $aReturn['_json']=$aJson;
            } else {
                $bOk=false;
                $sStatus='Http OK, but wrong response';
            }
        }
        $aReturn['_OK']=$bOk;
        $aReturn['_status']=$sStatus;
        $this->_writeDebug(__FUNCTION__ . ' result of request <pre>'.print_r($aReturn,1).'</pre>');

        return $aReturn;
    }

    /**
     * filter output for the response based on values $this->_aRequest['response']
     * @param array  $aData  response of $this->makeRequest();
     * @return array
     */
    protected function _filterOutput($aData){
        if(!array_key_exists('response', $this->_aRequest)){
            return $aData;
        }
        $bIsList=array_key_exists('results', $aData['_json']);
        $aTmp=$bIsList ? $aData['_json']['results'] : array($aData['_json']);
        if(!count($aTmp)){
            return array();
        }
        $aReturn=array();
        foreach($aTmp as $aItem){
            $aReturnitem=array();
            foreach($this->_aRequest['response'] as $sKey){
                if (array_key_exists($sKey, $aItem)){
                    $aReturnitem[$sKey]=$aItem[$sKey];
                }
            }
            $aReturn[] = $aReturnitem;
        }
        return ($bIsList==1)
                ? $aReturn 
                : (count($aReturn) ? $aReturn[0] : array());
    }


    // ----------------------------------------------------------------------
    // public foreman functions
    // ----------------------------------------------------------------------

    /**
     * enable/ disable debugging
     * @param boolean  $bNewDebugFlag  new value; true|false
     * @return boolean
     */
    public function setDebug($bNewDebugFlag){
        return $this->_bDebug=$bNewDebugFlag;
    }

    /**
     * check for missing config entries
     * @return type
     */
    public function selfcheck() {
        $sOut='';
        $sWarning='';
        $sOut.="<h1>selfcheck</h1>";
        $aApi=$this->read(array('request'=>array(array(''))));
        if($aApi['_OK']){
            foreach($aApi['_json']['links'] as $sKey=>$aCalls){
                $sOut.="<h2>$sKey</h2><ul>";
                foreach ($aCalls as $sLabel=>$sUrl){
                    $sOut.="<li>$sLabel .. $sUrl ";
                    $aTmp=preg_split('#\/#', $sUrl);
                    $sDir2=count($aTmp)>2 ? $aTmp[2] : '??';
                    $sDir3=count($aTmp)>3 ? $aTmp[3] : '??';
                    $sOut.="..... " 
                        . ($this->_guessPrefixUrl($sDir2).$this->_guessPrefixUrl($sDir3)
                            ?'<span style="background:#cfc">OK</span>'
                            :'<span style="background:#fcc">miss</span>' 
                        ) . ' ' . $sDir2.', '.$sDir3 . "</li>\n";
                    if (!($this->_guessPrefixUrl($sDir2).$this->_guessPrefixUrl($sDir3))){
                        $sWarning.="<li>$sKey - $sLabel - $sUrl</li>";
                    }
                }
                $sOut.="</ul>";
            }
        } else {
            $sOut.='ERROR: unable to connect to foreman or missing permissions.<br>';
        }
        if ($sWarning){
            echo 'WARNINGS:<ol>'.$sWarning.'</ol>';
        }
        echo $sOut;
        return true;
    }
    
    // ----------------------------------------------------------------------
    // public foreman API CRUD functions
    // ----------------------------------------------------------------------
    
    /**
     * TODO: create
     * @param array $aOptions
     */
    public function create($aOptions){
        /*
        $this->_aRequest=$aOptions;
        $this->_aRequest['method']='POST';
        return $this->makeRequest();
         */
    }
    /**
     * GETTER
     * $aOptions ... can contain optional subkeys
     * - request
     *      [] list of array(keyword [,id])
     * - filter (array)
     *      - search (string)
     *      - page (string)
     *      - per_page (string)
     *      - any attribute in the return resultset
     * - response (array)
     *      - list of keys, i.e. array('id', 'title')
     * 
     * @param array  $aOptions  
     * @return array
     */
    public function read($aOptions){
        $this->_aRequest=$aOptions;
        $this->_aRequest['method']='GET';
        $aData=$this->makeRequest();
        if (!$aData['_OK']){
            return false;
        }
        return $this->_filterOutput($aData);
    }
    
    /**
     * TODO
     * @param type $aOptions
     */
    public function update($aOptions){
        /*
        $this->_aRequest=$aOptions;
        $this->_aRequest['method']='PUT';
        return $this->makeRequest();
         */
    }
    
    /**
     * TODO
     * @param type $aOptions
     */
    public function delete($aOptions){
        /*
        $this->_aRequest=$aOptions;
        $this->_aRequest['method']='DELETE';
        return $this->makeRequest();
         */
    }
    
}
