Select Git revision
redirect.class.php
-
Hahn Axel (hahn) authoredHahn Axel (hahn) authored
redirect.class.php 13.07 KiB
<?php
/**
* ----------------------------------------------------------------------
* _____ __ __ _ _____ _ _ _
* |_ _| \/ | | | __ \ | (_) | |
* | | | \ / | | | |__) |___ __| |_ _ __ ___ ___| |_
* | | | |\/| | | | _ // _ \/ _` | | '__/ _ \/ __| __|
* _| |_| | | | |____ | | \ \ __/ (_| | | | | __/ (__| |_
* |_____|_| |_|______| |_| \_\___|\__,_|_|_| \___|\___|\__|
*
* ----------------------------------------------------------------------
* Loads a config json from outside webroot and makes a 3xx redirect
* if the definition exists
* ----------------------------------------------------------------------
* errorcodes:
* - code 500 - no config or statuscode is not supported
* - code 404 - the redirect does not exist in the json file or is
* incomplete (no target, no http status code)
* ----------------------------------------------------------------------
* ah=<axel.hahn@iml.unibe.ch>
* 2019-04-23 v1.0 ah first version (without tracking)
* 2019-04-25 v1.1 ah added regex handling; added GET param "debugredirect"
* 2019-04-25 v1.2 ah use REQUEST_URI (works on Win and Linux)
* 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-04 v1.6 ah php8 only: typed variables
*/
/**
* Description of redirect
*
* @author axel
*/
class redirect
{
// ----------------------------------------------------------------------
// CONFIG
// ----------------------------------------------------------------------
/**
* About message
* @var string
*/
protected string $_version = '1.7';
/**
* Flag: debug is enabled?
* @var bool
*/
protected bool $bDebug = false;
/**
* configuration dir
* @var string
*/
protected string $sConfigDir = __DIR__ . '/../../config';
/**
* About message
* @var string
*/
protected string $_app = 'IML redirect';
/**
* 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
// ----------------------------------------------------------------------
/**
* 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) {
$this->setRequest($sRequest);
}
}
// ----------------------------------------------------------------------
// DEBUG
// ----------------------------------------------------------------------
/**
* 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(string $sDebugMessage): bool
{
if (!$this->bDebug) {
return false;
}
static $i;
$i++;
header("X-DEBUG-$i: $sDebugMessage");
return true;
}
// ----------------------------------------------------------------------
// SET INTERNAL VARS
// ----------------------------------------------------------------------
/**
* Get a string with full path of a config file with aliases
* @return string
*/
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 hostname/ fqdn
* @return string
*/
protected function _generateCfgfile(string $sHostname): string
{
return $this->sConfigDir . '/redirects_' . $sHostname . '.json';
}
/**
* 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;
}
$aAliases = json_decode(file_get_contents($sAliasfile), 1);
return $aAliases;
}
/**
* Get an array with redirect config based on a given hostname.
* @return bool
*/
protected function _getConfig(): bool
{
$this->aConfig = [];
$this->aRedirect = [];
$this->_getEffectiveConfigfile();
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;
}
}
return true;
}
/**
* 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 bool|string
*/
protected function _getEffectiveConfigfile(string $sHostname = '', bool $bAbort = false): bool|string
{
if (!$sHostname) {
$sHostname = $this->sHostname;
}
$this->sCfgfile = false;
$sCfgfile = $this->_generateCfgfile($sHostname);
$this->_wd('check config ' . $sCfgfile);
if (!file_exists($sCfgfile)) {
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->sendBody(500, '<h1>Internal Server Error</h1>Errorcode 01');
return false;
}
// remark: with abort flag
return $this->_getEffectiveConfigfile($aAliases[$this->sHostname], 1);
}
$this->sCfgfile = $sCfgfile;
return $sCfgfile;
}
/**
* Enable/ disable debug
* @param boolean $bEnable
*/
public function setDebug(bool $bEnable): bool
{
$this->bDebug = !!$bEnable;
return true;
}
/**
* Set hostname; internally it detects the config too
* @param string $sHostname
* @return boolean
*/
public function setHost(string $sHostname): bool
{
$this->sHostname = $sHostname;
$this->_getConfig();
return true;
}
/**
* Set the request
* @param string $sRequest
* @return boolean
*/
public function setRequest(string $sRequest): bool
{
$this->sRequest = $sRequest;
$this->aRedirect = [];
return true;
}
// ----------------------------------------------------------------------
// FUNCTIONS - GET REDIRECT
// ----------------------------------------------------------------------
/**
* Get an array with the matching redirect; it returns false if none was
* detected
*
* @return array
*/
public function getRedirect(): array
{
if (is_array($this->aRedirect) && count($this->aRedirect)) {
return $this->aRedirect;
}
$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'])) {
foreach (array_keys($this->aConfig['regex']) as $sRegex) {
$this->_wd("check if regex [$sRegex] matches $this->sRequest");
if (preg_match('#' . $sRegex . '#', $this->sRequest)) {
$this->_wd("REGEX MATCH! aborting tests");
$aRedirect = $this->aConfig['regex'][$sRegex];
break;
}
}
}
}
$this->aRedirect = $aRedirect;
return $aRedirect;
}
/**
* 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;
}
/**
* Get Target url of redirect
* @return string
*/
public function getRedirectTarget(): string
{
$aRedirect = $this->getRedirect();
return isset($aRedirect['target']) ? $aRedirect['target'] : false;
}
// ----------------------------------------------------------------------
// FUNCTIONS - SEND DATA
// ----------------------------------------------------------------------
/**
* make the redirect if it was found ... or a 404
* @return true
*/
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");
$this->sendHttpStatusheader($iCode);
header("Location: " . $sTarget);
}
return true;
}
/**
* send http status header
* @param integer $iCode http code
* @return boolean
*/
public function sendHttpStatusheader(int $iCode): bool
{
$aHeaders = [
301 => 'Moved Permanently',
302 => 'Found', // (Moved Temporarily)
303 => 'See other', // redirect with GET
307 => 'Temporary Redirect',
308 => 'Permanent Redirect',
404 => 'Not found',
410 => 'Gone',
500 => 'Internal Server Error',
];
$iCode = (int) $iCode;
if (!isset($aHeaders[$iCode])) {
$this->sendBody(500, '<h1>Internal Server Error</h1>Errorcode 03: ' . $iCode);
}
header("HTTP/1.0 $iCode " . $aHeaders[$iCode]);
return true;
}
/**
* send http header for given status code, show document and exit
* @see sendHttpStatusheader()
*
* @param integer $iCode http status code
* @param string $sBody message text as html code
* @return void
*/
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->_app . ' <small>'.$this->_version.'</small></h2>'
. '<footer>© ' . date('Y') . ' ' . $this->sHostname . '</footer>'
. '</body></html>'
);
}
}