* @license GNU General Public License, version 2 (GPL-2.0)
*
* For full copyright and license information, please see
* the docs/CREDITS.txt file.
*
*/
/**
* @ignore
*/
if (!defined('IN_PHPBB'))
{
	exit;
}
// Common global functions
/**
* Load the autoloaders added by the extensions.
*
* @param string $phpbb_root_path Path to the phpbb root directory.
*/
function phpbb_load_extensions_autoloaders($phpbb_root_path)
{
	$iterator = new \RecursiveIteratorIterator(
		new \phpbb\recursive_dot_prefix_filter_iterator(
			new \RecursiveDirectoryIterator(
				$phpbb_root_path . 'ext/',
				\FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS
			)
		),
		\RecursiveIteratorIterator::SELF_FIRST
	);
	$iterator->setMaxDepth(2);
	foreach ($iterator as $file_info)
	{
		if ($file_info->getFilename() === 'vendor' && $iterator->getDepth() === 2)
		{
			$filename = $file_info->getRealPath() . '/autoload.php';
			if (file_exists($filename))
			{
				require $filename;
			}
		}
	}
}
/**
* Generates an alphanumeric random string of given length
*
* @param int $num_chars Length of random string, defaults to 8.
* This number should be less or equal than 64.
*
* @return string
*/
function gen_rand_string($num_chars = 8)
{
	$range = array_merge(range('A', 'Z'), range(0, 9));
	$size = count($range);
	$output = '';
	for ($i = 0; $i < $num_chars; $i++)
	{
		$rand = random_int(0, $size-1);
		$output .= $range[$rand];
	}
	return $output;
}
/**
* Generates a user-friendly alphanumeric random string of given length
* We remove 0 and O so users cannot confuse those in passwords etc.
*
* @param int $num_chars Length of random string, defaults to 8.
* This number should be less or equal than 64.
*
* @return string
*/
function gen_rand_string_friendly($num_chars = 8)
{
	$range = array_merge(range('A', 'N'), range('P', 'Z'), range(1, 9));
	$size = count($range);
	$output = '';
	for ($i = 0; $i < $num_chars; $i++)
	{
		$rand = random_int(0, $size-1);
		$output .= $range[$rand];
	}
	return $output;
}
/**
* Return unique id
*/
function unique_id()
{
	return strtolower(gen_rand_string(16));
}
/**
* Wrapper for mt_rand() which allows swapping $min and $max parameters.
*
* PHP does not allow us to swap the order of the arguments for mt_rand() anymore.
* (since PHP 5.3.4, see http://bugs.php.net/46587)
*
* @param int $min		Lowest value to be returned
* @param int $max		Highest value to be returned
*
* @return int			Random integer between $min and $max (or $max and $min)
*/
function phpbb_mt_rand($min, $max)
{
	return ($min > $max) ? mt_rand($max, $min) : mt_rand($min, $max);
}
/**
* Wrapper for getdate() which returns the equivalent array for UTC timestamps.
*
* @param int $time		Unix timestamp (optional)
*
* @return array			Returns an associative array of information related to the timestamp.
*						See http://www.php.net/manual/en/function.getdate.php
*/
function phpbb_gmgetdate($time = false)
{
	if ($time === false)
	{
		$time = time();
	}
	// getdate() interprets timestamps in local time.
	// What follows uses the fact that getdate() and
	// date('Z') balance each other out.
	return getdate($time - date('Z'));
}
/**
* Return formatted string for filesizes
*
* @param mixed	$value			filesize in bytes
*								(non-negative number; int, float or string)
* @param bool	$string_only	true if language string should be returned
* @param array	$allowed_units	only allow these units (data array indexes)
*
* @return mixed					data array if $string_only is false
*/
function get_formatted_filesize($value, $string_only = true, $allowed_units = false)
{
	global $user;
	$available_units = array(
		'tb' => array(
			'min' 		=> 1099511627776, // pow(2, 40)
			'index'		=> 4,
			'si_unit'	=> 'TB',
			'iec_unit'	=> 'TIB',
		),
		'gb' => array(
			'min' 		=> 1073741824, // pow(2, 30)
			'index'		=> 3,
			'si_unit'	=> 'GB',
			'iec_unit'	=> 'GIB',
		),
		'mb' => array(
			'min'		=> 1048576, // pow(2, 20)
			'index'		=> 2,
			'si_unit'	=> 'MB',
			'iec_unit'	=> 'MIB',
		),
		'kb' => array(
			'min'		=> 1024, // pow(2, 10)
			'index'		=> 1,
			'si_unit'	=> 'KB',
			'iec_unit'	=> 'KIB',
		),
		'b' => array(
			'min'		=> 0,
			'index'		=> 0,
			'si_unit'	=> 'BYTES', // Language index
			'iec_unit'	=> 'BYTES',  // Language index
		),
	);
	foreach ($available_units as $si_identifier => $unit_info)
	{
		if (!empty($allowed_units) && $si_identifier != 'b' && !in_array($si_identifier, $allowed_units))
		{
			continue;
		}
		if ($value >= $unit_info['min'])
		{
			$unit_info['si_identifier'] = $si_identifier;
			break;
		}
	}
	unset($available_units);
	for ($i = 0; $i < $unit_info['index']; $i++)
	{
		$value /= 1024;
	}
	$value = round($value, 2);
	// Lookup units in language dictionary
	$unit_info['si_unit'] = (isset($user->lang[$unit_info['si_unit']])) ? $user->lang[$unit_info['si_unit']] : $unit_info['si_unit'];
	$unit_info['iec_unit'] = (isset($user->lang[$unit_info['iec_unit']])) ? $user->lang[$unit_info['iec_unit']] : $unit_info['iec_unit'];
	// Default to IEC
	$unit_info['unit'] = $unit_info['iec_unit'];
	if (!$string_only)
	{
		$unit_info['value'] = $value;
		return $unit_info;
	}
	return $value  . ' ' . $unit_info['unit'];
}
/**
* Determine whether we are approaching the maximum execution time. Should be called once
* at the beginning of the script in which it's used.
* @return	bool	Either true if the maximum execution time is nearly reached, or false
*					if some time is still left.
*/
function still_on_time($extra_time = 15)
{
	static $max_execution_time, $start_time;
	$current_time = microtime(true);
	if (empty($max_execution_time))
	{
		$max_execution_time = (function_exists('ini_get')) ? (int) @ini_get('max_execution_time') : (int) @get_cfg_var('max_execution_time');
		// If zero, then set to something higher to not let the user catch the ten seconds barrier.
		if ($max_execution_time === 0)
		{
			$max_execution_time = 50 + $extra_time;
		}
		$max_execution_time = min(max(10, ($max_execution_time - $extra_time)), 50);
		// For debugging purposes
		// $max_execution_time = 10;
		global $starttime;
		$start_time = (empty($starttime)) ? $current_time : $starttime;
	}
	return (ceil($current_time - $start_time) < $max_execution_time) ? true : false;
}
/**
* Hashes an email address to a big integer
*
* @param string $email		Email address
*
* @return string			Unsigned Big Integer
*/
function phpbb_email_hash($email)
{
	return sprintf('%u', crc32(strtolower($email))) . strlen($email);
}
/**
* Wrapper for version_compare() that allows using uppercase A and B
* for alpha and beta releases.
*
* See http://www.php.net/manual/en/function.version-compare.php
*
* @param string $version1		First version number
* @param string $version2		Second version number
* @param string $operator		Comparison operator (optional)
*
* @return mixed					Boolean (true, false) if comparison operator is specified.
*								Integer (-1, 0, 1) otherwise.
*/
function phpbb_version_compare($version1, $version2, $operator = null)
{
	$version1 = strtolower($version1);
	$version2 = strtolower($version2);
	if (is_null($operator))
	{
		return version_compare($version1, $version2);
	}
	else
	{
		return version_compare($version1, $version2, $operator);
	}
}
// functions used for building option fields
/**
* Pick a language, any language ...
*/
function language_select($default = '')
{
	global $db;
	$sql = 'SELECT lang_iso, lang_local_name
		FROM ' . LANG_TABLE . '
		ORDER BY lang_english_name';
	$result = $db->sql_query($sql);
	$lang_options = '';
	while ($row = $db->sql_fetchrow($result))
	{
		$selected = ($row['lang_iso'] == $default) ? ' selected="selected"' : '';
		$lang_options .= '';
	}
	$db->sql_freeresult($result);
	return $lang_options;
}
/**
* Pick a template/theme combo,
*/
function style_select($default = '', $all = false)
{
	global $db;
	$sql_where = (!$all) ? 'WHERE style_active = 1 ' : '';
	$sql = 'SELECT style_id, style_name
		FROM ' . STYLES_TABLE . "
		$sql_where
		ORDER BY style_name";
	$result = $db->sql_query($sql);
	$style_options = '';
	while ($row = $db->sql_fetchrow($result))
	{
		$selected = ($row['style_id'] == $default) ? ' selected="selected"' : '';
		$style_options .= '';
	}
	$db->sql_freeresult($result);
	return $style_options;
}
/**
* Format the timezone offset with hours and minutes
*
* @param	int		$tz_offset	Timezone offset in seconds
* @param	bool	$show_null	Whether null offsets should be shown
* @return	string		Normalized offset string:	-7200 => -02:00
*													16200 => +04:30
*/
function phpbb_format_timezone_offset($tz_offset, $show_null = false)
{
	$sign = ($tz_offset < 0) ? '-' : '+';
	$time_offset = abs($tz_offset);
	if ($time_offset == 0 && $show_null == false)
	{
		return '';
	}
	$offset_seconds	= $time_offset % 3600;
	$offset_minutes	= $offset_seconds / 60;
	$offset_hours	= ($time_offset - $offset_seconds) / 3600;
	$offset_string	= sprintf("%s%02d:%02d", $sign, $offset_hours, $offset_minutes);
	return $offset_string;
}
/**
* Compares two time zone labels.
* Arranges them in increasing order by timezone offset.
* Places UTC before other timezones in the same offset.
*/
function phpbb_tz_select_compare($a, $b)
{
	$a_sign = $a[3];
	$b_sign = $b[3];
	if ($a_sign != $b_sign)
	{
		return $a_sign == '-' ? -1 : 1;
	}
	$a_offset = substr($a, 4, 5);
	$b_offset = substr($b, 4, 5);
	if ($a_offset == $b_offset)
	{
		$a_name = substr($a, 12);
		$b_name = substr($b, 12);
		if ($a_name == $b_name)
		{
			return 0;
		}
		else if ($a_name == 'UTC')
		{
			return -1;
		}
		else if ($b_name == 'UTC')
		{
			return 1;
		}
		else
		{
			return $a_name < $b_name ? -1 : 1;
		}
	}
	else
	{
		if ($a_sign == '-')
		{
			return $a_offset > $b_offset ? -1 : 1;
		}
		else
		{
			return $a_offset < $b_offset ? -1 : 1;
		}
	}
}
/**
* Return list of timezone identifiers
* We also add the selected timezone if we can create an object with it.
* DateTimeZone::listIdentifiers seems to not add all identifiers to the list,
* because some are only kept for backward compatible reasons. If the user has
* a deprecated value, we add it here, so it can still be kept. Once the user
* changed his value, there is no way back to deprecated values.
*
* @param	string		$selected_timezone		Additional timezone that shall
*												be added to the list of identiers
* @return		array		DateTimeZone::listIdentifiers and additional
*							selected_timezone if it is a valid timezone.
*/
function phpbb_get_timezone_identifiers($selected_timezone)
{
	$timezones = DateTimeZone::listIdentifiers();
	if (!in_array($selected_timezone, $timezones))
	{
		try
		{
			// Add valid timezones that are currently selected but not returned
			// by DateTimeZone::listIdentifiers
			$validate_timezone = new DateTimeZone($selected_timezone);
			$timezones[] = $selected_timezone;
		}
		catch (\Exception $e)
		{
		}
	}
	return $timezones;
}
/**
* Options to pick a timezone and date/time
*
* @param	\phpbb\template\template $template	phpBB template object
* @param	\phpbb\user	$user				Object of the current user
* @param	string		$default			A timezone to select
* @param	boolean		$truncate			Shall we truncate the options text
*
* @return		array		Returns an array containing the options for the time selector.
*/
function phpbb_timezone_select($template, $user, $default = '', $truncate = false)
{
	static $timezones;
	$default_offset = '';
	if (!isset($timezones))
	{
		$unsorted_timezones = phpbb_get_timezone_identifiers($default);
		$timezones = array();
		foreach ($unsorted_timezones as $timezone)
		{
			$tz = new DateTimeZone($timezone);
			$dt = $user->create_datetime('now', $tz);
			$offset = $dt->getOffset();
			$current_time = $dt->format($user->lang['DATETIME_FORMAT'], true);
			$offset_string = phpbb_format_timezone_offset($offset, true);
			$timezones['UTC' . $offset_string . ' - ' . $timezone] = array(
				'tz'		=> $timezone,
				'offset'	=> $offset_string,
				'current'	=> $current_time,
			);
			if ($timezone === $default)
			{
				$default_offset = 'UTC' . $offset_string;
			}
		}
		unset($unsorted_timezones);
		uksort($timezones, 'phpbb_tz_select_compare');
	}
	$tz_select = $opt_group = '';
	foreach ($timezones as $key => $timezone)
	{
		if ($opt_group != $timezone['offset'])
		{
			// Generate tz_select for backwards compatibility
			$tz_select .= ($opt_group) ? '' : '';
			$tz_select .= '';
	return $tz_select;
}
// Functions handling topic/post tracking/marking
/**
* Marks a topic/forum as read
* Marks a topic as posted to
*
* @param string $mode (all, topics, topic, post)
* @param int|bool $forum_id Used in all, topics, and topic mode
* @param int|bool $topic_id Used in topic and post mode
* @param int $post_time 0 means current time(), otherwise to set a specific mark time
* @param int $user_id can only be used with $mode == 'post'
*/
function markread($mode, $forum_id = false, $topic_id = false, $post_time = 0, $user_id = 0)
{
	global $db, $user, $config;
	global $request, $phpbb_container, $phpbb_dispatcher;
	$post_time = ($post_time === 0 || $post_time > time()) ? time() : (int) $post_time;
	$should_markread = true;
	/**
	 * This event is used for performing actions directly before marking forums,
	 * topics or posts as read.
	 *
	 * It is also possible to prevent the marking. For that, the $should_markread parameter
	 * should be set to FALSE.
	 *
	 * @event core.markread_before
	 * @var	string	mode				Variable containing marking mode value
	 * @var	mixed	forum_id			Variable containing forum id, or false
	 * @var	mixed	topic_id			Variable containing topic id, or false
	 * @var	int		post_time			Variable containing post time
	 * @var	int		user_id				Variable containing the user id
	 * @var	bool	should_markread		Flag indicating if the markread should be done or not.
	 * @since 3.1.4-RC1
	 */
	$vars = array(
		'mode',
		'forum_id',
		'topic_id',
		'post_time',
		'user_id',
		'should_markread',
	);
	extract($phpbb_dispatcher->trigger_event('core.markread_before', compact($vars)));
	if (!$should_markread)
	{
		return;
	}
	if ($mode == 'all')
	{
		if (empty($forum_id))
		{
			// Mark all forums read (index page)
			/* @var $phpbb_notifications \phpbb\notification\manager */
			$phpbb_notifications = $phpbb_container->get('notification_manager');
			// Mark all topic notifications read for this user
			$phpbb_notifications->mark_notifications(array(
				'notification.type.topic',
				'notification.type.quote',
				'notification.type.bookmark',
				'notification.type.post',
				'notification.type.approve_topic',
				'notification.type.approve_post',
			), false, $user->data['user_id'], $post_time);
			if ($config['load_db_lastread'] && $user->data['is_registered'])
			{
				// Mark all forums read (index page)
				$tables = array(TOPICS_TRACK_TABLE, FORUMS_TRACK_TABLE);
				foreach ($tables as $table)
				{
					$sql = 'DELETE FROM ' . $table . "
						WHERE user_id = {$user->data['user_id']}
							AND mark_time < $post_time";
					$db->sql_query($sql);
				}
				$sql = 'UPDATE ' . USERS_TABLE . "
					SET user_lastmark = $post_time
					WHERE user_id = {$user->data['user_id']}
						AND user_lastmark < $post_time";
				$db->sql_query($sql);
			}
			else if ($config['load_anon_lastread'] || $user->data['is_registered'])
			{
				$tracking_topics = $request->variable($config['cookie_name'] . '_track', '', true, \phpbb\request\request_interface::COOKIE);
				$tracking_topics = ($tracking_topics) ? tracking_unserialize($tracking_topics) : array();
				unset($tracking_topics['tf']);
				unset($tracking_topics['t']);
				unset($tracking_topics['f']);
				$tracking_topics['l'] = base_convert($post_time - $config['board_startdate'], 10, 36);
				$user->set_cookie('track', tracking_serialize($tracking_topics), $post_time + 31536000);
				$request->overwrite($config['cookie_name'] . '_track', tracking_serialize($tracking_topics), \phpbb\request\request_interface::COOKIE);
				unset($tracking_topics);
				if ($user->data['is_registered'])
				{
					$sql = 'UPDATE ' . USERS_TABLE . "
						SET user_lastmark = $post_time
						WHERE user_id = {$user->data['user_id']}
							AND user_lastmark < $post_time";
					$db->sql_query($sql);
				}
			}
		}
	}
	else if ($mode == 'topics')
	{
		// Mark all topics in forums read
		if (!is_array($forum_id))
		{
			$forum_id = array($forum_id);
		}
		else
		{
			$forum_id = array_unique($forum_id);
		}
		/* @var $phpbb_notifications \phpbb\notification\manager */
		$phpbb_notifications = $phpbb_container->get('notification_manager');
		$phpbb_notifications->mark_notifications_by_parent(array(
			'notification.type.topic',
			'notification.type.approve_topic',
		), $forum_id, $user->data['user_id'], $post_time);
		// Mark all post/quote notifications read for this user in this forum
		$topic_ids = array();
		$sql = 'SELECT topic_id
			FROM ' . TOPICS_TABLE . '
			WHERE ' . $db->sql_in_set('forum_id', $forum_id);
		$result = $db->sql_query($sql);
		while ($row = $db->sql_fetchrow($result))
		{
			$topic_ids[] = $row['topic_id'];
		}
		$db->sql_freeresult($result);
		$phpbb_notifications->mark_notifications_by_parent(array(
			'notification.type.quote',
			'notification.type.bookmark',
			'notification.type.post',
			'notification.type.approve_post',
		), $topic_ids, $user->data['user_id'], $post_time);
		// Add 0 to forums array to mark global announcements correctly
		// $forum_id[] = 0;
		if ($config['load_db_lastread'] && $user->data['is_registered'])
		{
			$sql = 'DELETE FROM ' . TOPICS_TRACK_TABLE . "
				WHERE user_id = {$user->data['user_id']}
					AND mark_time < $post_time
					AND " . $db->sql_in_set('forum_id', $forum_id);
			$db->sql_query($sql);
			$sql = 'SELECT forum_id
				FROM ' . FORUMS_TRACK_TABLE . "
				WHERE user_id = {$user->data['user_id']}
					AND " . $db->sql_in_set('forum_id', $forum_id);
			$result = $db->sql_query($sql);
			$sql_update = array();
			while ($row = $db->sql_fetchrow($result))
			{
				$sql_update[] = (int) $row['forum_id'];
			}
			$db->sql_freeresult($result);
			if (count($sql_update))
			{
				$sql = 'UPDATE ' . FORUMS_TRACK_TABLE . "
					SET mark_time = $post_time
					WHERE user_id = {$user->data['user_id']}
						AND mark_time < $post_time
						AND " . $db->sql_in_set('forum_id', $sql_update);
				$db->sql_query($sql);
			}
			if ($sql_insert = array_diff($forum_id, $sql_update))
			{
				$sql_ary = array();
				foreach ($sql_insert as $f_id)
				{
					$sql_ary[] = array(
						'user_id'	=> (int) $user->data['user_id'],
						'forum_id'	=> (int) $f_id,
						'mark_time'	=> $post_time,
					);
				}
				$db->sql_multi_insert(FORUMS_TRACK_TABLE, $sql_ary);
			}
		}
		else if ($config['load_anon_lastread'] || $user->data['is_registered'])
		{
			$tracking = $request->variable($config['cookie_name'] . '_track', '', true, \phpbb\request\request_interface::COOKIE);
			$tracking = ($tracking) ? tracking_unserialize($tracking) : array();
			foreach ($forum_id as $f_id)
			{
				$topic_ids36 = (isset($tracking['tf'][$f_id])) ? $tracking['tf'][$f_id] : array();
				if (isset($tracking['tf'][$f_id]))
				{
					unset($tracking['tf'][$f_id]);
				}
				foreach ($topic_ids36 as $topic_id36)
				{
					unset($tracking['t'][$topic_id36]);
				}
				if (isset($tracking['f'][$f_id]))
				{
					unset($tracking['f'][$f_id]);
				}
				$tracking['f'][$f_id] = base_convert($post_time - $config['board_startdate'], 10, 36);
			}
			if (isset($tracking['tf']) && empty($tracking['tf']))
			{
				unset($tracking['tf']);
			}
			$user->set_cookie('track', tracking_serialize($tracking), $post_time + 31536000);
			$request->overwrite($config['cookie_name'] . '_track', tracking_serialize($tracking), \phpbb\request\request_interface::COOKIE);
			unset($tracking);
		}
	}
	else if ($mode == 'topic')
	{
		if ($topic_id === false || $forum_id === false)
		{
			return;
		}
		/* @var $phpbb_notifications \phpbb\notification\manager */
		$phpbb_notifications = $phpbb_container->get('notification_manager');
		// Mark post notifications read for this user in this topic
		$phpbb_notifications->mark_notifications(array(
			'notification.type.topic',
			'notification.type.approve_topic',
		), $topic_id, $user->data['user_id'], $post_time);
		$phpbb_notifications->mark_notifications_by_parent(array(
			'notification.type.quote',
			'notification.type.bookmark',
			'notification.type.post',
			'notification.type.approve_post',
		), $topic_id, $user->data['user_id'], $post_time);
		if ($config['load_db_lastread'] && $user->data['is_registered'])
		{
			$sql = 'UPDATE ' . TOPICS_TRACK_TABLE . "
				SET mark_time = $post_time
				WHERE user_id = {$user->data['user_id']}
					AND mark_time < $post_time
					AND topic_id = $topic_id";
			$db->sql_query($sql);
			// insert row
			if (!$db->sql_affectedrows())
			{
				$db->sql_return_on_error(true);
				$sql_ary = array(
					'user_id'		=> (int) $user->data['user_id'],
					'topic_id'		=> (int) $topic_id,
					'forum_id'		=> (int) $forum_id,
					'mark_time'		=> $post_time,
				);
				$db->sql_query('INSERT INTO ' . TOPICS_TRACK_TABLE . ' ' . $db->sql_build_array('INSERT', $sql_ary));
				$db->sql_return_on_error(false);
			}
		}
		else if ($config['load_anon_lastread'] || $user->data['is_registered'])
		{
			$tracking = $request->variable($config['cookie_name'] . '_track', '', true, \phpbb\request\request_interface::COOKIE);
			$tracking = ($tracking) ? tracking_unserialize($tracking) : array();
			$topic_id36 = base_convert($topic_id, 10, 36);
			if (!isset($tracking['t'][$topic_id36]))
			{
				$tracking['tf'][$forum_id][$topic_id36] = true;
			}
			$tracking['t'][$topic_id36] = base_convert($post_time - (int) $config['board_startdate'], 10, 36);
			// If the cookie grows larger than 10000 characters we will remove the smallest value
			// This can result in old topics being unread - but most of the time it should be accurate...
			if (strlen($request->variable($config['cookie_name'] . '_track', '', true, \phpbb\request\request_interface::COOKIE)) > 10000)
			{
				//echo 'Cookie grown too large' . print_r($tracking, true);
				// We get the ten most minimum stored time offsets and its associated topic ids
				$time_keys = array();
				for ($i = 0; $i < 10 && count($tracking['t']); $i++)
				{
					$min_value = min($tracking['t']);
					$m_tkey = array_search($min_value, $tracking['t']);
					unset($tracking['t'][$m_tkey]);
					$time_keys[$m_tkey] = $min_value;
				}
				// Now remove the topic ids from the array...
				foreach ($tracking['tf'] as $f_id => $topic_id_ary)
				{
					foreach ($time_keys as $m_tkey => $min_value)
					{
						if (isset($topic_id_ary[$m_tkey]))
						{
							$tracking['f'][$f_id] = $min_value;
							unset($tracking['tf'][$f_id][$m_tkey]);
						}
					}
				}
				if ($user->data['is_registered'])
				{
					$user->data['user_lastmark'] = intval(base_convert(max($time_keys) + $config['board_startdate'], 36, 10));
					$sql = 'UPDATE ' . USERS_TABLE . "
						SET user_lastmark = $post_time
						WHERE user_id = {$user->data['user_id']}
							AND mark_time < $post_time";
					$db->sql_query($sql);
				}
				else
				{
					$tracking['l'] = max($time_keys);
				}
			}
			$user->set_cookie('track', tracking_serialize($tracking), $post_time + 31536000);
			$request->overwrite($config['cookie_name'] . '_track', tracking_serialize($tracking), \phpbb\request\request_interface::COOKIE);
		}
	}
	else if ($mode == 'post')
	{
		if ($topic_id === false)
		{
			return;
		}
		$use_user_id = (!$user_id) ? $user->data['user_id'] : $user_id;
		if ($config['load_db_track'] && $use_user_id != ANONYMOUS)
		{
			$db->sql_return_on_error(true);
			$sql_ary = array(
				'user_id'		=> (int) $use_user_id,
				'topic_id'		=> (int) $topic_id,
				'topic_posted'	=> 1,
			);
			$db->sql_query('INSERT INTO ' . TOPICS_POSTED_TABLE . ' ' . $db->sql_build_array('INSERT', $sql_ary));
			$db->sql_return_on_error(false);
		}
	}
	/**
	 * This event is used for performing actions directly after forums,
	 * topics or posts have been marked as read.
	 *
	 * @event core.markread_after
	 * @var	string		mode				Variable containing marking mode value
	 * @var	mixed		forum_id			Variable containing forum id, or false
	 * @var	mixed		topic_id			Variable containing topic id, or false
	 * @var	int			post_time			Variable containing post time
	 * @var	int			user_id				Variable containing the user id
	 * @since 3.2.6-RC1
	 */
	$vars = array(
		'mode',
		'forum_id',
		'topic_id',
		'post_time',
		'user_id',
	);
	extract($phpbb_dispatcher->trigger_event('core.markread_after', compact($vars)));
}
/**
* Get topic tracking info by using already fetched info
*/
function get_topic_tracking($forum_id, $topic_ids, &$rowset, $forum_mark_time, $global_announce_list = false)
{
	global $user;
	$last_read = array();
	if (!is_array($topic_ids))
	{
		$topic_ids = array($topic_ids);
	}
	foreach ($topic_ids as $topic_id)
	{
		if (!empty($rowset[$topic_id]['mark_time']))
		{
			$last_read[$topic_id] = $rowset[$topic_id]['mark_time'];
		}
	}
	$topic_ids = array_diff($topic_ids, array_keys($last_read));
	if (count($topic_ids))
	{
		$mark_time = array();
		if (!empty($forum_mark_time[$forum_id]) && $forum_mark_time[$forum_id] !== false)
		{
			$mark_time[$forum_id] = $forum_mark_time[$forum_id];
		}
		$user_lastmark = (isset($mark_time[$forum_id])) ? $mark_time[$forum_id] : $user->data['user_lastmark'];
		foreach ($topic_ids as $topic_id)
		{
			$last_read[$topic_id] = $user_lastmark;
		}
	}
	return $last_read;
}
/**
* Get topic tracking info from db (for cookie based tracking only this function is used)
*/
function get_complete_topic_tracking($forum_id, $topic_ids, $global_announce_list = false)
{
	global $config, $user, $request;
	$last_read = array();
	if (!is_array($topic_ids))
	{
		$topic_ids = array($topic_ids);
	}
	if ($config['load_db_lastread'] && $user->data['is_registered'])
	{
		global $db;
		$sql = 'SELECT topic_id, mark_time
			FROM ' . TOPICS_TRACK_TABLE . "
			WHERE user_id = {$user->data['user_id']}
				AND " . $db->sql_in_set('topic_id', $topic_ids);
		$result = $db->sql_query($sql);
		while ($row = $db->sql_fetchrow($result))
		{
			$last_read[$row['topic_id']] = $row['mark_time'];
		}
		$db->sql_freeresult($result);
		$topic_ids = array_diff($topic_ids, array_keys($last_read));
		if (count($topic_ids))
		{
			$sql = 'SELECT forum_id, mark_time
				FROM ' . FORUMS_TRACK_TABLE . "
				WHERE user_id = {$user->data['user_id']}
					AND forum_id = $forum_id";
			$result = $db->sql_query($sql);
			$mark_time = array();
			while ($row = $db->sql_fetchrow($result))
			{
				$mark_time[$row['forum_id']] = $row['mark_time'];
			}
			$db->sql_freeresult($result);
			$user_lastmark = (isset($mark_time[$forum_id])) ? $mark_time[$forum_id] : $user->data['user_lastmark'];
			foreach ($topic_ids as $topic_id)
			{
				$last_read[$topic_id] = $user_lastmark;
			}
		}
	}
	else if ($config['load_anon_lastread'] || $user->data['is_registered'])
	{
		global $tracking_topics;
		if (!isset($tracking_topics) || !count($tracking_topics))
		{
			$tracking_topics = $request->variable($config['cookie_name'] . '_track', '', true, \phpbb\request\request_interface::COOKIE);
			$tracking_topics = ($tracking_topics) ? tracking_unserialize($tracking_topics) : array();
		}
		if (!$user->data['is_registered'])
		{
			$user_lastmark = (isset($tracking_topics['l'])) ? base_convert($tracking_topics['l'], 36, 10) + $config['board_startdate'] : 0;
		}
		else
		{
			$user_lastmark = $user->data['user_lastmark'];
		}
		foreach ($topic_ids as $topic_id)
		{
			$topic_id36 = base_convert($topic_id, 10, 36);
			if (isset($tracking_topics['t'][$topic_id36]))
			{
				$last_read[$topic_id] = base_convert($tracking_topics['t'][$topic_id36], 36, 10) + $config['board_startdate'];
			}
		}
		$topic_ids = array_diff($topic_ids, array_keys($last_read));
		if (count($topic_ids))
		{
			$mark_time = array();
			if (isset($tracking_topics['f'][$forum_id]))
			{
				$mark_time[$forum_id] = base_convert($tracking_topics['f'][$forum_id], 36, 10) + $config['board_startdate'];
			}
			$user_lastmark = (isset($mark_time[$forum_id])) ? $mark_time[$forum_id] : $user_lastmark;
			foreach ($topic_ids as $topic_id)
			{
				$last_read[$topic_id] = $user_lastmark;
			}
		}
	}
	return $last_read;
}
/**
* Get list of unread topics
*
* @param int $user_id			User ID (or false for current user)
* @param string $sql_extra		Extra WHERE SQL statement
* @param string $sql_sort		ORDER BY SQL sorting statement
* @param string $sql_limit		Limits the size of unread topics list, 0 for unlimited query
* @param string $sql_limit_offset  Sets the offset of the first row to search, 0 to search from the start
*
* @return array[int][int]		Topic ids as keys, mark_time of topic as value
*/
function get_unread_topics($user_id = false, $sql_extra = '', $sql_sort = '', $sql_limit = 1001, $sql_limit_offset = 0)
{
	global $config, $db, $user, $request;
	global $phpbb_dispatcher;
	$user_id = ($user_id === false) ? (int) $user->data['user_id'] : (int) $user_id;
	// Data array we're going to return
	$unread_topics = array();
	if (empty($sql_sort))
	{
		$sql_sort = 'ORDER BY t.topic_last_post_time DESC, t.topic_last_post_id DESC';
	}
	if ($config['load_db_lastread'] && $user->data['is_registered'])
	{
		// Get list of the unread topics
		$last_mark = (int) $user->data['user_lastmark'];
		$sql_array = array(
			'SELECT'		=> 't.topic_id, t.topic_last_post_time, tt.mark_time as topic_mark_time, ft.mark_time as forum_mark_time',
			'FROM'			=> array(TOPICS_TABLE => 't'),
			'LEFT_JOIN'		=> array(
				array(
					'FROM'	=> array(TOPICS_TRACK_TABLE => 'tt'),
					'ON'	=> "tt.user_id = $user_id AND t.topic_id = tt.topic_id",
				),
				array(
					'FROM'	=> array(FORUMS_TRACK_TABLE => 'ft'),
					'ON'	=> "ft.user_id = $user_id AND t.forum_id = ft.forum_id",
				),
			),
			'WHERE'			=> "
				 t.topic_last_post_time > $last_mark AND
				(
				(tt.mark_time IS NOT NULL AND t.topic_last_post_time > tt.mark_time) OR
				(tt.mark_time IS NULL AND ft.mark_time IS NOT NULL AND t.topic_last_post_time > ft.mark_time) OR
				(tt.mark_time IS NULL AND ft.mark_time IS NULL)
				)
				$sql_extra
				$sql_sort",
		);
		/**
		 * Change SQL query for fetching unread topics data
		 *
		 * @event core.get_unread_topics_modify_sql
		 * @var array     sql_array    Fully assembled SQL query with keys SELECT, FROM, LEFT_JOIN, WHERE
		 * @var int       last_mark    User's last_mark time
		 * @var string    sql_extra    Extra WHERE SQL statement
		 * @var string    sql_sort     ORDER BY SQL sorting statement
		 * @since 3.1.4-RC1
		 */
		$vars = array(
			'sql_array',
			'last_mark',
			'sql_extra',
			'sql_sort',
		);
		extract($phpbb_dispatcher->trigger_event('core.get_unread_topics_modify_sql', compact($vars)));
		$sql = $db->sql_build_query('SELECT', $sql_array);
		$result = $db->sql_query_limit($sql, $sql_limit, $sql_limit_offset);
		while ($row = $db->sql_fetchrow($result))
		{
			$topic_id = (int) $row['topic_id'];
			$unread_topics[$topic_id] = ($row['topic_mark_time']) ? (int) $row['topic_mark_time'] : (($row['forum_mark_time']) ? (int) $row['forum_mark_time'] : $last_mark);
		}
		$db->sql_freeresult($result);
	}
	else if ($config['load_anon_lastread'] || $user->data['is_registered'])
	{
		global $tracking_topics;
		if (empty($tracking_topics))
		{
			$tracking_topics = $request->variable($config['cookie_name'] . '_track', '', false, \phpbb\request\request_interface::COOKIE);
			$tracking_topics = ($tracking_topics) ? tracking_unserialize($tracking_topics) : array();
		}
		if (!$user->data['is_registered'])
		{
			$user_lastmark = (isset($tracking_topics['l'])) ? base_convert($tracking_topics['l'], 36, 10) + $config['board_startdate'] : 0;
		}
		else
		{
			$user_lastmark = (int) $user->data['user_lastmark'];
		}
		$sql = 'SELECT t.topic_id, t.forum_id, t.topic_last_post_time
			FROM ' . TOPICS_TABLE . ' t
			WHERE t.topic_last_post_time > ' . $user_lastmark . "
			$sql_extra
			$sql_sort";
		$result = $db->sql_query_limit($sql, $sql_limit, $sql_limit_offset);
		while ($row = $db->sql_fetchrow($result))
		{
			$forum_id = (int) $row['forum_id'];
			$topic_id = (int) $row['topic_id'];
			$topic_id36 = base_convert($topic_id, 10, 36);
			if (isset($tracking_topics['t'][$topic_id36]))
			{
				$last_read = base_convert($tracking_topics['t'][$topic_id36], 36, 10) + $config['board_startdate'];
				if ($row['topic_last_post_time'] > $last_read)
				{
					$unread_topics[$topic_id] = $last_read;
				}
			}
			else if (isset($tracking_topics['f'][$forum_id]))
			{
				$mark_time = base_convert($tracking_topics['f'][$forum_id], 36, 10) + $config['board_startdate'];
				if ($row['topic_last_post_time'] > $mark_time)
				{
					$unread_topics[$topic_id] = $mark_time;
				}
			}
			else
			{
				$unread_topics[$topic_id] = $user_lastmark;
			}
		}
		$db->sql_freeresult($result);
	}
	return $unread_topics;
}
/**
* Check for read forums and update topic tracking info accordingly
*
* @param int $forum_id the forum id to check
* @param int $forum_last_post_time the forums last post time
* @param int $f_mark_time the forums last mark time if user is registered and load_db_lastread enabled
* @param int $mark_time_forum false if the mark time needs to be obtained, else the last users forum mark time
*
* @return true if complete forum got marked read, else false.
*/
function update_forum_tracking_info($forum_id, $forum_last_post_time, $f_mark_time = false, $mark_time_forum = false)
{
	global $db, $tracking_topics, $user, $config, $request, $phpbb_container;
	// Determine the users last forum mark time if not given.
	if ($mark_time_forum === false)
	{
		if ($config['load_db_lastread'] && $user->data['is_registered'])
		{
			$mark_time_forum = (!empty($f_mark_time)) ? $f_mark_time : $user->data['user_lastmark'];
		}
		else if ($config['load_anon_lastread'] || $user->data['is_registered'])
		{
			$tracking_topics = $request->variable($config['cookie_name'] . '_track', '', true, \phpbb\request\request_interface::COOKIE);
			$tracking_topics = ($tracking_topics) ? tracking_unserialize($tracking_topics) : array();
			if (!$user->data['is_registered'])
			{
				$user->data['user_lastmark'] = (isset($tracking_topics['l'])) ? (int) (base_convert($tracking_topics['l'], 36, 10) + $config['board_startdate']) : 0;
			}
			$mark_time_forum = (isset($tracking_topics['f'][$forum_id])) ? (int) (base_convert($tracking_topics['f'][$forum_id], 36, 10) + $config['board_startdate']) : $user->data['user_lastmark'];
		}
	}
	// Handle update of unapproved topics info.
	// Only update for moderators having m_approve permission for the forum.
	/* @var $phpbb_content_visibility \phpbb\content_visibility */
	$phpbb_content_visibility = $phpbb_container->get('content.visibility');
	// Check the forum for any left unread topics.
	// If there are none, we mark the forum as read.
	if ($config['load_db_lastread'] && $user->data['is_registered'])
	{
		if ($mark_time_forum >= $forum_last_post_time)
		{
			// We do not need to mark read, this happened before. Therefore setting this to true
			$row = true;
		}
		else
		{
			$sql = 'SELECT t.forum_id
				FROM ' . TOPICS_TABLE . ' t
				LEFT JOIN ' . TOPICS_TRACK_TABLE . ' tt
					ON (tt.topic_id = t.topic_id
						AND tt.user_id = ' . $user->data['user_id'] . ')
				WHERE t.forum_id = ' . $forum_id . '
					AND t.topic_last_post_time > ' . $mark_time_forum . '
					AND t.topic_moved_id = 0
					AND ' . $phpbb_content_visibility->get_visibility_sql('topic', $forum_id, 't.') . '
					AND (tt.topic_id IS NULL
						OR tt.mark_time < t.topic_last_post_time)';
			$result = $db->sql_query_limit($sql, 1);
			$row = $db->sql_fetchrow($result);
			$db->sql_freeresult($result);
		}
	}
	else if ($config['load_anon_lastread'] || $user->data['is_registered'])
	{
		// Get information from cookie
		if (!isset($tracking_topics['tf'][$forum_id]))
		{
			// We do not need to mark read, this happened before. Therefore setting this to true
			$row = true;
		}
		else
		{
			$sql = 'SELECT t.topic_id
				FROM ' . TOPICS_TABLE . ' t
				WHERE t.forum_id = ' . $forum_id . '
					AND t.topic_last_post_time > ' . $mark_time_forum . '
					AND t.topic_moved_id = 0
					AND ' . $phpbb_content_visibility->get_visibility_sql('topic', $forum_id, 't.');
			$result = $db->sql_query($sql);
			$check_forum = $tracking_topics['tf'][$forum_id];
			$unread = false;
			while ($row = $db->sql_fetchrow($result))
			{
				if (!isset($check_forum[base_convert($row['topic_id'], 10, 36)]))
				{
					$unread = true;
					break;
				}
			}
			$db->sql_freeresult($result);
			$row = $unread;
		}
	}
	else
	{
		$row = true;
	}
	if (!$row)
	{
		markread('topics', $forum_id);
		return true;
	}
	return false;
}
/**
* Transform an array into a serialized format
*/
function tracking_serialize($input)
{
	$out = '';
	foreach ($input as $key => $value)
	{
		if (is_array($value))
		{
			$out .= $key . ':(' . tracking_serialize($value) . ');';
		}
		else
		{
			$out .= $key . ':' . $value . ';';
		}
	}
	return $out;
}
/**
* Transform a serialized array into an actual array
*/
function tracking_unserialize($string, $max_depth = 3)
{
	$n = strlen($string);
	if ($n > 10010)
	{
		die('Invalid data supplied');
	}
	$data = $stack = array();
	$key = '';
	$mode = 0;
	$level = &$data;
	for ($i = 0; $i < $n; ++$i)
	{
		switch ($mode)
		{
			case 0:
				switch ($string[$i])
				{
					case ':':
						$level[$key] = 0;
						$mode = 1;
					break;
					case ')':
						unset($level);
						$level = array_pop($stack);
						$mode = 3;
					break;
					default:
						$key .= $string[$i];
				}
			break;
			case 1:
				switch ($string[$i])
				{
					case '(':
						if (count($stack) >= $max_depth)
						{
							die('Invalid data supplied');
						}
						$stack[] = &$level;
						$level[$key] = array();
						$level = &$level[$key];
						$key = '';
						$mode = 0;
					break;
					default:
						$level[$key] = $string[$i];
						$mode = 2;
					break;
				}
			break;
			case 2:
				switch ($string[$i])
				{
					case ')':
						unset($level);
						$level = array_pop($stack);
						$mode = 3;
					break;
					case ';':
						$key = '';
						$mode = 0;
					break;
					default:
						$level[$key] .= $string[$i];
					break;
				}
			break;
			case 3:
				switch ($string[$i])
				{
					case ')':
						unset($level);
						$level = array_pop($stack);
					break;
					case ';':
						$key = '';
						$mode = 0;
					break;
					default:
						die('Invalid data supplied');
					break;
				}
			break;
		}
	}
	if (count($stack) != 0 || ($mode != 0 && $mode != 3))
	{
		die('Invalid data supplied');
	}
	return $level;
}
// Server functions (building urls, redirecting...)
/**
* Append session id to url.
* This function supports hooks.
*
* @param string $url The url the session id needs to be appended to (can have params)
* @param mixed $params String or array of additional url parameters
* @param bool $is_amp Is url using & (true) or & (false)
* @param string $session_id Possibility to use a custom session id instead of the global one
* @param bool $is_route Is url generated by a route.
*
* @return string The corrected url.
*
* Examples:
* 
* append_sid("{$phpbb_root_path}viewtopic.$phpEx?t=1&f=2");
* append_sid("{$phpbb_root_path}viewtopic.$phpEx", 't=1&f=2');
* append_sid("{$phpbb_root_path}viewtopic.$phpEx", 't=1&f=2', false);
* append_sid("{$phpbb_root_path}viewtopic.$phpEx", array('t' => 1, 'f' => 2));
* 
*
*/
function append_sid($url, $params = false, $is_amp = true, $session_id = false, $is_route = false)
{
	global $_SID, $_EXTRA_URL, $phpbb_hook, $phpbb_path_helper;
	global $phpbb_dispatcher;
	if ($params === '' || (is_array($params) && empty($params)))
	{
		// Do not append the ? if the param-list is empty anyway.
		$params = false;
	}
	// Update the root path with the correct relative web path
	if (!$is_route && $phpbb_path_helper instanceof \phpbb\path_helper)
	{
		$url = $phpbb_path_helper->update_web_root_path($url);
	}
	$append_sid_overwrite = false;
	/**
	* This event can either supplement or override the append_sid() function
	*
	* To override this function, the event must set $append_sid_overwrite to
	* the new URL value, which will be returned following the event
	*
	* @event core.append_sid
	* @var	string		url						The url the session id needs
	*											to be appended to (can have
	*											params)
	* @var	mixed		params					String or array of additional
	*											url parameters
	* @var	bool		is_amp					Is url using & (true) or
	*											& (false)
	* @var	bool|string	session_id				Possibility to use a custom
	*											session id (string) instead of
	*											the global one (false)
	* @var	bool|string	append_sid_overwrite	Overwrite function (string
	*											URL) or not (false)
	* @var	bool	is_route					Is url generated by a route.
	* @since 3.1.0-a1
	*/
	$vars = array('url', 'params', 'is_amp', 'session_id', 'append_sid_overwrite', 'is_route');
	extract($phpbb_dispatcher->trigger_event('core.append_sid', compact($vars)));
	if ($append_sid_overwrite)
	{
		return $append_sid_overwrite;
	}
	// The following hook remains for backwards compatibility, though use of
	// the event above is preferred.
	// Developers using the hook function need to globalise the $_SID and $_EXTRA_URL on their own and also handle it appropriately.
	// They could mimic most of what is within this function
	if (!empty($phpbb_hook) && $phpbb_hook->call_hook(__FUNCTION__, $url, $params, $is_amp, $session_id))
	{
		if ($phpbb_hook->hook_return(__FUNCTION__))
		{
			return $phpbb_hook->hook_return_result(__FUNCTION__);
		}
	}
	$params_is_array = is_array($params);
	// Get anchor
	$anchor = '';
	if (strpos($url, '#') !== false)
	{
		list($url, $anchor) = explode('#', $url, 2);
		$anchor = '#' . $anchor;
	}
	else if (!$params_is_array && strpos($params, '#') !== false)
	{
		list($params, $anchor) = explode('#', $params, 2);
		$anchor = '#' . $anchor;
	}
	// Handle really simple cases quickly
	if ($_SID == '' && $session_id === false && empty($_EXTRA_URL) && !$params_is_array && !$anchor)
	{
		if ($params === false)
		{
			return $url;
		}
		$url_delim = (strpos($url, '?') === false) ? '?' : (($is_amp) ? '&' : '&');
		return $url . ($params !== false ? $url_delim. $params : '');
	}
	// Assign sid if session id is not specified
	if ($session_id === false)
	{
		$session_id = $_SID;
	}
	$amp_delim = ($is_amp) ? '&' : '&';
	$url_delim = (strpos($url, '?') === false) ? '?' : $amp_delim;
	// Appending custom url parameter?
	$append_url = (!empty($_EXTRA_URL)) ? implode($amp_delim, $_EXTRA_URL) : '';
	// Use the short variant if possible ;)
	if ($params === false)
	{
		// Append session id
		if (!$session_id)
		{
			return $url . (($append_url) ? $url_delim . $append_url : '') . $anchor;
		}
		else
		{
			return $url . (($append_url) ? $url_delim . $append_url . $amp_delim : $url_delim) . 'sid=' . $session_id . $anchor;
		}
	}
	// Build string if parameters are specified as array
	if (is_array($params))
	{
		$output = array();
		foreach ($params as $key => $item)
		{
			if ($item === NULL)
			{
				continue;
			}
			if ($key == '#')
			{
				$anchor = '#' . $item;
				continue;
			}
			$output[] = $key . '=' . $item;
		}
		$params = implode($amp_delim, $output);
	}
	// Append session id and parameters (even if they are empty)
	// If parameters are empty, the developer can still append his/her parameters without caring about the delimiter
	return $url . (($append_url) ? $url_delim . $append_url . $amp_delim : $url_delim) . $params . ((!$session_id) ? '' : $amp_delim . 'sid=' . $session_id) . $anchor;
}
/**
* Generate board url (example: http://www.example.com/phpBB)
*
* @param bool $without_script_path if set to true the script path gets not appended (example: http://www.example.com)
*
* @return string the generated board url
*/
function generate_board_url($without_script_path = false)
{
	global $config, $user, $request, $symfony_request;
	$server_name = $user->host;
	// Forcing server vars is the only way to specify/override the protocol
	if ($config['force_server_vars'] || !$server_name)
	{
		$server_protocol = ($config['server_protocol']) ? $config['server_protocol'] : (($config['cookie_secure']) ? 'https://' : 'http://');
		$server_name = $config['server_name'];
		$server_port = (int) $config['server_port'];
		$script_path = $config['script_path'];
		$url = $server_protocol . $server_name;
		$cookie_secure = $config['cookie_secure'];
	}
	else
	{
		$server_port = (int) $symfony_request->getPort();
		$forwarded_proto = $request->server('HTTP_X_FORWARDED_PROTO');
		if (!empty($forwarded_proto) && $forwarded_proto === 'https')
		{
			$server_port = 443;
		}
		// Do not rely on cookie_secure, users seem to think that it means a secured cookie instead of an encrypted connection
		$cookie_secure = $request->is_secure() ? 1 : 0;
		$url = (($cookie_secure) ? 'https://' : 'http://') . $server_name;
		$script_path = $user->page['root_script_path'];
	}
	if ($server_port && (($cookie_secure && $server_port <> 443) || (!$cookie_secure && $server_port <> 80)))
	{
		// HTTP HOST can carry a port number (we fetch $user->host, but for old versions this may be true)
		if (strpos($server_name, ':') === false)
		{
			$url .= ':' . $server_port;
		}
	}
	if (!$without_script_path)
	{
		$url .= $script_path;
	}
	// Strip / from the end
	if (substr($url, -1, 1) == '/')
	{
		$url = substr($url, 0, -1);
	}
	return $url;
}
/**
* Redirects the user to another page then exits the script nicely
* This function is intended for urls within the board. It's not meant to redirect to cross-domains.
*
* @param string $url The url to redirect to
* @param bool $return If true, do not redirect but return the sanitized URL. Default is no return.
* @param bool $disable_cd_check If true, redirect() will redirect to an external domain. If false, the redirect point to the boards url if it does not match the current domain. Default is false.
*/
function redirect($url, $return = false, $disable_cd_check = false)
{
	global $user, $phpbb_path_helper, $phpbb_dispatcher;
	if (!$user->is_setup())
	{
		$user->add_lang('common');
	}
	// Make sure no &'s are in, this will break the redirect
	$url = str_replace('&', '&', $url);
	// Determine which type of redirect we need to handle...
	$url_parts = @parse_url($url);
	if ($url_parts === false)
	{
		// Malformed url
		trigger_error('INSECURE_REDIRECT', E_USER_WARNING);
	}
	else if (!empty($url_parts['scheme']) && !empty($url_parts['host']))
	{
		// Attention: only able to redirect within the same domain if $disable_cd_check is false (yourdomain.com -> www.yourdomain.com will not work)
		if (!$disable_cd_check && $url_parts['host'] !== $user->host)
		{
			trigger_error('INSECURE_REDIRECT', E_USER_WARNING);
		}
	}
	else if ($url[0] == '/')
	{
		// Absolute uri, prepend direct url...
		$url = generate_board_url(true) . $url;
	}
	else
	{
		// Relative uri
		$pathinfo = pathinfo($url);
		// Is the uri pointing to the current directory?
		if ($pathinfo['dirname'] == '.')
		{
			$url = str_replace('./', '', $url);
			// Strip / from the beginning
			if ($url && substr($url, 0, 1) == '/')
			{
				$url = substr($url, 1);
			}
		}
		$url = $phpbb_path_helper->remove_web_root_path($url);
		if ($user->page['page_dir'])
		{
			$url = $user->page['page_dir'] . '/' . $url;
		}
		$url = generate_board_url() . '/' . $url;
	}
	// Clean URL and check if we go outside the forum directory
	$url = $phpbb_path_helper->clean_url($url);
	if (!$disable_cd_check && strpos($url, generate_board_url(true) . '/') !== 0)
	{
		trigger_error('INSECURE_REDIRECT', E_USER_WARNING);
	}
	// Make sure no linebreaks are there... to prevent http response splitting for PHP < 4.4.2
	if (strpos(urldecode($url), "\n") !== false || strpos(urldecode($url), "\r") !== false || strpos($url, ';') !== false)
	{
		trigger_error('INSECURE_REDIRECT', E_USER_WARNING);
	}
	// Now, also check the protocol and for a valid url the last time...
	$allowed_protocols = array('http', 'https', 'ftp', 'ftps');
	$url_parts = parse_url($url);
	if ($url_parts === false || empty($url_parts['scheme']) || !in_array($url_parts['scheme'], $allowed_protocols))
	{
		trigger_error('INSECURE_REDIRECT', E_USER_WARNING);
	}
	/**
	* Execute code and/or overwrite redirect()
	*
	* @event core.functions.redirect
	* @var	string	url					The url
	* @var	bool	return				If true, do not redirect but return the sanitized URL.
	* @var	bool	disable_cd_check	If true, redirect() will redirect to an external domain. If false, the redirect point to the boards url if it does not match the current domain.
	* @since 3.1.0-RC3
	*/
	$vars = array('url', 'return', 'disable_cd_check');
	extract($phpbb_dispatcher->trigger_event('core.functions.redirect', compact($vars)));
	if ($return)
	{
		return $url;
	}
	else
	{
		garbage_collection();
	}
	// Behave as per HTTP/1.1 spec for others
	header('Location: ' . $url);
	exit;
}
/**
* Re-Apply session id after page reloads
*/
function reapply_sid($url, $is_route = false)
{
	global $phpEx, $phpbb_root_path;
	if ($url === "index.$phpEx")
	{
		return append_sid("index.$phpEx");
	}
	else if ($url === "{$phpbb_root_path}index.$phpEx")
	{
		return append_sid("{$phpbb_root_path}index.$phpEx");
	}
	// Remove previously added sid
	if (strpos($url, 'sid=') !== false)
	{
		// All kind of links
		$url = preg_replace('/(\?)?(&|&)?sid=[a-z0-9]+/', '', $url);
		// if the sid was the first param, make the old second as first ones
		$url = preg_replace("/$phpEx(&|&)+?/", "$phpEx?", $url);
	}
	return append_sid($url, false, true, false, $is_route);
}
/**
* Returns url from the session/current page with an re-appended SID with optionally stripping vars from the url
*/
function build_url($strip_vars = false)
{
	global $config, $user, $phpbb_path_helper;
	$page = $phpbb_path_helper->get_valid_page($user->page['page'], $config['enable_mod_rewrite']);
	// Append SID
	$redirect = append_sid($page, false, false);
	if ($strip_vars !== false)
	{
		$redirect = $phpbb_path_helper->strip_url_params($redirect, $strip_vars, false);
	}
	else
	{
		$redirect = str_replace('&', '&', $redirect);
	}
	return $redirect . ((strpos($redirect, '?') === false) ? '?' : '');
}
/**
* Meta refresh assignment
* Adds META template variable with meta http tag.
*
* @param int $time Time in seconds for meta refresh tag
* @param string $url URL to redirect to. The url will go through redirect() first before the template variable is assigned
* @param bool $disable_cd_check If true, meta_refresh() will redirect to an external domain. If false, the redirect point to the boards url if it does not match the current domain. Default is false.
*/
function meta_refresh($time, $url, $disable_cd_check = false)
{
	global $template, $refresh_data, $request;
	$url = redirect($url, true, $disable_cd_check);
	if ($request->is_ajax())
	{
		$refresh_data = array(
			'time'	=> $time,
			'url'	=> $url,
		);
	}
	else
	{
		// For XHTML compatibility we change back & to &
		$url = str_replace('&', '&', $url);
		$template->assign_vars(array(
			'META' => '')
		);
	}
	return $url;
}
/**
* Outputs correct status line header.
*
* Depending on php sapi one of the two following forms is used:
*
* Status: 404 Not Found
*
* HTTP/1.x 404 Not Found
*
* HTTP version is taken from HTTP_VERSION environment variable,
* and defaults to 1.0.
*
* Sample usage:
*
* send_status_line(404, 'Not Found');
*
* @param int $code HTTP status code
* @param string $message Message for the status code
* @return null
*/
function send_status_line($code, $message)
{
	if (substr(strtolower(@php_sapi_name()), 0, 3) === 'cgi')
	{
		// in theory, we shouldn't need that due to php doing it. Reality offers a differing opinion, though
		header("Status: $code $message", true, $code);
	}
	else
	{
		$version = phpbb_request_http_version();
		header("$version $code $message", true, $code);
	}
}
/**
* Returns the HTTP version used in the current request.
*
* Handles the case of being called before $request is present,
* in which case it falls back to the $_SERVER superglobal.
*
* @return string HTTP version
*/
function phpbb_request_http_version()
{
	global $request;
	$version = '';
	if ($request && $request->server('SERVER_PROTOCOL'))
	{
		$version = $request->server('SERVER_PROTOCOL');
	}
	else if (isset($_SERVER['SERVER_PROTOCOL']))
	{
		$version = $_SERVER['SERVER_PROTOCOL'];
	}
	if (!empty($version) && is_string($version) && preg_match('#^HTTP/[0-9]\.[0-9]$#', $version))
	{
		return $version;
	}
	return 'HTTP/1.0';
}
//Form validation
/**
* Add a secret hash   for use in links/GET requests
* @param string  $link_name The name of the link; has to match the name used in check_link_hash, otherwise no restrictions apply
* @return string the hash
*/
function generate_link_hash($link_name)
{
	global $user;
	if (!isset($user->data["hash_$link_name"]))
	{
		$user->data["hash_$link_name"] = substr(sha1($user->data['user_form_salt'] . $link_name), 0, 8);
	}
	return $user->data["hash_$link_name"];
}
/**
* checks a link hash - for GET requests
* @param string $token the submitted token
* @param string $link_name The name of the link
* @return boolean true if all is fine
*/
function check_link_hash($token, $link_name)
{
	return $token === generate_link_hash($link_name);
}
/**
* Add a secret token to the form (requires the S_FORM_TOKEN template variable)
* @param string  $form_name The name of the form; has to match the name used in check_form_key, otherwise no restrictions apply
* @param string  $template_variable_suffix A string that is appended to the name of the template variable to which the form elements are assigned
*/
function add_form_key($form_name, $template_variable_suffix = '')
{
	global $config, $template, $user, $phpbb_dispatcher;
	$now = time();
	$token_sid = ($user->data['user_id'] == ANONYMOUS && !empty($config['form_token_sid_guests'])) ? $user->session_id : '';
	$token = sha1($now . $user->data['user_form_salt'] . $form_name . $token_sid);
	$s_fields = build_hidden_fields(array(
		'creation_time' => $now,
		'form_token'	=> $token,
	));
	/**
	* Perform additional actions on creation of the form token
	*
	* @event core.add_form_key
	* @var	string	form_name					The form name
	* @var	int		now							Current time timestamp
	* @var	string	s_fields					Generated hidden fields
	* @var	string	token						Form token
	* @var	string	token_sid					User session ID
	* @var	string	template_variable_suffix	The string that is appended to template variable name
	*
	* @since 3.1.0-RC3
	* @changed 3.1.11-RC1 Added template_variable_suffix
	*/
	$vars = array(
		'form_name',
		'now',
		's_fields',
		'token',
		'token_sid',
		'template_variable_suffix',
	);
	extract($phpbb_dispatcher->trigger_event('core.add_form_key', compact($vars)));
	$template->assign_var('S_FORM_TOKEN' . $template_variable_suffix, $s_fields);
}
/**
 * Check the form key. Required for all altering actions not secured by confirm_box
 *
 * @param	string	$form_name	The name of the form; has to match the name used
 *								in add_form_key, otherwise no restrictions apply
 * @param	int		$timespan	The maximum acceptable age for a submitted form
 *								in seconds. Defaults to the config setting.
 * @return	bool	True, if the form key was valid, false otherwise
 */
function check_form_key($form_name, $timespan = false)
{
	global $config, $request, $user;
	if ($timespan === false)
	{
		// we enforce a minimum value of half a minute here.
		$timespan = ($config['form_token_lifetime'] == -1) ? -1 : max(30, $config['form_token_lifetime']);
	}
	if ($request->is_set_post('creation_time') && $request->is_set_post('form_token'))
	{
		$creation_time	= abs($request->variable('creation_time', 0));
		$token = $request->variable('form_token', '');
		$diff = time() - $creation_time;
		// If creation_time and the time() now is zero we can assume it was not a human doing this (the check for if ($diff)...
		if (defined('DEBUG_TEST') || $diff && ($diff <= $timespan || $timespan === -1))
		{
			$token_sid = ($user->data['user_id'] == ANONYMOUS && !empty($config['form_token_sid_guests'])) ? $user->session_id : '';
			$key = sha1($creation_time . $user->data['user_form_salt'] . $form_name . $token_sid);
			if ($key === $token)
			{
				return true;
			}
		}
	}
	return false;
}
// Message/Login boxes
/**
* Build Confirm box
* @param boolean $check True for checking if confirmed (without any additional parameters) and false for displaying the confirm box
* @param string|array $title Title/Message used for confirm box.
*		message text is _CONFIRM appended to title.
*		If title cannot be found in user->lang a default one is displayed
*		If title_CONFIRM cannot be found in user->lang the text given is used.
*       If title is an array, the first array value is used as explained per above,
*       all other array values are sent as parameters to the language function.
* @param string $hidden Hidden variables
* @param string $html_body Template used for confirm box
* @param string $u_action Custom form action
*
* @return bool True if confirmation was successful, false if not
*/
function confirm_box($check, $title = '', $hidden = '', $html_body = 'confirm_body.html', $u_action = '')
{
	global $user, $template, $db, $request;
	global $config, $language, $phpbb_path_helper, $phpbb_dispatcher;
	if (isset($_POST['cancel']))
	{
		return false;
	}
	$confirm = ($language->lang('YES') === $request->variable('confirm', '', true, \phpbb\request\request_interface::POST));
	if ($check && $confirm)
	{
		$user_id = $request->variable('confirm_uid', 0);
		$session_id = $request->variable('sess', '');
		$confirm_key = $request->variable('confirm_key', '');
		if ($user_id != $user->data['user_id'] || $session_id != $user->session_id || !$confirm_key || !$user->data['user_last_confirm_key'] || $confirm_key != $user->data['user_last_confirm_key'])
		{
			return false;
		}
		// Reset user_last_confirm_key
		$sql = 'UPDATE ' . USERS_TABLE . " SET user_last_confirm_key = ''
			WHERE user_id = " . $user->data['user_id'];
		$db->sql_query($sql);
		return true;
	}
	else if ($check)
	{
		return false;
	}
	$s_hidden_fields = build_hidden_fields(array(
		'confirm_uid'	=> $user->data['user_id'],
		'sess'			=> $user->session_id,
		'sid'			=> $user->session_id,
	));
	// generate activation key
	$confirm_key = gen_rand_string(10);
	// generate language strings
	if (is_array($title))
	{
		$key = array_shift($title);
		$count = array_shift($title);
		$confirm_title =  $language->is_set($key) ? $language->lang($key, $count, $title) : $language->lang('CONFIRM');
		$confirm_text = $language->is_set($key . '_CONFIRM') ? $language->lang($key . '_CONFIRM', $count, $title) : $key;
	}
	else
	{
		$confirm_title = $language->is_set($title) ? $language->lang($title) : $language->lang('CONFIRM');
		$confirm_text = $language->is_set($title . '_CONFIRM') ? $language->lang($title . '_CONFIRM') : $title;
	}
	if (defined('IN_ADMIN') && isset($user->data['session_admin']) && $user->data['session_admin'])
	{
		adm_page_header($confirm_title);
	}
	else
	{
		page_header($confirm_title);
	}
	$template->set_filenames(array(
		'body' => $html_body)
	);
	// If activation key already exist, we better do not re-use the key (something very strange is going on...)
	if ($request->variable('confirm_key', ''))
	{
		// This should not occur, therefore we cancel the operation to safe the user
		return false;
	}
	// re-add sid / transform & to & for user->page (user->page is always using &)
	$use_page = ($u_action) ? $u_action : str_replace('&', '&', $user->page['page']);
	$u_action = reapply_sid($phpbb_path_helper->get_valid_page($use_page, $config['enable_mod_rewrite']));
	$u_action .= ((strpos($u_action, '?') === false) ? '?' : '&') . 'confirm_key=' . $confirm_key;
	$template->assign_vars(array(
		'MESSAGE_TITLE'		=> $confirm_title,
		'MESSAGE_TEXT'		=> $confirm_text,
		'YES_VALUE'			=> $language->lang('YES'),
		'S_CONFIRM_ACTION'	=> $u_action,
		'S_HIDDEN_FIELDS'	=> $hidden . $s_hidden_fields,
		'S_AJAX_REQUEST'	=> $request->is_ajax(),
	));
	$sql = 'UPDATE ' . USERS_TABLE . " SET user_last_confirm_key = '" . $db->sql_escape($confirm_key) . "'
		WHERE user_id = " . $user->data['user_id'];
	$db->sql_query($sql);
	if ($request->is_ajax())
	{
		$u_action .= '&confirm_uid=' . $user->data['user_id'] . '&sess=' . $user->session_id . '&sid=' . $user->session_id;
		$data = array(
			'MESSAGE_BODY'		=> $template->assign_display('body'),
			'MESSAGE_TITLE'		=> $confirm_title,
			'MESSAGE_TEXT'		=> $confirm_text,
			'YES_VALUE'			=> $language->lang('YES'),
			'S_CONFIRM_ACTION'	=> str_replace('&', '&', $u_action), //inefficient, rewrite whole function
			'S_HIDDEN_FIELDS'	=> $hidden . $s_hidden_fields
		);
		/**
		 * This event allows an extension to modify the ajax output of confirm box.
		 *
		 * @event core.confirm_box_ajax_before
		 * @var string	u_action		Action of the form
		 * @var array	data			Data to be sent
		 * @var string	hidden			Hidden fields generated by caller
		 * @var string	s_hidden_fields	Hidden fields generated by this function
		 * @since 3.2.8-RC1
		 */
		$vars = array(
			'u_action',
			'data',
			'hidden',
			's_hidden_fields',
		);
		extract($phpbb_dispatcher->trigger_event('core.confirm_box_ajax_before', compact($vars)));
		$json_response = new \phpbb\json_response;
		$json_response->send($data);
	}
	if (defined('IN_ADMIN') && isset($user->data['session_admin']) && $user->data['session_admin'])
	{
		adm_page_footer();
	}
	else
	{
		page_footer();
	}
	exit; // unreachable, page_footer() above will call exit()
}
/**
* Generate login box or verify password
*/
function login_box($redirect = '', $l_explain = '', $l_success = '', $admin = false, $s_display = true)
{
	global $user, $template, $auth, $phpEx, $phpbb_root_path, $config;
	global $request, $phpbb_container, $phpbb_dispatcher, $phpbb_log;
	$err = '';
	$form_name = 'login';
	// Make sure user->setup() has been called
	if (!$user->is_setup())
	{
		$user->setup();
	}
	/**
	 * This event allows an extension to modify the login process
	 *
	 * @event core.login_box_before
	 * @var string	redirect	Redirect string
	 * @var string	l_explain	Explain language string
	 * @var string	l_success	Success language string
	 * @var	bool	admin		Is admin?
	 * @var bool	s_display	Display full login form?
	 * @var string	err			Error string
	 * @since 3.1.9-RC1
	 */
	$vars = array('redirect', 'l_explain', 'l_success', 'admin', 's_display', 'err');
	extract($phpbb_dispatcher->trigger_event('core.login_box_before', compact($vars)));
	// Print out error if user tries to authenticate as an administrator without having the privileges...
	if ($admin && !$auth->acl_get('a_'))
	{
		// Not authd
		// anonymous/inactive users are never able to go to the ACP even if they have the relevant permissions
		if ($user->data['is_registered'])
		{
			$phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_ADMIN_AUTH_FAIL');
		}
		send_status_line(403, 'Forbidden');
		trigger_error('NO_AUTH_ADMIN');
	}
	if (empty($err) && ($request->is_set_post('login') || ($request->is_set('login') && $request->variable('login', '') == 'external')))
	{
		// Get credential
		if ($admin)
		{
			$credential = $request->variable('credential', '');
			if (strspn($credential, 'abcdef0123456789') !== strlen($credential) || strlen($credential) != 32)
			{
				if ($user->data['is_registered'])
				{
					$phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_ADMIN_AUTH_FAIL');
				}
				send_status_line(403, 'Forbidden');
				trigger_error('NO_AUTH_ADMIN');
			}
			$password	= $request->untrimmed_variable('password_' . $credential, '', true);
		}
		else
		{
			$password	= $request->untrimmed_variable('password', '', true);
		}
		$username	= $request->variable('username', '', true);
		$autologin	= $request->is_set_post('autologin');
		$viewonline = (int) !$request->is_set_post('viewonline');
		$admin 		= ($admin) ? 1 : 0;
		$viewonline = ($admin) ? $user->data['session_viewonline'] : $viewonline;
		// Check if the supplied username is equal to the one stored within the database if re-authenticating
		if ($admin && utf8_clean_string($username) != utf8_clean_string($user->data['username']))
		{
			// We log the attempt to use a different username...
			$phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_ADMIN_AUTH_FAIL');
			send_status_line(403, 'Forbidden');
			trigger_error('NO_AUTH_ADMIN_USER_DIFFER');
		}
		// Check form key
		if ($password && !defined('IN_CHECK_BAN') && !check_form_key($form_name))
		{
			$result = array(
				'status' => false,
				'error_msg' => 'FORM_INVALID',
			);
		}
		else
		{
			// If authentication is successful we redirect user to previous page
			$result = $auth->login($username, $password, $autologin, $viewonline, $admin);
		}
		// If admin authentication and login, we will log if it was a success or not...
		// We also break the operation on the first non-success login - it could be argued that the user already knows
		if ($admin)
		{
			if ($result['status'] == LOGIN_SUCCESS)
			{
				$phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_ADMIN_AUTH_SUCCESS');
			}
			else
			{
				// Only log the failed attempt if a real user tried to.
				// anonymous/inactive users are never able to go to the ACP even if they have the relevant permissions
				if ($user->data['is_registered'])
				{
					$phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_ADMIN_AUTH_FAIL');
				}
			}
		}
		// The result parameter is always an array, holding the relevant information...
		if ($result['status'] == LOGIN_SUCCESS)
		{
			$redirect = $request->variable('redirect', "{$phpbb_root_path}index.$phpEx");
			/**
			* This event allows an extension to modify the redirection when a user successfully logs in
			*
			* @event core.login_box_redirect
			* @var  string	redirect	Redirect string
			* @var	bool	admin		Is admin?
			* @var	array	result		Result from auth provider
			* @since 3.1.0-RC5
			* @changed 3.1.9-RC1 Removed undefined return variable
			* @changed 3.2.4-RC1 Added result
			*/
			$vars = array('redirect', 'admin', 'result');
			extract($phpbb_dispatcher->trigger_event('core.login_box_redirect', compact($vars)));
			// append/replace SID (may change during the session for AOL users)
			$redirect = reapply_sid($redirect);
			// Special case... the user is effectively banned, but we allow founders to login
			if (defined('IN_CHECK_BAN') && $result['user_row']['user_type'] != USER_FOUNDER)
			{
				return;
			}
			redirect($redirect);
		}
		// Something failed, determine what...
		if ($result['status'] == LOGIN_BREAK)
		{
			trigger_error($result['error_msg']);
		}
		// Special cases... determine
		switch ($result['status'])
		{
			case LOGIN_ERROR_PASSWORD_CONVERT:
				$err = sprintf(
					$user->lang[$result['error_msg']],
					($config['email_enable']) ? '' : '',
					($config['email_enable']) ? '' : '',
					'',
					''
				);
			break;
			case LOGIN_ERROR_ATTEMPTS:
				$captcha = $phpbb_container->get('captcha.factory')->get_instance($config['captcha_plugin']);
				$captcha->init(CONFIRM_LOGIN);
				// $captcha->reset();
				$template->assign_vars(array(
					'CAPTCHA_TEMPLATE'			=> $captcha->get_template(),
				));
			// no break;
			// Username, password, etc...
			default:
				$err = $user->lang[$result['error_msg']];
				// Assign admin contact to some error messages
				if ($result['error_msg'] == 'LOGIN_ERROR_USERNAME' || $result['error_msg'] == 'LOGIN_ERROR_PASSWORD')
				{
					$err = sprintf($user->lang[$result['error_msg']], '', '');
				}
			break;
		}
		/**
		 * This event allows an extension to process when a user fails a login attempt
		 *
		 * @event core.login_box_failed
		 * @var array   result      Login result data
		 * @var string  username    User name used to login
		 * @var string  password    Password used to login
		 * @var string  err         Error message
		 * @since 3.1.3-RC1
		 */
		$vars = array('result', 'username', 'password', 'err');
		extract($phpbb_dispatcher->trigger_event('core.login_box_failed', compact($vars)));
	}
	// Assign credential for username/password pair
	$credential = ($admin) ? md5(unique_id()) : false;
	$s_hidden_fields = array(
		'sid'		=> $user->session_id,
	);
	if ($redirect)
	{
		$s_hidden_fields['redirect'] = $redirect;
	}
	if ($admin)
	{
		$s_hidden_fields['credential'] = $credential;
	}
	/* @var $provider_collection \phpbb\auth\provider_collection */
	$provider_collection = $phpbb_container->get('auth.provider_collection');
	$auth_provider = $provider_collection->get_provider();
	$auth_provider_data = $auth_provider->get_login_data();
	if ($auth_provider_data)
	{
		if (isset($auth_provider_data['VARS']))
		{
			$template->assign_vars($auth_provider_data['VARS']);
		}
		if (isset($auth_provider_data['BLOCK_VAR_NAME']))
		{
			foreach ($auth_provider_data['BLOCK_VARS'] as $block_vars)
			{
				$template->assign_block_vars($auth_provider_data['BLOCK_VAR_NAME'], $block_vars);
			}
		}
		$template->assign_vars(array(
			'PROVIDER_TEMPLATE_FILE' => $auth_provider_data['TEMPLATE_FILE'],
		));
	}
	$s_hidden_fields = build_hidden_fields($s_hidden_fields);
	$login_box_template_data = array(
		'LOGIN_ERROR'		=> $err,
		'LOGIN_EXPLAIN'		=> $l_explain,
		'U_SEND_PASSWORD' 		=> ($config['email_enable']) ? append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=sendpassword') : '',
		'U_RESEND_ACTIVATION'	=> ($config['require_activation'] == USER_ACTIVATION_SELF && $config['email_enable']) ? append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=resend_act') : '',
		'U_TERMS_USE'			=> append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=terms'),
		'U_PRIVACY'				=> append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=privacy'),
		'UA_PRIVACY'			=> addslashes(append_sid("{$phpbb_root_path}ucp.$phpEx", 'mode=privacy')),
		'S_DISPLAY_FULL_LOGIN'	=> ($s_display) ? true : false,
		'S_HIDDEN_FIELDS' 		=> $s_hidden_fields,
		'S_ADMIN_AUTH'			=> $admin,
		'USERNAME'				=> ($admin) ? $user->data['username'] : '',
		'USERNAME_CREDENTIAL'	=> 'username',
		'PASSWORD_CREDENTIAL'	=> ($admin) ? 'password_' . $credential : 'password',
	);
	/**
	 * Event to add/modify login box template data
	 *
	 * @event core.login_box_modify_template_data
	 * @var	int		admin							Flag whether user is admin
	 * @var	string	username						User name
	 * @var	int		autologin						Flag whether autologin is enabled
	 * @var string	redirect						Redirect URL
	 * @var	array	login_box_template_data			Array with the login box template data
	 * @since 3.2.3-RC2
	 */
	$vars = array(
		'admin',
		'username',
		'autologin',
		'redirect',
		'login_box_template_data',
	);
	extract($phpbb_dispatcher->trigger_event('core.login_box_modify_template_data', compact($vars)));
	$template->assign_vars($login_box_template_data);
	page_header($user->lang['LOGIN']);
	$template->set_filenames(array(
		'body' => 'login_body.html')
	);
	make_jumpbox(append_sid("{$phpbb_root_path}viewforum.$phpEx"));
	page_footer();
}
/**
* Generate forum login box
*/
function login_forum_box($forum_data)
{
	global $db, $phpbb_container, $request, $template, $user, $phpbb_dispatcher, $phpbb_root_path, $phpEx;
	$password = $request->variable('password', '', true);
	$sql = 'SELECT forum_id
		FROM ' . FORUMS_ACCESS_TABLE . '
		WHERE forum_id = ' . $forum_data['forum_id'] . '
			AND user_id = ' . $user->data['user_id'] . "
			AND session_id = '" . $db->sql_escape($user->session_id) . "'";
	$result = $db->sql_query($sql);
	$row = $db->sql_fetchrow($result);
	$db->sql_freeresult($result);
	if ($row)
	{
		return true;
	}
	if ($password)
	{
		// Remove expired authorised sessions
		$sql = 'SELECT f.session_id
			FROM ' . FORUMS_ACCESS_TABLE . ' f
			LEFT JOIN ' . SESSIONS_TABLE . ' s ON (f.session_id = s.session_id)
			WHERE s.session_id IS NULL';
		$result = $db->sql_query($sql);
		if ($row = $db->sql_fetchrow($result))
		{
			$sql_in = array();
			do
			{
				$sql_in[] = (string) $row['session_id'];
			}
			while ($row = $db->sql_fetchrow($result));
			// Remove expired sessions
			$sql = 'DELETE FROM ' . FORUMS_ACCESS_TABLE . '
				WHERE ' . $db->sql_in_set('session_id', $sql_in);
			$db->sql_query($sql);
		}
		$db->sql_freeresult($result);
		/* @var $passwords_manager \phpbb\passwords\manager */
		$passwords_manager = $phpbb_container->get('passwords.manager');
		if ($passwords_manager->check($password, $forum_data['forum_password']))
		{
			$sql_ary = array(
				'forum_id'		=> (int) $forum_data['forum_id'],
				'user_id'		=> (int) $user->data['user_id'],
				'session_id'	=> (string) $user->session_id,
			);
			$db->sql_query('INSERT INTO ' . FORUMS_ACCESS_TABLE . ' ' . $db->sql_build_array('INSERT', $sql_ary));
			return true;
		}
		$template->assign_var('LOGIN_ERROR', $user->lang['WRONG_PASSWORD']);
	}
	/**
	* Performing additional actions, load additional data on forum login
	*
	* @event core.login_forum_box
	* @var	array	forum_data		Array with forum data
	* @var	string	password		Password entered
	* @since 3.1.0-RC3
	*/
	$vars = array('forum_data', 'password');
	extract($phpbb_dispatcher->trigger_event('core.login_forum_box', compact($vars)));
	page_header($user->lang['LOGIN']);
	$template->assign_vars(array(
		'FORUM_NAME'			=> isset($forum_data['forum_name']) ? $forum_data['forum_name'] : '',
		'S_LOGIN_ACTION'		=> build_url(array('f')),
		'S_HIDDEN_FIELDS'		=> build_hidden_fields(array('f' => $forum_data['forum_id'])))
	);
	$template->set_filenames(array(
		'body' => 'login_forum.html')
	);
	make_jumpbox(append_sid("{$phpbb_root_path}viewforum.$phpEx"), $forum_data['forum_id']);
	page_footer();
}
// Little helpers
/**
* Little helper for the build_hidden_fields function
*/
function _build_hidden_fields($key, $value, $specialchar, $stripslashes)
{
	$hidden_fields = '';
	if (!is_array($value))
	{
		$value = ($stripslashes) ? stripslashes($value) : $value;
		$value = ($specialchar) ? htmlspecialchars($value, ENT_COMPAT, 'UTF-8') : $value;
		$hidden_fields .= '' . "\n";
	}
	else
	{
		foreach ($value as $_key => $_value)
		{
			$_key = ($stripslashes) ? stripslashes($_key) : $_key;
			$_key = ($specialchar) ? htmlspecialchars($_key, ENT_COMPAT, 'UTF-8') : $_key;
			$hidden_fields .= _build_hidden_fields($key . '[' . $_key . ']', $_value, $specialchar, $stripslashes);
		}
	}
	return $hidden_fields;
}
/**
* Build simple hidden fields from array
*
* @param array $field_ary an array of values to build the hidden field from
* @param bool $specialchar if true, keys and values get specialchared
* @param bool $stripslashes if true, keys and values get stripslashed
*
* @return string the hidden fields
*/
function build_hidden_fields($field_ary, $specialchar = false, $stripslashes = false)
{
	$s_hidden_fields = '';
	foreach ($field_ary as $name => $vars)
	{
		$name = ($stripslashes) ? stripslashes($name) : $name;
		$name = ($specialchar) ? htmlspecialchars($name, ENT_COMPAT, 'UTF-8') : $name;
		$s_hidden_fields .= _build_hidden_fields($name, $vars, $specialchar, $stripslashes);
	}
	return $s_hidden_fields;
}
/**
* Parse cfg file
*/
function parse_cfg_file($filename, $lines = false)
{
	$parsed_items = array();
	if ($lines === false)
	{
		$lines = file($filename);
	}
	foreach ($lines as $line)
	{
		$line = trim($line);
		if (!$line || $line[0] == '#' || ($delim_pos = strpos($line, '=')) === false)
		{
			continue;
		}
		// Determine first occurrence, since in values the equal sign is allowed
		$key = htmlspecialchars(strtolower(trim(substr($line, 0, $delim_pos))));
		$value = trim(substr($line, $delim_pos + 1));
		if (in_array($value, array('off', 'false', '0')))
		{
			$value = false;
		}
		else if (in_array($value, array('on', 'true', '1')))
		{
			$value = true;
		}
		else if (!trim($value))
		{
			$value = '';
		}
		else if (($value[0] == "'" && $value[strlen($value) - 1] == "'") || ($value[0] == '"' && $value[strlen($value) - 1] == '"'))
		{
			$value = htmlspecialchars(substr($value, 1, strlen($value)-2));
		}
		else
		{
			$value = htmlspecialchars($value);
		}
		$parsed_items[$key] = $value;
	}
	if (isset($parsed_items['parent']) && isset($parsed_items['name']) && $parsed_items['parent'] == $parsed_items['name'])
	{
		unset($parsed_items['parent']);
	}
	return $parsed_items;
}
/**
* Return a nicely formatted backtrace.
*
* Turns the array returned by debug_backtrace() into HTML markup.
* Also filters out absolute paths to phpBB root.
*
* @return string	HTML markup
*/
function get_backtrace()
{
	$output = '
' . sprintf($user->lang['NOTIFY_ADMIN_EMAIL'], $config['board_contact']) . '
'; } } else { $msg_title = 'General Error'; $l_return_index = 'Return to index page'; $l_notify = ''; if (!empty($config['board_contact'])) { $l_notify = 'Please notify the board administrator or webmaster: ' . $config['board_contact'] . '
'; } } $log_text = $msg_text; $backtrace = get_backtrace(); if ($backtrace) { $log_text .= '