yf_redirect classRedirects handler.
| File | classes/yf_redirect.class.php |
|---|---|
| Use | _class('redirect') |
A site sets them in its config with $PROJECT_CONF['redirect']['NAME'] = ...;
| Property | Default | Description |
|---|---|---|
$ALLOWED_HOSTSarray |
conf('DOMAINS') plus WEB_DOMAIN */
Property [ public $ALLOWED_HOSTS = [] |
Allowed target hosts; empty = conf('DOMAINS') plus WEB_DOMAIN |
$AVAIL_TYPESarray |
['html', 'js', 'http', 'refresh', 'hybrid'] |
@conf_skip Available redirect types |
$FORCE_TYPEstring |
'http' |
Force using only this method (if text is empty), leave blank to disable |
$JS_SHOW_TEXTbool |
false |
|
$LOG_REDIRECTSbool |
true |
|
$LOOP_COUNTbool |
3 |
|
$LOOP_DEFENCEbool |
true |
|
$LOOP_EXCLUDE_SOURCEarray |
['~^/[a-z0-9_]+/filter_save/~i', '~&action=filter_save&~i'] |
of url patterns to exclude from defence |
$LOOP_EXCLUDE_TARGETarray |
[] |
of url patterns to exclude from defence |
$LOOP_KEEP_LASTbool |
10 |
|
$LOOP_TTLbool |
5 |
|
$RESTRICT_HOSTSbool |
false |
Redirect to ./ when the target is an external host not listed in ALLOWED_HOSTS (open redirect protection) |
$USE_DESIGNbool |
false |
None without a leading underscore.
_ hooks and helpers; not routable as page actionspublic 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;
}
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'],
]);
}
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'],
]);
}
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,
]);
}