diff --git a/public_html/classes/redirect.admin.class.php b/public_html/classes/redirect.admin.class.php index 7513e18e30ed4e2157056a8b230c3cb51c91f146..d11fbe0ffcca843ff61d96ecb6247703e3833291 100644 --- a/public_html/classes/redirect.admin.class.php +++ b/public_html/classes/redirect.admin.class.php @@ -18,6 +18,7 @@ require_once 'redirect.class.php'; * 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-03 v1.9 ah php8 only: typed variables */ /** @@ -25,39 +26,46 @@ require_once 'redirect.class.php'; * * @author axel */ -class redirectadmin extends redirect { +class redirectadmin extends redirect +{ - protected function _getCurlOptions(){ - $aReturn=array( + /** + * 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->sAbout), CURLOPT_VERBOSE => false, CURLOPT_ENCODING => 'gzip, deflate', // to fetch encoding - CURLOPT_HTTPHEADER => array( + CURLOPT_HTTPHEADER => [ 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Accept-Language: en', 'DNT: 1', - ), + ], // TODO: this is unsafe .. better: let the user configure it CURLOPT_SSL_VERIFYHOST => false, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_TIMEOUT => 5, - ); + ]; return $aReturn; } /** - * make a single http(s) get request and return the response body + * Make a single http(s) get request and return the response body * @param string $url url to fetch * @param boolean $bHeaderOnly optional: true=make HEAD request; default: false (=GET) * @return string */ - public function httpGet($url, $bHeaderOnly = false) { + public function httpGet(string $url, bool $bHeaderOnly = false): bool|string + { $ch = curl_init($url); - foreach ($this->_getCurlOptions() as $sCurlOption=>$sCurlValue){ + foreach ($this->_getCurlOptions() as $sCurlOption => $sCurlValue) { curl_setopt($ch, $sCurlOption, $sCurlValue); } if ($bHeaderOnly) { @@ -69,113 +77,124 @@ class redirectadmin extends redirect { return ($res); } - public function renderHttpResponseHeader($sHeader){ - $sReturn=$sHeader; - if(!$sReturn){ - $sReturn='<pre><span class="status status-error">Request failed. </span><br>' - .'No data... no response.<br>' - .'Maybe ... ' - .'<ul>' - .'<li>the nostname does not exist ... </li>' - .'<li>or there is a network problem ... or </li>' - .'<li>the webservice on the target system does not run.</li>' - .'</ul>' - .'</pre>' + /** + * Get html code for a response header of a request + * + * @param string $sHeader + * @return string + */ + public function renderHttpResponseHeader(string $sHeader): string + { + $sReturn = $sHeader; + if (!$sReturn) { + $sReturn = '<pre><span class="status status-error">Request failed. </span><br>' + . 'No data... no response.<br>' + . 'Maybe ... ' + . '<ul>' + . '<li>the nostname does not exist ... </li>' + . '<li>or there is a network problem ... or </li>' + . '<li>the webservice on the target system does not run.</li>' + . '</ul>' + . '</pre>' ; } else { - $sReturn=preg_replace('/(HTTP.*)\\r/', '</pre><pre><strong>$1</strong>', $sReturn); - $sReturn=preg_replace('/(HTTP.*200.*)/', '<span class="status status-ok">$1</span>', $sReturn); - $sReturn=preg_replace('/(HTTP.*30.*)/', '<span class="status status-redirect">$1</span>', $sReturn); - $sReturn=preg_replace('/(HTTP.*40.*)/', '<span class="status status-error">$1</span>', $sReturn); - $sReturn=preg_replace('/(HTTP.*50.*)/', '<span class="status status-error">$1</span>', $sReturn); - $sReturn=preg_replace('/\ ([1-5][0-9][0-9])\ /', ' <span class="statuscode">$1</span> ', $sReturn); + $sReturn = preg_replace('/(HTTP.*)\\r/', '</pre><pre><strong>$1</strong>', $sReturn); + $sReturn = preg_replace('/(HTTP.*200.*)/', '<span class="status status-ok">$1</span>', $sReturn); + $sReturn = preg_replace('/(HTTP.*30.*)/', '<span class="status status-redirect">$1</span>', $sReturn); + $sReturn = preg_replace('/(HTTP.*40.*)/', '<span class="status status-error">$1</span>', $sReturn); + $sReturn = preg_replace('/(HTTP.*50.*)/', '<span class="status status-error">$1</span>', $sReturn); + $sReturn = preg_replace('/\ ([1-5][0-9][0-9])\ /', ' <span class="statuscode">$1</span> ', $sReturn); - $sReturn=preg_replace('/(x-debug-.*)\\r/i', '<span class="debug">$1</span>', $sReturn); - $sReturn=preg_replace('/(location:.*)\\r/i', '<span class="location">$1</span>', $sReturn); - $sReturn.='</pre>'; + $sReturn = preg_replace('/(x-debug-.*)\\r/i', '<span class="debug">$1</span>', $sReturn); + $sReturn = preg_replace('/(location:.*)\\r/i', '<span class="location">$1</span>', $sReturn); + $sReturn .= '</pre>'; preg_match_all('/(HTTP\/.*)/i', $sReturn, $aTmp); // $sReturn.=print_r($aTmp, 1); - $iHops=(count($aTmp[0])-1); + $iHops = (count($aTmp[0]) - 1); - $sReturn=($iHops>0 - ? 'Found hops: <strong>'.$iHops.'</strong>' - .($iHops>1 ? ' <span class="warning"> ⚠️ Verify your redirect to skip unneeded hops.</span>' : '' ).'<br><br>' + $sReturn = ($iHops > 0 + ? 'Found hops: <strong>' . $iHops . '</strong>' + . ($iHops > 1 ? ' <span class="warning"> ⚠️ Verify your redirect to skip unneeded hops.</span>' : '') . '<br><br>' : '' - ).$sReturn - ; + ) . $sReturn + ; } return $sReturn; } /** - * check if admin is enabled + * Check if admin is enabled + * * @return bool */ - public function isEnabled(){ - $sFile2Enable=__DIR__ . '/'.basename(__FILE__).'_enabled.txt'; + 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 + * Get ip address of a given hostname as ip (string) or false * @return string|bool */ - protected function _getIp($sHostname){ - $sIp=gethostbyname($sHostname); - return $sIp===$sHostname ? false : $sIp; + protected function _getIp(string $sHostname): string + { + $sIp = gethostbyname($sHostname); + return $sIp === $sHostname ? '' : $sIp; } /** - * get an array with all config entries in all json files + * Get an array with all config entries in all json files * including some error checking + * * @return array */ - public function getHosts(){ - $aReturn = array(); - $aErrors = array(); - foreach(glob($this->sConfigDir . '/redirects_*.json') as $sFilename){ - $sMyHost= str_replace(array('redirects_', '.json'), array('',''), basename($sFilename)); - $aReturn[$sMyHost]=array( - 'type'=>'config', - 'file'=>$sFilename, - 'ip'=> $this->_getIp($sMyHost), - 'aliases'=>array(), - 'redirects'=>json_decode(file_get_contents($sFilename), 1), - ); - if (!$aReturn[$sMyHost]['ip']){ - $aErrors[]=basename($sFilename).': The hostname was not found in DNS: '.$sMyHost; + 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)."; + $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."; + 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]=array( - 'type'=>'alias', - 'target'=>$sConfig, - 'ip'=> $this->_getIp($sAlias), - ); - if (!$aReturn[$sAlias]['ip']){ - $aErrors[]='alias.json: The hostname was not found in DNS: '.$sAlias; + $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; + $aReturn['_errors'] = $aErrors; ksort($aReturn); return $aReturn; } - } diff --git a/public_html/classes/redirect.class.php b/public_html/classes/redirect.class.php index c04108a2279046efa59d944753aa28b4d45c725b..168151820d4dfb956b28d09f83b9ced86a023f86 100644 --- a/public_html/classes/redirect.class.php +++ b/public_html/classes/redirect.class.php @@ -24,6 +24,7 @@ * 2020-05-06 v1.3 ah added aliases for multiple domains with the same config * 2020-05-06 v1.4 ah rewrite as class * 2023-08-28 v1.5 ah fix loop over config with missing regex section. + * 2024-10-03 v1.6 ah php8 only: typed variables */ /** @@ -31,86 +32,156 @@ * * @author axel */ -class redirect { +class redirect +{ // ---------------------------------------------------------------------- // CONFIG // ---------------------------------------------------------------------- - protected $bDebug = false; - protected $sConfigDir = __DIR__ . '/../../config'; - protected $sAbout = 'IML redirect <small>v1.4</small>'; + /** + * Flag: debug is enabled? + * @var bool + */ + protected bool $bDebug = false; + + /** + * configuration dir + * @var string + */ + protected string $sConfigDir = __DIR__ . '/../../config'; - protected $sHostname = false; - protected $sRequest = false; + /** + * About message + * @var string + */ + protected string $sAbout = 'IML redirect <small>v1.4</small>'; - protected $sCfgfile = false; - protected $aConfig = false; - protected $aRedirect = false; - - public $urlRepo='https://git-repo.iml.unibe.ch/iml-open-source/redirect-handler'; - public $urlDocs='https://os-docs.iml.unibe.ch/redirect-handler/'; + /** + * Hostname + * @var string + */ + protected string $sHostname = ''; + + /** + * Request to handle and to redirect to aconfigured target url + * @var string + */ + protected string $sRequest = ''; + + /** + * Config file + * @var + */ + protected string $sCfgfile = ''; + + /** + * Config data for the current hostname + * @var + */ + protected array $aConfig = []; + + /** + * Redirect data for the current request + * @var array + */ + protected array $aRedirect = []; + + /** + * URL to the repository of this project + * @var string + */ + public string $urlRepo = 'https://git-repo.iml.unibe.ch/iml-open-source/redirect-handler'; + + /** + * Url to the docs + * @var string + */ + public string $urlDocs = 'https://os-docs.iml.unibe.ch/redirect-handler/'; // ---------------------------------------------------------------------- // CONSTRUCTOR // ---------------------------------------------------------------------- - public function __constructor($sHostname=false, $sRequest=false) { - if($sHostname){ + + /** + * Constructor + * @param string $sHostname hostname + * @param string $sRequest request to proces + * @return void + */ + public function __constructor(string $sHostname = '', string $sRequest = '') + { + if ($sHostname) { $this->setHost($sHostname); } - if($sRequest){ + if ($sRequest) { $this->setRequest($sRequest); } - - return true; + } - + // ---------------------------------------------------------------------- // DEBUG // ---------------------------------------------------------------------- - + /** - * write a debug message into the header + * Write a debug message into the http response header + * * @global boolean $bDebug flag: show debug infos? * @staticvar int $i counter + * * @param string $sDebugMessage message to show * @return boolean */ - protected function _wd($sDebugMessage) { - + protected function _wd(string $sDebugMessage): bool + { + if (!$this->bDebug) { return false; } static $i; $i++; - header('X-DEBUG-' . $i . ': ' . $sDebugMessage); + header("X-DEBUG-$i: $sDebugMessage"); return true; } + // ---------------------------------------------------------------------- // SET INTERNAL VARS // ---------------------------------------------------------------------- + /** - * get a string with full path of a config file with aliases + * Get a string with full path of a config file with aliases * @return string */ - protected function _generateAliasfile() { + protected function _generateAliasfile(): string + { return $this->sConfigDir . '/aliases.json'; } + /** - * get a string with full path of a config file based on hostname - * @param string $sHostname + * Get a string with full path of a config file based on hostname + * + * @param string $sHostname hostname/ fqdn * @return string */ - protected function _generateCfgfile($sHostname) { - + protected function _generateCfgfile(string $sHostname): string + { + return $this->sConfigDir . '/redirects_' . $sHostname . '.json'; } - - protected function _getAliases(){ - $sAliasfile=$this->_generateAliasfile(); - $this->_wd('check alias file '.$sAliasfile); - if(!file_exists($sAliasfile)){ + + /** + * Get Aliases from aliases.json + * It returns false if no alias was found + * + * @return bool|array + */ + protected function _getAliases(): bool|array + { + $sAliasfile = $this->_generateAliasfile(); + $this->_wd('check alias file ' . $sAliasfile); + if (!file_exists($sAliasfile)) { $this->_wd('alias do not exist'); // $this->sendBody(500, '<h1>Internal Server Error</h1>Errorcode 01'); return false; @@ -120,49 +191,51 @@ class redirect { } /** - * get an array with redirect config based on a given hostname - * @param string $sHostname hostname - * @param boolean $bAbort flag: true = do not scan aliases (used for loop detection) - * @return array + * Get an array with redirect config based on a given hostname. + * @return bool */ - protected function _getConfig() { - $this->aConfig=false; - $this->aRedirect=false; + protected function _getConfig(): bool + { + $this->aConfig = []; + $this->aRedirect = []; $this->_getEffectiveConfigfile(); - if($this->sCfgfile){ - $aConfig=json_decode(file_get_contents($this->sCfgfile), 1); + if ($this->sCfgfile) { + $aConfig = json_decode(file_get_contents($this->sCfgfile), 1); if (!is_array($aConfig) || !count($aConfig)) { $this->_wd('no config available'); // $this->sendBody(500, '<h1>Internal Server Error</h1>Errorcode 02'); + } else { + $this->aConfig = $aConfig; } - $this->aConfig=$aConfig; } return true; } - + /** - * get an array with redirect config based on a given hostname + * Get filename of config file based on a given hostname * or detection in the alias config + * * @param string $sHostname hostname * @param boolean $bAbort flag: true = do not scan aliases (used for loop detection) - * @return array + * @return bool|string */ - protected function _getEffectiveConfigfile($sHostname=false, $bAbort=false) { - if(!$sHostname){ - $sHostname=$this->sHostname; + protected function _getEffectiveConfigfile(string $sHostname = '', bool $bAbort = false): bool|string + { + if (!$sHostname) { + $sHostname = $this->sHostname; } - $this->sCfgfile=false; + $this->sCfgfile = false; $sCfgfile = $this->_generateCfgfile($sHostname); - $this->_wd('check config '.$sCfgfile); + $this->_wd('check config ' . $sCfgfile); if (!file_exists($sCfgfile)) { - if($bAbort){ + if ($bAbort) { // $this->sendBody(500, '<h1>Internal Server Error</h1>Errorcode 01'); return false; } $aAliases = $this->_getAliases(); - $this->_wd('checking aliases '.print_r($aAliases,1)); - if (!isset($aAliases[$sHostname]) || !file_exists($this->_generateCfgfile($aAliases[$sHostname]))){ - $this->_wd('sorry no valid alias for '. $sHostname); + $this->_wd('checking aliases ' . print_r($aAliases, 1)); + if (!isset($aAliases[$sHostname]) || !file_exists($this->_generateCfgfile($aAliases[$sHostname]))) { + $this->_wd('sorry no valid alias for ' . $sHostname); // $this->sendBody(500, '<h1>Internal Server Error</h1>Errorcode 01'); return false; } @@ -170,39 +243,42 @@ class redirect { return $this->_getEffectiveConfigfile($aAliases[$this->sHostname], 1); } - $this->sCfgfile=$sCfgfile; + $this->sCfgfile = $sCfgfile; return $sCfgfile; } - - + + /** - * enable/ disable debug + * Enable/ disable debug * @param boolean $bEnable */ - public function setDebug($bEnable){ - $this->bDebug=!!$bEnable; + public function setDebug(bool $bEnable): bool + { + $this->bDebug = !!$bEnable; return true; } - + /** - * set hostname; internally it detects the config too - * @param type $sHostname + * Set hostname; internally it detects the config too + * @param string $sHostname * @return boolean */ - public function setHost($sHostname){ - $this->sHostname=$sHostname; + public function setHost(string $sHostname): bool + { + $this->sHostname = $sHostname; $this->_getConfig(); return true; } - + /** - * set the request - * @param type $sRequest + * Set the request + * @param string $sRequest * @return boolean */ - public function setRequest($sRequest){ - $this->sRequest=$sRequest; - $this->aRedirect=false; + public function setRequest(string $sRequest): bool + { + $this->sRequest = $sRequest; + $this->aRedirect = false; return true; } @@ -211,26 +287,28 @@ class redirect { // ---------------------------------------------------------------------- /** - * get an array with the matching redirect; it returns false if none was + * Get an array with the matching redirect; it returns false if none was * detected + * * @return array */ - public function getRedirect(){ - if(is_array($this->aRedirect)){ + public function getRedirect(): array + { + if (is_array($this->aRedirect)) { return $this->aRedirect; } - - $aRedirect = false; - + + $aRedirect = []; + // remark: // $this->aConfig is set in setHost() with $this->_getConfig(); - + if (isset($this->aConfig['direct'][$this->sRequest])) { $this->_wd("DIRECT MATCH"); $aRedirect = $this->aConfig['direct'][$this->sRequest]; } else { $this->_wd("no direct match ... scanning regex"); - if(isset($this->aConfig['regex']) && is_array($this->aConfig['regex'])){ + if (isset($this->aConfig['regex']) && is_array($this->aConfig['regex'])) { foreach (array_keys($this->aConfig['regex']) as $sRegex) { $this->_wd("check if regex [$sRegex] matches $this->sRequest"); if (preg_match('#' . $sRegex . '#', $this->sRequest)) { @@ -241,21 +319,32 @@ class redirect { } } } - $this->aRedirect=$aRedirect; + $this->aRedirect = $aRedirect; return $aRedirect; } - - public function getRedirectCode($iNone=false){ - $aRedirect=$this->getRedirect(); + + /** + * Get 30x redirect code + * @param integer $iNone fallback value if no redirect status code was found + * @return integer + */ + public function getRedirectCode($iNone = 307): int + { + $aRedirect = $this->getRedirect(); return isset($aRedirect['code']) ? $aRedirect['code'] : $iNone; } - public function getRedirectTarget(){ - $aRedirect=$this->getRedirect(); - return isset($aRedirect['target']) ? $aRedirect['target'] : false; + + /** + * Get Target url of redirect + * @return string + */ + public function getRedirectTarget(): string + { + $aRedirect = $this->getRedirect(); + return $aRedirect['target'] ?? ''; } - - + // ---------------------------------------------------------------------- // FUNCTIONS - SEND DATA // ---------------------------------------------------------------------- @@ -264,27 +353,29 @@ class redirect { * make the redirect if it was found ... or a 404 * @return true */ - public function makeRedirect(){ - $sTarget=$this->getRedirectTarget(); - if(!$sTarget){ + public function makeRedirect(): bool + { + $sTarget = $this->getRedirectTarget(); + if (!$sTarget) { $this->_wd('send a not found'); $this->sendBody(404, '<h1>404 Not found</h1>'); } else { - $iCode=$this->getRedirectCode(); - $this->_wd("Redirect with http status [" . $iCode . "] to new Location: " . $sTarget); + $iCode = $this->getRedirectCode(); + $this->_wd("Redirect with http status [$iCode] to new Location: $sTarget"); $this->sendHttpStatusheader($iCode); header("Location: " . $sTarget); } return true; } - + /** * send http status header * @param integer $iCode http code * @return boolean */ - public function sendHttpStatusheader($iCode) { - $aHeaders = array( + public function sendHttpStatusheader(int $iCode): bool + { + $aHeaders = [ 301 => 'Moved Permanently', 302 => 'Found', // (Moved Temporarily) 303 => 'See other', // redirect with GET @@ -293,7 +384,7 @@ class redirect { 404 => 'Not found', 410 => 'Gone', 500 => 'Internal Server Error', - ); + ]; $iCode = (int) $iCode; if (!isset($aHeaders[$iCode])) { @@ -308,28 +399,29 @@ class redirect { * @see sendHttpStatusheader() * * @param integer $iCode http status code - * @param stringg $sBody message text as html code + * @param string $sBody message text as html code + * @return void */ - public function sendBody($iCode, $sBody) { + public function sendBody(int $iCode, string $sBody): void + { $this->sendHttpStatusheader($iCode); die('<!doctype html><html><head>' - . '<title>Redirect</title>' - . '<style>' - . 'body{background:#eee; background: linear-gradient(-10deg,#ccc,#eee,#ddd) fixed; color:#444; font-family: verdana,arial;}' - . 'h1{color:#a44;font-size: 300%;border-bottom: 1px solid #fff;}' - . 'h2{color:#ccc; color: rgba(0,0,0,0.1); font-size: 300%; position: absolute; right: 1em; bottom: 1em; text-align: right;}' - . 'h2 small{font-size: 50%;}' - . 'footer{background:#ccc; bottom: 1em; color:#666; position: absolute; padding: 1em; right: 1em; }' - . '</style>' - . '</head>' - . '<body>' - . $sBody - . '<h2>' . $this->sAbout . '</h2>' - . '<footer>© ' . date('Y') . ' '.$this->sHostname.'</footer>' - . '</body></html>' + . '<title>Redirect</title>' + . '<style>' + . 'body{background:#eee; background: linear-gradient(-10deg,#ccc,#eee,#ddd) fixed; color:#444; font-family: verdana,arial;}' + . 'h1{color:#a44;font-size: 300%;border-bottom: 1px solid #fff;}' + . 'h2{color:#ccc; color: rgba(0,0,0,0.1); font-size: 300%; position: absolute; right: 1em; bottom: 1em; text-align: right;}' + . 'h2 small{font-size: 50%;}' + . 'footer{background:#ccc; bottom: 1em; color:#666; position: absolute; padding: 1em; right: 1em; }' + . '</style>' + . '</head>' + . '<body>' + . $sBody + . '<h2>' . $this->sAbout . '</h2>' + . '<footer>© ' . date('Y') . ' ' . $this->sHostname . '</footer>' + . '</body></html>' ); } - }