Skip to content
Snippets Groups Projects
Select Git revision
  • 886b80aa22feb3699204e4da0215fba12ab52f28
  • master default protected
  • handle-cookies
  • add-docker
4 results

redirect.class.php

Blame
  • 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>&copy; ' . date('Y') . ' ' . $this->sHostname . '</footer>'
                . '</body></html>'
            );
        }
    
    }