<?php
require_once 'redirect.class.php';
/**
 * ----------------------------------------------------------------------
 *   _____ __  __ _        _____          _ _               _   
 *  |_   _|  \/  | |      |  __ \        | (_)             | |  
 *    | | | \  / | |      | |__) |___  __| |_ _ __ ___  ___| |_ 
 *    | | | |\/| | |      |  _  // _ \/ _` | | '__/ _ \/ __| __|
 *   _| |_| |  | | |____  | | \ \  __/ (_| | | | |  __/ (__| |_ 
 *  |_____|_|  |_|______| |_|  \_\___|\__,_|_|_|  \___|\___|\__|
 *
 * ----------------------------------------------------------------------
 * Loads a config json from outside webroot and makes a 3xx redirect
 * if the definition exists
 * ----------------------------------------------------------------------
 * 2020-05-11  v1.4  ah  rewrite as class
 * 2022-02-03  v1.5  ah  add method isEnabled
 * 2022-05-23  v1.6  ah  add http head check+render output; 
 * 2022-05-31  v1.7  ah  optical changes
 * 2023-08-28  v1.8  ah  remove php warning if there is no config yet
 * 2024-10-04  v1.9  ah  php8 only: typed variables
 * 2025-01-13  v1.10 ah  fetch curl error
 * 2025-01-20  v1.11 ah  Update infoblock for found redirects; handle and show cookies
 */

/**
 * Description of redirect
 *
 * @author axel
 */
class redirectadmin extends redirect
{

    /**
     * Filename to collect cookies of a request
     * @var string
     */
    protected string $_sCookiefile='';

    /**
     * Get default curl options
     * @return array
     */
    protected function _getCurlOptions(): array
    {
        $aReturn = [
            CURLOPT_HEADER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_USERAGENT => strip_tags($this->_app . ' ' . $this->_version),
            CURLOPT_VERBOSE => false,
            CURLOPT_ENCODING => 'gzip, deflate',  // to fetch encoding
            CURLOPT_HTTPHEADER => [
                'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
                'Accept-Language: en',
                'DNT: 1',
            ],

            CURLOPT_TIMEOUT => 5,
        ];
        return $aReturn;
    }

    /**
     * Make a single http(s) get request and return an array with response header, body, curl error info
     * @param string   $url          url to fetch
     * @param boolean  $bHeaderOnly  optional: true=make HEAD request; default: false (=GET)
     * @return array
     */
    public function httpGet(string $url, bool $bHeaderOnly = false): array
    {
        $aResult = [];
        $ch = curl_init($url);
        foreach ($this->_getCurlOptions() as $sCurlOption => $sCurlValue) {
            curl_setopt($ch, $sCurlOption, $sCurlValue);
        }

        // handle cookies
        $this->_sCookiefile = sys_get_temp_dir() . '/redirect_admin__found_cookies__' . md5($url) . '.txt';
        if(file_exists($this->_sCookiefile)){
            unlink($this->_sCookiefile);
        }
        curl_setopt($ch, CURLOPT_COOKIEFILE, $this->_sCookiefile);
        curl_setopt($ch, CURLOPT_COOKIEJAR, $this->_sCookiefile);

        if ($bHeaderOnly) {
            curl_setopt($ch, CURLOPT_HEADER, 1);
            curl_setopt($ch, CURLOPT_NOBODY, 1);
        }
        $res = curl_exec($ch);

        $sHeader = '';
        $sBody = '';

        if($bHeaderOnly){
            $sHeader = $res;
            $sBody = '';
        } else {
            $aResponse = explode("\r\n\r\n", $res, 2);
            list($sHeader, $sBody) = count($aResponse) > 1
            ? $aResponse
            : [$aResponse[0], ''];
        }

        $aResult = [
            'url' => $url,
            'response_header' => $sHeader,
            'response_body' => $sBody,
            // 'curlinfo' => curl_getinfo($ch),
            'curlerrorcode' => curl_errno($ch),
            'curlerrormsg' => curl_error($ch),
        ];

        curl_close($ch);
        return $aResult;
    }

    /**
     * Get html code to show found cookies.
     * The cookie file will be deleted too.
     * 
     * @return string
     */
    public function renderCookies(): string{
        $sReturn = '';
        $iCounter=0;
        if(file_exists($this->_sCookiefile))
        {
            $lines = explode(PHP_EOL, file_get_contents($this->_sCookiefile));
            foreach ($lines as $line) {
                if (substr_count($line, "\t") == 6) {
                    $iCounter++;
                    $sReturn.= $line . '<br>';
                }
            }

            unlink($this->_sCookiefile);
        }

        return $sReturn ? "Found cookies: $iCounter<pre>$sReturn</pre>" : '';
    }

    /**
     * Get html code for a response header of a request
     * 
     * @param array $aResponse  response array from httpGet() method
     * @return string
     */
    public function renderHttpResponseHeader(array $aResponse): string
    {
        $sHeader=$aResponse['response_header']."\r\n\r\n".$aResponse['response_body'];

        // $sReturn.="<pre>".print_r($aResponse, 1)."</pre>";
        $iJump = 0;
        $sBox = '';
        $sReturn = '';
        $aHosts = [];
        $aWebs = [];

        if ($aResponse['curlerrorcode']) {
            $sReturn .= '<br>'
                .'<span class="status status-error">Request failed.</span>'
                .'<pre>'
                .'Curl error #'.$aResponse['curlerrorcode'] .':<br>'
                . '<strong>'.$aResponse['curlerrormsg'].'</strong><br><br>'
                .'</pre>'
                .'🌐 <a href="https://curl.se/libcurl/c/libcurl-errors.html" target="_blank">Curl error codes</a>'
            ;
        } 

        $sUrl=$aResponse['url'];
        
        foreach(explode("\r\n\r\n",  $aResponse['response_header']."\r\n\r\n".$aResponse['response_body'] ) as $sBlock){
            if(strlen($sBlock)){
                $iJump++;

                // find http status
                preg_match('/HTTP\/.* ([0-9]*) /', $sBlock, $aTmp);
                // $sReturn.='<pre>'.print_r($aTmp, 1).'</pre>';
                $iStatus=$aTmp[1] ?? '0';

                $sStatus='';
                if($iStatus>=200 && $iStatus<300){
                    $sStatus='status-ok';
                } elseif($iStatus>=300 && $iStatus<400){
                    $sStatus='status-redirect';
                } elseif($iStatus>=400 && $iStatus<500){
                    $sStatus='status-error';
                } elseif($iStatus>=500 && $iStatus<600){
                    $sStatus='status-error';
                }

                // find location
                preg_match('/Location: (.*)/i', $sBlock, $aTmp);
                $sNextUrl=$aTmp[1] ?? '';


                // modify lines in block
                $sBlock = preg_replace('/(x-debug-.*)\\r/i', '<span class="debug">$1</span>', $sBlock);
                $sBlock = preg_replace('/(location:.*)\\r/i', '<span class="location">$1</span>', $sBlock);
    

                $sReturn.="<span class=\"status $sStatus\">$sUrl ... $iStatus</span><pre>$sBlock</pre>";
                // $sReturn .= '<strong>'.$iJump.') HTTP status: '.$iStatus.' - '.$sUrl.'</strong><pre>'.$sBlock.'</pre>';
                

                $sWebhost=preg_replace('/_$/', '', parse_url($sUrl, PHP_URL_HOST));
                $sIp=$this->_getIp($sWebhost);
                $aHosts[$sIp]=1;
                $aWebs[$sWebhost]=1;
                $sBox.="<div class=\"box $sStatus\">"
                    // .$sUrl.'<br>'
                    .$sWebhost.'<br>'
                    . $sIp
                    .'</div>';

                if($sNextUrl){
                    $sBox.= "<div class=\"redirectstatus\"><nobr> --- $iStatus ---&gt; </nobr></div>";
                    $sUrl=$sNextUrl;
                }

            }
        }
        $iHops = $iJump-1;
        $sReturn = '<br>'.($iHops > 0
            ? 'Found hops: <strong>' . $iHops . '</strong> '
            . ($iHops > 1 
                ? '<span class="warning"> ⚠️ Verify your redirect to skip unneeded hops.</span><br>'
                    .'The configured redirect is not the final url - it continues redirecting from there.'
                : ''
                )
            . '<br>'        
            .sprintf('Required webs to be online to reach the final url: <strong>%s</strong>; required number of hosts: <strong>%s</strong>', count($aWebs), count($aHosts)) 
            . '<br><br>'
            . '<div class="allJumps">'.$sBox.'</div>'
            : ''
        )
        . $this->renderCookies()
       
        . $sReturn;

        return $sReturn;
    }

    /**
     * Check if admin is enabled
     * 
     * @return bool
     */
    public function isEnabled(): bool
    {
        $sFile2Enable = __DIR__ . '/' . basename(__FILE__) . '_enabled.txt';
        return file_exists($sFile2Enable);
    }

    /**
     * Get ip address of a given hostname as ip (string) or false
     * @return string|bool
     */
    protected function _getIp(string $sHostname): string
    {
        $sIp = gethostbyname($sHostname);
        return $sIp === $sHostname ? '' : $sIp;
    }

    /**
     * Get an array with all config entries in all json files
     * including some error checking
     * 
     * @return array
     */
    public function getHosts(): array
    {
        $aReturn = [];
        $aErrors = [];
        foreach (glob($this->sConfigDir . '/redirects_*.json') as $sFilename) {
            $sMyHost = str_replace(['redirects_', '.json'], ['', ''], basename($sFilename));
            $aReturn[$sMyHost] = [
                'type' => 'config',
                'file' => $sFilename,
                'ip' => $this->_getIp($sMyHost),
                'aliases' => [],
                'redirects' => json_decode(file_get_contents($sFilename), 1),
            ];
            if (!$aReturn[$sMyHost]['ip']) {
                $aErrors[] = basename($sFilename) . ': The hostname was not found in DNS: ' . $sMyHost;
            }
        }
        $aAliases = $this->_getAliases();
        if (is_array($aAliases) && count($aAliases)) {
            foreach ($aAliases as $sAlias => $sConfig) {
                if (isset($aReturn[$sAlias])) {
                    $aErrors[] = "alias.json: A configuration for alias [$sAlias] is useless. There exists a file redirects_{$sAlias}.json (which has priority).";
                } else {
                    if (!isset($aReturn[$sConfig])) {
                        $aErrors[] = "alias.json: [$sAlias] points to a non existing host [$sConfig] - a file redirects_$sConfig.yml does not exist.";
                    } else {
                        $aReturn[$sConfig]['aliases'][] = $sAlias;
                        $aReturn[$sAlias] = [
                            'type' => 'alias',
                            'target' => $sConfig,
                            'ip' => $this->_getIp($sAlias),
                        ];
                        if (!$aReturn[$sAlias]['ip']) {
                            $aErrors[] = 'alias.json: The hostname was not found in DNS: ' . $sAlias;
                        }
                    }
                }
            }
        }
        $aReturn['_errors'] = $aErrors;
        ksort($aReturn);
        return $aReturn;
    }

    function getTitle(): string
    {
        return 'Redirect :: admin <small>'.$this->_version.'</small>';
    }
}