1<?php
2/**
3*
4* @package phpBB3
5* @version $Id$
6* @copyright (c) 2005 phpBB Group
7* @license http://opensource.org/licenses/gpl-license.php GNU Public License
8*
9*/
10
11/**
12* @ignore
13*/
14if (!defined('IN_PHPBB'))
15{
16	exit;
17}
18
19/**
20* Session class
21* @package phpBB3
22*/
23class session
24{
25	var $cookie_data = array();
26	var $page = array();
27	var $data = array();
28	var $browser = '';
29	var $forwarded_for = '';
30	var $host = '';
31	var $session_id = '';
32	var $ip = '';
33	var $load = 0;
34	var $time_now = 0;
35	var $update_session_page = true;
36
37	/**
38	* Extract current session page
39	*
40	* @param string $root_path current root path (phpbb_root_path)
41	*/
42	function extract_current_page($root_path)
43	{
44		$page_array = array();
45
46		// First of all, get the request uri...
47		$script_name = (!empty($_SERVER['PHP_SELF'])) ? $_SERVER['PHP_SELF'] : getenv('PHP_SELF');
48		$args = (!empty($_SERVER['QUERY_STRING'])) ? explode('&', $_SERVER['QUERY_STRING']) : explode('&', getenv('QUERY_STRING'));
49
50		// 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...
51		if (!$script_name)
52		{
53			$script_name = (!empty($_SERVER['REQUEST_URI'])) ? $_SERVER['REQUEST_URI'] : getenv('REQUEST_URI');
54			$script_name = (($pos = strpos($script_name, '?')) !== false) ? substr($script_name, 0, $pos) : $script_name;
55			$page_array['failover'] = 1;
56		}
57
58		// Replace backslashes and doubled slashes (could happen on some proxy setups)
59		$script_name = str_replace(array('\\', '//'), '/', $script_name);
60
61		// Now, remove the sid and let us get a clean query string...
62		$use_args = array();
63
64		// Since some browser do not encode correctly we need to do this with some "special" characters...
65		// " -> %22, ' => %27, < -> %3C, > -> %3E
66		$find = array('"', "'", '<', '>');
67		$replace = array('%22', '%27', '%3C', '%3E');
68
69		foreach ($args as $key => $argument)
70		{
71			if (strpos($argument, 'sid=') === 0)
72			{
73				continue;
74			}
75
76			$use_args[] = str_replace($find, $replace, $argument);
77		}
78		unset($args);
79
80		// The following examples given are for an request uri of {path to the phpbb directory}/adm/index.php?i=10&b=2
81
82		// The current query string
83		$query_string = trim(implode('&', $use_args));
84
85		// basenamed page name (for example: index.php)
86		$page_name = (substr($script_name, -1, 1) == '/') ? '' : basename($script_name);
87		$page_name = urlencode(htmlspecialchars($page_name));
88
89		// current directory within the phpBB root (for example: adm)
90		$root_dirs = explode('/', str_replace('\\', '/', phpbb_realpath($root_path)));
91		$page_dirs = explode('/', str_replace('\\', '/', phpbb_realpath('./')));
92		$intersection = array_intersect_assoc($root_dirs, $page_dirs);
93
94		$root_dirs = array_diff_assoc($root_dirs, $intersection);
95		$page_dirs = array_diff_assoc($page_dirs, $intersection);
96
97		$page_dir = str_repeat('../', sizeof($root_dirs)) . implode('/', $page_dirs);
98
99		if ($page_dir && substr($page_dir, -1, 1) == '/')
100		{
101			$page_dir = substr($page_dir, 0, -1);
102		}
103
104		// Current page from phpBB root (for example: adm/index.php?i=10&b=2)
105		$page = (($page_dir) ? $page_dir . '/' : '') . $page_name . (($query_string) ? "?$query_string" : '');
106
107		// The script path from the webroot to the current directory (for example: /phpBB3/adm/) : always prefixed with / and ends in /
108		$script_path = trim(str_replace('\\', '/', dirname($script_name)));
109
110		// The script path from the webroot to the phpBB root (for example: /phpBB3/)
111		$script_dirs = explode('/', $script_path);
112		array_splice($script_dirs, -sizeof($page_dirs));
113		$root_script_path = implode('/', $script_dirs) . (sizeof($root_dirs) ? '/' . implode('/', $root_dirs) : '');
114
115		// We are on the base level (phpBB root == webroot), lets adjust the variables a bit...
116		if (!$root_script_path)
117		{
118			$root_script_path = ($page_dir) ? str_replace($page_dir, '', $script_path) : $script_path;
119		}
120
121		$script_path .= (substr($script_path, -1, 1) == '/') ? '' : '/';
122		$root_script_path .= (substr($root_script_path, -1, 1) == '/') ? '' : '/';
123
124		$page_array += array(
125			'page_name'			=> $page_name,
126			'page_dir'			=> $page_dir,
127
128			'query_string'		=> $query_string,
129			'script_path'		=> str_replace(' ', '%20', htmlspecialchars($script_path)),
130			'root_script_path'	=> str_replace(' ', '%20', htmlspecialchars($root_script_path)),
131
132			'page'				=> $page,
133			'forum'				=> (isset($_REQUEST['f']) && $_REQUEST['f'] > 0) ? (int) $_REQUEST['f'] : 0,
134		);
135
136		return $page_array;
137	}
138
139	/**
140	* Get valid hostname/port. HTTP_HOST is used, SERVER_NAME if HTTP_HOST not present.
141	*/
142	function extract_current_hostname()
143	{
144		global $config;
145
146		// Get hostname
147		$host = (!empty($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : ((!empty($_SERVER['SERVER_NAME'])) ? $_SERVER['SERVER_NAME'] : getenv('SERVER_NAME'));
148
149		// Should be a string and lowered
150		$host = (string) strtolower($host);
151
152		// If host is equal the cookie domain or the server name (if config is set), then we assume it is valid
153		if ((isset($config['cookie_domain']) && $host === $config['cookie_domain']) || (isset($config['server_name']) && $host === $config['server_name']))
154		{
155			return $host;
156		}
157
158		// Is the host actually a IP? If so, we use the IP... (IPv4)
159		if (long2ip(ip2long($host)) === $host)
160		{
161			return $host;
162		}
163
164		// 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
165		$host = @parse_url('http://' . $host);
166		$host = (!empty($host['host'])) ? $host['host'] : '';
167
168		// Remove any portions not removed by parse_url (#)
169		$host = str_replace('#', '', $host);
170
171		// If, by any means, the host is now empty, we will use a "best approach" way to guess one
172		if (empty($host))
173		{
174			if (!empty($config['server_name']))
175			{
176				$host = $config['server_name'];
177			}
178			else if (!empty($config['cookie_domain']))
179			{
180				$host = (strpos($config['cookie_domain'], '.') === 0) ? substr($config['cookie_domain'], 1) : $config['cookie_domain'];
181			}
182			else
183			{
184				// Set to OS hostname or localhost
185				$host = (function_exists('php_uname')) ? php_uname('n') : 'localhost';
186			}
187		}
188
189		// It may be still no valid host, but for sure only a hostname (we may further expand on the cookie domain... if set)
190		return $host;
191	}
192
193	/**
194	* Start session management
195	*
196	* This is where all session activity begins. We gather various pieces of
197	* information from the client and server. We test to see if a session already
198	* exists. If it does, fine and dandy. If it doesn't we'll go on to create a
199	* new one ... pretty logical heh? We also examine the system load (if we're
200	* running on a system which makes such information readily available) and
201	* halt if it's above an admin definable limit.
202	*
203	* @param bool $update_session_page if true the session page gets updated.
204	*			This can be set to circumvent certain scripts to update the users last visited page.
205	*/
206	function session_begin($update_session_page = true)
207	{
208		global $phpEx, $SID, $_SID, $_EXTRA_URL, $db, $config, $phpbb_root_path;
209
210		// Give us some basic information
211		$this->time_now				= time();
212		$this->cookie_data			= array('u' => 0, 'k' => '');
213		$this->update_session_page	= $update_session_page;
214		$this->browser				= (!empty($_SERVER['HTTP_USER_AGENT'])) ? htmlspecialchars((string) $_SERVER['HTTP_USER_AGENT']) : '';
215		$this->referer				= (!empty($_SERVER['HTTP_REFERER'])) ? htmlspecialchars((string) $_SERVER['HTTP_REFERER']) : '';
216		$this->forwarded_for		= (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) ? htmlspecialchars((string) $_SERVER['HTTP_X_FORWARDED_FOR']) : '';
217
218		$this->host					= $this->extract_current_hostname();
219		$this->page					= $this->extract_current_page($phpbb_root_path);
220
221		// if the forwarded for header shall be checked we have to validate its contents
222		if ($config['forwarded_for_check'])
223		{
224			$this->forwarded_for = preg_replace('# {2,}#', ' ', str_replace(',', ' ', $this->forwarded_for));
225
226			// split the list of IPs
227			$ips = explode(' ', $this->forwarded_for);
228			foreach ($ips as $ip)
229			{
230				// check IPv4 first, the IPv6 is hopefully only going to be used very seldomly
231				if (!empty($ip) && !preg_match(get_preg_expression('ipv4'), $ip) && !preg_match(get_preg_expression('ipv6'), $ip))
232				{
233					// contains invalid data, don't use the forwarded for header
234					$this->forwarded_for = '';
235					break;
236				}
237			}
238		}
239		else
240		{
241			$this->forwarded_for = '';
242		}
243
244		if (isset($_COOKIE[$config['cookie_name'] . '_sid']) || isset($_COOKIE[$config['cookie_name'] . '_u']))
245		{
246			$this->cookie_data['u'] = request_var($config['cookie_name'] . '_u', 0, false, true);
247			$this->cookie_data['k'] = request_var($config['cookie_name'] . '_k', '', false, true);
248			$this->session_id 		= request_var($config['cookie_name'] . '_sid', '', false, true);
249
250			$SID = (defined('NEED_SID')) ? '?sid=' . $this->session_id : '?sid=';
251			$_SID = (defined('NEED_SID')) ? $this->session_id : '';
252
253			if (empty($this->session_id))
254			{
255				$this->session_id = $_SID = request_var('sid', '');
256				$SID = '?sid=' . $this->session_id;
257				$this->cookie_data = array('u' => 0, 'k' => '');
258			}
259		}
260		else
261		{
262			$this->session_id = $_SID = request_var('sid', '');
263			$SID = '?sid=' . $this->session_id;
264		}
265
266		$_EXTRA_URL = array();
267
268		// Why no forwarded_for et al? Well, too easily spoofed. With the results of my recent requests
269		// it's pretty clear that in the majority of cases you'll at least be left with a proxy/cache ip.
270		$this->ip = (!empty($_SERVER['REMOTE_ADDR'])) ? (string) $_SERVER['REMOTE_ADDR'] : '';
271		$this->ip = preg_replace('# {2,}#', ' ', str_replace(',', ' ', $this->ip));
272
273		// split the list of IPs
274		$ips = explode(' ', trim($this->ip));
275
276		// Default IP if REMOTE_ADDR is invalid
277		$this->ip = '127.0.0.1';
278
279		foreach ($ips as $ip)
280		{
281			if (preg_match(get_preg_expression('ipv4'), $ip))
282			{
283				$this->ip = $ip;
284			}
285			else if (preg_match(get_preg_expression('ipv6'), $ip))
286			{
287				// Quick check for IPv4-mapped address in IPv6
288				if (stripos($ip, '::ffff:') === 0)
289				{
290					$ipv4 = substr($ip, 7);
291
292					if (preg_match(get_preg_expression('ipv4'), $ipv4))
293					{
294						$ip = $ipv4;
295					}
296				}
297
298				$this->ip = $ip;
299			}
300			else
301			{
302				// We want to use the last valid address in the chain
303				// Leave foreach loop when address is invalid
304				break;
305			}
306		}
307
308		$this->load = false;
309
310		// Load limit check (if applicable)
311		if ($config['limit_load'] || $config['limit_search_load'])
312		{
313			if ((function_exists('sys_getloadavg') && $load = sys_getloadavg()) || ($load = explode(' ', @file_get_contents('/proc/loadavg'))))
314			{
315				$this->load = array_slice($load, 0, 1);
316				$this->load = floatval($this->load[0]);
317			}
318			else
319			{
320				set_config('limit_load', '0');
321				set_config('limit_search_load', '0');
322			}
323		}
324
325		// if no session id is set, redirect to index.php
326		if (defined('NEED_SID') && (!isset($_GET['sid']) || $this->session_id !== $_GET['sid']))
327		{
328			send_status_line(401, 'Unauthorized');
329			redirect(append_sid("{$phpbb_root_path}index.$phpEx"));
330		}
331
332		// if session id is set
333		if (!empty($this->session_id))
334		{
335			$sql = 'SELECT u.*, s.*
336				FROM ' . SESSIONS_TABLE . ' s, ' . USERS_TABLE . " u
337				WHERE s.session_id = '" . $db->sql_escape($this->session_id) . "'
338					AND u.user_id = s.session_user_id";
339			$result = $db->sql_query($sql);
340			$this->data = $db->sql_fetchrow($result);
341			$db->sql_freeresult($result);
342
343			// Did the session exist in the DB?
344			if (isset($this->data['user_id']))
345			{
346				// Validate IP length according to admin ... enforces an IP
347				// check on bots if admin requires this
348//				$quadcheck = ($config['ip_check_bot'] && $this->data['user_type'] & USER_BOT) ? 4 : $config['ip_check'];
349
350				if (strpos($this->ip, ':') !== false && strpos($this->data['session_ip'], ':') !== false)
351				{
352					$s_ip = short_ipv6($this->data['session_ip'], $config['ip_check']);
353					$u_ip = short_ipv6($this->ip, $config['ip_check']);
354				}
355				else
356				{
357					$s_ip = implode('.', array_slice(explode('.', $this->data['session_ip']), 0, $config['ip_check']));
358					$u_ip = implode('.', array_slice(explode('.', $this->ip), 0, $config['ip_check']));
359				}
360
361				$s_browser = ($config['browser_check']) ? trim(strtolower(substr($this->data['session_browser'], 0, 149))) : '';
362				$u_browser = ($config['browser_check']) ? trim(strtolower(substr($this->browser, 0, 149))) : '';
363
364				$s_forwarded_for = ($config['forwarded_for_check']) ? substr($this->data['session_forwarded_for'], 0, 254) : '';
365				$u_forwarded_for = ($config['forwarded_for_check']) ? substr($this->forwarded_for, 0, 254) : '';
366
367				// referer checks
368				// The @ before $config['referer_validation'] suppresses notices present while running the updater
369				$check_referer_path = (@$config['referer_validation'] == REFERER_VALIDATE_PATH);
370				$referer_valid = true;
371
372				// we assume HEAD and TRACE to be foul play and thus only whitelist GET
373				if (@$config['referer_validation'] && isset($_SERVER['REQUEST_METHOD']) && strtolower($_SERVER['REQUEST_METHOD']) !== 'get')
374				{
375					$referer_valid = $this->validate_referer($check_referer_path);
376				}
377
378				if ($u_ip === $s_ip && $s_browser === $u_browser && $s_forwarded_for === $u_forwarded_for && $referer_valid)
379				{
380					$session_expired = false;
381
382					// Check whether the session is still valid if we have one
383					$method = basename(trim($config['auth_method']));
384					include_once($phpbb_root_path . 'includes/auth/auth_' . $method . '.' . $phpEx);
385
386					$method = 'validate_session_' . $method;
387					if (function_exists($method))
388					{
389						if (!$method($this->data))
390						{
391							$session_expired = true;
392						}
393					}
394
395					if (!$session_expired)
396					{
397						// Check the session length timeframe if autologin is not enabled.
398						// Else check the autologin length... and also removing those having autologin enabled but no longer allowed board-wide.
399						if (!$this->data['session_autologin'])
400						{
401							if ($this->data['session_time'] < $this->time_now - ($config['session_length'] + 60))
402							{
403								$session_expired = true;
404							}
405						}
406						else if (!$config['allow_autologin'] || ($config['max_autologin_time'] && $this->data['session_time'] < $this->time_now - (86400 * (int) $config['max_autologin_time']) + 60))
407						{
408							$session_expired = true;
409						}
410					}
411
412					if (!$session_expired)
413					{
414						// Only update session DB a minute or so after last update or if page changes
415						if ($this->time_now - $this->data['session_time'] > 60 || ($this->update_session_page && $this->data['session_page'] != $this->page['page']))
416						{
417							$sql_ary = array('session_time' => $this->time_now);
418
419							if ($this->update_session_page)
420							{
421								$sql_ary['session_page'] = substr($this->page['page'], 0, 199);
422								$sql_ary['session_forum_id'] = $this->page['forum'];
423							}
424
425							$db->sql_return_on_error(true);
426
427							$sql = 'UPDATE ' . SESSIONS_TABLE . ' SET ' . $db->sql_build_array('UPDATE', $sql_ary) . "
428								WHERE session_id = '" . $db->sql_escape($this->session_id) . "'";
429							$result = $db->sql_query($sql);
430
431							$db->sql_return_on_error(false);
432
433							// If the database is not yet updated, there will be an error due to the session_forum_id
434							// @todo REMOVE for 3.0.2
435							if ($result === false)
436							{
437								unset($sql_ary['session_forum_id']);
438
439								$sql = 'UPDATE ' . SESSIONS_TABLE . ' SET ' . $db->sql_build_array('UPDATE', $sql_ary) . "
440									WHERE session_id = '" . $db->sql_escape($this->session_id) . "'";
441								$db->sql_query($sql);
442							}
443
444							if ($this->data['user_id'] != ANONYMOUS && !empty($config['new_member_post_limit']) && $this->data['user_new'] && $config['new_member_post_limit'] <= $this->data['user_posts'])
445							{
446								$this->leave_newly_registered();
447							}
448						}
449
450						$this->data['is_registered'] = ($this->data['user_id'] != ANONYMOUS && ($this->data['user_type'] == USER_NORMAL || $this->data['user_type'] == USER_FOUNDER)) ? true : false;
451						$this->data['is_bot'] = (!$this->data['is_registered'] && $this->data['user_id'] != ANONYMOUS) ? true : false;
452						$this->data['user_lang'] = basename($this->data['user_lang']);
453
454						return true;
455					}
456				}
457				else
458				{
459					// Added logging temporarly to help debug bugs...
460					if (defined('DEBUG_EXTRA') && $this->data['user_id'] != ANONYMOUS)
461					{
462						if ($referer_valid)
463						{
464							add_log('critical', 'LOG_IP_BROWSER_FORWARDED_CHECK', $u_ip, $s_ip, $u_browser, $s_browser, htmlspecialchars($u_forwarded_for), htmlspecialchars($s_forwarded_for));
465						}
466						else
467						{
468							add_log('critical', 'LOG_REFERER_INVALID', $this->referer);
469						}
470					}
471				}
472			}
473		}
474
475		// If we reach here then no (valid) session exists. So we'll create a new one
476		return $this->session_create();
477	}
478
479	/**
480	* Create a new session
481	*
482	* If upon trying to start a session we discover there is nothing existing we
483	* jump here. Additionally this method is called directly during login to regenerate
484	* the session for the specific user. In this method we carry out a number of tasks;
485	* garbage collection, (search)bot checking, banned user comparison. Basically
486	* though this method will result in a new session for a specific user.
487	*/
488	function session_create($user_id = false, $set_admin = false, $persist_login = false, $viewonline = true)
489	{
490		global $SID, $_SID, $db, $config, $cache, $phpbb_root_path, $phpEx;
491
492		$this->data = array();
493
494		/* Garbage collection ... remove old sessions updating user information
495		// if necessary. It means (potentially) 11 queries but only infrequently
496		if ($this->time_now > $config['session_last_gc'] + $config['session_gc'])
497		{
498			$this->session_gc();
499		}*/
500
501		// Do we allow autologin on this board? No? Then override anything
502		// that may be requested here
503		if (!$config['allow_autologin'])
504		{
505			$this->cookie_data['k'] = $persist_login = false;
506		}
507
508		/**
509		* Here we do a bot check, oh er saucy! No, not that kind of bot
510		* check. We loop through the list of bots defined by the admin and
511		* see if we have any useragent and/or IP matches. If we do, this is a
512		* bot, act accordingly
513		*/
514		$bot = false;
515		$active_bots = $cache->obtain_bots();
516
517		foreach ($active_bots as $row)
518		{
519			if ($row['bot_agent'] && preg_match('#' . str_replace('\*', '.*?', preg_quote($row['bot_agent'], '#')) . '#i', $this->browser))
520			{
521				$bot = $row['user_id'];
522			}
523
524			// If ip is supplied, we will make sure the ip is matching too...
525			if ($row['bot_ip'] && ($bot || !$row['bot_agent']))
526			{
527				// Set bot to false, then we only have to set it to true if it is matching
528				$bot = false;
529
530				foreach (explode(',', $row['bot_ip']) as $bot_ip)
531				{
532					$bot_ip = trim($bot_ip);
533
534					if (!$bot_ip)
535					{
536						continue;
537					}
538
539					if (strpos($this->ip, $bot_ip) === 0)
540					{
541						$bot = (int) $row['user_id'];
542						break;
543					}
544				}
545			}
546
547			if ($bot)
548			{
549				break;
550			}
551		}
552
553		$method = basename(trim($config['auth_method']));
554		include_once($phpbb_root_path . 'includes/auth/auth_' . $method . '.' . $phpEx);
555
556		$method = 'autologin_' . $method;
557		if (function_exists($method))
558		{
559			$this->data = $method();
560
561			if (sizeof($this->data))
562			{
563				$this->cookie_data['k'] = '';
564				$this->cookie_data['u'] = $this->data['user_id'];
565			}
566		}
567
568		// If we're presented with an autologin key we'll join against it.
569		// Else if we've been passed a user_id we'll grab data based on that
570		if (isset($this->cookie_data['k']) && $this->cookie_data['k'] && $this->cookie_data['u'] && !sizeof($this->data))
571		{
572			$sql = 'SELECT u.*
573				FROM ' . USERS_TABLE . ' u, ' . SESSIONS_KEYS_TABLE . ' k
574				WHERE u.user_id = ' . (int) $this->cookie_data['u'] . '
575					AND u.user_type IN (' . USER_NORMAL . ', ' . USER_FOUNDER . ")
576					AND k.user_id = u.user_id
577					AND k.key_id = '" . $db->sql_escape(md5($this->cookie_data['k'])) . "'";
578			$result = $db->sql_query($sql);
579			$this->data = $db->sql_fetchrow($result);
580			$db->sql_freeresult($result);
581			$bot = false;
582		}
583		else if ($user_id !== false && !sizeof($this->data))
584		{
585			$this->cookie_data['k'] = '';
586			$this->cookie_data['u'] = $user_id;
587
588			$sql = 'SELECT *
589				FROM ' . USERS_TABLE . '
590				WHERE user_id = ' . (int) $this->cookie_data['u'] . '
591					AND user_type IN (' . USER_NORMAL . ', ' . USER_FOUNDER . ')';
592			$result = $db->sql_query($sql);
593			$this->data = $db->sql_fetchrow($result);
594			$db->sql_freeresult($result);
595			$bot = false;
596		}
597
598		// Bot user, if they have a SID in the Request URI we need to get rid of it
599		// otherwise they'll index this page with the SID, duplicate content oh my!
600		if ($bot && isset($_GET['sid']))
601		{
602			send_status_line(301, 'Moved Permanently');
603			redirect(build_url(array('sid')));
604		}
605
606		// If no data was returned one or more of the following occurred:
607		// Key didn't match one in the DB
608		// User does not exist
609		// User is inactive
610		// User is bot
611		if (!sizeof($this->data) || !is_array($this->data))
612		{
613			$this->cookie_data['k'] = '';
614			$this->cookie_data['u'] = ($bot) ? $bot : ANONYMOUS;
615
616			if (!$bot)
617			{
618				$sql = 'SELECT *
619					FROM ' . USERS_TABLE . '
620					WHERE user_id = ' . (int) $this->cookie_data['u'];
621			}
622			else
623			{
624				// We give bots always the same session if it is not yet expired.
625				$sql = 'SELECT u.*, s.*
626					FROM ' . USERS_TABLE . ' u
627					LEFT JOIN ' . SESSIONS_TABLE . ' s ON (s.session_user_id = u.user_id)
628					WHERE u.user_id = ' . (int) $bot;
629			}
630
631			$result = $db->sql_query($sql);
632			$this->data = $db->sql_fetchrow($result);
633			$db->sql_freeresult($result);
634		}
635
636		if ($this->data['user_id'] != ANONYMOUS && !$bot)
637		{
638			$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());
639		}
640		else
641		{
642			$this->data['session_last_visit'] = $this->time_now;
643		}
644
645		// Force user id to be integer...
646		$this->data['user_id'] = (int) $this->data['user_id'];
647
648		// At this stage we should have a filled data array, defined cookie u and k data.
649		// data array should contain recent session info if we're a real user and a recent
650		// session exists in which case session_id will also be set
651
652		// Is user banned? Are they excluded? Won't return on ban, exists within method
653		if ($this->data['user_type'] != USER_FOUNDER)
654		{
655			if (!$config['forwarded_for_check'])
656			{
657				$this->check_ban($this->data['user_id'], $this->ip);
658			}
659			else
660			{
661				$ips = explode(' ', $this->forwarded_for);
662				$ips[] = $this->ip;
663				$this->check_ban($this->data['user_id'], $ips);
664			}
665		}
666
667		$this->data['is_registered'] = (!$bot && $this->data['user_id'] != ANONYMOUS && ($this->data['user_type'] == USER_NORMAL || $this->data['user_type'] == USER_FOUNDER)) ? true : false;
668		$this->data['is_bot'] = ($bot) ? true : false;
669
670		// If our friend is a bot, we re-assign a previously assigned session
671		if ($this->data['is_bot'] && $bot == $this->data['user_id'] && $this->data['session_id'])
672		{
673			// Only assign the current session if the ip, browser and forwarded_for match...
674			if (strpos($this->ip, ':') !== false && strpos($this->data['session_ip'], ':') !== false)
675			{
676				$s_ip = short_ipv6($this->data['session_ip'], $config['ip_check']);
677				$u_ip = short_ipv6($this->ip, $config['ip_check']);
678			}
679			else
680			{
681				$s_ip = implode('.', array_slice(explode('.', $this->data['session_ip']), 0, $config['ip_check']));
682				$u_ip = implode('.', array_slice(explode('.', $this->ip), 0, $config['ip_check']));
683			}
684
685			$s_browser = ($config['browser_check']) ? trim(strtolower(substr($this->data['session_browser'], 0, 149))) : '';
686			$u_browser = ($config['browser_check']) ? trim(strtolower(substr($this->browser, 0, 149))) : '';
687
688			$s_forwarded_for = ($config['forwarded_for_check']) ? substr($this->data['session_forwarded_for'], 0, 254) : '';
689			$u_forwarded_for = ($config['forwarded_for_check']) ? substr($this->forwarded_for, 0, 254) : '';
690
691			if ($u_ip === $s_ip && $s_browser === $u_browser && $s_forwarded_for === $u_forwarded_for)
692			{
693				$this->session_id = $this->data['session_id'];
694
695				// Only update session DB a minute or so after last update or if page changes
696				if ($this->time_now - $this->data['session_time'] > 60 || ($this->update_session_page && $this->data['session_page'] != $this->page['page']))
697				{
698					$this->data['session_time'] = $this->data['session_last_visit'] = $this->time_now;
699
700					$sql_ary = array('session_time' => $this->time_now, 'session_last_visit' => $this->time_now, 'session_admin' => 0);
701
702					if ($this->update_session_page)
703					{
704						$sql_ary['session_page'] = substr($this->page['page'], 0, 199);
705						$sql_ary['session_forum_id'] = $this->page['forum'];
706					}
707
708					$sql = 'UPDATE ' . SESSIONS_TABLE . ' SET ' . $db->sql_build_array('UPDATE', $sql_ary) . "
709						WHERE session_id = '" . $db->sql_escape($this->session_id) . "'";
710					$db->sql_query($sql);
711
712					// Update the last visit time
713					$sql = 'UPDATE ' . USERS_TABLE . '
714						SET user_lastvisit = ' . (int) $this->data['session_time'] . '
715						WHERE user_id = ' . (int) $this->data['user_id'];
716					$db->sql_query($sql);
717				}
718
719				$SID = '?sid=';
720				$_SID = '';
721				return true;
722			}
723			else
724			{
725				// If the ip and browser does not match make sure we only have one bot assigned to one session
726				$db->sql_query('DELETE FROM ' . SESSIONS_TABLE . ' WHERE session_user_id = ' . $this->data['user_id']);
727			}
728		}
729
730		$session_autologin = (($this->cookie_data['k'] || $persist_login) && $this->data['is_registered']) ? true : false;
731		$set_admin = ($set_admin && $this->data['is_registered']) ? true : false;
732
733		// Create or update the session
734		$sql_ary = array(
735			'session_user_id'		=> (int) $this->data['user_id'],
736			'session_start'			=> (int) $this->time_now,
737			'session_last_visit'	=> (int) $this->data['session_last_visit'],
738			'session_time'			=> (int) $this->time_now,
739			'session_browser'		=> (string) trim(substr($this->browser, 0, 149)),
740			'session_forwarded_for'	=> (string) $this->forwarded_for,
741			'session_ip'			=> (string) $this->ip,
742			'session_autologin'		=> ($session_autologin) ? 1 : 0,
743			'session_admin'			=> ($set_admin) ? 1 : 0,
744			'session_viewonline'	=> ($viewonline) ? 1 : 0,
745		);
746
747		if ($this->update_session_page)
748		{
749			$sql_ary['session_page'] = (string) substr($this->page['page'], 0, 199);
750			$sql_ary['session_forum_id'] = $this->page['forum'];
751		}
752
753		$db->sql_return_on_error(true);
754
755		$sql = 'DELETE
756			FROM ' . SESSIONS_TABLE . '
757			WHERE session_id = \'' . $db->sql_escape($this->session_id) . '\'
758				AND session_user_id = ' . ANONYMOUS;
759
760		if (!defined('IN_ERROR_HANDLER') && (!$this->session_id || !$db->sql_query($sql) || !$db->sql_affectedrows()))
761		{
762			// Limit new sessions in 1 minute period (if required)
763			if (empty($this->data['session_time']) && $config['active_sessions'])
764			{
765//				$db->sql_return_on_error(false);
766
767				$sql = 'SELECT COUNT(session_id) AS sessions
768					FROM ' . SESSIONS_TABLE . '
769					WHERE session_time >= ' . ($this->time_now - 60);
770				$result = $db->sql_query($sql);
771				$row = $db->sql_fetchrow($result);
772				$db->sql_freeresult($result);
773
774				if ((int) $row['sessions'] > (int) $config['active_sessions'])
775				{
776					send_status_line(503, 'Service Unavailable');
777					trigger_error('BOARD_UNAVAILABLE');
778				}
779			}
780		}
781
782		// Since we re-create the session id here, the inserted row must be unique. Therefore, we display potential errors.
783		// Commented out because it will not allow forums to update correctly
784//		$db->sql_return_on_error(false);
785
786		// Something quite important: session_page always holds the *last* page visited, except for the *first* visit.
787		// We are not able to simply have an empty session_page btw, therefore we need to tell phpBB how to detect this special case.
788		// 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.
789		if (empty($this->data['session_id']))
790		{
791			// This is a temporary variable, only set for the very first visit
792			$this->data['session_created'] = true;
793		}
794
795		$this->session_id = $this->data['session_id'] = md5(unique_id());
796
797		$sql_ary['session_id'] = (string) $this->session_id;
798		$sql_ary['session_page'] = (string) substr($this->page['page'], 0, 199);
799		$sql_ary['session_forum_id'] = $this->page['forum'];
800
801		$sql = 'INSERT INTO ' . SESSIONS_TABLE . ' ' . $db->sql_build_array('INSERT', $sql_ary);
802		$db->sql_query($sql);
803
804		$db->sql_return_on_error(false);
805
806		// Regenerate autologin/persistent login key
807		if ($session_autologin)
808		{
809			$this->set_login_key();
810		}
811
812		// refresh data
813		$SID = '?sid=' . $this->session_id;
814		$_SID = $this->session_id;
815		$this->data = array_merge($this->data, $sql_ary);
816
817		if (!$bot)
818		{
819			$cookie_expire = $this->time_now + (($config['max_autologin_time']) ? 86400 * (int) $config['max_autologin_time'] : 31536000);
820
821			$this->set_cookie('u', $this->cookie_data['u'], $cookie_expire);
822			$this->set_cookie('k', $this->cookie_data['k'], $cookie_expire);
823			$this->set_cookie('sid', $this->session_id, $cookie_expire);
824
825			unset($cookie_expire);
826
827			$sql = 'SELECT COUNT(session_id) AS sessions
828					FROM ' . SESSIONS_TABLE . '
829					WHERE session_user_id = ' . (int) $this->data['user_id'] . '
830					AND session_time >= ' . (int) ($this->time_now - (max($config['session_length'], $config['form_token_lifetime'])));
831			$result = $db->sql_query($sql);
832			$row = $db->sql_fetchrow($result);
833			$db->sql_freeresult($result);
834
835			if ((int) $row['sessions'] <= 1 || empty($this->data['user_form_salt']))
836			{
837				$this->data['user_form_salt'] = unique_id();
838				// Update the form key
839				$sql = 'UPDATE ' . USERS_TABLE . '
840					SET user_form_salt = \'' . $db->sql_escape($this->data['user_form_salt']) . '\'
841					WHERE user_id = ' . (int) $this->data['user_id'];
842				$db->sql_query($sql);
843			}
844		}
845		else
846		{
847			$this->data['session_time'] = $this->data['session_last_visit'] = $this->time_now;
848
849			// Update the last visit time
850			$sql = 'UPDATE ' . USERS_TABLE . '
851				SET user_lastvisit = ' . (int) $this->data['session_time'] . '
852				WHERE user_id = ' . (int) $this->data['user_id'];
853			$db->sql_query($sql);
854
855			$SID = '?sid=';
856			$_SID = '';
857		}
858
859		return true;
860	}
861
862	/**
863	* Kills a session
864	*
865	* This method does what it says on the tin. It will delete a pre-existing session.
866	* It resets cookie information (destroying any autologin key within that cookie data)
867	* and update the users information from the relevant session data. It will then
868	* grab guest user information.
869	*/
870	function session_kill($new_session = true)
871	{
872		global $SID, $_SID, $db, $config, $phpbb_root_path, $phpEx;
873
874		$sql = 'DELETE FROM ' . SESSIONS_TABLE . "
875			WHERE session_id = '" . $db->sql_escape($this->session_id) . "'
876				AND session_user_id = " . (int) $this->data['user_id'];
877		$db->sql_query($sql);
878
879		// Allow connecting logout with external auth method logout
880		$method = basename(trim($config['auth_method']));
881		include_once($phpbb_root_path . 'includes/auth/auth_' . $method . '.' . $phpEx);
882
883		$method = 'logout_' . $method;
884		if (function_exists($method))
885		{
886			$method($this->data, $new_session);
887		}
888
889		if ($this->data['user_id'] != ANONYMOUS)
890		{
891			// Delete existing session, update last visit info first!
892			if (!isset($this->data['session_time']))
893			{
894				$this->data['session_time'] = time();
895			}
896
897			$sql = 'UPDATE ' . USERS_TABLE . '
898				SET user_lastvisit = ' . (int) $this->data['session_time'] . '
899				WHERE user_id = ' . (int) $this->data['user_id'];
900			$db->sql_query($sql);
901
902			if ($this->cookie_data['k'])
903			{
904				$sql = 'DELETE FROM ' . SESSIONS_KEYS_TABLE . '
905					WHERE user_id = ' . (int) $this->data['user_id'] . "
906						AND key_id = '" . $db->sql_escape(md5($this->cookie_data['k'])) . "'";
907				$db->sql_query($sql);
908			}
909
910			// Reset the data array
911			$this->data = array();
912
913			$sql = 'SELECT *
914				FROM ' . USERS_TABLE . '
915				WHERE user_id = ' . ANONYMOUS;
916			$result = $db->sql_query($sql);
917			$this->data = $db->sql_fetchrow($result);
918			$db->sql_freeresult($result);
919		}
920
921		$cookie_expire = $this->time_now - 31536000;
922		$this->set_cookie('u', '', $cookie_expire);
923		$this->set_cookie('k', '', $cookie_expire);
924		$this->set_cookie('sid', '', $cookie_expire);
925		unset($cookie_expire);
926
927		$SID = '?sid=';
928		$this->session_id = $_SID = '';
929
930		// To make sure a valid session is created we create one for the anonymous user
931		if ($new_session)
932		{
933			$this->session_create(ANONYMOUS);
934		}
935
936		return true;
937	}
938
939	/**
940	* Session garbage collection
941	*
942	* This looks a lot more complex than it really is. Effectively we are
943	* deleting any sessions older than an admin definable limit. Due to the
944	* way in which we maintain session data we have to ensure we update user
945	* data before those sessions are destroyed. In addition this method
946	* removes autologin key information that is older than an admin defined
947	* limit.
948	*/
949	function session_gc()
950	{
951		global $db, $config, $phpbb_root_path, $phpEx;
952
953		$batch_size = 10;
954
955		if (!$this->time_now)
956		{
957			$this->time_now = time();
958		}
959
960		// Firstly, delete guest sessions
961		$sql = 'DELETE FROM ' . SESSIONS_TABLE . '
962			WHERE session_user_id = ' . ANONYMOUS . '
963				AND session_time < ' . (int) ($this->time_now - $config['session_length']);
964		$db->sql_query($sql);
965
966		// Get expired sessions, only most recent for each user
967		$sql = 'SELECT session_user_id, session_page, MAX(session_time) AS recent_time
968			FROM ' . SESSIONS_TABLE . '
969			WHERE session_time < ' . ($this->time_now - $config['session_length']) . '
970			GROUP BY session_user_id, session_page';
971		$result = $db->sql_query_limit($sql, $batch_size);
972
973		$del_user_id = array();
974		$del_sessions = 0;
975
976		while ($row = $db->sql_fetchrow($result))
977		{
978			$sql = 'UPDATE ' . USERS_TABLE . '
979				SET user_lastvisit = ' . (int) $row['recent_time'] . ", user_lastpage = '" . $db->sql_escape($row['session_page']) . "'
980				WHERE user_id = " . (int) $row['session_user_id'];
981			$db->sql_query($sql);
982
983			$del_user_id[] = (int) $row['session_user_id'];
984			$del_sessions++;
985		}
986		$db->sql_freeresult($result);
987
988		if (sizeof($del_user_id))
989		{
990			// Delete expired sessions
991			$sql = 'DELETE FROM ' . SESSIONS_TABLE . '
992				WHERE ' . $db->sql_in_set('session_user_id', $del_user_id) . '
993					AND session_time < ' . ($this->time_now - $config['session_length']);
994			$db->sql_query($sql);
995		}
996
997		if ($del_sessions < $batch_size)
998		{
999			// Less than 10 users, update gc timer ... else we want gc
1000			// called again to delete other sessions
1001			set_config('session_last_gc', $this->time_now, true);
1002
1003			if ($config['max_autologin_time'])
1004			{
1005				$sql = 'DELETE FROM ' . SESSIONS_KEYS_TABLE . '
1006					WHERE last_login < ' . (time() - (86400 * (int) $config['max_autologin_time']));
1007				$db->sql_query($sql);
1008			}
1009
1010			// only called from CRON; should be a safe workaround until the infrastructure gets going
1011			if (!class_exists('phpbb_captcha_factory'))
1012			{
1013				include($phpbb_root_path . "includes/captcha/captcha_factory." . $phpEx);
1014			}
1015			phpbb_captcha_factory::garbage_collect($config['captcha_plugin']);
1016
1017			$sql = 'DELETE FROM ' . LOGIN_ATTEMPT_TABLE . '
1018				WHERE attempt_time < ' . (time() - (int) $config['ip_login_limit_time']);
1019			$db->sql_query($sql);
1020		}
1021
1022		return;
1023	}
1024
1025	/**
1026	* Sets a cookie
1027	*
1028	* 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.
1029	*
1030	* @param string $name		Name of the cookie, will be automatically prefixed with the phpBB cookie name. track becomes [cookie_name]_track then.
1031	* @param string $cookiedata	The data to hold within the cookie
1032	* @param int $cookietime	The expiration time as UNIX timestamp. If 0 is provided, a session cookie is set.
1033	*/
1034	function set_cookie($name, $cookiedata, $cookietime)
1035	{
1036		global $config;
1037
1038		$name_data = rawurlencode($config['cookie_name'] . '_' . $name) . '=' . rawurlencode($cookiedata);
1039		$expire = gmdate('D, d-M-Y H:i:s \\G\\M\\T', $cookietime);
1040		$domain = (!$config['cookie_domain'] || $config['cookie_domain'] == 'localhost' || $config['cookie_domain'] == '127.0.0.1') ? '' : '; domain=' . $config['cookie_domain'];
1041
1042		header('Set-Cookie: ' . $name_data . (($cookietime) ? '; expires=' . $expire : '') . '; path=' . $config['cookie_path'] . $domain . ((!$config['cookie_secure']) ? '' : '; secure') . '; HttpOnly', false);
1043	}
1044
1045	/**
1046	* Check for banned user
1047	*
1048	* Checks whether the supplied user is banned by id, ip or email. If no parameters
1049	* are passed to the method pre-existing session data is used. If $return is false
1050	* this routine does not return on finding a banned user, it outputs a relevant
1051	* message and stops execution.
1052	*
1053	* @param string|array	$user_ips	Can contain a string with one IP or an array of multiple IPs
1054	*/
1055	function check_ban($user_id = false, $user_ips = false, $user_email = false, $return = false)
1056	{
1057		global $config, $db;
1058
1059		if (defined('IN_CHECK_BAN'))
1060		{
1061			return;
1062		}
1063
1064		$banned = false;
1065		$cache_ttl = 3600;
1066		$where_sql = array();
1067
1068		$sql = 'SELECT ban_ip, ban_userid, ban_email, ban_exclude, ban_give_reason, ban_end
1069			FROM ' . BANLIST_TABLE . '
1070			WHERE ';
1071
1072		// Determine which entries to check, only return those
1073		if ($user_email === false)
1074		{
1075			$where_sql[] = "ban_email = ''";
1076		}
1077
1078		if ($user_ips === false)
1079		{
1080			$where_sql[] = "(ban_ip = '' OR ban_exclude = 1)";
1081		}
1082
1083		if ($user_id === false)
1084		{
1085			$where_sql[] = '(ban_userid = 0 OR ban_exclude = 1)';
1086		}
1087		else
1088		{
1089			$cache_ttl = ($user_id == ANONYMOUS) ? 3600 : 0;
1090			$_sql = '(ban_userid = ' . $user_id;
1091
1092			if ($user_email !== false)
1093			{
1094				$_sql .= " OR ban_email <> ''";
1095			}
1096
1097			if ($user_ips !== false)
1098			{
1099				$_sql .= " OR ban_ip <> ''";
1100			}
1101
1102			$_sql .= ')';
1103
1104			$where_sql[] = $_sql;
1105		}
1106
1107		$sql .= (sizeof($where_sql)) ? implode(' AND ', $where_sql) : '';
1108		$result = $db->sql_query($sql, $cache_ttl);
1109
1110		$ban_triggered_by = 'user';
1111		while ($row = $db->sql_fetchrow($result))
1112		{
1113			if ($row['ban_end'] && $row['ban_end'] < time())
1114			{
1115				continue;
1116			}
1117
1118			$ip_banned = false;
1119			if (!empty($row['ban_ip']))
1120			{
1121				if (!is_array($user_ips))
1122				{
1123					$ip_banned = preg_match('#^' . str_replace('\*', '.*?', preg_quote($row['ban_ip'], '#')) . '$#i', $user_ips);
1124				}
1125				else
1126				{
1127					foreach ($user_ips as $user_ip)
1128					{
1129						if (preg_match('#^' . str_replace('\*', '.*?', preg_quote($row['ban_ip'], '#')) . '$#i', $user_ip))
1130						{
1131							$ip_banned = true;
1132							break;
1133						}
1134					}
1135				}
1136			}
1137
1138			if ((!empty($row['ban_userid']) && intval($row['ban_userid']) == $user_id) ||
1139				$ip_banned ||
1140				(!empty($row['ban_email']) && preg_match('#^' . str_replace('\*', '.*?', preg_quote($row['ban_email'], '#')) . '$#i', $user_email)))
1141			{
1142				if (!empty($row['ban_exclude']))
1143				{
1144					$banned = false;
1145					break;
1146				}
1147				else
1148				{
1149					$banned = true;
1150					$ban_row = $row;
1151
1152					if (!empty($row['ban_userid']) && intval($row['ban_userid']) == $user_id)
1153					{
1154						$ban_triggered_by = 'user';
1155					}
1156					else if ($ip_banned)
1157					{
1158						$ban_triggered_by = 'ip';
1159					}
1160					else
1161					{
1162						$ban_triggered_by = 'email';
1163					}
1164
1165					// Don't break. Check if there is an exclude rule for this user
1166				}
1167			}
1168		}
1169		$db->sql_freeresult($result);
1170
1171		if ($banned && !$return)
1172		{
1173			global $template;
1174
1175			// If the session is empty we need to create a valid one...
1176			if (empty($this->session_id))
1177			{
1178				// This seems to be no longer needed? - #14971
1179//				$this->session_create(ANONYMOUS);
1180			}
1181
1182			// Initiate environment ... since it won't be set at this stage
1183			$this->setup();
1184
1185			// Logout the user, banned users are unable to use the normal 'logout' link
1186			if ($this->data['user_id'] != ANONYMOUS)
1187			{
1188				$this->session_kill();
1189			}
1190
1191			// We show a login box here to allow founders accessing the board if banned by IP
1192			if (defined('IN_LOGIN') && $this->data['user_id'] == ANONYMOUS)
1193			{
1194				global $phpEx;
1195
1196				$this->setup('ucp');
1197				$this->data['is_registered'] = $this->data['is_bot'] = false;
1198
1199				// Set as a precaution to allow login_box() handling this case correctly as well as this function not being executed again.
1200				define('IN_CHECK_BAN', 1);
1201
1202				login_box("index.$phpEx");
1203
1204				// The false here is needed, else the user is able to circumvent the ban.
1205				$this->session_kill(false);
1206			}
1207
1208			// Ok, we catch the case of an empty session id for the anonymous user...
1209			// This can happen if the user is logging in, banned by username and the login_box() being called "again".
1210			if (empty($this->session_id) && defined('IN_CHECK_BAN'))
1211			{
1212				$this->session_create(ANONYMOUS);
1213			}
1214
1215
1216			// Determine which message to output
1217			$till_date = ($ban_row['ban_end']) ? $this->format_date($ban_row['ban_end']) : '';
1218			$message = ($ban_row['ban_end']) ? 'BOARD_BAN_TIME' : 'BOARD_BAN_PERM';
1219
1220			$message = sprintf($this->lang[$message], $till_date, '<a href="mailto:' . $config['board_contact'] . '">', '</a>');
1221			$message .= ($ban_row['ban_give_reason']) ? '<br /><br />' . sprintf($this->lang['BOARD_BAN_REASON'], $ban_row['ban_give_reason']) : '';
1222			$message .= '<br /><br /><em>' . $this->lang['BAN_TRIGGERED_BY_' . strtoupper($ban_triggered_by)] . '</em>';
1223
1224			// To circumvent session_begin returning a valid value and the check_ban() not called on second page view, we kill the session again
1225			$this->session_kill(false);
1226
1227			// A very special case... we are within the cron script which is not supposed to print out the ban message... show blank page
1228			if (defined('IN_CRON'))
1229			{
1230				garbage_collection();
1231				exit_handler();
1232				exit;
1233			}
1234
1235			trigger_error($message);
1236		}
1237
1238		return ($banned && $ban_row['ban_give_reason']) ? $ban_row['ban_give_reason'] : $banned;
1239	}
1240
1241	/**
1242	* Check if ip is blacklisted
1243	* This should be called only where absolutly necessary
1244	*
1245	* Only IPv4 (rbldns does not support AAAA records/IPv6 lookups)
1246	*
1247	* @author satmd (from the php manual)
1248	* @param string $mode register/post - spamcop for example is ommitted for posting
1249	* @return false if ip is not blacklisted, else an array([checked server], [lookup])
1250	*/
1251	function check_dnsbl($mode, $ip = false)
1252	{
1253		if ($ip === false)
1254		{
1255			$ip = $this->ip;
1256		}
1257
1258		// Neither Spamhaus nor Spamcop supports IPv6 addresses.
1259		if (strpos($ip, ':') !== false)
1260		{
1261			return false;
1262		}
1263
1264		$dnsbl_check = array(
1265			'sbl.spamhaus.org'	=> 'http://www.spamhaus.org/query/bl?ip=',
1266		);
1267
1268		if ($mode == 'register')
1269		{
1270			$dnsbl_check['bl.spamcop.net'] = 'http://spamcop.net/bl.shtml?';
1271		}
1272
1273		if ($ip)
1274		{
1275			$quads = explode('.', $ip);
1276			$reverse_ip = $quads[3] . '.' . $quads[2] . '.' . $quads[1] . '.' . $quads[0];
1277
1278			// Need to be listed on all servers...
1279			$listed = true;
1280			$info = array();
1281
1282			foreach ($dnsbl_check as $dnsbl => $lookup)
1283			{
1284				if (phpbb_checkdnsrr($reverse_ip . '.' . $dnsbl . '.', 'A') === true)
1285				{
1286					$info = array($dnsbl, $lookup . $ip);
1287				}
1288				else
1289				{
1290					$listed = false;
1291				}
1292			}
1293
1294			if ($listed)
1295			{
1296				return $info;
1297			}
1298		}
1299
1300		return false;
1301	}
1302
1303	/**
1304	* Check if URI is blacklisted
1305	* This should be called only where absolutly necessary, for example on the submitted website field
1306	* This function is not in use at the moment and is only included for testing purposes, it may not work at all!
1307	* This means it is untested at the moment and therefore commented out
1308	*
1309	* @param string $uri URI to check
1310	* @return true if uri is on blacklist, else false. Only blacklist is checked (~zero FP), no grey lists
1311	function check_uribl($uri)
1312	{
1313		// Normally parse_url() is not intended to parse uris
1314		// We need to get the top-level domain name anyway... change.
1315		$uri = parse_url($uri);
1316
1317		if ($uri === false || empty($uri['host']))
1318		{
1319			return false;
1320		}
1321
1322		$uri = trim($uri['host']);
1323
1324		if ($uri)
1325		{
1326			// One problem here... the return parameter for the "windows" method is different from what
1327			// we expect... this may render this check useless...
1328			if (phpbb_checkdnsrr($uri . '.multi.uribl.com.', 'A') === true)
1329			{
1330				return true;
1331			}
1332		}
1333
1334		return false;
1335	}
1336	*/
1337
1338	/**
1339	* Set/Update a persistent login key
1340	*
1341	* This method creates or updates a persistent session key. When a user makes
1342	* use of persistent (formerly auto-) logins a key is generated and stored in the
1343	* DB. When they revisit with the same key it's automatically updated in both the
1344	* DB and cookie. Multiple keys may exist for each user representing different
1345	* browsers or locations. As with _any_ non-secure-socket no passphrase login this
1346	* remains vulnerable to exploit.
1347	*/
1348	function set_login_key($user_id = false, $key = false, $user_ip = false)
1349	{
1350		global $config, $db;
1351
1352		$user_id = ($user_id === false) ? $this->data['user_id'] : $user_id;
1353		$user_ip = ($user_ip === false) ? $this->ip : $user_ip;
1354		$key = ($key === false) ? (($this->cookie_data['k']) ? $this->cookie_data['k'] : false) : $key;
1355
1356		$key_id = unique_id(hexdec(substr($this->session_id, 0, 8)));
1357
1358		$sql_ary = array(
1359			'key_id'		=> (string) md5($key_id),
1360			'last_ip'		=> (string) $this->ip,
1361			'last_login'	=> (int) time()
1362		);
1363
1364		if (!$key)
1365		{
1366			$sql_ary += array(
1367				'user_id'	=> (int) $user_id
1368			);
1369		}
1370
1371		if ($key)
1372		{
1373			$sql = 'UPDATE ' . SESSIONS_KEYS_TABLE . '
1374				SET ' . $db->sql_build_array('UPDATE', $sql_ary) . '
1375				WHERE user_id = ' . (int) $user_id . "
1376					AND key_id = '" . $db->sql_escape(md5($key)) . "'";
1377		}
1378		else
1379		{
1380			$sql = 'INSERT INTO ' . SESSIONS_KEYS_TABLE . ' ' . $db->sql_build_array('INSERT', $sql_ary);
1381		}
1382		$db->sql_query($sql);
1383
1384		$this->cookie_data['k'] = $key_id;
1385
1386		return false;
1387	}
1388
1389	/**
1390	* Reset all login keys for the specified user
1391	*
1392	* This method removes all current login keys for a specified (or the current)
1393	* user. It will be called on password change to render old keys unusable
1394	*/
1395	function reset_login_keys($user_id = false)
1396	{
1397		global $config, $db;
1398
1399		$user_id = ($user_id === false) ? (int) $this->data['user_id'] : (int) $user_id;
1400
1401		$sql = 'DELETE FROM ' . SESSIONS_KEYS_TABLE . '
1402			WHERE user_id = ' . (int) $user_id;
1403		$db->sql_query($sql);
1404
1405		// If the user is logged in, update last visit info first before deleting sessions
1406		$sql = 'SELECT session_time, session_page
1407			FROM ' . SESSIONS_TABLE . '
1408			WHERE session_user_id = ' . (int) $user_id . '
1409			ORDER BY session_time DESC';
1410		$result = $db->sql_query_limit($sql, 1);
1411		$row = $db->sql_fetchrow($result);
1412		$db->sql_freeresult($result);
1413
1414		if ($row)
1415		{
1416			$sql = 'UPDATE ' . USERS_TABLE . '
1417				SET user_lastvisit = ' . (int) $row['session_time'] . ", user_lastpage = '" . $db->sql_escape($row['session_page']) . "'
1418				WHERE user_id = " . (int) $user_id;
1419			$db->sql_query($sql);
1420		}
1421
1422		// Let's also clear any current sessions for the specified user_id
1423		// If it's the current user then we'll leave this session intact
1424		$sql_where = 'session_user_id = ' . (int) $user_id;
1425		$sql_where .= ($user_id === (int) $this->data['user_id']) ? " AND session_id <> '" . $db->sql_escape($this->session_id) . "'" : '';
1426
1427		$sql = 'DELETE FROM ' . SESSIONS_TABLE . "
1428			WHERE $sql_where";
1429		$db->sql_query($sql);
1430
1431		// We're changing the password of the current user and they have a key
1432		// Lets regenerate it to be safe
1433		if ($user_id === (int) $this->data['user_id'] && $this->cookie_data['k'])
1434		{
1435			$this->set_login_key($user_id);
1436		}
1437	}
1438
1439
1440	/**
1441	* Check if the request originated from the same page.
1442	* @param bool $check_script_path If true, the path will be checked as well
1443	*/
1444	function validate_referer($check_script_path = false)
1445	{
1446		global $config;
1447
1448		// no referer - nothing to validate, user's fault for turning it off (we only check on POST; so meta can't be the reason)
1449		if (empty($this->referer) || empty($this->host))
1450		{
1451			return true;
1452		}
1453
1454		$host = htmlspecialchars($this->host);
1455		$ref = substr($this->referer, strpos($this->referer, '://') + 3);
1456
1457		if (!(stripos($ref, $host) === 0) && (!$config['force_server_vars'] || !(stripos($ref, $config['server_name']) === 0)))
1458		{
1459			return false;
1460		}
1461		else if ($check_script_path && rtrim($this->page['root_script_path'], '/') !== '')
1462		{
1463			$ref = substr($ref, strlen($host));
1464			$server_port = (!empty($_SERVER['SERVER_PORT'])) ? (int) $_SERVER['SERVER_PORT'] : (int) getenv('SERVER_PORT');
1465
1466			if ($server_port !== 80 && $server_port !== 443 && stripos($ref, ":$server_port") === 0)
1467			{
1468				$ref = substr($ref, strlen(":$server_port"));
1469			}
1470
1471			if (!(stripos(rtrim($ref, '/'), rtrim($this->page['root_script_path'], '/')) === 0))
1472			{
1473				return false;
1474			}
1475		}
1476
1477		return true;
1478	}
1479
1480
1481	function unset_admin()
1482	{
1483		global $db;
1484		$sql = 'UPDATE ' . SESSIONS_TABLE . '
1485			SET session_admin = 0
1486			WHERE session_id = \'' . $db->sql_escape($this->session_id) . '\'';
1487		$db->sql_query($sql);
1488	}
1489}
1490
1491
1492/**
1493* Base user class
1494*
1495* This is the overarching class which contains (through session extend)
1496* all methods utilised for user functionality during a session.
1497*
1498* @package phpBB3
1499*/
1500class user extends session
1501{
1502	var $lang = array();
1503	var $help = array();
1504	var $theme = array();
1505	var $date_format;
1506	var $timezone;
1507	var $dst;
1508
1509	var $lang_name = false;
1510	var $lang_id = false;
1511	var $lang_path;
1512	var $img_lang;
1513	var $img_array = array();
1514
1515	// Able to add new options (up to id 31)
1516	var $keyoptions = array('viewimg' => 0, 'viewflash' => 1, 'viewsmilies' => 2, 'viewsigs' => 3, 'viewavatars' => 4, 'viewcensors' => 5, 'attachsig' => 6, 'bbcode' => 8, 'smilies' => 9, 'popuppm' => 10, 'sig_bbcode' => 15, 'sig_smilies' => 16, 'sig_links' => 17);
1517
1518	/**
1519	* Constructor to set the lang path
1520	*/
1521	function user()
1522	{
1523		global $phpbb_root_path;
1524
1525		$this->lang_path = $phpbb_root_path . 'language/';
1526	}
1527
1528	/**
1529	* Function to set custom language path (able to use directory outside of phpBB)
1530	*
1531	* @param string $lang_path New language path used.
1532	* @access public
1533	*/
1534	function set_custom_lang_path($lang_path)
1535	{
1536		$this->lang_path = $lang_path;
1537
1538		if (substr($this->lang_path, -1) != '/')
1539		{
1540			$this->lang_path .= '/';
1541		}
1542	}
1543
1544	/**
1545	* Setup basic user-specific items (style, language, ...)
1546	*/
1547	function setup($lang_set = false, $style = false)
1548	{
1549		global $db, $template, $config, $auth, $phpEx, $phpbb_root_path, $cache;
1550
1551		if ($this->data['user_id'] != ANONYMOUS)
1552		{
1553			$this->lang_name = (file_exists($this->lang_path . $this->data['user_lang'] . "/common.$phpEx")) ? $this->data['user_lang'] : basename($config['default_lang']);
1554
1555			$this->date_format = $this->data['user_dateformat'];
1556			$this->timezone = $this->data['user_timezone'] * 3600;
1557			$this->dst = $this->data['user_dst'] * 3600;
1558		}
1559		else
1560		{
1561			$this->lang_name = basename($config['default_lang']);
1562			$this->date_format = $config['default_dateformat'];
1563			$this->timezone = $config['board_timezone'] * 3600;
1564			$this->dst = $config['board_dst'] * 3600;
1565
1566			/**
1567			* If a guest user is surfing, we try to guess his/her language first by obtaining the browser language
1568			* If re-enabled we need to make sure only those languages installed are checked
1569			* Commented out so we do not loose the code.
1570
1571			if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE']))
1572			{
1573				$accept_lang_ary = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
1574
1575				foreach ($accept_lang_ary as $accept_lang)
1576				{
1577					// Set correct format ... guess full xx_YY form
1578					$accept_lang = substr($accept_lang, 0, 2) . '_' . strtoupper(substr($accept_lang, 3, 2));
1579					$accept_lang = basename($accept_lang);
1580
1581					if (file_exists($this->lang_path . $accept_lang . "/common.$phpEx"))
1582					{
1583						$this->lang_name = $config['default_lang'] = $accept_lang;
1584						break;
1585					}
1586					else
1587					{
1588						// No match on xx_YY so try xx
1589						$accept_lang = substr($accept_lang, 0, 2);
1590						$accept_lang = basename($accept_lang);
1591
1592						if (file_exists($this->lang_path . $accept_lang . "/common.$phpEx"))
1593						{
1594							$this->lang_name = $config['default_lang'] = $accept_lang;
1595							break;
1596						}
1597					}
1598				}
1599			}
1600			*/
1601		}
1602
1603		// We include common language file here to not load it every time a custom language file is included
1604		$lang = &$this->lang;
1605
1606		// Do not suppress error if in DEBUG_EXTRA mode
1607		$include_result = (defined('DEBUG_EXTRA')) ? (include $this->lang_path . $this->lang_name . "/common.$phpEx") : (@include $this->lang_path . $this->lang_name . "/common.$phpEx");
1608
1609		if ($include_result === false)
1610		{
1611			die('Language file ' . $this->lang_path . $this->lang_name . "/common.$phpEx" . " couldn't be opened.");
1612		}
1613
1614		$this->add_lang($lang_set);
1615		unset($lang_set);
1616
1617		if (!empty($_GET['style']) && $auth->acl_get('a_styles') && !defined('ADMIN_START'))
1618		{
1619			global $SID, $_EXTRA_URL;
1620
1621			$style = request_var('style', 0);
1622			$SID .= '&amp;style=' . $style;
1623			$_EXTRA_URL = array('style=' . $style);
1624		}
1625		else
1626		{
1627			// Set up style
1628			$style = ($style) ? $style : ((!$config['override_user_style']) ? $this->data['user_style'] : $config['default_style']);
1629		}
1630
1631		$sql = 'SELECT s.style_id, t.template_storedb, t.template_path, t.template_id, t.bbcode_bitfield, t.template_inherits_id, t.template_inherit_path, c.theme_path, c.theme_name, c.theme_storedb, c.theme_id, i.imageset_path, i.imageset_id, i.imageset_name
1632			FROM ' . STYLES_TABLE . ' s, ' . STYLES_TEMPLATE_TABLE . ' t, ' . STYLES_THEME_TABLE . ' c, ' . STYLES_IMAGESET_TABLE . " i
1633			WHERE s.style_id = $style
1634				AND t.template_id = s.template_id
1635				AND c.theme_id = s.theme_id
1636				AND i.imageset_id = s.imageset_id";
1637		$result = $db->sql_query($sql, 3600);
1638		$this->theme = $db->sql_fetchrow($result);
1639		$db->sql_freeresult($result);
1640
1641		// User has wrong style
1642		if (!$this->theme && $style == $this->data['user_style'])
1643		{
1644			$style = $this->data['user_style'] = $config['default_style'];
1645
1646			$sql = 'UPDATE ' . USERS_TABLE . "
1647				SET user_style = $style
1648				WHERE user_id = {$this->data['user_id']}";
1649			$db->sql_query($sql);
1650
1651			$sql = 'SELECT s.style_id, t.template_storedb, t.template_path, t.template_id, t.bbcode_bitfield, c.theme_path, c.theme_name, c.theme_storedb, c.theme_id, i.imageset_path, i.imageset_id, i.imageset_name
1652				FROM ' . STYLES_TABLE . ' s, ' . STYLES_TEMPLATE_TABLE . ' t, ' . STYLES_THEME_TABLE . ' c, ' . STYLES_IMAGESET_TABLE . " i
1653				WHERE s.style_id = $style
1654					AND t.template_id = s.template_id
1655					AND c.theme_id = s.theme_id
1656					AND i.imageset_id = s.imageset_id";
1657			$result = $db->sql_query($sql, 3600);
1658			$this->theme = $db->sql_fetchrow($result);
1659			$db->sql_freeresult($result);
1660		}
1661
1662		if (!$this->theme)
1663		{
1664			trigger_error('NO_STYLE_DATA', E_USER_ERROR);
1665		}
1666
1667		// Now parse the cfg file and cache it
1668		$parsed_items = $cache->obtain_cfg_items($this->theme);
1669
1670		// We are only interested in the theme configuration for now
1671		$parsed_items = $parsed_items['theme'];
1672
1673		$check_for = array(
1674			'parse_css_file'	=> (int) 0,
1675			'pagination_sep'	=> (string) ', '
1676		);
1677
1678		foreach ($check_for as $key => $default_value)
1679		{
1680			$this->theme[$key] = (isset($parsed_items[$key])) ? $parsed_items[$key] : $default_value;
1681			settype($this->theme[$key], gettype($default_value));
1682
1683			if (is_string($default_value))
1684			{
1685				$this->theme[$key] = htmlspecialchars($this->theme[$key]);
1686			}
1687		}
1688
1689		// If the style author specified the theme needs to be cached
1690		// (because of the used paths and variables) than make sure it is the case.
1691		// For example, if the theme uses language-specific images it needs to be stored in db.
1692		if (!$this->theme['theme_storedb'] && $this->theme['parse_css_file'])
1693		{
1694			$this->theme['theme_storedb'] = 1;
1695
1696			$stylesheet = file_get_contents("{$phpbb_root_path}styles/{$this->theme['theme_path']}/theme/stylesheet.css");
1697			// Match CSS imports
1698			$matches = array();
1699			preg_match_all('/@import url\(["\'](.*)["\']\);/i', $stylesheet, $matches);
1700
1701			if (sizeof($matches))
1702			{
1703				$content = '';
1704				foreach ($matches[0] as $idx => $match)
1705				{
1706					if ($content = @file_get_contents("{$phpbb_root_path}styles/{$this->theme['theme_path']}/theme/" . $matches[1][$idx]))
1707					{
1708						$content = trim($content);
1709					}
1710					else
1711					{
1712						$content = '';
1713					}
1714					$stylesheet = str_replace($match, $content, $stylesheet);
1715				}
1716				unset($content);
1717			}
1718
1719			$stylesheet = str_replace('./', 'styles/' . $this->theme['theme_path'] . '/theme/', $stylesheet);
1720
1721			$sql_ary = array(
1722				'theme_data'	=> $stylesheet,
1723				'theme_mtime'	=> time(),
1724				'theme_storedb'	=> 1
1725			);
1726
1727			$sql = 'UPDATE ' . STYLES_THEME_TABLE . '
1728				SET ' . $db->sql_build_array('UPDATE', $sql_ary) . '
1729				WHERE theme_id = ' . $this->theme['theme_id'];
1730			$db->sql_query($sql);
1731
1732			unset($sql_ary);
1733		}
1734
1735		$template->set_template();
1736
1737		$this->img_lang = (file_exists($phpbb_root_path . 'styles/' . $this->theme['imageset_path'] . '/imageset/' . $this->lang_name)) ? $this->lang_name : $config['default_lang'];
1738
1739		// Same query in style.php
1740		$sql = 'SELECT *
1741			FROM ' . STYLES_IMAGESET_DATA_TABLE . '
1742			WHERE imageset_id = ' . $this->theme['imageset_id'] . "
1743			AND image_filename <> ''
1744			AND image_lang IN ('" . $db->sql_escape($this->img_lang) . "', '')";
1745		$result = $db->sql_query($sql, 3600);
1746
1747		$localised_images = false;
1748		while ($row = $db->sql_fetchrow($result))
1749		{
1750			if ($row['image_lang'])
1751			{
1752				$localised_images = true;
1753			}
1754
1755			$row['image_filename'] = rawurlencode($row['image_filename']);
1756			$this->img_array[$row['image_name']] = $row;
1757		}
1758		$db->sql_freeresult($result);
1759
1760		// there were no localised images, try to refresh the localised imageset for the user's language
1761		if (!$localised_images)
1762		{
1763			// Attention: this code ignores the image definition list from acp_styles and just takes everything
1764			// that the config file contains
1765			$sql_ary = array();
1766
1767			$db->sql_transaction('begin');
1768
1769			$sql = 'DELETE FROM ' . STYLES_IMAGESET_DATA_TABLE . '
1770				WHERE imageset_id = ' . $this->theme['imageset_id'] . '
1771					AND image_lang = \'' . $db->sql_escape($this->img_lang) . '\'';
1772			$result = $db->sql_query($sql);
1773
1774			if (@file_exists("{$phpbb_root_path}styles/{$this->theme['imageset_path']}/imageset/{$this->img_lang}/imageset.cfg"))
1775			{
1776				$cfg_data_imageset_data = parse_cfg_file("{$phpbb_root_path}styles/{$this->theme['imageset_path']}/imageset/{$this->img_lang}/imageset.cfg");
1777				foreach ($cfg_data_imageset_data as $image_name => $value)
1778				{
1779					if (strpos($value, '*') !== false)
1780					{
1781						if (substr($value, -1, 1) === '*')
1782						{
1783							list($image_filename, $image_height) = explode('*', $value);
1784							$image_width = 0;
1785						}
1786						else
1787						{
1788							list($image_filename, $image_height, $image_width) = explode('*', $value);
1789						}
1790					}
1791					else
1792					{
1793						$image_filename = $value;
1794						$image_height = $image_width = 0;
1795					}
1796
1797					if (strpos($image_name, 'img_') === 0 && $image_filename)
1798					{
1799						$image_name = substr($image_name, 4);
1800						$sql_ary[] = array(
1801							'image_name'		=> (string) $image_name,
1802							'image_filename'	=> (string) $image_filename,
1803							'image_height'		=> (int) $image_height,
1804							'image_width'		=> (int) $image_width,
1805							'imageset_id'		=> (int) $this->theme['imageset_id'],
1806							'image_lang'		=> (string) $this->img_lang,
1807						);
1808					}
1809				}
1810			}
1811
1812			if (sizeof($sql_ary))
1813			{
1814				$db->sql_multi_insert(STYLES_IMAGESET_DATA_TABLE, $sql_ary);
1815				$db->sql_transaction('commit');
1816				$cache->destroy('sql', STYLES_IMAGESET_DATA_TABLE);
1817
1818				add_log('admin', 'LOG_IMAGESET_LANG_REFRESHED', $this->theme['imageset_name'], $this->img_lang);
1819			}
1820			else
1821			{
1822				$db->sql_transaction('commit');
1823				add_log('admin', 'LOG_IMAGESET_LANG_MISSING', $this->theme['imageset_name'], $this->img_lang);
1824			}
1825		}
1826
1827		// Call phpbb_user_session_handler() in case external application want to "bend" some variables or replace classes...
1828		// After calling it we continue script execution...
1829		phpbb_user_session_handler();
1830
1831		// If this function got called from the error handler we are finished here.
1832		if (defined('IN_ERROR_HANDLER'))
1833		{
1834			return;
1835		}
1836
1837		// Disable board if the install/ directory is still present
1838		// For the brave development army we do not care about this, else we need to comment out this everytime we develop locally
1839		if (!defined('DEBUG_EXTRA') && !defined('ADMIN_START') && !defined('IN_INSTALL') && !defined('IN_LOGIN') && file_exists($phpbb_root_path . 'install') && !is_file($phpbb_root_path . 'install'))
1840		{
1841			// Adjust the message slightly according to the permissions
1842			if ($auth->acl_gets('a_', 'm_') || $auth->acl_getf_global('m_'))
1843			{
1844				$message = 'REMOVE_INSTALL';
1845			}
1846			else
1847			{
1848				$message = (!empty($config['board_disable_msg'])) ? $config['board_disable_msg'] : 'BOARD_DISABLE';
1849			}
1850			trigger_error($message);
1851		}
1852
1853		// Is board disabled and user not an admin or moderator?
1854		if ($config['board_disable'] && !defined('IN_LOGIN') && !$auth->acl_gets('a_', 'm_') && !$auth->acl_getf_global('m_'))
1855		{
1856			if ($this->data['is_bot'])
1857			{
1858				send_status_line(503, 'Service Unavailable');
1859			}
1860
1861			$message = (!empty($config['board_disable_msg'])) ? $config['board_disable_msg'] : 'BOARD_DISABLE';
1862			trigger_error($message);
1863		}
1864
1865		// Is load exceeded?
1866		if ($config['limit_load'] && $this->load !== false)
1867		{
1868			if ($this->load > floatval($config['limit_load']) && !defined('IN_LOGIN') && !defined('IN_ADMIN'))
1869			{
1870				// Set board disabled to true to let the admins/mods get the proper notification
1871				$config['board_disable'] = '1';
1872
1873				if (!$auth->acl_gets('a_', 'm_') && !$auth->acl_getf_global('m_'))
1874				{
1875					if ($this->data['is_bot'])
1876					{
1877						send_status_line(503, 'Service Unavailable');
1878					}
1879					trigger_error('BOARD_UNAVAILABLE');
1880				}
1881			}
1882		}
1883
1884		if (isset($this->data['session_viewonline']))
1885		{
1886			// Make sure the user is able to hide his session
1887			if (!$this->data['session_viewonline'])
1888			{
1889				// Reset online status if not allowed to hide the session...
1890				if (!$auth->acl_get('u_hideonline'))
1891				{
1892					$sql = 'UPDATE ' . SESSIONS_TABLE . '
1893						SET session_viewonline = 1
1894						WHERE session_user_id = ' . $this->data['user_id'];
1895					$db->sql_query($sql);
1896					$this->data['session_viewonline'] = 1;
1897				}
1898			}
1899			else if (!$this->data['user_allow_viewonline'])
1900			{
1901				// the user wants to hide and is allowed to  -> cloaking device on.
1902				if ($auth->acl_get('u_hideonline'))
1903				{
1904					$sql = 'UPDATE ' . SESSIONS_TABLE . '
1905						SET session_viewonline = 0
1906						WHERE session_user_id = ' . $this->data['user_id'];
1907					$db->sql_query($sql);
1908					$this->data['session_viewonline'] = 0;
1909				}
1910			}
1911		}
1912
1913
1914		// Does the user need to change their password? If so, redirect to the
1915		// ucp profile reg_details page ... of course do not redirect if we're already in the ucp
1916		if (!defined('IN_ADMIN') && !defined('ADMIN_START') && $config['chg_passforce'] && !empty($this->data['is_registered']) && $auth->acl_get('u_chgpasswd') && $this->data['user_passchg'] < time() - ($config['chg_passforce'] * 86400))
1917		{
1918			if (strpos($this->page['query_string'], 'mode=reg_details') === false && $this->page['page_name'] != "ucp.$phpEx")
1919			{
1920				redirect(append_sid("{$phpbb_root_path}ucp.$phpEx", 'i=profile&amp;mode=reg_details'));
1921			}
1922		}
1923
1924		return;
1925	}
1926
1927	/**
1928	* More advanced language substitution
1929	* Function to mimic sprintf() with the possibility of using phpBB's language system to substitute nullar/singular/plural forms.
1930	* Params are the language key and the parameters to be substituted.
1931	* This function/functionality is inspired by SHS` and Ashe.
1932	*
1933	* Example call: <samp>$user->lang('NUM_POSTS_IN_QUEUE', 1);</samp>
1934	*/
1935	function lang()
1936	{
1937		$args = func_get_args();
1938		$key = $args[0];
1939
1940		if (is_array($key))
1941		{
1942			$lang = &$this->lang[array_shift($key)];
1943
1944			foreach ($key as $_key)
1945			{
1946				$lang = &$lang[$_key];
1947			}
1948		}
1949		else
1950		{
1951			$lang = &$this->lang[$key];
1952		}
1953
1954		// Return if language string does not exist
1955		if (!isset($lang) || (!is_string($lang) && !is_array($lang)))
1956		{
1957			return $key;
1958		}
1959
1960		// If the language entry is a string, we simply mimic sprintf() behaviour
1961		if (is_string($lang))
1962		{
1963			if (sizeof($args) == 1)
1964			{
1965				return $lang;
1966			}
1967
1968			// Replace key with language entry and simply pass along...
1969			$args[0] = $lang;
1970			return call_user_func_array('sprintf', $args);
1971		}
1972
1973		// It is an array... now handle different nullar/singular/plural forms
1974		$key_found = false;
1975
1976		// We now get the first number passed and will select the key based upon this number
1977		for ($i = 1, $num_args = sizeof($args); $i < $num_args; $i++)
1978		{
1979			if (is_int($args[$i]))
1980			{
1981				$numbers = array_keys($lang);
1982
1983				foreach ($numbers as $num)
1984				{
1985					if ($num > $args[$i])
1986					{
1987						break;
1988					}
1989
1990					$key_found = $num;
1991				}
1992				break;
1993			}
1994		}
1995
1996		// Ok, let's check if the key was found, else use the last entry (because it is mostly the plural form)
1997		if ($key_found === false)
1998		{
1999			$numbers = array_keys($lang);
2000			$key_found = end($numbers);
2001		}
2002
2003		// Use the language string we determined and pass it to sprintf()
2004		$args[0] = $lang[$key_found];
2005		return call_user_func_array('sprintf', $args);
2006	}
2007
2008	/**
2009	* Add Language Items - use_db and use_help are assigned where needed (only use them to force inclusion)
2010	*
2011	* @param mixed $lang_set specifies the language entries to include
2012	* @param bool $use_db internal variable for recursion, do not use
2013	* @param bool $use_help internal variable for recursion, do not use
2014	*
2015	* Examples:
2016	* <code>
2017	* $lang_set = array('posting', 'help' => 'faq');
2018	* $lang_set = array('posting', 'viewtopic', 'help' => array('bbcode', 'faq'))
2019	* $lang_set = array(array('posting', 'viewtopic'), 'help' => array('bbcode', 'faq'))
2020	* $lang_set = 'posting'
2021	* $lang_set = array('help' => 'faq', 'db' => array('help:faq', 'posting'))
2022	* </code>
2023	*/
2024	function add_lang($lang_set, $use_db = false, $use_help = false)
2025	{
2026		global $phpEx;
2027
2028		if (is_array($lang_set))
2029		{
2030			foreach ($lang_set as $key => $lang_file)
2031			{
2032				// Please do not delete this line.
2033				// We have to force the type here, else [array] language inclusion will not work
2034				$key = (string) $key;
2035
2036				if ($key == 'db')
2037				{
2038					$this->add_lang($lang_file, true, $use_help);
2039				}
2040				else if ($key == 'help')
2041				{
2042					$this->add_lang($lang_file, $use_db, true);
2043				}
2044				else if (!is_array($lang_file))
2045				{
2046					$this->set_lang($this->lang, $this->help, $lang_file, $use_db, $use_help);
2047				}
2048				else
2049				{
2050					$this->add_lang($lang_file, $use_db, $use_help);
2051				}
2052			}
2053			unset($lang_set);
2054		}
2055		else if ($lang_set)
2056		{
2057			$this->set_lang($this->lang, $this->help, $lang_set, $use_db, $use_help);
2058		}
2059	}
2060
2061	/**
2062	* Set language entry (called by add_lang)
2063	* @access private
2064	*/
2065	function set_lang(&$lang, &$help, $lang_file, $use_db = false, $use_help = false)
2066	{
2067		global $phpEx;
2068
2069		// Make sure the language name is set (if the user setup did not happen it is not set)
2070		if (!$this->lang_name)
2071		{
2072			global $config;
2073			$this->lang_name = basename($config['default_lang']);
2074		}
2075
2076		// $lang == $this->lang
2077		// $help == $this->help
2078		// - add appropriate variables here, name them as they are used within the language file...
2079		if (!$use_db)
2080		{
2081			if ($use_help && strpos($lang_file, '/') !== false)
2082			{
2083				$language_filename = $this->lang_path . $this->lang_name . '/' . substr($lang_file, 0, stripos($lang_file, '/') + 1) . 'help_' . substr($lang_file, stripos($lang_file, '/') + 1) . '.' . $phpEx;
2084			}
2085			else
2086			{
2087				$language_filename = $this->lang_path . $this->lang_name . '/' . (($use_help) ? 'help_' : '') . $lang_file . '.' . $phpEx;
2088			}
2089
2090			if (!file_exists($language_filename))
2091			{
2092				global $config;
2093
2094				if ($this->lang_name == 'en')
2095				{
2096					// The user's selected language is missing the file, the board default's language is missing the file, and the file doesn't exist in /en.
2097					$language_filename = str_replace($this->lang_path . 'en', $this->lang_path . $this->data['user_lang'], $language_filename);
2098					trigger_error('Language file ' . $language_filename . ' couldn\'t be opened.', E_USER_ERROR);
2099				}
2100				else if ($this->lang_name == basename($config['default_lang']))
2101				{
2102					// Fall back to the English Language
2103					$this->lang_name = 'en';
2104					$this->set_lang($lang, $help, $lang_file, $use_db, $use_help);
2105				}
2106				else if ($this->lang_name == $this->data['user_lang'])
2107				{
2108					// Fall back to the board default language
2109					$this->lang_name = basename($config['default_lang']);
2110					$this->set_lang($lang, $help, $lang_file, $use_db, $use_help);
2111				}
2112
2113				// Reset the lang name
2114				$this->lang_name = (file_exists($this->lang_path . $this->data['user_lang'] . "/common.$phpEx")) ? $this->data['user_lang'] : basename($config['default_lang']);
2115				return;
2116			}
2117
2118			// Do not suppress error if in DEBUG_EXTRA mode
2119			$include_result = (defined('DEBUG_EXTRA')) ? (include $language_filename) : (@include $language_filename);
2120
2121			if ($include_result === false)
2122			{
2123				trigger_error('Language file ' . $language_filename . ' couldn\'t be opened.', E_USER_ERROR);
2124			}
2125		}
2126		else if ($use_db)
2127		{
2128			// Get Database Language Strings
2129			// Put them into $lang if nothing is prefixed, put them into $help if help: is prefixed
2130			// For example: help:faq, posting
2131		}
2132	}
2133
2134	/**
2135	* Format user date
2136	*
2137	* @param int $gmepoch unix timestamp
2138	* @param string $format date format in date() notation. | used to indicate relative dates, for example |d m Y|, h:i is translated to Today, h:i.
2139	* @param bool $forcedate force non-relative date format.
2140	*
2141	* @return mixed translated date
2142	*/
2143	function format_date($gmepoch, $format = false, $forcedate = false)
2144	{
2145		static $midnight;
2146		static $date_cache;
2147
2148		$format = (!$format) ? $this->date_format : $format;
2149		$now = time();
2150		$delta = $now - $gmepoch;
2151
2152		if (!isset($date_cache[$format]))
2153		{
2154			// Is the user requesting a friendly date format (i.e. 'Today 12:42')?
2155			$date_cache[$format] = array(
2156				'is_short'		=> strpos($format, '|'),
2157				'format_short'	=> substr($format, 0, strpos($format, '|')) . '||' . substr(strrchr($format, '|'), 1),
2158				'format_long'	=> str_replace('|', '', $format),
2159				// Filter out values that are not strings (e.g. arrays) for strtr().
2160				'lang'			=> array_filter($this->lang['datetime'], 'is_string'),
2161			);
2162
2163			// Short representation of month in format? Some languages use different terms for the long and short format of May
2164			if ((strpos($format, '\M') === false && strpos($format, 'M') !== false) || (strpos($format, '\r') === false && strpos($format, 'r') !== false))
2165			{
2166				$date_cache[$format]['lang']['May'] = $this->lang['datetime']['May_short'];
2167			}
2168		}
2169
2170		// Zone offset
2171		$zone_offset = $this->timezone + $this->dst;
2172
2173		// Show date <= 1 hour ago as 'xx min ago' but not greater than 60 seconds in the future
2174		// A small tolerence is given for times in the future but in the same minute are displayed as '< than a minute ago'
2175		if ($delta <= 3600 && $delta > -60 && ($delta >= -5 || (($now / 60) % 60) == (($gmepoch / 60) % 60)) && $date_cache[$format]['is_short'] !== false && !$forcedate && isset($this->lang['datetime']['AGO']))
2176		{
2177			return $this->lang(array('datetime', 'AGO'), max(0, (int) floor($delta / 60)));
2178		}
2179
2180		if (!$midnight)
2181		{
2182			list($d, $m, $y) = explode(' ', gmdate('j n Y', time() + $zone_offset));
2183			$midnight = gmmktime(0, 0, 0, $m, $d, $y) - $zone_offset;
2184		}
2185
2186		if ($date_cache[$format]['is_short'] !== false && !$forcedate && !($gmepoch < $midnight - 86400 || $gmepoch > $midnight + 172800))
2187		{
2188			$day = false;
2189
2190			if ($gmepoch > $midnight + 86400)
2191			{
2192				$day = 'TOMORROW';
2193			}
2194			else if ($gmepoch > $midnight)
2195			{
2196				$day = 'TODAY';
2197			}
2198			else if ($gmepoch > $midnight - 86400)
2199			{
2200				$day = 'YESTERDAY';
2201			}
2202
2203			if ($day !== false)
2204			{
2205				return str_replace('||', $this->lang['datetime'][$day], strtr(@gmdate($date_cache[$format]['format_short'], $gmepoch + $zone_offset), $date_cache[$format]['lang']));
2206			}
2207		}
2208
2209		return strtr(@gmdate($date_cache[$format]['format_long'], $gmepoch + $zone_offset), $date_cache[$format]['lang']);
2210	}
2211
2212	/**
2213	* Get language id currently used by the user
2214	*/
2215	function get_iso_lang_id()
2216	{
2217		global $config, $db;
2218
2219		if (!empty($this->lang_id))
2220		{
2221			return $this->lang_id;
2222		}
2223
2224		if (!$this->lang_name)
2225		{
2226			$this->lang_name = $config['default_lang'];
2227		}
2228
2229		$sql = 'SELECT lang_id
2230			FROM ' . LANG_TABLE . "
2231			WHERE lang_iso = '" . $db->sql_escape($this->lang_name) . "'";
2232		$result = $db->sql_query($sql);
2233		$this->lang_id = (int) $db->sql_fetchfield('lang_id');
2234		$db->sql_freeresult($result);
2235
2236		return $this->lang_id;
2237	}
2238
2239	/**
2240	* Get users profile fields
2241	*/
2242	function get_profile_fields($user_id)
2243	{
2244		global $db;
2245
2246		if (isset($this->profile_fields))
2247		{
2248			return;
2249		}
2250
2251		$sql = 'SELECT *
2252			FROM ' . PROFILE_FIELDS_DATA_TABLE . "
2253			WHERE user_id = $user_id";
2254		$result = $db->sql_query_limit($sql, 1);
2255		$this->profile_fields = (!($row = $db->sql_fetchrow($result))) ? array() : $row;
2256		$db->sql_freeresult($result);
2257	}
2258
2259	/**
2260	* Specify/Get image
2261	* $suffix is no longer used - we know it. ;) It is there for backward compatibility.
2262	*/
2263	function img($img, $alt = '', $width = false, $suffix = '', $type = 'full_tag')
2264	{
2265		static $imgs;
2266		global $phpbb_root_path;
2267
2268		$img_data = &$imgs[$img];
2269
2270		if (empty($img_data))
2271		{
2272			if (!isset($this->img_array[$img]))
2273			{
2274				// Do not fill the image to let designers decide what to do if the image is empty
2275				$img_data = '';
2276				return $img_data;
2277			}
2278
2279			// Use URL if told so
2280			$root_path = (defined('PHPBB_USE_BOARD_URL_PATH') && PHPBB_USE_BOARD_URL_PATH) ? generate_board_url() . '/' : $phpbb_root_path;
2281
2282			$path = 'styles/' . rawurlencode($this->theme['imageset_path']) . '/imageset/' . ($this->img_array[$img]['image_lang'] ? $this->img_array[$img]['image_lang'] .'/' : '') . $this->img_array[$img]['image_filename'];
2283
2284			$img_data['src'] = $root_path . $path;
2285			$img_data['width'] = $this->img_array[$img]['image_width'];
2286			$img_data['height'] = $this->img_array[$img]['image_height'];
2287
2288			// We overwrite the width and height to the phpbb logo's width
2289			// and height here if the contents of the site_logo file are
2290			// really equal to the phpbb_logo
2291			// This allows us to change the dimensions of the phpbb_logo without
2292			// modifying the imageset.cfg and causing a conflict for everyone
2293			// who modified it for their custom logo on updating
2294			if ($img == 'site_logo' && file_exists($phpbb_root_path . $path))
2295			{
2296				global $cache;
2297
2298				$img_file_hashes = $cache->get('imageset_site_logo_md5');
2299
2300				if ($img_file_hashes === false)
2301				{
2302					$img_file_hashes = array();
2303				}
2304
2305				$key = $this->theme['imageset_path'] . '::' . $this->img_array[$img]['image_lang'];
2306				if (!isset($img_file_hashes[$key]))
2307				{
2308					$img_file_hashes[$key] = md5(file_get_contents($phpbb_root_path . $path));
2309					$cache->put('imageset_site_logo_md5', $img_file_hashes);
2310				}
2311
2312				$phpbb_logo_hash = '0c461a32cd3621643105f0d02a772c10';
2313
2314				if ($phpbb_logo_hash == $img_file_hashes[$key])
2315				{
2316					$img_data['width'] = '149';
2317					$img_data['height'] = '52';
2318				}
2319			}
2320		}
2321
2322		$alt = (!empty($this->lang[$alt])) ? $this->lang[$alt] : $alt;
2323
2324		switch ($type)
2325		{
2326			case 'src':
2327				return $img_data['src'];
2328			break;
2329
2330			case 'width':
2331				return ($width === false) ? $img_data['width'] : $width;
2332			break;
2333
2334			case 'height':
2335				return $img_data['height'];
2336			break;
2337
2338			default:
2339				$use_width = ($width === false) ? $img_data['width'] : $width;
2340
2341				return '<img src="' . $img_data['src'] . '"' . (($use_width) ? ' width="' . $use_width . '"' : '') . (($img_data['height']) ? ' height="' . $img_data['height'] . '"' : '') . ' alt="' . $alt . '" title="' . $alt . '" />';
2342			break;
2343		}
2344	}
2345
2346	/**
2347	* Get option bit field from user options.
2348	*
2349	* @param int $key option key, as defined in $keyoptions property.
2350	* @param int $data bit field value to use, or false to use $this->data['user_options']
2351	* @return bool true if the option is set in the bit field, false otherwise
2352	*/
2353	function optionget($key, $data = false)
2354	{
2355		$var = ($data !== false) ? $data : $this->data['user_options'];
2356		return phpbb_optionget($this->keyoptions[$key], $var);
2357	}
2358
2359	/**
2360	* Set option bit field for user options.
2361	*
2362	* @param int $key Option key, as defined in $keyoptions property.
2363	* @param bool $value True to set the option, false to clear the option.
2364	* @param int $data Current bit field value, or false to use $this->data['user_options']
2365	* @return int|bool If $data is false, the bit field is modified and
2366	*                  written back to $this->data['user_options'], and
2367	*                  return value is true if the bit field changed and
2368	*                  false otherwise. If $data is not false, the new
2369	*                  bitfield value is returned.
2370	*/
2371	function optionset($key, $value, $data = false)
2372	{
2373		$var = ($data !== false) ? $data : $this->data['user_options'];
2374
2375		$new_var = phpbb_optionset($this->keyoptions[$key], $value, $var);
2376
2377		if ($data === false)
2378		{
2379			if ($new_var != $var)
2380			{
2381				$this->data['user_options'] = $new_var;
2382				return true;
2383			}
2384			else
2385			{
2386				return false;
2387			}
2388		}
2389		else
2390		{
2391			return $new_var;
2392		}
2393	}
2394
2395	/**
2396	* Funtion to make the user leave the NEWLY_REGISTERED system group.
2397	* @access public
2398	*/
2399	function leave_newly_registered()
2400	{
2401		global $db;
2402
2403		if (empty($this->data['user_new']))
2404		{
2405			return false;
2406		}
2407
2408		if (!function_exists('remove_newly_registered'))
2409		{
2410			global $phpbb_root_path, $phpEx;
2411
2412			include($phpbb_root_path . 'includes/functions_user.' . $phpEx);
2413		}
2414		if ($group = remove_newly_registered($this->data['user_id'], $this->data))
2415		{
2416			$this->data['group_id'] = $group;
2417
2418		}
2419		$this->data['user_permissions'] = '';
2420		$this->data['user_new'] = 0;
2421
2422		return true;
2423	}
2424
2425	/**
2426	* Returns all password protected forum ids the user is currently NOT authenticated for.
2427	*
2428	* @return array		Array of forum ids
2429	* @access public
2430	*/
2431	function get_passworded_forums()
2432	{
2433		global $db;
2434
2435		$sql = 'SELECT f.forum_id, fa.user_id
2436			FROM ' . FORUMS_TABLE . ' f
2437			LEFT JOIN ' . FORUMS_ACCESS_TABLE . " fa
2438				ON (fa.forum_id = f.forum_id
2439					AND fa.session_id = '" . $db->sql_escape($this->session_id) . "')
2440			WHERE f.forum_password <> ''";
2441		$result = $db->sql_query($sql);
2442
2443		$forum_ids = array();
2444		while ($row = $db->sql_fetchrow($result))
2445		{
2446			$forum_id = (int) $row['forum_id'];
2447
2448			if ($row['user_id'] != $this->data['user_id'])
2449			{
2450				$forum_ids[$forum_id] = $forum_id;
2451			}
2452		}
2453		$db->sql_freeresult($result);
2454
2455		return $forum_ids;
2456	}
2457}
2458
2459?>