yf_redirect class

Redirects handler.

Fileclasses/yf_redirect.class.php
Use_class('redirect')

Public properties

A site sets them in its config with $PROJECT_CONF['redirect']['NAME'] = ...;

PropertyDefaultDescription
$ALLOWED_HOSTS
array
conf('DOMAINS') plus WEB_DOMAIN */ Property [ public $ALLOWED_HOSTS = [] Allowed target hosts; empty = conf('DOMAINS') plus WEB_DOMAIN
$AVAIL_TYPES
array
['html', 'js', 'http', 'refresh', 'hybrid'] @conf_skip Available redirect types
$FORCE_TYPE
string
'http' Force using only this method (if text is empty), leave blank to disable
$JS_SHOW_TEXT
bool
false
$LOG_REDIRECTS
bool
true
$LOOP_COUNT
bool
3
$LOOP_DEFENCE
bool
true
$LOOP_EXCLUDE_SOURCE
array
['~^/[a-z0-9_]+/filter_save/~i', '~&action=filter_save&~i'] of url patterns to exclude from defence
$LOOP_EXCLUDE_TARGET
array
[] of url patterns to exclude from defence
$LOOP_KEEP_LAST
bool
10
$LOOP_TTL
bool
5
$RESTRICT_HOSTS
bool
false Redirect to ./ when the target is an external host not listed in ALLOWED_HOSTS (open redirect protection)
$USE_DESIGN
bool
false

Public methods 12

None without a leading underscore.

Methods starting with _ hooks and helpers; not routable as page actions

__call

public function __call($name, $args)

Catch missing method call.

    public function __call($name, $args)
    {
        return main()->extend_call($this, $name, $args);
    }
public function _allowed_hosts()

ALLOWED_HOSTS plus conf('DOMAINS'), normalised to bare lower-case hosts; WEB_DOMAIN (derived from the Host header) is trusted only when no DOMAINS are configured.

    public function _allowed_hosts()
    {
        $domains = array_filter((array) conf('DOMAINS'), 'is_string');
        $items = array_merge((array) $this->ALLOWED_HOSTS, $domains, ( ! $domains && defined('WEB_DOMAIN')) ? [WEB_DOMAIN] : []);
        $result = [];
        foreach ($items as $item) {
            $item = trim((string) $item);
            if (strpos($item, '://') !== false) {
                $item = (string) parse_url($item, PHP_URL_HOST);
            }
            $item = rtrim(strtolower(preg_replace('/:\d+$/', '', $item)), '.');
            $item !== '' && $result[] = $item;
        }
        return $result;
    }

_go

public function _go($location, $rewrite = true, $redirect_type = 'hybrid', $text = '', $ttl = 3, $params = [])

Common redirect method.

    public function _go($location, $rewrite = true, $redirect_type = 'hybrid', $text = '', $ttl = 3, $params = [])
    {
        if (is_array($location)) {
            $params += $location;
            $rewrite = isset($params['rewrite']) ? $params['rewrite'] : $rewrite;
            $redirect_type = isset($params['type']) ? $params['type'] : (isset($params['redirect_type']) ? $params['redirect_type'] : $redirect_type);
            $text = isset($params['text']) ? $params['text'] : $text;
            $ttl = isset($params['ttl']) ? $params['ttl'] : $ttl;
            $location = isset($params['location']) ? $params['location'] : $params['url'];
        }
        // Avoid rewriting by mistake
        if (substr($location, 0, 1) === '/' && substr($location, 0, 2) !== '//' && $rewrite) {
            $location = url($location);
        }
        if (strpos($location, 'http://') === 0 || strpos($location, 'https://') === 0) {
            $rewrite = false;
        }
        $form_method = in_array(strtoupper($params['form_method'] ?? ''), ['GET', 'POST']) ? strtoupper($params['form_method']) : 'GET';
        if ($GLOBALS['no_redirect'] ?? false) {
            return $text;
        }
        if (main()->_IS_REDIRECTING) {
            return false;
        }
        main()->_IS_REDIRECTING = true;
        if (empty($location)) {
            $location = './?object=' . ($_GET['object'] ?? null)
                . (($_GET['action'] ?? null) != 'show' ? '&action=' . ($_GET['action'] ?? null) : '')
                . (($_GET['id'] ?? null) ? '&id=' . ($_GET['id'] ?? null) : '')
                . (($_GET['page'] ?? null) ? '&page=' . ($_GET['page'] ?? null) : '');
        }
        if ($rewrite && tpl()->REWRITE_MODE && MAIN_TYPE_USER) {
            $location = _class('rewrite')->_rewrite_replace_links($location, true);
        }
        $location = str_replace('??', '?', $location);
        if ($location == './?') {
            $location = './';
        }
        if ($this->RESTRICT_HOSTS && ! $this->_is_allowed_location($location)) {
            $this->_save_log([
                'url_to' => $location,
                'reason' => 'blocked by RESTRICT_HOSTS',
                'rewrite' => $rewrite,
                'ttl' => $ttl,
                'type' => $redirect_type,
            ]);
            $location = './';
        }
        // Exec hook before redirecting
        $hook_name = '_on_before_redirect';
        $obj = module(($_GET['object'] ?? null));
        if (method_exists($obj, $hook_name)) {
            $obj->$hook_name($text);
        }
        $loop_detected = null;
        if ($this->LOOP_DEFENCE) {
            $loop_detected = $this->_loop_detected($location);
            if ($loop_detected && ! DEBUG_MODE) {
                common()->message_error('Internal error: redirect loop detected. Please contact support');
                return false;
            }
        }
        $body = '';
        main()->NO_GRAPHICS = true;
        if (DEBUG_MODE) {
            $hidden_fields = '';
            if ($form_method == 'GET') {
                $query = parse_url($location, PHP_URL_QUERY);
                $fields = [];
                if ($query && strlen($query)) {
                    parse_str($query, $fields);
                }
                if ($fields) {
                    $tmp = [];
                    // Fix for fields sub-arrays
                    foreach ($fields as $k => $v) {
                        if (is_array($v)) {
                            foreach ($v as $k1 => $v1) {
                                if (is_array($v1)) {
                                    foreach ($v1 as $k2 => $v2) {
                                        $tmp[$k . '[' . $k1 . '][' . $k2 . ']'] = $v2;
                                    }
                                } else {
                                    $tmp[$k . '[' . $k1 . ']'] = $v1;
                                }
                            }
                        } else {
                            $tmp[$k] = $v;
                        }
                    }
                    $fields = $tmp;
                    unset($tmp);
                    $form = form($fields, ['no_form' => 1]);
                    foreach ((array) $fields as $k => $v) {
                        $form->hidden($k);
                    }
                    $hidden_fields = $form;
                }
            }
            if (in_array($redirect_type, ['http', 'hybrid', 'refresh', '301', '302'])) {
                $ttl = 0;
            }
            $body .= tpl()->parse('system/redirect', [
                'mode' => 'debug',
                'normal_mode' => $redirect_type,
                'rewrite' => (int) ((bool) $rewrite),
                'location' => $location,
                'text' => $text,
                'ttl' => (int) $ttl,
                'form_method' => $form_method,
                'hidden_fields' => $hidden_fields,
            ]);
            if ($loop_detected) {
                $body .= '<b style="color:red">Redirect loop detected!</b>';
            }
            $body .= '<pre><small>' . htmlspecialchars(main()->trace_string()) . '</small></pre>';
            if ($this->FORCE_TYPE) {
                $redirect_type = $this->FORCE_TYPE;
            }
            $this->_save_log([
                'url_to' => $location,
                'reason' => $text,
                'rewrite' => $rewrite,
                'ttl' => $ttl,
                'type' => $redirect_type,
            ]);
            return print common()->show_empty_page($body, ['full_width' => 1]);
        }
        if (empty($redirect_type) || ! in_array($redirect_type, $this->AVAIL_TYPES)) {
            $redirect_type = 'hybrid';
        }
        if ($this->FORCE_TYPE) {
            $redirect_type = $this->FORCE_TYPE;
        }
        if ($redirect_type == 'js') {
            $body = $this->_redirect_js($location, $text, $ttl, $params);
        } elseif ($redirect_type == 'html') {
            $body = $this->_redirect_html($location, $text, $ttl, $params);
        } elseif ($redirect_type == 'http' || $redirect_type == '302') {
            $body = $this->_redirect_http_302($location, $text, $ttl, $params);
        } elseif ($redirect_type == '301') {
            $body = $this->_redirect_http_301($location, $text, $ttl, $params);
        } elseif ($redirect_type == 'refresh') {
            $body = $this->_redirect_http_refresh($location, $text, $ttl, $params);
        } elseif ($redirect_type == 'hybrid') {
            $body = $this->_redirect_hybrid($location, $text, $ttl, $params);
        }
        $this->_save_log([
            'url_to' => $location,
            'reason' => $text,
            'rewrite' => $rewrite,
            'ttl' => $ttl,
            'type' => $redirect_type,
        ]);
        echo $this->USE_DESIGN && ! empty($body) ? common()->show_empty_page($body, ['full_width' => 1]) : $body;
    }
public function _is_allowed_location($location)

Check that redirect target is relative or points to one of allowed hosts.

    public function _is_allowed_location($location)
    {
        $location = (string) $location;
        if (preg_match('/[\x00-\x1F\x7F]/', $location) || strpos($location, '\\') !== false) {
            return false;
        }
        // browsers drop leading blanks, so " //evil" is "//evil"
        $location = ltrim($location, " \t");
        if (substr($location, 0, 2) === '//') {
            return false;
        }
        if ( ! preg_match('~^([a-z][a-z0-9+.\-]*):~i', $location, $m)) {
            return true;
        }
        if ( ! in_array(strtolower($m[1]), ['http', 'https'])) {
            return false;
        }
        $host = parse_url($location, PHP_URL_HOST);
        if ( ! is_string($host) || $host === '') {
            return false;
        }
        $host = rtrim(strtolower($host), '.');
        foreach ($this->_allowed_hosts() as $item) {
            if ($item === $host) {
                return true;
            }
        }
        return false;
    }
public function _loop_detected($cur_url)
    public function _loop_detected($cur_url)
    {
        if ( ! $this->LOOP_DEFENCE) {
            return false;
        }
        $cur_time = time();
        $loop_var = &$_SESSION['last_redirect_log'];
        if ( ! is_array($loop_var)) {
            $loop_var = [];
        }
        $loop_var[] = [
            'time' => $cur_time,
            'url' => $cur_url,
        ];
        $detected = false;
        $loop_count = $this->LOOP_COUNT;
        $detect_slice = array_reverse(array_slice((array) $loop_var, -$loop_count, $loop_count, true));
        if (count((array) $detect_slice) < $this->LOOP_COUNT) {
            return false;
        }
        $exclude_source = $this->LOOP_EXCLUDE_SOURCE;
        if ($exclude_source && ! is_array($exclude_source)) {
            $exclude_source = [$exclude_source];
        }
        // Check current/source page for exclude patterns
        $url_path_and_query = ($_SERVER['REQUEST_URI'] ?? null) . (strlen(($_SERVER['QUERY_STRING'] ?? '')) ? '?' . ($_SERVER['QUERY_STRING'] ?? null) : '');
        foreach ((array) $exclude_source as $pattern) {
            if (preg_match($pattern, $url_path_and_query)) {
                return false;
            }
        }
        // Check redirect target page for exclude patterns
        $exclude_target = $this->LOOP_EXCLUDE_TARGET;
        if ($exclude_target && ! is_array($exclude_target)) {
            $exclude_target = [$exclude_target];
        }
        $counter = 0;
        foreach ((array) $detect_slice as $v) {
            $time = $v['time'];
            $url = $v['url'];
            if ($time < ($cur_time - $this->LOOP_TTL)) {
                break;
            }
            if ($v['url'] != $cur_url) {
                break;
            }
            if ($exclude_target) {
                $u = parse_url($v['url']);
                $url_path_and_query = $u['path'] . (strlen($u['query']) ? '?' . $u['query'] : '');
                foreach ((array) $exclude_target as $pattern) {
                    if (preg_match($pattern, $url_path_and_query)) {
                        break 2;
                    }
                }
            }
            $counter++;
        }
        if ($counter >= $this->LOOP_COUNT) {
            $detected = true;
        }
        // Keep only last 10 redirects log
        $keep_last = $this->LOOP_KEEP_LAST;
        $loop_var = array_slice($loop_var, -$keep_last, $keep_last, true);

        return $detected;
    }
public function _redirect_html($location, $text = '', $ttl = 0, $params = [])

HTML redirect method.

    public function _redirect_html($location, $text = '', $ttl = 0, $params = [])
    {
        return tpl()->parse('system/redirect', [
            'mode' => 'html',
            'location' => $location,
            'text' => $text,
            'ttl' => (int) $ttl,
            'form_method' => $params['form_method'],
        ]);
    }
public function _redirect_http_301($location, $text = '', $ttl = 0, $params = [])
    public function _redirect_http_301($location, $text = '', $ttl = 0, $params = [])
    {
        header((($_SERVER['SERVER_PROTOCOL'] ?? null) ? ($_SERVER['SERVER_PROTOCOL'] ?? null) : 'HTTP/1.1') . ' 301 Moved Permanently');
        header('Location: ' . $location);
        return '';
    }
public function _redirect_http_302($location, $text = '', $ttl = 0, $params = [])
    public function _redirect_http_302($location, $text = '', $ttl = 0, $params = [])
    {
        header((($_SERVER['SERVER_PROTOCOL'] ?? null) ? ($_SERVER['SERVER_PROTOCOL'] ?? null) : 'HTTP/1.1') . ' 302 Found');
        header('Location: ' . $location);
        return '';
    }
public function _redirect_http_refresh($location, $text = '', $ttl = 0, $params = [])
    public function _redirect_http_refresh($location, $text = '', $ttl = 0, $params = [])
    {
        header((($_SERVER['SERVER_PROTOCOL'] ?? null) ? ($_SERVER['SERVER_PROTOCOL'] ?? null) : 'HTTP/1.1') . ' 302 Found');
        header('Refresh: ' . $ttl . '; url=' . $location);
        return '';
    }
public function _redirect_hybrid($location, $text = '', $ttl = 0, $params = [])

Hybrid redirect method (HTTP refresh + JS).

    public function _redirect_hybrid($location, $text = '', $ttl = 0, $params = [])
    {
        $this->_redirect_http_refresh($location, $text, $ttl, $params);
        return tpl()->parse('system/redirect', [
            'mode' => 'hybrid',
            'location' => $location,
            'text' => $text,
            'ttl' => (int) $ttl,
            'form_method' => $params['form_method'],
        ]);
    }

_redirect_js

public function _redirect_js($location, $text = '', $ttl = 0, $params = [])

JavaScript redirect method (with 'degrade gracefully' feature).

    public function _redirect_js($location, $text = '', $ttl = 0, $params = [])
    {
        return tpl()->parse('system/redirect', [
            'mode' => 'js',
            'location' => $location,
            'text' => $text,
            'ttl' => (int) $ttl,
            'html_redirect' => $this->_redirect_html($location, $text, $ttl),
            'js_show_text' => (int) ((bool) $this->JS_SHOW_TEXT),
            'form_method' => $params['form_method'],
        ]);
    }

_save_log

public function _save_log($params = [])
    public function _save_log($params = [])
    {
        if ( ! $this->LOG_REDIRECTS) {
            return false;
        }
        // slice 2 first elements (__FUNCTION__ and $this->_go) and leave only 5 more trace elements to save space
        $trace = implode(PHP_EOL, array_slice(explode(PHP_EOL, main()->trace_string()), 2, 7));

        $is_https = (($_SERVER['HTTPS'] ?? null) || ($_SERVER['SSL_PROTOCOL'] ?? null));

        return db()->insert_safe('log_redirects', [
            'url_from' => ($is_https ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? null) . ($_SERVER['REQUEST_URI'] ?? null),
            'url_to' => $params['url_to'],
            'reason' => $params['reason'],
            'use_rewrite' => (int) $params['rewrite'],
            'redirect_type' => $params['type'],
            'date' => gmdate('Y-m-d H:i:s'),
            'ip' => common()->get_ip(),
            'query_string' => ($_SERVER['QUERY_STRING'] ?? null),
            'user_agent' => ($_SERVER['HTTP_USER_AGENT'] ?? null),
            'referer' => ($_SERVER['HTTP_REFERER'] ?? null),
            'object' => ($_GET['object'] ?? null),
            'action' => ($_GET['action'] ?? null),
            'user_id' => MAIN_TYPE_ADMIN ? main()->ADMIN_ID : main()->USER_ID,
            'user_group' => MAIN_TYPE_ADMIN ? main()->ADMIN_GROUP : main()->USER_GROUP,
            'site_id' => (int) main()->SITE_ID,
            'server_id' => (int) main()->SERVER_ID,
            'locale' => conf('language'),
            'is_admin' => MAIN_TYPE_ADMIN ? 1 : 0,
            'rewrite_mode' => (int) tpl()->REWRITE_MODE,
            'debug_mode' => DEBUG_MODE ? 1 : 0,
            'exec_time' => str_replace(',', '.', round(microtime(true) - main()->_time_start, 4)),
            'trace' => $trace,
        ]);
    }