<?php
/**
*
* This file is part of the phpBB Forum Software package.
*
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/

namespace phpbb;

/**
* Session class
*/
class session
{
	var $cookie_data = array();
	var $page = array();
	var $data = array();
	var $browser = '';
	var $forwarded_for = '';
	var $host = '';
	var $session_id = '';
	var $ip = '';
	var $load = 0;
	var $time_now = 0;
	var $update_session_page = true;

	/**
	 * Extract current session page
	 *
	 * @param string $root_path current root path (phpbb_root_path)
	 * @return array
	 */
	static function extract_current_page($root_path)
	{
		global $request, $symfony_request, $phpbb_filesystem;

		$page_array = array();

		// First of all, get the request uri...
		$script_name = $request->escape($symfony_request->getScriptName(), true);
		$args = $request->escape(explode('&', $symfony_request->getQueryString()), true);

		// If we are unable to get the script name we use REQUEST_URI as a failover and note it within the page array for easier support...
		if (!$script_name)
		{
			$script_name = html_entity_decode($request->server('REQUEST_URI'), ENT_COMPAT);
			$script_name = (($pos = strpos($script_name, '?')) !== false) ? substr($script_name, 0, $pos) : $script_name;
			$page_array['failover'] = 1;
		}

		// Replace backslashes and doubled slashes (could happen on some proxy setups)
		$script_name = str_replace(array('\\', '//'), '/', $script_name);

		// Now, remove the sid and let us get a clean query string...
		$use_args = array();

		// Since some browser do not encode correctly we need to do this with some "special" characters...
		// " -> %22, ' => %27, < -> %3C, > -> %3E
		$find = array('"', "'", '<', '>', '&quot;', '&lt;', '&gt;');
		$replace = array('%22', '%27', '%3C', '%3E', '%22', '%3C', '%3E');

		foreach ($args as $key => $argument)
		{
			if (strpos($argument, 'sid=') === 0)
			{
				continue;
			}

			$use_args[] = str_replace($find, $replace, $argument);
		}
		unset($args);

		// The following examples given are for an request uri of {path to the phpbb directory}/adm/index.php?i=10&b=2

		// The current query string
		$query_string = trim(implode('&', $use_args));

		// basenamed page name (for example: index.php)
		$page_name = (substr($script_name, -1, 1) == '/') ? '' : basename($script_name);
		$page_name = urlencode(htmlspecialchars($page_name, ENT_COMPAT));

		$symfony_request_path = $phpbb_filesystem->clean_path($symfony_request->getPathInfo());
		if ($symfony_request_path !== '/')
		{
			$page_name .= str_replace('%2F', '/', urlencode($symfony_request_path));
		}

		if (substr($root_path, 0, 2) === './' && strpos($root_path, '..') === false)
		{
			$root_dirs = explode('/', str_replace('\\', '/', rtrim($root_path, '/')));
			$page_dirs = explode('/', str_replace('\\', '/', '.'));
		}
		else
		{
			// current directory within the phpBB root (for example: adm)
			$root_dirs = explode('/', str_replace('\\', '/', $phpbb_filesystem->realpath($root_path)));
			$page_dirs = explode('/', str_replace('\\', '/', $phpbb_filesystem->realpath('./')));
		}

		$intersection = array_intersect_assoc($root_dirs, $page_dirs);

		$root_dirs = array_diff_assoc($root_dirs, $intersection);
		$page_dirs = array_diff_assoc($page_dirs, $intersection);

		$page_dir = str_repeat('../', count($root_dirs)) . implode('/', $page_dirs);

		if ($page_dir && substr($page_dir, -1, 1) == '/')
		{
			$page_dir = substr($page_dir, 0, -1);
		}

		// Current page from phpBB root (for example: adm/index.php?i=10&b=2)
		$page = (($page_dir) ? $page_dir . '/' : '') . $page_name;
		if ($query_string)
		{
			$page .= '?' . $query_string;
		}

		// The script path from the webroot to the current directory (for example: /phpBB3/adm/) : always prefixed with / and ends in /
		$script_path = $symfony_request->getBasePath();

		// The script path from the webroot to the phpBB root (for example: /phpBB3/)
		$script_dirs = explode('/', $script_path);
		array_splice($script_dirs, -count($page_dirs));
		$root_script_path = implode('/', $script_dirs) . (count($root_dirs) ? '/' . implode('/', $root_dirs) : '');

		// We are on the base level (phpBB root == webroot), lets adjust the variables a bit...
		if (!$root_script_path)
		{
			$root_script_path = ($page_dir) ? str_replace($page_dir, '', $script_path) : $script_path;
		}

		$script_path .= (substr($script_path, -1, 1) == '/') ? '' : '/';
		$root_script_path .= (substr($root_script_path, -1, 1) == '/') ? '' : '/';

		$forum_id = $request->variable('f', 0);
		// maximum forum id value is maximum value of mediumint unsigned column
		$forum_id = ($forum_id > 0 && $forum_id < 16777215) ? $forum_id : 0;

		$page_array += array(
			'page_name'			=> $page_name,
			'page_dir'			=> $page_dir,

			'query_string'		=> $query_string,
			'script_path'		=> str_replace(' ', '%20', htmlspecialchars($script_path, ENT_COMPAT)),
			'root_script_path'	=> str_replace(' ', '%20', htmlspecialchars($root_script_path, ENT_COMPAT)),

			'page'				=> $page,
			'forum'				=> $forum_id,
		);

		return $page_array;
	}

	/**
	* Get valid hostname/port. HTTP_HOST is used, SERVER_NAME if HTTP_HOST not present.
	*/
	function extract_current_hostname()
	{
		global $config, $request;

		// Get hostname
		$host = html_entity_decode($request->header('Host', $request->server('SERVER_NAME')), ENT_COMPAT);

		// Should be a string and lowered
		$host = (string) strtolower($host);

		// If host is equal the cookie domain or the server name (if config is set), then we assume it is valid
		if ((isset($config['cookie_domain']) && $host === $config['cookie_domain']) || (isset($config['server_name']) && $host === $config['server_name']))
		{
			return $host;
		}

		// Is the host actually a IP? If so, we use the IP... (IPv4)
		if (long2ip(ip2long($host)) === $host)
		{
			return $host;
		}

		// Now return the hostname (this also removes any port definition). The http:// is prepended to construct a valid URL, hosts never have a scheme assigned
		$host = @parse_url('http://' . $host);
		$host = (!empty($host['host'])) ? $host['host'] : '';

		// Remove any portions not removed by parse_url (#)
		$host = str_replace('#', '', $host);

		// If, by any means, the host is now empty, we will use a "best approach" way to guess one
		if (empty($host))
		{
			if (!empty($config['server_name']))
			{
				$host = $config['server_name'];
			}
			else if (!empty($config['cookie_domain']))
			{
				$host = (strpos($config['cookie_domain'], '.') === 0) ? substr($config['cookie_domain'], 1) : $config['cookie_domain'];
			}
			else
			{
				// Set to OS hostname or localhost
				$host = (function_exists('php_uname')) ? php_uname('n') : 'localhost';
			}
		}

		// It may be still no valid host, but for sure only a hostname (we may further expand on the cookie domain... if set)
		return $host;
	}

	/**
	* Start session management
	*
	* This is where all session activity begins. We gather various pieces of
	* information from the client and server. We test to see if a session already
	* exists. If it does, fine and dandy. If it doesn't we'll go on to create a
	* new one ... pretty logical heh? We also examine the system load (if we're
	* running on a system which makes such information readily available) and
	* halt if it's above an admin definable limit.
	*
	* @param bool $update_session_page if true the session page gets updated.
	*			This can be set to circumvent certain scripts to update the users last visited page.
	*/
	function session_begin($update_session_page = true)
	{
		global $phpEx, $SID, $_SID, $_EXTRA_URL, $db, $config, $phpbb_root_path;
		global $request, $phpbb_container, $user, $phpbb_log, $phpbb_dispatcher;

		// Give us some basic information
		$this->time_now				= time();
		$this->cookie_data			= array('u' => 0, 'k' => '');
		$this->update_session_page	= $update_session_page;
		$this->browser				= $request->header('User-Agent');
		$this->referer				= $request->header('Referer');
		$this->forwarded_for		= $request->header('X-Forwarded-For');

		$this->host					= $this->extract_current_hostname();
		$this->page					= $this->extract_current_page($phpbb_root_path);

		// if the forwarded for header shall be checked we have to validate its contents
		if ($config['forwarded_for_check'])
		{
			$this->forwarded_for = preg_replace('# {2,}#', ' ', str_replace(',', ' ', $this->forwarded_for));

			// split the list of IPs
			$ips = explode(' ', $this->forwarded_for);
			foreach ($ips as $ip)
			{
				if (!filter_var($ip, FILTER_VALIDATE_IP))
				{
					// contains invalid data, don't use the forwarded for header
					$this->forwarded_for = '';
					break;
				}
			}
		}
		else
		{
			$this->forwarded_for = '';
		}

		if ($request->is_set($config['cookie_name'] . '_sid', \phpbb\request\request_interface::COOKIE) || $request->is_set($config['cookie_name'] . '_u', \phpbb\request\request_interface::COOKIE))
		{
			$this->cookie_data['u'] = $request->variable($config['cookie_name'] . '_u', 0, false, \phpbb\request\request_interface::COOKIE);
			$this->cookie_data['k'] = $request->variable($config['cookie_name'] . '_k', '', false, \phpbb\request\request_interface::COOKIE);
			$this->session_id 		= $request->variable($config['cookie_name'] . '_sid', '', false, \phpbb\request\request_interface::COOKIE);

			$SID = (defined('NEED_SID')) ? '?sid=' . $this->session_id : '?sid=';
			$_SID = (defined('NEED_SID')) ? $this->session_id : '';

			if (empty($this->session_id))
			{
				$this->session_id = $_SID = $request->variable('sid', '');
				$SID = '?sid=' . $this->session_id;
				$this->cookie_data = array('u' => 0, 'k' => '');
			}
		}
		else
		{
			$this->session_id = $_SID = $request->variable('sid', '');
			$SID = '?sid=' . $this->session_id;
		}

		$_EXTRA_URL = array();

		// Why no forwarded_for et al? Well, too easily spoofed. With the results of my recent requests
		// it's pretty clear that in the majority of cases you'll at least be left with a proxy/cache ip.
		$ip = html_entity_decode($request->server('REMOTE_ADDR'), ENT_COMPAT);
		$ip = preg_replace('# {2,}#', ' ', str_replace(',', ' ', $ip));

		/**
		* Event to alter user IP address
		*
		* @event core.session_ip_after
		* @var	string	ip	REMOTE_ADDR
		* @since 3.1.10-RC1
		*/
		$vars = array('ip');
		extract($phpbb_dispatcher->trigger_event('core.session_ip_after', compact($vars)));

		// split the list of IPs
		$ips = explode(' ', trim($ip));

		// Default IP if REMOTE_ADDR is invalid
		$this->ip = '127.0.0.1';

		foreach ($ips as $ip)
		{
			// Normalise IP address
			$ip = phpbb_ip_normalise($ip);

			if ($ip === false)
			{
				// IP address is invalid.
				break;
			}

			// IP address is valid.
			$this->ip = $ip;
		}

		$this->load = false;

		// Load limit check (if applicable)
		if ($config['limit_load'] || $config['limit_search_load'])
		{
			if ((function_exists('sys_getloadavg') && $load = sys_getloadavg()) || ($load = explode(' ', @file_get_contents('/proc/loadavg'))))
			{
				$this->load = array_slice($load, 0, 1);
				$this->load = floatval($this->load[0]);
			}
			else
			{
				$config->set('limit_load', '0');
				$config->set('limit_search_load', '0');
			}
		}

		// if no session id is set, redirect to index.php
		$session_id = $request->variable('sid', '');
		if (defined('NEED_SID') && (empty($session_id) || $this->session_id !== $session_id))
		{
			send_status_line(401, 'Unauthorized');
			redirect(append_sid("{$phpbb_root_path}index.$phpEx"));
		}

		// if session id is set
		if (!empty($this->session_id))
		{
			$sql = 'SELECT u.*, s.*
				FROM ' . SESSIONS_TABLE . ' s, ' . USERS_TABLE . " u
				WHERE s.session_id = '" . $db->sql_escape($this->session_id) . "'
					AND u.user_id = s.session_user_id";
			$result = $db->sql_query($sql);
			$this->data = $db->sql_fetchrow($result);
			$db->sql_freeresult($result);

			// Did the session exist in the DB?
			if (isset($this->data['user_id']))
			{
				// Validate IP length according to admin ... enforces an IP
				// check on bots if admin requires this
//				$quadcheck = ($config['ip_check_bot'] && $this->data['user_type'] & USER_BOT) ? 4 : $config['ip_check'];

				if (strpos($this->ip, ':') !== false && strpos($this->data['session_ip'], ':') !== false)
				{
					$s_ip = short_ipv6($this->data['session_ip'], $config['ip_check']);
					$u_ip = short_ipv6($this->ip, $config['ip_check']);
				}
				else
				{
					$s_ip = implode('.', array_slice(explode('.', $this->data['session_ip']), 0, $config['ip_check']));
					$u_ip = implode('.', array_slice(explode('.', $this->ip), 0, $config['ip_check']));
				}

				$s_browser = ($config['browser_check']) ? trim(strtolower(substr($this->data['session_browser'], 0, 149))) : '';
				$u_browser = ($config['browser_check']) ? trim(strtolower(substr($this->browser, 0, 149))) : '';

				$s_forwarded_for = ($config['forwarded_for_check']) ? substr($this->data['session_forwarded_for'], 0, 254) : '';
				$u_forwarded_for = ($config['forwarded_for_check']) ? substr($this->forwarded_for, 0, 254) : '';

				// referer checks
				// The @ before $config['referer_validation'] suppresses notices present while running the updater
				$check_referer_path = (@$config['referer_validation'] == REFERER_VALIDATE_PATH);
				$referer_valid = true;

				// we assume HEAD and TRACE to be foul play and thus only whitelist GET
				if (@$config['referer_validation'] && strtolower($request->server('REQUEST_METHOD')) !== 'get')
				{
					$referer_valid = $this->validate_referer($check_referer_path);
				}

				if ($u_ip === $s_ip && $s_browser === $u_browser && $s_forwarded_for === $u_forwarded_for && $referer_valid)
				{
					$session_expired = false;

					// Check whether the session is still valid if we have one
					/* @var $provider_collection \phpbb\auth\provider_collection */
					$provider_collection = $phpbb_container->get('auth.provider_collection');
					$provider = $provider_collection->get_provider();

					if (!($provider instanceof \phpbb\auth\provider\provider_interface))
					{
						throw new \RuntimeException($provider . ' must implement \phpbb\auth\provider\provider_interface');
					}

					$ret = $provider->validate_session($this->data);
					if ($ret !== null && !$ret)
					{
						$session_expired = true;
					}

					if (!$session_expired)
					{
						// Check the session length timeframe if autologin is not enabled.
						// Else check the autologin length... and also removing those having autologin enabled but no longer allowed board-wide.
						if (!$this->data['session_autologin'])
						{
							if ($this->data['session_time'] < $this->time_now - ((int) $config['session_length'] + 60))
							{
								$session_expired = true;
							}
						}
						else if (!$config['allow_autologin'] || ($config['max_autologin_time'] && $this->data['session_time'] < $this->time_now - (86400 * (int) $config['max_autologin_time']) + 60))
						{
							$session_expired = true;
						}
					}

					if (!$session_expired)
					{
						$this->data['is_registered'] = ($this->data['user_id'] != ANONYMOUS && ($this->data['user_type'] == USER_NORMAL || $this->data['user_type'] == USER_FOUNDER)) ? true : false;
						$this->data['is_bot'] = (!$this->data['is_registered'] && $this->data['user_id'] != ANONYMOUS) ? true : false;
						$this->data['user_lang'] = basename($this->data['user_lang']);

						// Is user banned? Are they excluded? Won't return on ban, exists within method
						$this->check_ban_for_current_session($config);

						// Update user last active time accordingly, but in a minute or so
						if ($this->time_now - (int) $this->data['user_last_active'] > 60)
						{
							$this->update_last_active_time();
						}

						return true;
					}
				}
				else
				{
					// Added logging temporarily to help debug bugs...
					if ($phpbb_container->getParameter('session.log_errors') && $this->data['user_id'] != ANONYMOUS)
					{
						if ($referer_valid)
						{
							$phpbb_log->add('critical', $user->data['user_id'], $user->ip, 'LOG_IP_BROWSER_FORWARDED_CHECK', false, array(
								$u_ip,
								$s_ip,
								$u_browser,
								$s_browser,
								htmlspecialchars($u_forwarded_for, ENT_COMPAT),
								htmlspecialchars($s_forwarded_for, ENT_COMPAT)
							));
						}
						else
						{
							$phpbb_log->add('critical', $user->data['user_id'], $user->ip, 'LOG_REFERER_INVALID', false, array($this->referer));
						}
					}
				}
			}
		}

		// If we reach here then no (valid) session exists. So we'll create a new one
		return $this->session_create();
	}

	/**
	* Create a new session
	*
	* If upon trying to start a session we discover there is nothing existing we
	* jump here. Additionally this method is called directly during login to regenerate
	* the session for the specific user. In this method we carry out a number of tasks;
	* garbage collection, (search)bot checking, banned user comparison. Basically
	* though this method will result in a new session for a specific user.
	*/
	function session_create($user_id = false, $set_admin = false, $persist_login = false, $viewonline = true)
	{
		global $SID, $_SID, $db, $config, $cache, $phpbb_container, $phpbb_dispatcher;

		$this->data = array();

		/* Garbage collection ... remove old sessions updating user information
		// if necessary. It means (potentially) 11 queries but only infrequently
		if ($this->time_now > $config['session_last_gc'] + $config['session_gc'])
		{
			$this->session_gc();
		}*/

		// Do we allow autologin on this board? No? Then override anything
		// that may be requested here
		if (!$config['allow_autologin'])
		{
			$this->cookie_data['k'] = $persist_login = false;
		}

		/**
		* Here we do a bot check, oh er saucy! No, not that kind of bot
		* check. We loop through the list of bots defined by the admin and
		* see if we have any useragent and/or IP matches. If we do, this is a
		* bot, act accordingly
		*/
		$bot = false;
		$active_bots = $cache->obtain_bots();

		foreach ($active_bots as $row)
		{
			if ($row['bot_agent'] && preg_match('#' . str_replace('\*', '.*?', preg_quote($row['bot_agent'], '#')) . '#i', $this->browser))
			{
				$bot = $row['user_id'];
			}

			// If ip is supplied, we will make sure the ip is matching too...
			if ($row['bot_ip'] && ($bot || !$row['bot_agent']))
			{
				// Set bot to false, then we only have to set it to true if it is matching
				$bot = false;

				foreach (explode(',', $row['bot_ip']) as $bot_ip)
				{
					$bot_ip = trim($bot_ip);

					if (!$bot_ip)
					{
						continue;
					}

					if (strpos($this->ip, $bot_ip) === 0)
					{
						$bot = (int) $row['user_id'];
						break;
					}
				}
			}

			if ($bot)
			{
				break;
			}
		}

		/* @var $provider_collection \phpbb\auth\provider_collection */
		$provider_collection = $phpbb_container->get('auth.provider_collection');
		$provider = $provider_collection->get_provider();
		$this->data = $provider->autologin();

		if ($user_id !== false && isset($this->data['user_id']) && $this->data['user_id'] != $user_id)
		{
			$this->data = array();
		}

		if (isset($this->data['user_id']))
		{
			$this->cookie_data['k'] = '';
			$this->cookie_data['u'] = $this->data['user_id'];
		}

		// If we're presented with an autologin key we'll join against it.
		// Else if we've been passed a user_id we'll grab data based on that
		if (isset($this->cookie_data['k']) && $this->cookie_data['k'] && $this->cookie_data['u'] && empty($this->data))
		{
			$sql = 'SELECT u.*
				FROM ' . USERS_TABLE . ' u, ' . SESSIONS_KEYS_TABLE . ' k
				WHERE u.user_id = ' . (int) $this->cookie_data['u'] . '
					AND u.user_type IN (' . USER_NORMAL . ', ' . USER_FOUNDER . ")
					AND k.user_id = u.user_id
					AND k.key_id = '" . $db->sql_escape(md5($this->cookie_data['k'])) . "'";
			$result = $db->sql_query($sql);
			$user_data = $db->sql_fetchrow($result);

			if ($user_id === false || (isset($user_data['user_id']) && $user_id == $user_data['user_id']))
			{
				$this->data = $user_data;
				$bot = false;
			}

			$db->sql_freeresult($result);
		}

		if ($user_id !== false && empty($this->data))
		{
			$this->cookie_data['k'] = '';
			$this->cookie_data['u'] = $user_id;

			$sql = 'SELECT *
				FROM ' . USERS_TABLE . '
				WHERE user_id = ' . (int) $this->cookie_data['u'] . '
					AND user_type IN (' . USER_NORMAL . ', ' . USER_FOUNDER . ')';
			$result = $db->sql_query($sql);
			$this->data = $db->sql_fetchrow($result);
			$db->sql_freeresult($result);
			$bot = false;
		}

		// Bot user, if they have a SID in the Request URI we need to get rid of it
		// otherwise they'll index this page with the SID, duplicate content oh my!
		if ($bot && isset($_GET['sid']))
		{
			send_status_line(301, 'Moved Permanently');
			redirect(build_url(array('sid')));
		}

		// If no data was returned one or more of the following occurred:
		// Key didn't match one in the DB
		// User does not exist
		// User is inactive
		// User is bot
		if (!is_array($this->data) || !count($this->data))
		{
			$this->cookie_data['k'] = '';
			$this->cookie_data['u'] = ($bot) ? $bot : ANONYMOUS;

			if (!$bot)
			{
				$sql = 'SELECT *
					FROM ' . USERS_TABLE . '
					WHERE user_id = ' . (int) $this->cookie_data['u'];
			}
			else
			{
				// We give bots always the same session if it is not yet expired.
				$sql = 'SELECT u.*, s.*
					FROM ' . USERS_TABLE . ' u
					LEFT JOIN ' . SESSIONS_TABLE . ' s ON (s.session_user_id = u.user_id)
					WHERE u.user_id = ' . (int) $bot;
			}

			$result = $db->sql_query($sql);
			$this->data = $db->sql_fetchrow($result);
			$db->sql_freeresult($result);
		}

		if ($this->data['user_id'] != ANONYMOUS && !$bot)
		{
			$this->data['session_last_visit'] = (isset($this->data['session_time']) && $this->data['session_time']) ? $this->data['session_time'] : (($this->data['user_lastvisit']) ? $this->data['user_lastvisit'] : time());
		}
		else
		{
			$this->data['session_last_visit'] = $this->time_now;
		}

		// Force user id to be integer...
		$this->data['user_id'] = (int) $this->data['user_id'];

		// At this stage we should have a filled data array, defined cookie u and k data.
		// data array should contain recent session info if we're a real user and a recent
		// session exists in which case session_id will also be set

		// Is user banned? Are they excluded? Won't return on ban, exists within method
		$this->check_ban_for_current_session($config);

		$this->data['is_registered'] = (!$bot && $this->data['user_id'] != ANONYMOUS && ($this->data['user_type'] == USER_NORMAL || $this->data['user_type'] == USER_FOUNDER)) ? true : false;
		$this->data['is_bot'] = ($bot) ? true : false;

		// If our friend is a bot, we re-assign a previously assigned session
		if ($this->data['is_bot'] && $bot == $this->data['user_id'] && $this->data['session_id'])
		{
			// Only assign the current session if the ip, browser and forwarded_for match...
			if (strpos($this->ip, ':') !== false && strpos($this->data['session_ip'], ':') !== false)
			{
				$s_ip = short_ipv6($this->data['session_ip'], $config['ip_check']);
				$u_ip = short_ipv6($this->ip, $config['ip_check']);
			}
			else
			{
				$s_ip = implode('.', array_slice(explode('.', $this->data['session_ip']), 0, $config['ip_check']));
				$u_ip = implode('.', array_slice(explode('.', $this->ip), 0, $config['ip_check']));
			}

			$s_browser = ($config['browser_check']) ? trim(strtolower(substr($this->data['session_browser'], 0, 149))) : '';
			$u_browser = ($config['browser_check']) ? trim(strtolower(substr($this->browser, 0, 149))) : '';

			$s_forwarded_for = ($config['forwarded_for_check']) ? substr($this->data['session_forwarded_for'], 0, 254) : '';
			$u_forwarded_for = ($config['forwarded_for_check']) ? substr($this->forwarded_for, 0, 254) : '';

			if ($u_ip === $s_ip && $s_browser === $u_browser && $s_forwarded_for === $u_forwarded_for)
			{
				$this->session_id = $this->data['session_id'];

				// Only update session DB a minute or so after last update or if page changes
				if ($this->time_now - $this->data['session_time'] > 60 || ($this->update_session_page && $this->data['session_page'] != $this->page['page']))
				{
					// Update the last visit time
					$this->update_user_lastvisit();
				}

				$SID = '?sid=';
				$_SID = '';
				return true;
			}
			else
			{
				// If the ip and browser does not match make sure we only have one bot assigned to one session
				$db->sql_query('DELETE FROM ' . SESSIONS_TABLE . ' WHERE session_user_id = ' . $this->data['user_id']);
			}
		}

		$session_autologin = (($this->cookie_data['k'] || $persist_login) && $this->data['is_registered']) ? true : false;
		$set_admin = ($set_admin && $this->data['is_registered']) ? true : false;

		// Create or update the session
		$sql_ary = array(
			'session_user_id'		=> (int) $this->data['user_id'],
			'session_start'			=> (int) $this->time_now,
			'session_last_visit'	=> (int) $this->data['session_last_visit'],
			'session_time'			=> (int) $this->time_now,
			'session_browser'		=> (string) trim(substr($this->browser, 0, 149)),
			'session_forwarded_for'	=> (string) $this->forwarded_for,
			'session_ip'			=> (string) $this->ip,
			'session_autologin'		=> ($session_autologin) ? 1 : 0,
			'session_admin'			=> ($set_admin) ? 1 : 0,
			'session_viewonline'	=> ($viewonline) ? 1 : 0,
		);

		if ($this->update_session_page)
		{
			$sql_ary['session_page'] = (string) substr($this->page['page'], 0, 199);
			$sql_ary['session_forum_id'] = $this->page['forum'];
		}

		$db->sql_return_on_error(true);

		$sql = 'DELETE
			FROM ' . SESSIONS_TABLE . '
			WHERE session_id = \'' . $db->sql_escape($this->session_id) . '\'
				AND session_user_id = ' . ANONYMOUS;

		if (!defined('IN_ERROR_HANDLER') && (!$this->session_id || !$db->sql_query($sql) || !$db->sql_affectedrows()))
		{
			// Limit new sessions in 1 minute period (if required)
			if (empty($this->data['session_time']) && $config['active_sessions'])
			{
//				$db->sql_return_on_error(false);

				$sql = 'SELECT COUNT(session_id) AS sessions
					FROM ' . SESSIONS_TABLE . '
					WHERE session_time >= ' . ($this->time_now - 60);
				$result = $db->sql_query($sql);
				$row = $db->sql_fetchrow($result);
				$db->sql_freeresult($result);

				if ((int) $row['sessions'] > (int) $config['active_sessions'])
				{
					send_status_line(503, 'Service Unavailable');
					trigger_error('BOARD_UNAVAILABLE');
				}
			}
		}

		// Since we re-create the session id here, the inserted row must be unique. Therefore, we display potential errors.
		// Commented out because it will not allow forums to update correctly
//		$db->sql_return_on_error(false);

		// Something quite important: session_page always holds the *last* page visited, except for the *first* visit.
		// We are not able to simply have an empty session_page btw, therefore we need to tell phpBB how to detect this special case.
		// If the session id is empty, we have a completely new one and will set an "identifier" here. This identifier is able to be checked later.
		if (empty($this->data['session_id']))
		{
			// This is a temporary variable, only set for the very first visit
			$this->data['session_created'] = true;
		}

		$this->session_id = $this->data['session_id'] = md5(unique_id());

		$sql_ary['session_id'] = (string) $this->session_id;
		$sql_ary['session_page'] = (string) substr($this->page['page'], 0, 199);
		$sql_ary['session_forum_id'] = $this->page['forum'];

		$sql = 'INSERT INTO ' . SESSIONS_TABLE . ' ' . $db->sql_build_array('INSERT', $sql_ary);
		$db->sql_query($sql);

		$db->sql_return_on_error(false);

		// Regenerate autologin/persistent login key
		if ($session_autologin)
		{
			$this->set_login_key();
		}

		// refresh data
		$SID = '?sid=' . $this->session_id;
		$_SID = $this->session_id;
		$this->data = array_merge($this->data, $sql_ary);

		if (!$bot)
		{
			$cookie_expire = $this->time_now + (($config['max_autologin_time']) ? 86400 * (int) $config['max_autologin_time'] : 31536000);

			$this->set_cookie('u', $this->cookie_data['u'], $cookie_expire);
			$this->set_cookie('k', $this->cookie_data['k'], $cookie_expire);
			$this->set_cookie('sid', $this->session_id, $cookie_expire);

			unset($cookie_expire);

			if ($this->data['user_id'] != ANONYMOUS)
			{
				$sql = 'SELECT COUNT(session_id) AS sessions
						FROM ' . SESSIONS_TABLE . '
						WHERE session_user_id = ' . (int) $this->data['user_id'] . '
						AND session_time >= ' . (int) ($this->time_now - (max((int) $config['session_length'], (int) $config['form_token_lifetime'])));
				$result = $db->sql_query($sql);
				$row = $db->sql_fetchrow($result);
				$db->sql_freeresult($result);

				if ((int) $row['sessions'] <= 1 || empty($this->data['user_form_salt']))
				{
					$this->data['user_form_salt'] = unique_id();
					// Update the form key
					$sql = 'UPDATE ' . USERS_TABLE . '
					SET user_form_salt = \'' . $db->sql_escape($this->data['user_form_salt']) . '\',
						user_last_active = ' . (int) $this->time_now . '
					WHERE user_id = ' . (int) $this->data['user_id'];
					$db->sql_query($sql);
				}
			}
			else
			{
				$this->update_last_active_time();
			}
		}
		else
		{
			$this->data['session_time'] = $this->data['session_last_visit'] = $this->time_now;

			$this->update_user_lastvisit();

			$SID = '?sid=';
			$_SID = '';
		}

		$session_data = $sql_ary;
		/**
		* Event to send new session data to extension
		* Read-only event
		*
		* @event core.session_create_after
		* @var	array		session_data				Associative array of session keys to be updated
		* @since 3.1.6-RC1
		*/
		$vars = array('session_data');
		extract($phpbb_dispatcher->trigger_event('core.session_create_after', compact($vars)));
		unset($session_data);

		return true;
	}

	/**
	* Kills a session
	*
	* This method does what it says on the tin. It will delete a pre-existing session.
	* It resets cookie information (destroying any autologin key within that cookie data)
	* and update the users information from the relevant session data. It will then
	* grab guest user information.
	*/
	function session_kill($new_session = true)
	{
		global $SID, $_SID, $db, $phpbb_container, $phpbb_dispatcher;

		$sql = 'DELETE FROM ' . SESSIONS_TABLE . "
			WHERE session_id = '" . $db->sql_escape($this->session_id) . "'
				AND session_user_id = " . (int) $this->data['user_id'];
		$db->sql_query($sql);

		$user_id = (int) $this->data['user_id'];
		$session_id = $this->session_id;
		/**
		* Event to send session kill information to extension
		* Read-only event
		*
		* @event core.session_kill_after
		* @var	int		user_id				user_id of the session user.
		* @var	string		session_id				current user's session_id
		* @var	bool	new_session 	should we create new session for user
		* @since 3.1.6-RC1
		*/
		$vars = array('user_id', 'session_id', 'new_session');
		extract($phpbb_dispatcher->trigger_event('core.session_kill_after', compact($vars)));
		unset($user_id);
		unset($session_id);

		// Allow connecting logout with external auth method logout
		/* @var $provider_collection \phpbb\auth\provider_collection */
		$provider_collection = $phpbb_container->get('auth.provider_collection');
		$provider = $provider_collection->get_provider();
		$provider->logout($this->data, $new_session);

		if ($this->data['user_id'] != ANONYMOUS)
		{
			// Delete existing session, update last visit info first!
			if (!isset($this->data['session_time']))
			{
				$this->data['session_time'] = time();
			}

			$sql = 'UPDATE ' . USERS_TABLE . '
				SET user_lastvisit = ' . (int) $this->data['session_time'] . '
				WHERE user_id = ' . (int) $this->data['user_id'];
			$db->sql_query($sql);

			if ($this->cookie_data['k'])
			{
				$sql = 'DELETE FROM ' . SESSIONS_KEYS_TABLE . '
					WHERE user_id = ' . (int) $this->data['user_id'] . "
						AND key_id = '" . $db->sql_escape(md5($this->cookie_data['k'])) . "'";
				$db->sql_query($sql);
			}

			// Reset the data array
			$this->data = array();

			$sql = 'SELECT *
				FROM ' . USERS_TABLE . '
				WHERE user_id = ' . ANONYMOUS;
			$result = $db->sql_query($sql);
			$this->data = $db->sql_fetchrow($result);
			$db->sql_freeresult($result);
		}

		$cookie_expire = $this->time_now - 31536000;
		$this->set_cookie('u', '', $cookie_expire);
		$this->set_cookie('k', '', $cookie_expire);
		$this->set_cookie('sid', '', $cookie_expire);
		unset($cookie_expire);

		$SID = '?sid=';
		$this->session_id = $_SID = '';

		// To make sure a valid session is created we create one for the anonymous user
		if ($new_session)
		{
			$this->session_create(ANONYMOUS);
		}

		return true;
	}

	/**
	* Session garbage collection
	*
	* This looks a lot more complex than it really is. Effectively we are
	* deleting any sessions older than an admin definable limit. Due to the
	* way in which we maintain session data we have to ensure we update user
	* data before those sessions are destroyed. In addition this method
	* removes autologin key information that is older than an admin defined
	* limit.
	*/
	function session_gc()
	{
		global $db, $config, $phpbb_container, $phpbb_dispatcher;

		if (!$this->time_now)
		{
			$this->time_now = time();
		}

		/**
		 * Get expired sessions for registered users, only most recent for each user
		 * Inner SELECT gets most recent expired sessions for unique session_user_id
		 * Outer SELECT gets data for them
		 */
		$sql_select = 'SELECT s1.session_page, s1.session_user_id, s1.session_time AS recent_time
			FROM ' . SESSIONS_TABLE . ' AS s1
			INNER JOIN (
				SELECT session_user_id, MAX(session_time) AS recent_time
				FROM ' . SESSIONS_TABLE . '
				WHERE session_time < ' . ($this->time_now - (int) $config['session_length']) . '
					AND session_user_id <> ' . ANONYMOUS . '
				GROUP BY session_user_id
			) AS s2
			ON s1.session_user_id = s2.session_user_id
				AND s1.session_time = s2.recent_time';

		switch ($db->get_sql_layer())
		{
			case 'sqlite3':
				if (phpbb_version_compare($db->sql_server_info(true), '3.8.3', '>='))
				{
					// For SQLite versions 3.8.3+ which support Common Table Expressions (CTE)
					$sql = "WITH s3 (session_page, session_user_id, session_time) AS ($sql_select)
						UPDATE " . USERS_TABLE . '
						SET (user_lastpage, user_lastvisit) = (SELECT session_page, session_time FROM s3 WHERE session_user_id = user_id)
						WHERE EXISTS (SELECT session_user_id FROM s3 WHERE session_user_id = user_id)';
					$db->sql_query($sql);

					break;
				}

			// No break, for SQLite versions prior to 3.8.3 and Oracle
			case 'oracle':
				$result = $db->sql_query($sql_select);
				while ($row = $db->sql_fetchrow($result))
				{
					$sql = 'UPDATE ' . USERS_TABLE . '
						SET user_lastvisit = ' . (int) $row['recent_time'] . ", user_lastpage = '" . $db->sql_escape($row['session_page']) . "'
						WHERE user_id = " . (int) $row['session_user_id'];
					$db->sql_query($sql);
				}
				$db->sql_freeresult($result);
			break;

			case 'mysqli':
				$sql = 'UPDATE ' . USERS_TABLE . " u,
					($sql_select) s3
					SET u.user_lastvisit = s3.recent_time, u.user_lastpage = s3.session_page
					WHERE u.user_id = s3.session_user_id";
				$db->sql_query($sql);
			break;

			default:
				$sql = 'UPDATE ' . USERS_TABLE . "
					SET user_lastvisit = s3.recent_time, user_lastpage = s3.session_page
					FROM ($sql_select) s3
					WHERE user_id = s3.session_user_id";
				$db->sql_query($sql);
			break;
		}

		// Delete all expired sessions
		$sql = 'DELETE FROM ' . SESSIONS_TABLE . '
			WHERE session_time < ' . ($this->time_now - (int) $config['session_length']);
		$db->sql_query($sql);

		// Update gc timer
		$config->set('session_last_gc', $this->time_now, false);

		if ($config['max_autologin_time'])
		{
			$sql = 'DELETE FROM ' . SESSIONS_KEYS_TABLE . '
				WHERE last_login < ' . (time() - (86400 * (int) $config['max_autologin_time']));
			$db->sql_query($sql);
		}

		// only called from CRON; should be a safe workaround until the infrastructure gets going
		/* @var \phpbb\captcha\factory $captcha_factory */
		$captcha_factory = $phpbb_container->get('captcha.factory');
		$captcha_factory->garbage_collect($config['captcha_plugin']);

		$sql = 'DELETE FROM ' . LOGIN_ATTEMPT_TABLE . '
			WHERE attempt_time < ' . (time() - (int) $config['ip_login_limit_time']);
		$db->sql_query($sql);

		/**
		* Event to trigger extension on session_gc
		*
		* @event core.session_gc_after
		* @since 3.1.6-RC1
		*/
		$phpbb_dispatcher->dispatch('core.session_gc_after');

		return;
	}

	/**
	* Sets a cookie
	*
	* Sets a cookie of the given name with the specified data for the given length of time. If no time is specified, a session cookie will be set.
	*
	* @param string $name		Name of the cookie, will be automatically prefixed with the phpBB cookie name. track becomes [cookie_name]_track then.
	* @param string $cookiedata	The data to hold within the cookie
	* @param int $cookietime	The expiration time as UNIX timestamp. If 0 is provided, a session cookie is set.
	* @param bool $httponly		Use HttpOnly. Defaults to true. Use false to make cookie accessible by client-side scripts.
	*/
	function set_cookie($name, $cookiedata, $cookietime, $httponly = true)
	{
		global $config, $phpbb_dispatcher;

		// If headers are already set, we just return
		if (headers_sent())
		{
			return;
		}

		$disable_cookie = false;
		/**
		* Event to modify or disable setting cookies
		*
		* @event core.set_cookie
		* @var	bool		disable_cookie	Set to true to disable setting this cookie
		* @var	string		name			Name of the cookie
		* @var	string		cookiedata		The data to hold within the cookie
		* @var	int			cookietime		The expiration time as UNIX timestamp
		* @var	bool		httponly		Use HttpOnly?
		* @since 3.2.9-RC1
		*/
		$vars = array(
			'disable_cookie',
			'name',
			'cookiedata',
			'cookietime',
			'httponly',
		);
		extract($phpbb_dispatcher->trigger_event('core.set_cookie', compact($vars)));

		if ($disable_cookie)
		{
			return;
		}

		$name_data = rawurlencode($config['cookie_name'] . '_' . $name) . '=' . rawurlencode($cookiedata);
		$expire = gmdate('D, d-M-Y H:i:s \\G\\M\\T', $cookietime);
		$domain = (!$config['cookie_domain'] || $config['cookie_domain'] == '127.0.0.1' || strpos($config['cookie_domain'], '.') === false) ? '' : '; domain=' . $config['cookie_domain'];

		header('Set-Cookie: ' . $name_data . (($cookietime) ? '; expires=' . $expire : '') . '; path=' . $config['cookie_path'] . $domain . ((!$config['cookie_secure']) ? '' : '; secure') . ';' . (($httponly) ? ' HttpOnly' : ''), false);
	}

	/**
	* Check for banned user
	*
	* Checks whether the supplied user is banned by id, ip or email. If no parameters
	* are passed to the method pre-existing session data is used.
	*
	* @param int|false		$user_id		The user id
	* @param mixed			$user_ips		Can contain a string with one IP or an array of multiple IPs
	* @param string|false	$user_email		The user email
	* @param bool			$return			If $return is false this routine does not return on finding a banned user,
	*	it outputs a relevant message and stops execution.
	*/
	function check_ban($user_id = false, $user_ips = false, $user_email = false, $return = false)
	{
		global $config, $db, $phpbb_dispatcher;

		if (defined('IN_CHECK_BAN') || defined('SKIP_CHECK_BAN'))
		{
			return;
		}

		$banned = false;
		$cache_ttl = 3600;
		$where_sql = array();

		$sql = 'SELECT ban_ip, ban_userid, ban_email, ban_exclude, ban_give_reason, ban_end
			FROM ' . BANLIST_TABLE . '
			WHERE ';

		// Determine which entries to check, only return those
		if ($user_email === false)
		{
			$where_sql[] = "ban_email = ''";
		}

		if ($user_ips === false)
		{
			$where_sql[] = "(ban_ip = '' OR ban_exclude = 1)";
		}

		if ($user_id === false)
		{
			$where_sql[] = '(ban_userid = 0 OR ban_exclude = 1)';
		}
		else
		{
			$cache_ttl = ($user_id == ANONYMOUS) ? 3600 : 0;
			$_sql = '(ban_userid = ' . $user_id;

			if ($user_email !== false)
			{
				$_sql .= " OR ban_email <> ''";
			}

			if ($user_ips !== false)
			{
				$_sql .= " OR ban_ip <> ''";
			}

			$_sql .= ')';

			$where_sql[] = $_sql;
		}

		$sql .= (count($where_sql)) ? implode(' AND ', $where_sql) : '';
		$result = $db->sql_query($sql, $cache_ttl);

		$ban_triggered_by = 'user';
		while ($row = $db->sql_fetchrow($result))
		{
			if ($row['ban_end'] && $row['ban_end'] < time())
			{
				continue;
			}

			$ip_banned = false;
			if (!empty($row['ban_ip']))
			{
				if (!is_array($user_ips))
				{
					$ip_banned = preg_match('#^' . str_replace('\*', '.*?', preg_quote($row['ban_ip'], '#')) . '$#i', $user_ips);
				}
				else
				{
					foreach ($user_ips as $user_ip)
					{
						if (preg_match('#^' . str_replace('\*', '.*?', preg_quote($row['ban_ip'], '#')) . '$#i', $user_ip))
						{
							$ip_banned = true;
							break;
						}
					}
				}
			}

			if ((!empty($row['ban_userid']) && intval($row['ban_userid']) == $user_id) ||
				$ip_banned ||
				(!empty($row['ban_email']) && preg_match('#^' . str_replace('\*', '.*?', preg_quote($row['ban_email'], '#')) . '$#i', $user_email)))
			{
				if (!empty($row['ban_exclude']))
				{
					$banned = false;
					break;
				}
				else
				{
					$banned = true;
					$ban_row = $row;

					if (!empty($row['ban_userid']) && intval($row['ban_userid']) == $user_id)
					{
						$ban_triggered_by = 'user';
					}
					else if ($ip_banned)
					{
						$ban_triggered_by = 'ip';
					}
					else
					{
						$ban_triggered_by = 'email';
					}

					// Don't break. Check if there is an exclude rule for this user
				}
			}
		}
		$db->sql_freeresult($result);

		/**
		* Event to set custom ban type
		*
		* @event core.session_set_custom_ban
		* @var	bool		return				If $return is false this routine does not return on finding a banned user, it outputs a relevant message and stops execution
		* @var	bool		banned				Check if user already banned
		* @var	array|false	ban_row				Ban data
		* @var	string		ban_triggered_by	Method that caused ban, can be your custom method
		* @since 3.1.3-RC1
		*/
		$ban_row = isset($ban_row) ? $ban_row : false;
		$vars = array('return', 'banned', 'ban_row', 'ban_triggered_by');
		extract($phpbb_dispatcher->trigger_event('core.session_set_custom_ban', compact($vars)));

		if ($banned && !$return)
		{
			global $phpbb_root_path, $phpEx;

			// If the session is empty we need to create a valid one...
			if (empty($this->session_id))
			{
				// This seems to be no longer needed? - #14971
//				$this->session_create(ANONYMOUS);
			}

			// Initiate environment ... since it won't be set at this stage
			$this->setup();

			// Logout the user, banned users are unable to use the normal 'logout' link
			if ($this->data['user_id'] != ANONYMOUS)
			{
				$this->session_kill();
			}

			// We show a login box here to allow founders accessing the board if banned by IP
			if (defined('IN_LOGIN') && $this->data['user_id'] == ANONYMOUS)
			{
				$this->setup('ucp');
				$this->data['is_registered'] = $this->data['is_bot'] = false;

				// Set as a precaution to allow login_box() handling this case correctly as well as this function not being executed again.
				define('IN_CHECK_BAN', 1);

				login_box("index.$phpEx");

				// The false here is needed, else the user is able to circumvent the ban.
				$this->session_kill(false);
			}

			// Ok, we catch the case of an empty session id for the anonymous user...
			// This can happen if the user is logging in, banned by username and the login_box() being called "again".
			if (empty($this->session_id) && defined('IN_CHECK_BAN'))
			{
				$this->session_create(ANONYMOUS);
			}

			// Determine which message to output
			$till_date = ($ban_row['ban_end']) ? $this->format_date($ban_row['ban_end']) : '';
			$message = ($ban_row['ban_end']) ? 'BOARD_BAN_TIME' : 'BOARD_BAN_PERM';

			$contact_link = phpbb_get_board_contact_link($config, $phpbb_root_path, $phpEx);
			$message = sprintf($this->lang[$message], $till_date, '<a href="' . $contact_link . '">', '</a>');
			$message .= ($ban_row['ban_give_reason']) ? '<br /><br />' . sprintf($this->lang['BOARD_BAN_REASON'], $ban_row['ban_give_reason']) : '';
			$message .= '<br /><br /><em>' . $this->lang['BAN_TRIGGERED_BY_' . strtoupper($ban_triggered_by)] . '</em>';

			// A very special case... we are within the cron script which is not supposed to print out the ban message... show blank page
			if (defined('IN_CRON'))
			{
				garbage_collection();
				exit_handler();
				exit;
			}

			// To circumvent session_begin returning a valid value and the check_ban() not called on second page view, we kill the session again
			$this->session_kill(false);

			trigger_error($message);
		}

		if (!empty($ban_row))
		{
			$ban_row['ban_triggered_by'] = $ban_triggered_by;
		}

		return ($banned && $ban_row) ? $ban_row : $banned;
	}

	/**
	 * Check the current session for bans
	 *
	 * @return true if session user is banned.
	 */
	protected function check_ban_for_current_session($config)
	{
		if (!defined('SKIP_CHECK_BAN') && $this->data['user_type'] != USER_FOUNDER)
		{
			if (!$config['forwarded_for_check'])
			{
				$this->check_ban($this->data['user_id'], $this->ip);
			}
			else
			{
				$ips = explode(' ', $this->forwarded_for);
				$ips[] = $this->ip;
				$this->check_ban($this->data['user_id'], $ips);
			}
		}
	}

	/**
	* Check if ip is blacklisted by Spamhaus SBL
	*
	* Disables DNSBL setting if errors are returned by Spamhaus due to a policy violation.
	* https://www.spamhaus.com/product/help-for-spamhaus-public-mirror-users/
	*
	* @param string 		$dnsbl	the blacklist to check against
	* @param string|false	$ip		the IPv4 address to check
	*
	* @return bool true if listed in spamhaus database, false if not
	*/
	function check_dnsbl_spamhaus($dnsbl, $ip = false)
	{
		global $config, $phpbb_log;

		if ($ip === false)
		{
			$ip = $this->ip;
		}

		// Spamhaus does not support IPv6 addresses.
		if (strpos($ip, ':') !== false)
		{
			return false;
		}

		if ($ip)
		{
			$quads = explode('.', $ip);
			$reverse_ip = $quads[3] . '.' . $quads[2] . '.' . $quads[1] . '.' . $quads[0];

			$records = dns_get_record($reverse_ip . '.' . $dnsbl . '.', DNS_A);
			if (empty($records))
			{
				return false;
			}
			else
			{
				$error = false;
				foreach ($records as $record)
				{
					if ($record['ip'] == '127.255.255.254')
					{
						$error = 'LOG_SPAMHAUS_OPEN_RESOLVER';
						break;
					}
					else if ($record['ip'] == '127.255.255.255')
					{
						$error = 'LOG_SPAMHAUS_VOLUME_LIMIT';
						break;
					}
				}

				if ($error !== false)
				{
					$config->set('check_dnsbl', 0);
					$phpbb_log->add('critical', $this->data['user_id'], $ip, $error);
				}
				else
				{
					// The existence of a non-error A record means it's a hit
					return true;
				}
			}
		}

		return false;
	}

	/**
	* Checks if an IPv4 address is in a specified DNS blacklist
	*
	* Only checks if a record is returned or not.
	*
	* @param string 		$dnsbl	the blacklist to check against
	* @param string|false	$ip		the IPv4 address to check
	*
	* @return bool true if record is returned, false if not
	*/
	function check_dnsbl_ipv4_generic($dnsbl, $ip = false)
	{
		if ($ip === false)
		{
			$ip = $this->ip;
		}

		// This function does not support IPv6 addresses.
		if (strpos($ip, ':') !== false)
		{
			return false;
		}

		$quads = explode('.', $ip);
		$reverse_ip = $quads[3] . '.' . $quads[2] . '.' . $quads[1] . '.' . $quads[0];

		if (checkdnsrr($reverse_ip . '.' . $dnsbl . '.', 'A') === true)
		{
			return true;
		}

		return false;
	}

	/**
	* Check if ip is blacklisted
	* This should be called only where absolutely necessary
	*
	* Only IPv4 (rbldns does not support AAAA records/IPv6 lookups)
	*
	* @author satmd (from the php manual)
	* @param string 		$mode	register/post - spamcop for example is omitted for posting
	* @param string|false	$ip		the IPv4 address to check
	*
	* @return false if ip is not blacklisted, else an array([checked server], [lookup])
	*/
	function check_dnsbl($mode, $ip = false)
	{
		if ($ip === false)
		{
			$ip = $this->ip;
		}

		// Neither Spamhaus nor Spamcop supports IPv6 addresses.
		if (strpos($ip, ':') !== false)
		{
			return false;
		}

		$dnsbl_check = array(
			'sbl.spamhaus.org'	=> ['https://check.spamhaus.org/listed/?searchterm=', 'check_dnsbl_spamhaus'],
		);

		if ($mode == 'register')
		{
			$dnsbl_check['bl.spamcop.net'] = ['https://www.spamcop.net/bl.shtml?', 'check_dnsbl_ipv4_generic'];
		}

		if ($ip)
		{
			// Need to be listed on all servers...
			$listed = true;
			$info = array();

			foreach ($dnsbl_check as $dnsbl => $lookup)
			{
				if (call_user_func(array($this, $lookup[1]), $dnsbl, $ip) === true)
				{
					$info = array($dnsbl, $lookup[0] . $ip);
				}
				else
				{
					$listed = false;
				}
			}

			if ($listed)
			{
				return $info;
			}
		}

		return false;
	}

	/**
	* Check if URI is blacklisted
	* This should be called only where absolutely necessary, for example on the submitted website field
	* This function is not in use at the moment and is only included for testing purposes, it may not work at all!
	* This means it is untested at the moment and therefore commented out
	*
	* @param string $uri URI to check
	* @return true if uri is on blacklist, else false. Only blacklist is checked (~zero FP), no grey lists
	function check_uribl($uri)
	{
		// Normally parse_url() is not intended to parse uris
		// We need to get the top-level domain name anyway... change.
		$uri = parse_url($uri);

		if ($uri === false || empty($uri['host']))
		{
			return false;
		}

		$uri = trim($uri['host']);

		if ($uri)
		{
			// One problem here... the return parameter for the "windows" method is different from what
			// we expect... this may render this check useless...
			if (checkdnsrr($uri . '.multi.uribl.com.', 'A') === true)
			{
				return true;
			}
		}

		return false;
	}
	*/

	/**
	* Set/Update a persistent login key
	*
	* This method creates or updates a persistent session key. When a user makes
	* use of persistent (formerly auto-) logins a key is generated and stored in the
	* DB. When they revisit with the same key it's automatically updated in both the
	* DB and cookie. Multiple keys may exist for each user representing different
	* browsers or locations. As with _any_ non-secure-socket no passphrase login this
	* remains vulnerable to exploit.
	*/
	function set_login_key($user_id = false, $key = false, $user_ip = false)
	{
		global $db, $phpbb_dispatcher;

		$user_id = ($user_id === false) ? $this->data['user_id'] : $user_id;
		$user_ip = ($user_ip === false) ? $this->ip : $user_ip;
		$key = ($key === false) ? (($this->cookie_data['k']) ? $this->cookie_data['k'] : false) : $key;

		$key_id = unique_id(hexdec(substr($this->session_id, 0, 8)));

		$sql_ary = array(
			'key_id'		=> (string) md5($key_id),
			'last_ip'		=> (string) $user_ip,
			'last_login'	=> (int) time()
		);

		if (!$key)
		{
			$sql_ary += array(
				'user_id'	=> (int) $user_id
			);
		}

		if ($key)
		{
			$sql = 'UPDATE ' . SESSIONS_KEYS_TABLE . '
				SET ' . $db->sql_build_array('UPDATE', $sql_ary) . '
				WHERE user_id = ' . (int) $user_id . "
					AND key_id = '" . $db->sql_escape(md5($key)) . "'";
		}
		else
		{
			$sql = 'INSERT INTO ' . SESSIONS_KEYS_TABLE . ' ' . $db->sql_build_array('INSERT', $sql_ary);
		}

		/**
		 * Event to adjust autologin keys process
		 *
		 * @event core.set_login_key
		 * @var	string|false	key			Current autologin key if exists, false otherwise
		 * @var	string			key_id		New autologin key
		 * @var	string			sql			SQL query to update/insert autologin key
		 * @var	array			sql_ary		Aray with autologin key data
		 * @var	int				user_id		Current user's ID
		 * @var	string			user_ip		Current user's IP address
		 * @since 3.3.2-RC1
		 */
		$vars = [
			'key',
			'key_id',
			'sql',
			'sql_ary',
			'user_id',
			'user_ip',
		];
		extract($phpbb_dispatcher->trigger_event('core.set_login_key', compact($vars)));

		$db->sql_query($sql);

		$this->cookie_data['k'] = $key_id;

		return false;
	}

	/**
	* Reset all login keys for the specified user
	*
	* This method removes all current login keys for a specified (or the current)
	* user. It will be called on password change to render old keys unusable
	*/
	function reset_login_keys($user_id = false)
	{
		global $db;

		$user_id = ($user_id === false) ? (int) $this->data['user_id'] : (int) $user_id;

		$sql = 'DELETE FROM ' . SESSIONS_KEYS_TABLE . '
			WHERE user_id = ' . (int) $user_id;
		$db->sql_query($sql);

		// If the user is logged in, update last visit info first before deleting sessions
		$sql = 'SELECT session_time, session_page
			FROM ' . SESSIONS_TABLE . '
			WHERE session_user_id = ' . (int) $user_id . '
			ORDER BY session_time DESC';
		$result = $db->sql_query_limit($sql, 1);
		$row = $db->sql_fetchrow($result);
		$db->sql_freeresult($result);

		if ($row)
		{
			$sql = 'UPDATE ' . USERS_TABLE . '
				SET user_lastvisit = ' . (int) $row['session_time'] . ", user_lastpage = '" . $db->sql_escape($row['session_page']) . "'
				WHERE user_id = " . (int) $user_id;
			$db->sql_query($sql);
		}

		// Let's also clear any current sessions for the specified user_id
		// If it's the current user then we'll leave this session intact
		$sql_where = 'session_user_id = ' . (int) $user_id;
		$sql_where .= ($user_id === (int) $this->data['user_id']) ? " AND session_id <> '" . $db->sql_escape($this->session_id) . "'" : '';

		$sql = 'DELETE FROM ' . SESSIONS_TABLE . "
			WHERE $sql_where";
		$db->sql_query($sql);

		// We're changing the password of the current user and they have a key
		// Lets regenerate it to be safe
		if ($user_id === (int) $this->data['user_id'] && $this->cookie_data['k'])
		{
			$this->set_login_key($user_id);
		}
	}


	/**
	* Check if the request originated from the same page.
	* @param bool $check_script_path If true, the path will be checked as well
	*/
	function validate_referer($check_script_path = false)
	{
		global $config, $request;

		// no referer - nothing to validate, user's fault for turning it off (we only check on POST; so meta can't be the reason)
		if (empty($this->referer) || empty($this->host))
		{
			return true;
		}

		$host = htmlspecialchars($this->host, ENT_COMPAT);
		$ref = substr($this->referer, strpos($this->referer, '://') + 3);

		if (!(stripos($ref, $host) === 0) && (!$config['force_server_vars'] || !(stripos($ref, $config['server_name']) === 0)))
		{
			return false;
		}
		else if ($check_script_path && rtrim($this->page['root_script_path'], '/') !== '')
		{
			$ref = substr($ref, strlen($host));
			$server_port = $request->server('SERVER_PORT', 0);

			if ($server_port !== 80 && $server_port !== 443 && stripos($ref, ":$server_port") === 0)
			{
				$ref = substr($ref, strlen(":$server_port"));
			}

			if (!(stripos(rtrim($ref, '/'), rtrim($this->page['root_script_path'], '/')) === 0))
			{
				return false;
			}
		}

		return true;
	}


	function unset_admin()
	{
		global $db;
		$sql = 'UPDATE ' . SESSIONS_TABLE . '
			SET session_admin = 0
			WHERE session_id = \'' . $db->sql_escape($this->session_id) . '\'';
		$db->sql_query($sql);
	}

	/**
	* Update the session data
	*
	* @param array $session_data associative array of session keys to be updated
	* @param string $session_id optional session_id, defaults to current user's session_id
	*/
	public function update_session($session_data, $session_id = null)
	{
		global $db, $phpbb_dispatcher;

		$session_id = ($session_id) ? $session_id : $this->session_id;

		$sql = 'UPDATE ' . SESSIONS_TABLE . ' SET ' . $db->sql_build_array('UPDATE', $session_data) . "
			WHERE session_id = '" . $db->sql_escape($session_id) . "'";
		$db->sql_query($sql);

		/**
		* Event to send update session information to extension
		* Read-only event
		*
		* @event core.update_session_after
		* @var	array		session_data				Associative array of session keys to be updated
		* @var	string		session_id				current user's session_id
		* @since 3.1.6-RC1
		*/
		$vars = array('session_data', 'session_id');
		extract($phpbb_dispatcher->trigger_event('core.update_session_after', compact($vars)));
	}

	public function update_session_infos()
	{
		global $config, $db, $request;

		// No need to update if it's a new session. Informations are already inserted by session_create()
		if (isset($this->data['session_created']) && $this->data['session_created'])
		{
			return;
		}

		// Do not update the session page for ajax requests, so the view online still works as intended
		$page_changed = $this->update_session_page && (!isset($this->data['session_page']) || $this->data['session_page'] != $this->page['page'] || $this->data['session_forum_id'] != $this->page['forum']) && !$request->is_ajax();

		// Only update session DB a minute or so after last update or if page changes
		if ($this->time_now - (isset($this->data['session_time']) ? $this->data['session_time'] : 0) > 60 || $page_changed)
		{
			$sql_ary = array('session_time' => $this->time_now);

			if ($page_changed)
			{
				$sql_ary['session_page'] = substr($this->page['page'], 0, 199);
				$sql_ary['session_forum_id'] = $this->page['forum'];
			}

			$db->sql_return_on_error(true);

			$this->update_session($sql_ary);

			$db->sql_return_on_error(false);

			$this->data = array_merge($this->data, $sql_ary);

			if ($this->data['user_id'] != ANONYMOUS && isset($config['new_member_post_limit']) && $this->data['user_new'] && $config['new_member_post_limit'] <= $this->data['user_posts'])
			{
				$this->leave_newly_registered();
			}
		}
	}

	/**
	 * Get user ID
	 *
	 * @return int User ID
	 */
	public function id() : int
	{
		return isset($this->data['user_id']) ? (int) $this->data['user_id'] : ANONYMOUS;
	}

	/**
	 * Update user last visit time
	 */
	public function update_user_lastvisit()
	{
		global $db;

		if (isset($this->data['session_time'], $this->data['user_id']))
		{
			$sql = 'UPDATE ' . USERS_TABLE . '
				SET user_lastvisit = ' . (int) $this->data['session_time'] . ',
					user_last_active = ' . $this->time_now . '
				WHERE user_id = ' . (int) $this->data['user_id'];
			$db->sql_query($sql);
		}
	}

	/**
	 * Update user's last active time
	 *
	 * @return void
	 */
	public function update_last_active_time()
	{
		global $db;

		if (isset($this->time_now, $this->data['user_id']))
		{
			$sql = 'UPDATE ' . USERS_TABLE . '
				SET user_last_active = ' . $this->time_now . '
				WHERE user_id = ' . (int) $this->data['user_id'];
			$db->sql_query($sql);
		}
	}
}
