Spuds 12 years ago
commit
a18e888838
100 changed files with 15254 additions and 0 deletions
  1. 5 0
      Packages/.htaccess
  2. 5 0
      Packages/backups/.htaccess
  3. 9 0
      Packages/backups/index.php
  4. 16 0
      Packages/index.php
  5. 0 0
      Packages/installed.list
  6. 1969 0
      SSI.php
  7. BIN
      Smileys/aaron/afro.gif
  8. BIN
      Smileys/aaron/angel.gif
  9. BIN
      Smileys/aaron/angry.gif
  10. BIN
      Smileys/aaron/azn.gif
  11. BIN
      Smileys/aaron/blank.gif
  12. BIN
      Smileys/aaron/cheesy.gif
  13. BIN
      Smileys/aaron/cool.gif
  14. BIN
      Smileys/aaron/cry.gif
  15. BIN
      Smileys/aaron/embarrassed.gif
  16. BIN
      Smileys/aaron/evil.gif
  17. BIN
      Smileys/aaron/grin.gif
  18. BIN
      Smileys/aaron/huh.gif
  19. 9 0
      Smileys/aaron/index.php
  20. BIN
      Smileys/aaron/kiss.gif
  21. BIN
      Smileys/aaron/laugh.gif
  22. BIN
      Smileys/aaron/lipsrsealed.gif
  23. BIN
      Smileys/aaron/police.gif
  24. BIN
      Smileys/aaron/rolleyes.gif
  25. BIN
      Smileys/aaron/sad.gif
  26. BIN
      Smileys/aaron/shocked.gif
  27. BIN
      Smileys/aaron/smiley.gif
  28. BIN
      Smileys/aaron/tongue.gif
  29. BIN
      Smileys/aaron/undecided.gif
  30. BIN
      Smileys/aaron/wink.gif
  31. BIN
      Smileys/akyhne/afro.gif
  32. BIN
      Smileys/akyhne/angel.gif
  33. BIN
      Smileys/akyhne/angry.gif
  34. BIN
      Smileys/akyhne/azn.gif
  35. BIN
      Smileys/akyhne/blank.gif
  36. BIN
      Smileys/akyhne/cheesy.gif
  37. BIN
      Smileys/akyhne/cool.gif
  38. BIN
      Smileys/akyhne/cry.gif
  39. BIN
      Smileys/akyhne/embarrassed.gif
  40. BIN
      Smileys/akyhne/evil.gif
  41. BIN
      Smileys/akyhne/grin.gif
  42. BIN
      Smileys/akyhne/huh.gif
  43. 9 0
      Smileys/akyhne/index.php
  44. BIN
      Smileys/akyhne/kiss.gif
  45. BIN
      Smileys/akyhne/laugh.gif
  46. BIN
      Smileys/akyhne/lipsrsealed.gif
  47. BIN
      Smileys/akyhne/police.gif
  48. BIN
      Smileys/akyhne/rolleyes.gif
  49. BIN
      Smileys/akyhne/sad.gif
  50. BIN
      Smileys/akyhne/shocked.gif
  51. BIN
      Smileys/akyhne/smiley.gif
  52. BIN
      Smileys/akyhne/tongue.gif
  53. BIN
      Smileys/akyhne/undecided.gif
  54. BIN
      Smileys/akyhne/wink.gif
  55. BIN
      Smileys/default/afro.gif
  56. BIN
      Smileys/default/angel.gif
  57. BIN
      Smileys/default/angry.gif
  58. BIN
      Smileys/default/azn.gif
  59. BIN
      Smileys/default/blank.gif
  60. BIN
      Smileys/default/cheesy.gif
  61. BIN
      Smileys/default/cool.gif
  62. BIN
      Smileys/default/cry.gif
  63. BIN
      Smileys/default/embarrassed.gif
  64. BIN
      Smileys/default/evil.gif
  65. BIN
      Smileys/default/grin.gif
  66. BIN
      Smileys/default/huh.gif
  67. 9 0
      Smileys/default/index.php
  68. BIN
      Smileys/default/kiss.gif
  69. BIN
      Smileys/default/laugh.gif
  70. BIN
      Smileys/default/lipsrsealed.gif
  71. BIN
      Smileys/default/police.gif
  72. BIN
      Smileys/default/rolleyes.gif
  73. BIN
      Smileys/default/sad.gif
  74. BIN
      Smileys/default/shocked.gif
  75. BIN
      Smileys/default/smiley.gif
  76. BIN
      Smileys/default/tongue.gif
  77. BIN
      Smileys/default/undecided.gif
  78. BIN
      Smileys/default/wink.gif
  79. 16 0
      Smileys/index.php
  80. 999 0
      Sources/Admin.php
  81. 143 0
      Sources/BoardIndex.php
  82. 487 0
      Sources/Calendar.php
  83. 715 0
      Sources/Class-Graphics.php
  84. 1019 0
      Sources/Class-Package.php
  85. 454 0
      Sources/DbExtra-mysql.php
  86. 328 0
      Sources/DbExtra-postgresql.php
  87. 346 0
      Sources/DbExtra-sqlite.php
  88. 582 0
      Sources/DbPackages-mysql.php
  89. 746 0
      Sources/DbPackages-postgresql.php
  90. 736 0
      Sources/DbPackages-sqlite.php
  91. 77 0
      Sources/DbSearch-mysql.php
  92. 122 0
      Sources/DbSearch-postgresql.php
  93. 106 0
      Sources/DbSearch-sqlite.php
  94. 1727 0
      Sources/Display.php
  95. 182 0
      Sources/DumpDatabase.php
  96. 416 0
      Sources/Errors.php
  97. 967 0
      Sources/Groups.php
  98. 111 0
      Sources/Help.php
  99. 204 0
      Sources/Karma.php
  100. 2740 0
      Sources/Load.php

+ 5 - 0
Packages/.htaccess

@@ -0,0 +1,5 @@
+<Files *>
+	Order Deny,Allow
+	Deny from all
+	Allow from localhost
+</Files>

+ 5 - 0
Packages/backups/.htaccess

@@ -0,0 +1,5 @@
+<Files *>
+	Order Deny,Allow
+	Deny from all
+	Allow from localhost
+</Files>

+ 9 - 0
Packages/backups/index.php

@@ -0,0 +1,9 @@
+<?php
+
+// Try to handle it with the upper level index.php.  (it should know what to do.)
+if (file_exists(dirname(dirname(__FILE__)) . '/index.php'))
+	include (dirname(dirname(__FILE__)) . '/index.php');
+else
+	exit;
+
+?>

+ 16 - 0
Packages/index.php

@@ -0,0 +1,16 @@
+<?php
+
+// This file is here solely to protect your Packages directory.
+
+// Look for Settings.php....
+if (file_exists(dirname(dirname(__FILE__)) . '/Settings.php'))
+{
+	// Found it!
+	require(dirname(dirname(__FILE__)) . '/Settings.php');
+	header('Location: ' . $boardurl);
+}
+// Can't find it... just forget it.
+else
+	exit;
+
+?>

+ 0 - 0
Packages/installed.list


+ 1969 - 0
SSI.php

@@ -0,0 +1,1969 @@
+<?php
+
+/**
+ * Simple Machines Forum (SMF)
+ *
+ * @package SMF
+ * @author Simple Machines http://www.simplemachines.org
+ * @copyright 2011 Simple Machines
+ * @license http://www.simplemachines.org/about/smf/license.php BSD
+ *
+ * @version 2.0
+ */
+
+// Don't do anything if SMF is already loaded.
+if (defined('SMF'))
+	return true;
+
+define('SMF', 'SSI');
+
+// We're going to want a few globals... these are all set later.
+global $time_start, $maintenance, $msubject, $mmessage, $mbname, $language;
+global $boardurl, $boarddir, $sourcedir, $webmaster_email, $cookiename;
+global $db_server, $db_name, $db_user, $db_prefix, $db_persist, $db_error_send, $db_last_error;
+global $db_connection, $modSettings, $context, $sc, $user_info, $topic, $board, $txt;
+global $smcFunc, $ssi_db_user, $scripturl, $ssi_db_passwd, $db_passwd, $cachedir;
+
+// Remember the current configuration so it can be set back.
+$ssi_magic_quotes_runtime = function_exists('get_magic_quotes_gpc') && get_magic_quotes_runtime();
+if (function_exists('set_magic_quotes_runtime'))
+	@set_magic_quotes_runtime(0);
+$time_start = microtime();
+
+// Just being safe...
+foreach (array('db_character_set', 'cachedir') as $variable)
+	if (isset($GLOBALS[$variable]))
+		unset($GLOBALS[$variable]);
+
+// Get the forum's settings for database and file paths.
+require_once(dirname(__FILE__) . '/Settings.php');
+
+// Make absolutely sure the cache directory is defined.
+if ((empty($cachedir) || !file_exists($cachedir)) && file_exists($boarddir . '/cache'))
+	$cachedir = $boarddir . '/cache';
+
+$ssi_error_reporting = error_reporting(defined('E_STRICT') ? E_ALL | E_STRICT : E_ALL);
+/* Set this to one of three values depending on what you want to happen in the case of a fatal error.
+	false:	Default, will just load the error sub template and die - not putting any theme layers around it.
+	true:	Will load the error sub template AND put the SMF layers around it (Not useful if on total custom pages).
+	string:	Name of a callback function to call in the event of an error to allow you to define your own methods. Will die after function returns.
+*/
+$ssi_on_error_method = false;
+
+// Don't do john didley if the forum's been shut down competely.
+if ($maintenance == 2 && (!isset($ssi_maintenance_off) || $ssi_maintenance_off !== true))
+	die($mmessage);
+
+// Fix for using the current directory as a path.
+if (substr($sourcedir, 0, 1) == '.' && substr($sourcedir, 1, 1) != '.')
+	$sourcedir = dirname(__FILE__) . substr($sourcedir, 1);
+
+// Load the important includes.
+require_once($sourcedir . '/QueryString.php');
+require_once($sourcedir . '/Subs.php');
+require_once($sourcedir . '/Errors.php');
+require_once($sourcedir . '/Load.php');
+require_once($sourcedir . '/Security.php');
+
+// Using an pre-PHP 5.1 version?
+if (@version_compare(PHP_VERSION, '5.1') == -1)
+	require_once($sourcedir . '/Subs-Compat.php');
+
+// Create a variable to store some SMF specific functions in.
+$smcFunc = array();
+
+// Initate the database connection and define some database functions to use.
+loadDatabase();
+
+// Load installed 'Mods' settings.
+reloadSettings();
+// Clean the request variables.
+cleanRequest();
+
+// Seed the random generator?
+if (empty($modSettings['rand_seed']) || mt_rand(1, 250) == 69)
+	smf_seed_generator();
+
+// Check on any hacking attempts.
+if (isset($_REQUEST['GLOBALS']) || isset($_COOKIE['GLOBALS']))
+	die('Hacking attempt...');
+elseif (isset($_REQUEST['ssi_theme']) && (int) $_REQUEST['ssi_theme'] == (int) $ssi_theme)
+	die('Hacking attempt...');
+elseif (isset($_COOKIE['ssi_theme']) && (int) $_COOKIE['ssi_theme'] == (int) $ssi_theme)
+	die('Hacking attempt...');
+elseif (isset($_REQUEST['ssi_layers'], $ssi_layers) && (@get_magic_quotes_gpc() ? stripslashes($_REQUEST['ssi_layers']) : $_REQUEST['ssi_layers']) == $ssi_layers)
+	die('Hacking attempt...');
+if (isset($_REQUEST['context']))
+	die('Hacking attempt...');
+
+// Make sure wireless is always off.
+define('WIRELESS', false);
+
+// Gzip output? (because it must be boolean and true, this can't be hacked.)
+if (isset($ssi_gzip) && $ssi_gzip === true && @ini_get('zlib.output_compression') != '1' && @ini_get('output_handler') != 'ob_gzhandler' && @version_compare(PHP_VERSION, '4.2.0') != -1)
+	ob_start('ob_gzhandler');
+else
+	$modSettings['enableCompressedOutput'] = '0';
+
+// Primarily, this is to fix the URLs...
+ob_start('ob_sessrewrite');
+
+// Start the session... known to scramble SSI includes in cases...
+if (!headers_sent())
+	loadSession();
+else
+{
+	if (isset($_COOKIE[session_name()]) || isset($_REQUEST[session_name()]))
+	{
+		// Make a stab at it, but ignore the E_WARNINGs generated because we can't send headers.
+		$temp = error_reporting(error_reporting() & !E_WARNING);
+		loadSession();
+		error_reporting($temp);
+	}
+
+	if (!isset($_SESSION['session_value']))
+	{
+		$_SESSION['session_var'] = substr(md5(mt_rand() . session_id() . mt_rand()), 0, rand(7, 12));
+		$_SESSION['session_value'] = md5(session_id() . mt_rand());
+	}
+	$sc = $_SESSION['session_value'];
+}
+
+// Get rid of $board and $topic... do stuff loadBoard would do.
+unset($board, $topic);
+$user_info['is_mod'] = false;
+$context['user']['is_mod'] = &$user_info['is_mod'];
+$context['linktree'] = array();
+
+// Load the user and their cookie, as well as their settings.
+loadUserSettings();
+
+// Load the current user's permissions....
+loadPermissions();
+
+// Load the current or SSI theme. (just use $ssi_theme = id_theme;)
+loadTheme(isset($ssi_theme) ? (int) $ssi_theme : 0);
+
+// Take care of any banning that needs to be done.
+if (isset($_REQUEST['ssi_ban']) || (isset($ssi_ban) && $ssi_ban === true))
+	is_not_banned();
+
+// Do we allow guests in here?
+if (empty($ssi_guest_access) && empty($modSettings['allow_guestAccess']) && $user_info['is_guest'] && basename($_SERVER['PHP_SELF']) != 'SSI.php')
+{
+	require_once($sourcedir . '/Subs-Auth.php');
+	KickGuest();
+	obExit(null, true);
+}
+
+// Load the stuff like the menu bar, etc.
+if (isset($ssi_layers))
+{
+	$context['template_layers'] = $ssi_layers;
+	template_header();
+}
+else
+	setupThemeContext();
+
+// Make sure they didn't muss around with the settings... but only if it's not cli.
+if (isset($_SERVER['REMOTE_ADDR']) && !isset($_SERVER['is_cli']) && session_id() == '')
+	trigger_error($txt['ssi_session_broken'], E_USER_NOTICE);
+
+// Without visiting the forum this session variable might not be set on submit.
+if (!isset($_SESSION['USER_AGENT']) && (!isset($_GET['ssi_function']) || $_GET['ssi_function'] !== 'pollVote'))
+	$_SESSION['USER_AGENT'] = $_SERVER['HTTP_USER_AGENT'];
+
+// Call a function passed by GET.
+if (isset($_GET['ssi_function']) && function_exists('ssi_' . $_GET['ssi_function']) && (!empty($modSettings['allow_guestAccess']) || !$user_info['is_guest']))
+{
+	call_user_func('ssi_' . $_GET['ssi_function']);
+	exit;
+}
+if (isset($_GET['ssi_function']))
+	exit;
+// You shouldn't just access SSI.php directly by URL!!
+elseif (basename($_SERVER['PHP_SELF']) == 'SSI.php')
+	die(sprintf($txt['ssi_not_direct'], $user_info['is_admin'] ? '\'' . addslashes(__FILE__) . '\'' : '\'SSI.php\''));
+
+error_reporting($ssi_error_reporting);
+if (function_exists('set_magic_quotes_runtime'))
+	@set_magic_quotes_runtime($ssi_magic_quotes_runtime);
+
+return true;
+
+// This shuts down the SSI and shows the footer.
+function ssi_shutdown()
+{
+	if (!isset($_GET['ssi_function']) || $_GET['ssi_function'] != 'shutdown')
+		template_footer();
+}
+
+// Display a welcome message, like:  Hey, User, you have 0 messages, 0 are new.
+function ssi_welcome($output_method = 'echo')
+{
+	global $context, $txt, $scripturl;
+
+	if ($output_method == 'echo')
+	{
+		if ($context['user']['is_guest'])
+			echo sprintf($txt['welcome_guest'], $txt['guest_title']);
+		else
+			echo $txt['hello_member'], ' <strong>', $context['user']['name'], '</strong>', allowedTo('pm_read') ? ', ' . $txt['msg_alert_you_have'] . ' <a href="' . $scripturl . '?action=pm">' . $context['user']['messages'] . ' ' . ($context['user']['messages'] == '1' ? $txt['message_lowercase'] : $txt['msg_alert_messages']) . '</a>' . $txt['newmessages4'] . ' ' . $context['user']['unread_messages'] . ' ' . ($context['user']['unread_messages'] == '1' ? $txt['newmessages0'] : $txt['newmessages1']) : '', '.';
+	}
+	// Don't echo... then do what?!
+	else
+		return $context['user'];
+}
+
+// Display a menu bar, like is displayed at the top of the forum.
+function ssi_menubar($output_method = 'echo')
+{
+	global $context;
+
+	if ($output_method == 'echo')
+		template_menu();
+	// What else could this do?
+	else
+		return $context['menu_buttons'];
+}
+
+// Show a logout link.
+function ssi_logout($redirect_to = '', $output_method = 'echo')
+{
+	global $context, $txt, $scripturl;
+
+	if ($redirect_to != '')
+		$_SESSION['logout_url'] = $redirect_to;
+
+	// Guests can't log out.
+	if ($context['user']['is_guest'])
+		return false;
+
+	$link = '<a href="' . $scripturl . '?action=logout;' . $context['session_var'] . '=' . $context['session_id'] . '">' . $txt['logout'] . '</a>';
+
+	if ($output_method == 'echo')
+		echo $link;
+	else
+		return $link;
+}
+
+// Recent post list:   [board] Subject by Poster	Date
+function ssi_recentPosts($num_recent = 8, $exclude_boards = null, $include_boards = null, $output_method = 'echo', $limit_body = true)
+{
+	global $context, $settings, $scripturl, $txt, $db_prefix, $user_info;
+	global $modSettings, $smcFunc;
+
+	// Excluding certain boards...
+	if ($exclude_boards === null && !empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0)
+		$exclude_boards = array($modSettings['recycle_board']);
+	else
+		$exclude_boards = empty($exclude_boards) ? array() : (is_array($exclude_boards) ? $exclude_boards : array($exclude_boards));
+
+	// What about including certain boards - note we do some protection here as pre-2.0 didn't have this parameter.
+	if (is_array($include_boards) || (int) $include_boards === $include_boards)
+	{
+		$include_boards = is_array($include_boards) ? $include_boards : array($include_boards);
+	}
+	elseif ($include_boards != null)
+	{
+		$include_boards = array();
+	}
+
+	// Let's restrict the query boys (and girls)
+	$query_where = '
+		m.id_msg >= {int:min_message_id}
+		' . (empty($exclude_boards) ? '' : '
+		AND b.id_board NOT IN ({array_int:exclude_boards})') . '
+		' . ($include_boards === null ? '' : '
+		AND b.id_board IN ({array_int:include_boards})') . '
+		AND {query_wanna_see_board}' . ($modSettings['postmod_active'] ? '
+		AND m.approved = {int:is_approved}' : '');
+
+	$query_where_params = array(
+		'is_approved' => 1,
+		'include_boards' => $include_boards === null ? '' : $include_boards,
+		'exclude_boards' => empty($exclude_boards) ? '' : $exclude_boards,
+		'min_message_id' => $modSettings['maxMsgID'] - 25 * min($num_recent, 5),
+	);
+
+	// Past to this simpleton of a function...
+	return ssi_queryPosts($query_where, $query_where_params, $num_recent, 'm.id_msg DESC', $output_method, $limit_body);
+}
+
+// Fetch a post with a particular ID. By default will only show if you have permission to the see the board in question - this can be overriden.
+function ssi_fetchPosts($post_ids, $override_permissions = false, $output_method = 'echo')
+{
+	global $user_info, $modSettings;
+
+	// Allow the user to request more than one - why not?
+	$post_ids = is_array($post_ids) ? $post_ids : array($post_ids);
+
+	// Restrict the posts required...
+	$query_where = '
+		m.id_msg IN ({array_int:message_list})' . ($override_permissions ? '' : '
+			AND {query_wanna_see_board}') . ($modSettings['postmod_active'] ? '
+			AND m.approved = {int:is_approved}' : '');
+	$query_where_params = array(
+		'message_list' => $post_ids,
+		'is_approved' => 1,
+	);
+
+	// Then make the query and dump the data.
+	return ssi_queryPosts($query_where, $query_where_params, '', 'm.id_msg DESC', $output_method);
+}
+
+// This removes code duplication in other queries - don't call it direct unless you really know what you're up to.
+function ssi_queryPosts($query_where = '', $query_where_params = array(), $query_limit = '', $query_order = 'm.id_msg DESC', $output_method = 'echo', $limit_body = false)
+{
+	global $context, $settings, $scripturl, $txt, $db_prefix, $user_info;
+	global $modSettings, $smcFunc;
+
+	// Find all the posts. Newer ones will have higher IDs.
+	$request = $smcFunc['db_query']('substring', '
+		SELECT
+			m.poster_time, m.subject, m.id_topic, m.id_member, m.id_msg, m.id_board, b.name AS board_name,
+			IFNULL(mem.real_name, m.poster_name) AS poster_name, ' . ($user_info['is_guest'] ? '1 AS is_read, 0 AS new_from' : '
+			IFNULL(lt.id_msg, IFNULL(lmr.id_msg, 0)) >= m.id_msg_modified AS is_read,
+			IFNULL(lt.id_msg, IFNULL(lmr.id_msg, -1)) + 1 AS new_from') . ', ' . ($limit_body ? 'SUBSTRING(m.body, 1, 384) AS body' : 'm.body') . ', m.smileys_enabled
+		FROM {db_prefix}messages AS m
+			INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board)
+			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)' . (!$user_info['is_guest'] ? '
+			LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = m.id_topic AND lt.id_member = {int:current_member})
+			LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = m.id_board AND lmr.id_member = {int:current_member})' : '') . '
+		' . (empty($query_where) ? '' : 'WHERE ' . $query_where) . '
+		ORDER BY ' . $query_order . '
+		' . ($query_limit == '' ? '' : 'LIMIT ' . $query_limit),
+		array_merge($query_where_params, array(
+			'current_member' => $user_info['id'],
+		))
+	);
+	$posts = array();
+	while ($row = $smcFunc['db_fetch_assoc']($request))
+	{
+		$row['body'] = parse_bbc($row['body'], $row['smileys_enabled'], $row['id_msg']);
+
+		// Censor it!
+		censorText($row['subject']);
+		censorText($row['body']);
+
+		$preview = strip_tags(strtr($row['body'], array('<br />' => '&#10;')));
+
+		// Build the array.
+		$posts[] = array(
+			'id' => $row['id_msg'],
+			'board' => array(
+				'id' => $row['id_board'],
+				'name' => $row['board_name'],
+				'href' => $scripturl . '?board=' . $row['id_board'] . '.0',
+				'link' => '<a href="' . $scripturl . '?board=' . $row['id_board'] . '.0">' . $row['board_name'] . '</a>'
+			),
+			'topic' => $row['id_topic'],
+			'poster' => array(
+				'id' => $row['id_member'],
+				'name' => $row['poster_name'],
+				'href' => empty($row['id_member']) ? '' : $scripturl . '?action=profile;u=' . $row['id_member'],
+				'link' => empty($row['id_member']) ? $row['poster_name'] : '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['poster_name'] . '</a>'
+			),
+			'subject' => $row['subject'],
+			'short_subject' => shorten_subject($row['subject'], 25),
+			'preview' => $smcFunc['strlen']($preview) > 128 ? $smcFunc['substr']($preview, 0, 128) . '...' : $preview,
+			'body' => $row['body'],
+			'time' => timeformat($row['poster_time']),
+			'timestamp' => forum_time(true, $row['poster_time']),
+			'href' => $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['id_msg'] . ';topicseen#new',
+			'link' => '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['id_msg'] . '#msg' . $row['id_msg'] . '" rel="nofollow">' . $row['subject'] . '</a>',
+			'new' => !empty($row['is_read']),
+			'is_new' => empty($row['is_read']),
+			'new_from' => $row['new_from'],
+		);
+	}
+	$smcFunc['db_free_result']($request);
+
+	// Just return it.
+	if ($output_method != 'echo' || empty($posts))
+		return $posts;
+
+	echo '
+		<table border="0" class="ssi_table">';
+	foreach ($posts as $post)
+		echo '
+			<tr>
+				<td align="right" valign="top" nowrap="nowrap">
+					[', $post['board']['link'], ']
+				</td>
+				<td valign="top">
+					<a href="', $post['href'], '">', $post['subject'], '</a>
+					', $txt['by'], ' ', $post['poster']['link'], '
+					', $post['is_new'] ? '<a href="' . $scripturl . '?topic=' . $post['topic'] . '.msg' . $post['new_from'] . ';topicseen#new" rel="nofollow"><img src="' . $settings['lang_images_url'] . '/new.gif" alt="' . $txt['new'] . '" /></a>' : '', '
+				</td>
+				<td align="right" nowrap="nowrap">
+					', $post['time'], '
+				</td>
+			</tr>';
+	echo '
+		</table>';
+}
+
+// Recent topic list:   [board] Subject by Poster	Date
+function ssi_recentTopics($num_recent = 8, $exclude_boards = null, $include_boards = null, $output_method = 'echo')
+{
+	global $context, $settings, $scripturl, $txt, $db_prefix, $user_info;
+	global $modSettings, $smcFunc;
+
+	if ($exclude_boards === null && !empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0)
+		$exclude_boards = array($modSettings['recycle_board']);
+	else
+		$exclude_boards = empty($exclude_boards) ? array() : (is_array($exclude_boards) ? $exclude_boards : array($exclude_boards));
+
+	// Only some boards?.
+	if (is_array($include_boards) || (int) $include_boards === $include_boards)
+	{
+		$include_boards = is_array($include_boards) ? $include_boards : array($include_boards);
+	}
+	elseif ($include_boards != null)
+	{
+		$output_method = $include_boards;
+		$include_boards = array();
+	}
+
+	$stable_icons = array('xx', 'thumbup', 'thumbdown', 'exclamation', 'question', 'lamp', 'smiley', 'angry', 'cheesy', 'grin', 'sad', 'wink', 'moved', 'recycled', 'wireless');
+	$icon_sources = array();
+	foreach ($stable_icons as $icon)
+		$icon_sources[$icon] = 'images_url';
+
+	// Find all the posts in distinct topics.  Newer ones will have higher IDs.
+	$request = $smcFunc['db_query']('substring', '
+		SELECT
+			m.poster_time, ms.subject, m.id_topic, m.id_member, m.id_msg, b.id_board, b.name AS board_name, t.num_replies, t.num_views,
+			IFNULL(mem.real_name, m.poster_name) AS poster_name, ' . ($user_info['is_guest'] ? '1 AS is_read, 0 AS new_from' : '
+			IFNULL(lt.id_msg, IFNULL(lmr.id_msg, 0)) >= m.id_msg_modified AS is_read,
+			IFNULL(lt.id_msg, IFNULL(lmr.id_msg, -1)) + 1 AS new_from') . ', SUBSTRING(m.body, 1, 384) AS body, m.smileys_enabled, m.icon
+		FROM {db_prefix}topics AS t
+			INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_last_msg)
+			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
+			INNER JOIN {db_prefix}messages AS ms ON (ms.id_msg = t.id_first_msg)
+			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)' . (!$user_info['is_guest'] ? '
+			LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = t.id_topic AND lt.id_member = {int:current_member})
+			LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = b.id_board AND lmr.id_member = {int:current_member})' : '') . '
+		WHERE t.id_last_msg >= {int:min_message_id}
+			' . (empty($exclude_boards) ? '' : '
+			AND b.id_board NOT IN ({array_int:exclude_boards})') . '
+			' . (empty($include_boards) ? '' : '
+			AND b.id_board IN ({array_int:include_boards})') . '
+			AND {query_wanna_see_board}' . ($modSettings['postmod_active'] ? '
+			AND t.approved = {int:is_approved}
+			AND m.approved = {int:is_approved}' : '') . '
+		ORDER BY t.id_last_msg DESC
+		LIMIT ' . $num_recent,
+		array(
+			'current_member' => $user_info['id'],
+			'include_boards' => empty($include_boards) ? '' : $include_boards,
+			'exclude_boards' => empty($exclude_boards) ? '' : $exclude_boards,
+			'min_message_id' => $modSettings['maxMsgID'] - 35 * min($num_recent, 5),
+			'is_approved' => 1,
+		)
+	);
+	$posts = array();
+	while ($row = $smcFunc['db_fetch_assoc']($request))
+	{
+		$row['body'] = strip_tags(strtr(parse_bbc($row['body'], $row['smileys_enabled'], $row['id_msg']), array('<br />' => '&#10;')));
+		if ($smcFunc['strlen']($row['body']) > 128)
+			$row['body'] = $smcFunc['substr']($row['body'], 0, 128) . '...';
+
+		// Censor the subject.
+		censorText($row['subject']);
+		censorText($row['body']);
+
+		if (empty($modSettings['messageIconChecks_disable']) && !isset($icon_sources[$row['icon']]))
+			$icon_sources[$row['icon']] = file_exists($settings['theme_dir'] . '/images/post/' . $row['icon'] . '.gif') ? 'images_url' : 'default_images_url';
+
+		// Build the array.
+		$posts[] = array(
+			'board' => array(
+				'id' => $row['id_board'],
+				'name' => $row['board_name'],
+				'href' => $scripturl . '?board=' . $row['id_board'] . '.0',
+				'link' => '<a href="' . $scripturl . '?board=' . $row['id_board'] . '.0">' . $row['board_name'] . '</a>'
+			),
+			'topic' => $row['id_topic'],
+			'poster' => array(
+				'id' => $row['id_member'],
+				'name' => $row['poster_name'],
+				'href' => empty($row['id_member']) ? '' : $scripturl . '?action=profile;u=' . $row['id_member'],
+				'link' => empty($row['id_member']) ? $row['poster_name'] : '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['poster_name'] . '</a>'
+			),
+			'subject' => $row['subject'],
+			'replies' => $row['num_replies'],
+			'views' => $row['num_views'],
+			'short_subject' => shorten_subject($row['subject'], 25),
+			'preview' => $row['body'],
+			'time' => timeformat($row['poster_time']),
+			'timestamp' => forum_time(true, $row['poster_time']),
+			'href' => $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['id_msg'] . ';topicseen#new',
+			'link' => '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['id_msg'] . '#new" rel="nofollow">' . $row['subject'] . '</a>',
+			// Retained for compatibility - is technically incorrect!
+			'new' => !empty($row['is_read']),
+			'is_new' => empty($row['is_read']),
+			'new_from' => $row['new_from'],
+			'icon' => '<img src="' . $settings[$icon_sources[$row['icon']]] . '/post/' . $row['icon'] . '.gif" align="middle" alt="' . $row['icon'] . '" />',
+		);
+	}
+	$smcFunc['db_free_result']($request);
+
+	// Just return it.
+	if ($output_method != 'echo' || empty($posts))
+		return $posts;
+
+	echo '
+		<table border="0" class="ssi_table">';
+	foreach ($posts as $post)
+		echo '
+			<tr>
+				<td align="right" valign="top" nowrap="nowrap">
+					[', $post['board']['link'], ']
+				</td>
+				<td valign="top">
+					<a href="', $post['href'], '">', $post['subject'], '</a>
+					', $txt['by'], ' ', $post['poster']['link'], '
+					', !$post['is_new'] ? '' : '<a href="' . $scripturl . '?topic=' . $post['topic'] . '.msg' . $post['new_from'] . ';topicseen#new" rel="nofollow"><img src="' . $settings['lang_images_url'] . '/new.gif" alt="' . $txt['new'] . '" /></a>', '
+				</td>
+				<td align="right" nowrap="nowrap">
+					', $post['time'], '
+				</td>
+			</tr>';
+	echo '
+		</table>';
+}
+
+// Show the top poster's name and profile link.
+function ssi_topPoster($topNumber = 1, $output_method = 'echo')
+{
+	global $db_prefix, $scripturl, $smcFunc;
+
+	// Find the latest poster.
+	$request = $smcFunc['db_query']('', '
+		SELECT id_member, real_name, posts
+		FROM {db_prefix}members
+		ORDER BY posts DESC
+		LIMIT ' . $topNumber,
+		array(
+		)
+	);
+	$return = array();
+	while ($row = $smcFunc['db_fetch_assoc']($request))
+		$return[] = array(
+			'id' => $row['id_member'],
+			'name' => $row['real_name'],
+			'href' => $scripturl . '?action=profile;u=' . $row['id_member'],
+			'link' => '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['real_name'] . '</a>',
+			'posts' => $row['posts']
+		);
+	$smcFunc['db_free_result']($request);
+
+	// Just return all the top posters.
+	if ($output_method != 'echo')
+		return $return;
+
+	// Make a quick array to list the links in.
+	$temp_array = array();
+	foreach ($return as $member)
+		$temp_array[] = $member['link'];
+
+	echo implode(', ', $temp_array);
+}
+
+// Show boards by activity.
+function ssi_topBoards($num_top = 10, $output_method = 'echo')
+{
+	global $context, $settings, $db_prefix, $txt, $scripturl, $user_info, $modSettings, $smcFunc;
+
+	// Find boards with lots of posts.
+	$request = $smcFunc['db_query']('', '
+		SELECT
+			b.name, b.num_topics, b.num_posts, b.id_board,' . (!$user_info['is_guest'] ? ' 1 AS is_read' : '
+			(IFNULL(lb.id_msg, 0) >= b.id_last_msg) AS is_read') . '
+		FROM {db_prefix}boards AS b
+			LEFT JOIN {db_prefix}log_boards AS lb ON (lb.id_board = b.id_board AND lb.id_member = {int:current_member})
+		WHERE {query_wanna_see_board}' . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? '
+			AND b.id_board != {int:recycle_board}' : '') . '
+		ORDER BY b.num_posts DESC
+		LIMIT ' . $num_top,
+		array(
+			'current_member' => $user_info['id'],
+			'recycle_board' => (int) $modSettings['recycle_board'],
+		)
+	);
+	$boards = array();
+	while ($row = $smcFunc['db_fetch_assoc']($request))
+		$boards[] = array(
+			'id' => $row['id_board'],
+			'num_posts' => $row['num_posts'],
+			'num_topics' => $row['num_topics'],
+			'name' => $row['name'],
+			'new' => empty($row['is_read']),
+			'href' => $scripturl . '?board=' . $row['id_board'] . '.0',
+			'link' => '<a href="' . $scripturl . '?board=' . $row['id_board'] . '.0">' . $row['name'] . '</a>'
+		);
+	$smcFunc['db_free_result']($request);
+
+	// If we shouldn't output or have nothing to output, just jump out.
+	if ($output_method != 'echo' || empty($boards))
+		return $boards;
+
+	echo '
+		<table class="ssi_table">
+			<tr>
+				<th align="left">', $txt['board'], '</th>
+				<th align="left">', $txt['board_topics'], '</th>
+				<th align="left">', $txt['posts'], '</th>
+			</tr>';
+	foreach ($boards as $board)
+		echo '
+			<tr>
+				<td>', $board['link'], $board['new'] ? ' <a href="' . $board['href'] . '"><img src="' . $settings['lang_images_url'] . '/new.gif" alt="' . $txt['new'] . '" /></a>' : '', '</td>
+				<td align="right">', comma_format($board['num_topics']), '</td>
+				<td align="right">', comma_format($board['num_posts']), '</td>
+			</tr>';
+	echo '
+		</table>';
+}
+
+// Shows the top topics.
+function ssi_topTopics($type = 'replies', $num_topics = 10, $output_method = 'echo')
+{
+	global $db_prefix, $txt, $scripturl, $user_info, $modSettings, $smcFunc, $context;
+
+	if ($modSettings['totalMessages'] > 100000)
+	{
+		// !!! Why don't we use {query(_wanna)_see_board}?
+		$request = $smcFunc['db_query']('', '
+			SELECT id_topic
+			FROM {db_prefix}topics
+			WHERE num_' . ($type != 'replies' ? 'views' : 'replies') . ' != 0' . ($modSettings['postmod_active'] ? '
+				AND approved = {int:is_approved}' : '') . '
+			ORDER BY num_' . ($type != 'replies' ? 'views' : 'replies') . ' DESC
+			LIMIT {int:limit}',
+			array(
+				'is_approved' => 1,
+				'limit' => $num_topics > 100 ? ($num_topics + ($num_topics / 2)) : 100,
+			)
+		);
+		$topic_ids = array();
+		while ($row = $smcFunc['db_fetch_assoc']($request))
+			$topic_ids[] = $row['id_topic'];
+		$smcFunc['db_free_result']($request);
+	}
+	else
+		$topic_ids = array();
+
+	$request = $smcFunc['db_query']('', '
+		SELECT m.subject, m.id_topic, t.num_views, t.num_replies
+		FROM {db_prefix}topics AS t
+			INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)
+			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
+		WHERE {query_wanna_see_board}' . ($modSettings['postmod_active'] ? '
+			AND t.approved = {int:is_approved}' : '') . (!empty($topic_ids) ? '
+			AND t.id_topic IN ({array_int:topic_list})' : '') . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? '
+			AND b.id_board != {int:recycle_enable}' : '') . '
+		ORDER BY t.num_' . ($type != 'replies' ? 'views' : 'replies') . ' DESC
+		LIMIT {int:limit}',
+		array(
+			'topic_list' => $topic_ids,
+			'is_approved' => 1,
+			'recycle_enable' => $modSettings['recycle_board'],
+			'limit' => $num_topics,
+		)
+	);
+	$topics = array();
+	while ($row = $smcFunc['db_fetch_assoc']($request))
+	{
+		censorText($row['subject']);
+
+		$topics[] = array(
+			'id' => $row['id_topic'],
+			'subject' => $row['subject'],
+			'num_replies' => $row['num_replies'],
+			'num_views' => $row['num_views'],
+			'href' => $scripturl . '?topic=' . $row['id_topic'] . '.0',
+			'link' => '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.0">' . $row['subject'] . '</a>',
+		);
+	}
+	$smcFunc['db_free_result']($request);
+
+	if ($output_method != 'echo' || empty($topics))
+		return $topics;
+
+	echo '
+		<table class="ssi_table">
+			<tr>
+				<th align="left"></th>
+				<th align="left">', $txt['views'], '</th>
+				<th align="left">', $txt['replies'], '</th>
+			</tr>';
+	foreach ($topics as $topic)
+		echo '
+			<tr>
+				<td align="left">
+					', $topic['link'], '
+				</td>
+				<td align="right">', comma_format($topic['num_views']), '</td>
+				<td align="right">', comma_format($topic['num_replies']), '</td>
+			</tr>';
+	echo '
+		</table>';
+}
+
+// Shows the top topics, by replies.
+function ssi_topTopicsReplies($num_topics = 10, $output_method = 'echo')
+{
+	return ssi_topTopics('replies', $num_topics, $output_method);
+}
+
+// Shows the top topics, by views.
+function ssi_topTopicsViews($num_topics = 10, $output_method = 'echo')
+{
+	return ssi_topTopics('views', $num_topics, $output_method);
+}
+
+// Show a link to the latest member:  Please welcome, Someone, out latest member.
+function ssi_latestMember($output_method = 'echo')
+{
+	global $db_prefix, $txt, $scripturl, $context;
+
+	if ($output_method == 'echo')
+		echo '
+	', $txt['welcome_member'], ' ', $context['common_stats']['latest_member']['link'], '', $txt['newest_member'], '<br />';
+	else
+		return $context['common_stats']['latest_member'];
+}
+
+// Fetch a random member - if type set to 'day' will only change once a day!
+function ssi_randomMember($random_type = '', $output_method = 'echo')
+{
+	global $modSettings;
+
+	// If we're looking for something to stay the same each day then seed the generator.
+	if ($random_type == 'day')
+	{
+		// Set the seed to change only once per day.
+		mt_srand(floor(time() / 86400));
+	}
+
+	// Get the lowest ID we're interested in.
+	$member_id = mt_rand(1, $modSettings['latestMember']);
+
+	$where_query = '
+		id_member >= {int:selected_member}
+		AND is_activated = {int:is_activated}';
+
+	$query_where_params = array(
+		'selected_member' => $member_id,
+		'is_activated' => 1,
+	);
+
+	$result = ssi_queryMembers($where_query, $query_where_params, 1, 'id_member ASC', $output_method);
+
+	// If we got nothing do the reverse - in case of unactivated members.
+	if (empty($result))
+	{
+		$where_query = '
+			id_member <= {int:selected_member}
+			AND is_activated = {int:is_activated}';
+
+		$query_where_params = array(
+			'selected_member' => $member_id,
+			'is_activated' => 1,
+		);
+
+		$result = ssi_queryMembers($where_query, $query_where_params, 1, 'id_member DESC', $output_method);
+	}
+
+	// Just to be sure put the random generator back to something... random.
+	if ($random_type != '')
+		mt_srand(time());
+
+	return $result;
+}
+
+// Fetch a specific member.
+function ssi_fetchMember($member_ids, $output_method = 'echo')
+{
+	// Can have more than one member if you really want...
+	$member_ids = is_array($member_ids) ? $member_ids : array($member_ids);
+
+	// Restrict it right!
+	$query_where = '
+		id_member IN ({array_int:member_list})';
+
+	$query_where_params = array(
+		'member_list' => $member_ids,
+	);
+
+	// Then make the query and dump the data.
+	return ssi_queryMembers($query_where, $query_where_params, '', 'id_member', $output_method);
+}
+
+// Get all members of a group.
+function ssi_fetchGroupMembers($group_id, $output_method = 'echo')
+{
+	$query_where = '
+		id_group = {int:id_group}
+		OR id_post_group = {int:id_group}
+		OR FIND_IN_SET({int:id_group}, additional_groups)';
+
+	$query_where_params = array(
+		'id_group' => $group_id,
+	);
+
+	return ssi_queryMembers($query_where, $query_where_params, '', 'real_name', $output_method);
+}
+
+// Fetch some member data!
+function ssi_queryMembers($query_where, $query_where_params = array(), $query_limit = '', $query_order = 'id_member DESC', $output_method = 'echo')
+{
+	global $context, $settings, $scripturl, $txt, $db_prefix, $user_info;
+	global $modSettings, $smcFunc, $memberContext;
+
+	// Fetch the members in question.
+	$request = $smcFunc['db_query']('', '
+		SELECT id_member
+		FROM {db_prefix}members
+		WHERE ' . $query_where . '
+		ORDER BY ' . $query_order . '
+		' . ($query_limit == '' ? '' : 'LIMIT ' . $query_limit),
+		array_merge($query_where_params, array(
+		))
+	);
+	$members = array();
+	while ($row = $smcFunc['db_fetch_assoc']($request))
+		$members[] = $row['id_member'];
+	$smcFunc['db_free_result']($request);
+
+	if (empty($members))
+		return array();
+
+	// Load the members.
+	loadMemberData($members);
+
+	// Draw the table!
+	if ($output_method == 'echo')
+		echo '
+		<table border="0" class="ssi_table">';
+
+	$query_members = array();
+	foreach ($members as $member)
+	{
+		// Load their context data.
+		if (!loadMemberContext($member))
+			continue;
+
+		// Store this member's information.
+		$query_members[$member] = $memberContext[$member];
+
+		// Only do something if we're echo'ing.
+		if ($output_method == 'echo')
+			echo '
+			<tr>
+				<td align="right" valign="top" nowrap="nowrap">
+					', $query_members[$member]['link'], '
+					<br />', $query_members[$member]['blurb'], '
+					<br />', $query_members[$member]['avatar']['image'], '
+				</td>
+			</tr>';
+	}
+
+	// End the table if appropriate.
+	if ($output_method == 'echo')
+		echo '
+		</table>';
+
+	// Send back the data.
+	return $query_members;
+}
+
+// Show some basic stats:  Total This: XXXX, etc.
+function ssi_boardStats($output_method = 'echo')
+{
+	global $db_prefix, $txt, $scripturl, $modSettings, $smcFunc;
+
+	$totals = array(
+		'members' => $modSettings['totalMembers'],
+		'posts' => $modSettings['totalMessages'],
+		'topics' => $modSettings['totalTopics']
+	);
+
+	$result = $smcFunc['db_query']('', '
+		SELECT COUNT(*)
+		FROM {db_prefix}boards',
+		array(
+		)
+	);
+	list ($totals['boards']) = $smcFunc['db_fetch_row']($result);
+	$smcFunc['db_free_result']($result);
+
+	$result = $smcFunc['db_query']('', '
+		SELECT COUNT(*)
+		FROM {db_prefix}categories',
+		array(
+		)
+	);
+	list ($totals['categories']) = $smcFunc['db_fetch_row']($result);
+	$smcFunc['db_free_result']($result);
+
+	if ($output_method != 'echo')
+		return $totals;
+
+	echo '
+		', $txt['total_members'], ': <a href="', $scripturl . '?action=mlist">', comma_format($totals['members']), '</a><br />
+		', $txt['total_posts'], ': ', comma_format($totals['posts']), '<br />
+		', $txt['total_topics'], ': ', comma_format($totals['topics']), ' <br />
+		', $txt['total_cats'], ': ', comma_format($totals['categories']), '<br />
+		', $txt['total_boards'], ': ', comma_format($totals['boards']);
+}
+
+// Shows a list of online users:  YY Guests, ZZ Users and then a list...
+function ssi_whosOnline($output_method = 'echo')
+{
+	global $user_info, $txt, $sourcedir, $settings, $modSettings;
+
+	require_once($sourcedir . '/Subs-MembersOnline.php');
+	$membersOnlineOptions = array(
+		'show_hidden' => allowedTo('moderate_forum'),
+		'sort' => 'log_time',
+		'reverse_sort' => true,
+	);
+	$return = getMembersOnlineStats($membersOnlineOptions);
+
+	// Add some redundancy for backwards compatibility reasons.
+	if ($output_method != 'echo')
+		return $return + array(
+			'users' => $return['users_online'],
+			'guests' => $return['num_guests'],
+			'hidden' => $return['num_users_hidden'],
+			'buddies' => $return['num_buddies'],
+			'num_users' => $return['num_users_online'],
+			'total_users' => $return['num_users_online'] + $return['num_guests'] + $return['num_spiders'],
+		);
+
+	echo '
+		', comma_format($return['num_guests']), ' ', $return['num_guests'] == 1 ? $txt['guest'] : $txt['guests'], ', ', comma_format($return['num_users_online']), ' ', $return['num_users_online'] == 1 ? $txt['user'] : $txt['users'];
+
+	$bracketList = array();
+	if (!empty($user_info['buddies']))
+		$bracketList[] = comma_format($return['num_buddies']) . ' ' . ($return['num_buddies'] == 1 ? $txt['buddy'] : $txt['buddies']);
+	if (!empty($return['num_spiders']))
+		$bracketList[] = comma_format($return['num_spiders']) . ' ' . ($return['num_spiders'] == 1 ? $txt['spider'] : $txt['spiders']);
+	if (!empty($return['num_users_hidden']))
+		$bracketList[] = comma_format($return['num_users_hidden']) . ' ' . $txt['hidden'];
+
+	if (!empty($bracketList))
+		echo ' (' . implode(', ', $bracketList) . ')';
+
+	echo '<br />
+			', implode(', ', $return['list_users_online']);
+
+	// Showing membergroups?
+	if (!empty($settings['show_group_key']) && !empty($return['membergroups']))
+		echo '<br />
+			[' . implode(']&nbsp;&nbsp;[', $return['membergroups']) . ']';
+}
+
+// Just like whosOnline except it also logs the online presence.
+function ssi_logOnline($output_method = 'echo')
+{
+	writeLog();
+
+	if ($output_method != 'echo')
+		return ssi_whosOnline($output_method);
+	else
+		ssi_whosOnline($output_method);
+}
+
+// Shows a login box.
+function ssi_login($redirect_to = '', $output_method = 'echo')
+{
+	global $scripturl, $txt, $user_info, $context, $modSettings;
+
+	if ($redirect_to != '')
+		$_SESSION['login_url'] = $redirect_to;
+
+	if ($output_method != 'echo' || !$user_info['is_guest'])
+		return $user_info['is_guest'];
+
+	echo '
+		<form action="', $scripturl, '?action=login2" method="post" accept-charset="', $context['character_set'], '">
+			<table border="0" cellspacing="1" cellpadding="0" class="ssi_table">
+				<tr>
+					<td align="right"><label for="user">', $txt['username'], ':</label>&nbsp;</td>
+					<td><input type="text" id="user" name="user" size="9" value="', $user_info['username'], '" class="input_text" /></td>
+				</tr><tr>
+					<td align="right"><label for="passwrd">', $txt['password'], ':</label>&nbsp;</td>
+					<td><input type="password" name="passwrd" id="passwrd" size="9" class="input_password" /></td>
+				</tr>';
+
+	// Open ID?
+	if (!empty($modSettings['enableOpenID']))
+		echo '<tr>
+					<td colspan="2" align="center"><strong>&mdash;', $txt['or'], '&mdash;</strong></td>
+				</tr><tr>
+					<td align="right"><label for="openid_url">', $txt['openid'], ':</label>&nbsp;</td>
+					<td><input type="text" name="openid_identifier" id="openid_url" class="input_text openid_login" size="17" /></td>
+				</tr>';
+
+	echo '<tr>
+					<td><input type="hidden" name="cookielength" value="-1" /></td>
+					<td><input type="submit" value="', $txt['login'], '" class="button_submit" /></td>
+				</tr>
+			</table>
+		</form>';
+
+}
+
+// Show the most-voted-in poll.
+function ssi_topPoll($output_method = 'echo')
+{
+	// Just use recentPoll, no need to duplicate code...
+	return ssi_recentPoll(true, $output_method);
+}
+
+// Show the most recently posted poll.
+function ssi_recentPoll($topPollInstead = false, $output_method = 'echo')
+{
+	global $db_prefix, $txt, $settings, $boardurl, $user_info, $context, $smcFunc, $modSettings;
+
+	$boardsAllowed = array_intersect(boardsAllowedTo('poll_view'), boardsAllowedTo('poll_vote'));
+
+	if (empty($boardsAllowed))
+		return array();
+
+	$request = $smcFunc['db_query']('', '
+		SELECT p.id_poll, p.question, t.id_topic, p.max_votes, p.guest_vote, p.hide_results, p.expire_time
+		FROM {db_prefix}polls AS p
+			INNER JOIN {db_prefix}topics AS t ON (t.id_poll = p.id_poll' . ($modSettings['postmod_active'] ? ' AND t.approved = {int:is_approved}' : '') . ')
+			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)' . ($topPollInstead ? '
+			INNER JOIN {db_prefix}poll_choices AS pc ON (pc.id_poll = p.id_poll)' : '') . '
+			LEFT JOIN {db_prefix}log_polls AS lp ON (lp.id_poll = p.id_poll AND lp.id_member > {int:no_member} AND lp.id_member = {int:current_member})
+		WHERE p.voting_locked = {int:voting_opened}
+			AND (p.expire_time = {int:no_expiration} OR {int:current_time} < p.expire_time)
+			AND ' . ($user_info['is_guest'] ? 'p.guest_vote = {int:guest_vote_allowed}' : 'lp.id_choice IS NULL') . '
+			AND {query_wanna_see_board}' . (!in_array(0, $boardsAllowed) ? '
+			AND b.id_board IN ({array_int:boards_allowed_list})' : '') . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? '
+			AND b.id_board != {int:recycle_enable}' : '') . '
+		ORDER BY ' . ($topPollInstead ? 'pc.votes' : 'p.id_poll') . ' DESC
+		LIMIT 1',
+		array(
+			'current_member' => $user_info['id'],
+			'boards_allowed_list' => $boardsAllowed,
+			'is_approved' => 1,
+			'guest_vote_allowed' => 1,
+			'no_member' => 0,
+			'voting_opened' => 0,
+			'no_expiration' => 0,
+			'current_time' => time(),
+			'recycle_enable' => $modSettings['recycle_board'],
+		)
+	);
+	$row = $smcFunc['db_fetch_assoc']($request);
+	$smcFunc['db_free_result']($request);
+
+	// This user has voted on all the polls.
+	if ($row === false)
+		return array();
+
+	// If this is a guest who's voted we'll through ourselves to show poll to show the results.
+	if ($user_info['is_guest'] && (!$row['guest_vote'] || (isset($_COOKIE['guest_poll_vote']) && in_array($row['id_poll'], explode(',', $_COOKIE['guest_poll_vote'])))))
+		return ssi_showPoll($row['id_topic'], $output_method);
+
+	$request = $smcFunc['db_query']('', '
+		SELECT COUNT(DISTINCT id_member)
+		FROM {db_prefix}log_polls
+		WHERE id_poll = {int:current_poll}',
+		array(
+			'current_poll' => $row['id_poll'],
+		)
+	);
+	list ($total) = $smcFunc['db_fetch_row']($request);
+	$smcFunc['db_free_result']($request);
+
+	$request = $smcFunc['db_query']('', '
+		SELECT id_choice, label, votes
+		FROM {db_prefix}poll_choices
+		WHERE id_poll = {int:current_poll}',
+		array(
+			'current_poll' => $row['id_poll'],
+		)
+	);
+	$options = array();
+	while ($rowChoice = $smcFunc['db_fetch_assoc']($request))
+	{
+		censorText($rowChoice['label']);
+
+		$options[$rowChoice['id_choice']] = array($rowChoice['label'], $rowChoice['votes']);
+	}
+	$smcFunc['db_free_result']($request);
+
+	// Can they view it?
+	$is_expired = !empty($row['expire_time']) && $row['expire_time'] < time();
+	$allow_view_results = allowedTo('moderate_board') || $row['hide_results'] == 0 || $is_expired;
+
+	$return = array(
+		'id' => $row['id_poll'],
+		'image' => 'poll',
+		'question' => $row['question'],
+		'total_votes' => $total,
+		'is_locked' => false,
+		'topic' => $row['id_topic'],
+		'allow_view_results' => $allow_view_results,
+		'options' => array()
+	);
+
+	// Calculate the percentages and bar lengths...
+	$divisor = $return['total_votes'] == 0 ? 1 : $return['total_votes'];
+	foreach ($options as $i => $option)
+	{
+		$bar = floor(($option[1] * 100) / $divisor);
+		$barWide = $bar == 0 ? 1 : floor(($bar * 5) / 3);
+		$return['options'][$i] = array(
+			'id' => 'options-' . ($topPollInstead ? 'top-' : 'recent-') . $i,
+			'percent' => $bar,
+			'votes' => $option[1],
+			'bar' => '<span style="white-space: nowrap;"><img src="' . $settings['images_url'] . '/poll_' . ($context['right_to_left'] ? 'right' : 'left') . '.gif" alt="" /><img src="' . $settings['images_url'] . '/poll_middle.gif" width="' . $barWide . '" height="12" alt="-" /><img src="' . $settings['images_url'] . '/poll_' . ($context['right_to_left'] ? 'left' : 'right') . '.gif" alt="" /></span>',
+			'option' => parse_bbc($option[0]),
+			'vote_button' => '<input type="' . ($row['max_votes'] > 1 ? 'checkbox' : 'radio') . '" name="options[]" id="options-' . ($topPollInstead ? 'top-' : 'recent-') . $i . '" value="' . $i . '" class="input_' . ($row['max_votes'] > 1 ? 'check' : 'radio') . '" />'
+		);
+	}
+
+	$return['allowed_warning'] = $row['max_votes'] > 1 ? sprintf($txt['poll_options6'], min(count($options), $row['max_votes'])) : '';
+
+	if ($output_method != 'echo')
+		return $return;
+
+	if ($allow_view_results)
+	{
+		echo '
+		<form class="ssi_poll" action="', $boardurl, '/SSI.php?ssi_function=pollVote" method="post" accept-charset="', $context['character_set'], '">
+			<strong>', $return['question'], '</strong><br />
+			', !empty($return['allowed_warning']) ? $return['allowed_warning'] . '<br />' : '';
+
+		foreach ($return['options'] as $option)
+			echo '
+			<label for="', $option['id'], '">', $option['vote_button'], ' ', $option['option'], '</label><br />';
+
+		echo '
+			<input type="submit" value="', $txt['poll_vote'], '" class="button_submit" />
+			<input type="hidden" name="poll" value="', $return['id'], '" />
+			<input type="hidden" name="', $context['session_var'], '" value="', $context['session_id'], '" />
+		</form>';
+	}
+	else
+		echo $txt['poll_cannot_see'];
+}
+
+function ssi_showPoll($topic = null, $output_method = 'echo')
+{
+	global $db_prefix, $txt, $settings, $boardurl, $user_info, $context, $smcFunc, $modSettings;
+
+	$boardsAllowed = boardsAllowedTo('poll_view');
+
+	if (empty($boardsAllowed))
+		return array();
+
+	if ($topic === null && isset($_REQUEST['ssi_topic']))
+		$topic = (int) $_REQUEST['ssi_topic'];
+	else
+		$topic = (int) $topic;
+
+	$request = $smcFunc['db_query']('', '
+		SELECT
+			p.id_poll, p.question, p.voting_locked, p.hide_results, p.expire_time, p.max_votes, p.guest_vote, b.id_board
+		FROM {db_prefix}topics AS t
+			INNER JOIN {db_prefix}polls AS p ON (p.id_poll = t.id_poll)
+			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
+		WHERE t.id_topic = {int:current_topic}
+			AND {query_see_board}' . (!in_array(0, $boardsAllowed) ? '
+			AND b.id_board IN ({array_int:boards_allowed_see})' : '') . ($modSettings['postmod_active'] ? '
+			AND t.approved = {int:is_approved}' : '') . '
+		LIMIT 1',
+		array(
+			'current_topic' => $topic,
+			'boards_allowed_see' => $boardsAllowed,
+			'is_approved' => 1,
+		)
+	);
+
+	// Either this topic has no poll, or the user cannot view it.
+	if ($smcFunc['db_num_rows']($request) == 0)
+		return array();
+
+	$row = $smcFunc['db_fetch_assoc']($request);
+	$smcFunc['db_free_result']($request);
+
+	// Check if they can vote.
+	if (!empty($row['expire_time']) && $row['expire_time'] < time())
+		$allow_vote = false;
+	elseif ($user_info['is_guest'] && $row['guest_vote'] && (!isset($_COOKIE['guest_poll_vote']) || !in_array($row['id_poll'], explode(',', $_COOKIE['guest_poll_vote']))))
+		$allow_vote = true;
+	elseif ($user_info['is_guest'])
+		$allow_vote = false;
+	elseif (!empty($row['voting_locked']) || !allowedTo('poll_vote', $row['id_board']))
+		$allow_vote = false;
+	else
+	{
+		$request = $smcFunc['db_query']('', '
+			SELECT id_member
+			FROM {db_prefix}log_polls
+			WHERE id_poll = {int:current_poll}
+				AND id_member = {int:current_member}
+			LIMIT 1',
+			array(
+				'current_member' => $user_info['id'],
+				'current_poll' => $row['id_poll'],
+			)
+		);
+		$allow_vote = $smcFunc['db_num_rows']($request) == 0;
+		$smcFunc['db_free_result']($request);
+	}
+
+	// Can they view?
+	$is_expired = !empty($row['expire_time']) && $row['expire_time'] < time();
+	$allow_view_results = allowedTo('moderate_board') || $row['hide_results'] == 0 || ($row['hide_results'] == 1 && !$allow_vote) || $is_expired;
+
+	$request = $smcFunc['db_query']('', '
+		SELECT COUNT(DISTINCT id_member)
+		FROM {db_prefix}log_polls
+		WHERE id_poll = {int:current_poll}',
+		array(
+			'current_poll' => $row['id_poll'],
+		)
+	);
+	list ($total) = $smcFunc['db_fetch_row']($request);
+	$smcFunc['db_free_result']($request);
+
+	$request = $smcFunc['db_query']('', '
+		SELECT id_choice, label, votes
+		FROM {db_prefix}poll_choices
+		WHERE id_poll = {int:current_poll}',
+		array(
+			'current_poll' => $row['id_poll'],
+		)
+	);
+	$options = array();
+	$total_votes = 0;
+	while ($rowChoice = $smcFunc['db_fetch_assoc']($request))
+	{
+		censorText($rowChoice['label']);
+
+		$options[$rowChoice['id_choice']] = array($rowChoice['label'], $rowChoice['votes']);
+		$total_votes += $rowChoice['votes'];
+	}
+	$smcFunc['db_free_result']($request);
+
+	$return = array(
+		'id' => $row['id_poll'],
+		'image' => empty($pollinfo['voting_locked']) ? 'poll' : 'locked_poll',
+		'question' => $row['question'],
+		'total_votes' => $total,
+		'is_locked' => !empty($pollinfo['voting_locked']),
+		'allow_vote' => $allow_vote,
+		'allow_view_results' => $allow_view_results,
+		'topic' => $topic
+	);
+
+	// Calculate the percentages and bar lengths...
+	$divisor = $total_votes == 0 ? 1 : $total_votes;
+	foreach ($options as $i => $option)
+	{
+		$bar = floor(($option[1] * 100) / $divisor);
+		$barWide = $bar == 0 ? 1 : floor(($bar * 5) / 3);
+		$return['options'][$i] = array(
+			'id' => 'options-' . $i,
+			'percent' => $bar,
+			'votes' => $option[1],
+			'bar' => '<span style="white-space: nowrap;"><img src="' . $settings['images_url'] . '/poll_' . ($context['right_to_left'] ? 'right' : 'left') . '.gif" alt="" /><img src="' . $settings['images_url'] . '/poll_middle.gif" width="' . $barWide . '" height="12" alt="-" /><img src="' . $settings['images_url'] . '/poll_' . ($context['right_to_left'] ? 'left' : 'right') . '.gif" alt="" /></span>',
+			'option' => parse_bbc($option[0]),
+			'vote_button' => '<input type="' . ($row['max_votes'] > 1 ? 'checkbox' : 'radio') . '" name="options[]" id="options-' . $i . '" value="' . $i . '" class="input_' . ($row['max_votes'] > 1 ? 'check' : 'radio') . '" />'
+		);
+	}
+
+	$return['allowed_warning'] = $row['max_votes'] > 1 ? sprintf($txt['poll_options6'], min(count($options), $row['max_votes'])) : '';
+
+	if ($output_method != 'echo')
+		return $return;
+
+	if ($return['allow_vote'])
+	{
+		echo '
+			<form class="ssi_poll" action="', $boardurl, '/SSI.php?ssi_function=pollVote" method="post" accept-charset="', $context['character_set'], '">
+				<strong>', $return['question'], '</strong><br />
+				', !empty($return['allowed_warning']) ? $return['allowed_warning'] . '<br />' : '';
+
+		foreach ($return['options'] as $option)
+			echo '
+				<label for="', $option['id'], '">', $option['vote_button'], ' ', $option['option'], '</label><br />';
+
+		echo '
+				<input type="submit" value="', $txt['poll_vote'], '" class="button_submit" />
+				<input type="hidden" name="poll" value="', $return['id'], '" />
+				<input type="hidden" name="', $context['session_var'], '" value="', $context['session_id'], '" />
+			</form>';
+	}
+	elseif ($return['allow_view_results'])
+	{
+		echo '
+			<div class="ssi_poll">
+				<strong>', $return['question'], '</strong>
+				<dl>';
+
+		foreach ($return['options'] as $option)
+			echo '
+					<dt>', $option['option'], '</dt>
+					<dd>
+						<div class="ssi_poll_bar" style="border: 1px solid #666; height: 1em">
+							<div class="ssi_poll_bar_fill" style="background: #ccf; height: 1em; width: ', $option['percent'], '%;">
+							</div>
+						</div>
+						', $option['votes'], ' (', $option['percent'], '%)
+					</dd>';
+		echo '
+				</dl>
+				<strong>', $txt['poll_total_voters'], ': ', $return['total_votes'], '</strong>
+			</div>';
+	}
+	// Cannot see it I'm afraid!
+	else
+		echo $txt['poll_cannot_see'];
+}
+
+// Takes care of voting - don't worry, this is done automatically.
+function ssi_pollVote()
+{
+	global $context, $db_prefix, $user_info, $sc, $smcFunc, $sourcedir, $modSettings;
+
+	if (!isset($_POST[$context['session_var']]) || $_POST[$context['session_var']] != $sc || empty($_POST['options']) || !isset($_POST['poll']))
+	{
+		echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+<head>
+	<script type="text/javascript"><!-- // --><![CDATA[
+		history.go(-1);
+	// ]]></script>
+</head>
+<body>&laquo;</body>
+</html>';
+		return;
+	}
+
+	// This can cause weird errors! (ie. copyright missing.)
+	checkSession();
+
+	$_POST['poll'] = (int) $_POST['poll'];
+
+	// Check if they have already voted, or voting is locked.
+	$request = $smcFunc['db_query']('', '
+		SELECT
+			p.id_poll, p.voting_locked, p.expire_time, p.max_votes, p.guest_vote,
+			t.id_topic,
+			IFNULL(lp.id_choice, -1) AS selected
+		FROM {db_prefix}polls AS p
+			INNER JOIN {db_prefix}topics AS t ON (t.id_poll = {int:current_poll})
+			INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
+			LEFT JOIN {db_prefix}log_polls AS lp ON (lp.id_poll = p.id_poll AND lp.id_member = {int:current_member})
+		WHERE p.id_poll = {int:current_poll}
+			AND {query_see_board}' . ($modSettings['postmod_active'] ? '
+			AND t.approved = {int:is_approved}' : '') . '
+		LIMIT 1',
+		array(
+			'current_member' => $user_info['id'],
+			'current_poll' => $_POST['poll'],
+			'is_approved' => 1,
+		)
+	);
+	if ($smcFunc['db_num_rows']($request) == 0)
+		die;
+	$row = $smcFunc['db_fetch_assoc']($request);
+	$smcFunc['db_free_result']($request);
+
+	if (!empty($row['voting_locked']) || ($row['selected'] != -1 && !$user_info['is_guest']) || (!empty($row['expire_time']) && time() > $row['expire_time']))
+		redirectexit('topic=' . $row['id_topic'] . '.0');
+
+	// Too many options checked?
+	if (count($_REQUEST['options']) > $row['max_votes'])
+		redirectexit('topic=' . $row['id_topic'] . '.0');
+
+	// It's a guest who has already voted?
+	if ($user_info['is_guest'])
+	{
+		// Guest voting disabled?
+		if (!$row['guest_vote'])
+			redirectexit('topic=' . $row['id_topic'] . '.0');
+		// Already voted?
+		elseif (isset($_COOKIE['guest_poll_vote']) && in_array($row['id_poll'], explode(',', $_COOKIE['guest_poll_vote'])))
+			redirectexit('topic=' . $row['id_topic'] . '.0');
+	}
+
+	$options = array();
+	$inserts = array();
+	foreach ($_REQUEST['options'] as $id)
+	{
+		$id = (int) $id;
+
+		$options[] = $id;
+		$inserts[] = array($_POST['poll'], $user_info['id'], $id);
+	}
+
+	// Add their vote in to the tally.
+	$smcFunc['db_insert']('insert',
+		$db_prefix . 'log_polls',
+		array('id_poll' => 'int', 'id_member' => 'int', 'id_choice' => 'int'),
+		$inserts,
+		array('id_poll', 'id_member', 'id_choice')
+	);
+	$smcFunc['db_query']('', '
+		UPDATE {db_prefix}poll_choices
+		SET votes = votes + 1
+		WHERE id_poll = {int:current_poll}
+			AND id_choice IN ({array_int:option_list})',
+		array(
+			'option_list' => $options,
+			'current_poll' => $_POST['poll'],
+		)
+	);
+
+	// Track the vote if a guest.
+	if ($user_info['is_guest'])
+	{
+		$_COOKIE['guest_poll_vote'] = !empty($_COOKIE['guest_poll_vote']) ? ($_COOKIE['guest_poll_vote'] . ',' . $row['id_poll']) : $row['id_poll'];
+
+		require_once($sourcedir . '/Subs-Auth.php');
+		$cookie_url = url_parts(!empty($modSettings['localCookies']), !empty($modSettings['globalCookies']));
+		setcookie('guest_poll_vote', $_COOKIE['guest_poll_vote'], time() + 2500000, $cookie_url[1], $cookie_url[0], 0);
+	}
+
+	redirectexit('topic=' . $row['id_topic'] . '.0');
+}
+
+// Show a search box.
+function ssi_quickSearch($output_method = 'echo')
+{
+	global $scripturl, $txt, $context;
+
+	if ($output_method != 'echo')
+		return $scripturl . '?action=search';
+
+	echo '
+		<form action="', $scripturl, '?action=search2" method="post" accept-charset="', $context['character_set'], '">
+			<input type="hidden" name="advanced" value="0" /><input type="text" name="search" size="30" class="input_text" /> <input type="submit" name="submit" value="', $txt['search'], '" class="button_submit" />
+		</form>';
+}
+
+// Show what would be the forum news.
+function ssi_news($output_method = 'echo')
+{
+	global $context;
+
+	if ($output_method != 'echo')
+		return $context['random_news_line'];
+
+	echo $context['random_news_line'];
+}
+
+// Show today's birthdays.
+function ssi_todaysBirthdays($output_method = 'echo')
+{
+	global $scripturl, $modSettings, $user_info;
+
+	$eventOptions = array(
+		'include_birthdays' => true,
+		'num_days_shown' => empty($modSettings['cal_days_for_index']) || $modSettings['cal_days_for_index'] < 1 ? 1 : $modSettings['cal_days_for_index'],
+	);
+	$return = cache_quick_get('calendar_index_offset_' . ($user_info['time_offset'] + $modSettings['time_offset']), 'Subs-Calendar.php', 'cache_getRecentEvents', array($eventOptions));
+
+	if ($output_method != 'echo')
+		return $return['calendar_birthdays'];
+
+	foreach ($return['calendar_birthdays'] as $member)
+		echo '
+			<a href="', $scripturl, '?action=profile;u=', $member['id'], '">' . $member['name'] . (isset($member['age']) ? ' (' . $member['age'] . ')' : '') . '</a>' . (!$member['is_last'] ? ', ' : '');
+}
+
+// Show today's holidays.
+function ssi_todaysHolidays($output_method = 'echo')
+{
+	global $modSettings, $user_info;
+
+	$eventOptions = array(
+		'include_holidays' => true,
+		'num_days_shown' => empty($modSettings['cal_days_for_index']) || $modSettings['cal_days_for_index'] < 1 ? 1 : $modSettings['cal_days_for_index'],
+	);
+	$return = cache_quick_get('calendar_index_offset_' . ($user_info['time_offset'] + $modSettings['time_offset']), 'Subs-Calendar.php', 'cache_getRecentEvents', array($eventOptions));
+
+	if ($output_method != 'echo')
+		return $return['calendar_holidays'];
+
+	echo '
+		', implode(', ', $return['calendar_holidays']);
+}
+
+// Show today's events.
+function ssi_todaysEvents($output_method = 'echo')
+{
+	global $modSettings, $user_info;
+
+	$eventOptions = array(
+		'include_events' => true,
+		'num_days_shown' => empty($modSettings['cal_days_for_index']) || $modSettings['cal_days_for_index'] < 1 ? 1 : $modSettings['cal_days_for_index'],
+	);
+	$return = cache_quick_get('calendar_index_offset_' . ($user_info['time_offset'] + $modSettings['time_offset']), 'Subs-Calendar.php', 'cache_getRecentEvents', array($eventOptions));
+
+	if ($output_method != 'echo')
+		return $return['calendar_events'];
+
+	foreach ($return['calendar_events'] as $event)
+	{
+		if ($event['can_edit'])
+			echo '
+	<a href="' . $event['modify_href'] . '" style="color: #ff0000;">*</a> ';
+		echo '
+	' . $event['link'] . (!$event['is_last'] ? ', ' : '');
+	}
+}
+
+// Show all calendar entires for today. (birthdays, holodays, and events.)
+function ssi_todaysCalendar($output_method = 'echo')
+{
+	global $modSettings, $txt, $scripturl, $user_info;
+
+	$eventOptions = array(
+		'include_birthdays' => true,
+		'include_holidays' => true,
+		'include_events' => true,
+		'num_days_shown' => empty($modSettings['cal_days_for_index']) || $modSettings['cal_days_for_index'] < 1 ? 1 : $modSettings['cal_days_for_index'],
+	);
+	$return = cache_quick_get('calendar_index_offset_' . ($user_info['time_offset'] + $modSettings['time_offset']), 'Subs-Calendar.php', 'cache_getRecentEvents', array($eventOptions));
+
+	if ($output_method != 'echo')
+		return $return;
+
+	if (!empty($return['calendar_holidays']))
+		echo '
+			<span class="holiday">' . $txt['calendar_prompt'] . ' ' . implode(', ', $return['calendar_holidays']) . '<br /></span>';
+	if (!empty($return['calendar_birthdays']))
+	{
+		echo '
+			<span class="birthday">' . $txt['birthdays_upcoming'] . '</span> ';
+		foreach ($return['calendar_birthdays'] as $member)
+			echo '
+			<a href="', $scripturl, '?action=profile;u=', $member['id'], '">', $member['name'], isset($member['age']) ? ' (' . $member['age'] . ')' : '', '</a>', !$member['is_last'] ? ', ' : '';
+		echo '
+			<br />';
+	}
+	if (!empty($return['calendar_events']))
+	{
+		echo '
+			<span class="event">' . $txt['events_upcoming'] . '</span> ';
+		foreach ($return['calendar_events'] as $event)
+		{
+			if ($event['can_edit'])
+				echo '
+			<a href="' . $event['modify_href'] . '" style="color: #ff0000;">*</a> ';
+			echo '
+			' . $event['link'] . (!$event['is_last'] ? ', ' : '');
+		}
+	}
+}
+
+// Show the latest news, with a template... by board.
+function ssi_boardNews($board = null, $limit = null, $start = null, $length = null, $output_method = 'echo')
+{
+	global $scripturl, $db_prefix, $txt, $settings, $modSettings, $context;
+	global $smcFunc;
+
+	loadLanguage('Stats');
+
+	// Must be integers....
+	if ($limit === null)
+		$limit = isset($_GET['limit']) ? (int) $_GET['limit'] : 5;
+	else
+		$limit = (int) $limit;
+
+	if ($start === null)
+		$start = isset($_GET['start']) ? (int) $_GET['start'] : 0;
+	else
+		$start = (int) $start;
+
+	if ($board !== null)
+		$board = (int) $board;
+	elseif (isset($_GET['board']))
+		$board = (int) $_GET['board'];
+
+	if ($length === null)
+		$length = isset($_GET['length']) ? (int) $_GET['length'] : 0;
+	else
+		$length = (int) $length;
+
+	$limit = max(0, $limit);
+	$start = max(0, $start);
+
+	// Make sure guests can see this board.
+	$request = $smcFunc['db_query']('', '
+		SELECT id_board
+		FROM {db_prefix}boards
+		WHERE ' . ($board === null ? '' : 'id_board = {int:current_board}
+			AND ') . 'FIND_IN_SET(-1, member_groups)
+		LIMIT 1',
+		array(
+			'current_board' => $board,
+		)
+	);
+	if ($smcFunc['db_num_rows']($request) == 0)
+	{
+		if ($output_method == 'echo')
+			die($txt['ssi_no_guests']);
+		else
+			return array();
+	}
+	list ($board) = $smcFunc['db_fetch_row']($request);
+	$smcFunc['db_free_result']($request);
+
+	// Load the message icons - the usual suspects.
+	$stable_icons = array('xx', 'thumbup', 'thumbdown', 'exclamation', 'question', 'lamp', 'smiley', 'angry', 'cheesy', 'grin', 'sad', 'wink', 'moved', 'recycled', 'wireless');
+	$icon_sources = array();
+	foreach ($stable_icons as $icon)
+		$icon_sources[$icon] = 'images_url';
+
+	// Find the post ids.
+	$request = $smcFunc['db_query']('', '
+		SELECT id_first_msg
+		FROM {db_prefix}topics
+		WHERE id_board = {int:current_board}' . ($modSettings['postmod_active'] ? '
+			AND approved = {int:is_approved}' : '') . '
+		ORDER BY id_first_msg DESC
+		LIMIT ' . $start . ', ' . $limit,
+		array(
+			'current_board' => $board,
+			'is_approved' => 1,
+		)
+	);
+	$posts = array();
+	while ($row = $smcFunc['db_fetch_assoc']($request))
+		$posts[] = $row['id_first_msg'];
+	$smcFunc['db_free_result']($request);
+
+	if (empty($posts))
+		return array();
+
+	// Find the posts.
+	$request = $smcFunc['db_query']('', '
+		SELECT
+			m.icon, m.subject, m.body, IFNULL(mem.real_name, m.poster_name) AS poster_name, m.poster_time,
+			t.num_replies, t.id_topic, m.id_member, m.smileys_enabled, m.id_msg, t.locked, t.id_last_msg
+		FROM {db_prefix}topics AS t
+			INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)
+			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)
+		WHERE t.id_first_msg IN ({array_int:post_list})
+		ORDER BY t.id_first_msg DESC
+		LIMIT ' . count($posts),
+		array(
+			'post_list' => $posts,
+		)
+	);
+	$return = array();
+	while ($row = $smcFunc['db_fetch_assoc']($request))
+	{
+		// If we want to limit the length of the post.
+		if (!empty($length) && $smcFunc['strlen']($row['body']) > $length)
+		{
+			$row['body'] = $smcFunc['substr']($row['body'], 0, $length);
+
+			// The first space or line break. (<br />, etc.)
+			$cutoff = max(strrpos($row['body'], ' '), strrpos($row['body'], '<'));
+
+			if ($cutoff !== false)
+				$row['body'] = $smcFunc['substr']($row['body'], 0, $cutoff);
+			$row['body'] .= '...';
+		}
+
+		$row['body'] = parse_bbc($row['body'], $row['smileys_enabled'], $row['id_msg']);
+
+		// Check that this message icon is there...
+		if (empty($modSettings['messageIconChecks_disable']) && !isset($icon_sources[$row['icon']]))
+			$icon_sources[$row['icon']] = file_exists($settings['theme_dir'] . '/images/post/' . $row['icon'] . '.gif') ? 'images_url' : 'default_images_url';
+
+		censorText($row['subject']);
+		censorText($row['body']);
+
+		$return[] = array(
+			'id' => $row['id_topic'],
+			'message_id' => $row['id_msg'],
+			'icon' => '<img src="' . $settings[$icon_sources[$row['icon']]] . '/post/' . $row['icon'] . '.gif" alt="' . $row['icon'] . '" />',
+			'subject' => $row['subject'],
+			'time' => timeformat($row['poster_time']),
+			'timestamp' => forum_time(true, $row['poster_time']),
+			'body' => $row['body'],
+			'href' => $scripturl . '?topic=' . $row['id_topic'] . '.0',
+			'link' => '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.0">' . $row['num_replies'] . ' ' . ($row['num_replies'] == 1 ? $txt['ssi_comment'] : $txt['ssi_comments']) . '</a>',
+			'replies' => $row['num_replies'],
+			'comment_href' => !empty($row['locked']) ? '' : $scripturl . '?action=post;topic=' . $row['id_topic'] . '.' . $row['num_replies'] . ';last_msg=' . $row['id_last_msg'],
+			'comment_link' => !empty($row['locked']) ? '' : '<a href="' . $scripturl . '?action=post;topic=' . $row['id_topic'] . '.' . $row['num_replies'] . ';last_msg=' . $row['id_last_msg'] . '">' . $txt['ssi_write_comment'] . '</a>',
+			'new_comment' => !empty($row['locked']) ? '' : '<a href="' . $scripturl . '?action=post;topic=' . $row['id_topic'] . '.' . $row['num_replies'] . '">' . $txt['ssi_write_comment'] . '</a>',
+			'poster' => array(
+				'id' => $row['id_member'],
+				'name' => $row['poster_name'],
+				'href' => !empty($row['id_member']) ? $scripturl . '?action=profile;u=' . $row['id_member'] : '',
+				'link' => !empty($row['id_member']) ? '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['poster_name'] . '</a>' : $row['poster_name']
+			),
+			'locked' => !empty($row['locked']),
+			'is_last' => false
+		);
+	}
+	$smcFunc['db_free_result']($request);
+
+	if (empty($return))
+		return $return;
+
+	$return[count($return) - 1]['is_last'] = true;
+
+	if ($output_method != 'echo')
+		return $return;
+
+	foreach ($return as $news)
+	{
+		echo '
+			<div class="news_item">
+				<h3 class="news_header">
+					', $news['icon'], '
+					<a href="', $news['href'], '">', $news['subject'], '</a>
+				</h3>
+				<div class="news_timestamp">', $news['time'], ' ', $txt['by'], ' ', $news['poster']['link'], '</div>
+				<div class="news_body" style="padding: 2ex 0;">', $news['body'], '</div>
+				', $news['link'], $news['locked'] ? '' : ' | ' . $news['comment_link'], '
+			</div>';
+
+		if (!$news['is_last'])
+			echo '
+			<hr />';
+	}
+}
+
+// Show the most recent events.
+function ssi_recentEvents($max_events = 7, $output_method = 'echo')
+{
+	global $db_prefix, $user_info, $scripturl, $modSettings, $txt, $context, $smcFunc;
+
+	// Find all events which are happening in the near future that the member can see.
+	$request = $smcFunc['db_query']('', '
+		SELECT
+			cal.id_event, cal.start_date, cal.end_date, cal.title, cal.id_member, cal.id_topic,
+			cal.id_board, t.id_first_msg, t.approved
+		FROM {db_prefix}calendar AS cal
+			LEFT JOIN {db_prefix}boards AS b ON (b.id_board = cal.id_board)
+			LEFT JOIN {db_prefix}topics AS t ON (t.id_topic = cal.id_topic)
+		WHERE cal.start_date <= {date:current_date}
+			AND cal.end_date >= {date:current_date}
+			AND (cal.id_board = {int:no_board} OR {query_wanna_see_board})
+		ORDER BY cal.start_date DESC
+		LIMIT ' . $max_events,
+		array(
+			'current_date' => strftime('%Y-%m-%d', forum_time(false)),
+			'no_board' => 0,
+		)
+	);
+	$return = array();
+	$duplicates = array();
+	while ($row = $smcFunc['db_fetch_assoc']($request))
+	{
+		// Check if we've already come by an event linked to this same topic with the same title... and don't display it if we have.
+		if (!empty($duplicates[$row['title'] . $row['id_topic']]))
+			continue;
+
+		// Censor the title.
+		censorText($row['title']);
+
+		if ($row['start_date'] < strftime('%Y-%m-%d', forum_time(false)))
+			$date = strftime('%Y-%m-%d', forum_time(false));
+		else
+			$date = $row['start_date'];
+
+		// If the topic it is attached to is not approved then don't link it.
+		if (!empty($row['id_first_msg']) && !$row['approved'])
+			$row['id_board'] = $row['id_topic'] = $row['id_first_msg'] = 0;
+
+		$return[$date][] = array(
+			'id' => $row['id_event'],
+			'title' => $row['title'],
+			'can_edit' => allowedTo('calendar_edit_any') || ($row['id_member'] == $user_info['id'] && allowedTo('calendar_edit_own')),
+			'modify_href' => $scripturl . '?action=' . ($row['id_board'] == 0 ? 'calendar;sa=post;' : 'post;msg=' . $row['id_first_msg'] . ';topic=' . $row['id_topic'] . '.0;calendar;') . 'eventid=' . $row['id_event'] . ';' . $context['session_var'] . '=' . $context['session_id'],
+			'href' => $row['id_board'] == 0 ? '' : $scripturl . '?topic=' . $row['id_topic'] . '.0',
+			'link' => $row['id_board'] == 0 ? $row['title'] : '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.0">' . $row['title'] . '</a>',
+			'start_date' => $row['start_date'],
+			'end_date' => $row['end_date'],
+			'is_last' => false
+		);
+
+		// Let's not show this one again, huh?
+		$duplicates[$row['title'] . $row['id_topic']] = true;
+	}
+	$smcFunc['db_free_result']($request);
+
+	foreach ($return as $mday => $array)
+		$return[$mday][count($array) - 1]['is_last'] = true;
+
+	if ($output_method != 'echo' || empty($return))
+		return $return;
+
+	// Well the output method is echo.
+	echo '
+			<span class="event">' . $txt['events'] . '</span> ';
+	foreach ($return as $mday => $array)
+		foreach ($array as $event)
+		{
+			if ($event['can_edit'])
+				echo '
+				<a href="' . $event['modify_href'] . '" style="color: #ff0000;">*</a> ';
+
+			echo '
+				' . $event['link'] . (!$event['is_last'] ? ', ' : '');
+		}
+}
+
+// Check the passed id_member/password.  If $is_username is true, treats $id as a username.
+function ssi_checkPassword($id = null, $password = null, $is_username = false)
+{
+	global $db_prefix, $sourcedir, $smcFunc;
+
+	// If $id is null, this was most likely called from a query string and should do nothing.
+	if ($id === null)
+		return;
+
+	$request = $smcFunc['db_query']('', '
+		SELECT passwd, member_name, is_activated
+		FROM {db_prefix}members
+		WHERE ' . ($is_username ? 'member_name' : 'id_member') . ' = {string:id}
+		LIMIT 1',
+		array(
+			'id' => $id,
+		)
+	);
+	list ($pass, $user, $active) = $smcFunc['db_fetch_row']($request);
+	$smcFunc['db_free_result']($request);
+
+	return sha1(strtolower($user) . $password) == $pass && $active == 1;
+}
+
+// We want to show the recent attachments outside of the forum.
+function ssi_recentAttachments($num_attachments = 10, $attachment_ext = array(), $output_method = 'echo')
+{
+	global $smcFunc, $context, $modSettings, $scripturl, $txt, $settings;
+
+	// We want to make sure that we only get attachments for boards that we can see *if* any.
+	$attachments_boards = boardsAllowedTo('view_attachments');
+
+	// No boards?  Adios amigo.
+	if (empty($attachments_boards))
+		return array();
+
+	// Is it an array?
+	if (!is_array($attachment_ext))
+		$attachment_ext = array($attachment_ext);
+
+	// Lets build the query.
+	$request = $smcFunc['db_query']('', '
+		SELECT
+			att.id_attach, att.id_msg, att.filename, IFNULL(att.size, 0) AS filesize, att.downloads, mem.id_member,
+			IFNULL(mem.real_name, m.poster_name) AS poster_name, m.id_topic, m.subject, t.id_board, m.poster_time,
+			att.width, att.height' . (empty($modSettings['attachmentShowImages']) || empty($modSettings['attachmentThumbnails']) ? '' : ', IFNULL(thumb.id_attach, 0) AS id_thumb, thumb.width AS thumb_width, thumb.height AS thumb_height') . '
+		FROM {db_prefix}attachments AS att
+			INNER JOIN {db_prefix}messages AS m ON (m.id_msg = att.id_msg)
+			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic)
+			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)' . (empty($modSettings['attachmentShowImages']) || empty($modSettings['attachmentThumbnails']) ? '' : '
+			LEFT JOIN {db_prefix}attachments AS thumb ON (thumb.id_attach = att.id_thumb)') . '
+		WHERE att.attachment_type = 0' . ($attachments_boards === array(0) ? '' : '
+			AND m.id_board IN ({array_int:boards_can_see})') . (!empty($attachment_ext) ? '
+			AND att.fileext IN ({array_string:attachment_ext})' : '') .
+			(!$modSettings['postmod_active'] || allowedTo('approve_posts') ? '' : '
+			AND t.approved = {int:is_approved}
+			AND m.approved = {int:is_approved}
+			AND att.approved = {int:is_approved}') . '
+		ORDER BY att.id_attach DESC
+		LIMIT {int:num_attachments}',
+		array(
+			'boards_can_see' => $attachments_boards,
+			'attachment_ext' => $attachment_ext,
+			'num_attachments' => $num_attachments,
+			'is_approved' => 1,
+		)
+	);
+
+	// We have something.
+	$attachments = array();
+	while ($row = $smcFunc['db_fetch_assoc']($request))
+	{
+		$filename = preg_replace('~&amp;#(\\d{1,7}|x[0-9a-fA-F]{1,6});~', '&#\\1;', htmlspecialchars($row['filename']));
+
+		// Is it an image?
+		$attachments[$row['id_attach']] = array(
+			'member' => array(
+				'id' => $row['id_member'],
+				'name' => $row['poster_name'],
+				'link' => empty($row['id_member']) ? $row['poster_name'] : '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['poster_name'] . '</a>',
+			),
+			'file' => array(
+				'filename' => $filename,
+				'filesize' => round($row['filesize'] /1024, 2) . $txt['kilobyte'],
+				'downloads' => $row['downloads'],
+				'href' => $scripturl . '?action=dlattach;topic=' . $row['id_topic'] . '.0;attach=' . $row['id_attach'],
+				'link' => '<img src="' . $settings['images_url'] . '/icons/clip.gif" alt="" /> <a href="' . $scripturl . '?action=dlattach;topic=' . $row['id_topic'] . '.0;attach=' . $row['id_attach'] . '">' . $filename . '</a>',
+				'is_image' => !empty($row['width']) && !empty($row['height']) && !empty($modSettings['attachmentShowImages']),
+			),
+			'topic' => array(
+				'id' => $row['id_topic'],
+				'subject' => $row['subject'],
+				'href' => $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['id_msg'] . '#msg' . $row['id_msg'],
+				'link' => '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['id_msg'] . '#msg' . $row['id_msg'] . '">' . $row['subject'] . '</a>',
+				'time' => timeformat($row['poster_time']),
+			),
+		);
+
+		// Images.
+		if ($attachments[$row['id_attach']]['file']['is_image'])
+		{
+			$id_thumb = empty($row['id_thumb']) ? $row['id_attach'] : $row['id_thumb'];
+			$attachments[$row['id_attach']]['file']['image'] = array(
+				'id' => $id_thumb,
+				'width' => $row['width'],
+				'height' => $row['height'],
+				'img' => '<img src="' . $scripturl . '?action=dlattach;topic=' . $row['id_topic'] . '.0;attach=' . $row['id_attach'] . ';image" alt="' . $filename . '" />',
+				'thumb' => '<img src="' . $scripturl . '?action=dlattach;topic=' . $row['id_topic'] . '.0;attach=' . $id_thumb . ';image" alt="' . $filename . '" />',
+				'href' => $scripturl . '?action=dlattach;topic=' . $row['id_topic'] . '.0;attach=' . $id_thumb . ';image',
+				'link' => '<a href="' . $scripturl . '?action=dlattach;topic=' . $row['id_topic'] . '.0;attach=' . $row['id_attach'] . ';image"><img src="' . $scripturl . '?action=dlattach;topic=' . $row['id_topic'] . '.0;attach=' . $id_thumb . ';image" alt="' . $filename . '" /></a>',
+			);
+		}
+	}
+	$smcFunc['db_free_result']($request);
+
+	// So you just want an array?  Here you can have it.
+	if ($output_method == 'array' || empty($attachments))
+		return $attachments;
+
+	// Give them the default.
+	echo '
+		<table class="ssi_downloads" cellpadding="2">
+			<tr>
+				<th align="left">', $txt['file'], '</th>
+				<th align="left">', $txt['posted_by'], '</th>
+				<th align="left">', $txt['downloads'], '</th>
+				<th align="left">', $txt['filesize'], '</th>
+			</tr>';
+	foreach ($attachments as $attach)
+		echo '
+			<tr>
+				<td>', $attach['file']['link'], '</td>
+				<td>', $attach['member']['link'], '</td>
+				<td align="center">', $attach['file']['downloads'], '</td>
+				<td>', $attach['file']['filesize'], '</td>
+			</tr>';
+	echo '
+		</table>';
+}
+
+?>

BIN
Smileys/aaron/afro.gif


BIN
Smileys/aaron/angel.gif


BIN
Smileys/aaron/angry.gif


BIN
Smileys/aaron/azn.gif


BIN
Smileys/aaron/blank.gif


BIN
Smileys/aaron/cheesy.gif


BIN
Smileys/aaron/cool.gif


BIN
Smileys/aaron/cry.gif


BIN
Smileys/aaron/embarrassed.gif


BIN
Smileys/aaron/evil.gif


BIN
Smileys/aaron/grin.gif


BIN
Smileys/aaron/huh.gif


+ 9 - 0
Smileys/aaron/index.php

@@ -0,0 +1,9 @@
+<?php
+
+// Try to handle it with the upper level index.php.  (it should know what to do.)
+if (file_exists(dirname(dirname(__FILE__)) . '/index.php'))
+	include (dirname(dirname(__FILE__)) . '/index.php');
+else
+	exit;
+
+?>

BIN
Smileys/aaron/kiss.gif


BIN
Smileys/aaron/laugh.gif


BIN
Smileys/aaron/lipsrsealed.gif


BIN
Smileys/aaron/police.gif


BIN
Smileys/aaron/rolleyes.gif


BIN
Smileys/aaron/sad.gif


BIN
Smileys/aaron/shocked.gif


BIN
Smileys/aaron/smiley.gif


BIN
Smileys/aaron/tongue.gif


BIN
Smileys/aaron/undecided.gif


BIN
Smileys/aaron/wink.gif


BIN
Smileys/akyhne/afro.gif


BIN
Smileys/akyhne/angel.gif


BIN
Smileys/akyhne/angry.gif


BIN
Smileys/akyhne/azn.gif


BIN
Smileys/akyhne/blank.gif


BIN
Smileys/akyhne/cheesy.gif


BIN
Smileys/akyhne/cool.gif


BIN
Smileys/akyhne/cry.gif


BIN
Smileys/akyhne/embarrassed.gif


BIN
Smileys/akyhne/evil.gif


BIN
Smileys/akyhne/grin.gif


BIN
Smileys/akyhne/huh.gif


+ 9 - 0
Smileys/akyhne/index.php

@@ -0,0 +1,9 @@
+<?php
+
+// Try to handle it with the upper level index.php.  (it should know what to do.)
+if (file_exists(dirname(dirname(__FILE__)) . '/index.php'))
+	include (dirname(dirname(__FILE__)) . '/index.php');
+else
+	exit;
+
+?>

BIN
Smileys/akyhne/kiss.gif


BIN
Smileys/akyhne/laugh.gif


BIN
Smileys/akyhne/lipsrsealed.gif


BIN
Smileys/akyhne/police.gif


BIN
Smileys/akyhne/rolleyes.gif


BIN
Smileys/akyhne/sad.gif


BIN
Smileys/akyhne/shocked.gif


BIN
Smileys/akyhne/smiley.gif


BIN
Smileys/akyhne/tongue.gif


BIN
Smileys/akyhne/undecided.gif


BIN
Smileys/akyhne/wink.gif


BIN
Smileys/default/afro.gif


BIN
Smileys/default/angel.gif


BIN
Smileys/default/angry.gif


BIN
Smileys/default/azn.gif


BIN
Smileys/default/blank.gif


BIN
Smileys/default/cheesy.gif


BIN
Smileys/default/cool.gif


BIN
Smileys/default/cry.gif


BIN
Smileys/default/embarrassed.gif


BIN
Smileys/default/evil.gif


BIN
Smileys/default/grin.gif


BIN
Smileys/default/huh.gif


+ 9 - 0
Smileys/default/index.php

@@ -0,0 +1,9 @@
+<?php
+
+// Try to handle it with the upper level index.php.  (it should know what to do.)
+if (file_exists(dirname(dirname(__FILE__)) . '/index.php'))
+	include (dirname(dirname(__FILE__)) . '/index.php');
+else
+	exit;
+
+?>

BIN
Smileys/default/kiss.gif


BIN
Smileys/default/laugh.gif


BIN
Smileys/default/lipsrsealed.gif


BIN
Smileys/default/police.gif


BIN
Smileys/default/rolleyes.gif


BIN
Smileys/default/sad.gif


BIN
Smileys/default/shocked.gif


BIN
Smileys/default/smiley.gif


BIN
Smileys/default/tongue.gif


BIN
Smileys/default/undecided.gif


BIN
Smileys/default/wink.gif


+ 16 - 0
Smileys/index.php

@@ -0,0 +1,16 @@
+<?php
+
+// This file is here solely to protect your Smileys directory.
+
+// Look for Settings.php....
+if (file_exists(dirname(dirname(__FILE__)) . '/Settings.php'))
+{
+	// Found it!
+	require(dirname(dirname(__FILE__)) . '/Settings.php');
+	header('Location: ' . $boardurl);
+}
+// Can't find it... just forget it.
+else
+	exit;
+
+?>

+ 999 - 0
Sources/Admin.php

@@ -0,0 +1,999 @@
+<?php
+
+/**
+ * Simple Machines Forum (SMF)
+ *
+ * @package SMF
+ * @author Simple Machines
+ *
+ * @copyright 2011 Simple Machines
+ * @license http://www.simplemachines.org/about/smf/license.php BSD
+ *
+ * @version 2.0
+ */
+
+if (!defined('SMF'))
+	die('Hacking attempt...');
+
+/*	This file, unpredictable as this might be, handles basic administration.
+
+	void AdminMain()
+		- initialises all the basic context required for the admin center.
+		- passes execution onto the relevant admin section.
+		- if the passed section is not found it shows the admin home page.
+
+	void AdminHome()
+		- prepares all the data necessary for the administration front page.
+		- uses the Admin template along with the admin sub template.
+		- requires the moderate_forum, manage_membergroups, manage_bans,
+		  admin_forum, manage_permissions, manage_attachments, manage_smileys,
+		  manage_boards, edit_news, or send_mail permission.
+		- uses the index administrative area.
+		- can be found by going to ?action=admin.
+
+	void AdminSearch()
+		- allocates out all the search stuff.
+
+	void AdminSearchInternal()
+		- a complicated but relatively quick internal search.
+
+	void AdminSearchMember()
+		- pass through to manage members.
+
+	void DisplayAdminFile()
+		- get one of the admin information files from Simple Machines.
+
+*/
+
+// The main admin handling function.
+function AdminMain()
+{
+	global $txt, $context, $scripturl, $sc, $modSettings, $user_info, $settings, $sourcedir, $options, $smcFunc, $boarddir;
+
+	// Load the language and templates....
+	loadLanguage('Admin');
+	loadTemplate('Admin', 'admin');
+
+	// No indexing evil stuff.
+	$context['robot_no_index'] = true;
+
+	require_once($sourcedir . '/Subs-Menu.php');
+
+	// Some preferences.
+	$context['admin_preferences'] = !empty($options['admin_preferences']) ? unserialize($options['admin_preferences']) : array();
+
+	// Define all the menu structure - see Subs-Menu.php for details!
+	$admin_areas = array(
+		'forum' => array(
+			'title' => $txt['admin_main'],
+			'permission' => array('admin_forum', 'manage_permissions', 'moderate_forum', 'manage_membergroups', 'manage_bans', 'send_mail', 'edit_news', 'manage_boards', 'manage_smileys', 'manage_attachments'),
+			'areas' => array(
+				'index' => array(
+					'label' => $txt['admin_center'],
+					'function' => 'AdminHome',
+					'icon' => 'administration.gif',
+				),
+				'credits' => array(
+					'label' => $txt['support_credits_title'],
+					'function' => 'AdminHome',
+					'icon' => 'support.gif',
+				),
+				'news' => array(
+					'label' => $txt['news_title'],
+					'file' => 'ManageNews.php',
+					'function' => 'ManageNews',
+					'icon' => 'news.gif',
+					'permission' => array('edit_news', 'send_mail', 'admin_forum'),
+					'subsections' => array(
+						'editnews' => array($txt['admin_edit_news'], 'edit_news'),
+						'mailingmembers' => array($txt['admin_newsletters'], 'send_mail'),
+						'settings' => array($txt['settings'], 'admin_forum'),
+					),
+				),
+				'packages' => array(
+					'label' => $txt['package'],
+					'file' => 'Packages.php',
+					'function' => 'Packages',
+					'permission' => array('admin_forum'),
+					'icon' => 'packages.gif',
+					'subsections' => array(
+						'browse' => array($txt['browse_packages']),
+						'packageget' => array($txt['download_packages'], 'url' => $scripturl . '?action=admin;area=packages;sa=packageget;get'),
+						'installed' => array($txt['installed_packages']),
+						'perms' => array($txt['package_file_perms']),
+						'options' => array($txt['package_settings']),
+					),
+				),
+				'search' => array(
+					'function' => 'AdminSearch',
+					'permission' => array('admin_forum'),
+					'select' => 'index'
+				),
+			),
+		),
+		'config' => array(
+			'title' => $txt['admin_config'],
+			'permission' => array('admin_forum'),
+			'areas' => array(
+				'corefeatures' => array(
+					'label' => $txt['core_settings_title'],
+					'file' => 'ManageSettings.php',
+					'function' => 'ModifyCoreFeatures',
+					'icon' => 'corefeatures.gif',
+				),
+				'featuresettings' => array(
+					'label' => $txt['modSettings_title'],
+					'file' => 'ManageSettings.php',
+					'function' => 'ModifyFeatureSettings',
+					'icon' => 'features.gif',
+					'subsections' => array(
+						'basic' => array($txt['mods_cat_features']),
+						'layout' => array($txt['mods_cat_layout']),
+						'karma' => array($txt['karma'], 'enabled' => in_array('k', $context['admin_features'])),
+						'sig' => array($txt['signature_settings_short']),
+						'profile' => array($txt['custom_profile_shorttitle'], 'enabled' => in_array('cp', $context['admin_features'])),
+					),
+				),
+				'securitysettings' => array(
+					'label' => $txt['admin_security_moderation'],
+					'file' => 'ManageSettings.php',
+					'function' => 'ModifySecuritySettings',
+					'icon' => 'security.gif',
+					'subsections' => array(
+						'general' => array($txt['mods_cat_security_general']),
+						'spam' => array($txt['antispam_title']),
+						'moderation' => array($txt['moderation_settings_short'], 'enabled' => substr($modSettings['warning_settings'], 0, 1) == 1),
+					),
+				),
+				'languages' => array(
+					'label' => $txt['language_configuration'],
+					'file' => 'ManageServer.php',
+					'function' => 'ManageLanguages',
+					'icon' => 'languages.gif',
+					'subsections' => array(
+						'edit' => array($txt['language_edit']),
+						'add' => array($txt['language_add']),
+						'settings' => array($txt['language_settings']),
+					),
+				),
+				'serversettings' => array(
+					'label' => $txt['admin_server_settings'],
+					'file' => 'ManageServer.php',
+					'function' => 'ModifySettings',
+					'icon' => 'server.gif',
+					'subsections' => array(
+						'general' => array($txt['general_settings']),
+						'database' => array($txt['database_paths_settings']),
+						'cookie' => array($txt['cookies_sessions_settings']),
+						'cache' => array($txt['caching_settings']),
+						'loads' => array($txt['load_balancing_settings']),
+					),
+				),
+				'current_theme' => array(
+					'label' => $txt['theme_current_settings'],
+					'file' => 'Themes.php',
+					'function' => 'ThemesMain',
+					'custom_url' => $scripturl . '?action=admin;area=theme;sa=settings;th=' . $settings['theme_id'],
+					'icon' => 'current_theme.gif',
+				),
+				'theme' => array(
+					'label' => $txt['theme_admin'],
+					'file' => 'Themes.php',
+					'function' => 'ThemesMain',
+					'custom_url' => $scripturl . '?action=admin;area=theme;sa=admin',
+					'icon' => 'themes.gif',
+					'subsections' => array(
+						'admin' => array($txt['themeadmin_admin_title']),
+						'list' => array($txt['themeadmin_list_title']),
+						'reset' => array($txt['themeadmin_reset_title']),
+						'edit' => array($txt['themeadmin_edit_title']),
+					),
+				),
+				'modsettings' => array(
+					'label' => $txt['admin_modifications'],
+					'file' => 'ManageSettings.php',
+					'function' => 'ModifyModSettings',
+					'icon' => 'modifications.gif',
+					'subsections' => array(
+						'general' => array($txt['mods_cat_modifications_misc']),
+						// Mod Authors for a "ADD AFTER" on this line. Ensure you end your change with a comma. For example:
+						// 'shout' => array($txt['shout']),
+						// Note the comma!! The setting with automatically appear with the first mod to be added.
+					),
+				),
+			),
+		),
+		'layout' => array(
+			'title' => $txt['layout_controls'],
+			'permission' => array('manage_boards', 'admin_forum', 'manage_smileys', 'manage_attachments', 'moderate_forum'),
+			'areas' => array(
+				'manageboards' => array(
+					'label' => $txt['admin_boards'],
+					'file' => 'ManageBoards.php',
+					'function' => 'ManageBoards',
+					'icon' => 'boards.gif',
+					'permission' => array('manage_boards'),
+					'subsections' => array(
+						'main' => array($txt['boardsEdit']),
+						'newcat' => array($txt['mboards_new_cat']),
+						'settings' => array($txt['settings'], 'admin_forum'),
+					),
+				),
+				'postsettings' => array(
+					'label' => $txt['manageposts'],
+					'file' => 'ManagePosts.php',
+					'function' => 'ManagePostSettings',
+					'permission' => array('admin_forum'),
+					'icon' => 'posts.gif',
+					'subsections' => array(
+						'posts' => array($txt['manageposts_settings']),
+						'bbc' => array($txt['manageposts_bbc_settings']),
+						'censor' => array($txt['admin_censored_words']),
+						'topics' => array($txt['manageposts_topic_settings']),
+					),
+				),
+				'managecalendar' => array(
+					'label' => $txt['manage_calendar'],
+					'file' => 'ManageCalendar.php',
+					'function' => 'ManageCalendar',
+					'icon' => 'calendar.gif',
+					'permission' => array('admin_forum'),
+					'enabled' => in_array('cd', $context['admin_features']),
+					'subsections' => array(
+						'holidays' => array($txt['manage_holidays'], 'admin_forum', 'enabled' => !empty($modSettings['cal_enabled'])),
+						'settings' => array($txt['calendar_settings'], 'admin_forum'),
+					),
+				),
+				'managesearch' => array(
+					'label' => $txt['manage_search'],
+					'file' => 'ManageSearch.php',
+					'function' => 'ManageSearch',
+					'icon' => 'search.gif',
+					'permission' => array('admin_forum'),
+					'subsections' => array(
+						'weights' => array($txt['search_weights']),
+						'method' => array($txt['search_method']),
+						'settings' => array($txt['settings']),
+					),
+				),
+				'smileys' => array(
+					'label' => $txt['smileys_manage'],
+					'file' => 'ManageSmileys.php',
+					'function' => 'ManageSmileys',
+					'icon' => 'smiley.gif',
+					'permission' => array('manage_smileys'),
+					'subsections' => array(
+						'editsets' => array($txt['smiley_sets']),
+						'addsmiley' => array($txt['smileys_add'], 'enabled' => !empty($modSettings['smiley_enable'])),
+						'editsmileys' => array($txt['smileys_edit'], 'enabled' => !empty($modSettings['smiley_enable'])),
+						'setorder' => array($txt['smileys_set_order'], 'enabled' => !empty($modSettings['smiley_enable'])),
+						'editicons' => array($txt['icons_edit_message_icons'], 'enabled' => !empty($modSettings['messageIcons_enable'])),
+						'settings' => array($txt['settings']),
+					),
+				),
+				'manageattachments' => array(
+					'label' => $txt['attachments_avatars'],
+					'file' => 'ManageAttachments.php',
+					'function' => 'ManageAttachments',
+					'icon' => 'attachment.gif',
+					'permission' => array('manage_attachments'),
+					'subsections' => array(
+						'browse' => array($txt['attachment_manager_browse']),
+						'attachments' => array($txt['attachment_manager_settings']),
+						'avatars' => array($txt['attachment_manager_avatar_settings']),
+						'maintenance' => array($txt['attachment_manager_maintenance']),
+					),
+				),
+			),
+		),
+		'members' => array(
+			'title' => $txt['admin_manage_members'],
+			'permission' => array('moderate_forum', 'manage_membergroups', 'manage_bans', 'manage_permissions', 'admin_forum'),
+			'areas' => array(
+				'viewmembers' => array(
+					'label' => $txt['admin_users'],
+					'file' => 'ManageMembers.php',
+					'function' => 'ViewMembers',
+					'icon' => 'members.gif',
+					'permission' => array('moderate_forum'),
+					'subsections' => array(
+						'all' => array($txt['view_all_members']),
+						'search' => array($txt['mlist_search']),
+					),
+				),
+				'membergroups' => array(
+					'label' => $txt['admin_groups'],
+					'file' => 'ManageMembergroups.php',
+					'function' => 'ModifyMembergroups',
+					'icon' => 'membergroups.gif',
+					'permission' => array('manage_membergroups'),
+					'subsections' => array(
+						'index' => array($txt['membergroups_edit_groups'], 'manage_membergroups'),
+						'add' => array($txt['membergroups_new_group'], 'manage_membergroups'),
+						'settings' => array($txt['settings'], 'admin_forum'),
+					),
+				),
+				'permissions' => array(
+					'label' => $txt['edit_permissions'],
+					'file' => 'ManagePermissions.php',
+					'function' => 'ModifyPermissions',
+					'icon' => 'permissions.gif',
+					'permission' => array('manage_permissions'),
+					'subsections' => array(
+						'index' => array($txt['permissions_groups'], 'manage_permissions'),
+						'board' => array($txt['permissions_boards'], 'manage_permissions'),
+						'profiles' => array($txt['permissions_profiles'], 'manage_permissions'),
+						'postmod' => array($txt['permissions_post_moderation'], 'manage_permissions', 'enabled' => $modSettings['postmod_active']),
+						'settings' => array($txt['settings'], 'admin_forum'),
+					),
+				),
+				'regcenter' => array(
+					'label' => $txt['registration_center'],
+					'file' => 'ManageRegistration.php',
+					'function' => 'RegCenter',
+					'icon' => 'regcenter.gif',
+					'permission' => array('admin_forum', 'moderate_forum'),
+					'subsections' => array(
+						'register' => array($txt['admin_browse_register_new'], 'moderate_forum'),
+						'agreement' => array($txt['registration_agreement'], 'admin_forum'),
+						'reservednames' => array($txt['admin_reserved_set'], 'admin_forum'),
+						'settings' => array($txt['settings'], 'admin_forum'),
+					),
+				),
+				'ban' => array(
+					'label' => $txt['ban_title'],
+					'file' => 'ManageBans.php',
+					'function' => 'Ban',
+					'icon' => 'ban.gif',
+					'permission' => 'manage_bans',
+					'subsections' => array(
+						'list' => array($txt['ban_edit_list']),
+						'add' => array($txt['ban_add_new']),
+						'browse' => array($txt['ban_trigger_browse']),
+						'log' => array($txt['ban_log']),
+					),
+				),
+				'paidsubscribe' => array(
+					'label' => $txt['paid_subscriptions'],
+					'enabled' => in_array('ps', $context['admin_features']),
+					'file' => 'ManagePaid.php',
+					'icon' => 'paid.gif',
+					'function' => 'ManagePaidSubscriptions',
+					'permission' => 'admin_forum',
+					'subsections' => array(
+						'view' => array($txt['paid_subs_view']),
+						'settings' => array($txt['settings']),
+					),
+				),
+				'sengines' => array(
+					'label' => $txt['search_engines'],
+					'enabled' => in_array('sp', $context['admin_features']),
+					'file' => 'ManageSearchEngines.php',
+					'icon' => 'engines.gif',
+					'function' => 'SearchEngines',
+					'permission' => 'admin_forum',
+					'subsections' => array(
+						'stats' => array($txt['spider_stats']),
+						'logs' => array($txt['spider_logs']),
+						'spiders' => array($txt['spiders']),
+						'settings' => array($txt['settings']),
+					),
+				),
+			),
+		),
+		'maintenance' => array(
+			'title' => $txt['admin_maintenance'],
+			'permission' => array('admin_forum'),
+			'areas' => array(
+				'maintain' => array(
+					'label' => $txt['maintain_title'],
+					'file' => 'ManageMaintenance.php',
+					'icon' => 'maintain.gif',
+					'function' => 'ManageMaintenance',
+					'subsections' => array(
+						'routine' => array($txt['maintain_sub_routine'], 'admin_forum'),
+						'database' => array($txt['maintain_sub_database'], 'admin_forum'),
+						'members' => array($txt['maintain_sub_members'], 'admin_forum'),
+						'topics' => array($txt['maintain_sub_topics'], 'admin_forum'),
+					),
+				),
+				'scheduledtasks' => array(
+					'label' => $txt['maintain_tasks'],
+					'file' => 'ManageScheduledTasks.php',
+					'icon' => 'scheduled.gif',
+					'function' => 'ManageScheduledTasks',
+					'subsections' => array(
+						'tasks' => array($txt['maintain_tasks'], 'admin_forum'),
+						'tasklog' => array($txt['scheduled_log'], 'admin_forum'),
+					),
+				),
+				'mailqueue' => array(
+					'label' => $txt['mailqueue_title'],
+					'file' => 'ManageMail.php',
+					'function' => 'ManageMail',
+					'icon' => 'mail.gif',
+					'subsections' => array(
+						'browse' => array($txt['mailqueue_browse'], 'admin_forum'),
+						'settings' => array($txt['mailqueue_settings'], 'admin_forum'),
+					),
+				),
+				'reports' => array(
+					'enabled' => in_array('rg', $context['admin_features']),
+					'label' => $txt['generate_reports'],
+					'file' => 'Reports.php',
+					'function' => 'ReportsMain',
+					'icon' => 'reports.gif',
+				),
+				'logs' => array(
+					'label' => $txt['logs'],
+					'function' => 'AdminLogs',
+					'icon' => 'logs.gif',
+					'subsections' => array(
+						'errorlog' => array($txt['errlog'], 'admin_forum', 'enabled' => !empty($modSettings['enableErrorLogging']), 'url' => $scripturl . '?action=admin;area=logs;sa=errorlog;desc'),
+						'adminlog' => array($txt['admin_log'], 'admin_forum', 'enabled' => in_array('ml', $context['admin_features'])),
+						'modlog' => array($txt['moderation_log'], 'admin_forum', 'enabled' => in_array('ml', $context['admin_features'])),
+						'banlog' => array($txt['ban_log'], 'manage_bans'),
+						'spiderlog' => array($txt['spider_logs'], 'admin_forum', 'enabled' => in_array('sp', $context['admin_features'])),
+						'tasklog' => array($txt['scheduled_log'], 'admin_forum'),
+						'pruning' => array($txt['pruning_title'], 'admin_forum'),
+					),
+				),
+				'repairboards' => array(
+					'label' => $txt['admin_repair'],
+					'file' => 'RepairBoards.php',
+					'function' => 'RepairBoards',
+					'select' => 'maintain',
+					'hidden' => true,
+				),
+			),
+		),
+	);
+
+	// Any files to include for administration?
+	if (!empty($modSettings['integrate_admin_include']))
+	{
+		$admin_includes = explode(',', $modSettings['integrate_admin_include']);
+		foreach ($admin_includes as $include)
+		{
+			$include = strtr(trim($include), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir']));
+			if (file_exists($include))
+				require_once($include);
+		}
+	}
+
+	// Let them modify admin areas easily.
+	call_integration_hook('integrate_admin_areas', array(&$admin_areas));
+
+	// Make sure the administrator has a valid session...
+	validateSession();
+
+	// Actually create the menu!
+	$admin_include_data = createMenu($admin_areas);
+	unset($admin_areas);
+
+	// Nothing valid?
+	if ($admin_include_data == false)
+		fatal_lang_error('no_access', false);
+
+	// Build the link tree.
+	$context['linktree'][] = array(
+		'url' => $scripturl . '?action=admin',
+		'name' => $txt['admin_center'],
+	);
+	if (isset($admin_include_data['current_area']) && $admin_include_data['current_area'] != 'index')
+		$context['linktree'][] = array(
+			'url' => $scripturl . '?action=admin;area=' . $admin_include_data['current_area'] . ';' . $context['session_var'] . '=' . $context['session_id'],
+			'name' => $admin_include_data['label'],
+		);
+	if (!empty($admin_include_data['current_subsection']) && $admin_include_data['subsections'][$admin_include_data['current_subsection']][0] != $admin_include_data['label'])
+		$context['linktree'][] = array(
+			'url' => $scripturl . '?action=admin;area=' . $admin_include_data['current_area'] . ';sa=' . $admin_include_data['current_subsection'] . ';' . $context['session_var'] . '=' . $context['session_id'],
+			'name' => $admin_include_data['subsections'][$admin_include_data['current_subsection']][0],
+		);
+
+	// Make a note of the Unique ID for this menu.
+	$context['admin_menu_id'] = $context['max_menu_id'];
+	$context['admin_menu_name'] = 'menu_data_' . $context['admin_menu_id'];
+
+	// Why on the admin are we?
+	$context['admin_area'] = $admin_include_data['current_area'];
+
+	// Now - finally - call the right place!
+	if (isset($admin_include_data['file']))
+		require_once($sourcedir . '/' . $admin_include_data['file']);
+
+	$admin_include_data['function']();
+}
+
+// The main administration section.
+function AdminHome()
+{
+	global $sourcedir, $forum_version, $txt, $scripturl, $context, $user_info, $boardurl, $modSettings, $smcFunc;
+
+	// You have to be able to do at least one of the below to see this page.
+	isAllowedTo(array('admin_forum', 'manage_permissions', 'moderate_forum', 'manage_membergroups', 'manage_bans', 'send_mail', 'edit_news', 'manage_boards', 'manage_smileys', 'manage_attachments'));
+
+	// Find all of this forum's administrators...
+	require_once($sourcedir . '/Subs-Membergroups.php');
+	if (listMembergroupMembers_Href($context['administrators'], 1, 32) && allowedTo('manage_membergroups'))
+	{
+		// Add a 'more'-link if there are more than 32.
+		$context['more_admins_link'] = '<a href="' . $scripturl . '?action=moderate;area=viewgroups;sa=members;group=1">' . $txt['more'] . '</a>';
+	}
+
+	// Load the credits stuff.
+	require_once($sourcedir . '/Who.php');
+	Credits(true);
+
+	// This makes it easier to get the latest news with your time format.
+	$context['time_format'] = urlencode($user_info['time_format']);
+
+	$context['current_versions'] = array(
+		'php' => array('title' => $txt['support_versions_php'], 'version' => PHP_VERSION),
+		'db' => array('title' => sprintf($txt['support_versions_db'], $smcFunc['db_title']), 'version' => ''),
+		'server' => array('title' => $txt['support_versions_server'], 'version' => $_SERVER['SERVER_SOFTWARE']),
+	);
+	$context['forum_version'] = $forum_version;
+
+	// Get a list of current server versions.
+	require_once($sourcedir . '/Subs-Admin.php');
+	$checkFor = array(
+		'gd',
+		'db_server',
+		'mmcache',
+		'eaccelerator',
+		'phpa',
+		'apc',
+		'memcache',
+		'xcache',
+		'php',
+		'server',
+	);
+	$context['current_versions'] = getServerVersions($checkFor);
+
+	$context['can_admin'] = allowedTo('admin_forum');
+
+	$context['sub_template'] = $context['admin_area'] == 'credits' ? 'credits' : 'admin';
+	$context['page_title'] = $context['admin_area'] == 'credits' ? $txt['support_credits_title'] : $txt['admin_center'];
+
+	// The format of this array is: permission, action, title, description, icon.
+	$quick_admin_tasks = array(
+		array('', 'credits', 'support_credits_title', 'support_credits_info', 'support_and_credits.png'),
+		array('admin_forum', 'featuresettings', 'modSettings_title', 'modSettings_info', 'features_and_options.png'),
+		array('admin_forum', 'maintain', 'maintain_title', 'maintain_info', 'forum_maintenance.png'),
+		array('manage_permissions', 'permissions', 'edit_permissions', 'edit_permissions_info', 'permissions.png'),
+		array('admin_forum', 'theme;sa=admin;' . $context['session_var'] . '=' . $context['session_id'], 'theme_admin', 'theme_admin_info', 'themes_and_layout.png'),
+		array('admin_forum', 'packages', 'package', 'package_info', 'packages.png'),
+		array('manage_smileys', 'smileys', 'smileys_manage', 'smileys_manage_info', 'smilies_and_messageicons.png'),
+		array('moderate_forum', 'viewmembers', 'admin_users', 'member_center_info', 'members.png'),
+	);
+
+	$context['quick_admin_tasks'] = array();
+	foreach ($quick_admin_tasks as $task)
+	{
+		if (!empty($task[0]) && !allowedTo($task[0]))
+			continue;
+
+		$context['quick_admin_tasks'][] = array(
+			'href' => $scripturl . '?action=admin;area=' . $task[1],
+			'link' => '<a href="' . $scripturl . '?action=admin;area=' . $task[1] . '">' . $txt[$task[2]] . '</a>',
+			'title' => $txt[$task[2]],
+			'description' => $txt[$task[3]],
+			'icon' => $task[4],
+			'is_last' => false
+		);
+	}
+
+	if (count($context['quick_admin_tasks']) % 2 == 1)
+	{
+		$context['quick_admin_tasks'][] = array(
+			'href' => '',
+			'link' => '',
+			'title' => '',
+			'description' => '',
+			'is_last' => true
+		);
+		$context['quick_admin_tasks'][count($context['quick_admin_tasks']) - 2]['is_last'] = true;
+	}
+	elseif (count($context['quick_admin_tasks']) != 0)
+	{
+		$context['quick_admin_tasks'][count($context['quick_admin_tasks']) - 1]['is_last'] = true;
+		$context['quick_admin_tasks'][count($context['quick_admin_tasks']) - 2]['is_last'] = true;
+	}
+
+	// Lastly, fill in the blanks in the support resources paragraphs.
+	$txt['support_resources_p1'] = sprintf($txt['support_resources_p1'],
+		'http://wiki.simplemachines.org/',
+		'http://wiki.simplemachines.org/smf/features2',
+		'http://wiki.simplemachines.org/smf/options2',
+		'http://wiki.simplemachines.org/smf/themes2',
+		'http://wiki.simplemachines.org/smf/packages2'
+	);
+	$txt['support_resources_p2'] = sprintf($txt['support_resources_p2'],
+		'http://www.simplemachines.org/community/',
+		'http://www.simplemachines.org/redirect/english_support',
+		'http://www.simplemachines.org/redirect/international_support_boards',
+		'http://www.simplemachines.org/redirect/smf_support',
+		'http://www.simplemachines.org/redirect/customize_support'
+	);
+}
+
+// Get one of the admin information files from Simple Machines.
+function DisplayAdminFile()
+{
+	global $context, $modSettings, $smcFunc;
+
+	@ini_set('memory_limit', '32M');
+
+	if (empty($_REQUEST['filename']) || !is_string($_REQUEST['filename']))
+		fatal_lang_error('no_access', false);
+
+	$request = $smcFunc['db_query']('', '
+		SELECT data, filetype
+		FROM {db_prefix}admin_info_files
+		WHERE filename = {string:current_filename}
+		LIMIT 1',
+		array(
+			'current_filename' => $_REQUEST['filename'],
+		)
+	);
+
+	if ($smcFunc['db_num_rows']($request) == 0)
+		fatal_lang_error('admin_file_not_found', true, array($_REQUEST['filename']));
+
+	list ($file_data, $filetype) = $smcFunc['db_fetch_row']($request);
+	$smcFunc['db_free_result']($request);
+
+	// !!! Temp.
+	// Figure out if sesc is still being used.
+	if (strpos($file_data, ';sesc=') !== false)
+		$file_data = '
+if (!(\'smfForum_sessionvar\' in window))
+	window.smfForum_sessionvar = \'sesc\';
+' . strtr($file_data, array(';sesc=' => ';\' + window.smfForum_sessionvar + \'='));
+
+	$context['template_layers'] = array();
+	// Lets make sure we aren't going to output anything nasty.
+	@ob_end_clean();
+	if (!empty($modSettings['enableCompressedOutput']))
+		@ob_start('ob_gzhandler');
+	else
+		@ob_start();
+
+	// Make sure they know what type of file we are.
+	header('Content-Type: ' . $filetype);
+	echo $file_data;
+	obExit(false);
+}
+
+// This allocates out all the search stuff.
+function AdminSearch()
+{
+	global $txt, $context, $smcFunc, $sourcedir;
+
+	isAllowedTo('admin_forum');
+
+	// What can we search for?
+	$subactions = array(
+		'internal' => 'AdminSearchInternal',
+		'online' => 'AdminSearchOM',
+		'member' => 'AdminSearchMember',
+	);
+
+	$context['search_type'] = !isset($_REQUEST['search_type']) || !isset($subactions[$_REQUEST['search_type']]) ? 'internal' : $_REQUEST['search_type'];
+	$context['search_term'] = isset($_REQUEST['search_term']) ? $smcFunc['htmlspecialchars']($_REQUEST['search_term'], ENT_QUOTES) : '';
+
+	$context['sub_template'] = 'admin_search_results';
+	$context['page_title'] = $txt['admin_search_results'];
+
+	// Keep track of what the admin wants.
+	if (empty($context['admin_preferences']['sb']) || $context['admin_preferences']['sb'] != $context['search_type'])
+	{
+		$context['admin_preferences']['sb'] = $context['search_type'];
+
+		// Update the preferences.
+		require_once($sourcedir . '/Subs-Admin.php');
+		updateAdminPreferences();
+	}
+
+	if (trim($context['search_term']) == '')
+		$context['search_results'] = array();
+	else
+		$subactions[$context['search_type']]();
+}
+
+// A complicated but relatively quick internal search.
+function AdminSearchInternal()
+{
+	global $context, $txt, $helptxt, $scripturl, $sourcedir;
+
+	// Try to get some more memory.
+	@ini_set('memory_limit', '128M');
+
+	// Load a lot of language files.
+	$language_files = array(
+		'Help', 'ManageMail', 'ManageSettings', 'ManageCalendar', 'ManageBoards', 'ManagePaid', 'ManagePermissions', 'Search',
+		'Login', 'ManageSmileys',
+	);
+	loadLanguage(implode('+', $language_files));
+
+	// All the files we need to include.
+	$include_files = array(
+		'ManageSettings', 'ManageBoards', 'ManageNews', 'ManageAttachments', 'ManageCalendar', 'ManageMail', 'ManagePaid', 'ManagePermissions',
+		'ManagePosts', 'ManageRegistration', 'ManageSearch', 'ManageSearchEngines', 'ManageServer', 'ManageSmileys',
+	);
+	foreach ($include_files as $file)
+		require_once($sourcedir . '/' . $file . '.php');
+
+	/* This is the huge array that defines everything... it's a huge array of items formatted as follows:
+		0 = Language index (Can be array of indexes) to search through for this setting.
+		1 = URL for this indexes page.
+		2 = Help index for help associated with this item (If different from 0)
+	*/
+
+	$search_data = array(
+		// All the major sections of the forum.
+		'sections' => array(
+		),
+		'settings' => array(
+			array('COPPA', 'area=regcenter;sa=settings'),
+			array('CAPTCHA', 'area=securitysettings;sa=spam'),
+		),
+	);
+
+	// Go through the admin menu structure trying to find suitably named areas!
+	foreach ($context[$context['admin_menu_name']]['sections'] as $section)
+	{
+		foreach ($section['areas'] as $menu_key => $menu_item)
+		{
+			$search_data['sections'][] = array($menu_item['label'], 'area=' . $menu_key);
+			if (!empty($menu_item['subsections']))
+				foreach ($menu_item['subsections'] as $key => $sublabel)
+				{
+					if (isset($sublabel['label']))
+						$search_data['sections'][] = array($sublabel['label'], 'area=' . $menu_key . ';sa=' . $key);
+				}
+		}
+	}
+
+	// This is a special array of functions that contain setting data - we query all these to simply pull all setting bits!
+	$settings_search = array(
+		array('ModifyCoreFeatures', 'area=corefeatures'),
+		array('ModifyBasicSettings', 'area=featuresettings;sa=basic'),
+		array('ModifyLayoutSettings', 'area=featuresettings;sa=layout'),
+		array('ModifyKarmaSettings', 'area=featuresettings;sa=karma'),
+		array('ModifySignatureSettings', 'area=featuresettings;sa=sig'),
+		array('ModifyGeneralSecuritySettings', 'area=securitysettings;sa=general'),
+		array('ModifySpamSettings', 'area=securitysettings;sa=spam'),
+		array('ModifyModerationSettings', 'area=securitysettings;sa=moderation'),
+		array('ModifyGeneralModSettings', 'area=modsettings;sa=general'),
+		// Mod authors if you want to be "real freaking good" then add any setting pages for your mod BELOW this line!
+		array('ManageAttachmentSettings', 'area=manageattachments;sa=attachments'),
+		array('ManageAvatarSettings', 'area=manageattachments;sa=avatars'),
+		array('ModifyCalendarSettings', 'area=managecalendar;sa=settings'),
+		array('EditBoardSettings', 'area=manageboards;sa=settings'),
+		array('ModifyMailSettings', 'area=mailqueue;sa=settings'),
+		array('ModifyNewsSettings', 'area=news;sa=settings'),
+		array('GeneralPermissionSettings', 'area=permissions;sa=settings'),
+		array('ModifyPostSettings', 'area=postsettings;sa=posts'),
+		array('ModifyBBCSettings', 'area=postsettings;sa=bbc'),
+		array('ModifyTopicSettings', 'area=postsettings;sa=topics'),
+		array('EditSearchSettings', 'area=managesearch;sa=settings'),
+		array('EditSmileySettings', 'area=smileys;sa=settings'),
+		array('ModifyGeneralSettings', 'area=serversettings;sa=general'),
+		array('ModifyDatabaseSettings', 'area=serversettings;sa=database'),
+		array('ModifyCookieSettings', 'area=serversettings;sa=cookie'),
+		array('ModifyCacheSettings', 'area=serversettings;sa=cache'),
+		array('ModifyLanguageSettings', 'area=languages;sa=settings'),
+		array('ModifyRegistrationSettings', 'area=regcenter;sa=settings'),
+		array('ManageSearchEngineSettings', 'area=sengines;sa=settings'),
+		array('ModifySubscriptionSettings', 'area=paidsubscribe;sa=settings'),
+		array('ModifyPruningSettings', 'area=logs;sa=pruning'),
+	);
+
+	foreach ($settings_search as $setting_area)
+	{
+		// Get a list of their variables.
+		$config_vars = $setting_area[0](true);
+
+		foreach ($config_vars as $var)
+			if (!empty($var[1]) && !in_array($var[0], array('permissions', 'switch')))
+				$search_data['settings'][] = array($var[(isset($var[2]) && in_array($var[2], array('file', 'db'))) ? 0 : 1], $setting_area[1]);
+	}
+
+	$context['page_title'] = $txt['admin_search_results'];
+	$context['search_results'] = array();
+
+	$search_term = strtolower($context['search_term']);
+	// Go through all the search data trying to find this text!
+	foreach ($search_data as $section => $data)
+	{
+		foreach ($data as $item)
+		{
+			$found = false;
+			if (!is_array($item[0]))
+				$item[0] = array($item[0]);
+			foreach ($item[0] as $term)
+			{
+				$lc_term = strtolower($term);
+				if (strpos($lc_term, $search_term) !== false || (isset($txt[$term]) && strpos(strtolower($txt[$term]), $search_term) !== false) || (isset($txt['setting_' . $term]) && strpos(strtolower($txt['setting_' . $term]), $search_term) !== false))
+				{
+					$found = $term;
+					break;
+				}
+			}
+
+			if ($found)
+			{
+				// Format the name - and remove any descriptions the entry may have.
+				$name = isset($txt[$found]) ? $txt[$found] : (isset($txt['setting_' . $found]) ? $txt['setting_' . $found] : $found);
+				$name = preg_replace('~<(?:div|span)\sclass="smalltext">.+?</(?:div|span)>~', '', $name);
+
+				$context['search_results'][] = array(
+					'url' => (substr($item[1], 0, 4) == 'area' ? $scripturl . '?action=admin;' . $item[1] : $item[1]) . ';' . $context['session_var'] . '=' . $context['session_id'] . ((substr($item[1], 0, 4) == 'area' && $section == 'settings' ? '#' . $item[0][0] : '')),
+					'name' => $name,
+					'type' => $section,
+					'help' => shorten_subject(isset($item[2]) ? strip_tags($helptxt[$item2]) : (isset($helptxt[$found]) ? strip_tags($helptxt[$found]) : ''), 255),
+				);
+			}
+		}
+	}
+}
+
+// All this does is pass through to manage members.
+function AdminSearchMember()
+{
+	global $context, $sourcedir;
+
+	require_once($sourcedir . '/ManageMembers.php');
+	$_REQUEST['sa'] = 'query';
+
+	$_POST['membername'] = $context['search_term'];
+
+	ViewMembers();
+}
+
+// This file allows the user to search the SM online manual for a little of help.
+function AdminSearchOM()
+{
+	global $context, $sourcedir;
+
+	$docsURL = 'docs.simplemachines.org';
+	$context['doc_scripturl'] = 'http://docs.simplemachines.org/index.php';
+
+	// Set all the parameters search might expect.
+	$postVars = array(
+		'search' => $context['search_term'],
+	);
+
+	// Encode the search data.
+	foreach ($postVars as $k => $v)
+		$postVars[$k] = urlencode($k) . '=' . urlencode($v);
+
+	// This is what we will send.
+	$postVars = implode('&', $postVars);
+
+	// Get the results from the doc site.
+	require_once($sourcedir . '/Subs-Package.php');
+	$search_results = fetch_web_data($context['doc_scripturl'] . '?action=search2&xml', $postVars);
+
+	// If we didn't get any xml back we are in trouble - perhaps the doc site is overloaded?
+	if (!$search_results || preg_match('~<' . '\?xml\sversion="\d+\.\d+"\sencoding=".+?"\?' . '>\s*(<smf>.+?</smf>)~is', $search_results, $matches) != true)
+		fatal_lang_error('cannot_connect_doc_site');
+
+	$search_results = $matches[1];
+
+	// Otherwise we simply walk through the XML and stick it in context for display.
+	$context['search_results'] = array();
+	loadClassFile('Class-Package.php');
+
+	// Get the results loaded into an array for processing!
+	$results = new xmlArray($search_results, false);
+
+	// Move through the smf layer.
+	if (!$results->exists('smf'))
+		fatal_lang_error('cannot_connect_doc_site');
+	$results = $results->path('smf[0]');
+
+	// Are there actually some results?
+	if (!$results->exists('noresults') && !$results->exists('results'))
+		fatal_lang_error('cannot_connect_doc_site');
+	elseif ($results->exists('results'))
+	{
+		foreach ($results->set('results/result') as $result)
+		{
+			if (!$result->exists('messages'))
+				continue;
+
+			$context['search_results'][$result->fetch('id')] = array(
+				'topic_id' => $result->fetch('id'),
+				'relevance' => $result->fetch('relevance'),
+				'board' => array(
+					'id' => $result->fetch('board/id'),
+					'name' => $result->fetch('board/name'),
+					'href' => $result->fetch('board/href'),
+				),
+				'category' => array(
+					'id' => $result->fetch('category/id'),
+					'name' => $result->fetch('category/name'),
+					'href' => $result->fetch('category/href'),
+				),
+				'messages' => array(),
+			);
+
+			// Add the messages.
+			foreach ($result->set('messages/message') as $message)
+				$context['search_results'][$result->fetch('id')]['messages'][] = array(
+					'id' => $message->fetch('id'),
+					'subject' => $message->fetch('subject'),
+					'body' => $message->fetch('body'),
+					'time' => $message->fetch('time'),
+					'timestamp' => $message->fetch('timestamp'),
+					'start' => $message->fetch('start'),
+					'author' => array(
+						'id' => $message->fetch('author/id'),
+						'name' => $message->fetch('author/name'),
+						'href' => $message->fetch('author/href'),
+					),
+				);
+		}
+	}
+}
+
+// This function decides which log to load.
+function AdminLogs()
+{
+	global $sourcedir, $context, $txt, $scripturl;
+
+	// These are the logs they can load.
+	$log_functions = array(
+		'errorlog' => array('ManageErrors.php', 'ViewErrorLog'),
+		'adminlog' => array('Modlog.php', 'ViewModlog'),
+		'modlog' => array('Modlog.php', 'ViewModlog'),
+		'banlog' => array('ManageBans.php', 'BanLog'),
+		'spiderlog' => array('ManageSearchEngines.php', 'SpiderLogs'),
+		'tasklog' => array('ManageScheduledTasks.php', 'TaskLog'),
+		'pruning' => array('ManageSettings.php', 'ModifyPruningSettings'),
+	);
+
+	$sub_action = isset($_REQUEST['sa']) && isset($log_functions[$_REQUEST['sa']]) ? $_REQUEST['sa'] : 'errorlog';
+	// If it's not got a sa set it must have come here for first time, pretend error log should be reversed.
+	if (!isset($_REQUEST['sa']))
+		$_REQUEST['desc'] = true;
+
+	// Setup some tab stuff.
+	$context[$context['admin_menu_name']]['tab_data'] = array(
+		'title' => $txt['logs'],
+		'help' => '',
+		'description' => $txt['maintain_info'],
+		'tabs' => array(
+			'errorlog' => array(
+				'url' => $scripturl . '?action=admin;area=logs;sa=errorlog;desc',
+				'description' => sprintf($txt['errlog_desc'], $txt['remove']),
+			),
+			'adminlog' => array(
+				'description' => $txt['admin_log_desc'],
+			),
+			'modlog' => array(
+				'description' => $txt['moderation_log_desc'],
+			),
+			'banlog' => array(
+				'description' => $txt['ban_log_description'],
+			),
+			'spiderlog' => array(
+				'description' => $txt['spider_log_desc'],
+			),
+			'tasklog' => array(
+				'description' => $txt['scheduled_log_desc'],
+			),
+			'pruning' => array(
+				'description' => $txt['pruning_log_desc'],
+			),
+		),
+	);
+
+	require_once($sourcedir . '/' . $log_functions[$sub_action][0]);
+	$log_functions[$sub_action][1]();
+}
+
+?>

+ 143 - 0
Sources/BoardIndex.php

@@ -0,0 +1,143 @@
+<?php
+
+/**
+ * Simple Machines Forum (SMF)
+ *
+ * @package SMF
+ * @author Simple Machines http://www.simplemachines.org
+ * @copyright 2011 Simple Machines
+ * @license http://www.simplemachines.org/about/smf/license.php BSD
+ *
+ * @version 2.0
+ */
+
+if (!defined('SMF'))
+	die('Hacking attempt...');
+
+/*	The single function this file contains is used to display the main
+	board index.  It uses just the following functions:
+
+	void BoardIndex()
+		- shows the board index.
+		- uses the BoardIndex template, and main sub template.
+		- may use the boardindex subtemplate for wireless support.
+		- updates the most online statistics.
+		- is accessed by ?action=boardindex.
+
+	void CollapseCategory()
+		- collapse or expand a category
+*/
+
+// Show the board index!
+function BoardIndex()
+{
+	global $txt, $user_info, $sourcedir, $modSettings, $context, $settings, $scripturl;
+
+	// For wireless, we use the Wireless template...
+	if (WIRELESS)
+		$context['sub_template'] = WIRELESS_PROTOCOL . '_boardindex';
+	else
+		loadTemplate('BoardIndex');
+
+	// Set a canonical URL for this page.
+	$context['canonical_url'] = $scripturl;
+
+	// Do not let search engines index anything if there is a random thing in $_GET.
+	if (!empty($_GET))
+		$context['robot_no_index'] = true;
+
+	// Retrieve the categories and boards.
+	require_once($sourcedir . '/Subs-BoardIndex.php');
+	$boardIndexOptions = array(
+		'include_categories' => true,
+		'base_level' => 0,
+		'parent_id' => 0,
+		'set_latest_post' => true,
+		'countChildPosts' => !empty($modSettings['countChildPosts']),
+	);
+	$context['categories'] = getBoardIndex($boardIndexOptions);
+
+	// Get the user online list.
+	require_once($sourcedir . '/Subs-MembersOnline.php');
+	$membersOnlineOptions = array(
+		'show_hidden' => allowedTo('moderate_forum'),
+		'sort' => 'log_time',
+		'reverse_sort' => true,
+	);
+	$context += getMembersOnlineStats($membersOnlineOptions);
+
+	$context['show_buddies'] = !empty($user_info['buddies']);
+
+	// Are we showing all membergroups on the board index?
+	if (!empty($settings['show_group_key']))
+		$context['membergroups'] = cache_quick_get('membergroup_list', 'Subs-Membergroups.php', 'cache_getMembergroupList', array());
+
+	// Track most online statistics? (Subs-MembersOnline.php)
+	if (!empty($modSettings['trackStats']))
+		trackStatsUsersOnline($context['num_guests'] + $context['num_spiders'] + $context['num_users_online']);
+
+	// Retrieve the latest posts if the theme settings require it.
+	if (isset($settings['number_recent_posts']) && $settings['number_recent_posts'] > 1)
+	{
+		$latestPostOptions = array(
+			'number_posts' => $settings['number_recent_posts'],
+		);
+		$context['latest_posts'] = cache_quick_get('boardindex-latest_posts:' . md5($user_info['query_wanna_see_board'] . $user_info['language']), 'Subs-Recent.php', 'cache_getLastPosts', array($latestPostOptions));
+	}
+
+	$settings['display_recent_bar'] = !empty($settings['number_recent_posts']) ? $settings['number_recent_posts'] : 0;
+	$settings['show_member_bar'] &= allowedTo('view_mlist');
+	$context['show_stats'] = allowedTo('view_stats') && !empty($modSettings['trackStats']);
+	$context['show_member_list'] = allowedTo('view_mlist');
+	$context['show_who'] = allowedTo('who_view') && !empty($modSettings['who_enabled']);
+
+	// Load the calendar?
+	if (!empty($modSettings['cal_enabled']) && allowedTo('calendar_view'))
+	{
+		// Retrieve the calendar data (events, birthdays, holidays).
+		$eventOptions = array(
+			'include_holidays' => $modSettings['cal_showholidays'] > 1,
+			'include_birthdays' => $modSettings['cal_showbdays'] > 1,
+			'include_events' => $modSettings['cal_showevents'] > 1,
+			'num_days_shown' => empty($modSettings['cal_days_for_index']) || $modSettings['cal_days_for_index'] < 1 ? 1 : $modSettings['cal_days_for_index'],
+		);
+		$context += cache_quick_get('calendar_index_offset_' . ($user_info['time_offset'] + $modSettings['time_offset']), 'Subs-Calendar.php', 'cache_getRecentEvents', array($eventOptions));
+
+		// Whether one or multiple days are shown on the board index.
+		$context['calendar_only_today'] = $modSettings['cal_days_for_index'] == 1;
+
+		// This is used to show the "how-do-I-edit" help.
+		$context['calendar_can_edit'] = allowedTo('calendar_edit_any');
+	}
+	else
+		$context['show_calendar'] = false;
+
+	$context['page_title'] = sprintf($txt['forum_index'], $context['forum_name']);
+}
+
+// Collapse or expand a category
+function CollapseCategory()
+{
+	global $user_info, $sourcedir, $context;
+
+	// Just in case, no need, no need.
+	$context['robot_no_index'] = true;
+
+	checkSession('request');
+
+	if (!isset($_GET['sa']))
+		fatal_lang_error('no_access', false);
+
+	// Check if the input values are correct.
+	if (in_array($_REQUEST['sa'], array('expand', 'collapse', 'toggle')) && isset($_REQUEST['c']))
+	{
+		// And collapse/expand/toggle the category.
+		require_once($sourcedir . '/Subs-Categories.php');
+		collapseCategories(array((int) $_REQUEST['c']), $_REQUEST['sa'], array($user_info['id']));
+	}
+
+	// And go back to the board index.
+	BoardIndex();
+}
+
+?>

+ 487 - 0
Sources/Calendar.php

@@ -0,0 +1,487 @@
+<?php
+
+/**
+ * Simple Machines Forum (SMF)
+ *
+ * @package SMF
+ * @author Simple Machines http://www.simplemachines.org
+ * @copyright 2011 Simple Machines
+ * @license http://www.simplemachines.org/about/smf/license.php BSD
+ *
+ * @version 2.0
+ */
+
+// Original module by Aaron O'Neil - [email protected]
+
+if (!defined('SMF'))
+	die('Hacking attempt...');
+
+/*	This file has only one real task... showing the calendar.  Posting is done
+	in Post.php - this just has the following functions:
+
+	void CalendarMain()
+		- loads the specified month's events, holidays, and birthdays.
+		- requires the calendar_view permission.
+		- depends on the cal_enabled setting, and many of the other cal_
+		  settings.
+		- uses the calendar_start_day theme option. (Monday/Sunday)
+		- uses the main sub template in the Calendar template.
+		- goes to the month and year passed in 'month' and 'year' by
+		  get or post.
+		- accessed through ?action=calendar.
+
+	void CalendarPost()
+		- processes posting/editing/deleting a calendar event.
+		- calls Post() function if event is linked to a post.
+		- calls insertEvent() to insert the event if not linked to post.
+		- requires the calendar_post permission to use.
+		- uses the event_post sub template in the Calendar template.
+		- is accessed with ?action=calendar;sa=post.
+
+	void iCalDownload()
+		- offers up a download of an event in iCal 2.0 format.
+*/
+
+// Show the calendar.
+function CalendarMain()
+{
+	global $txt, $context, $modSettings, $scripturl, $options, $sourcedir;
+
+	// Permissions, permissions, permissions.
+	isAllowedTo('calendar_view');
+
+	// Doing something other than calendar viewing?
+	$subActions = array(
+		'ical' => 'iCalDownload',
+		'post' => 'CalendarPost',
+	);
+
+	if (isset($_GET['sa']) && isset($subActions[$_GET['sa']]) && !WIRELESS)
+		return $subActions[$_GET['sa']]();
+
+	// This is gonna be needed...
+	loadTemplate('Calendar');
+
+	// You can't do anything if the calendar is off.
+	if (empty($modSettings['cal_enabled']))
+		fatal_lang_error('calendar_off', false);
+
+	// Set the page title to mention the calendar ;).
+	$context['page_title'] = $txt['calendar'];
+
+	// Is this a week view?
+	$context['view_week'] = isset($_GET['viewweek']);
+
+	// Don't let search engines index weekly calendar pages.
+	if ($context['view_week'])
+		$context['robot_no_index'] = true;
+
+	// Get the current day of month...
+	require_once($sourcedir . '/Subs-Calendar.php');
+	$today = getTodayInfo();
+
+	// If the month and year are not passed in, use today's date as a starting point.
+	$curPage = array(
+		'day' => isset($_REQUEST['day']) ? (int) $_REQUEST['day'] : $today['day'],
+		'month' => isset($_REQUEST['month']) ? (int) $_REQUEST['month'] : $today['month'],
+		'year' => isset($_REQUEST['year']) ? (int) $_REQUEST['year'] : $today['year']
+	);
+
+	// Make sure the year and month are in valid ranges.
+	if ($curPage['month'] < 1 || $curPage['month'] > 12)
+		fatal_lang_error('invalid_month', false);
+	if ($curPage['year'] < $modSettings['cal_minyear'] || $curPage['year'] > $modSettings['cal_maxyear'])
+		fatal_lang_error('invalid_year', false);
+	// If we have a day clean that too.
+	if ($context['view_week'])
+	{
+		// Note $isValid is -1 < PHP 5.1
+		$isValid = mktime(0, 0, 0, $curPage['month'], $curPage['day'], $curPage['year']);
+		if ($curPage['day'] > 31 || !$isValid || $isValid == -1)
+			fatal_lang_error('invalid_day', false);
+	}
+
+	// Load all the context information needed to show the calendar grid.
+	$calendarOptions = array(
+		'start_day' => !empty($options['calendar_start_day']) ? $options['calendar_start_day'] : 0,
+		'show_birthdays' => in_array($modSettings['cal_showbdays'], array(1, 2)),
+		'show_events' => in_array($modSettings['cal_showevents'], array(1, 2)),
+		'show_holidays' => in_array($modSettings['cal_showholidays'], array(1, 2)),
+		'show_week_num' => true,
+		'short_day_titles' => false,
+		'show_next_prev' => true,
+		'show_week_links' => true,
+		'size' => 'large',
+	);
+
+	// Load up the main view.
+	if ($context['view_week'])
+		$context['calendar_grid_main'] = getCalendarWeek($curPage['month'], $curPage['year'], $curPage['day'], $calendarOptions);
+	else
+		$context['calendar_grid_main'] = getCalendarGrid($curPage['month'], $curPage['year'], $calendarOptions);
+
+	// Load up the previous and next months.
+	$calendarOptions['show_birthdays'] = $calendarOptions['show_events'] = $calendarOptions['show_holidays'] = false;
+	$calendarOptions['short_day_titles'] = true;
+	$calendarOptions['show_next_prev'] = false;
+	$calendarOptions['show_week_links'] = false;
+	$calendarOptions['size'] = 'small';
+	$context['calendar_grid_current'] = getCalendarGrid($curPage['month'], $curPage['year'], $calendarOptions);
+	// Only show previous month if it isn't pre-January of the min-year
+	if ($context['calendar_grid_current']['previous_calendar']['year'] > $modSettings['cal_minyear'] || $curPage['month'] != 1)
+		$context['calendar_grid_prev'] = getCalendarGrid($context['calendar_grid_current']['previous_calendar']['month'], $context['calendar_grid_current']['previous_calendar']['year'], $calendarOptions);
+	// Only show next month if it isn't post-December of the max-year
+	if ($context['calendar_grid_current']['next_calendar']['year'] < $modSettings['cal_maxyear'] || $curPage['month'] != 12)
+		$context['calendar_grid_next'] = getCalendarGrid($context['calendar_grid_current']['next_calendar']['month'], $context['calendar_grid_current']['next_calendar']['year'], $calendarOptions);
+
+	// Basic template stuff.
+	$context['can_post'] = allowedTo('calendar_post');
+	$context['current_day'] = $curPage['day'];
+	$context['current_month'] = $curPage['month'];
+	$context['current_year'] = $curPage['year'];
+	$context['show_all_birthdays'] = isset($_GET['showbd']);
+
+	// Set the page title to mention the month or week, too
+	$context['page_title'] .= ' - ' . ($context['view_week'] ? sprintf($txt['calendar_week_title'], $context['calendar_grid_main']['week_number'], ($context['calendar_grid_main']['week_number'] == 53 ? $context['current_year'] - 1 : $context['current_year'])) : $txt['months'][$context['current_month']] . ' ' . $context['current_year']);
+
+	// Load up the linktree!
+	$context['linktree'][] = array(
+		'url' => $scripturl . '?action=calendar',
+		'name' => $txt['calendar']
+	);
+	// Add the current month to the linktree.
+	$context['linktree'][] = array(
+		'url' => $scripturl . '?action=calendar;year=' . $context['current_year'] . ';month=' . $context['current_month'],
+		'name' => $txt['months'][$context['current_month']] . ' ' . $context['current_year']
+	);
+	// If applicable, add the current week to the linktree.
+	if ($context['view_week'])
+		$context['linktree'][] = array(
+			'url' => $scripturl . '?action=calendar;viewweek;year=' . $context['current_year'] . ';month=' . $context['current_month'] . ';day=' . $context['current_day'],
+			'name' => $txt['calendar_week'] . ' ' . $context['calendar_grid_main']['week_number']
+		);
+}
+
+function CalendarPost()
+{
+	global $context, $txt, $user_info, $sourcedir, $scripturl;
+	global $modSettings, $topic, $smcFunc;
+
+	// Well - can they?
+	isAllowedTo('calendar_post');
+
+	// We need this for all kinds of useful functions.
+	require_once($sourcedir . '/Subs-Calendar.php');
+
+	// Cast this for safety...
+	if (isset($_REQUEST['eventid']))
+		$_REQUEST['eventid'] = (int) $_REQUEST['eventid'];
+
+	// Submitting?
+	if (isset($_POST[$context['session_var']], $_REQUEST['eventid']))
+	{
+		checkSession();
+
+		// Validate the post...
+		if (!isset($_POST['link_to_board']))
+			validateEventPost();
+
+		// If you're not allowed to edit any events, you have to be the poster.
+		if ($_REQUEST['eventid'] > 0 && !allowedTo('calendar_edit_any'))
+			isAllowedTo('calendar_edit_' . (!empty($user_info['id']) && getEventPoster($_REQUEST['eventid']) == $user_info['id'] ? 'own' : 'any'));
+
+		// New - and directing?
+		if ($_REQUEST['eventid'] == -1 && isset($_POST['link_to_board']))
+		{
+			$_REQUEST['calendar'] = 1;
+			require_once($sourcedir . '/Post.php');
+			return Post();
+		}
+		// New...
+		elseif ($_REQUEST['eventid'] == -1)
+		{
+			$eventOptions = array(
+				'board' => 0,
+				'topic' => 0,
+				'title' => substr($_REQUEST['evtitle'], 0, 60),
+				'member' => $user_info['id'],
+				'start_date' => sprintf('%04d-%02d-%02d', $_POST['year'], $_POST['month'], $_POST['day']),
+				'span' => isset($_POST['span']) && $_POST['span'] > 0 ? min((int) $modSettings['cal_maxspan'], (int) $_POST['span'] - 1) : 0,
+			);
+			insertEvent($eventOptions);
+		}
+
+		// Deleting...
+		elseif (isset($_REQUEST['deleteevent']))
+			removeEvent($_REQUEST['eventid']);
+
+		// ... or just update it?
+		else
+		{
+			$eventOptions = array(
+				'title' => substr($_REQUEST['evtitle'], 0, 60),
+				'span' => empty($modSettings['cal_allowspan']) || empty($_POST['span']) || $_POST['span'] == 1 || empty($modSettings['cal_maxspan']) || $_POST['span'] > $modSettings['cal_maxspan'] ? 0 : min((int) $modSettings['cal_maxspan'], (int) $_POST['span'] - 1),
+				'start_date' => strftime('%Y-%m-%d', mktime(0, 0, 0, (int) $_REQUEST['month'], (int) $_REQUEST['day'], (int) $_REQUEST['year'])),
+			);
+
+			modifyEvent($_REQUEST['eventid'], $eventOptions);
+		}
+
+		updateSettings(array(
+			'calendar_updated' => time(),
+		));
+
+		// No point hanging around here now...
+		redirectexit($scripturl . '?action=calendar;month=' . $_POST['month'] . ';year=' . $_POST['year']);
+	}
+
+	// If we are not enabled... we are not enabled.
+	if (empty($modSettings['cal_allow_unlinked']) && empty($_REQUEST['eventid']))
+	{
+		$_REQUEST['calendar'] = 1;
+		require_once($sourcedir . '/Post.php');
+		return Post();
+	}
+
+	// New?
+	if (!isset($_REQUEST['eventid']))
+	{
+		$today = getdate();
+
+		$context['event'] = array(
+			'boards' => array(),
+			'board' => 0,
+			'new' => 1,
+			'eventid' => -1,
+			'year' => isset($_REQUEST['year']) ? $_REQUEST['year'] : $today['year'],
+			'month' => isset($_REQUEST['month']) ? $_REQUEST['month'] : $today['mon'],
+			'day' => isset($_REQUEST['day']) ? $_REQUEST['day'] : $today['mday'],
+			'title' => '',
+			'span' => 1,
+		);
+		$context['event']['last_day'] = (int) strftime('%d', mktime(0, 0, 0, $context['event']['month'] == 12 ? 1 : $context['event']['month'] + 1, 0, $context['event']['month'] == 12 ? $context['event']['year'] + 1 : $context['event']['year']));
+
+		// Get list of boards that can be posted in.
+		$boards = boardsAllowedTo('post_new');
+		if (empty($boards))
+			fatal_lang_error('cannot_post_new', 'permission');
+
+		// Load the list of boards and categories in the context.
+		require_once($sourcedir . '/Subs-MessageIndex.php');
+		$boardListOptions = array(
+			'included_boards' => in_array(0, $boards) ? null : $boards,
+			'not_redirection' => true,
+			'use_permissions' => true,
+			'selected_board' => $modSettings['cal_defaultboard'],
+		);
+		$context['event']['categories'] = getBoardList($boardListOptions);
+	}
+	else
+	{
+		$context['event'] = getEventProperties($_REQUEST['eventid']);
+
+		if ($context['event'] === false)
+			fatal_lang_error('no_access', false);
+
+		// If it has a board, then they should be editing it within the topic.
+		if (!empty($context['event']['topic']['id']) && !empty($context['event']['topic']['first_msg']))
+		{
+			// We load the board up, for a check on the board access rights...
+			$topic = $context['event']['topic']['id'];
+			loadBoard();
+		}
+
+		// Make sure the user is allowed to edit this event.
+		if ($context['event']['member'] != $user_info['id'])
+			isAllowedTo('calendar_edit_any');
+		elseif (!allowedTo('calendar_edit_any'))
+			isAllowedTo('calendar_edit_own');
+	}
+
+	// Template, sub template, etc.
+	loadTemplate('Calendar');
+	$context['sub_template'] = 'event_post';
+
+	$context['page_title'] = isset($_REQUEST['eventid']) ? $txt['calendar_edit'] : $txt['calendar_post_event'];
+	$context['linktree'][] = array(
+		'name' => $context['page_title'],
+	);
+}
+
+function iCalDownload()
+{
+	global $smcFunc, $sourcedir, $forum_version, $context, $modSettings;
+
+	// Goes without saying that this is required.
+	if (!isset($_REQUEST['eventid']))
+		fatal_lang_error('no_access', false);
+
+	// This is kinda wanted.
+	require_once($sourcedir . '/Subs-Calendar.php');
+
+	// Load up the event in question and check it exists.
+	$event = getEventProperties($_REQUEST['eventid']);
+
+	if ($event === false)
+		fatal_lang_error('no_access', false);
+
+	// Check the title isn't too long - iCal requires some formatting if so.
+	$title = str_split($event['title'], 30);
+	foreach ($title as $id => $line)
+	{
+		if ($id != 0)
+			$title[$id] = ' ' . $title[$id];
+		$title[$id] .= "\n";
+	}
+
+	// Format the date.
+	$date = $event['year'] . '-' . ($event['month'] < 10 ? '0' . $event['month'] : $event['month']) . '-' . ($event['day'] < 10 ? '0' . $event['day'] : $event['day']) . 'T';
+	$date .= '1200:00:00Z';
+
+	// This is what we will be sending later.
+	$filecontents = '';
+	$filecontents .= 'BEGIN:VCALENDAR' . "\n";
+	$filecontents .= 'VERSION:2.0' . "\n";
+	$filecontents .= 'PRODID:-//SimpleMachines//SMF ' . (empty($forum_version) ? 1.0 : strtr($forum_version, array('SMF ' => ''))) . '//EN' . "\n";
+	$filecontents .= 'BEGIN:VEVENT' . "\n";
+	$filecontents .= 'DTSTART:' . $date . "\n";
+	$filecontents .= 'DTEND:' . $date . "\n";
+	$filecontents .= 'SUMMARY:' . implode('', $title);
+	$filecontents .= 'END:VEVENT' . "\n";
+	$filecontents .= 'END:VCALENDAR';
+
+	// Send some standard headers.
+	ob_end_clean();
+	if (!empty($modSettings['enableCompressedOutput']))
+		@ob_start('ob_gzhandler');
+	else
+		ob_start();
+
+	// Send the file headers
+	header('Pragma: ');
+	header('Cache-Control: no-cache');
+	if (!$context['browser']['is_gecko'])
+		header('Content-Transfer-Encoding: binary');
+	header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 525600 * 60) . ' GMT');
+	header('Last-Modified: ' . gmdate('D, d M Y H:i:s', time()) . 'GMT');
+	header('Accept-Ranges: bytes');
+	header('Connection: close');
+	header('Content-Disposition: attachment; filename=' . $event['title'] . '.ics');
+
+	// How big is it?
+	if (empty($modSettings['enableCompressedOutput']))
+		header('Content-Length: ' . $smcFunc['strlen']($filecontents));
+
+	// This is a calendar item!
+	header('Content-Type: text/calendar');
+
+	// Chuck out the card.
+	echo $filecontents;
+
+	// Off we pop - lovely!
+	obExit(false);
+}
+
+// This is not the code you are looking for.
+function clock()
+{
+	global $settings, $context;
+	$context['onimg'] = $settings['images_url'] . '/bbc/bbc_bg.gif';
+	$context['offimg'] = $settings['images_url'] . '/bbc/bbc_hoverbg.gif';
+
+	$context['page_title'] = 'Anyone know what time it is?';
+	$context['robot_no_index'] = true;
+
+	$omfg = isset($_REQUEST['omfg']);
+	$bcd = !isset($_REQUEST['rb']) && !isset($_REQUEST['omfg']) && !isset($_REQUEST['time']);
+
+	loadTemplate('Calendar');
+
+	if ($bcd && !$omfg)
+	{
+		$context['sub_template'] = 'bcd';
+		$context['clockicons'] = unserialize(base64_decode('YTo2OntzOjI6ImgxIjthOjI6e2k6MDtpOjI7aToxO2k6MTt9czoyOiJoMiI7YTo0OntpOjA7aTo4O2k6MTtpOjQ7aToyO2k6MjtpOjM7aToxO31zOjI6Im0xIjthOjM6e2k6MDtpOjQ7aToxO2k6MjtpOjI7aToxO31zOjI6Im0yIjthOjQ6e2k6MDtpOjg7aToxO2k6NDtpOjI7aToyO2k6MztpOjE7fXM6MjoiczEiO2E6Mzp7aTowO2k6NDtpOjE7aToyO2k6MjtpOjE7fXM6MjoiczIiO2E6NDp7aTowO2k6ODtpOjE7aTo0O2k6MjtpOjI7aTozO2k6MTt9fQ=='));
+	}
+	elseif (!$omfg && !isset($_REQUEST['time']))
+	{
+		$context['sub_template'] = 'hms';
+		$context['clockicons'] = unserialize(base64_decode('YTozOntzOjE6ImgiO2E6NTp7aTowO2k6MTY7aToxO2k6ODtpOjI7aTo0O2k6MztpOjI7aTo0O2k6MTt9czoxOiJtIjthOjY6e2k6MDtpOjMyO2k6MTtpOjE2O2k6MjtpOjg7aTozO2k6NDtpOjQ7aToyO2k6NTtpOjE7fXM6MToicyI7YTo2OntpOjA7aTozMjtpOjE7aToxNjtpOjI7aTo4O2k6MztpOjQ7aTo0O2k6MjtpOjU7aToxO319'));
+	}
+	elseif ($omfg)
+	{
+		$context['sub_template'] = 'omfg';
+		$context['clockicons'] = unserialize(base64_decode('YTo2OntzOjQ6InllYXIiO2E6Nzp7aTowO2k6NjQ7aToxO2k6MzI7aToyO2k6MTY7aTozO2k6ODtpOjQ7aTo0O2k6NTtpOjI7aTo2O2k6MTt9czo1OiJtb250aCI7YTo0OntpOjA7aTo4O2k6MTtpOjQ7aToyO2k6MjtpOjM7aToxO31zOjM6ImRheSI7YTo1OntpOjA7aToxNjtpOjE7aTo4O2k6MjtpOjQ7aTozO2k6MjtpOjQ7aToxO31zOjQ6ImhvdXIiO2E6NTp7aTowO2k6MTY7aToxO2k6ODtpOjI7aTo0O2k6MztpOjI7aTo0O2k6MTt9czozOiJtaW4iO2E6Njp7aTowO2k6MzI7aToxO2k6MTY7aToyO2k6ODtpOjM7aTo0O2k6NDtpOjI7aTo1O2k6MTt9czozOiJzZWMiO2E6Njp7aTowO2k6MzI7aToxO2k6MTY7aToyO2k6ODtpOjM7aTo0O2k6NDtpOjI7aTo1O2k6MTt9fQ=='));
+	}
+	elseif (isset($_REQUEST['time']))
+	{
+		$context['sub_template'] = 'thetime';
+		$time = getdate($_REQUEST['time'] == 'now' ? time() : (int) $_REQUEST['time']);
+
+		$context['clockicons'] = array(
+			'year' => array(
+				64 => false,
+				32 => false,
+				16 => false,
+				8  => false,
+				4  => false,
+				2  => false,
+				1  => false
+			),
+			'month' => array(
+				8  => false,
+				4  => false,
+				2  => false,
+				1  => false
+			),
+			'day' => array(
+				16 => false,
+				4  => false,
+				8  => false,
+				2  => false,
+				1  => false
+			),
+			'hour' => array(
+				32 => false,
+				16 => false,
+				8  => false,
+				4  => false,
+				2  => false,
+				1  => false
+			),
+			'min' => array(
+				32 => false,
+				16 => false,
+				8  => false,
+				4  => false,
+				2  => false,
+				1  => false
+			),
+			'sec' => array(
+				32 => false,
+				16 => false,
+				8  => false,
+				4  => false,
+				2  => false,
+				1  => false
+			),
+		);
+
+		$year = $time['year'] % 100;
+		$month = $time['mon'];
+		$day = $time['mday'];
+		$hour = $time['hours'];
+		$min = $time['minutes'];
+		$sec = $time['seconds'];
+
+		foreach ($context['clockicons'] as $t => $vs)
+			foreach ($vs as $v => $dumb)
+			{
+				if ($$t >= $v)
+				{
+					$$t -= $v;
+					$context['clockicons'][$t][$v] = true;
+				}
+			}
+	}
+}
+?>

+ 715 - 0
Sources/Class-Graphics.php

@@ -0,0 +1,715 @@
+<?php
+
+/**
+ * Simple Machines Forum (SMF)
+ *
+ * @package SMF
+ * @author Simple Machines http://www.simplemachines.org
+ * @copyright 2011 Simple Machines
+ * @license http://www.simplemachines.org/about/smf/license.php BSD
+ *
+ * @version 2.0
+ */
+
+/*	Gif Util copyright 2003 by Yamasoft (S/C). All rights reserved.
+	Do not remove this portion of the header, or use these functions except
+	from the original author. To get it, please navigate to:
+	http://www.yamasoft.com/php-gif.zip
+*/
+
+if (!defined('SMF'))
+	die('Hacking attempt...');
+
+/*	Classes used for reading gif files (in case PHP's GD doesn't provide the
+	proper gif-functions).
+*/
+
+class gif_lzw_compression
+{
+	public $MAX_LZW_BITS;
+	public $Fresh, $CodeSize, $SetCodeSize, $MaxCode, $MaxCodeSize, $FirstCode, $OldCode;
+	public $ClearCode, $EndCode, $Next, $Vals, $Stack, $sp, $Buf, $CurBit, $LastBit, $Done, $LastByte;
+
+	public function __construct()
+	{
+		$this->MAX_LZW_BITS = 12;
+		unset($this->Next, $this->Vals, $this->Stack, $this->Buf);
+
+		$this->Next  = range(0, (1 << $this->MAX_LZW_BITS)       - 1);
+		$this->Vals  = range(0, (1 << $this->MAX_LZW_BITS)       - 1);
+		$this->Stack = range(0, (1 << ($this->MAX_LZW_BITS + 1)) - 1);
+		$this->Buf   = range(0, 279);
+	}
+
+	public function decompress($data, &$datLen)
+	{
+		$stLen  = strlen($data);
+		$datLen = 0;
+		$ret    = '';
+
+		$this->LZWCommand($data, true);
+
+		while (($iIndex = $this->LZWCommand($data, false)) >= 0)
+			$ret .= chr($iIndex);
+
+		$datLen = $stLen - strlen($data);
+
+		if ($iIndex != -2)
+			return false;
+
+		return $ret;
+	}
+
+	public function LZWCommand(&$data, $bInit)
+	{
+		if ($bInit)
+		{
+			$this->SetCodeSize = ord($data[0]);
+			$data = substr($data, 1);
+
+			$this->CodeSize    = $this->SetCodeSize + 1;
+			$this->ClearCode   = 1 << $this->SetCodeSize;
+			$this->EndCode     = $this->ClearCode + 1;
+			$this->MaxCode     = $this->ClearCode + 2;
+			$this->MaxCodeSize = $this->ClearCode << 1;
+
+			$this->GetCode($data, $bInit);
+
+			$this->Fresh = 1;
+			for ($i = 0; $i < $this->ClearCode; $i++)
+			{
+				$this->Next[$i] = 0;
+				$this->Vals[$i] = $i;
+			}
+
+			for (; $i < (1 << $this->MAX_LZW_BITS); $i++)
+			{
+				$this->Next[$i] = 0;
+				$this->Vals[$i] = 0;
+			}
+
+			$this->sp = 0;
+			return 1;
+		}
+
+		if ($this->Fresh)
+		{
+			$this->Fresh = 0;
+			do
+			{
+				$this->FirstCode = $this->GetCode($data, $bInit);
+				$this->OldCode   = $this->FirstCode;
+			}
+			while ($this->FirstCode == $this->ClearCode);
+
+			return $this->FirstCode;
+		}
+
+		if ($this->sp > 0)
+		{
+			$this->sp--;
+			return $this->Stack[$this->sp];
+		}
+
+		while (($Code = $this->GetCode($data, $bInit)) >= 0)
+		{
+			if ($Code == $this->ClearCode)
+			{
+				for ($i = 0; $i < $this->ClearCode; $i++)
+				{
+					$this->Next[$i] = 0;
+					$this->Vals[$i] = $i;
+				}
+
+				for (; $i < (1 << $this->MAX_LZW_BITS); $i++)
+				{
+					$this->Next[$i] = 0;
+					$this->Vals[$i] = 0;
+				}
+
+				$this->CodeSize    = $this->SetCodeSize + 1;
+				$this->MaxCodeSize = $this->ClearCode << 1;
+				$this->MaxCode     = $this->ClearCode + 2;
+				$this->sp          = 0;
+				$this->FirstCode   = $this->GetCode($data, $bInit);
+				$this->OldCode     = $this->FirstCode;
+
+				return $this->FirstCode;
+			}
+
+			if ($Code == $this->EndCode)
+				return -2;
+
+			$InCode = $Code;
+			if ($Code >= $this->MaxCode)
+			{
+				$this->Stack[$this->sp] = $this->FirstCode;
+				$this->sp++;
+				$Code = $this->OldCode;
+			}
+
+			while ($Code >= $this->ClearCode)
+			{
+				$this->Stack[$this->sp] = $this->Vals[$Code];
+				$this->sp++;
+
+				if ($Code == $this->Next[$Code]) // Circular table entry, big GIF Error!
+					return -1;
+
+				$Code = $this->Next[$Code];
+			}
+
+			$this->FirstCode = $this->Vals[$Code];
+			$this->Stack[$this->sp] = $this->FirstCode;
+			$this->sp++;
+
+			if (($Code = $this->MaxCode) < (1 << $this->MAX_LZW_BITS))
+			{
+				$this->Next[$Code] = $this->OldCode;
+				$this->Vals[$Code] = $this->FirstCode;
+				$this->MaxCode++;
+
+				if (($this->MaxCode >= $this->MaxCodeSize) && ($this->MaxCodeSize < (1 << $this->MAX_LZW_BITS)))
+				{
+					$this->MaxCodeSize *= 2;
+					$this->CodeSize++;
+				}
+			}
+
+			$this->OldCode = $InCode;
+			if ($this->sp > 0)
+			{
+				$this->sp--;
+				return $this->Stack[$this->sp];
+			}
+		}
+
+		return $Code;
+	}
+
+	public function GetCode(&$data, $bInit)
+	{
+		if ($bInit)
+		{
+			$this->CurBit   = 0;
+			$this->LastBit  = 0;
+			$this->Done     = 0;
+			$this->LastByte = 2;
+
+			return 1;
+		}
+
+		if (($this->CurBit + $this->CodeSize) >= $this->LastBit)
+		{
+			if ($this->Done)
+			{
+				// Ran off the end of my bits...
+				if ($this->CurBit >= $this->LastBit)
+					return 0;
+
+				return -1;
+			}
+
+			$this->Buf[0] = $this->Buf[$this->LastByte - 2];
+			$this->Buf[1] = $this->Buf[$this->LastByte - 1];
+
+			$count = ord($data[0]);
+			$data  = substr($data, 1);
+
+			if ($count)
+			{
+				for ($i = 0; $i < $count; $i++)
+					$this->Buf[2 + $i] = ord($data{$i});
+
+				$data = substr($data, $count);
+			}
+			else
+				$this->Done = 1;
+
+			$this->LastByte = 2 + $count;
+			$this->CurBit = ($this->CurBit - $this->LastBit) + 16;
+			$this->LastBit  = (2 + $count) << 3;
+		}
+
+		$iRet = 0;
+		for ($i = $this->CurBit, $j = 0; $j < $this->CodeSize; $i++, $j++)
+			$iRet |= (($this->Buf[intval($i / 8)] & (1 << ($i % 8))) != 0) << $j;
+
+		$this->CurBit += $this->CodeSize;
+		return $iRet;
+	}
+}
+
+class gif_color_table
+{
+	public $m_nColors;
+	public $m_arColors;
+
+	public function __construct()
+	{
+		unset($this->m_nColors, $this->m_arColors);
+	}
+
+	public function load($lpData, $num)
+	{
+		$this->m_nColors  = 0;
+		$this->m_arColors = array();
+
+		for ($i = 0; $i < $num; $i++)
+		{
+			$rgb = substr($lpData, $i * 3, 3);
+			if (strlen($rgb) < 3)
+				return false;
+
+			$this->m_arColors[] = (ord($rgb[2]) << 16) + (ord($rgb[1]) << 8) + ord($rgb[0]);
+			$this->m_nColors++;
+		}
+
+		return true;
+	}
+
+	public function toString()
+	{
+		$ret = '';
+
+		for ($i = 0; $i < $this->m_nColors; $i++)
+		{
+			$ret .=
+				chr(($this->m_arColors[$i] & 0x000000FF))       . // R
+				chr(($this->m_arColors[$i] & 0x0000FF00) >>  8) . // G
+				chr(($this->m_arColors[$i] & 0x00FF0000) >> 16);  // B
+		}
+
+		return $ret;
+	}
+
+	public function colorIndex($rgb)
+	{
+		$rgb  = intval($rgb) & 0xFFFFFF;
+		$r1   = ($rgb & 0x0000FF);
+		$g1   = ($rgb & 0x00FF00) >>  8;
+		$b1   = ($rgb & 0xFF0000) >> 16;
+		$idx  = -1;
+
+		for ($i = 0; $i < $this->m_nColors; $i++)
+		{
+			$r2 = ($this->m_arColors[$i] & 0x000000FF);
+			$g2 = ($this->m_arColors[$i] & 0x0000FF00) >>  8;
+			$b2 = ($this->m_arColors[$i] & 0x00FF0000) >> 16;
+			$d  = abs($r2 - $r1) + abs($g2 - $g1) + abs($b2 - $b1);
+
+			if (($idx == -1) || ($d < $dif))
+			{
+				$idx = $i;
+				$dif = $d;
+			}
+		}
+
+		return $idx;
+	}
+}
+
+class gif_file_header
+{
+	public $m_lpVer, $m_nWidth, $m_nHeight, $m_bGlobalClr, $m_nColorRes;
+	public $m_bSorted, $m_nTableSize, $m_nBgColor, $m_nPixelRatio;
+	public $m_colorTable;
+
+	public function __construct()
+	{
+		unset($this->m_lpVer, $this->m_nWidth, $this->m_nHeight, $this->m_bGlobalClr, $this->m_nColorRes);
+		unset($this->m_bSorted, $this->m_nTableSize, $this->m_nBgColor, $this->m_nPixelRatio, $this->m_colorTable);
+	}
+
+	public function load($lpData, &$hdrLen)
+	{
+		$hdrLen = 0;
+
+		$this->m_lpVer = substr($lpData, 0, 6);
+		if (($this->m_lpVer != 'GIF87a') && ($this->m_lpVer != 'GIF89a'))
+			return false;
+
+		list ($this->m_nWidth, $this->m_nHeight) = array_values(unpack('v2', substr($lpData, 6, 4)));
+
+		if (!$this->m_nWidth || !$this->m_nHeight)
+			return false;
+
+		$b = ord(substr($lpData, 10, 1));
+		$this->m_bGlobalClr  = ($b & 0x80) ? true : false;
+		$this->m_nColorRes   = ($b & 0x70) >> 4;
+		$this->m_bSorted     = ($b & 0x08) ? true : false;
+		$this->m_nTableSize  = 2 << ($b & 0x07);
+		$this->m_nBgColor    = ord(substr($lpData, 11, 1));
+		$this->m_nPixelRatio = ord(substr($lpData, 12, 1));
+		$hdrLen = 13;
+
+		if ($this->m_bGlobalClr)
+		{
+			$this->m_colorTable = new gif_color_table();
+			if (!$this->m_colorTable->load(substr($lpData, $hdrLen), $this->m_nTableSize))
+				return false;
+
+			$hdrLen += 3 * $this->m_nTableSize;
+		}
+
+		return true;
+	}
+}
+
+class gif_image_header
+{
+	public $m_nLeft, $m_nTop, $m_nWidth, $m_nHeight, $m_bLocalClr;
+	public $m_bInterlace, $m_bSorted, $m_nTableSize, $m_colorTable;
+
+	public function __construct()
+	{
+		unset($this->m_nLeft, $this->m_nTop, $this->m_nWidth, $this->m_nHeight, $this->m_bLocalClr);
+		unset($this->m_bInterlace, $this->m_bSorted, $this->m_nTableSize, $this->m_colorTable);
+	}
+
+	public function load($lpData, &$hdrLen)
+	{
+		$hdrLen = 0;
+
+		// Get the width/height/etc. from the header.
+		list ($this->m_nLeft, $this->m_nTop, $this->m_nWidth, $this->m_nHeight) = array_values(unpack('v4', substr($lpData, 0, 8)));
+
+		if (!$this->m_nWidth || !$this->m_nHeight)
+			return false;
+
+		$b = ord($lpData[8]);
+		$this->m_bLocalClr  = ($b & 0x80) ? true : false;
+		$this->m_bInterlace = ($b & 0x40) ? true : false;
+		$this->m_bSorted    = ($b & 0x20) ? true : false;
+		$this->m_nTableSize = 2 << ($b & 0x07);
+		$hdrLen = 9;
+
+		if ($this->m_bLocalClr)
+		{
+			$this->m_colorTable = new gif_color_table();
+			if (!$this->m_colorTable->load(substr($lpData, $hdrLen), $this->m_nTableSize))
+				return false;
+
+			$hdrLen += 3 * $this->m_nTableSize;
+		}
+
+		return true;
+	}
+}
+
+class gif_image
+{
+	public $m_disp, $m_bUser, $m_bTrans, $m_nDelay, $m_nTrans, $m_lpComm;
+	public $m_gih, $m_data, $m_lzw;
+
+	public function __construct()
+	{
+		unset($this->m_disp, $this->m_bUser, $this->m_nDelay, $this->m_nTrans, $this->m_lpComm, $this->m_data);
+		$this->m_gih = new gif_image_header();
+		$this->m_lzw = new gif_lzw_compression();
+	}
+
+	public function load($data, &$datLen)
+	{
+		$datLen = 0;
+
+		while (true)
+		{
+			$b = ord($data[0]);
+			$data = substr($data, 1);
+			$datLen++;
+
+			switch ($b)
+			{
+			// Extension...
+			case 0x21:
+				$len = 0;
+				if (!$this->skipExt($data, $len))
+					return false;
+
+				$datLen += $len;
+				break;
+
+			// Image...
+			case 0x2C:
+				// Load the header and color table.
+				$len = 0;
+				if (!$this->m_gih->load($data, $len))
+					return false;
+
+				$data = substr($data, $len);
+				$datLen += $len;
+
+				// Decompress the data, and ride on home ;).
+				$len = 0;
+				if (!($this->m_data = $this->m_lzw->decompress($data, $len)))
+					return false;
+
+				$data = substr($data, $len);
+				$datLen += $len;
+
+				if ($this->m_gih->m_bInterlace)
+					$this->deInterlace();
+
+				return true;
+
+			case 0x3B: // EOF
+			default:
+				return false;
+			}
+		}
+		return false;
+	}
+
+	public function skipExt(&$data, &$extLen)
+	{
+		$extLen = 0;
+
+		$b = ord($data[0]);
+		$data = substr($data, 1);
+		$extLen++;
+
+		switch ($b)
+		{
+		// Graphic Control...
+		case 0xF9:
+			$b = ord($data[1]);
+			$this->m_disp   = ($b & 0x1C) >> 2;
+			$this->m_bUser  = ($b & 0x02) ? true : false;
+			$this->m_bTrans = ($b & 0x01) ? true : false;
+			list ($this->m_nDelay) = array_values(unpack('v', substr($data, 2, 2)));
+			$this->m_nTrans = ord($data[4]);
+			break;
+
+		// Comment...
+		case 0xFE:
+			$this->m_lpComm = substr($data, 1, ord($data[0]));
+			break;
+
+		// Plain text...
+		case 0x01:
+			break;
+
+		// Application...
+		case 0xFF:
+			break;
+		}
+
+		// Skip default as defs may change.
+		$b = ord($data[0]);
+		$data = substr($data, 1);
+		$extLen++;
+		while ($b > 0)
+		{
+			$data = substr($data, $b);
+			$extLen += $b;
+			$b    = ord($data[0]);
+			$data = substr($data, 1);
+			$extLen++;
+		}
+		return true;
+	}
+
+	public function deInterlace()
+	{
+		$data = $this->m_data;
+
+		for ($i = 0; $i < 4; $i++)
+		{
+			switch ($i)
+			{
+			case 0:
+				$s = 8;
+				$y = 0;
+				break;
+
+			case 1:
+				$s = 8;
+				$y = 4;
+				break;
+
+			case 2:
+				$s = 4;
+				$y = 2;
+				break;
+
+			case 3:
+				$s = 2;
+				$y = 1;
+				break;
+			}
+
+			for (; $y < $this->m_gih->m_nHeight; $y += $s)
+			{
+				$lne = substr($this->m_data, 0, $this->m_gih->m_nWidth);
+				$this->m_data = substr($this->m_data, $this->m_gih->m_nWidth);
+
+				$data =
+					substr($data, 0, $y * $this->m_gih->m_nWidth) .
+					$lne .
+					substr($data, ($y + 1) * $this->m_gih->m_nWidth);
+			}
+		}
+
+		$this->m_data = $data;
+	}
+}
+
+class gif_file
+{
+	public $header, $image, $data, $loaded;
+
+	public function __construct()
+	{
+		$this->data = '';
+		$this->loaded = false;
+		$this->header = new gif_file_header();
+		$this->image = new gif_image();
+	}
+
+	public function loadFile($filename, $iIndex)
+	{
+		if ($iIndex < 0)
+			return false;
+
+		$this->data = @file_get_contents($filename);
+		if ($this->data === false)
+			return false;
+
+		// Tell the header to load up....
+		$len = 0;
+		if (!$this->header->load($this->data, $len))
+			return false;
+
+		$this->data = substr($this->data, $len);
+
+		// Keep reading (at least once) so we get to the actual image we're looking for.
+		for ($j = 0; $j <= $iIndex; $j++)
+		{
+			$imgLen = 0;
+			if (!$this->image->load($this->data, $imgLen))
+				return false;
+
+			$this->data = substr($this->data, $imgLen);
+		}
+
+		$this->loaded = true;
+		return true;
+	}
+
+	public function get_png_data($background_color)
+	{
+		if (!$this->loaded)
+			return false;
+
+		// Prepare the color table.
+		if ($this->image->m_gih->m_bLocalClr)
+		{
+			$colors = $this->image->m_gih->m_nTableSize;
+			$pal = $this->image->m_gih->m_colorTable->toString();
+
+			if ($background_color != -1)
+				$background_color = $this->image->m_gih->m_colorTable->colorIndex($background_color);
+		}
+		elseif ($this->header->m_bGlobalClr)
+		{
+			$colors = $this->header->m_nTableSize;
+			$pal = $this->header->m_colorTable->toString();
+
+			if ($background_color != -1)
+				$background_color = $this->header->m_colorTable->colorIndex($background_color);
+		}
+		else
+		{
+			$colors = 0;
+			$background_color = -1;
+		}
+
+		if ($background_color == -1)
+			$background_color = $this->header->m_nBgColor;
+
+		$data = &$this->image->m_data;
+		$header = &$this->image->m_gih;
+
+		$i = 0;
+		$bmp = '';
+
+		// Prepare the bitmap itself.
+		for ($y = 0; $y < $this->header->m_nHeight; $y++)
+		{
+			$bmp .= "\x00";
+
+			for ($x = 0; $x < $this->header->m_nWidth; $x++, $i++)
+			{
+				// Is this in the proper range?  If so, get the specific pixel data...
+				if ($x >= $header->m_nLeft && $y >= $header->m_nTop && $x < ($header->m_nLeft + $header->m_nWidth) && $y < ($header->m_nTop + $header->m_nHeight))
+					$bmp .= $data{$i};
+				// Otherwise, this is background...
+				else
+					$bmp .= chr($background_color);
+			}
+		}
+
+		$bmp = gzcompress($bmp, 9);
+
+		// Output the basic signature first of all.
+		$out = "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A";
+
+		// Now, we want the header...
+		$out .= "\x00\x00\x00\x0D";
+		$tmp = 'IHDR' . pack('N', (int) $this->header->m_nWidth) . pack('N', (int) $this->header->m_nHeight) . "\x08\x03\x00\x00\x00";
+		$out .= $tmp . pack('N', smf_crc32($tmp));
+
+		// The palette, assuming we have one to speak of...
+		if ($colors > 0)
+		{
+			$out .= pack('N', (int) $colors * 3);
+			$tmp = 'PLTE' . $pal;
+			$out .= $tmp . pack('N', smf_crc32($tmp));
+		}
+
+		// Do we have any transparency we want to make available?
+		if ($this->image->m_bTrans && $colors > 0)
+		{
+			$out .= pack('N', (int) $colors);
+			$tmp = 'tRNS';
+
+			// Stick each color on - full transparency or none.
+			for ($i = 0; $i < $colors; $i++)
+				$tmp .= $i == $this->image->m_nTrans ? "\x00" : "\xFF";
+
+			$out .= $tmp . pack('N', smf_crc32($tmp));
+		}
+
+		// Here's the data itself!
+		$out .= pack('N', strlen($bmp));
+		$tmp = 'IDAT' . $bmp;
+		$out .= $tmp . pack('N', smf_crc32($tmp));
+
+		// EOF marker...
+		$out .= "\x00\x00\x00\x00" . 'IEND' . "\xAE\x42\x60\x82";
+
+		return $out;
+	}
+}
+
+// crc32 doesn't work as expected on 64-bit functions - make our own.
+// http://www.php.net/crc32#79567
+if (!function_exists('smf_crc32'))
+{
+	function smf_crc32($number)
+	{
+		$crc = crc32($number);
+
+		if ($crc & 0x80000000)
+		{
+			$crc ^= 0xffffffff;
+			$crc += 1;
+			$crc = -$crc;
+		}
+
+		return $crc;
+	}
+}
+
+?>

+ 1019 - 0
Sources/Class-Package.php

@@ -0,0 +1,1019 @@
+<?php
+
+/**
+ * Simple Machines Forum (SMF)
+ *
+ * @package SMF
+ * @author Simple Machines http://www.simplemachines.org
+ * @copyright 2011 Simple Machines
+ * @license http://www.simplemachines.org/about/smf/license.php BSD
+ *
+ * @version 2.0
+ */
+
+if (!defined('SMF'))
+	die('Hacking attempt...');
+
+/*	The following functions are all within the xmlArray class, which is the xml
+	parser.  There are more functions, but these are the ones that should be
+	used from outside the class:
+
+	class xmlArray(string data, bool auto_trim = false,
+			int error_level = error_reporting(), bool is_clone = false)
+		- creates a new xmlArray, which is an simple xml dom parser.
+		- data should be the xml data or an array of, unless is_clone is true.
+		- auto_trim can be used to automatically trim textual data.
+		- error_level specifies whether notices should be generated for
+		  missing elements and attributes.
+		- if is_clone is true, the xmlArray is cloned from another - used
+		  internally only.
+
+	string xmlArray::name()
+		- retrieves the name of the current element, usually ''.
+
+	string xmlArray::fetch(string path, bool get_elements = false)
+		- retrieves the textual value of the specified path.
+		- children are parsed for text, but only textual data is returned
+		  unless get_elements is true.
+
+	xmlArray xmlArray::path(string path, bool return_set = false)
+		- finds any elements that match the path specified.
+		- will always return a set if there is more than one of the element
+		  or return_set is true.
+		- returns in the form of a new xmlArray.
+
+	bool xmlArray::exists(string path)
+		- returns whether the specified path matches at least one element.
+
+	int xmlArray::count(string path)
+		- returns the number of elements the path matches.
+
+	array xmlArray::set(string path)
+		- returns an array of xmlArray's matching the specified path.
+		- this differs from ->path(path, true) in that instead of an xmlArray
+		  of elements, an array of xmlArray's is returned for use with foreach.
+
+	string xmlArray::create_xml(string path = '.')
+		- returns the specified path as an xml file.
+*/
+
+// An xml array.  Reads in xml, allows you to access it simply.  Version 1.1.
+class xmlArray
+{
+	// The array and debugging output level.
+	public $array, $debug_level, $trim;
+
+	// Create an xml array.
+	//	the xml data, trim elements?, debugging output level, reserved.
+	//ie. $xml = new xmlArray(file('data.xml'));
+	public function __construct($data, $auto_trim = false, $level = null, $is_clone = false)
+	{
+		// If we're using this try to get some more memory.
+		@ini_set('memory_limit', '32M');
+
+		// Set the debug level.
+		$this->debug_level = $level !== null ? $level : error_reporting();
+		$this->trim = $auto_trim;
+
+		// Is the data already parsed?
+		if ($is_clone)
+		{
+			$this->array = $data;
+			return;
+		}
+
+		// Is the input an array? (ie. passed from file()?)
+		if (is_array($data))
+			$data = implode('', $data);
+
+		// Remove any xml declaration or doctype, and parse out comments and CDATA.
+		$data = preg_replace('/<!--.*?-->/s', '', $this->_to_cdata(preg_replace(array('/^<\?xml.+?\?' . '>/is', '/<!DOCTYPE[^>]+?' . '>/s'), '', $data)));
+
+		// Now parse the xml!
+		$this->array = $this->_parse($data);
+	}
+
+	// Get the root element's name.
+	//ie. echo $element->name();
+	public function name()
+	{
+		return isset($this->array['name']) ? $this->array['name'] : '';
+	}
+
+	// Get a specified element's value or attribute by path.
+	//	the path to the element to fetch, whether to include elements?
+	//ie. $data = $xml->fetch('html/head/title');
+	public function fetch($path, $get_elements = false)
+	{
+		// Get the element, in array form.
+		$array = $this->path($path);
+
+		if ($array === false)
+			return false;
+
+		// Getting elements into this is a bit complicated...
+		if ($get_elements && !is_string($array))
+		{
+			$temp = '';
+
+			// Use the _xml() function to get the xml data.
+			foreach ($array->array as $val)
+			{
+				// Skip the name and any attributes.
+				if (is_array($val))
+					$temp .= $this->_xml($val, null);
+			}
+
+			// Just get the XML data and then take out the CDATAs.
+			return $this->_to_cdata($temp);
+		}
+
+		// Return the value - taking care to pick out all the text values.
+		return is_string($array) ? $array : $this->_fetch($array->array);
+	}
+
+	// Get an element, returns a new xmlArray.
+	//	the path to the element to get, always return full result set? (ie. don't contract a single item.)
+	//ie. $element = $xml->path('html/body');
+	public function path($path, $return_full = false)
+	{
+		// Split up the path.
+		$path = explode('/', $path);
+
+		// Start with a base array.
+		$array = $this->array;
+
+		// For each element in the path.
+		foreach ($path as $el)
+		{
+			// Deal with sets....
+			if (strpos($el, '[') !== false)
+			{
+				$lvl = (int) substr($el, strpos($el, '[') + 1);
+				$el = substr($el, 0, strpos($el, '['));
+			}
+			// Find an attribute.
+			elseif (substr($el, 0, 1) == '@')
+			{
+				// It simplifies things if the attribute is already there ;).
+				if (isset($array[$el]))
+					return $array[$el];
+				else
+				{
+					if (function_exists('debug_backtrace'))
+					{
+						$trace = debug_backtrace();
+						$i = 0;
+						while ($i < count($trace) && isset($trace[$i]['class']) && $trace[$i]['class'] == get_class($this))
+							$i++;
+						$debug = ' from ' . $trace[$i - 1]['file'] . ' on line ' . $trace[$i - 1]['line'];
+					}
+					else
+						$debug = '';
+
+					// Cause an error.
+					if ($this->debug_level & E_NOTICE)
+						trigger_error('Undefined XML attribute: ' . substr($el, 1) . $debug, E_USER_NOTICE);
+					return false;
+				}
+			}
+			else
+				$lvl = null;
+
+			// Find this element.
+			$array = $this->_path($array, $el, $lvl);
+		}
+
+		// Clean up after $lvl, for $return_full.
+		if ($return_full && (!isset($array['name']) || substr($array['name'], -1) != ']'))
+			$array = array('name' => $el . '[]', $array);
+
+		// Create the right type of class...
+		$newClass = get_class($this);
+
+		// Return a new xmlArray for the result.
+		return $array === false ? false : new $newClass($array, $this->trim, $this->debug_level, true);
+	}
+
+	// Check if an element exists.
+	//	the path to the element to get.
+	//ie. echo $xml->exists('html/body') ? 'y' : 'n';
+	public function exists($path)
+	{
+		// Split up the path.
+		$path = explode('/', $path);
+
+		// Start with a base array.
+		$array = $this->array;
+
+		// For each element in the path.
+		foreach ($path as $el)
+		{
+			// Deal with sets....
+			if (strpos($el, '[') !== false)
+			{
+				$lvl = (int) substr($el, strpos($el, '[') + 1);
+				$el = substr($el, 0, strpos($el, '['));
+			}
+			// Find an attribute.
+			elseif (substr($el, 0, 1) == '@')
+				return isset($array[$el]);
+			else
+				$lvl = null;
+
+			// Find this element.
+			$array = $this->_path($array, $el, $lvl, true);
+		}
+
+		return $array !== false;
+	}
+
+	// Count the number of occurances of a path.
+	//	the path to search for.
+	//ie. echo $xml->count('html/head/meta');
+	public function count($path)
+	{
+		// Get the element, always returning a full set.
+		$temp = $this->path($path, true);
+
+		// Start at zero, then count up all the numeric keys.
+		$i = 0;
+		foreach ($temp->array as $item)
+		{
+			if (is_array($item))
+				$i++;
+		}
+
+		return $i;
+	}
+
+	// Get an array of xmlArray's for use with foreach.
+	//	the path to search for.
+	//ie. foreach ($xml->set('html/body/p') as $p)
+	public function set($path)
+	{
+		// None as yet, just get the path.
+		$array = array();
+		$xml = $this->path($path, true);
+
+		foreach ($xml->array as $val)
+		{
+			// Skip these, they aren't elements.
+			if (!is_array($val) || $val['name'] == '!')
+				continue;
+
+			// Create the right type of class...
+			$newClass = get_class($this);
+
+			// Create a new xmlArray and stick it in the array.
+			$array[] = new $newClass($val, $this->trim, $this->debug_level, true);
+		}
+
+		return $array;
+	}
+
+	// Create an xml file from an xml array.
+	//	the path to the element. (optional)
+	//ie. echo $this->create_xml()
+	public function create_xml($path = null)
+	{
+		// Was a path specified?  If so, use that array.
+		if ($path !== null)
+		{
+			$path = $this->path($path);
+
+			// The path was not found
+			if ($path === false)
+				return false;
+
+			$path = $path->array;
+		}
+		// Just use the current array.
+		else
+			$path = $this->array;
+
+		// Add the xml declaration to the front.
+		return '<?xml version="1.0"?' . '>' . $this->_xml($path, 0);
+	}
+
+	// Output the xml in an array form.
+	//	the path to output.
+	//ie. print_r($xml->to_array());
+	public function to_array($path = null)
+	{
+		// Are we doing a specific path?
+		if ($path !== null)
+		{
+			$path = $this->path($path);
+
+			// The path was not found
+			if ($path === false)
+				return false;
+
+			$path = $path->array;
+		}
+		// No, so just use the current array.
+		else
+			$path = $this->array;
+
+		return $this->_array($path);
+	}
+
+	// Parse data into an array. (privately used...)
+	protected function _parse($data)
+	{
+		// Start with an 'empty' array with no data.
+		$current = array(
+		);
+
+		// Loop until we're out of data.
+		while ($data != '')
+		{
+			// Find and remove the next tag.
+			preg_match('/\A<([\w\-:]+)((?:\s+.+?)?)([\s]?\/)?' . '>/', $data, $match);
+			if (isset($match[0]))
+				$data = preg_replace('/' . preg_quote($match[0], '/') . '/s', '', $data, 1);
+
+			// Didn't find a tag?  Keep looping....
+			if (!isset($match[1]) || $match[1] == '')
+			{
+				// If there's no <, the rest is data.
+				if (strpos($data, '<') === false)
+				{
+					$text_value = $this->_from_cdata($data);
+					$data = '';
+
+					if ($text_value != '')
+						$current[] = array(
+							'name' => '!',
+							'value' => $text_value
+						);
+				}
+				// If the < isn't immediately next to the current position... more data.
+				elseif (strpos($data, '<') > 0)
+				{
+					$text_value = $this->_from_cdata(substr($data, 0, strpos($data, '<')));
+					$data = substr($data, strpos($data, '<'));
+
+					if ($text_value != '')
+						$current[] = array(
+							'name' => '!',
+							'value' => $text_value
+						);
+				}
+				// If we're looking at a </something> with no start, kill it.
+				elseif (strpos($data, '<') !== false && strpos($data, '<') == 0)
+				{
+					if (strpos($data, '<', 1) !== false)
+					{
+						$text_value = $this->_from_cdata(substr($data, 0, strpos($data, '<', 1)));
+						$data = substr($data, strpos($data, '<', 1));
+
+						if ($text_value != '')
+							$current[] = array(
+								'name' => '!',
+								'value' => $text_value
+							);
+					}
+					else
+					{
+						$text_value = $this->_from_cdata($data);
+						$data = '';
+
+						if ($text_value != '')
+							$current[] = array(
+								'name' => '!',
+								'value' => $text_value
+							);
+					}
+				}
+
+				// Wait for an actual occurance of an element.
+				continue;
+			}
+
+			// Create a new element in the array.
+			$el = &$current[];
+			$el['name'] = $match[1];
+
+			// If this ISN'T empty, remove the close tag and parse the inner data.
+			if ((!isset($match[3]) || trim($match[3]) != '/') && (!isset($match[2]) || trim($match[2]) != '/'))
+			{
+				// Because PHP 5.2.0+ seems to croak using regex, we'll have to do this the less fun way.
+				$last_tag_end = strpos($data, '</' . $match[1]. '>');
+				if ($last_tag_end === false)
+					continue;
+
+				$offset = 0;
+				while (1 == 1)
+				{
+					// Where is the next start tag?
+					$next_tag_start = strpos($data, '<' . $match[1], $offset);
+					// If the next start tag is after the last end tag then we've found the right close.
+					if ($next_tag_start === false || $next_tag_start > $last_tag_end)
+						break;
+
+					// If not then find the next ending tag.
+					$next_tag_end = strpos($data, '</' . $match[1]. '>', $offset);
+
+					// Didn't find one? Then just use the last and sod it.
+					if ($next_tag_end === false)
+						break;
+					else
+					{
+						$last_tag_end = $next_tag_end;
+						$offset = $next_tag_start + 1;
+					}
+				}
+				// Parse the insides.
+				$inner_match = substr($data, 0, $last_tag_end);
+				// Data now starts from where this section ends.
+				$data = substr($data, $last_tag_end + strlen('</' . $match[1]. '>'));
+
+				if (!empty($inner_match))
+				{
+					// Parse the inner data.
+					if (strpos($inner_match, '<') !== false)
+						$el += $this->_parse($inner_match);
+					elseif (trim($inner_match) != '')
+					{
+						$text_value = $this->_from_cdata($inner_match);
+						if ($text_value != '')
+							$el[] = array(
+								'name' => '!',
+								'value' => $text_value
+							);
+					}
+				}
+			}
+
+			// If we're dealing with attributes as well, parse them out.
+			if (isset($match[2]) && $match[2] != '')
+			{
+				// Find all the attribute pairs in the string.
+				preg_match_all('/([\w:]+)="(.+?)"/', $match[2], $attr, PREG_SET_ORDER);
+
+				// Set them as @attribute-name.
+				foreach ($attr as $match_attr)
+					$el['@' . $match_attr[1]] = $match_attr[2];
+			}
+		}
+
+		// Return the parsed array.
+		return $current;
+	}
+
+	// Get a specific element's xml. (privately used...)
+	protected function _xml($array, $indent)
+	{
+		$indentation = $indent !== null ? '
+' . str_repeat('	', $indent) : '';
+
+		// This is a set of elements, with no name...
+		if (is_array($array) && !isset($array['name']))
+		{
+			$temp = '';
+			foreach ($array as $val)
+				$temp .= $this->_xml($val, $indent);
+			return $temp;
+		}
+
+		// This is just text!
+		if ($array['name'] == '!')
+			return $indentation . '<![CDATA[' . $array['value'] . ']]>';
+		elseif (substr($array['name'], -2) == '[]')
+			$array['name'] = substr($array['name'], 0, -2);
+
+		// Start the element.
+		$output = $indentation . '<' . $array['name'];
+
+		$inside_elements = false;
+		$output_el = '';
+
+		// Run through and recurively output all the elements or attrbutes inside this.
+		foreach ($array as $k => $v)
+		{
+			if (substr($k, 0, 1) == '@')
+				$output .= ' ' . substr($k, 1) . '="' . $v . '"';
+			elseif (is_array($v))
+			{
+				$output_el .= $this->_xml($v, $indent === null ? null : $indent + 1);
+				$inside_elements = true;
+			}
+		}
+
+		// Indent, if necessary.... then close the tag.
+		if ($inside_elements)
+			$output .= '>' . $output_el . $indentation . '</' . $array['name'] . '>';
+		else
+			$output .= ' />';
+
+		return $output;
+	}
+
+	// Return an element as an array...
+	protected function _array($array)
+	{
+		$return = array();
+		$text = '';
+		foreach ($array as $value)
+		{
+			if (!is_array($value) || !isset($value['name']))
+				continue;
+
+			if ($value['name'] == '!')
+				$text .= $value['value'];
+			else
+				$return[$value['name']] = $this->_array($value);
+		}
+
+		if (empty($return))
+			return $text;
+		else
+			return $return;
+	}
+
+	// Parse out CDATA tags. (htmlspecialchars them...)
+	function _to_cdata($data)
+	{
+		$inCdata = $inComment = false;
+		$output = '';
+
+		$parts = preg_split('~(<!\[CDATA\[|\]\]>|<!--|-->)~', $data, -1, PREG_SPLIT_DELIM_CAPTURE);
+		foreach ($parts as $part)
+		{
+			// Handle XML comments.
+			if (!$inCdata && $part === '<!--')
+				$inComment = true;
+			if ($inComment && $part === '-->')
+				$inComment = false;
+			elseif ($inComment)
+				continue;
+
+			// Handle Cdata blocks.
+			elseif (!$inComment && $part === '<![CDATA[')
+				$inCdata = true;
+			elseif ($inCdata && $part === ']]>')
+				$inCdata = false;
+			elseif ($inCdata)
+				$output .= htmlentities($part, ENT_QUOTES);
+
+			// Everything else is kept as is.
+			else
+				$output .= $part;
+		}
+
+		return $output;
+	}
+
+	// Turn the CDATAs back to normal text.
+	protected function _from_cdata($data)
+	{
+		// Get the HTML translation table and reverse it.
+		$trans_tbl = array_flip(get_html_translation_table(HTML_ENTITIES, ENT_QUOTES));
+
+		// Translate all the entities out.
+		$data = strtr(preg_replace('~&#(\d{1,4});~e', "chr('\$1')", $data), $trans_tbl);
+
+		return $this->trim ? trim($data) : $data;
+	}
+
+	// Given an array, return the text from that array. (recursive and privately used.)
+	protected function _fetch($array)
+	{
+		// Don't return anything if this is just a string.
+		if (is_string($array))
+			return '';
+
+		$temp = '';
+		foreach ($array as $text)
+		{
+			// This means it's most likely an attribute or the name itself.
+			if (!isset($text['name']))
+				continue;
+
+			// This is text!
+			if ($text['name'] == '!')
+				$temp .= $text['value'];
+			// Another element - dive in ;).
+			else
+				$temp .= $this->_fetch($text);
+		}
+
+		// Return all the bits and pieces we've put together.
+		return $temp;
+	}
+
+	// Get a specific array by path, one level down. (privately used...)
+	protected function _path($array, $path, $level, $no_error = false)
+	{
+		// Is $array even an array?  It might be false!
+		if (!is_array($array))
+			return false;
+
+		// Asking for *no* path?
+		if ($path == '' || $path == '.')
+			return $array;
+		$paths = explode('|', $path);
+
+		// A * means all elements of any name.
+		$show_all = in_array('*', $paths);
+
+		$results = array();
+
+		// Check each element.
+		foreach ($array as $value)
+		{
+			if (!is_array($value) || $value['name'] === '!')
+				continue;
+
+			if ($show_all || in_array($value['name'], $paths))
+			{
+				// Skip elements before "the one".
+				if ($level !== null && $level > 0)
+					$level--;
+				else
+					$results[] = $value;
+			}
+		}
+
+		// No results found...
+		if (empty($results))
+		{
+			if (function_exists('debug_backtrace'))
+			{
+				$trace = debug_backtrace();
+				$i = 0;
+				while ($i < count($trace) && isset($trace[$i]['class']) && $trace[$i]['class'] == get_class($this))
+					$i++;
+				$debug = ' from ' . $trace[$i - 1]['file'] . ' on line ' . $trace[$i - 1]['line'];
+			}
+			else
+				$debug = '';
+
+			// Cause an error.
+			if ($this->debug_level & E_NOTICE && !$no_error)
+				trigger_error('Undefined XML element: ' . $path . $debug, E_USER_NOTICE);
+			return false;
+		}
+		// Only one result.
+		elseif (count($results) == 1 || $level !== null)
+			return $results[0];
+		// Return the result set.
+		else
+			return $results + array('name' => $path . '[]');
+	}
+}
+
+// http://www.faqs.org/rfcs/rfc959.html
+if (!class_exists('ftp_connection'))
+{
+	class ftp_connection
+	{
+		public $connection, $error, $last_message, $pasv;
+
+		// Create a new FTP connection...
+		public function __construct($ftp_server, $ftp_port = 21, $ftp_user = 'anonymous', $ftp_pass = '[email protected]')
+		{
+			// Initialize variables.
+			$this->connection = 'no_connection';
+			$this->error = false;
+			$this->pasv = array();
+
+			if ($ftp_server !== null)
+				$this->connect($ftp_server, $ftp_port, $ftp_user, $ftp_pass);
+		}
+
+		public function connect($ftp_server, $ftp_port = 21, $ftp_user = 'anonymous', $ftp_pass = '[email protected]')
+		{
+			if (substr($ftp_server, 0, 6) == 'ftp://')
+				$ftp_server = substr($ftp_server, 6);
+			elseif (substr($ftp_server, 0, 7) == 'ftps://')
+				$ftp_server = 'ssl://' . substr($ftp_server, 7);
+			if (substr($ftp_server, 0, 7) == 'http://')
+				$ftp_server = substr($ftp_server, 7);
+			$ftp_server = strtr($ftp_server, array('/' => '', ':' => '', '@' => ''));
+
+			// Connect to the FTP server.
+			$this->connection = @fsockopen($ftp_server, $ftp_port, $err, $err, 5);
+			if (!$this->connection)
+			{
+				$this->error = 'bad_server';
+				return;
+			}
+
+			// Get the welcome message...
+			if (!$this->check_response(220))
+			{
+				$this->error = 'bad_response';
+				return;
+			}
+
+			// Send the username, it should ask for a password.
+			fwrite($this->connection, 'USER ' . $ftp_user . "\r\n");
+			if (!$this->check_response(331))
+			{
+				$this->error = 'bad_username';
+				return;
+			}
+
+			// Now send the password... and hope it goes okay.
+			fwrite($this->connection, 'PASS ' . $ftp_pass . "\r\n");
+			if (!$this->check_response(230))
+			{
+				$this->error = 'bad_password';
+				return;
+			}
+		}
+
+		public function chdir($ftp_path)
+		{
+			if (!is_resource($this->connection))
+				return false;
+
+			// No slash on the end, please...
+			if ($ftp_path !== '/' && substr($ftp_path, -1) === '/')
+				$ftp_path = substr($ftp_path, 0, -1);
+
+			fwrite($this->connection, 'CWD ' . $ftp_path . "\r\n");
+			if (!$this->check_response(250))
+			{
+				$this->error = 'bad_path';
+				return false;
+			}
+
+			return true;
+		}
+
+		public function chmod($ftp_file, $chmod)
+		{
+			if (!is_resource($this->connection))
+				return false;
+
+			if ($ftp_file == '')
+				$ftp_file = '.';
+
+			// Convert the chmod value from octal (0777) to text ("777").
+			fwrite($this->connection, 'SITE CHMOD ' . decoct($chmod) . ' ' . $ftp_file . "\r\n");
+			if (!$this->check_response(200))
+			{
+				$this->error = 'bad_file';
+				return false;
+			}
+
+			return true;
+		}
+
+		public function unlink($ftp_file)
+		{
+			// We are actually connected, right?
+			if (!is_resource($this->connection))
+				return false;
+
+			// Delete file X.
+			fwrite($this->connection, 'DELE ' . $ftp_file . "\r\n");
+			if (!$this->check_response(250))
+			{
+				fwrite($this->connection, 'RMD ' . $ftp_file . "\r\n");
+
+				// Still no love?
+				if (!$this->check_response(250))
+				{
+					$this->error = 'bad_file';
+					return false;
+				}
+			}
+
+			return true;
+		}
+
+		public function check_response($desired)
+		{
+			// Wait for a response that isn't continued with -, but don't wait too long.
+			$time = time();
+			do
+				$this->last_message = fgets($this->connection, 1024);
+			while ((strlen($this->last_message) < 4 || substr($this->last_message, 0, 1) == ' ' || substr($this->last_message, 3, 1) != ' ') && time() - $time < 5);
+
+			// Was the desired response returned?
+			return is_array($desired) ? in_array(substr($this->last_message, 0, 3), $desired) : substr($this->last_message, 0, 3) == $desired;
+		}
+
+		public function passive()
+		{
+			// We can't create a passive data connection without a primary one first being there.
+			if (!is_resource($this->connection))
+				return false;
+
+			// Request a passive connection - this means, we'll talk to you, you don't talk to us.
+			@fwrite($this->connection, 'PASV' . "\r\n");
+			$time = time();
+			do
+				$response = fgets($this->connection, 1024);
+			while (substr($response, 3, 1) != ' ' && time() - $time < 5);
+
+			// If it's not 227, we weren't given an IP and port, which means it failed.
+			if (substr($response, 0, 4) != '227 ')
+			{
+				$this->error = 'bad_response';
+				return false;
+			}
+
+			// Snatch the IP and port information, or die horribly trying...
+			if (preg_match('~\((\d+),\s*(\d+),\s*(\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+))\)~', $response, $match) == 0)
+			{
+				$this->error = 'bad_response';
+				return false;
+			}
+
+			// This is pretty simple - store it for later use ;).
+			$this->pasv = array('ip' => $match[1] . '.' . $match[2] . '.' . $match[3] . '.' . $match[4], 'port' => $match[5] * 256 + $match[6]);
+
+			return true;
+		}
+
+		public function create_file($ftp_file)
+		{
+			// First, we have to be connected... very important.
+			if (!is_resource($this->connection))
+				return false;
+
+			// I'd like one passive mode, please!
+			if (!$this->passive())
+				return false;
+
+			// Seems logical enough, so far...
+			fwrite($this->connection, 'STOR ' . $ftp_file . "\r\n");
+
+			// Okay, now we connect to the data port.  If it doesn't work out, it's probably "file already exists", etc.
+			$fp = @fsockopen($this->pasv['ip'], $this->pasv['port'], $err, $err, 5);
+			if (!$fp || !$this->check_response(150))
+			{
+				$this->error = 'bad_file';
+				@fclose($fp);
+				return false;
+			}
+
+			// This may look strange, but we're just closing it to indicate a zero-byte upload.
+			fclose($fp);
+			if (!$this->check_response(226))
+			{
+				$this->error = 'bad_response';
+				return false;
+			}
+
+			return true;
+		}
+
+		public function list_dir($ftp_path = '', $search = false)
+		{
+			// Are we even connected...?
+			if (!is_resource($this->connection))
+				return false;
+
+			// Passive... non-agressive...
+			if (!$this->passive())
+				return false;
+
+			// Get the listing!
+			fwrite($this->connection, 'LIST -1' . ($search ? 'R' : '') . ($ftp_path == '' ? '' : ' ' . $ftp_path) . "\r\n");
+
+			// Connect, assuming we've got a connection.
+			$fp = @fsockopen($this->pasv['ip'], $this->pasv['port'], $err, $err, 5);
+			if (!$fp || !$this->check_response(array(150, 125)))
+			{
+				$this->error = 'bad_response';
+				@fclose($fp);
+				return false;
+			}
+
+			// Read in the file listing.
+			$data = '';
+			while (!feof($fp))
+				$data .= fread($fp, 4096);
+			fclose($fp);
+
+			// Everything go okay?
+			if (!$this->check_response(226))
+			{
+				$this->error = 'bad_response';
+				return false;
+			}
+
+			return $data;
+		}
+
+		public function locate($file, $listing = null)
+		{
+			if ($listing === null)
+				$listing = $this->list_dir('', true);
+			$listing = explode("\n", $listing);
+
+			@fwrite($this->connection, 'PWD' . "\r\n");
+			$time = time();
+			do
+				$response = fgets($this->connection, 1024);
+			while ($response[3] != ' ' && time() - $time < 5);
+
+			// Check for 257!
+			if (preg_match('~^257 "(.+?)" ~', $response, $match) != 0)
+				$current_dir = strtr($match[1], array('""' => '"'));
+			else
+				$current_dir = '';
+
+			for ($i = 0, $n = count($listing); $i < $n; $i++)
+			{
+				if (trim($listing[$i]) == '' && isset($listing[$i + 1]))
+				{
+					$current_dir = substr(trim($listing[++$i]), 0, -1);
+					$i++;
+				}
+
+				// Okay, this file's name is:
+				$listing[$i] = $current_dir . '/' . trim(strlen($listing[$i]) > 30 ? strrchr($listing[$i], ' ') : $listing[$i]);
+
+				if ($file[0] == '*' && substr($listing[$i], -(strlen($file) - 1)) == substr($file, 1))
+					return $listing[$i];
+				if (substr($file, -1) == '*' && substr($listing[$i], 0, strlen($file) - 1) == substr($file, 0, -1))
+					return $listing[$i];
+				if (basename($listing[$i]) == $file || $listing[$i] == $file)
+					return $listing[$i];
+			}
+
+			return false;
+		}
+
+		public function create_dir($ftp_dir)
+		{
+			// We must be connected to the server to do something.
+			if (!is_resource($this->connection))
+				return false;
+
+			// Make this new beautiful directory!
+			fwrite($this->connection, 'MKD ' . $ftp_dir . "\r\n");
+			if (!$this->check_response(257))
+			{
+				$this->error = 'bad_file';
+				return false;
+			}
+
+			return true;
+		}
+
+		public function detect_path($filesystem_path, $lookup_file = null)
+		{
+			$username = '';
+
+			if (isset($_SERVER['DOCUMENT_ROOT']))
+			{
+				if (preg_match('~^/home[2]?/([^/]+?)/public_html~', $_SERVER['DOCUMENT_ROOT'], $match))
+				{
+					$username = $match[1];
+
+					$path = strtr($_SERVER['DOCUMENT_ROOT'], array('/home/' . $match[1] . '/' => '', '/home2/' . $match[1] . '/' => ''));
+
+					if (substr($path, -1) == '/')
+						$path = substr($path, 0, -1);
+
+					if (strlen(dirname($_SERVER['PHP_SELF'])) > 1)
+						$path .= dirname($_SERVER['PHP_SELF']);
+				}
+				elseif (substr($filesystem_path, 0, 9) == '/var/www/')
+					$path = substr($filesystem_path, 8);
+				else
+					$path = strtr(strtr($filesystem_path, array('\\' => '/')), array($_SERVER['DOCUMENT_ROOT'] => ''));
+			}
+			else
+				$path = '';
+
+			if (is_resource($this->connection) && $this->list_dir($path) == '')
+			{
+				$data = $this->list_dir('', true);
+
+				if ($lookup_file === null)
+					$lookup_file = $_SERVER['PHP_SELF'];
+
+				$found_path = dirname($this->locate('*' . basename(dirname($lookup_file)) . '/' . basename($lookup_file), $data));
+				if ($found_path == false)
+					$found_path = dirname($this->locate(basename($lookup_file)));
+				if ($found_path != false)
+					$path = $found_path;
+			}
+			elseif (is_resource($this->connection))
+				$found_path = true;
+
+			return array($username, $path, isset($found_path));
+		}
+
+		public function close()
+		{
+			// Goodbye!
+			fwrite($this->connection, 'QUIT' . "\r\n");
+			fclose($this->connection);
+
+			return true;
+		}
+	}
+}
+
+?>

+ 454 - 0
Sources/DbExtra-mysql.php

@@ -0,0 +1,454 @@
+<?php
+
+/**
+ * Simple Machines Forum (SMF)
+ *
+ * @package SMF
+ * @author Simple Machines http://www.simplemachines.org
+ * @copyright 2011 Simple Machines
+ * @license http://www.simplemachines.org/about/smf/license.php BSD
+ *
+ * @version 2.0
+ */
+
+if (!defined('SMF'))
+	die('Hacking attempt...');
+
+/*	This file contains rarely used extended database functionality.
+
+	void db_extra_init()
+		- add this file's functions to the $smcFunc array.
+
+	resource smf_db_backup_table($table, $backup_table)
+		- backup $table to $backup_table.
+		- returns the request handle to the table creation query
+
+	string function smf_db_get_version()
+		- get the version number.
+
+	string db_insert_sql(string table_name)
+		- gets all the necessary INSERTs for the table named table_name.
+		- goes in 250 row segments.
+		- returns the query to insert the data back in.
+		- returns an empty string if the table was empty.
+
+	array smf_db_list_tables($db = false, $filter = false)
+		- lists all tables in the database
+		- could be filtered according to $filter
+		- returns an array of table names. (strings)
+
+	float smf_db_optimize_table($table)
+		- optimize a table
+		- $table - the table to be optimized
+		- returns how much it was gained
+
+	string db_table_sql(string table_name)
+		- dumps the CREATE for the specified table. (by table_name.)
+		- returns the CREATE statement.
+
+*/
+
+// Add the file functions to the $smcFunc array.
+function db_extra_init()
+{
+	global $smcFunc;
+
+	if (!isset($smcFunc['db_backup_table']) || $smcFunc['db_backup_table'] != 'smf_db_backup_table')
+		$smcFunc += array(
+			'db_backup_table' => 'smf_db_backup_table',
+			'db_optimize_table' => 'smf_db_optimize_table',
+			'db_insert_sql' => 'smf_db_insert_sql',
+			'db_table_sql' => 'smf_db_table_sql',
+			'db_list_tables' => 'smf_db_list_tables',
+			'db_get_version' => 'smf_db_get_version',
+		);
+}
+
+// Backup $table to $backup_table.
+function smf_db_backup_table($table, $backup_table)
+{
+	global $smcFunc, $db_prefix;
+
+	$table = str_replace('{db_prefix}', $db_prefix, $table);
+
+	// First, get rid of the old table.
+	$smcFunc['db_query']('', '
+		DROP TABLE IF EXISTS {raw:backup_table}',
+		array(
+			'backup_table' => $backup_table,
+		)
+	);
+
+	// Can we do this the quick way?
+	$result = $smcFunc['db_query']('', '
+		CREATE TABLE {raw:backup_table} LIKE {raw:table}',
+		array(
+			'backup_table' => $backup_table,
+			'table' => $table
+	));
+	// If this failed, we go old school.
+	if ($result)
+	{
+		$request = $smcFunc['db_query']('', '
+			INSERT INTO {raw:backup_table}
+			SELECT *
+			FROM {raw:table}',
+			array(
+				'backup_table' => $backup_table,
+				'table' => $table
+			));
+
+		// Old school or no school?
+		if ($request)
+			return $request;
+	}
+
+	// At this point, the quick method failed.
+	$result = $smcFunc['db_query']('', '
+		SHOW CREATE TABLE {raw:table}',
+		array(
+			'table' => $table,
+		)
+	);
+	list (, $create) = $smcFunc['db_fetch_row']($result);
+	$smcFunc['db_free_result']($result);
+
+	$create = preg_split('/[\n\r]/', $create);
+
+	$auto_inc = '';
+	// Default engine type.
+	$engine = 'MyISAM';
+	$charset = '';
+	$collate = '';
+
+	foreach ($create as $k => $l)
+	{
+		// Get the name of the auto_increment column.
+		if (strpos($l, 'auto_increment'))
+			$auto_inc = trim($l);
+
+		// For the engine type, see if we can work out what it is.
+		if (strpos($l, 'ENGINE') !== false || strpos($l, 'TYPE') !== false)
+		{
+			// Extract the engine type.
+			preg_match('~(ENGINE|TYPE)=(\w+)(\sDEFAULT)?(\sCHARSET=(\w+))?(\sCOLLATE=(\w+))?~', $l, $match);
+
+			if (!empty($match[1]))
+				$engine = $match[1];
+
+			if (!empty($match[2]))
+				$engine = $match[2];
+
+			if (!empty($match[5]))
+				$charset = $match[5];
+
+			if (!empty($match[7]))
+				$collate = $match[7];
+		}
+
+		// Skip everything but keys...
+		if (strpos($l, 'KEY') === false)
+			unset($create[$k]);
+	}
+
+	if (!empty($create))
+		$create = '(
+			' . implode('
+			', $create) . ')';
+	else
+		$create = '';
+
+	$request = $smcFunc['db_query']('', '
+		CREATE TABLE {raw:backup_table} {raw:create}
+		ENGINE={raw:engine}' . (empty($charset) ? '' : ' CHARACTER SET {raw:charset}' . (empty($collate) ? '' : ' COLLATE {raw:collate}')) . '
+		SELECT *
+		FROM {raw:table}',
+		array(
+			'backup_table' => $backup_table,
+			'table' => $table,
+			'create' => $create,
+			'engine' => $engine,
+			'charset' => empty($charset) ? '' : $charset,
+			'collate' => empty($collate) ? '' : $collate,
+		)
+	);
+
+	if ($auto_inc != '')
+	{
+		if (preg_match('~\`(.+?)\`\s~', $auto_inc, $match) != 0 && substr($auto_inc, -1, 1) == ',')
+			$auto_inc = substr($auto_inc, 0, -1);
+
+		$smcFunc['db_query']('', '
+			ALTER TABLE {raw:backup_table}
+			CHANGE COLUMN {raw:column_detail} {raw:auto_inc}',
+			array(
+				'backup_table' => $backup_table,
+				'column_detail' => $match[1],
+				'auto_inc' => $auto_inc,
+			)
+		);
+	}
+
+	return $request;
+}
+
+// Optimize a table - return data freed!
+function smf_db_optimize_table($table)
+{
+	global $smcFunc, $db_name, $db_prefix;
+
+	$table = str_replace('{db_prefix}', $db_prefix, $table);
+
+	// Get how much overhead there is.
+	$request = $smcFunc['db_query']('', '
+			SHOW TABLE STATUS LIKE {string:table_name}',
+			array(
+				'table_name' => str_replace('_', '\_', $table),
+			)
+		);
+	$row = $smcFunc['db_fetch_assoc']($request);
+	$smcFunc['db_free_result']($request);
+
+	$data_before = isset($row['Data_free']) ? $row['Data_free'] : 0;
+	$request = $smcFunc['db_query']('', '
+			OPTIMIZE TABLE `{raw:table}`',
+			array(
+				'table' => $table,
+			)
+		);
+	if (!$request)
+		return -1;
+
+	// How much left?
+	$request = $smcFunc['db_query']('', '
+			SHOW TABLE STATUS LIKE {string:table}',
+			array(
+				'table' => str_replace('_', '\_', $table),
+			)
+		);
+	$row = $smcFunc['db_fetch_assoc']($request);
+	$smcFunc['db_free_result']($request);
+
+	$total_change = isset($row['Data_free']) && $data_before > $row['Data_free'] ? $data_before / 1024 : 0;
+
+	return $total_change;
+}
+
+// List all the tables in the database.
+function smf_db_list_tables($db = false, $filter = false)
+{
+	global $db_name, $smcFunc;
+
+	$db = $db == false ? $db_name : $db;
+	$db = trim($db);
+	$filter = $filter == false ? '' : ' LIKE \'' . $filter . '\'';
+
+	$request = $smcFunc['db_query']('', '
+		SHOW TABLES
+		FROM `{raw:db}`
+		{raw:filter}',
+		array(
+			'db' => $db[0] == '`' ? strtr($db, array('`' => '')) : $db,
+			'filter' => $filter,
+		)
+	);
+	$tables = array();
+	while ($row = $smcFunc['db_fetch_row']($request))
+		$tables[] = $row[0];
+	$smcFunc['db_free_result']($request);
+
+	return $tables;
+}
+
+// Get the content (INSERTs) for a table.
+function smf_db_insert_sql($tableName)
+{
+	global $smcFunc, $db_prefix;
+
+	$tableName = str_replace('{db_prefix}', $db_prefix, $tableName);
+
+	// This will be handy...
+	$crlf = "\r\n";
+
+	// Get everything from the table.
+	$result = $smcFunc['db_query']('', '
+		SELECT /*!40001 SQL_NO_CACHE */ *
+		FROM `{raw:table}`',
+		array(
+			'table' => $tableName,
+		)
+	);
+
+	// The number of rows, just for record keeping and breaking INSERTs up.
+	$num_rows = $smcFunc['db_num_rows']($result);
+	$current_row = 0;
+
+	if ($num_rows == 0)
+		return '';
+
+	$fields = array_keys($smcFunc['db_fetch_assoc']($result));
+	$smcFunc['db_data_seek']($result, 0);
+
+	// Start it off with the basic INSERT INTO.
+	$data = 'INSERT INTO `' . $tableName . '`' . $crlf . "\t" . '(`' . implode('`, `', $fields) . '`)' . $crlf . 'VALUES ';
+
+	// Loop through each row.
+	while ($row = $smcFunc['db_fetch_row']($result))
+	{
+		$current_row++;
+
+		// Get the fields in this row...
+		$field_list = array();
+		for ($j = 0; $j < $smcFunc['db_num_fields']($result); $j++)
+		{
+			// Try to figure out the type of each field. (NULL, number, or 'string'.)
+			if (!isset($row[$j]))
+				$field_list[] = 'NULL';
+			elseif (is_numeric($row[$j]) && (int) $row[$j] == $row[$j])
+				$field_list[] = $row[$j];
+			else
+				$field_list[] = '\'' . $smcFunc['db_escape_string']($row[$j]) . '\'';
+		}
+
+		// 'Insert' the data.
+		$data .= '(' . implode(', ', $field_list) . ')';
+
+		// All done!
+		if ($current_row == $num_rows)
+			$data .= ';' . $crlf;
+		// Start a new INSERT statement after every 250....
+		elseif ($current_row > 249 && $current_row % 250 == 0)
+			$data .= ';' . $crlf . 'INSERT INTO `' . $tableName . '`' . $crlf . "\t" . '(`' . implode('`, `', $fields) . '`)' . $crlf . 'VALUES ';
+		// Otherwise, go to the next line.
+		else
+			$data .= ',' . $crlf . "\t";
+	}
+	$smcFunc['db_free_result']($result);
+
+	// Return an empty string if there were no rows.
+	return $num_rows == 0 ? '' : $data;
+}
+
+// Get the schema (CREATE) for a table.
+function smf_db_table_sql($tableName)
+{
+	global $smcFunc, $db_prefix;
+
+	$tableName = str_replace('{db_prefix}', $db_prefix, $tableName);
+
+	// This will be needed...
+	$crlf = "\r\n";
+
+	// Drop it if it exists.
+	$schema_create = 'DROP TABLE IF EXISTS `' . $tableName . '`;' . $crlf . $crlf;
+
+	// Start the create table...
+	$schema_create .= 'CREATE TABLE `' . $tableName . '` (' . $crlf;
+
+	// Find all the fields.
+	$result = $smcFunc['db_query']('', '
+		SHOW FIELDS
+		FROM `{raw:table}`',
+		array(
+			'table' => $tableName,
+		)
+	);
+	while ($row = $smcFunc['db_fetch_assoc']($result))
+	{
+		// Make the CREATE for this column.
+		$schema_create .= ' `' . $row['Field'] . '` ' . $row['Type'] . ($row['Null'] != 'YES' ? ' NOT NULL' : '');
+
+		// Add a default...?
+		if (!empty($row['Default']) || $row['Null'] !== 'YES')
+		{
+			// Make a special case of auto-timestamp.
+			if ($row['Default'] == 'CURRENT_TIMESTAMP')
+				$schema_create .= ' /*!40102 NOT NULL default CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP */';
+			// Text shouldn't have a default.
+			elseif ($row['Default'] !== null)
+			{
+				// If this field is numeric the default needs no escaping.
+				$type = strtolower($row['Type']);
+				$isNumericColumn = strpos($type, 'int') !== false || strpos($type, 'bool') !== false || strpos($type, 'bit') !== false || strpos($type, 'float') !== false || strpos($type, 'double') !== false || strpos($type, 'decimal') !== false;
+
+				$schema_create .= ' default ' . ($isNumericColumn ? $row['Default'] : '\'' . $smcFunc['db_escape_string']($row['Default']) . '\'');
+			}
+		}
+
+		// And now any extra information. (such as auto_increment.)
+		$schema_create .= ($row['Extra'] != '' ? ' ' . $row['Extra'] : '') . ',' . $crlf;
+	}
+	$smcFunc['db_free_result']($result);
+
+	// Take off the last comma.
+	$schema_create = substr($schema_create, 0, -strlen($crlf) - 1);
+
+	// Find the keys.
+	$result = $smcFunc['db_query']('', '
+		SHOW KEYS
+		FROM `{raw:table}`',
+		array(
+			'table' => $tableName,
+		)
+	);
+	$indexes = array();
+	while ($row = $smcFunc['db_fetch_assoc']($result))
+	{
+		// IS this a primary key, unique index, or regular index?
+		$row['Key_name'] = $row['Key_name'] == 'PRIMARY' ? 'PRIMARY KEY' : (empty($row['Non_unique']) ? 'UNIQUE ' : ($row['Comment'] == 'FULLTEXT' || (isset($row['Index_type']) && $row['Index_type'] == 'FULLTEXT') ? 'FULLTEXT ' : 'KEY ')) . '`' . $row['Key_name'] . '`';
+
+		// Is this the first column in the index?
+		if (empty($indexes[$row['Key_name']]))
+			$indexes[$row['Key_name']] = array();
+
+		// A sub part, like only indexing 15 characters of a varchar.
+		if (!empty($row['Sub_part']))
+			$indexes[$row['Key_name']][$row['Seq_in_index']] = '`' . $row['Column_name'] . '`(' . $row['Sub_part'] . ')';
+		else
+			$indexes[$row['Key_name']][$row['Seq_in_index']] = '`' . $row['Column_name'] . '`';
+	}
+	$smcFunc['db_free_result']($result);
+
+	// Build the CREATEs for the keys.
+	foreach ($indexes as $keyname => $columns)
+	{
+		// Ensure the columns are in proper order.
+		ksort($columns);
+
+		$schema_create .= ',' . $crlf . ' ' . $keyname . ' (' . implode($columns, ', ') . ')';
+	}
+
+	// Now just get the comment and type... (MyISAM, etc.)
+	$result = $smcFunc['db_query']('', '
+		SHOW TABLE STATUS
+		LIKE {string:table}',
+		array(
+			'table' => strtr($tableName, array('_' => '\\_', '%' => '\\%')),
+		)
+	);
+	$row = $smcFunc['db_fetch_assoc']($result);
+	$smcFunc['db_free_result']($result);
+
+	// Probably MyISAM.... and it might have a comment.
+	$schema_create .= $crlf . ') ENGINE=' . (isset($row['Type']) ? $row['Type'] : $row['Engine']) . ($row['Comment'] != '' ? ' COMMENT="' . $row['Comment'] . '"' : '');
+
+	return $schema_create;
+}
+
+// Get the version number.
+function smf_db_get_version()
+{
+	global $smcFunc;
+
+	$request = $smcFunc['db_query']('', '
+		SELECT VERSION()',
+		array(
+		)
+	);
+	list ($ver) = $smcFunc['db_fetch_row']($request);
+	$smcFunc['db_free_result']($request);
+
+	return $ver;
+}
+
+?>

+ 328 - 0
Sources/DbExtra-postgresql.php

@@ -0,0 +1,328 @@
+<?php
+
+/**
+ * Simple Machines Forum (SMF)
+ *
+ * @package SMF
+ * @author Simple Machines http://www.simplemachines.org
+ * @copyright 2011 Simple Machines
+ * @license http://www.simplemachines.org/about/smf/license.php BSD
+ *
+ * @version 2.0
+ */
+
+if (!defined('SMF'))
+	die('Hacking attempt...');
+
+/*	This file contains rarely used extended database functionality.
+
+	void db_extra_init()
+		- add this file's functions to the $smcFunc array.
+
+	resource smf_db_backup_table($table, $backup_table)
+		- backup $table to $backup_table.
+		- returns the request handle to the table creation query
+
+	string function smf_db_get_version()
+		- get the version number.
+
+	string db_insert_sql(string table_name)
+		- gets all the necessary INSERTs for the table named table_name.
+		- goes in 250 row segments.
+		- returns the query to insert the data back in.
+		- returns an empty string if the table was empty.
+
+	array smf_db_list_tables($db = false, $filter = false)
+		- lists all tables in the database
+		- could be filtered according to $filter
+		- returns an array of table names. (strings)
+
+	float smf_db_optimize_table($table)
+		- optimize a table
+		- $table - the table to be optimized
+		- returns how much it was gained
+
+	string db_table_sql(string table_name)
+		- dumps the CREATE for the specified table. (by table_name.)
+		- returns the CREATE statement.
+
+*/
+
+// Add the file functions to the $smcFunc array.
+function db_extra_init()
+{
+	global $smcFunc;
+
+	if (!isset($smcFunc['db_backup_table']) || $smcFunc['db_backup_table'] != 'smf_db_backup_table')
+		$smcFunc += array(
+			'db_backup_table' => 'smf_db_backup_table',
+			'db_optimize_table' => 'smf_db_optimize_table',
+			'db_insert_sql' => 'smf_db_insert_sql',
+			'db_table_sql' => 'smf_db_table_sql',
+			'db_list_tables' => 'smf_db_list_tables',
+			'db_get_version' => 'smf_db_get_version',
+		);
+}
+
+// Backup $table to $backup_table.
+function smf_db_backup_table($table, $backup_table)
+{
+	global $smcFunc, $db_prefix;
+
+	$table = str_replace('{db_prefix}', $db_prefix, $table);
+
+	// Do we need to drop it first?
+	$tables = smf_db_list_tables(false, $backup_table);
+	if (!empty($tables))
+		$smcFunc['db_query']('', '
+			DROP TABLE {raw:backup_table}',
+			array(
+				'backup_table' => $backup_table,
+			)
+		);
+
+	//!!! Should we create backups of sequences as well?
+	$smcFunc['db_query']('', '
+		CREATE TABLE {raw:backup_table}
+		(
+			LIKE {raw:table}
+			INCLUDING DEFAULTS
+		)',
+		array(
+			'backup_table' => $backup_table,
+			'table' => $table,
+		)
+	);
+	$smcFunc['db_query']('', '
+		INSERT INTO {raw:backup_table}
+		SELECT * FROM {raw:table}',
+		array(
+			'backup_table' => $backup_table,
+			'table' => $table,
+		)
+	);
+}
+
+// Optimize a table - return data freed!
+function smf_db_optimize_table($table)
+{
+	global $smcFunc, $db_prefix;
+
+	$table = str_replace('{db_prefix}', $db_prefix, $table);
+
+	$request = $smcFunc['db_query']('', '
+			VACUUM ANALYZE {raw:table}',
+			array(
+				'table' => $table,
+			)
+		);
+	if (!$request)
+		return -1;
+
+	$row = $smcFunc['db_fetch_assoc']($request);
+	$smcFunc['db_free_result']($request);
+
+	if (isset($row['Data_free']))
+			return $row['Data_free'] / 1024;
+	else
+		return 0;
+}
+
+// List all the tables in the database.
+function smf_db_list_tables($db = false, $filter = false)
+{
+	global $smcFunc;
+
+	$request = $smcFunc['db_query']('', '
+		SELECT tablename
+		FROM pg_tables
+		WHERE schemaname = {string:schema_public}' . ($filter == false ? '' : '
+			AND tablename LIKE {string:filter}') . '
+		ORDER BY tablename',
+		array(
+			'schema_public' => 'public',
+			'filter' => $filter,
+		)
+	);
+
+	$tables = array();
+	while ($row = $smcFunc['db_fetch_row']($request))
+		$tables[] = $row[0];
+	$smcFunc['db_free_result']($request);
+
+	return $tables;
+}
+
+// Get the content (INSERTs) for a table.
+function smf_db_insert_sql($tableName)
+{
+	global $smcFunc, $db_prefix;
+
+	$tableName = str_replace('{db_prefix}', $db_prefix, $tableName);
+
+	// This will be handy...
+	$crlf = "\r\n";
+
+	// Get everything from the table.
+	$result = $smcFunc['db_query']('', '
+		SELECT *
+		FROM {raw:table}',
+		array(
+			'table' => $tableName,
+		)
+	);
+
+	// The number of rows, just for record keeping and breaking INSERTs up.
+	$num_rows = $smcFunc['db_num_rows']($result);
+
+	if ($num_rows == 0)
+		return '';
+
+	$fields = array_keys($smcFunc['db_fetch_assoc']($result));
+	$smcFunc['db_data_seek']($result, 0);
+
+	// Start it off with the basic INSERT INTO.
+	$data = '';
+	$insert_msg = $crlf . 'INSERT INTO ' . $tableName . $crlf . "\t" . '(' . implode(', ', $fields) . ')' . $crlf . 'VALUES ' . $crlf . "\t";
+
+	// Loop through each row.
+	while ($row = $smcFunc['db_fetch_row']($result))
+	{
+		// Get the fields in this row...
+		$field_list = array();
+		for ($j = 0; $j < $smcFunc['db_num_fields']($result); $j++)
+		{
+			// Try to figure out the type of each field. (NULL, number, or 'string'.)
+			if (!isset($row[$j]))
+				$field_list[] = 'NULL';
+			elseif (is_numeric($row[$j]) && (int) $row[$j] == $row[$j])
+				$field_list[] = $row[$j];
+			else
+				$field_list[] = '\'' . $smcFunc['db_escape_string']($row[$j]) . '\'';
+		}
+
+		// 'Insert' the data.
+		$data .= $insert_msg . '(' . implode(', ', $field_list) . ');';
+	}
+	$smcFunc['db_free_result']($result);
+
+	// Return an empty string if there were no rows.
+	return $num_rows == 0 ? '' : $data;
+}
+
+// Get the schema (CREATE) for a table.
+function smf_db_table_sql($tableName)
+{
+	global $smcFunc, $db_prefix;
+
+	$tableName = str_replace('{db_prefix}', $db_prefix, $tableName);
+
+	// This will be needed...
+	$crlf = "\r\n";
+
+	// Start the create table...
+	$schema_create = 'CREATE TABLE ' . $tableName . ' (' . $crlf;
+	$index_create = '';
+	$seq_create = '';
+
+	// Find all the fields.
+	$result = $smcFunc['db_query']('', '
+		SELECT column_name, column_default, is_nullable, data_type, character_maximum_length
+		FROM information_schema.columns
+		WHERE table_name = {string:table}
+		ORDER BY ordinal_position',
+		array(
+			'table' => $tableName,
+		)
+	);
+	while ($row = $smcFunc['db_fetch_assoc']($result))
+	{
+		if ($row['data_type'] == 'character varying')
+			$row['data_type'] = 'varchar';
+		elseif ($row['data_type'] == 'character')
+			$row['data_type'] = 'char';
+		if ($row['character_maximum_length'])
+			$row['data_type'] .= '(' . $row['character_maximum_length'] . ')';
+
+		// Make the CREATE for this column.
+		$schema_create .= ' "' . $row['column_name'] . '" ' . $row['data_type'] . ($row['is_nullable'] != 'YES' ? ' NOT NULL' : '');
+
+		// Add a default...?
+		if (trim($row['column_default']) != '')
+		{
+			$schema_create .= ' default ' . $row['column_default'] . '';
+
+			// Auto increment?
+			if (preg_match('~nextval\(\'(.+?)\'(.+?)*\)~i', $row['column_default'], $matches) != 0)
+			{
+				// Get to find the next variable first!
+				$count_req = $smcFunc['db_query']('', '
+					SELECT MAX("{raw:column}")
+					FROM {raw:table}',
+					array(
+						'column' => $row['column_name'],
+						'table' => $tableName,
+					)
+				);
+				list ($max_ind) = $smcFunc['db_fetch_row']($count_req);
+				$smcFunc['db_free_result']($count_req);
+				// Get the right bloody start!
+				$seq_create .= 'CREATE SEQUENCE ' . $matches[1] . ' START WITH ' . ($max_ind + 1) . ';' . $crlf . $crlf;
+			}
+		}
+
+		$schema_create .= ',' . $crlf;
+	}
+	$smcFunc['db_free_result']($result);
+
+	// Take off the last comma.
+	$schema_create = substr($schema_create, 0, -strlen($crlf) - 1);
+
+	$result = $smcFunc['db_query']('', '
+		SELECT CASE WHEN i.indisprimary THEN 1 ELSE 0 END AS is_primary, pg_get_indexdef(i.indexrelid) AS inddef
+		FROM pg_class AS c
+			INNER JOIN pg_index AS i ON (i.indrelid = c.oid)
+			INNER JOIN pg_class AS c2 ON (c2.oid = i.indexrelid)
+		WHERE c.relname = {string:table}',
+		array(
+			'table' => $tableName,
+		)
+	);
+	$indexes = array();
+	while ($row = $smcFunc['db_fetch_assoc']($result))
+	{
+		if ($row['is_primary'])
+		{
+			if (preg_match('~\(([^\)]+?)\)~i', $row['inddef'], $matches) == 0)
+				continue;
+
+			$index_create .= $crlf . 'ALTER TABLE ' . $tableName . ' ADD PRIMARY KEY ("' . $matches[1] . '");';
+		}
+		else
+			$index_create .= $crlf . $row['inddef'] . ';';
+	}
+	$smcFunc['db_free_result']($result);
+
+	// Finish it off!
+	$schema_create .= $crlf . ');';
+
+	return $seq_create . $schema_create . $index_create;
+}
+
+// Get the version number.
+function smf_db_get_version()
+{
+	global $smcFunc;
+
+	$request = $smcFunc['db_query']('', '
+		SHOW server_version',
+		array(
+		)
+	);
+	list ($ver) = $smcFunc['db_fetch_row']($request);
+	$smcFunc['db_free_result']($request);
+
+	return $ver;
+}
+
+?>

+ 346 - 0
Sources/DbExtra-sqlite.php

@@ -0,0 +1,346 @@
+<?php
+
+/**
+ * Simple Machines Forum (SMF)
+ *
+ * @package SMF
+ * @author Simple Machines http://www.simplemachines.org
+ * @copyright 2011 Simple Machines
+ * @license http://www.simplemachines.org/about/smf/license.php BSD
+ *
+ * @version 2.0
+ */
+
+if (!defined('SMF'))
+	die('Hacking attempt...');
+
+/*	This file contains rarely used extended database functionality.
+
+	void db_extra_init()
+		- add this file's functions to the $smcFunc array.
+
+	resource smf_db_backup_table($table, $backup_table)
+		- backup $table to $backup_table.
+		- returns the request handle to the table creation query
+
+	string function smf_db_get_version()
+		- get the version number.
+
+	string db_insert_sql(string table_name)
+		- gets all the necessary INSERTs for the table named table_name.
+		- goes in 250 row segments.
+		- returns the query to insert the data back in.
+		- returns an empty string if the table was empty.
+
+	array smf_db_list_tables($db = false, $filter = false)
+		- lists all tables in the database
+		- could be filtered according to $filter
+		- returns an array of table names. (strings)
+
+	float smf_db_optimize_table($table)
+		- optimize a table
+		- $table - the table to be optimized
+		- returns how much it was gained
+
+	string db_table_sql(string table_name)
+		- dumps the CREATE for the specified table. (by table_name.)
+		- returns the CREATE statement.
+
+*/
+
+// Add the file functions to the $smcFunc array.
+function db_extra_init()
+{
+	global $smcFunc;
+
+	if (!isset($smcFunc['db_backup_table']) || $smcFunc['db_backup_table'] != 'smf_db_backup_table')
+		$smcFunc += array(
+			'db_backup_table' => 'smf_db_backup_table',
+			'db_optimize_table' => 'smf_db_optimize_table',
+			'db_insert_sql' => 'smf_db_insert_sql',
+			'db_table_sql' => 'smf_db_table_sql',
+			'db_list_tables' => 'smf_db_list_tables',
+			'db_get_backup' => 'smf_db_get_backup',
+			'db_get_version' => 'smf_db_get_version',
+		);
+}
+
+// Backup $table to $backup_table.
+function smf_db_backup_table($table, $backup_table)
+{
+	global $smcFunc, $db_prefix;
+
+	$table = str_replace('{db_prefix}', $db_prefix, $table);
+
+	$result = $smcFunc['db_query']('', '
+		SELECT sql
+		FROM sqlite_master
+		WHERE type = {string:txttable}
+			AND name = {string:table}',
+		array(
+			'table' => $table,
+			'txttable' => 'table'
+		)
+	);
+	list ($create) = $smcFunc['db_fetch_row']($result);
+	$smcFunc['db_free_result']($result);
+
+	$create = preg_split('/[\n\r]/', $create);
+	$auto_inc = '';
+
+	// Remove the first line and check to see if the second one contain useless info.
+	unset($create[0]);
+	if (trim($create[1]) == '(')
+		unset($create[1]);
+	if (trim($create[count($create)]) == ')')
+		unset($create[count($create)]);
+
+	foreach ($create as $k => $l)
+	{
+		// Get the name of the auto_increment column.
+		if (strpos($l, 'primary') || strpos($l, 'PRIMARY'))
+			$auto_inc = trim($l);
+
+		// Skip everything but keys...
+		if ((strpos($l, 'KEY') !== false && strpos($l, 'PRIMARY KEY') === false) || strpos($l, $table) !== false || strpos(trim($l), 'PRIMARY KEY') === 0)
+			unset($create[$k]);
+	}
+
+	if (!empty($create))
+		$create = '(
+			' . implode('
+			', $create) . ')';
+	else
+		$create = '';
+
+	// Is there an extra junk at the end?
+	if (substr($create, -2, 1) == ',')
+		$create = substr($create, 0, -2) . ')';
+	if (substr($create, -2) == '))')
+		$create = substr($create, 0, -1);
+
+	$smcFunc['db_query']('', '
+		DROP TABLE {raw:backup_table}',
+		array(
+			'backup_table' => $backup_table,
+			'db_error_skip' => true,
+		)
+	);
+
+	$request = $smcFunc['db_quote']('
+		CREATE TABLE {raw:backup_table} {raw:create}',
+		array(
+			'backup_table' => $backup_table,
+			'create' => $create,
+	));
+
+	$smcFunc['db_query']('', '
+		CREATE TABLE {raw:backup_table} {raw:create}',
+		array(
+			'backup_table' => $backup_table,
+			'create' => $create,
+	));
+
+	$request = $smcFunc['db_query']('', '
+		INSERT INTO {raw:backup_table}
+		SELECT *
+		FROM {raw:table}',
+		array(
+			'backup_table' => $backup_table,
+			'table' => $table,
+	));
+
+	return $request;
+}
+
+// Optimize a table - return data freed!
+function smf_db_optimize_table($table)
+{
+	global $smcFunc, $db_prefix;
+
+	$table = str_replace('{db_prefix}', $db_prefix, $table);
+
+	$request = $smcFunc['db_query']('', '
+		VACUUM {raw:table}',
+		array(
+			'table' => $table,
+		)
+	);
+	if (!$request)
+		return -1;
+
+	$row = $smcFunc['db_fetch_assoc']($request);
+	$smcFunc['db_free_result']($request);
+
+	// The function returns nothing.
+	return 0;
+}
+
+// List all the tables in the database.
+function smf_db_list_tables($db = false, $filter = false)
+{
+	global $smcFunc;
+
+	$filter = $filter == false ? '' : ' AND name LIKE \'' . str_replace("\_", "_", $filter) . '\'';
+
+	$request = $smcFunc['db_query']('', '
+		SELECT name
+		FROM sqlite_master
+		WHERE type = {string:type}
+		{raw:filter}
+		ORDER BY name',
+		array(
+			'type' => 'table',
+			'filter' => $filter,
+		)
+	);
+	$tables = array();
+	while ($row = $smcFunc['db_fetch_row']($request))
+		$tables[] = $row[0];
+	$smcFunc['db_free_result']($request);
+
+	return $tables;
+}
+
+// Get the content (INSERTs) for a table.
+function smf_db_insert_sql($tableName)
+{
+	global $smcFunc, $db_prefix;
+
+	$tableName = str_replace('{db_prefix}', $db_prefix, $tableName);
+
+	// This will be handy...
+	$crlf = "\r\n";
+
+	// Get everything from the table.
+	$result = $smcFunc['db_query']('', '
+		SELECT *
+		FROM {raw:table}',
+		array(
+			'table' => $tableName,
+		)
+	);
+
+	// The number of rows, just for record keeping and breaking INSERTs up.
+	$num_rows = $smcFunc['db_num_rows']($result);
+
+	if ($num_rows == 0)
+		return '';
+
+	$fields = array_keys($smcFunc['db_fetch_assoc']($result));
+
+	// SQLite fetches an array so we need to filter out the numberic index for the columns.
+	foreach ($fields as $key => $name)
+		if (is_numeric($name))
+			unset($fields[$key]);
+
+	$smcFunc['db_data_seek']($result, 0);
+
+	// Start it off with the basic INSERT INTO.
+	$data = 'BEGIN TRANSACTION;' . $crlf;
+
+	// Loop through each row.
+	while ($row = $smcFunc['db_fetch_row']($result))
+	{
+		// Get the fields in this row...
+		$field_list = array();
+		for ($j = 0; $j < $smcFunc['db_num_fields']($result); $j++)
+		{
+			// Try to figure out the type of each field. (NULL, number, or 'string'.)
+			if (!isset($row[$j]))
+				$field_list[] = 'NULL';
+			elseif (is_numeric($row[$j]) && (int) $row[$j] == $row[$j])
+				$field_list[] = $row[$j];
+			else
+				$field_list[] = '\'' . $smcFunc['db_escape_string']($row[$j]) . '\'';
+		}
+		$data .= 'INSERT INTO ' . $tableName . ' (' . implode(', ', $fields) . ') VALUES (' . implode(', ', $field_list) . ');' . $crlf;
+	}
+	$smcFunc['db_free_result']($result);
+
+	// Return an empty string if there were no rows.
+	return $num_rows == 0 ? '' : $data . 'COMMIT;' . $crlf;
+}
+
+// Get the schema (CREATE) for a table.
+function smf_db_table_sql($tableName)
+{
+	global $smcFunc, $db_prefix;
+
+	$tableName = str_replace('{db_prefix}', $db_prefix, $tableName);
+
+	// This will be needed...
+	$crlf = "\r\n";
+
+	// Start the create table...
+	$schema_create = '';
+	$index_create = '';
+
+	// Let's get the create statement directly from SQLite.
+	$result = $smcFunc['db_query']('', '
+		SELECT sql
+		FROM sqlite_master
+		WHERE type = {string:type}
+			AND name = {string:table_name}',
+		array(
+			'type' => 'table',
+			'table_name' => $tableName,
+		)
+	);
+	list ($schema_create) = $smcFunc['db_fetch_row']($result);
+	$smcFunc['db_free_result']($result);
+
+	// Now the indexes.
+	$result = $smcFunc['db_query']('', '
+		SELECT sql
+		FROM sqlite_master
+		WHERE type = {string:type}
+			AND tbl_name = {string:table_name}',
+		array(
+			'type' => 'index',
+			'table_name' => $tableName,
+		)
+	);
+	$indexes = array();
+	while ($row = $smcFunc['db_fetch_assoc']($result))
+		if (trim($row['sql']) != '')
+			$indexes[] = $row['sql'];
+	$smcFunc['db_free_result']($result);
+
+	$index_create .= implode(';' . $crlf, $indexes);
+	$schema_create = empty($indexes) ? rtrim($schema_create) : $schema_create . ';' . $crlf . $crlf;
+
+	return $schema_create . $index_create;
+}
+
+// Get the version number.
+function smf_db_get_version()
+{
+	return sqlite_libversion();
+}
+
+// Simple return the database - and die!
+function smf_db_get_backup()
+{
+	global $db_name;
+
+	$db_file = substr($db_name, -3) === '.db' ? $db_name : $db_name . '.db';
+
+	// Add more info if zipped...
+	$ext = '';
+	if (isset($_REQUEST['compress']) && function_exists('gzencode'))
+		$ext = '.gz';
+
+	// Do the remaining headers.
+	header('Content-Disposition: attachment; filename="' . $db_file . $ext . '"');
+	header('Cache-Control: private');
+	header('Connection: close');
+
+	// Literally dump the contents.  Try reading the file first.
+	if (@readfile($db_file) == null)
+		echo file_get_contents($db_file);
+
+	obExit(false);
+}
+
+?>

+ 582 - 0
Sources/DbPackages-mysql.php

@@ -0,0 +1,582 @@
+<?php
+
+/**
+ * Simple Machines Forum (SMF)
+ *
+ * @package SMF
+ * @author Simple Machines http://www.simplemachines.org
+ * @copyright 2011 Simple Machines
+ * @license http://www.simplemachines.org/about/smf/license.php BSD
+ *
+ * @version 2.0
+ */
+
+if (!defined('SMF'))
+	die('Hacking attempt...');
+
+/*	This file contains database functionality specifically designed for packages to utilize.
+
+	bool smf_db_create_table(string table_name, array columns, array indexes = array(),
+		array parameters = array(), string if_exists = 'ignore')
+		- Can be used to create a table without worrying about schema compatabilities.
+		- If the table exists will, by default, do nothing.
+		- Builds table with columns as passed to it - at least one column must be sent.
+		  The columns array should have one sub-array for each column - these sub arrays contain:
+			+ 'name' = Column name
+			+ 'type' = Type of column - values from (smallint,mediumint,int,text,varchar,char,tinytext,mediumtext,largetext)
+			+ 'size' => Size of column (If applicable) - for example 255 for a large varchar, 10 for an int etc. If not
+						set SMF will pick a size.
+			+ 'default' = Default value - do not set if no default required.
+			+ 'null' => Can it be null (true or false) - if not set default will be false.
+			+ 'auto' => Set to true to make it an auto incrementing column. Set to a numerical value to set
+						from what it should begin counting.
+		- Adds indexes as specified within indexes parameter. Each index should be a member of $indexes. Values are:
+			+ 'name' => Index name (If left empty SMF will generate).
+			+ 'type' => Type of index. Choose from 'primary', 'unique' or 'index'. If not set will default to 'index'.
+			+ 'columns' => Array containing columns that form part of key - in the order the index is to be created.
+		- parameters: (None yet)
+		- if_exists values:
+			+ 'ignore' will do nothing if the table exists. (And will return true)
+			+ 'overwrite' will drop any existing table of the same name.
+			+ 'error' will return false if the table already exists.
+
+*/
+
+// Add the file functions to the $smcFunc array.
+function db_packages_init()
+{
+	global $smcFunc, $reservedTables, $db_package_log, $db_prefix;
+
+	if (!isset($smcFunc['db_create_table']) || $smcFunc['db_create_table'] != 'smf_db_create_table')
+	{
+		$smcFunc += array(
+			'db_add_column' => 'smf_db_add_column',
+			'db_add_index' => 'smf_db_add_index',
+			'db_calculate_type' => 'smf_db_calculate_type',
+			'db_change_column' => 'smf_db_change_column',
+			'db_create_table' => 'smf_db_create_table',
+			'db_drop_table' => 'smf_db_drop_table',
+			'db_table_structure' => 'smf_db_table_structure',
+			'db_list_columns' => 'smf_db_list_columns',
+			'db_list_indexes' => 'smf_db_list_indexes',
+			'db_remove_column' => 'smf_db_remove_column',
+			'db_remove_index' => 'smf_db_remove_index',
+		);
+		$db_package_log = array();
+	}
+
+	// We setup an array of SMF tables we can't do auto-remove on - in case a mod writer cocks it up!
+	$reservedTables = array('admin_info_files', 'approval_queue', 'attachments', 'ban_groups', 'ban_items',
+		'board_permissions', 'boards', 'calendar', 'calendar_holidays', 'categories', 'collapsed_categories',
+		'custom_fields', 'group_moderators', 'log_actions', 'log_activity', 'log_banned', 'log_boards',
+		'log_digest', 'log_errors', 'log_floodcontrol', 'log_group_requests', 'log_karma', 'log_mark_read',
+		'log_notify', 'log_online', 'log_packages', 'log_polls', 'log_reported', 'log_reported_comments',
+		'log_scheduled_tasks', 'log_search_messages', 'log_search_results', 'log_search_subjects',
+		'log_search_topics', 'log_topics', 'mail_queue', 'membergroups', 'members', 'message_icons',
+		'messages', 'moderators', 'package_servers', 'permission_profiles', 'permissions', 'personal_messages',
+		'pm_recipients', 'poll_choices', 'polls', 'scheduled_tasks', 'sessions', 'settings', 'smileys',
+		'themes', 'topics');
+	foreach ($reservedTables as $k => $table_name)
+		$reservedTables[$k] = strtolower($db_prefix . $table_name);
+
+	// We in turn may need the extra stuff.
+	db_extend('extra');
+}
+
+// Create a table.
+function smf_db_create_table($table_name, $columns, $indexes = array(), $parameters = array(), $if_exists = 'ignore', $error = 'fatal')
+{
+	global $reservedTables, $smcFunc, $db_package_log, $db_prefix, $db_character_set;
+
+	// Strip out the table name, we might not need it in some cases
+	$real_prefix = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $db_prefix, $match) === 1 ? $match[3] : $db_prefix;
+
+	// With or without the database name, the fullname looks like this.
+	$full_table_name = str_replace('{db_prefix}', $real_prefix, $table_name);
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	// First - no way do we touch SMF tables.
+	if (in_array(strtolower($table_name), $reservedTables))
+		return false;
+
+	// Log that we'll want to remove this on uninstall.
+	$db_package_log[] = array('remove_table', $table_name);
+
+	// Slightly easier on MySQL than the others...
+	$tables = $smcFunc['db_list_tables']();
+	if (in_array($full_table_name, $tables))
+	{
+		// This is a sad day... drop the table? If not, return false (error) by default.
+		if ($if_exists == 'overwrite')
+			$smcFunc['db_drop_table']($table_name);
+		else
+			return $if_exists == 'ignore';
+	}
+
+	// Righty - let's do the damn thing!
+	$table_query = 'CREATE TABLE ' . $table_name . "\n" . '(';
+	foreach ($columns as $column)
+	{
+		// Auto increment is easy here!
+		if (!empty($column['auto']))
+		{
+			$default = 'auto_increment';
+		}
+		elseif (isset($column['default']) && $column['default'] !== null)
+			$default = 'default \'' . $smcFunc['db_escape_string']($column['default']) . '\'';
+		else
+			$default = '';
+
+		// Sort out the size... and stuff...
+		$column['size'] = isset($column['size']) && is_numeric($column['size']) ? $column['size'] : null;
+		list ($type, $size) = $smcFunc['db_calculate_type']($column['type'], $column['size']);
+
+		// Allow unsigned integers (mysql only)
+		$unsigned = in_array($type, array('int', 'tinyint', 'smallint', 'mediumint', 'bigint')) && !empty($column['unsigned']) ? 'unsigned ' : '';
+
+		if ($size !== null)
+			$type = $type . '(' . $size . ')';
+
+		// Now just put it together!
+		$table_query .= "\n\t`" .$column['name'] . '` ' . $type . ' ' . (!empty($unsigned) ? $unsigned : '') . (!empty($column['null']) ? '' : 'NOT NULL') . ' ' . $default . ',';
+	}
+
+	// Loop through the indexes next...
+	foreach ($indexes as $index)
+	{
+		$columns = implode(',', $index['columns']);
+
+		// Is it the primary?
+		if (isset($index['type']) && $index['type'] == 'primary')
+			$table_query .= "\n\t" . 'PRIMARY KEY (' . implode(',', $index['columns']) . '),';
+		else
+		{
+			if (empty($index['name']))
+				$index['name'] = implode('_', $index['columns']);
+			$table_query .= "\n\t" . (isset($index['type']) && $index['type'] == 'unique' ? 'UNIQUE' : 'KEY') . ' ' . $index['name'] . ' (' . $columns . '),';
+		}
+	}
+
+	// No trailing commas!
+	if (substr($table_query, -1) == ',')
+		$table_query = substr($table_query, 0, -1);
+
+	$table_query .= ') ENGINE=MyISAM';
+	if (!empty($db_character_set) && $db_character_set == 'utf8')
+		$table_query .= ' DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci';
+
+	// Create the table!
+	$smcFunc['db_query']('', $table_query,
+		array(
+			'security_override' => true,
+		)
+	);
+}
+
+// Drop a table.
+function smf_db_drop_table($table_name, $parameters = array(), $error = 'fatal')
+{
+	global $reservedTables, $smcFunc, $db_prefix;
+
+	// After stripping away the database name, this is what's left.
+	$real_prefix = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $db_prefix, $match) === 1 ? $match[3] : $db_prefix;
+
+	// Get some aliases.
+	$full_table_name = str_replace('{db_prefix}', $real_prefix, $table_name);
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	// God no - dropping one of these = bad.
+	if (in_array(strtolower($table_name), $reservedTables))
+		return false;
+
+	// Does it exist?
+	if (in_array($full_table_name, $smcFunc['db_list_tables']()))
+	{
+		$query = 'DROP TABLE ' . $table_name;
+		$smcFunc['db_query']('',
+			$query,
+			array(
+				'security_override' => true,
+			)
+		);
+
+		return true;
+	}
+
+	// Otherwise do 'nout.
+	return false;
+}
+
+// Add a column.
+function smf_db_add_column($table_name, $column_info, $parameters = array(), $if_exists = 'update', $error = 'fatal')
+{
+	global $smcFunc, $db_package_log, $txt, $db_prefix;
+
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	// Log that we will want to uninstall this!
+	$db_package_log[] = array('remove_column', $table_name, $column_info['name']);
+
+	// Does it exist - if so don't add it again!
+	$columns = $smcFunc['db_list_columns']($table_name, false);
+	foreach ($columns as $column)
+		if ($column == $column_info['name'])
+		{
+			// If we're going to overwrite then use change column.
+			if ($if_exists == 'update')
+				return $smcFunc['db_change_column']($table_name, $column_info['name'], $column_info);
+			else
+				return false;
+		}
+
+	// Get the specifics...
+	$column_info['size'] = isset($column_info['size']) && is_numeric($column_info['size']) ? $column_info['size'] : null;
+	list ($type, $size) = $smcFunc['db_calculate_type']($column_info['type'], $column_info['size']);
+
+	// Allow unsigned integers (mysql only)
+	$unsigned = in_array($type, array('int', 'tinyint', 'smallint', 'mediumint', 'bigint')) && !empty($column_info['unsigned']) ? 'unsigned ' : '';
+
+	if ($size !== null)
+		$type = $type . '(' . $size . ')';
+
+	// Now add the thing!
+	$query = '
+		ALTER TABLE ' . $table_name . '
+		ADD `' . $column_info['name'] . '` ' . $type . ' ' . (!empty($unsigned) ? $unsigned : '') . (empty($column_info['null']) ? 'NOT NULL' : '') . ' ' .
+			(!isset($column_info['default']) ? '' : 'default \'' . $smcFunc['db_escape_string']($column_info['default']) . '\'') . ' ' .
+			(empty($column_info['auto']) ? '' : 'auto_increment primary key') . ' ';
+	$smcFunc['db_query']('', $query,
+		array(
+			'security_override' => true,
+		)
+	);
+
+	return true;
+}
+
+// Remove a column.
+function smf_db_remove_column($table_name, $column_name, $parameters = array(), $error = 'fatal')
+{
+	global $smcFunc, $db_prefix;
+
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	// Does it exist?
+	$columns = $smcFunc['db_list_columns']($table_name, true);
+	foreach ($columns as $column)
+		if ($column['name'] == $column_name)
+		{
+			$smcFunc['db_query']('', '
+				ALTER TABLE ' . $table_name . '
+				DROP COLUMN ' . $column_name,
+				array(
+					'security_override' => true,
+				)
+			);
+
+			return true;
+		}
+
+	// If here we didn't have to work - joy!
+	return false;
+}
+
+// Change a column.
+function smf_db_change_column($table_name, $old_column, $column_info, $parameters = array(), $error = 'fatal')
+{
+	global $smcFunc, $db_prefix;
+
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	// Check it does exist!
+	$columns = $smcFunc['db_list_columns']($table_name, true);
+	$old_info = null;
+	foreach ($columns as $column)
+		if ($column['name'] == $old_column)
+			$old_info = $column;
+
+	// Nothing?
+	if ($old_info == null)
+		return false;
+
+	// Get the right bits.
+	if (!isset($column_info['name']))
+		$column_info['name'] = $old_column;
+	if (!isset($column_info['default']))
+		$column_info['default'] = $old_info['default'];
+	if (!isset($column_info['null']))
+		$column_info['null'] = $old_info['null'];
+	if (!isset($column_info['auto']))
+		$column_info['auto'] = $old_info['auto'];
+	if (!isset($column_info['type']))
+		$column_info['type'] = $old_info['type'];
+	if (!isset($column_info['size']) || !is_numeric($column_info['size']))
+		$column_info['size'] = $old_info['size'];
+	if (!isset($column_info['unsigned']) || !in_array($column_info['type'], array('int', 'tinyint', 'smallint', 'mediumint', 'bigint')))
+		$column_info['unsigned'] = '';
+
+	list ($type, $size) = $smcFunc['db_calculate_type']($column_info['type'], $column_info['size']);
+
+	// Allow for unsigned integers (mysql only)
+	$unsigned = in_array($type, array('int', 'tinyint', 'smallint', 'mediumint', 'bigint')) && !empty($column_info['unsigned']) ? 'unsigned ' : '';
+
+	if ($size !== null)
+		$type = $type . '(' . $size . ')';
+
+	$smcFunc['db_query']('', '
+		ALTER TABLE ' . $table_name . '
+		CHANGE COLUMN `' . $old_column . '` `' . $column_info['name'] . '` ' . $type . ' ' . (!empty($unsigned) ? $unsigned : '') . (empty($column_info['null']) ? 'NOT NULL' : '') . ' ' .
+			(!isset($column_info['default']) ? '' : 'default \'' . $smcFunc['db_escape_string']($column_info['default']) . '\'') . ' ' .
+			(empty($column_info['auto']) ? '' : 'auto_increment') . ' ',
+		array(
+			'security_override' => true,
+		)
+	);
+}
+
+// Add an index.
+function smf_db_add_index($table_name, $index_info, $parameters = array(), $if_exists = 'update', $error = 'fatal')
+{
+	global $smcFunc, $db_package_log, $db_prefix;
+
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	// No columns = no index.
+	if (empty($index_info['columns']))
+		return false;
+	$columns = implode(',', $index_info['columns']);
+
+	// No name - make it up!
+	if (empty($index_info['name']))
+	{
+		// No need for primary.
+		if (isset($index_info['type']) && $index_info['type'] == 'primary')
+			$index_info['name'] = '';
+		else
+			$index_info['name'] = implode('_', $index_info['columns']);
+	}
+	else
+		$index_info['name'] = $index_info['name'];
+
+	// Log that we are going to want to remove this!
+	$db_package_log[] = array('remove_index', $table_name, $index_info['name']);
+
+	// Let's get all our indexes.
+	$indexes = $smcFunc['db_list_indexes']($table_name, true);
+	// Do we already have it?
+	foreach ($indexes as $index)
+	{
+		if ($index['name'] == $index_info['name'] || ($index['type'] == 'primary' && isset($index_info['type']) && $index_info['type'] == 'primary'))
+		{
+			// If we want to overwrite simply remove the current one then continue.
+			if ($if_exists != 'update' || $index['type'] == 'primary')
+				return false;
+			else
+				$smcFunc['db_remove_index']($table_name, $index_info['name']);
+		}
+	}
+
+	// If we're here we know we don't have the index - so just add it.
+	if (!empty($index_info['type']) && $index_info['type'] == 'primary')
+	{
+		$smcFunc['db_query']('', '
+			ALTER TABLE ' . $table_name . '
+			ADD PRIMARY KEY (' . $columns . ')',
+			array(
+				'security_override' => true,
+			)
+		);
+	}
+	else
+	{
+		$smcFunc['db_query']('', '
+			ALTER TABLE ' . $table_name . '
+			ADD ' . (isset($index_info['type']) && $index_info['type'] == 'unique' ? 'UNIQUE' : 'INDEX') . ' ' . $index_info['name'] . ' (' . $columns . ')',
+			array(
+				'security_override' => true,
+			)
+		);
+	}
+}
+
+// Remove an index.
+function smf_db_remove_index($table_name, $index_name, $parameters = array(), $error = 'fatal')
+{
+	global $smcFunc, $db_prefix;
+
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	// Better exist!
+	$indexes = $smcFunc['db_list_indexes']($table_name, true);
+
+	foreach ($indexes as $index)
+	{
+		// If the name is primary we want the primary key!
+		if ($index['type'] == 'primary' && $index_name == 'primary')
+		{
+			// Dropping primary key?
+			$smcFunc['db_query']('', '
+				ALTER TABLE ' . $table_name . '
+				DROP PRIMARY KEY',
+				array(
+					'security_override' => true,
+				)
+			);
+
+			return true;
+		}
+		if ($index['name'] == $index_name)
+		{
+			// Drop the bugger...
+			$smcFunc['db_query']('', '
+				ALTER TABLE ' . $table_name . '
+				DROP INDEX ' . $index_name,
+				array(
+					'security_override' => true,
+				)
+			);
+
+			return true;
+		}
+	}
+
+	// Not to be found ;(
+	return false;
+}
+
+// Get the schema formatted name for a type.
+function smf_db_calculate_type($type_name, $type_size = null, $reverse = false)
+{
+	// MySQL is actually the generic baseline.
+	return array($type_name, $type_size);
+}
+
+// Get table structure.
+function smf_db_table_structure($table_name, $parameters = array())
+{
+	global $smcFunc, $db_prefix;
+
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	return array(
+		'name' => $table_name,
+		'columns' => $smcFunc['db_list_columns']($table_name, true),
+		'indexes' => $smcFunc['db_list_indexes']($table_name, true),
+	);
+}
+
+// Return column information for a table.
+function smf_db_list_columns($table_name, $detail = false, $parameters = array())
+{
+	global $smcFunc, $db_prefix;
+
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	$result = $smcFunc['db_query']('', '
+		SHOW FIELDS
+		FROM {raw:table_name}',
+		array(
+			'table_name' => substr($table_name, 0, 1) == '`' ? $table_name : '`' . $table_name . '`',
+		)
+	);
+	$columns = array();
+	while ($row = $smcFunc['db_fetch_assoc']($result))
+	{
+		if (!$detail)
+		{
+			$columns[] = $row['Field'];
+		}
+		else
+		{
+			// Is there an auto_increment?
+			$auto = strpos($row['Extra'], 'auto_increment') !== false ? true : false;
+
+			// Can we split out the size?
+			if (preg_match('~(.+?)\s*\((\d+)\)(?:(?:\s*)?(unsigned))?~i', $row['Type'], $matches) === 1)
+			{
+				$type = $matches[1];
+				$size = $matches[2];
+				if (!empty($matches[3]) && $matches[3] == 'unsigned')
+					$unsigned = true;
+			}
+			else
+			{
+				$type = $row['Type'];
+				$size = null;
+			}
+
+			$columns[$row['Field']] = array(
+				'name' => $row['Field'],
+				'null' => $row['Null'] != 'YES' ? false : true,
+				'default' => isset($row['Default']) ? $row['Default'] : null,
+				'type' => $type,
+				'size' => $size,
+				'auto' => $auto,
+			);
+
+			if (isset($unsigned))
+			{
+				$columns[$row['Field']]['unsigned'] = $unsigned;
+				unset($unsigned);
+			}
+		}
+	}
+	$smcFunc['db_free_result']($result);
+
+	return $columns;
+}
+
+// What about some index information?
+function smf_db_list_indexes($table_name, $detail = false, $parameters = array())
+{
+	global $smcFunc, $db_prefix;
+
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	$result = $smcFunc['db_query']('', '
+		SHOW KEYS
+		FROM {raw:table_name}',
+		array(
+			'table_name' => substr($table_name, 0, 1) == '`' ? $table_name : '`' . $table_name . '`',
+		)
+	);
+	$indexes = array();
+	while ($row = $smcFunc['db_fetch_assoc']($result))
+	{
+		if (!$detail)
+			$indexes[] = $row['Key_name'];
+		else
+		{
+			// What is the type?
+			if ($row['Key_name'] == 'PRIMARY')
+				$type = 'primary';
+			elseif (empty($row['Non_unique']))
+				$type = 'unique';
+			elseif (isset($row['Index_type']) && $row['Index_type'] == 'FULLTEXT')
+				$type = 'fulltext';
+			else
+				$type = 'index';
+
+			// This is the first column we've seen?
+			if (empty($indexes[$row['Key_name']]))
+			{
+				$indexes[$row['Key_name']] = array(
+					'name' => $row['Key_name'],
+					'type' => $type,
+					'columns' => array(),
+				);
+			}
+
+			// Is it a partial index?
+			if (!empty($row['Sub_part']))
+				$indexes[$row['Key_name']]['columns'][] = $row['Column_name'] . '(' . $row['Sub_part'] . ')';
+			else
+				$indexes[$row['Key_name']]['columns'][] = $row['Column_name'];
+		}
+	}
+	$smcFunc['db_free_result']($result);
+
+	return $indexes;
+}
+
+?>

+ 746 - 0
Sources/DbPackages-postgresql.php

@@ -0,0 +1,746 @@
+<?php
+
+/**
+ * Simple Machines Forum (SMF)
+ *
+ * @package SMF
+ * @author Simple Machines http://www.simplemachines.org
+ * @copyright 2011 Simple Machines
+ * @license http://www.simplemachines.org/about/smf/license.php BSD
+ *
+ * @version 2.0
+ */
+
+if (!defined('SMF'))
+	die('Hacking attempt...');
+
+/*	This file contains database functionality specifically designed for packages to utilize.
+
+	bool smf_db_create_table(string table_name, array columns, array indexes = array(),
+		array parameters = array(), string if_exists = 'ignore')
+		- Can be used to create a table without worrying about schema compatabilities.
+		- If the table exists will, by default, do nothing.
+		- Builds table with columns as passed to it - at least one column must be sent.
+		  The columns array should have one sub-array for each column - these sub arrays contain:
+			+ 'name' = Column name
+			+ 'type' = Type of column - values from (smallint,mediumint,int,text,varchar,char,tinytext,mediumtext,largetext)
+			+ 'size' => Size of column (If applicable) - for example 255 for a large varchar, 10 for an int etc. If not
+						set SMF will pick a size.
+			+ 'default' = Default value - do not set if no default required.
+			+ 'null' => Can it be null (true or false) - if not set default will be false.
+			+ 'auto' => Set to true to make it an auto incrementing column. Set to a numerical value to set
+						from what it should begin counting.
+		- Adds indexes as specified within indexes parameter. Each index should be a member of $indexes. Values are:
+			+ 'name' => Index name (If left empty SMF will generate).
+			+ 'type' => Type of index. Choose from 'primary', 'unique' or 'index'. If not set will default to 'index'.
+			+ 'columns' => Array containing columns that form part of key - in the order the index is to be created.
+		- parameters: (None yet)
+		- if_exists values:
+			+ 'ignore' will do nothing if the table exists. (And will return true)
+			+ 'overwrite' will drop any existing table of the same name.
+			+ 'error' will return false if the table already exists.
+
+*/
+
+// Add the file functions to the $smcFunc array.
+function db_packages_init()
+{
+	global $smcFunc, $reservedTables, $db_package_log, $db_prefix;
+
+	if (!isset($smcFunc['db_create_table']) || $smcFunc['db_create_table'] != 'smf_db_create_table')
+	{
+		$smcFunc += array(
+			'db_add_column' => 'smf_db_add_column',
+			'db_add_index' => 'smf_db_add_index',
+			'db_calculate_type' => 'smf_db_calculate_type',
+			'db_change_column' => 'smf_db_change_column',
+			'db_create_table' => 'smf_db_create_table',
+			'db_drop_table' => 'smf_db_drop_table',
+			'db_table_structure' => 'smf_db_table_structure',
+			'db_list_columns' => 'smf_db_list_columns',
+			'db_list_indexes' => 'smf_db_list_indexes',
+			'db_remove_column' => 'smf_db_remove_column',
+			'db_remove_index' => 'smf_db_remove_index',
+		);
+		$db_package_log = array();
+	}
+
+	// We setup an array of SMF tables we can't do auto-remove on - in case a mod writer cocks it up!
+	$reservedTables = array('admin_info_files', 'approval_queue', 'attachments', 'ban_groups', 'ban_items',
+		'board_permissions', 'boards', 'calendar', 'calendar_holidays', 'categories', 'collapsed_categories',
+		'custom_fields', 'group_moderators', 'log_actions', 'log_activity', 'log_banned', 'log_boards',
+		'log_digest', 'log_errors', 'log_floodcontrol', 'log_group_requests', 'log_karma', 'log_mark_read',
+		'log_notify', 'log_online', 'log_packages', 'log_polls', 'log_reported', 'log_reported_comments',
+		'log_scheduled_tasks', 'log_search_messages', 'log_search_results', 'log_search_subjects',
+		'log_search_topics', 'log_topics', 'mail_queue', 'membergroups', 'members', 'message_icons',
+		'messages', 'moderators', 'package_servers', 'permission_profiles', 'permissions', 'personal_messages',
+		'pm_recipients', 'poll_choices', 'polls', 'scheduled_tasks', 'sessions', 'settings', 'smileys',
+		'themes', 'topics');
+	foreach ($reservedTables as $k => $table_name)
+		$reservedTables[$k] = strtolower($db_prefix . $table_name);
+
+	// We in turn may need the extra stuff.
+	db_extend('extra');
+}
+
+// Create a table.
+function smf_db_create_table($table_name, $columns, $indexes = array(), $parameters = array(), $if_exists = 'ignore', $error = 'fatal')
+{
+	global $reservedTables, $smcFunc, $db_package_log, $db_prefix;
+
+	// Strip out the table name, we might not need it in some cases
+	$real_prefix = preg_match('~^("?)(.+?)\\1\\.(.*?)$~', $db_prefix, $match) === 1 ? $match[3] : $db_prefix;
+
+	// With or without the database name, the fullname looks like this.
+	$full_table_name = str_replace('{db_prefix}', $real_prefix, $table_name);
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	// First - no way do we touch SMF tables.
+	if (in_array(strtolower($table_name), $reservedTables))
+		return false;
+
+	// Log that we'll want to remove this on uninstall.
+	$db_package_log[] = array('remove_table', $table_name);
+
+	// This... my friends... is a function in a half - let's start by checking if the table exists!
+	$tables = $smcFunc['db_list_tables']();
+	if (in_array($full_table_name, $tables))
+	{
+		// This is a sad day... drop the table? If not, return false (error) by default.
+		if ($if_exists == 'overwrite')
+			$smcFunc['db_drop_table']($table_name);
+		else
+			return $if_exists == 'ignore';
+	}
+
+	// If we've got this far - good news - no table exists. We can build our own!
+	$smcFunc['db_transaction']('begin');
+	$table_query = 'CREATE TABLE ' . $table_name . "\n" . '(';
+	foreach ($columns as $column)
+	{
+		// If we have an auto increment do it!
+		if (!empty($column['auto']))
+		{
+			$smcFunc['db_query']('', '
+				CREATE SEQUENCE ' . $table_name . '_seq',
+				array(
+					'security_override' => true,
+				)
+			);
+			$default = 'default nextval(\'' . $table_name . '_seq\')';
+		}
+		elseif (isset($column['default']) && $column['default'] !== null)
+			$default = 'default \'' . $smcFunc['db_escape_string']($column['default']) . '\'';
+		else
+			$default = '';
+
+		// Sort out the size...
+		$column['size'] = isset($column['size']) && is_numeric($column['size']) ? $column['size'] : null;
+		list ($type, $size) = $smcFunc['db_calculate_type']($column['type'], $column['size']);
+		if ($size !== null)
+			$type = $type . '(' . $size . ')';
+
+		// Now just put it together!
+		$table_query .= "\n\t\"" . $column['name'] . '" ' . $type . ' ' . (!empty($column['null']) ? '' : 'NOT NULL') . ' ' . $default . ',';
+	}
+
+	// Loop through the indexes a sec...
+	$index_queries = array();
+	foreach ($indexes as $index)
+	{
+		$columns = implode(',', $index['columns']);
+
+		// Primary goes in the table...
+		if (isset($index['type']) && $index['type'] == 'primary')
+			$table_query .= "\n\t" . 'PRIMARY KEY (' . implode(',', $index['columns']) . '),';
+		else
+		{
+			if (empty($index['name']))
+				$index['name'] = implode('_', $index['columns']);
+			$index_queries[] = 'CREATE ' . (isset($index['type']) && $index['type'] == 'unique' ? 'UNIQUE' : '') . ' INDEX ' . $table_name . '_' . $index['name'] . ' ON ' . $table_name . ' (' . $columns . ')';
+		}
+	}
+
+	// No trailing commas!
+	if (substr($table_query, -1) == ',')
+		$table_query = substr($table_query, 0, -1);
+
+	$table_query .= ')';
+
+	// Create the table!
+	$smcFunc['db_query']('', $table_query,
+		array(
+			'security_override' => true,
+		)
+	);
+	// And the indexes...
+	foreach ($index_queries as $query)
+		$smcFunc['db_query']('', $query,
+		array(
+			'security_override' => true,
+		)
+	);
+
+	// Go, go power rangers!
+	$smcFunc['db_transaction']('commit');
+}
+
+// Drop a table.
+function smf_db_drop_table($table_name, $parameters = array(), $error = 'fatal')
+{
+	global $reservedTables, $smcFunc, $db_prefix;
+
+	// After stripping away the database name, this is what's left.
+	$real_prefix = preg_match('~^("?)(.+?)\\1\\.(.*?)$~', $db_prefix, $match) === 1 ? $match[3] : $db_prefix;
+
+	// Get some aliases.
+	$full_table_name = str_replace('{db_prefix}', $real_prefix, $table_name);
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	// God no - dropping one of these = bad.
+	if (in_array(strtolower($table_name), $reservedTables))
+		return false;
+
+	// Does it exist?
+	if (in_array($full_table_name, $smcFunc['db_list_tables']()))
+	{
+		// We can then drop the table.
+		$smcFunc['db_transaction']('begin');
+
+		// the table
+		$table_query = 'DROP TABLE ' . $table_name;
+
+		// and the assosciated sequence, if any
+		$sequence_query = 'DROP SEQUENCE IF EXISTS ' . $table_name . '_seq';
+
+		// drop them
+		$smcFunc['db_query']('',
+			$table_query,
+			array(
+				'security_override' => true,
+			)
+		);
+		$smcFunc['db_query']('',
+			$sequence_query,
+			array(
+				'security_override' => true,
+			)
+		);
+
+		$smcFunc['db_transaction']('commit');
+
+		return true;
+	}
+
+	// Otherwise do 'nout.
+	return false;
+}
+
+// Add a column.
+function smf_db_add_column($table_name, $column_info, $parameters = array(), $if_exists = 'update', $error = 'fatal')
+{
+	global $smcFunc, $db_package_log, $txt, $db_prefix;
+
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	// Log that we will want to uninstall this!
+	$db_package_log[] = array('remove_column', $table_name, $column_info['name']);
+
+	// Does it exist - if so don't add it again!
+	$columns = $smcFunc['db_list_columns']($table_name, false);
+	foreach ($columns as $column)
+		if ($column == $column_info['name'])
+		{
+			// If we're going to overwrite then use change column.
+			if ($if_exists == 'update')
+				return $smcFunc['db_change_column']($table_name, $column_info['name'], $column_info);
+			else
+				return false;
+		}
+
+	// Get the specifics...
+	$column_info['size'] = isset($column_info['size']) && is_numeric($column_info['size']) ? $column_info['size'] : null;
+	list ($type, $size) = $smcFunc['db_calculate_type']($column_info['type'], $column_info['size']);
+	if ($size !== null)
+		$type = $type . '(' . $size . ')';
+
+	// Now add the thing!
+	$query = '
+		ALTER TABLE ' . $table_name . '
+		ADD COLUMN ' . $column_info['name'] . ' ' . $type;
+	$smcFunc['db_query']('', $query,
+		array(
+			'security_override' => true,
+		)
+	);
+
+	// If there's more attributes they need to be done via a change on PostgreSQL.
+	unset($column_info['type'], $column_info['size']);
+
+	if (count($column_info) != 1)
+		return $smcFunc['db_change_column']($table_name, $column_info['name'], $column_info);
+	else
+		return true;
+}
+
+// Remove a column.
+function smf_db_remove_column($table_name, $column_name, $parameters = array(), $error = 'fatal')
+{
+	global $smcFunc, $db_prefix;
+
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	// Does it exist?
+	$columns = $smcFunc['db_list_columns']($table_name, true);
+	foreach ($columns as $column)
+		if ($column['name'] == $column_name)
+		{
+			// If there is an auto we need remove it!
+			if ($column['auto'])
+				$smcFunc['db_query']('',
+					'DROP SEQUENCE ' . $table_name . '_seq',
+					array(
+						'security_override' => true,
+					)
+				);
+
+			$smcFunc['db_query']('', '
+				ALTER TABLE ' . $table_name . '
+				DROP COLUMN ' . $column_name,
+				array(
+					'security_override' => true,
+				)
+			);
+
+			return true;
+		}
+
+	// If here we didn't have to work - joy!
+	return false;
+}
+
+// Change a column.
+function smf_db_change_column($table_name, $old_column, $column_info, $parameters = array(), $error = 'fatal')
+{
+	global $smcFunc, $db_prefix;
+
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	// Check it does exist!
+	$columns = $smcFunc['db_list_columns']($table_name, true);
+	$old_info = null;
+	foreach ($columns as $column)
+		if ($column['name'] == $old_column)
+			$old_info = $column;
+
+	// Nothing?
+	if ($old_info == null)
+		return false;
+
+	// Now we check each bit individually and ALTER as required.
+	if (isset($column_info['name']) && $column_info['name'] != $old_column)
+	{
+		$smcFunc['db_query']('', '
+			ALTER TABLE ' . $table_name . '
+			RENAME COLUMN ' . $old_column . ' TO ' . $column_info['name'],
+			array(
+				'security_override' => true,
+			)
+		);
+	}
+	// Different default?
+	if (isset($column_info['default']) && $column_info['default'] != $old_info['default'])
+	{
+		$action = $column_info['default'] !== null ? 'SET DEFAULT \'' . $smcFunc['db_escape_string']($column_info['default']) . '\'' : 'DROP DEFAULT';
+		$smcFunc['db_query']('', '
+			ALTER TABLE ' . $table_name . '
+			ALTER COLUMN ' . $column_info['name'] . ' ' . $action,
+			array(
+				'security_override' => true,
+			)
+		);
+	}
+	// Is it null - or otherwise?
+	if (isset($column_info['null']) && $column_info['null'] != $old_info['null'])
+	{
+		$action = $column_info['null'] ? 'DROP' : 'SET';
+		$smcFunc['db_transaction']('begin');
+		if (!$column_info['null'])
+		{
+			// We have to set it to something if we are making it NOT NULL.
+			$setTo = isset($column_info['default']) ? $column_info['default'] : '';
+			$smcFunc['db_query']('', '
+				UPDATE ' . $table_name . '
+				SET ' . $column_info['name'] . ' = \'' . $setTo . '\'
+				WHERE ' . $column_info['name'] . ' = NULL',
+				array(
+					'security_override' => true,
+				)
+			);
+		}
+		$smcFunc['db_query']('', '
+			ALTER TABLE ' . $table_name . '
+			ALTER COLUMN ' . $column_info['name'] . ' ' . $action . ' NOT NULL',
+			array(
+				'security_override' => true,
+			)
+		);
+		$smcFunc['db_transaction']('commit');
+	}
+	// What about a change in type?
+	if (isset($column_info['type']) && ($column_info['type'] != $old_info['type'] || (isset($column_info['size']) && $column_info['size'] != $old_info['size'])))
+	{
+		$column_info['size'] = isset($column_info['size']) && is_numeric($column_info['size']) ? $column_info['size'] : null;
+		list ($type, $size) = $smcFunc['db_calculate_type']($column_info['type'], $column_info['size']);
+		if ($size !== null)
+			$type = $type . '(' . $size . ')';
+
+		// The alter is a pain.
+		$smcFunc['db_transaction']('begin');
+		$smcFunc['db_query']('', '
+			ALTER TABLE ' . $table_name . '
+			ADD COLUMN ' . $column_info['name'] . '_tempxx ' . $type,
+			array(
+				'security_override' => true,
+			)
+		);
+		$smcFunc['db_query']('', '
+			UPDATE ' . $table_name . '
+			SET ' . $column_info['name'] . '_tempxx = CAST(' . $column_info['name'] . ' AS ' . $type . ')',
+			array(
+				'security_override' => true,
+			)
+		);
+		$smcFunc['db_query']('', '
+			ALTER TABLE ' . $table_name . '
+			DROP COLUMN ' . $column_info['name'],
+			array(
+				'security_override' => true,
+			)
+		);
+		$smcFunc['db_query']('', '
+			ALTER TABLE ' . $table_name . '
+			RENAME COLUMN ' . $column_info['name'] . '_tempxx TO ' . $column_info['name'],
+			array(
+				'security_override' => true,
+			)
+		);
+		$smcFunc['db_transaction']('commit');
+	}
+	// Finally - auto increment?!
+	if (isset($column_info['auto']) && $column_info['auto'] != $old_info['auto'])
+	{
+		// Are we removing an old one?
+		if ($old_info['auto'])
+		{
+			// Alter the table first - then drop the sequence.
+			$smcFunc['db_query']('', '
+				ALTER TABLE ' . $table_name . '
+				ALTER COLUMN ' . $column_info['name'] . ' SET DEFAULT \'0\'',
+				array(
+					'security_override' => true,
+				)
+			);
+			$smcFunc['db_query']('', '
+				DROP SEQUENCE ' . $table_name . '_seq',
+				array(
+					'security_override' => true,
+				)
+			);
+		}
+		// Otherwise add it!
+		else
+		{
+			$smcFunc['db_query']('', '
+				CREATE SEQUENCE ' . $table_name . '_seq',
+				array(
+					'security_override' => true,
+				)
+			);
+			$smcFunc['db_query']('', '
+				ALTER TABLE ' . $table_name . '
+				ALTER COLUMN ' . $column_info['name'] . ' SET DEFAULT nextval(\'' . $table_name . '_seq\')',
+				array(
+					'security_override' => true,
+				)
+			);
+		}
+	}
+}
+
+// Add an index.
+function smf_db_add_index($table_name, $index_info, $parameters = array(), $if_exists = 'update', $error = 'fatal')
+{
+	global $smcFunc, $db_package_log, $db_prefix;
+
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	// No columns = no index.
+	if (empty($index_info['columns']))
+		return false;
+	$columns = implode(',', $index_info['columns']);
+
+	// No name - make it up!
+	if (empty($index_info['name']))
+	{
+		// No need for primary.
+		if (isset($index_info['type']) && $index_info['type'] == 'primary')
+			$index_info['name'] = '';
+		else
+			$index_info['name'] = $table_name . implode('_', $index_info['columns']);
+	}
+	else
+		$index_info['name'] = $table_name . $index_info['name'];
+
+	// Log that we are going to want to remove this!
+	$db_package_log[] = array('remove_index', $table_name, $index_info['name']);
+
+	// Let's get all our indexes.
+	$indexes = $smcFunc['db_list_indexes']($table_name, true);
+	// Do we already have it?
+	foreach ($indexes as $index)
+	{
+		if ($index['name'] == $index_info['name'] || ($index['type'] == 'primary' && isset($index_info['type']) && $index_info['type'] == 'primary'))
+		{
+			// If we want to overwrite simply remove the current one then continue.
+			if ($if_exists != 'update' || $index['type'] == 'primary')
+				return false;
+			else
+				$smcFunc['db_remove_index']($table_name, $index_info['name']);
+		}
+	}
+
+	// If we're here we know we don't have the index - so just add it.
+	if (!empty($index_info['type']) && $index_info['type'] == 'primary')
+	{
+		$smcFunc['db_query']('', '
+			ALTER TABLE ' . $table_name . '
+			ADD PRIMARY KEY (' . $columns . ')',
+			array(
+				'security_override' => true,
+			)
+		);
+	}
+	else
+	{
+		$smcFunc['db_query']('', '
+			CREATE ' . (isset($index_info['type']) && $index_info['type'] == 'unique' ? 'UNIQUE' : '') . ' INDEX ' . $index_info['name'] . ' ON ' . $table_name . ' (' . $columns . ')',
+			array(
+				'security_override' => true,
+			)
+		);
+	}
+}
+
+// Remove an index.
+function smf_db_remove_index($table_name, $index_name, $parameters = array(), $error = 'fatal')
+{
+	global $smcFunc, $db_prefix;
+
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	// Better exist!
+	$indexes = $smcFunc['db_list_indexes']($table_name, true);
+	if ($index_name != 'primary')
+		$index_name = $table_name . '_' . $index_name;
+
+	foreach ($indexes as $index)
+	{
+		// If the name is primary we want the primary key!
+		if ($index['type'] == 'primary' && $index_name == 'primary')
+		{
+			// Dropping primary key is odd...
+			$smcFunc['db_query']('', '
+				ALTER TABLE ' . $table_name . '
+				DROP CONSTRAINT ' . $index['name'],
+				array(
+					'security_override' => true,
+				)
+			);
+
+			return true;
+		}
+		if ($index['name'] == $index_name)
+		{
+			// Drop the bugger...
+			$smcFunc['db_query']('', '
+				DROP INDEX ' . $index_name,
+				array(
+					'security_override' => true,
+				)
+			);
+
+			return true;
+		}
+	}
+
+	// Not to be found ;(
+	return false;
+}
+
+// Get the schema formatted name for a type.
+function smf_db_calculate_type($type_name, $type_size = null, $reverse = false)
+{
+	// Generic => Specific.
+	if (!$reverse)
+	{
+		$types = array(
+			'varchar' => 'character varying',
+			'char' => 'character',
+			'mediumint' => 'int',
+			'tinyint' => 'smallint',
+			'tinytext' => 'character varying',
+			'mediumtext' => 'text',
+			'largetext' => 'text',
+		);
+	}
+	else
+	{
+		$types = array(
+			'character varying' => 'varchar',
+			'character' => 'char',
+			'integer' => 'int',
+		);
+	}
+
+	// Got it? Change it!
+	if (isset($types[$type_name]))
+	{
+		if ($type_name == 'tinytext')
+			$type_size = 255;
+		$type_name = $types[$type_name];
+	}
+	// Numbers don't have a size.
+	if (strpos($type_name, 'int') !== false)
+			$type_size = null;
+
+	return array($type_name, $type_size);
+}
+
+// Get table structure.
+function smf_db_table_structure($table_name, $parameters = array())
+{
+	global $smcFunc, $db_prefix;
+
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	return array(
+		'name' => $table_name,
+		'columns' => $smcFunc['db_list_columns']($table_name, true),
+		'indexes' => $smcFunc['db_list_indexes']($table_name, true),
+	);
+}
+
+// Return column information for a table.
+function smf_db_list_columns($table_name, $detail = false, $parameters = array())
+{
+	global $smcFunc, $db_prefix;
+
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	$result = $smcFunc['db_query']('', '
+		SELECT column_name, column_default, is_nullable, data_type, character_maximum_length
+		FROM information_schema.columns
+		WHERE table_name = \'' . $table_name . '\'
+		ORDER BY ordinal_position',
+		array(
+			'security_override' => true,
+		)
+	);
+	$columns = array();
+	while ($row = $smcFunc['db_fetch_assoc']($result))
+	{
+		if (!$detail)
+		{
+			$columns[] = $row['column_name'];
+		}
+		else
+		{
+			$auto = false;
+			// What is the default?
+			if (preg_match('~nextval\(\'(.+?)\'(.+?)*\)~i', $row['column_default'], $matches) != 0)
+			{
+				$default = null;
+				$auto = true;
+			}
+			elseif (trim($row['column_default']) != '')
+				$default = strpos($row['column_default'], '::') === false ? $row['column_default'] : substr($row['column_default'], 0, strpos($row['column_default'], '::'));
+			else
+				$default = null;
+
+			// Make the type generic.
+			list ($type, $size) = $smcFunc['db_calculate_type']($row['data_type'], $row['character_maximum_length'], true);
+
+			$columns[$row['column_name']] = array(
+				'name' => $row['column_name'],
+				'null' => $row['is_nullable'] ? true : false,
+				'default' => $default,
+				'type' => $type,
+				'size' => $size,
+				'auto' => $auto,
+			);
+		}
+	}
+	$smcFunc['db_free_result']($result);
+
+	return $columns;
+}
+
+// What about some index information?
+function smf_db_list_indexes($table_name, $detail = false, $parameters = array())
+{
+	global $smcFunc, $db_prefix;
+
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	$result = $smcFunc['db_query']('', '
+		SELECT CASE WHEN i.indisprimary THEN 1 ELSE 0 END AS is_primary,
+			CASE WHEN i.indisunique THEN 1 ELSE 0 END AS is_unique,
+			c2.relname AS name,
+			pg_get_indexdef(i.indexrelid) AS inddef
+		FROM pg_class AS c, pg_class AS c2, pg_index AS i
+		WHERE c.relname = \'' . $table_name . '\'
+			AND c.oid = i.indrelid
+			AND i.indexrelid = c2.oid',
+		array(
+			'security_override' => true,
+		)
+	);
+	$indexes = array();
+	while ($row = $smcFunc['db_fetch_assoc']($result))
+	{
+		// Try get the columns that make it up.
+		if (preg_match('~\(([^\)]+?)\)~i', $row['inddef'], $matches) == 0)
+			continue;
+
+		$columns = explode(',', $matches[1]);
+
+		if (empty($columns))
+			continue;
+
+		foreach ($columns as $k => $v)
+			$columns[$k] = trim($v);
+
+		// Fix up the name to be consistent cross databases
+		if (substr($row['name'], -5) == '_pkey' && $row['is_primary'] == 1)
+			$row['name'] = 'PRIMARY';
+		else
+			$row['name'] = str_replace($table_name . '_', '', $row['name']);
+
+		if (!$detail)
+			$indexes[] = $row['name'];
+		else
+		{
+			$indexes[$row['name']] = array(
+				'name' => $row['name'],
+				'type' => $row['is_primary'] ? 'primary' : ($row['is_unique'] ? 'unique' : 'index'),
+				'columns' => $columns,
+			);
+		}
+	}
+	$smcFunc['db_free_result']($result);
+
+	return $indexes;
+}
+
+?>

+ 736 - 0
Sources/DbPackages-sqlite.php

@@ -0,0 +1,736 @@
+<?php
+
+/**
+ * Simple Machines Forum (SMF)
+ *
+ * @package SMF
+ * @author Simple Machines http://www.simplemachines.org
+ * @copyright 2011 Simple Machines
+ * @license http://www.simplemachines.org/about/smf/license.php BSD
+ *
+ * @version 2.0
+ */
+
+if (!defined('SMF'))
+	die('Hacking attempt...');
+
+/*	This file contains database functionality specifically designed for packages to utilize.
+
+	bool smf_db_create_table(string table_name, array columns, array indexes = array(),
+		array parameters = array(), string if_exists = 'ignore')
+		- Can be used to create a table without worrying about schema compatabilities.
+		- If the table exists will, by default, do nothing.
+		- Builds table with columns as passed to it - at least one column must be sent.
+		  The columns array should have one sub-array for each column - these sub arrays contain:
+			+ 'name' = Column name
+			+ 'type' = Type of column - values from (smallint,mediumint,int,text,varchar,char,tinytext,mediumtext,largetext)
+			+ 'size' => Size of column (If applicable) - for example 255 for a large varchar, 10 for an int etc. If not
+						set SMF will pick a size.
+			+ 'default' = Default value - do not set if no default required.
+			+ 'null' => Can it be null (true or false) - if not set default will be false.
+			+ 'auto' => Set to true to make it an auto incrementing column. Set to a numerical value to set
+						from what it should begin counting.
+		- Adds indexes as specified within indexes parameter. Each index should be a member of $indexes. Values are:
+			+ 'name' => Index name (If left empty SMF will generate).
+			+ 'type' => Type of index. Choose from 'primary', 'unique' or 'index'. If not set will default to 'index'.
+			+ 'columns' => Array containing columns that form part of key - in the order the index is to be created.
+		- parameters: (None yet)
+		- if_exists values:
+			+ 'ignore' will do nothing if the table exists. (And will return true)
+			+ 'overwrite' will drop any existing table of the same name.
+			+ 'error' will return false if the table already exists.
+
+*/
+
+// Add the file functions to the $smcFunc array.
+function db_packages_init()
+{
+	global $smcFunc, $reservedTables, $db_package_log, $db_prefix;
+
+	if (!isset($smcFunc['db_create_table']) || $smcFunc['db_create_table'] != 'smf_db_create_table')
+	{
+		$smcFunc += array(
+			'db_add_column' => 'smf_db_add_column',
+			'db_add_index' => 'smf_db_add_index',
+			'db_alter_table' => 'smf_db_alter_table',
+			'db_calculate_type' => 'smf_db_calculate_type',
+			'db_change_column' => 'smf_db_change_column',
+			'db_create_table' => 'smf_db_create_table',
+			'db_drop_table' => 'smf_db_drop_table',
+			'db_table_structure' => 'smf_db_table_structure',
+			'db_list_columns' => 'smf_db_list_columns',
+			'db_list_indexes' => 'smf_db_list_indexes',
+			'db_remove_column' => 'smf_db_remove_column',
+			'db_remove_index' => 'smf_db_remove_index',
+		);
+		$db_package_log = array();
+	}
+
+	// We setup an array of SMF tables we can't do auto-remove on - in case a mod writer cocks it up!
+	$reservedTables = array('admin_info_files', 'approval_queue', 'attachments', 'ban_groups', 'ban_items',
+		'board_permissions', 'boards', 'calendar', 'calendar_holidays', 'categories', 'collapsed_categories',
+		'custom_fields', 'group_moderators', 'log_actions', 'log_activity', 'log_banned', 'log_boards',
+		'log_digest', 'log_errors', 'log_floodcontrol', 'log_group_requests', 'log_karma', 'log_mark_read',
+		'log_notify', 'log_online', 'log_packages', 'log_polls', 'log_reported', 'log_reported_comments',
+		'log_scheduled_tasks', 'log_search_messages', 'log_search_results', 'log_search_subjects',
+		'log_search_topics', 'log_topics', 'mail_queue', 'membergroups', 'members', 'message_icons',
+		'messages', 'moderators', 'package_servers', 'permission_profiles', 'permissions', 'personal_messages',
+		'pm_recipients', 'poll_choices', 'polls', 'scheduled_tasks', 'sessions', 'settings', 'smileys',
+		'themes', 'topics');
+	foreach ($reservedTables as $k => $table_name)
+		$reservedTables[$k] = strtolower($db_prefix . $table_name);
+
+	// We in turn may need the extra stuff.
+	db_extend('extra');
+}
+
+// Create a table.
+function smf_db_create_table($table_name, $columns, $indexes = array(), $parameters = array(), $if_exists = 'ignore', $error = 'fatal')
+{
+	global $reservedTables, $smcFunc, $db_package_log, $db_prefix;
+
+	// With or without the database name, the full name looks like this.
+	$real_prefix = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $db_prefix, $match) === 1 ? $match[3] : $db_prefix;
+	$full_table_name = str_replace('{db_prefix}', $real_prefix, $table_name);
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	// First - no way do we touch SMF tables.
+	// Commented out for now. We need to alter SMF tables in order to use this in the upgrade.
+/*
+	if (in_array(strtolower($table_name), $reservedTables))
+		return false;
+*/
+
+	// Log that we'll want to remove this on uninstall.
+	$db_package_log[] = array('remove_table', $table_name);
+
+	// Does this table exist or not?
+	$tables = $smcFunc['db_list_tables']();
+	if (in_array($full_table_name, $tables))
+	{
+		// This is a sad day... drop the table? If not, return false (error) by default.
+		if ($if_exists == 'overwrite')
+			$smcFunc['db_drop_table']($table_name);
+		else
+			return $if_exists == 'ignore';
+	}
+
+	// Righty - let's do the damn thing!
+	$table_query = 'CREATE TABLE ' . $table_name . "\n" . '(';
+	$done_primary = false;
+	foreach ($columns as $column)
+	{
+		// Auto increment is special
+		if (!empty($column['auto']))
+		{
+			$table_query .= "\n" . $column['name'] . ' integer PRIMARY KEY,';
+			$done_primary = true;
+			continue;
+		}
+		elseif (isset($column['default']) && $column['default'] !== null)
+			$default = 'default \'' . $smcFunc['db_escape_string']($column['default']) . '\'';
+		else
+			$default = '';
+
+		// Sort out the size... and stuff...
+		$column['size'] = isset($column['size']) && is_numeric($column['size']) ? $column['size'] : null;
+		list ($type, $size) = $smcFunc['db_calculate_type']($column['type'], $column['size']);
+		if ($size !== null)
+			$type = $type . '(' . $size . ')';
+
+		// Now just put it together!
+		$table_query .= "\n\t" . $column['name'] . ' ' . $type . ' ' . (!empty($column['null']) ? '' : 'NOT NULL') . ' ' . $default . ',';
+	}
+
+	// Loop through the indexes next...
+	$index_queries = array();
+	foreach ($indexes as $index)
+	{
+		$columns = implode(',', $index['columns']);
+
+		// Is it the primary?
+		if (isset($index['type']) && $index['type'] == 'primary')
+		{
+			// If we've done the primary via auto_inc, don't do it again!
+			if (!$done_primary)
+				$table_query .= "\n\t" . 'PRIMARY KEY (' . implode(',', $index['columns']) . '),';
+		}
+		else
+		{
+			if (empty($index['name']))
+				$index['name'] = implode('_', $index['columns']);
+			$index_queries[] = 'CREATE ' . (isset($index['type']) && $index['type'] == 'unique' ? 'UNIQUE' : '') . ' INDEX ' . $table_name . '_' . $index['name'] . ' ON ' . $table_name . ' (' . $columns . ')';
+		}
+	}
+
+	// No trailing commas!
+	if (substr($table_query, -1) == ',')
+		$table_query = substr($table_query, 0, -1);
+
+	$table_query .= ')';
+
+	if (empty($parameters['skip_transaction']))
+		$smcFunc['db_transaction']('begin');
+
+	// Do the table and indexes...
+	$smcFunc['db_query']('', $table_query,
+		array(
+			'security_override' => true,
+		)
+	);
+	foreach ($index_queries as $query)
+		$smcFunc['db_query']('', $query,
+		array(
+			'security_override' => true,
+		)
+	);
+
+	if (empty($parameters['skip_transaction']))
+		$smcFunc['db_transaction']('commit');
+}
+
+// Drop a table.
+function smf_db_drop_table($table_name, $parameters = array(), $error = 'fatal')
+{
+	global $reservedTables, $smcFunc, $db_prefix;
+
+	// Strip out the table name, we might not need it in some cases
+	$real_prefix = preg_match('~^(`?)(.+?)\\1\\.(.*?)$~', $db_prefix, $match) === 1 ? $match[3] : $db_prefix;
+	$full_table_name = str_replace('{db_prefix}', $real_prefix, $table_name);
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	// God no - dropping one of these = bad.
+	if (in_array(strtolower($table_name), $reservedTables))
+		return false;
+
+	// Does it exist?
+	if (in_array($full_table_name, $smcFunc['db_list_tables']()))
+	{
+		$query = 'DROP TABLE ' . $table_name;
+		$smcFunc['db_query']('', $query,
+			array(
+				'security_override' => true,
+			)
+		);
+
+		return true;
+	}
+
+	// Otherwise do 'nout.
+	return false;
+}
+
+// Add a column.
+function smf_db_add_column($table_name, $column_info, $parameters = array(), $if_exists = 'update', $error = 'fatal')
+{
+	global $smcFunc, $db_package_log, $txt, $db_prefix;
+
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	// Log that we will want to uninstall this!
+	$db_package_log[] = array('remove_column', $table_name, $column_info['name']);
+
+	// Does it exist - if so don't add it again!
+	$columns = $smcFunc['db_list_columns']($table_name, false);
+	foreach ($columns as $column)
+		if ($column == $column_info['name'])
+		{
+			// If we're going to overwrite then use change column.
+			if ($if_exists == 'update')
+				return $smcFunc['db_change_column']($table_name, $column_info['name'], $column_info);
+			else
+				return false;
+		}
+
+	// Alter the table to add the column.
+	if ($smcFunc['db_alter_table']($table_name, array('add' => array($column_info))) === false)
+		return false;
+
+	return true;
+}
+
+// We can't reliably do this on SQLite - damn!
+function smf_db_remove_column($table_name, $column_name, $parameters = array(), $error = 'fatal')
+{
+	global $smcFunc, $db_prefix;
+
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	if ($smcFunc['db_alter_table']($table_name, array('remove' => array(array('name' => $column_name)))))
+		return true;
+	else
+		return false;
+}
+
+// Change a column.
+function smf_db_change_column($table_name, $old_column, $column_info, $parameters = array(), $error = 'fatal')
+{
+	global $smcFunc, $db_prefix;
+
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	if ($smcFunc['db_alter_table']($table_name, array('change' => array(array('name' => $old_column) + $column_info))))
+		return true;
+	else
+		return false;
+}
+
+// Add an index.
+function smf_db_add_index($table_name, $index_info, $parameters = array(), $if_exists = 'update', $error = 'fatal')
+{
+	global $smcFunc, $db_package_log, $db_prefix;
+
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	// No columns = no index.
+	if (empty($index_info['columns']))
+		return false;
+	$columns = implode(',', $index_info['columns']);
+
+	// No name - make it up!
+	if (empty($index_info['name']))
+	{
+		// No need for primary.
+		if (isset($index_info['type']) && $index_info['type'] == 'primary')
+			$index_info['name'] = '';
+		else
+			$index_info['name'] = implode('_', $index_info['columns']);
+	}
+	else
+		$index_info['name'] = $index_info['name'];
+
+	// Log that we are going to want to remove this!
+	$db_package_log[] = array('remove_index', $table_name, $index_info['name']);
+
+	// Let's get all our indexes.
+	$indexes = $smcFunc['db_list_indexes']($table_name, true);
+	// Do we already have it?
+	foreach ($indexes as $index)
+	{
+		if ($index['name'] == $index_info['name'] || ($index['type'] == 'primary' && isset($index_info['type']) && $index_info['type'] == 'primary'))
+		{
+			// If we want to overwrite simply remove the current one then continue.
+			if ($if_exists != 'update' || $index['type'] == 'primary')
+				return false;
+			else
+				$smcFunc['db_remove_index']($table_name, $index_info['name']);
+		}
+	}
+
+	// If we're here we know we don't have the index - so just add it.
+	if (!empty($index_info['type']) && $index_info['type'] == 'primary')
+	{
+		//!!! Doesn't work with PRIMARY KEY yet.
+	}
+	else
+	{
+		$smcFunc['db_query']('', '
+			CREATE ' . (isset($index_info['type']) && $index_info['type'] == 'unique' ? 'UNIQUE' : '') . ' INDEX ' . $index_info['name'] . ' ON ' . $table_name . ' (' . $columns . ')',
+			array(
+				'security_override' => true,
+			)
+		);
+	}
+}
+
+// Remove an index.
+function smf_db_remove_index($table_name, $index_name, $parameters = array(), $error = 'fatal')
+{
+	global $smcFunc, $db_prefix;
+
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	// Better exist!
+	$indexes = $smcFunc['db_list_indexes']($table_name, true);
+
+	foreach ($indexes as $index)
+	{
+		//!!! Doesn't do primary key at the moment!
+		if ($index['type'] != 'primary' && $index['name'] == $index_name)
+		{
+			// Drop the bugger...
+			$smcFunc['db_query']('', '
+				DROP INDEX ' . $index_name,
+				array(
+					'security_override' => true,
+				)
+			);
+
+			return true;
+		}
+	}
+
+	// Not to be found ;(
+	return false;
+}
+
+// Get the schema formatted name for a type.
+function smf_db_calculate_type($type_name, $type_size = null, $reverse = false)
+{
+	// Generic => Specific.
+	if (!$reverse)
+	{
+		$types = array(
+			'mediumint' => 'int',
+			'tinyint' => 'smallint',
+			'mediumtext' => 'text',
+			'largetext' => 'text',
+		);
+	}
+	else
+	{
+		$types = array(
+			'integer' => 'int',
+		);
+	}
+
+	// Got it? Change it!
+	if (isset($types[$type_name]))
+	{
+		if ($type_name == 'tinytext')
+			$type_size = 255;
+		$type_name = $types[$type_name];
+	}
+	// Numbers don't have a size.
+	if (strpos($type_name, 'int') !== false)
+		$type_size = null;
+
+	return array($type_name, $type_size);
+}
+
+// Get table structure.
+function smf_db_table_structure($table_name, $parameters = array())
+{
+	global $smcFunc, $db_prefix;
+
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	return array(
+		'name' => $table_name,
+		'columns' => $smcFunc['db_list_columns']($table_name, true),
+		'indexes' => $smcFunc['db_list_indexes']($table_name, true),
+	);
+}
+
+// Harder than it should be on sqlite!
+function smf_db_list_columns($table_name, $detail = false, $parameters = array())
+{
+	global $smcFunc, $db_prefix;
+
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	$result = $smcFunc['db_query']('', '
+		PRAGMA table_info(' . $table_name . ')',
+		array(
+			'security_override' => true,
+		)
+	);
+	$columns = array();
+
+	$primaries = array();
+	while ($row = $smcFunc['db_fetch_assoc']($result))
+	{
+		if (!$detail)
+		{
+			$columns[] = $row['name'];
+		}
+		else
+		{
+			// Auto increment is hard to tell really... if there's only one primary it probably is.
+			if ($row['pk'])
+				$primaries[] = $row['name'];
+
+			// Can we split out the size?
+			if (preg_match('~(.+?)\s*\((\d+)\)~i', $row['type'], $matches))
+			{
+				$type = $matches[1];
+				$size = $matches[2];
+			}
+			else
+			{
+				$type = $row['type'];
+				$size = null;
+			}
+
+			$columns[$row['name']] = array(
+				'name' => $row['name'],
+				'null' => $row['notnull'] ? false : true,
+				'default' => $row['dflt_value'],
+				'type' => $type,
+				'size' => $size,
+				'auto' => false,
+			);
+		}
+	}
+	$smcFunc['db_free_result']($result);
+
+	// Put in our guess at auto_inc.
+	if (count($primaries) == 1)
+		$columns[$primaries[0]]['auto'] = true;
+
+	return $columns;
+}
+
+// What about some index information?
+function smf_db_list_indexes($table_name, $detail = false, $parameters = array())
+{
+	global $smcFunc, $db_prefix;
+
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	$result = $smcFunc['db_query']('', '
+		PRAGMA index_list(' . $table_name . ')',
+		array(
+			'security_override' => true,
+		)
+	);
+	$indexes = array();
+	while ($row = $smcFunc['db_fetch_assoc']($result))
+	{
+		if (!$detail)
+			$indexes[] = $row['name'];
+		else
+		{
+			$result2 = $smcFunc['db_query']('', '
+				PRAGMA index_info(' . $row['name'] . ')',
+				array(
+					'security_override' => true,
+				)
+			);
+			while ($row2 = $smcFunc['db_fetch_assoc']($result2))
+			{
+				// What is the type?
+				if ($row['unique'])
+					$type = 'unique';
+				else
+					$type = 'index';
+
+				// This is the first column we've seen?
+				if (empty($indexes[$row['name']]))
+				{
+					$indexes[$row['name']] = array(
+						'name' => $row['name'],
+						'type' => $type,
+						'columns' => array(),
+					);
+				}
+
+				// Add the column...
+				$indexes[$row['name']]['columns'][] = $row2['name'];
+			}
+			$smcFunc['db_free_result']($result2);
+		}
+	}
+	$smcFunc['db_free_result']($result);
+
+	return $indexes;
+}
+
+function smf_db_alter_table($table_name, $columns)
+{
+	global $smcFunc, $db_prefix, $db_name, $boarddir;
+
+	$db_file = substr($db_name, -3) === '.db' ? $db_name : $db_name . '.db';
+
+	$table_name = str_replace('{db_prefix}', $db_prefix, $table_name);
+
+	// Let's get the current columns for the table.
+	$current_columns = $smcFunc['db_list_columns']($table_name, true);
+
+	// Let's get a list of columns for the temp table.
+	$temp_table_columns = array();
+
+	// Let's see if we have columns to remove or columns that are being added that already exist.
+	foreach ($current_columns as $key => $column)
+	{
+		$exists = false;
+		if (isset($columns['remove']))
+			foreach ($columns['remove'] as $drop)
+				if ($drop['name'] == $column['name'])
+				{
+					$exists = true;
+					break;
+				}
+
+		if (isset($columns['add']))
+			foreach ($columns['add'] as $key2 => $add)
+				if ($add['name'] == $column['name'])
+				{
+					unset($columns['add'][$key2]);
+					break;
+				}
+
+		// Doesn't exist then we 'remove'.
+		if (!$exists)
+			$temp_table_columns[] = $column['name'];
+	}
+
+	// If they are equal then that means that the column that we are adding exists or it doesn't exist and we are not looking to change any one of them.
+	if (count($temp_table_columns) == count($current_columns) && empty($columns['change']) && empty($columns['add']))
+		return true;
+
+	// Drop the temp table.
+	$smcFunc['db_query']('', '
+		DROP TABLE {raw:temp_table_name}',
+		array(
+			'temp_table_name' => $table_name . '_tmp',
+			'db_error_skip' => true,
+		)
+	);
+
+	// Let's make a backup of the current database.
+	// We only want the first backup of a table modification.  So if there is a backup file and older than an hour just delete and back up again
+	$db_backup_file = $boarddir . '/Packages/backups/backup_' . $table_name . '_' . basename($db_file) . md5($table_name . $db_file);
+	if (file_exists($db_backup_file) && time() - filemtime($db_backup_file) > 3600)
+	{
+		@unlink($db_backup_file);
+		@copy($db_file, $db_backup_file);
+	}
+	elseif (!file_exists($db_backup_file))
+		@copy($db_file, $db_backup_file);
+
+	// If we don't have temp tables then everything crapped out.  Just exit.
+	if (empty($temp_table_columns))
+		return false;
+
+	// Start
+	$smcFunc['db_transaction']('begin');
+
+	// Let's create the temporary table.
+	$createTempTable = $smcFunc['db_query']('', '
+		CREATE TEMPORARY TABLE {raw:temp_table_name}
+		(
+			{raw:columns}
+		);',
+		array(
+			'temp_table_name' => $table_name . '_tmp',
+			'columns' => implode(', ', $temp_table_columns),
+			'db_error_skip' => true,
+		)
+	) !== false;
+
+	if (!$createTempTable)
+		return false;
+
+	// Insert into temp table.
+	$smcFunc['db_query']('', '
+		INSERT INTO {raw:temp_table_name}
+			({raw:columns})
+		SELECT {raw:columns}
+		FROM {raw:table_name}',
+		array(
+			'table_name' => $table_name,
+			'columns' => implode(', ', $temp_table_columns),
+			'temp_table_name' => $table_name . '_tmp',
+		)
+	);
+
+	// Drop the current table.
+	$dropTable = $smcFunc['db_query']('', '
+		DROP TABLE {raw:table_name}',
+		array(
+			'table_name' => $table_name,
+			'db_error_skip' => true,
+		)
+	) !== false;
+
+	// If you can't drop the main table then there is no where to go from here. Just return.
+	if (!$dropTable)
+		return false;
+
+	// We need to keep track of the structure for the current columns and the new columns.
+	$new_columns = array();
+	$column_names = array();
+
+	// Let's get the ones that we already have first.
+	foreach ($current_columns as $name => $column)
+	{
+		if (in_array($name, $temp_table_columns))
+		{
+			$new_columns[$name] = array(
+				'name' => $name,
+				'type' => $column['type'],
+				'size' => isset($column['size']) ? (int) $column['size'] : null,
+				'null' => !empty($column['null']),
+				'auto' => isset($column['auto']) ? $column['auto'] : false,
+				'default' => isset($column['default']) ? $column['default'] : '',
+			);
+
+			// Lets keep track of the name for the column.
+			$column_names[$name] = $name;
+		}
+	}
+
+	// Now the new.
+	if (!empty($columns['add']))
+		foreach ($columns['add'] as $add)
+		{
+			$new_columns[$add['name']] = array(
+				'name' => $add['name'],
+				'type' => $add['type'],
+				'size' => isset($add['size']) ? (int) $add['size'] : null,
+				'null' => !empty($add['null']),
+				'auto' => isset($add['auto']) ? $add['auto'] : false,
+				'default' => isset($add['default']) ? $add['default'] : '',
+			);
+
+			// Let's keep track of the name for the column.
+			$column_names[$add['name']] = strstr('int', $add['type']) ? ' 0 AS ' . $add['name'] : ' {string:empty_string} AS ' . $add['name'];
+		}
+
+	// Now to change a column.  Not drop but change it.
+	if (isset($columns['change']))
+		foreach ($columns['change'] as $change)
+			if (isset($new_columns[$change['name']]))
+				$new_columns[$change['name']] = array(
+					'name' => $change['name'],
+					'type' => $change['type'],
+					'size' => isset($change['size']) ? (int) $change['size'] : null,
+					'null' => !empty($change['null']),
+					'auto' => isset($change['auto']) ? $change['auto'] : false,
+					'default' => isset($change['default']) ? $change['default'] : '',
+				);
+
+	// Now let's create the table.
+	$createTable = $smcFunc['db_create_table']($table_name, $new_columns, array(), array('skip_transaction' => true));
+
+	// Did it create correctly?
+	if ($createTable === false)
+		return false;
+
+	// Back to it's original table.
+	$insertData = $smcFunc['db_query']('', '
+		INSERT INTO {raw:table_name}
+			({raw:columns})
+		SELECT ' . implode(', ', $column_names) . '
+		FROM {raw:temp_table_name}',
+		array(
+			'table_name' => $table_name,
+			'columns' => implode(', ', array_keys($new_columns)),
+			'columns_select' => implode(', ', $column_names),
+			'temp_table_name' => $table_name . '_tmp',
+			'empty_string' => '',
+		)
+	);
+
+	// Did everything insert correctly?
+	if (!$insertData)
+		return false;
+
+	// Drop the temp table.
+	$smcFunc['db_query']('', '
+		DROP TABLE {raw:temp_table_name}',
+		array(
+			'temp_table_name' => $table_name . '_tmp',
+			'db_error_skip' => true,
+		)
+	);
+
+	// Commit or else there is no point in doing the previous steps.
+	$smcFunc['db_transaction']('commit');
+
+	// We got here so we're good.  The temp table should be deleted, if not it will be gone later on >:D.
+	return true;
+}
+
+?>

+ 77 - 0
Sources/DbSearch-mysql.php

@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * Simple Machines Forum (SMF)
+ *
+ * @package SMF
+ * @author Simple Machines http://www.simplemachines.org
+ * @copyright 2011 Simple Machines
+ * @license http://www.simplemachines.org/about/smf/license.php BSD
+ *
+ * @version 2.0
+ */
+
+if (!defined('SMF'))
+	die('Hacking attempt...');
+
+/*	This file contains database functions specific to search related activity.
+
+	void db_search_init()
+		- adds the functions in this file to the $smcFunc array
+
+	boolean smf_db_search_support($search_type)
+		- whether this database type support the search type $search_type
+
+	void smf_db_create_word_search($size)
+ 		- create the custom word index table
+
+*/
+
+// Add the file functions to the $smcFunc array.
+function db_search_init()
+{
+	global $smcFunc;
+
+	if (!isset($smcFunc['db_search_query']) || $smcFunc['db_search_query'] != 'smf_db_query')
+		$smcFunc += array(
+			'db_search_query' => 'smf_db_query',
+			'db_search_support' => 'smf_db_search_support',
+			'db_create_word_search' => 'smf_db_create_word_search',
+			'db_support_ignore' => true,
+		);
+}
+
+// Does this database type support this search type?
+function smf_db_search_support($search_type)
+{
+	$supported_types = array('fulltext');
+
+	return in_array($search_type, $supported_types);
+}
+
+// Highly specific - create the custom word index table!
+function smf_db_create_word_search($size)
+{
+	global $smcFunc;
+
+	if ($size == 'small')
+		$size = 'smallint(5)';
+	elseif ($size == 'medium')
+		$size = 'mediumint(8)';
+	else
+		$size = 'int(10)';
+
+	$smcFunc['db_query']('', '
+		CREATE TABLE {db_prefix}log_search_words (
+			id_word {raw:size} unsigned NOT NULL default {string:string_zero},
+			id_msg int(10) unsigned NOT NULL default {string:string_zero},
+			PRIMARY KEY (id_word, id_msg)
+		) ENGINE=InnoDB',
+		array(
+			'string_zero' => '0',
+			'size' => $size,
+		)
+	);
+}
+
+?>

+ 122 - 0
Sources/DbSearch-postgresql.php

@@ -0,0 +1,122 @@
+<?php
+
+/**
+ * Simple Machines Forum (SMF)
+ *
+ * @package SMF
+ * @author Simple Machines http://www.simplemachines.org
+ * @copyright 2011 Simple Machines
+ * @license http://www.simplemachines.org/about/smf/license.php BSD
+ *
+ * @version 2.0
+ */
+
+if (!defined('SMF'))
+	die('Hacking attempt...');
+
+/*	This file contains database functions specific to search related activity.
+
+	void db_search_init()
+		- adds the functions in this file to the $smcFunc array
+
+	boolean smf_db_search_support($search_type)
+		- whether this database type support the search type $search_type
+
+	void smf_db_create_word_search($size)
+ 		- create the custom word index table
+
+	resource smf_db_search_query($identifier, $db_string, $db_values = array(), $connection = null)
+		- returns the correct query for this search type.
+*/
+
+// Add the file functions to the $smcFunc array.
+function db_search_init()
+{
+	global $smcFunc;
+
+	if (!isset($smcFunc['db_search_query']) || $smcFunc['db_search_query'] != 'smf_db_search_query')
+		$smcFunc += array(
+			'db_search_query' => 'smf_db_search_query',
+			'db_search_support' => 'smf_db_search_support',
+			'db_create_word_search' => 'smf_db_create_word_search',
+			'db_support_ignore' => false,
+		);
+}
+
+// Does this database type support this search type?
+function smf_db_search_support($search_type)
+{
+	$supported_types = array('custom');
+
+	return in_array($search_type, $supported_types);
+}
+
+// Returns the correct query for this search type.
+function smf_db_search_query($identifier, $db_string, $db_values = array(), $connection = null)
+{
+	global $smcFunc;
+
+	$replacements = array(
+		'create_tmp_log_search_topics' => array(
+			'~mediumint\(\d\)~i' => 'int',
+			'~unsigned~i' => '',
+			'~TYPE=HEAP~i' => '',
+		),
+		'create_tmp_log_search_messages' => array(
+			'~mediumint\(\d\)' => 'int',
+			'~unsigned~i' => '',
+			'~TYPE=HEAP~i' => '',
+		),
+		'drop_tmp_log_search_topics' => array(
+			'~IF\sEXISTS~i' => '',
+		),
+		'drop_tmp_log_search_messages' => array(
+			'~IF\sEXISTS~i' => '',
+		),
+		'insert_into_log_messages_fulltext' => array(
+			'~NOT\sRLIKE~i' => '!~*',
+			'~RLIKE~i' => '~*',
+		),
+		'insert_log_search_results_subject' => array(
+			'~NOT\sRLIKE~i' => '!~*',
+			'~RLIKE~i' => '~*',
+		),
+	);
+
+	if (isset($replacements[$identifier]))
+		$db_string = preg_replace(array_keys($replacements[$identifier]), array_values($replacements[$identifier]), $db_string);
+	elseif (preg_match('~^\s*INSERT\sIGNORE~i', $db_string) != 0)
+	{
+		$db_string = preg_replace('~^\s*INSERT\sIGNORE~i', 'INSERT', $db_string);
+		// Don't error on multi-insert.
+		$db_values['db_error_skip'] = true;
+	}
+
+	$return = $smcFunc['db_query']('', $db_string,
+		$db_values, $connection
+	);
+
+	return $return;
+}
+
+// Highly specific - create the custom word index table!
+function smf_db_create_word_search($size)
+{
+	global $smcFunc;
+
+	$size = 'int';
+
+	$smcFunc['db_query']('', '
+		CREATE TABLE {db_prefix}log_search_words (
+			id_word {raw:size} NOT NULL default {string:string_zero},
+			id_msg int NOT NULL default {string:string_zero},
+			PRIMARY KEY (id_word, id_msg)
+		)',
+		array(
+			'size' => $size,
+			'string_zero' => '0',
+		)
+	);
+}
+
+?>

+ 106 - 0
Sources/DbSearch-sqlite.php

@@ -0,0 +1,106 @@
+<?php
+
+/**
+ * Simple Machines Forum (SMF)
+ *
+ * @package SMF
+ * @author Simple Machines http://www.simplemachines.org
+ * @copyright 2011 Simple Machines
+ * @license http://www.simplemachines.org/about/smf/license.php BSD
+ *
+ * @version 2.0
+ */
+
+if (!defined('SMF'))
+	die('Hacking attempt...');
+
+/*	This file contains database functions specific to search related activity.
+
+	void db_search_init()
+		- adds the functions in this file to the $smcFunc array
+
+	boolean smf_db_search_support($search_type)
+		- whether this database type support the search type $search_type
+
+	void smf_db_create_word_search($size)
+ 		- create the custom word index table
+
+ 	resource smf_db_search_query($identifier, $db_string, $db_values = array(), $connection = null)
+		- returns the correct query for this search type.
+*/
+
+// Add the file functions to the $smcFunc array.
+function db_search_init()
+{
+	global $smcFunc;
+
+	if (!isset($smcFunc['db_search_query']) || $smcFunc['db_search_query'] != 'smf_db_search_query')
+		$smcFunc += array(
+			'db_search_query' => 'smf_db_search_query',
+			'db_search_support' => 'smf_db_search_support',
+			'db_create_word_search' => 'smf_db_create_word_search',
+			'db_support_ignore' => false,
+		);
+}
+
+// Does this database type support this search type?
+function smf_db_search_support($search_type)
+{
+	$supported_types = array('custom');
+
+	return in_array($search_type, $supported_types);
+}
+
+// Returns the correct query for this search type.
+function smf_db_search_query($identifier, $db_string, $db_values = array(), $connection = null)
+{
+	global $smcFunc;
+
+	$replacements = array(
+		'create_tmp_log_search_topics' => array(
+			'~mediumint\(\d\)~i' => 'int',
+			'~TYPE=HEAP~i' => '',
+		),
+		'create_tmp_log_search_messages' => array(
+			'~mediumint\(\d\)~i' => 'int',
+			'~TYPE=HEAP~i' => '',
+		),
+	);
+
+	if (isset($replacements[$identifier]))
+		$db_string = preg_replace(array_keys($replacements[$identifier]), array_values($replacements[$identifier]), $db_string);
+	elseif (preg_match('~^\s*INSERT\sIGNORE~i', $db_string) != 0)
+	{
+		$db_string = preg_replace('~^\s*INSERT\sIGNORE~i', 'INSERT', $db_string);
+		// Don't error on multi-insert.
+		$db_values['db_error_skip'] = true;
+	}
+
+	$return = $smcFunc['db_query']('', $db_string,
+		$db_values, $connection
+	);
+
+	return $return;
+}
+
+// Highly specific - create the custom word index table!
+function smf_db_create_word_search($size)
+{
+	global $smcFunc;
+
+	$size = 'int';
+
+	$smcFunc['db_query']('', '
+		CREATE TABLE {db_prefix}log_search_words (
+			id_word {raw:size} NOT NULL default {string:string_zero},
+			id_msg int(10) NOT NULL default {string:string_zero},
+			PRIMARY KEY (id_word, id_msg)
+		)',
+		array(
+			'size' => $size,
+			'string_zero' => '0',
+		)
+	);
+}
+
+?>

+ 1727 - 0
Sources/Display.php

@@ -0,0 +1,1727 @@
+<?php
+
+/**
+ * Simple Machines Forum (SMF)
+ *
+ * @package SMF
+ * @author Simple Machines http://www.simplemachines.org
+ * @copyright 2011 Simple Machines
+ * @license http://www.simplemachines.org/about/smf/license.php BSD
+ *
+ * @version 2.0
+ */
+
+if (!defined('SMF'))
+	die('Hacking attempt...');
+
+/*	This is perhaps the most important and probably most accessed files in all
+	of SMF.  This file controls topic, message, and attachment display.  It
+	does so with the following functions:
+
+	void Display()
+		- loads the posts in a topic up so they can be displayed.
+		- supports wireless, using wap/wap2/imode and the Wireless templates.
+		- uses the main sub template of the Display template.
+		- requires a topic, and can go to the previous or next topic from it.
+		- jumps to the correct post depending on a number/time/IS_MSG passed.
+		- depends on the messages_per_page, defaultMaxMessages and enableAllMessages settings.
+		- is accessed by ?topic=id_topic.START.
+
+	array prepareDisplayContext(bool reset = false)
+		- actually gets and prepares the message context.
+		- starts over from the beginning if reset is set to true, which is
+		  useful for showing an index before or after the posts.
+
+	void Download()
+		- downloads an attachment or avatar, and increments the downloads.
+		- requires the view_attachments permission. (not for avatars!)
+		- disables the session parser, and clears any previous output.
+		- depends on the attachmentUploadDir setting being correct.
+		- is accessed via the query string ?action=dlattach.
+		- views to attachments and avatars do not increase hits and are not
+		  logged in the "Who's Online" log.
+
+	array loadAttachmentContext(int id_msg)
+		- loads an attachment's contextual data including, most importantly,
+		  its size if it is an image.
+		- expects the $attachments array to have been filled with the proper
+		  attachment data, as Display() does.
+		- requires the view_attachments permission to calculate image size.
+		- attempts to keep the "aspect ratio" of the posted image in line,
+		  even if it has to be resized by the max_image_width and
+		  max_image_height settings.
+
+	int approved_attach_sort(array a, array b)
+		- a sort function for putting unapproved attachments first.
+
+	void QuickInTopicModeration()
+		- in-topic quick moderation.
+
+*/
+
+// The central part of the board - topic display.
+function Display()
+{
+	global $scripturl, $txt, $modSettings, $context, $settings;
+	global $options, $sourcedir, $user_info, $board_info, $topic, $board;
+	global $attachments, $messages_request, $topicinfo, $language, $smcFunc;
+
+	// What are you gonna display if these are empty?!
+	if (empty($topic))
+		fatal_lang_error('no_board', false);
+
+	// Load the proper template and/or sub template.
+	if (WIRELESS)
+		$context['sub_template'] = WIRELESS_PROTOCOL . '_display';
+	else
+		loadTemplate('Display');
+
+	// Not only does a prefetch make things slower for the server, but it makes it impossible to know if they read it.
+	if (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] == 'prefetch')
+	{
+		ob_end_clean();
+		header('HTTP/1.1 403 Prefetch Forbidden');
+		die;
+	}
+
+	// How much are we sticking on each page?
+	$context['messages_per_page'] = empty($modSettings['disableCustomPerPage']) && !empty($options['messages_per_page']) && !WIRELESS ? $options['messages_per_page'] : $modSettings['defaultMaxMessages'];
+
+	// Let's do some work on what to search index.
+	if (count($_GET) > 2)
+		foreach ($_GET as $k => $v)
+		{
+			if (!in_array($k, array('topic', 'board', 'start', session_name())))
+				$context['robot_no_index'] = true;
+		}
+
+	if (!empty($_REQUEST['start']) && (!is_numeric($_REQUEST['start']) || $_REQUEST['start'] % $context['messages_per_page'] != 0))
+		$context['robot_no_index'] = true;
+
+	// Find the previous or next topic.  Make a fuss if there are no more.
+	if (isset($_REQUEST['prev_next']) && ($_REQUEST['prev_next'] == 'prev' || $_REQUEST['prev_next'] == 'next'))
+	{
+		// No use in calculating the next topic if there's only one.
+		if ($board_info['num_topics'] > 1)
+		{
+			// Just prepare some variables that are used in the query.
+			$gt_lt = $_REQUEST['prev_next'] == 'prev' ? '>' : '<';
+			$order = $_REQUEST['prev_next'] == 'prev' ? '' : ' DESC';
+
+			$request = $smcFunc['db_query']('', '
+				SELECT t2.id_topic
+				FROM {db_prefix}topics AS t
+					INNER JOIN {db_prefix}topics AS t2 ON (' . (empty($modSettings['enableStickyTopics']) ? '
+					t2.id_last_msg ' . $gt_lt . ' t.id_last_msg' : '
+					(t2.id_last_msg ' . $gt_lt . ' t.id_last_msg AND t2.is_sticky ' . $gt_lt . '= t.is_sticky) OR t2.is_sticky ' . $gt_lt . ' t.is_sticky') . ')
+				WHERE t.id_topic = {int:current_topic}
+					AND t2.id_board = {int:current_board}' . (!$modSettings['postmod_active'] || allowedTo('approve_posts') ? '' : '
+					AND (t2.approved = {int:is_approved} OR (t2.id_member_started != {int:id_member_started} AND t2.id_member_started = {int:current_member}))') . '
+				ORDER BY' . (empty($modSettings['enableStickyTopics']) ? '' : ' t2.is_sticky' . $order . ',') . ' t2.id_last_msg' . $order . '
+				LIMIT 1',
+				array(
+					'current_board' => $board,
+					'current_member' => $user_info['id'],
+					'current_topic' => $topic,
+					'is_approved' => 1,
+					'id_member_started' => 0,
+				)
+			);
+
+			// No more left.
+			if ($smcFunc['db_num_rows']($request) == 0)
+			{
+				$smcFunc['db_free_result']($request);
+
+				// Roll over - if we're going prev, get the last - otherwise the first.
+				$request = $smcFunc['db_query']('', '
+					SELECT id_topic
+					FROM {db_prefix}topics
+					WHERE id_board = {int:current_board}' . (!$modSettings['postmod_active'] || allowedTo('approve_posts') ? '' : '
+						AND (approved = {int:is_approved} OR (id_member_started != {int:id_member_started} AND id_member_started = {int:current_member}))') . '
+					ORDER BY' . (empty($modSettings['enableStickyTopics']) ? '' : ' is_sticky' . $order . ',') . ' id_last_msg' . $order . '
+					LIMIT 1',
+					array(
+						'current_board' => $board,
+						'current_member' => $user_info['id'],
+						'is_approved' => 1,
+						'id_member_started' => 0,
+					)
+				);
+			}
+
+			// Now you can be sure $topic is the id_topic to view.
+			list ($topic) = $smcFunc['db_fetch_row']($request);
+			$smcFunc['db_free_result']($request);
+
+			$context['current_topic'] = $topic;
+		}
+
+		// Go to the newest message on this topic.
+		$_REQUEST['start'] = 'new';
+	}
+
+	// Add 1 to the number of views of this topic.
+	if (empty($_SESSION['last_read_topic']) || $_SESSION['last_read_topic'] != $topic)
+	{
+		$smcFunc['db_query']('', '
+			UPDATE {db_prefix}topics
+			SET num_views = num_views + 1
+			WHERE id_topic = {int:current_topic}',
+			array(
+				'current_topic' => $topic,
+			)
+		);
+
+		$_SESSION['last_read_topic'] = $topic;
+	}
+
+	// Get all the important topic info.
+	$request = $smcFunc['db_query']('', '
+		SELECT
+			t.num_replies, t.num_views, t.locked, ms.subject, t.is_sticky, t.id_poll,
+			t.id_member_started, t.id_first_msg, t.id_last_msg, t.approved, t.unapproved_posts,
+			' . ($user_info['is_guest'] ? 't.id_last_msg + 1' : 'IFNULL(lt.id_msg, IFNULL(lmr.id_msg, -1)) + 1') . ' AS new_from
+			' . (!empty($modSettings['recycle_board']) && $modSettings['recycle_board'] == $board ? ', id_previous_board, id_previous_topic' : '') . '
+		FROM {db_prefix}topics AS t
+			INNER JOIN {db_prefix}messages AS ms ON (ms.id_msg = t.id_first_msg)' . ($user_info['is_guest'] ? '' : '
+			LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = {int:current_topic} AND lt.id_member = {int:current_member})
+			LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = {int:current_board} AND lmr.id_member = {int:current_member})') . '
+		WHERE t.id_topic = {int:current_topic}
+		LIMIT 1',
+		array(
+			'current_member' => $user_info['id'],
+			'current_topic' => $topic,
+			'current_board' => $board,
+		)
+	);
+	if ($smcFunc['db_num_rows']($request) == 0)
+		fatal_lang_error('not_a_topic', false);
+	$topicinfo = $smcFunc['db_fetch_assoc']($request);
+	$smcFunc['db_free_result']($request);
+
+	$context['real_num_replies'] = $context['num_replies'] = $topicinfo['num_replies'];
+	$context['topic_first_message'] = $topicinfo['id_first_msg'];
+	$context['topic_last_message'] = $topicinfo['id_last_msg'];
+
+	// Add up unapproved replies to get real number of replies...
+	if ($modSettings['postmod_active'] && allowedTo('approve_posts'))
+		$context['real_num_replies'] += $topicinfo['unapproved_posts'] - ($topicinfo['approved'] ? 0 : 1);
+
+	// If this topic has unapproved posts, we need to work out how many posts the user can see, for page indexing.
+	if ($modSettings['postmod_active'] && $topicinfo['unapproved_posts'] && !$user_info['is_guest'] && !allowedTo('approve_posts'))
+	{
+		$request = $smcFunc['db_query']('', '
+			SELECT COUNT(id_member) AS my_unapproved_posts
+			FROM {db_prefix}messages
+			WHERE id_topic = {int:current_topic}
+				AND id_member = {int:current_member}
+				AND approved = 0',
+			array(
+				'current_topic' => $topic,
+				'current_member' => $user_info['id'],
+			)
+		);
+		list ($myUnapprovedPosts) = $smcFunc['db_fetch_row']($request);
+		$smcFunc['db_free_result']($request);
+
+		$context['total_visible_posts'] = $context['num_replies'] + $myUnapprovedPosts + ($topicinfo['approved'] ? 1 : 0);
+	}
+	else
+		$context['total_visible_posts'] = $context['num_replies'] + $topicinfo['unapproved_posts'] + ($topicinfo['approved'] ? 1 : 0);
+
+	// When was the last time this topic was replied to?  Should we warn them about it?
+	$request = $smcFunc['db_query']('', '
+		SELECT poster_time
+		FROM {db_prefix}messages
+		WHERE id_msg = {int:id_last_msg}
+		LIMIT 1',
+		array(
+			'id_last_msg' => $topicinfo['id_last_msg'],
+		)
+	);
+
+	list ($lastPostTime) = $smcFunc['db_fetch_row']($request);
+	$smcFunc['db_free_result']($request);
+
+	$context['oldTopicError'] = !empty($modSettings['oldTopicDays']) && $lastPostTime + $modSettings['oldTopicDays'] * 86400 < time() && empty($sticky);
+
+	// The start isn't a number; it's information about what to do, where to go.
+	if (!is_numeric($_REQUEST['start']))
+	{
+		// Redirect to the page and post with new messages, originally by Omar Bazavilvazo.
+		if ($_REQUEST['start'] == 'new')
+		{
+			// Guests automatically go to the last post.
+			if ($user_info['is_guest'])
+			{
+				$context['start_from'] = $context['total_visible_posts'] - 1;
+				$_REQUEST['start'] = empty($options['view_newest_first']) ? $context['start_from'] : 0;
+			}
+			else
+			{
+				// Find the earliest unread message in the topic. (the use of topics here is just for both tables.)
+				$request = $smcFunc['db_query']('', '
+					SELECT IFNULL(lt.id_msg, IFNULL(lmr.id_msg, -1)) + 1 AS new_from
+					FROM {db_prefix}topics AS t
+						LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = {int:current_topic} AND lt.id_member = {int:current_member})
+						LEFT JOIN {db_prefix}log_mark_read AS lmr ON (lmr.id_board = {int:current_board} AND lmr.id_member = {int:current_member})
+					WHERE t.id_topic = {int:current_topic}
+					LIMIT 1',
+					array(
+						'current_board' => $board,
+						'current_member' => $user_info['id'],
+						'current_topic' => $topic,
+					)
+				);
+				list ($new_from) = $smcFunc['db_fetch_row']($request);
+				$smcFunc['db_free_result']($request);
+
+				// Fall through to the next if statement.
+				$_REQUEST['start'] = 'msg' . $new_from;
+			}
+		}
+
+		// Start from a certain time index, not a message.
+		if (substr($_REQUEST['start'], 0, 4) == 'from')
+		{
+			$timestamp = (int) substr($_REQUEST['start'], 4);
+			if ($timestamp === 0)
+				$_REQUEST['start'] = 0;
+			else
+			{
+				// Find the number of messages posted before said time...
+				$request = $smcFunc['db_query']('', '
+					SELECT COUNT(*)
+					FROM {db_prefix}messages
+					WHERE poster_time < {int:timestamp}
+						AND id_topic = {int:current_topic}' . ($modSettings['postmod_active'] && $topicinfo['unapproved_posts'] && !allowedTo('approve_posts') ? '
+						AND (approved = {int:is_approved}' . ($user_info['is_guest'] ? '' : ' OR id_member = {int:current_member}') . ')' : ''),
+					array(
+						'current_topic' => $topic,
+						'current_member' => $user_info['id'],
+						'is_approved' => 1,
+						'timestamp' => $timestamp,
+					)
+				);
+				list ($context['start_from']) = $smcFunc['db_fetch_row']($request);
+				$smcFunc['db_free_result']($request);
+
+				// Handle view_newest_first options, and get the correct start value.
+				$_REQUEST['start'] = empty($options['view_newest_first']) ? $context['start_from'] : $context['total_visible_posts'] - $context['start_from'] - 1;
+			}
+		}
+
+		// Link to a message...
+		elseif (substr($_REQUEST['start'], 0, 3) == 'msg')
+		{
+			$virtual_msg = (int) substr($_REQUEST['start'], 3);
+			if (!$topicinfo['unapproved_posts'] && $virtual_msg >= $topicinfo['id_last_msg'])
+				$context['start_from'] = $context['total_visible_posts'] - 1;
+			elseif (!$topicinfo['unapproved_posts'] && $virtual_msg <= $topicinfo['id_first_msg'])
+				$context['start_from'] = 0;
+			else
+			{
+				// Find the start value for that message......
+				$request = $smcFunc['db_query']('', '
+					SELECT COUNT(*)
+					FROM {db_prefix}messages
+					WHERE id_msg < {int:virtual_msg}
+						AND id_topic = {int:current_topic}' . ($modSettings['postmod_active'] && $topicinfo['unapproved_posts'] && !allowedTo('approve_posts') ? '
+						AND (approved = {int:is_approved}' . ($user_info['is_guest'] ? '' : ' OR id_member = {int:current_member}') . ')' : ''),
+					array(
+						'current_member' => $user_info['id'],
+						'current_topic' => $topic,
+						'virtual_msg' => $virtual_msg,
+						'is_approved' => 1,
+						'no_member' => 0,
+					)
+				);
+				list ($context['start_from']) = $smcFunc['db_fetch_row']($request);
+				$smcFunc['db_free_result']($request);
+			}
+
+			// We need to reverse the start as well in this case.
+			$_REQUEST['start'] = empty($options['view_newest_first']) ? $context['start_from'] : $context['total_visible_posts'] - $context['start_from'] - 1;
+		}
+	}
+
+	// Create a previous next string if the selected theme has it as a selected option.
+	$context['previous_next'] = $modSettings['enablePreviousNext'] ? '<a href="' . $scripturl . '?topic=' . $topic . '.0;prev_next=prev#new">' . $txt['previous_next_back'] . '</a> <a href="' . $scripturl . '?topic=' . $topic . '.0;prev_next=next#new">' . $txt['previous_next_forward'] . '</a>' : '';
+
+	// Check if spellchecking is both enabled and actually working. (for quick reply.)
+	$context['show_spellchecking'] = !empty($modSettings['enableSpellChecking']) && function_exists('pspell_new');
+
+	// Do we need to show the visual verification image?
+	$context['require_verification'] = !$user_info['is_mod'] && !$user_info['is_admin'] && !empty($modSettings['posts_require_captcha']) && ($user_info['posts'] < $modSettings['posts_require_captcha'] || ($user_info['is_guest'] && $modSettings['posts_require_captcha'] == -1));
+	if ($context['require_verification'])
+	{
+		require_once($sourcedir . '/Subs-Editor.php');
+		$verificationOptions = array(
+			'id' => 'post',
+		);
+		$context['require_verification'] = create_control_verification($verificationOptions);
+		$context['visual_verification_id'] = $verificationOptions['id'];
+	}
+
+	// Are we showing signatures - or disabled fields?
+	$context['signature_enabled'] = substr($modSettings['signature_settings'], 0, 1) == 1;
+	$context['disabled_fields'] = isset($modSettings['disabled_profile_fields']) ? array_flip(explode(',', $modSettings['disabled_profile_fields'])) : array();
+
+	// Censor the title...
+	censorText($topicinfo['subject']);
+	$context['page_title'] = $topicinfo['subject'];
+
+	// Is this topic sticky, or can it even be?
+	$topicinfo['is_sticky'] = empty($modSettings['enableStickyTopics']) ? '0' : $topicinfo['is_sticky'];
+
+	// Default this topic to not marked for notifications... of course...
+	$context['is_marked_notify'] = false;
+
+	// Did we report a post to a moderator just now?
+	$context['report_sent'] = isset($_GET['reportsent']);
+
+	// Let's get nosey, who is viewing this topic?
+	if (!empty($settings['display_who_viewing']))
+	{
+		// Start out with no one at all viewing it.
+		$context['view_members'] = array();
+		$context['view_members_list'] = array();
+		$context['view_num_hidden'] = 0;
+
+		// Search for members who have this topic set in their GET data.
+		$request = $smcFunc['db_query']('', '
+			SELECT
+				lo.id_member, lo.log_time, mem.real_name, mem.member_name, mem.show_online,
+				mg.online_color, mg.id_group, mg.group_name
+			FROM {db_prefix}log_online AS lo
+				LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = lo.id_member)
+				LEFT JOIN {db_prefix}membergroups AS mg ON (mg.id_group = CASE WHEN mem.id_group = {int:reg_id_group} THEN mem.id_post_group ELSE mem.id_group END)
+			WHERE INSTR(lo.url, {string:in_url_string}) > 0 OR lo.session = {string:session}',
+			array(
+				'reg_id_group' => 0,
+				'in_url_string' => 's:5:"topic";i:' . $topic . ';',
+				'session' => $user_info['is_guest'] ? 'ip' . $user_info['ip'] : session_id(),
+			)
+		);
+		while ($row = $smcFunc['db_fetch_assoc']($request))
+		{
+			if (empty($row['id_member']))
+				continue;
+
+			if (!empty($row['online_color']))
+				$link = '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '" style="color: ' . $row['online_color'] . ';">' . $row['real_name'] . '</a>';
+			else
+				$link = '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['real_name'] . '</a>';
+
+			$is_buddy = in_array($row['id_member'], $user_info['buddies']);
+			if ($is_buddy)
+				$link = '<strong>' . $link . '</strong>';
+
+			// Add them both to the list and to the more detailed list.
+			if (!empty($row['show_online']) || allowedTo('moderate_forum'))
+				$context['view_members_list'][$row['log_time'] . $row['member_name']] = empty($row['show_online']) ? '<em>' . $link . '</em>' : $link;
+			$context['view_members'][$row['log_time'] . $row['member_name']] = array(
+				'id' => $row['id_member'],
+				'username' => $row['member_name'],
+				'name' => $row['real_name'],
+				'group' => $row['id_group'],
+				'href' => $scripturl . '?action=profile;u=' . $row['id_member'],
+				'link' => $link,
+				'is_buddy' => $is_buddy,
+				'hidden' => empty($row['show_online']),
+			);
+
+			if (empty($row['show_online']))
+				$context['view_num_hidden']++;
+		}
+
+		// The number of guests is equal to the rows minus the ones we actually used ;).
+		$context['view_num_guests'] = $smcFunc['db_num_rows']($request) - count($context['view_members']);
+		$smcFunc['db_free_result']($request);
+
+		// Sort the list.
+		krsort($context['view_members']);
+		krsort($context['view_members_list']);
+	}
+
+	// If all is set, but not allowed... just unset it.
+	$can_show_all = !empty($modSettings['enableAllMessages']) && $context['total_visible_posts'] > $context['messages_per_page'] && $context['total_visible_posts'] < $modSettings['enableAllMessages'];
+	if (isset($_REQUEST['all']) && !$can_show_all)
+		unset($_REQUEST['all']);
+	// Otherwise, it must be allowed... so pretend start was -1.
+	elseif (isset($_REQUEST['all']))
+		$_REQUEST['start'] = -1;
+
+	// Construct the page index, allowing for the .START method...
+	$context['page_index'] = constructPageIndex($scripturl . '?topic=' . $topic . '.%1$d', $_REQUEST['start'], $context['total_visible_posts'], $context['messages_per_page'], true);
+	$context['start'] = $_REQUEST['start'];
+
+	// This is information about which page is current, and which page we're on - in case you don't like the constructed page index. (again, wireles..)
+	$context['page_info'] = array(
+		'current_page' => $_REQUEST['start'] / $context['messages_per_page'] + 1,
+		'num_pages' => floor(($context['total_visible_posts'] - 1) / $context['messages_per_page']) + 1,
+	);
+
+	// Figure out all the link to the next/prev/first/last/etc. for wireless mainly.
+	$context['links'] = array(
+		'first' => $_REQUEST['start'] >= $context['messages_per_page'] ? $scripturl . '?topic=' . $topic . '.0' : '',
+		'prev' => $_REQUEST['start'] >= $context['messages_per_page'] ? $scripturl . '?topic=' . $topic . '.' . ($_REQUEST['start'] - $context['messages_per_page']) : '',
+		'next' => $_REQUEST['start'] + $context['messages_per_page'] < $context['total_visible_posts'] ? $scripturl . '?topic=' . $topic. '.' . ($_REQUEST['start'] + $context['messages_per_page']) : '',
+		'last' => $_REQUEST['start'] + $context['messages_per_page'] < $context['total_visible_posts'] ? $scripturl . '?topic=' . $topic. '.' . (floor($context['total_visible_posts'] / $context['messages_per_page']) * $context['messages_per_page']) : '',
+		'up' => $scripturl . '?board=' . $board . '.0'
+	);
+
+	// If they are viewing all the posts, show all the posts, otherwise limit the number.
+	if ($can_show_all)
+	{
+		if (isset($_REQUEST['all']))
+		{
+			// No limit! (actually, there is a limit, but...)
+			$context['messages_per_page'] = -1;
+			$context['page_index'] .= empty($modSettings['compactTopicPagesEnable']) ? '<strong>' . $txt['all'] . '</strong> ' : '[<strong>' . $txt['all'] . '</strong>] ';
+
+			// Set start back to 0...
+			$_REQUEST['start'] = 0;
+		}
+		// They aren't using it, but the *option* is there, at least.
+		else
+			$context['page_index'] .= '&nbsp;<a href="' . $scripturl . '?topic=' . $topic . '.0;all">' . $txt['all'] . '</a> ';
+	}
+
+	// Build the link tree.
+	$context['linktree'][] = array(
+		'url' => $scripturl . '?topic=' . $topic . '.0',
+		'name' => $topicinfo['subject'],
+		'extra_before' => $settings['linktree_inline'] ? $txt['topic'] . ': ' : ''
+	);
+
+	// Build a list of this board's moderators.
+	$context['moderators'] = &$board_info['moderators'];
+	$context['link_moderators'] = array();
+	if (!empty($board_info['moderators']))
+	{
+		// Add a link for each moderator...
+		foreach ($board_info['moderators'] as $mod)
+			$context['link_moderators'][] = '<a href="' . $scripturl . '?action=profile;u=' . $mod['id'] . '" title="' . $txt['board_moderator'] . '">' . $mod['name'] . '</a>';
+
+		// And show it after the board's name.
+		$context['linktree'][count($context['linktree']) - 2]['extra_after'] = ' (' . (count($context['link_moderators']) == 1 ? $txt['moderator'] : $txt['moderators']) . ': ' . implode(', ', $context['link_moderators']) . ')';
+	}
+
+	// Information about the current topic...
+	$context['is_locked'] = $topicinfo['locked'];
+	$context['is_sticky'] = $topicinfo['is_sticky'];
+	$context['is_very_hot'] = $topicinfo['num_replies'] >= $modSettings['hotTopicVeryPosts'];
+	$context['is_hot'] = $topicinfo['num_replies'] >= $modSettings['hotTopicPosts'];
+	$context['is_approved'] = $topicinfo['approved'];
+
+	// We don't want to show the poll icon in the topic class here, so pretend it's not one.
+	$context['is_poll'] = false;
+	determineTopicClass($context);
+
+	$context['is_poll'] = $topicinfo['id_poll'] > 0 && $modSettings['pollMode'] == '1' && allowedTo('poll_view');
+
+	// Did this user start the topic or not?
+	$context['user']['started'] = $user_info['id'] == $topicinfo['id_member_started'] && !$user_info['is_guest'];
+	$context['topic_starter_id'] = $topicinfo['id_member_started'];
+
+	// Set the topic's information for the template.
+	$context['subject'] = $topicinfo['subject'];
+	$context['num_views'] = $topicinfo['num_views'];
+	$context['mark_unread_time'] = $topicinfo['new_from'];
+
+	// Set a canonical URL for this page.
+	$context['canonical_url'] = $scripturl . '?topic=' . $topic . '.' . $context['start'];
+
+	// For quick reply we need a response prefix in the default forum language.
+	if (!isset($context['response_prefix']) && !($context['response_prefix'] = cache_get_data('response_prefix', 600)))
+	{
+		if ($language === $user_info['language'])
+			$context['response_prefix'] = $txt['response_prefix'];
+		else
+		{
+			loadLanguage('index', $language, false);
+			$context['response_prefix'] = $txt['response_prefix'];
+			loadLanguage('index');
+		}
+		cache_put_data('response_prefix', $context['response_prefix'], 600);
+	}
+
+	// If we want to show event information in the topic, prepare the data.
+	if (allowedTo('calendar_view') && !empty($modSettings['cal_showInTopic']) && !empty($modSettings['cal_enabled']))
+	{
+		// First, try create a better time format, ignoring the "time" elements.
+		if (preg_match('~%[AaBbCcDdeGghjmuYy](?:[^%]*%[AaBbCcDdeGghjmuYy])*~', $user_info['time_format'], $matches) == 0 || empty($matches[0]))
+			$date_string = $user_info['time_format'];
+		else
+			$date_string = $matches[0];
+
+		// Any calendar information for this topic?
+		$request = $smcFunc['db_query']('', '
+			SELECT cal.id_event, cal.start_date, cal.end_date, cal.title, cal.id_member, mem.real_name
+			FROM {db_prefix}calendar AS cal
+				LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = cal.id_member)
+			WHERE cal.id_topic = {int:current_topic}
+			ORDER BY start_date',
+			array(
+				'current_topic' => $topic,
+			)
+		);
+		$context['linked_calendar_events'] = array();
+		while ($row = $smcFunc['db_fetch_assoc']($request))
+		{
+			// Prepare the dates for being formatted.
+			$start_date = sscanf($row['start_date'], '%04d-%02d-%02d');
+			$start_date = mktime(12, 0, 0, $start_date[1], $start_date[2], $start_date[0]);
+			$end_date = sscanf($row['end_date'], '%04d-%02d-%02d');
+			$end_date = mktime(12, 0, 0, $end_date[1], $end_date[2], $end_date[0]);
+
+			$context['linked_calendar_events'][] = array(
+				'id' => $row['id_event'],
+				'title' => $row['title'],
+				'can_edit' => allowedTo('calendar_edit_any') || ($row['id_member'] == $user_info['id'] && allowedTo('calendar_edit_own')),
+				'modify_href' => $scripturl . '?action=post;msg=' . $topicinfo['id_first_msg'] . ';topic=' . $topic . '.0;calendar;eventid=' . $row['id_event'] . ';' . $context['session_var'] . '=' . $context['session_id'],
+				'start_date' => timeformat($start_date, $date_string, 'none'),
+				'start_timestamp' => $start_date,
+				'end_date' => timeformat($end_date, $date_string, 'none'),
+				'end_timestamp' => $end_date,
+				'is_last' => false
+			);
+		}
+		$smcFunc['db_free_result']($request);
+
+		if (!empty($context['linked_calendar_events']))
+			$context['linked_calendar_events'][count($context['linked_calendar_events']) - 1]['is_last'] = true;
+	}
+
+	// Create the poll info if it exists.
+	if ($context['is_poll'])
+	{
+		// Get the question and if it's locked.
+		$request = $smcFunc['db_query']('', '
+			SELECT
+				p.question, p.voting_locked, p.hide_results, p.expire_time, p.max_votes, p.change_vote,
+				p.guest_vote, p.id_member, IFNULL(mem.real_name, p.poster_name) AS poster_name, p.num_guest_voters, p.reset_poll
+			FROM {db_prefix}polls AS p
+				LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = p.id_member)
+			WHERE p.id_poll = {int:id_poll}
+			LIMIT 1',
+			array(
+				'id_poll' => $topicinfo['id_poll'],
+			)
+		);
+		$pollinfo = $smcFunc['db_fetch_assoc']($request);
+		$smcFunc['db_free_result']($request);
+
+		$request = $smcFunc['db_query']('', '
+			SELECT COUNT(DISTINCT id_member) AS total
+			FROM {db_prefix}log_polls
+			WHERE id_poll = {int:id_poll}
+				AND id_member != {int:not_guest}',
+			array(
+				'id_poll' => $topicinfo['id_poll'],
+				'not_guest' => 0,
+			)
+		);
+		list ($pollinfo['total']) = $smcFunc['db_fetch_row']($request);
+		$smcFunc['db_free_result']($request);
+
+		// Total voters needs to include guest voters
+		$pollinfo['total'] += $pollinfo['num_guest_voters'];
+
+		// Get all the options, and calculate the total votes.
+		$request = $smcFunc['db_query']('', '
+			SELECT pc.id_choice, pc.label, pc.votes, IFNULL(lp.id_choice, -1) AS voted_this
+			FROM {db_prefix}poll_choices AS pc
+				LEFT JOIN {db_prefix}log_polls AS lp ON (lp.id_choice = pc.id_choice AND lp.id_poll = {int:id_poll} AND lp.id_member = {int:current_member} AND lp.id_member != {int:not_guest})
+			WHERE pc.id_poll = {int:id_poll}',
+			array(
+				'current_member' => $user_info['id'],
+				'id_poll' => $topicinfo['id_poll'],
+				'not_guest' => 0,
+			)
+		);
+		$pollOptions = array();
+		$realtotal = 0;
+		$pollinfo['has_voted'] = false;
+		while ($row = $smcFunc['db_fetch_assoc']($request))
+		{
+			censorText($row['label']);
+			$pollOptions[$row['id_choice']] = $row;
+			$realtotal += $row['votes'];
+			$pollinfo['has_voted'] |= $row['voted_this'] != -1;
+		}
+		$smcFunc['db_free_result']($request);
+
+		// If this is a guest we need to do our best to work out if they have voted, and what they voted for.
+		if ($user_info['is_guest'] && $pollinfo['guest_vote'] && allowedTo('poll_vote'))
+		{
+			if (!empty($_COOKIE['guest_poll_vote']) && preg_match('~^[0-9,;]+$~', $_COOKIE['guest_poll_vote']) && strpos($_COOKIE['guest_poll_vote'], ';' . $topicinfo['id_poll'] . ',') !== false)
+			{
+				// ;id,timestamp,[vote,vote...]; etc
+				$guestinfo = explode(';', $_COOKIE['guest_poll_vote']);
+				// Find the poll we're after.
+				foreach ($guestinfo as $i => $guestvoted)
+				{
+					$guestvoted = explode(',', $guestvoted);
+					if ($guestvoted[0] == $topicinfo['id_poll'])
+						break;
+				}
+				// Has the poll been reset since guest voted?
+				if ($pollinfo['reset_poll'] > $guestvoted[1])
+				{
+					// Remove the poll info from the cookie to allow guest to vote again
+					unset($guestinfo[$i]);
+					if (!empty($guestinfo))
+						$_COOKIE['guest_poll_vote'] = ';' . implode(';', $guestinfo);
+					else
+						unset($_COOKIE['guest_poll_vote']);
+				}
+				else
+				{
+					// What did they vote for?
+					unset($guestvoted[0], $guestvoted[1]);
+					foreach ($pollOptions as $choice => $details)
+					{
+						$pollOptions[$choice]['voted_this'] = in_array($choice, $guestvoted) ? 1 : -1;
+						$pollinfo['has_voted'] |= $pollOptions[$choice]['voted_this'] != -1;
+					}
+					unset($choice, $details, $guestvoted);
+				}
+				unset($guestinfo, $guestvoted, $i);
+			}
+		}
+
+		// Set up the basic poll information.
+		$context['poll'] = array(
+			'id' => $topicinfo['id_poll'],
+			'image' => 'normal_' . (empty($pollinfo['voting_locked']) ? 'poll' : 'locked_poll'),
+			'question' => parse_bbc($pollinfo['question']),
+			'total_votes' => $pollinfo['total'],
+			'change_vote' => !empty($pollinfo['change_vote']),
+			'is_locked' => !empty($pollinfo['voting_locked']),
+			'options' => array(),
+			'lock' => allowedTo('poll_lock_any') || ($context['user']['started'] && allowedTo('poll_lock_own')),
+			'edit' => allowedTo('poll_edit_any') || ($context['user']['started'] && allowedTo('poll_edit_own')),
+			'allowed_warning' => $pollinfo['max_votes'] > 1 ? sprintf($txt['poll_options6'], min(count($pollOptions), $pollinfo['max_votes'])) : '',
+			'is_expired' => !empty($pollinfo['expire_time']) && $pollinfo['expire_time'] < time(),
+			'expire_time' => !empty($pollinfo['expire_time']) ? timeformat($pollinfo['expire_time']) : 0,
+			'has_voted' => !empty($pollinfo['has_voted']),
+			'starter' => array(
+				'id' => $pollinfo['id_member'],
+				'name' => $row['poster_name'],
+				'href' => $pollinfo['id_member'] == 0 ? '' : $scripturl . '?action=profile;u=' . $pollinfo['id_member'],
+				'link' => $pollinfo['id_member'] == 0 ? $row['poster_name'] : '<a href="' . $scripturl . '?action=profile;u=' . $pollinfo['id_member'] . '">' . $row['poster_name'] . '</a>'
+			)
+		);
+
+		// Make the lock and edit permissions defined above more directly accessible.
+		$context['allow_lock_poll'] = $context['poll']['lock'];
+		$context['allow_edit_poll'] = $context['poll']['edit'];
+
+		// You're allowed to vote if:
+		// 1. the poll did not expire, and
+		// 2. you're either not a guest OR guest voting is enabled... and
+		// 3. you're not trying to view the results, and
+		// 4. the poll is not locked, and
+		// 5. you have the proper permissions, and
+		// 6. you haven't already voted before.
+		$context['allow_vote'] = !$context['poll']['is_expired'] && (!$user_info['is_guest'] || ($pollinfo['guest_vote'] && allowedTo('poll_vote'))) && empty($pollinfo['voting_locked']) && allowedTo('poll_vote') && !$context['poll']['has_voted'];
+
+		// You're allowed to view the results if:
+		// 1. you're just a super-nice-guy, or
+		// 2. anyone can see them (hide_results == 0), or
+		// 3. you can see them after you voted (hide_results == 1), or
+		// 4. you've waited long enough for the poll to expire. (whether hide_results is 1 or 2.)
+		$context['allow_poll_view'] = allowedTo('moderate_board') || $pollinfo['hide_results'] == 0 || ($pollinfo['hide_results'] == 1 && $context['poll']['has_voted']) || $context['poll']['is_expired'];
+		$context['poll']['show_results'] = $context['allow_poll_view'] && (isset($_REQUEST['viewresults']) || isset($_REQUEST['viewResults']));
+		$context['show_view_results_button'] = $context['allow_vote'] && (!$context['allow_poll_view'] || !$context['poll']['show_results'] || !$context['poll']['has_voted']);
+
+		// You're allowed to change your vote if:
+		// 1. the poll did not expire, and
+		// 2. you're not a guest... and
+		// 3. the poll is not locked, and
+		// 4. you have the proper permissions, and
+		// 5. you have already voted, and
+		// 6. the poll creator has said you can!
+		$context['allow_change_vote'] = !$context['poll']['is_expired'] && !$user_info['is_guest'] && empty($pollinfo['voting_locked']) && allowedTo('poll_vote') && $context['poll']['has_voted'] && $context['poll']['change_vote'];
+
+		// You're allowed to return to voting options if:
+		// 1. you are (still) allowed to vote.
+		// 2. you are currently seeing the results.
+		$context['allow_return_vote'] = $context['allow_vote'] && $context['poll']['show_results'];
+
+		// Calculate the percentages and bar lengths...
+		$divisor = $realtotal == 0 ? 1 : $realtotal;
+
+		// Determine if a decimal point is needed in order for the options to add to 100%.
+		$precision = $realtotal == 100 ? 0 : 1;
+
+		// Now look through each option, and...
+		foreach ($pollOptions as $i => $option)
+		{
+			// First calculate the percentage, and then the width of the bar...
+			$bar = round(($option['votes'] * 100) / $divisor, $precision);
+			$barWide = $bar == 0 ? 1 : floor(($bar * 8) / 3);
+
+			// Now add it to the poll's contextual theme data.
+			$context['poll']['options'][$i] = array(
+				'id' => 'options-' . $i,
+				'percent' => $bar,
+				'votes' => $option['votes'],
+				'voted_this' => $option['voted_this'] != -1,
+				'bar' => '<span style="white-space: nowrap;"><img src="' . $settings['images_url'] . '/poll_' . ($context['right_to_left'] ? 'right' : 'left') . '.gif" alt="" /><img src="' . $settings['images_url'] . '/poll_middle.gif" width="' . $barWide . '" height="12" alt="-" /><img src="' . $settings['images_url'] . '/poll_' . ($context['right_to_left'] ? 'left' : 'right') . '.gif" alt="" /></span>',
+				// Note: IE < 8 requires us to set a width on the container, too.
+				'bar_ndt' => $bar > 0 ? '<div class="bar" style="width: ' . ($bar * 3.5 + 4) . 'px;"><div style="width: ' . $bar * 3.5 . 'px;"></div></div>' : '',
+				'bar_width' => $barWide,
+				'option' => parse_bbc($option['label']),
+				'vote_button' => '<input type="' . ($pollinfo['max_votes'] > 1 ? 'checkbox' : 'radio') . '" name="options[]" id="options-' . $i . '" value="' . $i . '" class="input_' . ($pollinfo['max_votes'] > 1 ? 'check' : 'radio') . '" />'
+			);
+		}
+	}
+
+	// Calculate the fastest way to get the messages!
+	$ascending = empty($options['view_newest_first']);
+	$start = $_REQUEST['start'];
+	$limit = $context['messages_per_page'];
+	$firstIndex = 0;
+	if ($start >= $context['total_visible_posts'] / 2 && $context['messages_per_page'] != -1)
+	{
+		$ascending = !$ascending;
+		$limit = $context['total_visible_posts'] <= $start + $limit ? $context['total_visible_posts'] - $start : $limit;
+		$start = $context['total_visible_posts'] <= $start + $limit ? 0 : $context['total_visible_posts'] - $start - $limit;
+		$firstIndex = $limit - 1;
+	}
+
+	// Get each post and poster in this topic.
+	$request = $smcFunc['db_query']('display_get_post_poster', '
+		SELECT id_msg, id_member, approved
+		FROM {db_prefix}messages
+		WHERE id_topic = {int:current_topic}' . (!$modSettings['postmod_active'] || allowedTo('approve_posts') ? '' : (!empty($modSettings['db_mysql_group_by_fix']) ? '' : '
+		GROUP BY id_msg') . '
+		HAVING (approved = {int:is_approved}' . ($user_info['is_guest'] ? '' : ' OR id_member = {int:current_member}') . ')') . '
+		ORDER BY id_msg ' . ($ascending ? '' : 'DESC') . ($context['messages_per_page'] == -1 ? '' : '
+		LIMIT ' . $start . ', ' . $limit),
+		array(
+			'current_member' => $user_info['id'],
+			'current_topic' => $topic,
+			'is_approved' => 1,
+			'blank_id_member' => 0,
+		)
+	);
+
+	$messages = array();
+	$all_posters = array();
+	while ($row = $smcFunc['db_fetch_assoc']($request))
+	{
+		if (!empty($row['id_member']))
+			$all_posters[$row['id_msg']] = $row['id_member'];
+		$messages[] = $row['id_msg'];
+	}
+	$smcFunc['db_free_result']($request);
+	$posters = array_unique($all_posters);
+
+	// Guests can't mark topics read or for notifications, just can't sorry.
+	if (!$user_info['is_guest'])
+	{
+		$mark_at_msg = max($messages);
+		if ($mark_at_msg >= $topicinfo['id_last_msg'])
+			$mark_at_msg = $modSettings['maxMsgID'];
+		if ($mark_at_msg >= $topicinfo['new_from'])
+		{
+			$smcFunc['db_insert']($topicinfo['new_from'] == 0 ? 'ignore' : 'replace',
+				'{db_prefix}log_topics',
+				array(
+					'id_member' => 'int', 'id_topic' => 'int', 'id_msg' => 'int',
+				),
+				array(
+					$user_info['id'], $topic, $mark_at_msg,
+				),
+				array('id_member', 'id_topic')
+			);
+		}
+
+		// Check for notifications on this topic OR board.
+		$request = $smcFunc['db_query']('', '
+			SELECT sent, id_topic
+			FROM {db_prefix}log_notify
+			WHERE (id_topic = {int:current_topic} OR id_board = {int:current_board})
+				AND id_member = {int:current_member}
+			LIMIT 2',
+			array(
+				'current_board' => $board,
+				'current_member' => $user_info['id'],
+				'current_topic' => $topic,
+			)
+		);
+		$do_once = true;
+		while ($row = $smcFunc['db_fetch_assoc']($request))
+		{
+			// Find if this topic is marked for notification...
+			if (!empty($row['id_topic']))
+				$context['is_marked_notify'] = true;
+
+			// Only do this once, but mark the notifications as "not sent yet" for next time.
+			if (!empty($row['sent']) && $do_once)
+			{
+				$smcFunc['db_query']('', '
+					UPDATE {db_prefix}log_notify
+					SET sent = {int:is_not_sent}
+					WHERE (id_topic = {int:current_topic} OR id_board = {int:current_board})
+						AND id_member = {int:current_member}',
+					array(
+						'current_board' => $board,
+						'current_member' => $user_info['id'],
+						'current_topic' => $topic,
+						'is_not_sent' => 0,
+					)
+				);
+				$do_once = false;
+			}
+		}
+
+		// Have we recently cached the number of new topics in this board, and it's still a lot?
+		if (isset($_REQUEST['topicseen']) && isset($_SESSION['topicseen_cache'][$board]) && $_SESSION['topicseen_cache'][$board] > 5)
+			$_SESSION['topicseen_cache'][$board]--;
+		// Mark board as seen if this is the only new topic.
+		elseif (isset($_REQUEST['topicseen']))
+		{
+			// Use the mark read tables... and the last visit to figure out if this should be read or not.
+			$request = $smcFunc['db_query']('', '
+				SELECT COUNT(*)
+				FROM {db_prefix}topics AS t
+					LEFT JOIN {db_prefix}log_boards AS lb ON (lb.id_board = {int:current_board} AND lb.id_member = {int:current_member})
+					LEFT JOIN {db_prefix}log_topics AS lt ON (lt.id_topic = t.id_topic AND lt.id_member = {int:current_member})
+				WHERE t.id_board = {int:current_board}
+					AND t.id_last_msg > IFNULL(lb.id_msg, 0)
+					AND t.id_last_msg > IFNULL(lt.id_msg, 0)' . (empty($_SESSION['id_msg_last_visit']) ? '' : '
+					AND t.id_last_msg > {int:id_msg_last_visit}'),
+				array(
+					'current_board' => $board,
+					'current_member' => $user_info['id'],
+					'id_msg_last_visit' => (int) $_SESSION['id_msg_last_visit'],
+				)
+			);
+			list ($numNewTopics) = $smcFunc['db_fetch_row']($request);
+			$smcFunc['db_free_result']($request);
+
+			// If there're no real new topics in this board, mark the board as seen.
+			if (empty($numNewTopics))
+				$_REQUEST['boardseen'] = true;
+			else
+				$_SESSION['topicseen_cache'][$board] = $numNewTopics;
+		}
+		// Probably one less topic - maybe not, but even if we decrease this too fast it will only make us look more often.
+		elseif (isset($_SESSION['topicseen_cache'][$board]))
+			$_SESSION['topicseen_cache'][$board]--;
+
+		// Mark board as seen if we came using last post link from BoardIndex. (or other places...)
+		if (isset($_REQUEST['boardseen']))
+		{
+			$smcFunc['db_insert']('replace',
+				'{db_prefix}log_boards',
+				array('id_msg' => 'int', 'id_member' => 'int', 'id_board' => 'int'),
+				array($modSettings['maxMsgID'], $user_info['id'], $board),
+				array('id_member', 'id_board')
+			);
+		}
+	}
+
+	$attachments = array();
+
+	// If there _are_ messages here... (probably an error otherwise :!)
+	if (!empty($messages))
+	{
+		// Fetch attachments.
+		if (!empty($modSettings['attachmentEnable']) && allowedTo('view_attachments'))
+		{
+			$request = $smcFunc['db_query']('', '
+				SELECT
+					a.id_attach, a.id_folder, a.id_msg, a.filename, a.file_hash, IFNULL(a.size, 0) AS filesize, a.downloads, a.approved,
+					a.width, a.height' . (empty($modSettings['attachmentShowImages']) || empty($modSettings['attachmentThumbnails']) ? '' : ',
+					IFNULL(thumb.id_attach, 0) AS id_thumb, thumb.width AS thumb_width, thumb.height AS thumb_height') . '
+				FROM {db_prefix}attachments AS a' . (empty($modSettings['attachmentShowImages']) || empty($modSettings['attachmentThumbnails']) ? '' : '
+					LEFT JOIN {db_prefix}attachments AS thumb ON (thumb.id_attach = a.id_thumb)') . '
+				WHERE a.id_msg IN ({array_int:message_list})
+					AND a.attachment_type = {int:attachment_type}',
+				array(
+					'message_list' => $messages,
+					'attachment_type' => 0,
+					'is_approved' => 1,
+				)
+			);
+			$temp = array();
+			while ($row = $smcFunc['db_fetch_assoc']($request))
+			{
+				if (!$row['approved'] && $modSettings['postmod_active'] && !allowedTo('approve_posts') && (!isset($all_posters[$row['id_msg']]) || $all_posters[$row['id_msg']] != $user_info['id']))
+					continue;
+
+				$temp[$row['id_attach']] = $row;
+
+				if (!isset($attachments[$row['id_msg']]))
+					$attachments[$row['id_msg']] = array();
+			}
+			$smcFunc['db_free_result']($request);
+
+			// This is better than sorting it with the query...
+			ksort($temp);
+
+			foreach ($temp as $row)
+				$attachments[$row['id_msg']][] = $row;
+		}
+
+		// What?  It's not like it *couldn't* be only guests in this topic...
+		if (!empty($posters))
+			loadMemberData($posters);
+		$messages_request = $smcFunc['db_query']('', '
+			SELECT
+				id_msg, icon, subject, poster_time, poster_ip, id_member, modified_time, modified_name, body,
+				smileys_enabled, poster_name, poster_email, approved,
+				id_msg_modified < {int:new_from} AS is_read
+			FROM {db_prefix}messages
+			WHERE id_msg IN ({array_int:message_list})
+			ORDER BY id_msg' . (empty($options['view_newest_first']) ? '' : ' DESC'),
+			array(
+				'message_list' => $messages,
+				'new_from' => $topicinfo['new_from'],
+			)
+		);
+
+		// Go to the last message if the given time is beyond the time of the last message.
+		if (isset($context['start_from']) && $context['start_from'] >= $topicinfo['num_replies'])
+			$context['start_from'] = $topicinfo['num_replies'];
+
+		// Since the anchor information is needed on the top of the page we load these variables beforehand.
+		$context['first_message'] = isset($messages[$firstIndex]) ? $messages[$firstIndex] : $messages[0];
+		if (empty($options['view_newest_first']))
+			$context['first_new_message'] = isset($context['start_from']) && $_REQUEST['start'] == $context['start_from'];
+		else
+			$context['first_new_message'] = isset($context['start_from']) && $_REQUEST['start'] == $topicinfo['num_replies'] - $context['start_from'];
+	}
+	else
+	{
+		$messages_request = false;
+		$context['first_message'] = 0;
+		$context['first_new_message'] = false;
+	}
+
+	$context['jump_to'] = array(
+		'label' => addslashes(un_htmlspecialchars($txt['jump_to'])),
+		'board_name' => htmlspecialchars(strtr(strip_tags($board_info['name']), array('&amp;' => '&'))),
+		'child_level' => $board_info['child_level'],
+	);
+
+	// Set the callback.  (do you REALIZE how much memory all the messages would take?!?)
+	$context['get_message'] = 'prepareDisplayContext';
+
+	// Now set all the wonderful, wonderful permissions... like moderation ones...
+	$common_permissions = array(
+		'can_approve' => 'approve_posts',
+		'can_ban' => 'manage_bans',
+		'can_sticky' => 'make_sticky',
+		'can_merge' => 'merge_any',
+		'can_split' => 'split_any',
+		'calendar_post' => 'calendar_post',
+		'can_mark_notify' => 'mark_any_notify',
+		'can_send_topic' => 'send_topic',
+		'can_send_pm' => 'pm_send',
+		'can_report_moderator' => 'report_any',
+		'can_moderate_forum' => 'moderate_forum',
+		'can_issue_warning' => 'issue_warning',
+		'can_restore_topic' => 'move_any',
+		'can_restore_msg' => 'move_any',
+	);
+	foreach ($common_permissions as $contextual => $perm)
+		$context[$contextual] = allowedTo($perm);
+
+	// Permissions with _any/_own versions.  $context[YYY] => ZZZ_any/_own.
+	$anyown_permissions = array(
+		'can_move' => 'move',
+		'can_lock' => 'lock',
+		'can_delete' => 'remove',
+		'can_add_poll' => 'poll_add',
+		'can_remove_poll' => 'poll_remove',
+		'can_reply' => 'post_reply',
+		'can_reply_unapproved' => 'post_unapproved_replies',
+	);
+	foreach ($anyown_permissions as $contextual => $perm)
+		$context[$contextual] = allowedTo($perm . '_any') || ($context['user']['started'] && allowedTo($perm . '_own'));
+
+	// Cleanup all the permissions with extra stuff...
+	$context['can_mark_notify'] &= !$context['user']['is_guest'];
+	$context['can_sticky'] &= !empty($modSettings['enableStickyTopics']);
+	$context['calendar_post'] &= !empty($modSettings['cal_enabled']);
+	$context['can_add_poll'] &= $modSettings['pollMode'] == '1' && $topicinfo['id_poll'] <= 0;
+	$context['can_remove_poll'] &= $modSettings['pollMode'] == '1' && $topicinfo['id_poll'] > 0;
+	$context['can_reply'] &= empty($topicinfo['locked']) || allowedTo('moderate_board');
+	$context['can_reply_unapproved'] &= $modSettings['postmod_active'] && (empty($topicinfo['locked']) || allowedTo('moderate_board'));
+	$context['can_issue_warning'] &= in_array('w', $context['admin_features']) && $modSettings['warning_settings'][0] == 1;
+	// Handle approval flags...
+	$context['can_reply_approved'] = $context['can_reply'];
+	$context['can_reply'] |= $context['can_reply_unapproved'];
+	$context['can_quote'] = $context['can_reply'] && (empty($modSettings['disabledBBC']) || !in_array('quote', explode(',', $modSettings['disabledBBC'])));
+	$context['can_mark_unread'] = !$user_info['is_guest'] && $settings['show_mark_read'];
+
+	$context['can_send_topic'] = (!$modSettings['postmod_active'] || $topicinfo['approved']) && allowedTo('send_topic');
+
+	// Start this off for quick moderation - it will be or'd for each post.
+	$context['can_remove_post'] = allowedTo('delete_any') || (allowedTo('delete_replies') && $context['user']['started']);
+
+	// Can restore topic?  That's if the topic is in the recycle board and has a previous restore state.
+	$context['can_restore_topic'] &= !empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] == $board && !empty($topicinfo['id_previous_board']);
+	$context['can_restore_msg'] &= !empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] == $board && !empty($topicinfo['id_previous_topic']);
+
+	// Wireless shows a "more" if you can do anything special.
+	if (WIRELESS && WIRELESS_PROTOCOL != 'wap')
+	{
+		$context['wireless_more'] = $context['can_sticky'] || $context['can_lock'] || allowedTo('modify_any');
+		$context['wireless_moderate'] = isset($_GET['moderate']) ? ';moderate' : '';
+	}
+
+	// Load up the "double post" sequencing magic.
+	if (!empty($options['display_quick_reply']))
+	{
+		checkSubmitOnce('register');
+		$context['name'] = isset($_SESSION['guest_name']) ? $_SESSION['guest_name'] : '';
+		$context['email'] = isset($_SESSION['guest_email']) ? $_SESSION['guest_email'] : '';
+	}
+}
+
+// Callback for the message display.
+function prepareDisplayContext($reset = false)
+{
+	global $settings, $txt, $modSettings, $scripturl, $options, $user_info, $smcFunc;
+	global $memberContext, $context, $messages_request, $topic, $attachments, $topicinfo;
+
+	static $counter = null;
+
+	// If the query returned false, bail.
+	if ($messages_request == false)
+		return false;
+
+	// Remember which message this is.  (ie. reply #83)
+	if ($counter === null || $reset)
+		$counter = empty($options['view_newest_first']) ? $context['start'] : $context['total_visible_posts'] - $context['start'];
+
+	// Start from the beginning...
+	if ($reset)
+		return @$smcFunc['db_data_seek']($messages_request, 0);
+
+	// Attempt to get the next message.
+	$message = $smcFunc['db_fetch_assoc']($messages_request);
+	if (!$message)
+	{
+		$smcFunc['db_free_result']($messages_request);
+		return false;
+	}
+
+	// $context['icon_sources'] says where each icon should come from - here we set up the ones which will always exist!
+	if (empty($context['icon_sources']))
+	{
+		$stable_icons = array('xx', 'thumbup', 'thumbdown', 'exclamation', 'question', 'lamp', 'smiley', 'angry', 'cheesy', 'grin', 'sad', 'wink', 'moved', 'recycled', 'wireless', 'clip');
+		$context['icon_sources'] = array();
+		foreach ($stable_icons as $icon)
+			$context['icon_sources'][$icon] = 'images_url';
+	}
+
+	// Message Icon Management... check the images exist.
+	if (empty($modSettings['messageIconChecks_disable']))
+	{
+		// If the current icon isn't known, then we need to do something...
+		if (!isset($context['icon_sources'][$message['icon']]))
+			$context['icon_sources'][$message['icon']] = file_exists($settings['theme_dir'] . '/images/post/' . $message['icon'] . '.gif') ? 'images_url' : 'default_images_url';
+	}
+	elseif (!isset($context['icon_sources'][$message['icon']]))
+		$context['icon_sources'][$message['icon']] = 'images_url';
+
+	// If you're a lazy bum, you probably didn't give a subject...
+	$message['subject'] = $message['subject'] != '' ? $message['subject'] : $txt['no_subject'];
+
+	// Are you allowed to remove at least a single reply?
+	$context['can_remove_post'] |= allowedTo('delete_own') && (empty($modSettings['edit_disable_time']) || $message['poster_time'] + $modSettings['edit_disable_time'] * 60 >= time()) && $message['id_member'] == $user_info['id'];
+
+	// If it couldn't load, or the user was a guest.... someday may be done with a guest table.
+	if (!loadMemberContext($message['id_member'], true))
+	{
+		// Notice this information isn't used anywhere else....
+		$memberContext[$message['id_member']]['name'] = $message['poster_name'];
+		$memberContext[$message['id_member']]['id'] = 0;
+		$memberContext[$message['id_member']]['group'] = $txt['guest_title'];
+		$memberContext[$message['id_member']]['link'] = $message['poster_name'];
+		$memberContext[$message['id_member']]['email'] = $message['poster_email'];
+		$memberContext[$message['id_member']]['show_email'] = showEmailAddress(true, 0);
+		$memberContext[$message['id_member']]['is_guest'] = true;
+	}
+	else
+	{
+		$memberContext[$message['id_member']]['can_view_profile'] = allowedTo('profile_view_any') || ($message['id_member'] == $user_info['id'] && allowedTo('profile_view_own'));
+		$memberContext[$message['id_member']]['is_topic_starter'] = $message['id_member'] == $context['topic_starter_id'];
+		$memberContext[$message['id_member']]['can_see_warning'] = !isset($context['disabled_fields']['warning_status']) && $memberContext[$message['id_member']]['warning_status'] && ($context['user']['can_mod'] || (!$user_info['is_guest'] && !empty($modSettings['warning_show']) && ($modSettings['warning_show'] > 1 || $message['id_member'] == $user_info['id'])));
+	}
+
+	$memberContext[$message['id_member']]['ip'] = $message['poster_ip'];
+
+	// Do the censor thang.
+	censorText($message['body']);
+	censorText($message['subject']);
+
+	// Run BBC interpreter on the message.
+	$message['body'] = parse_bbc($message['body'], $message['smileys_enabled'], $message['id_msg']);
+
+	// Compose the memory eat- I mean message array.
+	$output = array(
+		'attachment' => loadAttachmentContext($message['id_msg']),
+		'alternate' => $counter % 2,
+		'id' => $message['id_msg'],
+		'href' => $scripturl . '?topic=' . $topic . '.msg' . $message['id_msg'] . '#msg' . $message['id_msg'],
+		'link' => '<a href="' . $scripturl . '?topic=' . $topic . '.msg' . $message['id_msg'] . '#msg' . $message['id_msg'] . '" rel="nofollow">' . $message['subject'] . '</a>',
+		'member' => &$memberContext[$message['id_member']],
+		'icon' => $message['icon'],
+		'icon_url' => $settings[$context['icon_sources'][$message['icon']]] . '/post/' . $message['icon'] . '.gif',
+		'subject' => $message['subject'],
+		'time' => timeformat($message['poster_time']),
+		'timestamp' => forum_time(true, $message['poster_time']),
+		'counter' => $counter,
+		'modified' => array(
+			'time' => timeformat($message['modified_time']),
+			'timestamp' => forum_time(true, $message['modified_time']),
+			'name' => $message['modified_name']
+		),
+		'body' => $message['body'],
+		'new' => empty($message['is_read']),
+		'approved' => $message['approved'],
+		'first_new' => isset($context['start_from']) && $context['start_from'] == $counter,
+		'is_ignored' => !empty($modSettings['enable_buddylist']) && !empty($options['posts_apply_ignore_list']) && in_array($message['id_member'], $context['user']['ignoreusers']),
+		'can_approve' => !$message['approved'] && $context['can_approve'],
+		'can_unapprove' => $message['approved'] && $context['can_approve'],
+		'can_modify' => (!$context['is_locked'] || allowedTo('moderate_board')) && (allowedTo('modify_any') || (allowedTo('modify_replies') && $context['user']['started']) || (allowedTo('modify_own') && $message['id_member'] == $user_info['id'] && (empty($modSettings['edit_disable_time']) || !$message['approved'] || $message['poster_time'] + $modSettings['edit_disable_time'] * 60 > time()))),
+		'can_remove' => allowedTo('delete_any') || (allowedTo('delete_replies') && $context['user']['started']) || (allowedTo('delete_own') && $message['id_member'] == $user_info['id'] && (empty($modSettings['edit_disable_time']) || $message['poster_time'] + $modSettings['edit_disable_time'] * 60 > time())),
+		'can_see_ip' => allowedTo('moderate_forum') || ($message['id_member'] == $user_info['id'] && !empty($user_info['id'])),
+	);
+
+	// Is this user the message author?
+	$output['is_message_author'] = $message['id_member'] == $user_info['id'];
+
+	if (empty($options['view_newest_first']))
+		$counter++;
+	else
+		$counter--;
+
+	return $output;
+}
+
+// Download an attachment.
+function Download()
+{
+	global $txt, $modSettings, $user_info, $scripturl, $context, $sourcedir, $topic, $smcFunc;
+
+	// Some defaults that we need.
+	$context['character_set'] = empty($modSettings['global_character_set']) ? (empty($txt['lang_character_set']) ? 'ISO-8859-1' : $txt['lang_character_set']) : $modSettings['global_character_set'];
+	$context['utf8'] = $context['character_set'] === 'UTF-8' && (strpos(strtolower(PHP_OS), 'win') === false || @version_compare(PHP_VERSION, '4.2.3') != -1);
+	$context['no_last_modified'] = true;
+
+	// Make sure some attachment was requested!
+	if (!isset($_REQUEST['attach']) && !isset($_REQUEST['id']))
+		fatal_lang_error('no_access', false);
+
+	$_REQUEST['attach'] = isset($_REQUEST['attach']) ? (int) $_REQUEST['attach'] : (int) $_REQUEST['id'];
+
+	if (isset($_REQUEST['type']) && $_REQUEST['type'] == 'avatar')
+	{
+		$request = $smcFunc['db_query']('', '
+			SELECT id_folder, filename, file_hash, fileext, id_attach, attachment_type, mime_type, approved, id_member
+			FROM {db_prefix}attachments
+			WHERE id_attach = {int:id_attach}
+				AND id_member > {int:blank_id_member}
+			LIMIT 1',
+			array(
+				'id_attach' => $_REQUEST['attach'],
+				'blank_id_member' => 0,
+			)
+		);
+		$_REQUEST['image'] = true;
+	}
+	// This is just a regular attachment...
+	else
+	{
+		// This checks only the current board for $board/$topic's permissions.
+		isAllowedTo('view_attachments');
+
+		// Make sure this attachment is on this board.
+		// NOTE: We must verify that $topic is the attachment's topic, or else the permission check above is broken.
+		$request = $smcFunc['db_query']('', '
+			SELECT a.id_folder, a.filename, a.file_hash, a.fileext, a.id_attach, a.attachment_type, a.mime_type, a.approved, m.id_member
+			FROM {db_prefix}attachments AS a
+				INNER JOIN {db_prefix}messages AS m ON (m.id_msg = a.id_msg AND m.id_topic = {int:current_topic})
+				INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board AND {query_see_board})
+			WHERE a.id_attach = {int:attach}
+			LIMIT 1',
+			array(
+				'attach' => $_REQUEST['attach'],
+				'current_topic' => $topic,
+			)
+		);
+	}
+	if ($smcFunc['db_num_rows']($request) == 0)
+		fatal_lang_error('no_access', false);
+	list ($id_folder, $real_filename, $file_hash, $file_ext, $id_attach, $attachment_type, $mime_type, $is_approved, $id_member) = $smcFunc['db_fetch_row']($request);
+	$smcFunc['db_free_result']($request);
+
+	// If it isn't yet approved, do they have permission to view it?
+	if (!$is_approved && ($id_member == 0 || $user_info['id'] != $id_member) && ($attachment_type == 0 || $attachment_type == 3))
+		isAllowedTo('approve_posts');
+
+	// Update the download counter (unless it's a thumbnail).
+	if ($attachment_type != 3)
+		$smcFunc['db_query']('attach_download_increase', '
+			UPDATE LOW_PRIORITY {db_prefix}attachments
+			SET downloads = downloads + 1
+			WHERE id_attach = {int:id_attach}',
+			array(
+				'id_attach' => $id_attach,
+			)
+		);
+
+	$filename = getAttachmentFilename($real_filename, $_REQUEST['attach'], $id_folder, false, $file_hash);
+
+	// This is done to clear any output that was made before now. (would use ob_clean(), but that's PHP 4.2.0+...)
+	ob_end_clean();
+	if (!empty($modSettings['enableCompressedOutput']) && @version_compare(PHP_VERSION, '4.2.0') >= 0 && @filesize($filename) <= 4194304 && in_array($file_ext, array('txt', 'html', 'htm', 'js', 'doc', 'pdf', 'docx', 'rtf', 'css', 'php', 'log', 'xml', 'sql', 'c', 'java')))
+		@ob_start('ob_gzhandler');
+	else
+	{
+		ob_start();
+		header('Content-Encoding: none');
+	}
+
+	// No point in a nicer message, because this is supposed to be an attachment anyway...
+	if (!file_exists($filename))
+	{
+		loadLanguage('Errors');
+
+		header('HTTP/1.0 404 ' . $txt['attachment_not_found']);
+		header('Content-Type: text/plain; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
+
+		// We need to die like this *before* we send any anti-caching headers as below.
+		die('404 - ' . $txt['attachment_not_found']);
+	}
+
+	// If it hasn't been modified since the last time this attachement was retrieved, there's no need to display it again.
+	if (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE']))
+	{
+		list($modified_since) = explode(';', $_SERVER['HTTP_IF_MODIFIED_SINCE']);
+		if (strtotime($modified_since) >= filemtime($filename))
+		{
+			ob_end_clean();
+
+			// Answer the question - no, it hasn't been modified ;).
+			header('HTTP/1.1 304 Not Modified');
+			exit;
+		}
+	}
+
+	// Check whether the ETag was sent back, and cache based on that...
+	$eTag = '"' . substr($_REQUEST['attach'] . $real_filename . filemtime($filename), 0, 64) . '"';
+	if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) && strpos($_SERVER['HTTP_IF_NONE_MATCH'], $eTag) !== false)
+	{
+		ob_end_clean();
+
+		header('HTTP/1.1 304 Not Modified');
+		exit;
+	}
+
+	// Send the attachment headers.
+	header('Pragma: ');
+	if (!$context['browser']['is_gecko'])
+		header('Content-Transfer-Encoding: binary');
+	header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 525600 * 60) . ' GMT');
+	header('Last-Modified: ' . gmdate('D, d M Y H:i:s', filemtime($filename)) . ' GMT');
+	header('Accept-Ranges: bytes');
+	header('Connection: close');
+	header('ETag: ' . $eTag);
+
+	// IE 6 just doesn't play nice. As dirty as this seems, it works.
+	if ($context['browser']['is_ie6'] && isset($_REQUEST['image']))
+		unset($_REQUEST['image']);
+
+	// Make sure the mime type warrants an inline display.
+	elseif (isset($_REQUEST['image']) && !empty($mime_type) && strpos($mime_type, 'image/') !== 0)
+		unset($_REQUEST['image']);
+
+	// Does this have a mime type?
+	elseif (!empty($mime_type) && (isset($_REQUEST['image']) || !in_array($file_ext, array('jpg', 'gif', 'jpeg', 'x-ms-bmp', 'png', 'psd', 'tiff', 'iff'))))
+		header('Content-Type: ' . strtr($mime_type, array('image/bmp' => 'image/x-ms-bmp')));
+
+	else
+	{
+		header('Content-Type: ' . ($context['browser']['is_ie'] || $context['browser']['is_opera'] ? 'application/octetstream' : 'application/octet-stream'));
+		if (isset($_REQUEST['image']))
+			unset($_REQUEST['image']);
+	}
+
+	// Convert the file to UTF-8, cuz most browsers dig that.
+	$utf8name = !$context['utf8'] && function_exists('iconv') ? iconv($context['character_set'], 'UTF-8', $real_filename) : (!$context['utf8'] && function_exists('mb_convert_encoding') ? mb_convert_encoding($real_filename, 'UTF-8', $context['character_set']) : $real_filename);
+	$fixchar = create_function('$n', '
+		if ($n < 32)
+			return \'\';
+		elseif ($n < 128)
+			return chr($n);
+		elseif ($n < 2048)
+			return chr(192 | $n >> 6) . chr(128 | $n & 63);
+		elseif ($n < 65536)
+			return chr(224 | $n >> 12) . chr(128 | $n >> 6 & 63) . chr(128 | $n & 63);
+		else
+			return chr(240 | $n >> 18) . chr(128 | $n >> 12 & 63) . chr(128 | $n >> 6 & 63) . chr(128 | $n & 63);');
+
+	$disposition = !isset($_REQUEST['image']) ? 'attachment' : 'inline';
+
+	// Different browsers like different standards...
+	if ($context['browser']['is_firefox'])
+		header('Content-Disposition: ' . $disposition . '; filename*="UTF-8\'\'' . preg_replace('~&#(\d{3,8});~e', '$fixchar(\'$1\')', $utf8name) . '"');
+
+	elseif ($context['browser']['is_opera'])
+		header('Content-Disposition: ' . $disposition . '; filename="' . preg_replace('~&#(\d{3,8});~e', '$fixchar(\'$1\')', $utf8name) . '"');
+
+	elseif ($context['browser']['is_ie'])
+		header('Content-Disposition: ' . $disposition . '; filename="' . urlencode(preg_replace('~&#(\d{3,8});~e', '$fixchar(\'$1\')', $utf8name)) . '"');
+
+	else
+		header('Content-Disposition: ' . $disposition . '; filename="' . $utf8name . '"');
+
+	// If this has an "image extension" - but isn't actually an image - then ensure it isn't cached cause of silly IE.
+	if (!isset($_REQUEST['image']) && in_array($file_ext, array('gif', 'jpg', 'bmp', 'png', 'jpeg', 'tiff')))
+		header('Cache-Control: no-cache');
+	else
+		header('Cache-Control: max-age=' . (525600 * 60) . ', private');
+
+	if (empty($modSettings['enableCompressedOutput']) || filesize($filename) > 4194304)
+		header('Content-Length: ' . filesize($filename));
+
+	// Try to buy some time...
+	@set_time_limit(600);
+
+	// Recode line endings for text files, if enabled.
+	if (!empty($modSettings['attachmentRecodeLineEndings']) && !isset($_REQUEST['image']) && in_array($file_ext, array('txt', 'css', 'htm', 'html', 'php', 'xml')))
+	{
+		if (strpos($_SERVER['HTTP_USER_AGENT'], 'Windows') !== false)
+			$callback = create_function('$buffer', 'return preg_replace(\'~[\r]?\n~\', "\r\n", $buffer);');
+		elseif (strpos($_SERVER['HTTP_USER_AGENT'], 'Mac') !== false)
+			$callback = create_function('$buffer', 'return preg_replace(\'~[\r]?\n~\', "\r", $buffer);');
+		else
+			$callback = create_function('$buffer', 'return preg_replace(\'~[\r]?\n~\', "\n", $buffer);');
+	}
+
+	// Since we don't do output compression for files this large...
+	if (filesize($filename) > 4194304)
+	{
+		// Forcibly end any output buffering going on.
+		if (function_exists('ob_get_level'))
+		{
+			while (@ob_get_level() > 0)
+				@ob_end_clean();
+		}
+		else
+		{
+			@ob_end_clean();
+			@ob_end_clean();
+			@ob_end_clean();
+		}
+
+		$fp = fopen($filename, 'rb');
+		while (!feof($fp))
+		{
+			if (isset($callback))
+				echo $callback(fread($fp, 8192));
+			else
+				echo fread($fp, 8192);
+			flush();
+		}
+		fclose($fp);
+	}
+	// On some of the less-bright hosts, readfile() is disabled.  It's just a faster, more byte safe, version of what's in the if.
+	elseif (isset($callback) || @readfile($filename) == null)
+		echo isset($callback) ? $callback(file_get_contents($filename)) : file_get_contents($filename);
+
+	obExit(false);
+}
+
+function loadAttachmentContext($id_msg)
+{
+	global $attachments, $modSettings, $txt, $scripturl, $topic, $sourcedir, $smcFunc;
+
+	// Set up the attachment info - based on code by Meriadoc.
+	$attachmentData = array();
+	$have_unapproved = false;
+	if (isset($attachments[$id_msg]) && !empty($modSettings['attachmentEnable']))
+	{
+		foreach ($attachments[$id_msg] as $i => $attachment)
+		{
+			$attachmentData[$i] = array(
+				'id' => $attachment['id_attach'],
+				'name' => preg_replace('~&amp;#(\\d{1,7}|x[0-9a-fA-F]{1,6});~', '&#\\1;', htmlspecialchars($attachment['filename'])),
+				'downloads' => $attachment['downloads'],
+				'size' => round($attachment['filesize'] / 1024, 2) . ' ' . $txt['kilobyte'],
+				'byte_size' => $attachment['filesize'],
+				'href' => $scripturl . '?action=dlattach;topic=' . $topic . '.0;attach=' . $attachment['id_attach'],
+				'link' => '<a href="' . $scripturl . '?action=dlattach;topic=' . $topic . '.0;attach=' . $attachment['id_attach'] . '">' . htmlspecialchars($attachment['filename']) . '</a>',
+				'is_image' => !empty($attachment['width']) && !empty($attachment['height']) && !empty($modSettings['attachmentShowImages']),
+				'is_approved' => $attachment['approved'],
+			);
+
+			// If something is unapproved we'll note it so we can sort them.
+			if (!$attachment['approved'])
+				$have_unapproved = true;
+
+			if (!$attachmentData[$i]['is_image'])
+				continue;
+
+			$attachmentData[$i]['real_width'] = $attachment['width'];
+			$attachmentData[$i]['width'] = $attachment['width'];
+			$attachmentData[$i]['real_height'] = $attachment['height'];
+			$attachmentData[$i]['height'] = $attachment['height'];
+
+			// Let's see, do we want thumbs?
+			if (!empty($modSettings['attachmentThumbnails']) && !empty($modSettings['attachmentThumbWidth']) && !empty($modSettings['attachmentThumbHeight']) && ($attachment['width'] > $modSettings['attachmentThumbWidth'] || $attachment['height'] > $modSettings['attachmentThumbHeight']) && strlen($attachment['filename']) < 249)
+			{
+				// A proper thumb doesn't exist yet? Create one!
+				if (empty($attachment['id_thumb']) || $attachment['thumb_width'] > $modSettings['attachmentThumbWidth'] || $attachment['thumb_height'] > $modSettings['attachmentThumbHeight'] || ($attachment['thumb_width'] < $modSettings['attachmentThumbWidth'] && $attachment['thumb_height'] < $modSettings['attachmentThumbHeight']))
+				{
+					$filename = getAttachmentFilename($attachment['filename'], $attachment['id_attach'], $attachment['id_folder']);
+
+					require_once($sourcedir . '/Subs-Graphics.php');
+					if (createThumbnail($filename, $modSettings['attachmentThumbWidth'], $modSettings['attachmentThumbHeight']))
+					{
+						// So what folder are we putting this image in?
+						if (!empty($modSettings['currentAttachmentUploadDir']))
+						{
+							if (!is_array($modSettings['attachmentUploadDir']))
+								$modSettings['attachmentUploadDir'] = @unserialize($modSettings['attachmentUploadDir']);
+							$path = $modSettings['attachmentUploadDir'][$modSettings['currentAttachmentUploadDir']];
+							$id_folder_thumb = $modSettings['currentAttachmentUploadDir'];
+						}
+						else
+						{
+							$path = $modSettings['attachmentUploadDir'];
+							$id_folder_thumb = 1;
+						}
+
+						// Calculate the size of the created thumbnail.
+						$size = @getimagesize($filename . '_thumb');
+						list ($attachment['thumb_width'], $attachment['thumb_height']) = $size;
+						$thumb_size = filesize($filename . '_thumb');
+
+						// These are the only valid image types for SMF.
+						$validImageTypes = array(1 => 'gif', 2 => 'jpeg', 3 => 'png', 5 => 'psd', 6 => 'bmp', 7 => 'tiff', 8 => 'tiff', 9 => 'jpeg', 14 => 'iff');
+
+						// What about the extension?
+						$thumb_ext = isset($validImageTypes[$size[2]]) ? $validImageTypes[$size[2]] : '';
+
+						// Figure out the mime type.
+						if (!empty($size['mime']))
+							$thumb_mime = $size['mime'];
+						else
+							$thumb_mime = 'image/' . $thumb_ext;
+
+						$thumb_filename = $attachment['filename'] . '_thumb';
+						$thumb_hash = getAttachmentFilename($thumb_filename, false, null, true);
+
+						// Add this beauty to the database.
+						$smcFunc['db_insert']('',
+							'{db_prefix}attachments',
+							array('id_folder' => 'int', 'id_msg' => 'int', 'attachment_type' => 'int', 'filename' => 'string', 'file_hash' => 'string', 'size' => 'int', 'width' => 'int', 'height' => 'int', 'fileext' => 'string', 'mime_type' => 'string'),
+							array($id_folder_thumb, $id_msg, 3, $thumb_filename, $thumb_hash, (int) $thumb_size, (int) $attachment['thumb_width'], (int) $attachment['thumb_height'], $thumb_ext, $thumb_mime),
+							array('id_attach')
+						);
+						$old_id_thumb = $attachment['id_thumb'];
+						$attachment['id_thumb'] = $smcFunc['db_insert_id']('{db_prefix}attachments', 'id_attach');
+						if (!empty($attachment['id_thumb']))
+						{
+							$smcFunc['db_query']('', '
+								UPDATE {db_prefix}attachments
+								SET id_thumb = {int:id_thumb}
+								WHERE id_attach = {int:id_attach}',
+								array(
+									'id_thumb' => $attachment['id_thumb'],
+									'id_attach' => $attachment['id_attach'],
+								)
+							);
+
+							$thumb_realname = getAttachmentFilename($thumb_filename, $attachment['id_thumb'], $id_folder_thumb, false, $thumb_hash);
+							rename($filename . '_thumb', $thumb_realname);
+
+							// Do we need to remove an old thumbnail?
+							if (!empty($old_id_thumb))
+							{
+								require_once($sourcedir . '/ManageAttachments.php');
+								removeAttachments(array('id_attach' => $old_id_thumb), '', false, false);
+							}
+						}
+					}
+				}
+
+				// Only adjust dimensions on successful thumbnail creation.
+				if (!empty($attachment['thumb_width']) && !empty($attachment['thumb_height']))
+				{
+					$attachmentData[$i]['width'] = $attachment['thumb_width'];
+					$attachmentData[$i]['height'] = $attachment['thumb_height'];
+				}
+			}
+
+			if (!empty($attachment['id_thumb']))
+				$attachmentData[$i]['thumbnail'] = array(
+					'id' => $attachment['id_thumb'],
+					'href' => $scripturl . '?action=dlattach;topic=' . $topic . '.0;attach=' . $attachment['id_thumb'] . ';image',
+				);
+			$attachmentData[$i]['thumbnail']['has_thumb'] = !empty($attachment['id_thumb']);
+
+			// If thumbnails are disabled, check the maximum size of the image.
+			if (!$attachmentData[$i]['thumbnail']['has_thumb'] && ((!empty($modSettings['max_image_width']) && $attachment['width'] > $modSettings['max_image_width']) || (!empty($modSettings['max_image_height']) && $attachment['height'] > $modSettings['max_image_height'])))
+			{
+				if (!empty($modSettings['max_image_width']) && (empty($modSettings['max_image_height']) || $attachment['height'] * $modSettings['max_image_width'] / $attachment['width'] <= $modSettings['max_image_height']))
+				{
+					$attachmentData[$i]['width'] = $modSettings['max_image_width'];
+					$attachmentData[$i]['height'] = floor($attachment['height'] * $modSettings['max_image_width'] / $attachment['width']);
+				}
+				elseif (!empty($modSettings['max_image_width']))
+				{
+					$attachmentData[$i]['width'] = floor($attachment['width'] * $modSettings['max_image_height'] / $attachment['height']);
+					$attachmentData[$i]['height'] = $modSettings['max_image_height'];
+				}
+			}
+			elseif ($attachmentData[$i]['thumbnail']['has_thumb'])
+			{
+				// If the image is too large to show inline, make it a popup.
+				if (((!empty($modSettings['max_image_width']) && $attachmentData[$i]['real_width'] > $modSettings['max_image_width']) || (!empty($modSettings['max_image_height']) && $attachmentData[$i]['real_height'] > $modSettings['max_image_height'])))
+					$attachmentData[$i]['thumbnail']['javascript'] = 'return reqWin(\'' . $attachmentData[$i]['href'] . ';image\', ' . ($attachment['width'] + 20) . ', ' . ($attachment['height'] + 20) . ', true);';
+				else
+					$attachmentData[$i]['thumbnail']['javascript'] = 'return expandThumb(' . $attachment['id_attach'] . ');';
+			}
+
+			if (!$attachmentData[$i]['thumbnail']['has_thumb'])
+				$attachmentData[$i]['downloads']++;
+		}
+	}
+
+	// Do we need to instigate a sort?
+	if ($have_unapproved)
+		usort($attachmentData, 'approved_attach_sort');
+
+	return $attachmentData;
+}
+
+// A sort function for putting unapproved attachments first.
+function approved_attach_sort($a, $b)
+{
+	if ($a['is_approved'] == $b['is_approved'])
+		return 0;
+
+	return $a['is_approved'] > $b['is_approved'] ? -1 : 1;
+}
+
+// In-topic quick moderation.
+function QuickInTopicModeration()
+{
+	global $sourcedir, $topic, $board, $user_info, $smcFunc, $modSettings, $context;
+
+	// Check the session = get or post.
+	checkSession('request');
+
+	require_once($sourcedir . '/RemoveTopic.php');
+
+	if (empty($_REQUEST['msgs']))
+		redirectexit('topic=' . $topic . '.' . $_REQUEST['start']);
+
+	$messages = array();
+	foreach ($_REQUEST['msgs'] as $dummy)
+		$messages[] = (int) $dummy;
+
+	// We are restoring messages. We handle this in another place.
+	if (isset($_REQUEST['restore_selected']))
+		redirectexit('action=restoretopic;msgs=' . implode(',', $messages) . ';' . $context['session_var'] . '=' . $context['session_id']);
+
+	// Allowed to delete any message?
+	if (allowedTo('delete_any'))
+		$allowed_all = true;
+	// Allowed to delete replies to their messages?
+	elseif (allowedTo('delete_replies'))
+	{
+		$request = $smcFunc['db_query']('', '
+			SELECT id_member_started
+			FROM {db_prefix}topics
+			WHERE id_topic = {int:current_topic}
+			LIMIT 1',
+			array(
+				'current_topic' => $topic,
+			)
+		);
+		list ($starter) = $smcFunc['db_fetch_row']($request);
+		$smcFunc['db_free_result']($request);
+
+		$allowed_all = $starter == $user_info['id'];
+	}
+	else
+		$allowed_all = false;
+
+	// Make sure they're allowed to delete their own messages, if not any.
+	if (!$allowed_all)
+		isAllowedTo('delete_own');
+
+	// Allowed to remove which messages?
+	$request = $smcFunc['db_query']('', '
+		SELECT id_msg, subject, id_member, poster_time
+		FROM {db_prefix}messages
+		WHERE id_msg IN ({array_int:message_list})
+			AND id_topic = {int:current_topic}' . (!$allowed_all ? '
+			AND id_member = {int:current_member}' : '') . '
+		LIMIT ' . count($messages),
+		array(
+			'current_member' => $user_info['id'],
+			'current_topic' => $topic,
+			'message_list' => $messages,
+		)
+	);
+	$messages = array();
+	while ($row = $smcFunc['db_fetch_assoc']($request))
+	{
+		if (!$allowed_all && !empty($modSettings['edit_disable_time']) && $row['poster_time'] + $modSettings['edit_disable_time'] * 60 < time())
+			continue;
+
+		$messages[$row['id_msg']] = array($row['subject'], $row['id_member']);
+	}
+	$smcFunc['db_free_result']($request);
+
+	// Get the first message in the topic - because you can't delete that!
+	$request = $smcFunc['db_query']('', '
+		SELECT id_first_msg, id_last_msg
+		FROM {db_prefix}topics
+		WHERE id_topic = {int:current_topic}
+		LIMIT 1',
+		array(
+			'current_topic' => $topic,
+		)
+	);
+	list ($first_message, $last_message) = $smcFunc['db_fetch_row']($request);
+	$smcFunc['db_free_result']($request);
+
+	// Delete all the messages we know they can delete. ($messages)
+	foreach ($messages as $message => $info)
+	{
+		// Just skip the first message - if it's not the last.
+		if ($message == $first_message && $message != $last_message)
+			continue;
+		// If the first message is going then don't bother going back to the topic as we're effectively deleting it.
+		elseif ($message == $first_message)
+			$topicGone = true;
+
+		removeMessage($message);
+
+		// Log this moderation action ;).
+		if (allowedTo('delete_any') && (!allowedTo('delete_own') || $info[1] != $user_info['id']))
+			logAction('delete', array('topic' => $topic, 'subject' => $info[0], 'member' => $info[1], 'board' => $board));
+	}
+
+	redirectexit(!empty($topicGone) ? 'board=' . $board : 'topic=' . $topic . '.' . $_REQUEST['start']);
+}
+
+?>

+ 182 - 0
Sources/DumpDatabase.php

@@ -0,0 +1,182 @@
+<?php
+
+/**
+ * Simple Machines Forum (SMF)
+ *
+ * @package SMF
+ * @author Simple Machines http://www.simplemachines.org
+ * @copyright 2011 Simple Machines
+ * @license http://www.simplemachines.org/about/smf/license.php BSD
+ *
+ * @version 2.0
+ */
+
+if (!defined('SMF'))
+	die('Hacking attempt...');
+
+/*
+	This file has a single job - database backup.
+
+	void DumpDatabase2()
+		- writes all of the database to standard output.
+		- uses gzip compression if compress is set in the URL/post data.
+		- may possibly time out in some cases.
+		- the data dumped depends on whether "struct" and "data" are passed.
+		- requires an administrator and the session hash by post.
+		- is called from ManageMaintenance.php.
+
+*/
+
+// Dumps the database to a file.
+function DumpDatabase2()
+{
+	global $db_name, $scripturl, $context, $modSettings, $crlf, $smcFunc, $db_prefix;
+
+	// Administrators only!
+	if (!allowedTo('admin_forum'))
+		fatal_lang_error('no_dump_database', 'critical');
+
+	// You can't dump nothing!
+	if (!isset($_REQUEST['struct']) && !isset($_REQUEST['data']))
+		$_REQUEST['data'] = true;
+
+	checkSession('post');
+
+	// We will need this, badly!
+	db_extend();
+
+	// Attempt to stop from dying...
+	@set_time_limit(600);
+	if (@ini_get('memory_limit') < 256)
+		@ini_set('memory_limit', '256M');
+
+	// Start saving the output... (don't do it otherwise for memory reasons.)
+	if (isset($_REQUEST['compress']) && function_exists('gzencode'))
+	{
+		// Make sure we're gzipping output, but then say we're not in the header ^_^.
+		if (empty($modSettings['enableCompressedOutput']))
+			@ob_start('ob_gzhandler');
+		// Try to clean any data already outputted.
+		elseif (ob_get_length() != 0)
+		{
+			ob_end_clean();
+			@ob_start('ob_gzhandler');
+		}
+
+		// Send faked headers so it will just save the compressed output as a gzip.
+		header('Content-Type: application/x-gzip');
+		header('Accept-Ranges: bytes');
+		header('Content-Encoding: none');
+
+		// Gecko browsers... don't like this. (Mozilla, Firefox, etc.)
+		if (!$context['browser']['is_gecko'])
+			header('Content-Transfer-Encoding: binary');
+
+		// The file extension will include .gz...
+		$extension = '.sql.gz';
+	}
+	else
+	{
+		// Get rid of the gzipping alreading being done.
+		if (!empty($modSettings['enableCompressedOutput']))
+			@ob_end_clean();
+		// If we can, clean anything already sent from the output buffer...
+		elseif (function_exists('ob_clean') && ob_get_length() != 0)
+			ob_clean();
+
+		// Tell the client to save this file, even though it's text.
+		header('Content-Type: ' . ($context['browser']['is_ie'] || $context['browser']['is_opera'] ? 'application/octetstream' : 'application/octet-stream'));
+		header('Content-Encoding: none');
+
+		// This time the extension should just be .sql.
+		$extension = '.sql';
+	}
+
+	// This should turn off the session URL parser.
+	$scripturl = '';
+
+	// If this database is flat file and has a handler function pass it to that.
+	if (!empty($smcFunc['db_get_backup']))
+	{
+		$smcFunc['db_get_backup']();
+		exit;
+	}
+
+	// Send the proper headers to let them download this file.
+	header('Content-Disposition: filename="' . $db_name . '-' . (empty($_REQUEST['struct']) ? 'data' : (empty($_REQUEST['data']) ? 'structure' : 'complete')) . '_' . strftime('%Y-%m-%d') . $extension . '"');
+	header('Cache-Control: private');
+	header('Connection: close');
+
+	// This makes things simpler when using it so very very often.
+	$crlf = "\r\n";
+
+	// SQL Dump Header.
+	echo
+		'-- ==========================================================', $crlf,
+		'--', $crlf,
+		'-- Database dump of tables in `', $db_name, '`', $crlf,
+		'-- ', timeformat(time(), false), $crlf,
+		'--', $crlf,
+		'-- ==========================================================', $crlf,
+		$crlf;
+
+	// Get all tables in the database....
+	if (preg_match('~^`(.+?)`\.(.+?)$~', $db_prefix, $match) != 0)
+	{
+		$db = strtr($match[1], array('`' => ''));
+		$dbp = str_replace('_', '\_', $match[2]);
+	}
+	else
+	{
+		$db = false;
+		$dbp = $db_prefix;
+	}
+
+	// Dump each table.
+	$tables = $smcFunc['db_list_tables'](false, $db_prefix . '%');
+	foreach ($tables as $tableName)
+	{
+		if (function_exists('apache_reset_timeout'))
+			@apache_reset_timeout();
+
+		// Are we dumping the structures?
+		if (isset($_REQUEST['struct']))
+		{
+			echo
+				$crlf,
+				'--', $crlf,
+				'-- Table structure for table `', $tableName, '`', $crlf,
+				'--', $crlf,
+				$crlf,
+				$smcFunc['db_table_sql']($tableName), ';', $crlf;
+		}
+
+		// How about the data?
+		if (!isset($_REQUEST['data']) || substr($tableName, -10) == 'log_errors')
+			continue;
+
+		// Are there any rows in this table?
+		$get_rows = $smcFunc['db_insert_sql']($tableName);
+
+		// No rows to get - skip it.
+		if (empty($get_rows))
+			continue;
+
+		echo
+			$crlf,
+			'--', $crlf,
+			'-- Dumping data in `', $tableName, '`', $crlf,
+			'--', $crlf,
+			$crlf,
+			$get_rows,
+			'-- --------------------------------------------------------', $crlf;
+	}
+
+	echo
+		$crlf,
+		'-- Done', $crlf;
+
+	exit;
+}
+
+?>

+ 416 - 0
Sources/Errors.php

@@ -0,0 +1,416 @@
+<?php
+
+/**
+ * Simple Machines Forum (SMF)
+ *
+ * @package SMF
+ * @author Simple Machines http://www.simplemachines.org
+ * @copyright 2011 Simple Machines
+ * @license http://www.simplemachines.org/about/smf/license.php BSD
+ *
+ * @version 2.0
+ */
+
+if (!defined('SMF'))
+	die('Hacking attempt...');
+
+/*	The purpose of this file is... errors. (hard to guess, huh?)  It takes
+	care of logging, error messages, error handling, database errors, and
+	error log administration.  It does this with:
+
+	bool db_fatal_error(bool loadavg = false)
+		- calls show_db_error().
+		- this is used for database connection error handling.
+		- loadavg means this is a load average problem, not a database error.
+
+	string log_error(string error_message, string error_type = general,
+			string filename = none, int line = none)
+		- logs an error, if error logging is enabled.
+		- depends on the enableErrorLogging setting.
+		- filename and line should be __FILE__ and __LINE__, respectively.
+		- returns the error message. (ie. die(log_error($msg));)
+
+	void fatal_error(string error_message, mixed (bool or string) log = general)
+		- stops execution and displays an error message.
+		- logs the error message if log is missing or true.
+
+	void fatal_lang_error(string error_message_key, mixed (bool or string) log = general,
+			array sprintf = array())
+		- stops execution and displays an error message by key.
+		- uses the string with the error_message_key key.
+		- loads the Errors language file.
+		- applies the sprintf information if specified.
+		- the information is logged if log is true or missing.
+		- logs the error in the forum's default language while displaying the error
+		  message in the user's language
+
+	void error_handler(int error_level, string error_string, string filename,
+			int line)
+		- this is a standard PHP error handler replacement.
+		- dies with fatal_error() if the error_level matches with
+		  error_reporting.
+
+	void setup_fatal_error_context(string error_message)
+		- uses the fatal_error sub template of the Errors template - or the
+		  error sub template in the Wireless template.
+		- used by fatal_error() and fatal_lang_error()
+
+	void show_db_error(bool loadavg = false)
+		- called by db_fatal_error() function
+		- shows a complete page independent of language files or themes.
+		- used only if there's no way to connect to the database or the
+		  load averages are too high to do so.
+		- loadavg means this is a load average problem, not a database error.
+		- stops further execution of the script.
+*/
+
+// Handle fatal errors - like connection errors or load average problems
+function db_fatal_error($loadavg = false)
+{
+	global $sourcedir;
+
+	show_db_error($loadavg);
+
+	// Since we use "or db_fatal_error();" this is needed...
+	return false;
+}
+
+// Log an error, if the option is on.
+function log_error($error_message, $error_type = 'general', $file = null, $line = null)
+{
+	global $txt, $modSettings, $sc, $user_info, $smcFunc, $scripturl, $last_error;
+
+	// Check if error logging is actually on.
+	if (empty($modSettings['enableErrorLogging']))
+		return $error_message;
+
+	// Basically, htmlspecialchars it minus &. (for entities!)
+	$error_message = strtr($error_message, array('<' => '&lt;', '>' => '&gt;', '"' => '&quot;'));
+	$error_message = strtr($error_message, array('&lt;br /&gt;' => '<br />', '&lt;b&gt;' => '<strong>', '&lt;/b&gt;' => '</strong>', "\n" => '<br />'));
+
+	// Add a file and line to the error message?
+	// Don't use the actual txt entries for file and line but instead use %1$s for file and %2$s for line
+	if ($file == null)
+		$file = '';
+	else
+		// Window style slashes don't play well, lets convert them to the unix style.
+		$file = str_replace('\\', '/', $file);
+
+	if ($line == null)
+		$line = 0;
+	else
+		$line = (int) $line;
+
+	// Just in case there's no id_member or IP set yet.
+	if (empty($user_info['id']))
+		$user_info['id'] = 0;
+	if (empty($user_info['ip']))
+		$user_info['ip'] = '';
+
+	// Find the best query string we can...
+	$query_string = empty($_SERVER['QUERY_STRING']) ? (empty($_SERVER['REQUEST_URL']) ? '' : str_replace($scripturl, '', $_SERVER['REQUEST_URL'])) : $_SERVER['QUERY_STRING'];
+
+	// Don't log the session hash in the url twice, it's a waste.
+	$query_string = htmlspecialchars((SMF == 'SSI' ? '' : '?') . preg_replace(array('~;sesc=[^&;]+~', '~' . session_name() . '=' . session_id() . '[&;]~'), array(';sesc', ''), $query_string));
+
+	// Just so we know what board error messages are from.
+	if (isset($_POST['board']) && !isset($_GET['board']))
+		$query_string .= ($query_string == '' ? 'board=' : ';board=') . $_POST['board'];
+
+	// What types of categories do we have?
+	$known_error_types = array(
+		'general',
+		'critical',
+		'database',
+		'undefined_vars',
+		'user',
+		'template',
+		'debug',
+	);
+
+	// Make sure the category that was specified is a valid one
+	$error_type = in_array($error_type, $known_error_types) && $error_type !== true ? $error_type : 'general';
+
+	// Don't log the same error countless times, as we can get in a cycle of depression...
+	$error_info = array($user_info['id'], time(), $user_info['ip'], $query_string, $error_message, (string) $sc, $error_type, $file, $line);
+	if (empty($last_error) || $last_error != $error_info)
+	{
+		// Insert the error into the database.
+		$smcFunc['db_insert']('',
+			'{db_prefix}log_errors',
+			array('id_member' => 'int', 'log_time' => 'int', 'ip' => 'string-16', 'url' => 'string-65534', 'message' => 'string-65534', 'session' => 'string', 'error_type' => 'string', 'file' => 'string-255', 'line' => 'int'),
+			$error_info,
+			array('id_error')
+		);
+		$last_error = $error_info;
+	}
+
+	// Return the message to make things simpler.
+	return $error_message;
+}
+
+// An irrecoverable error.
+function fatal_error($error, $log = 'general')
+{
+	global $txt, $context, $modSettings;
+
+	// We don't have $txt yet, but that's okay...
+	if (empty($txt))
+		die($error);
+
+	setup_fatal_error_context($log || (!empty($modSettings['enableErrorLogging']) && $modSettings['enableErrorLogging'] == 2) ? log_error($error, $log) : $error);
+}
+
+// A fatal error with a message stored in the language file.
+function fatal_lang_error($error, $log = 'general', $sprintf = array())
+{
+	global $txt, $language, $modSettings, $user_info, $context;
+	static $fatal_error_called = false;
+
+	// Try to load a theme if we don't have one.
+	if (empty($context['theme_loaded']) && empty($fatal_error_called))
+	{
+		$fatal_error_called = true;
+		loadTheme();
+	}
+
+	// If we have no theme stuff we can't have the language file...
+	if (empty($context['theme_loaded']))
+		die($error);
+
+	$reload_lang_file = true;
+	// Log the error in the forum's language, but don't waste the time if we aren't logging
+	if ($log || (!empty($modSettings['enableErrorLogging']) && $modSettings['enableErrorLogging'] == 2))
+	{
+		loadLanguage('Errors', $language);
+		$reload_lang_file = $language != $user_info['language'];
+		$error_message = empty($sprintf) ? $txt[$error] : vsprintf($txt[$error], $sprintf);
+		log_error($error_message, $log);
+	}
+
+	// Load the language file, only if it needs to be reloaded
+	if ($reload_lang_file)
+	{
+		loadLanguage('Errors');
+		$error_message = empty($sprintf) ? $txt[$error] : vsprintf($txt[$error], $sprintf);
+	}
+
+	setup_fatal_error_context($error_message);
+}
+
+// Handler for standard error messages.
+function error_handler($error_level, $error_string, $file, $line)
+{
+	global $settings, $modSettings, $db_show_debug;
+
+	// Ignore errors if we're ignoring them or they are strict notices from PHP 5 (which cannot be solved without breaking PHP 4.)
+	if (error_reporting() == 0 || (defined('E_STRICT') && $error_level == E_STRICT && (empty($modSettings['enableErrorLogging']) || $modSettings['enableErrorLogging'] != 2)))
+		return;
+
+	if (strpos($file, 'eval()') !== false && !empty($settings['current_include_filename']))
+	{
+		if (function_exists('debug_backtrace'))
+		{
+			$array = debug_backtrace();
+			for ($i = 0; $i < count($array); $i++)
+			{
+				if ($array[$i]['function'] != 'loadSubTemplate')
+					continue;
+
+				// This is a bug in PHP, with eval, it seems!
+				if (empty($array[$i]['args']))
+					$i++;
+				break;
+			}
+
+			if (isset($array[$i]) && !empty($array[$i]['args']))
+				$file = realpath($settings['current_include_filename']) . ' (' . $array[$i]['args'][0] . ' sub template - eval?)';
+			else
+				$file = realpath($settings['current_include_filename']) . ' (eval?)';
+		}
+		else
+			$file = realpath($settings['current_include_filename']) . ' (eval?)';
+	}
+
+	if (isset($db_show_debug) && $db_show_debug === true)
+	{
+		// Commonly, undefined indexes will occur inside attributes; try to show them anyway!
+		if ($error_level % 255 != E_ERROR)
+		{
+			$temporary = ob_get_contents();
+			if (substr($temporary, -2) == '="')
+				echo '"';
+		}
+
+		// Debugging!  This should look like a PHP error message.
+		echo '<br />
+<strong>', $error_level % 255 == E_ERROR ? 'Error' : ($error_level % 255 == E_WARNING ? 'Warning' : 'Notice'), '</strong>: ', $error_string, ' in <strong>', $file, '</strong> on line <strong>', $line, '</strong><br />';
+	}
+
+	$error_type = strpos(strtolower($error_string), 'undefined') !== false ? 'undefined_vars' : 'general';
+
+	$message = log_error($error_level . ': ' . $error_string, $error_type, $file, $line);
+
+	// Let's give integrations a chance to ouput a bit differently
+	call_integration_hook('integrate_output_error', array($message, $error_type, $error_level, $file, $line));
+
+	// Dying on these errors only causes MORE problems (blank pages!)
+	if ($file == 'Unknown')
+		return;
+
+	// If this is an E_ERROR or E_USER_ERROR.... die.  Violently so.
+	if ($error_level % 255 == E_ERROR)
+		obExit(false);
+	else
+		return;
+
+	// If this is an E_ERROR, E_USER_ERROR, E_WARNING, or E_USER_WARNING.... die.  Violently so.
+	if ($error_level % 255 == E_ERROR || $error_level % 255 == E_WARNING)
+		fatal_error(allowedTo('admin_forum') ? $message : $error_string, false);
+
+	// We should NEVER get to this point.  Any fatal error MUST quit, or very bad things can happen.
+	if ($error_level % 255 == E_ERROR)
+		die('Hacking attempt...');
+}
+
+function setup_fatal_error_context($error_message)
+{
+	global $context, $txt, $ssi_on_error_method;
+	static $level = 0;
+
+	// Attempt to prevent a recursive loop.
+	++$level;
+	if ($level > 1)
+		return false;
+
+	// Maybe they came from dlattach or similar?
+	if (SMF != 'SSI' && empty($context['theme_loaded']))
+		loadTheme();
+
+	// Don't bother indexing errors mate...
+	$context['robot_no_index'] = true;
+
+	if (!isset($context['error_title']))
+		$context['error_title'] = $txt['error_occured'];
+	$context['error_message'] = isset($context['error_message']) ? $context['error_message'] : $error_message;
+
+	if (empty($context['page_title']))
+		$context['page_title'] = $context['error_title'];
+
+	// Display the error message - wireless?
+	if (defined('WIRELESS') && WIRELESS)
+		$context['sub_template'] = WIRELESS_PROTOCOL . '_error';
+	// Load the template and set the sub template.
+	else
+	{
+		loadTemplate('Errors');
+		$context['sub_template'] = 'fatal_error';
+	}
+
+	// If this is SSI, what do they want us to do?
+	if (SMF == 'SSI')
+	{
+		if (!empty($ssi_on_error_method) && $ssi_on_error_method !== true && is_callable($ssi_on_error_method))
+			$ssi_on_error_method();
+		elseif (empty($ssi_on_error_method) || $ssi_on_error_method !== true)
+			loadSubTemplate('fatal_error');
+
+		// No layers?
+		if (empty($ssi_on_error_method) || $ssi_on_error_method !== true)
+			exit;
+	}
+
+	// We want whatever for the header, and a footer. (footer includes sub template!)
+	obExit(null, true, false, true);
+
+	/* DO NOT IGNORE:
+		If you are creating a bridge to SMF or modifying this function, you MUST
+		make ABSOLUTELY SURE that this function quits and DOES NOT RETURN TO NORMAL
+		PROGRAM FLOW.  Otherwise, security error messages will not be shown, and
+		your forum will be in a very easily hackable state.
+	*/
+	trigger_error('Hacking attempt...', E_USER_ERROR);
+}
+
+// Show an error message for the connection problems.
+function show_db_error($loadavg = false)
+{
+	global $sourcedir, $mbname, $maintenance, $mtitle, $mmessage, $modSettings;
+	global $db_connection, $webmaster_email, $db_last_error, $db_error_send, $smcFunc;
+
+	// Don't cache this page!
+	header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
+	header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
+	header('Cache-Control: no-cache');
+
+	// Send the right error codes.
+	header('HTTP/1.1 503 Service Temporarily Unavailable');
+	header('Status: 503 Service Temporarily Unavailable');
+	header('Retry-After: 3600');
+
+	if ($loadavg == false)
+	{
+		// For our purposes, we're gonna want this on if at all possible.
+		$modSettings['cache_enable'] = '1';
+
+		if (($temp = cache_get_data('db_last_error', 600)) !== null)
+			$db_last_error = max($db_last_error, $temp);
+
+		if ($db_last_error < time() - 3600 * 24 * 3 && empty($maintenance) && !empty($db_error_send))
+		{
+			require_once($sourcedir . '/Subs-Admin.php');
+
+			// Avoid writing to the Settings.php file if at all possible; use shared memory instead.
+			cache_put_data('db_last_error', time(), 600);
+			if (($temp = cache_get_data('db_last_error', 600)) == null)
+				updateLastDatabaseError();
+
+			// Language files aren't loaded yet :(.
+			$db_error = @$smcFunc['db_error']($db_connection);
+			@mail($webmaster_email, $mbname . ': SMF Database Error!', 'There has been a problem with the database!' . ($db_error == '' ? '' : "\n" . $smcFunc['db_title'] . ' reported:' . "\n" . $db_error) . "\n\n" . 'This is a notice email to let you know that SMF could not connect to the database, contact your host if this continues.');
+		}
+	}
+
+	if (!empty($maintenance))
+		echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+	<head>
+		<meta name="robots" content="noindex" />
+		<title>', $mtitle, '</title>
+	</head>
+	<body>
+		<h3>', $mtitle, '</h3>
+		', $mmessage, '
+	</body>
+</html>';
+	// If this is a load average problem, display an appropriate message (but we still don't have language files!)
+	elseif ($loadavg)
+		echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+	<head>
+		<meta name="robots" content="noindex" />
+		<title>Temporarily Unavailable</title>
+	</head>
+	<body>
+		<h3>Temporarily Unavailable</h3>
+		Due to high stress on the server the forum is temporarily unavailable.  Please try again later.
+	</body>
+</html>';
+	// What to do?  Language files haven't and can't be loaded yet...
+	else
+		echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+	<head>
+		<meta name="robots" content="noindex" />
+		<title>Connection Problems</title>
+	</head>
+	<body>
+		<h3>Connection Problems</h3>
+		Sorry, SMF was unable to connect to the database.  This may be caused by the server being busy.  Please try again later.
+	</body>
+</html>';
+
+	die;
+}
+
+?>

+ 967 - 0
Sources/Groups.php

@@ -0,0 +1,967 @@
+<?php
+
+/**
+ * Simple Machines Forum (SMF)
+ *
+ * @package SMF
+ * @author Simple Machines http://www.simplemachines.org
+ * @copyright 2011 Simple Machines
+ * @license http://www.simplemachines.org/about/smf/license.php BSD
+ *
+ * @version 2.0
+ */
+
+if (!defined('SMF'))
+	die('Hacking attempt...');
+
+/* This file currently just shows group info, and allows certain privaledged members to add/remove members.
+
+	void Groups()
+		- allows moderators and users to access the group showing functions.
+		- handles permission checks, and puts the moderation bar on as required.
+
+	void MembergroupMembers()
+		- can be called from ManageMembergroups if it needs templating within the admin environment.
+		- show a list of members that are part of a given membergroup.
+		- called by ?action=moderate;area=viewgroups;sa=members;group=x
+		- requires the manage_membergroups permission.
+		- uses the group_members sub template of ManageMembergroups.
+		- allows to add and remove members from the selected membergroup.
+		- allows sorting on several columns.
+		- redirects to itself.
+
+	int list_getGroupRequestCount(string where)
+		- callback function for createList()
+		- returns the count of group requests
+
+	array list_getGroupRequests(int start, int items_per_page, string sort, string where)
+		- callback function for createList()
+		- returns an array of group requests
+		- each group request has:
+			'id'
+			'member_link'
+			'group_link'
+			'reason'
+			'time_submitted'
+
+*/
+
+// Entry point, permission checks, admin bars, etc.
+function Groups()
+{
+	global $context, $txt, $scripturl, $sourcedir, $user_info;
+
+	// The sub-actions that we can do. Format "Function Name, Mod Bar Index if appropriate".
+	$subActions = array(
+		'index' => array('GroupList', 'view_groups'),
+		'members' => array('MembergroupMembers', 'view_groups'),
+		'requests' => array('GroupRequests', 'group_requests'),
+	);
+
+	// Default to sub action 'index' or 'settings' depending on permissions.
+	$_REQUEST['sa'] = isset($_REQUEST['sa']) && isset($subActions[$_REQUEST['sa']]) ? $_REQUEST['sa'] : 'index';
+
+	// Get the template stuff up and running.
+	loadLanguage('ManageMembers');
+	loadLanguage('ModerationCenter');
+	loadTemplate('ManageMembergroups');
+
+	// If we can see the moderation center, and this has a mod bar entry, add the mod center bar.
+	if (allowedTo('access_mod_center') || $user_info['mod_cache']['bq'] != '0=1' || $user_info['mod_cache']['gq'] != '0=1' || allowedTo('manage_membergroups'))
+	{
+		require_once($sourcedir . '/ModerationCenter.php');
+		$_GET['area'] = $_REQUEST['sa'] == 'requests' ? 'groups' : 'viewgroups';
+		ModerationMain(true);
+	}
+	// Otherwise add something to the link tree, for normal people.
+	else
+	{
+		isAllowedTo('view_mlist');
+
+		$context['linktree'][] = array(
+			'url' => $scripturl . '?action=groups',
+			'name' => $txt['groups'],
+		);
+	}
+
+	// Call the actual function.
+	$subActions[$_REQUEST['sa']][0]();
+}
+
+// This very simply lists the groups, nothing snazy.
+function GroupList()
+{
+	global $txt, $scripturl, $user_profile, $user_info, $context, $settings, $modSettings, $smcFunc, $sourcedir;
+
+	// Yep, find the groups...
+	$request = $smcFunc['db_query']('', '
+		SELECT mg.id_group, mg.group_name, mg.description, mg.group_type, mg.online_color, mg.hidden,
+			mg.stars, IFNULL(gm.id_member, 0) AS can_moderate
+		FROM {db_prefix}membergroups AS mg
+			LEFT JOIN {db_prefix}group_moderators AS gm ON (gm.id_group = mg.id_group AND gm.id_member = {int:current_member})
+		WHERE mg.min_posts = {int:min_posts}
+			AND mg.id_group != {int:mod_group}' . (allowedTo('admin_forum') ? '' : '
+			AND mg.group_type != {int:is_protected}') . '
+		ORDER BY group_name',
+		array(
+			'current_member' => $user_info['id'],
+			'min_posts' => -1,
+			'mod_group' => 3,
+			'is_protected' => 1,
+		)
+	);
+	// This is where we store our groups.
+	$context['groups'] = array();
+	$group_ids = array();
+	$context['can_moderate'] = allowedTo('manage_membergroups');
+	while ($row = $smcFunc['db_fetch_assoc']($request))
+	{
+		// We only list the groups they can see.
+		if ($row['hidden'] && !$row['can_moderate'] && !allowedTo('manage_membergroups'))
+			continue;
+
+		$row['stars'] = explode('#', $row['stars']);
+
+		$context['groups'][$row['id_group']] = array(
+			'id' => $row['id_group'],
+			'name' => $row['group_name'],
+			'desc' => $row['description'],
+			'color' => $row['online_color'],
+			'type' => $row['group_type'],
+			'num_members' => 0,
+			'stars' => !empty($row['stars'][0]) && !empty($row['stars'][1]) ? str_repeat('<img src="' . $settings['images_url'] . '/' . $row['stars'][1] . '" alt="*" />', $row['stars'][0]) : '',
+		);
+
+		$context['can_moderate'] |= $row['can_moderate'];
+		$group_ids[] = $row['id_group'];
+	}
+	$smcFunc['db_free_result']($request);
+
+	// Count up the members separately...
+	if (!empty($group_ids))
+	{
+		$query = $smcFunc['db_query']('', '
+			SELECT id_group, COUNT(*) AS num_members
+			FROM {db_prefix}members
+			WHERE id_group IN ({array_int:group_list})
+			GROUP BY id_group',
+			array(
+				'group_list' => $group_ids,
+			)
+		);
+		while ($row = $smcFunc['db_fetch_assoc']($query))
+			$context['groups'][$row['id_group']]['num_members'] += $row['num_members'];
+		$smcFunc['db_free_result']($query);
+
+		// Only do additional groups if we can moderate...
+		if ($context['can_moderate'])
+		{
+			$query = $smcFunc['db_query']('', '
+				SELECT mg.id_group, COUNT(*) AS num_members
+				FROM {db_prefix}membergroups AS mg
+					INNER JOIN {db_prefix}members AS mem ON (mem.additional_groups != {string:blank_screen}
+						AND mem.id_group != mg.id_group
+						AND FIND_IN_SET(mg.id_group, mem.additional_groups) != 0)
+				WHERE mg.id_group IN ({array_int:group_list})
+				GROUP BY mg.id_group',
+				array(
+					'group_list' => $group_ids,
+					'blank_screen' => '',
+				)
+			);
+			while ($row = $smcFunc['db_fetch_assoc']($query))
+				$context['groups'][$row['id_group']]['num_members'] += $row['num_members'];
+			$smcFunc['db_free_result']($query);
+		}
+	}
+
+	$context['sub_template'] = 'group_index';
+	$context['page_title'] = $txt['viewing_groups'];
+
+	// Making a list is not hard with this beauty.
+	require_once($sourcedir . '/Subs-List.php');
+
+	// Use the standard templates for showing this.
+	$listOptions = array(
+		'id' => 'group_lists',
+		'title' => $context['page_title'],
+		'get_items' => array(
+			'function' => 'list_getGroups',
+		),
+		'columns' => array(
+			'group' => array(
+				'header' => array(
+					'value' => $txt['name'],
+				),
+				'data' => array(
+					'function' => create_function('$group', '
+						global $scripturl, $context;
+
+						$output = \'<a href="\' . $scripturl . \'?action=\' . $context[\'current_action\'] . (isset($context[\'admin_area\']) ? \';area=\' . $context[\'admin_area\'] : \'\') . \';sa=members;group=\' . $group[\'id\'] . \'" \' . ($group[\'color\'] ? \'style="color: \' . $group[\'color\'] . \';"\' : \'\') . \'>\' . $group[\'name\'] . \'</a>\';
+
+						if ($group[\'desc\'])
+							$output .= \'<div class="smalltext">\' . $group[\'desc\'] . \'</div>\';
+
+						return $output;
+					'),
+					'style' => 'width: 50%;',
+				),
+			),
+			'stars' => array(
+				'header' => array(
+					'value' => $txt['membergroups_stars'],
+				),
+				'data' => array(
+					'db' => 'stars',
+				),
+			),
+			'moderators' => array(
+				'header' => array(
+					'value' => $txt['moderators'],
+				),
+				'data' => array(
+					'function' => create_function('$group', '
+						global $txt;
+
+						return empty($group[\'moderators\']) ? \'<em>\' . $txt[\'membergroups_new_copy_none\'] . \'</em>\' : implode(\', \', $group[\'moderators\']);
+					'),
+				),
+			),
+			'members' => array(
+				'header' => array(
+					'value' => $txt['membergroups_members_top'],
+				),
+				'data' => array(
+					'comma_format' => true,
+					'db' => 'num_members',
+				),
+			),
+		),
+	);
+
+	// Create the request list.
+	createList($listOptions);
+
+	$context['sub_template'] = 'show_list';
+	$context['default_list'] = 'group_lists';
+}
+
+// Get the group information for the list.
+function list_getGroups($start, $items_per_page, $sort)
+{
+	global $smcFunc, $txt, $scripturl, $user_info, $settings;
+
+	// Yep, find the groups...
+	$request = $smcFunc['db_query']('', '
+		SELECT mg.id_group, mg.group_name, mg.description, mg.group_type, mg.online_color, mg.hidden,
+			mg.stars, IFNULL(gm.id_member, 0) AS can_moderate
+		FROM {db_prefix}membergroups AS mg
+			LEFT JOIN {db_prefix}group_moderators AS gm ON (gm.id_group = mg.id_group AND gm.id_member = {int:current_member})
+		WHERE mg.min_posts = {int:min_posts}
+			AND mg.id_group != {int:mod_group}' . (allowedTo('admin_forum') ? '' : '
+			AND mg.group_type != {int:is_protected}') . '
+		ORDER BY group_name',
+		array(
+			'current_member' => $user_info['id'],
+			'min_posts' => -1,
+			'mod_group' => 3,
+			'is_protected' => 1,
+		)
+	);
+	// Start collecting the data.
+	$groups = array();
+	$group_ids = array();
+	$context['can_moderate'] = allowedTo('manage_membergroups');
+	while ($row = $smcFunc['db_fetch_assoc']($request))
+	{
+		// We only list the groups they can see.
+		if ($row['hidden'] && !$row['can_moderate'] && !allowedTo('manage_membergroups'))
+			continue;
+
+		$row['stars'] = explode('#', $row['stars']);
+
+		$groups[$row['id_group']] = array(
+			'id' => $row['id_group'],
+			'name' => $row['group_name'],
+			'desc' => $row['description'],
+			'color' => $row['online_color'],
+			'type' => $row['group_type'],
+			'num_members' => 0,
+			'moderators' => array(),
+			'stars' => !empty($row['stars'][0]) && !empty($row['stars'][1]) ? str_repeat('<img src="' . $settings['images_url'] . '/' . $row['stars'][1] . '" alt="*" />', $row['stars'][0]) : '',
+		);
+
+		$context['can_moderate'] |= $row['can_moderate'];
+		$group_ids[] = $row['id_group'];
+	}
+	$smcFunc['db_free_result']($request);
+
+	// Count up the members separately...
+	if (!empty($group_ids))
+	{
+		$query = $smcFunc['db_query']('', '
+			SELECT id_group, COUNT(*) AS num_members
+			FROM {db_prefix}members
+			WHERE id_group IN ({array_int:group_list})
+			GROUP BY id_group',
+			array(
+				'group_list' => $group_ids,
+			)
+		);
+		while ($row = $smcFunc['db_fetch_assoc']($query))
+			$groups[$row['id_group']]['num_members'] += $row['num_members'];
+		$smcFunc['db_free_result']($query);
+
+		// Only do additional groups if we can moderate...
+		if ($context['can_moderate'])
+		{
+			$query = $smcFunc['db_query']('', '
+				SELECT mg.id_group, COUNT(*) AS num_members
+				FROM {db_prefix}membergroups AS mg
+					INNER JOIN {db_prefix}members AS mem ON (mem.additional_groups != {string:blank_screen}
+						AND mem.id_group != mg.id_group
+						AND FIND_IN_SET(mg.id_group, mem.additional_groups) != 0)
+				WHERE mg.id_group IN ({array_int:group_list})
+				GROUP BY mg.id_group',
+				array(
+					'group_list' => $group_ids,
+					'blank_screen' => '',
+				)
+			);
+			while ($row = $smcFunc['db_fetch_assoc']($query))
+				$groups[$row['id_group']]['num_members'] += $row['num_members'];
+			$smcFunc['db_free_result']($query);
+		}
+	}
+
+	// Get any group moderators.
+	// Count up the members separately...
+	if (!empty($group_ids))
+	{
+		$query = $smcFunc['db_query']('', '
+			SELECT mods.id_group, mods.id_member, mem.member_name, mem.real_name
+			FROM {db_prefix}group_moderators AS mods
+				INNER JOIN {db_prefix}members AS mem ON (mem.id_member = mods.id_member)
+			WHERE mods.id_group IN ({array_int:group_list})',
+			array(
+				'group_list' => $group_ids,
+			)
+		);
+		while ($row = $smcFunc['db_fetch_assoc']($query))
+			$groups[$row['id_group']]['moderators'][] = '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['real_name'] . '</a>';
+		$smcFunc['db_free_result']($query);
+	}
+
+	return $groups;
+}
+
+// How many groups are there that are visible?
+function list_getGroupCount()
+{
+	global $smcFunc;
+
+	$request = $smcFunc['db_query']('', '
+		SELECT COUNT(id_group) AS group_count
+		FROM {db_prefix}membergroups
+		WHERE mg.min_posts = {int:min_posts}
+			AND mg.id_group != {int:mod_group}' . (allowedTo('admin_forum') ? '' : '
+			AND mg.group_type != {int:is_protected}'),
+		array(
+			'min_posts' => -1,
+			'mod_group' => 3,
+			'is_protected' => 1,
+		)
+	);
+	list ($group_count) = $smcFunc['db_fetch_row']($request);
+	$smcFunc['db_free_result']($request);
+
+	return $group_count;
+}
+
+// Display members of a group, and allow adding of members to a group. Silly function name though ;)
+function MembergroupMembers()
+{
+	global $txt, $scripturl, $context, $modSettings, $sourcedir, $user_info, $settings, $smcFunc;
+
+	$_REQUEST['group'] = isset($_REQUEST['group']) ? (int) $_REQUEST['group'] : 0;
+
+	// No browsing of guests, membergroup 0 or moderators.
+	if (in_array($_REQUEST['group'], array(-1, 0, 3)))
+		fatal_lang_error('membergroup_does_not_exist', false);
+
+	// Load up the group details.
+	$request = $smcFunc['db_query']('', '
+		SELECT id_group AS id, group_name AS name, CASE WHEN min_posts = {int:min_posts} THEN 1 ELSE 0 END AS assignable, hidden, online_color,
+			stars, description, CASE WHEN min_posts != {int:min_posts} THEN 1 ELSE 0 END AS is_post_group, group_type
+		FROM {db_prefix}membergroups
+		WHERE id_group = {int:id_group}
+		LIMIT 1',
+		array(
+			'min_posts' => -1,
+			'id_group' => $_REQUEST['group'],
+		)
+	);
+	// Doesn't exist?
+	if ($smcFunc['db_num_rows']($request) == 0)
+		fatal_lang_error('membergroup_does_not_exist', false);
+	$context['group'] = $smcFunc['db_fetch_assoc']($request);
+	$smcFunc['db_free_result']($request);
+
+	// Fix the stars.
+	$context['group']['stars'] = explode('#', $context['group']['stars']);
+	$context['group']['stars'] = !empty($context['group']['stars'][0]) && !empty($context['group']['stars'][1]) ? str_repeat('<img src="' . $settings['images_url'] . '/' . $context['group']['stars'][1] . '" alt="*" />', $context['group']['stars'][0]) : '';
+	$context['group']['can_moderate'] = allowedTo('manage_membergroups') && (allowedTo('admin_forum') || $context['group']['group_type'] != 1);
+
+	$context['linktree'][] = array(
+		'url' => $scripturl . '?action=groups;sa=members;group=' . $context['group']['id'],
+		'name' => $context['group']['name'],
+	);
+
+	// Load all the group moderators, for fun.
+	$request = $smcFunc['db_query']('', '
+		SELECT mem.id_member, mem.real_name
+		FROM {db_prefix}group_moderators AS mods
+			INNER JOIN {db_prefix}members AS mem ON (mem.id_member = mods.id_member)
+		WHERE mods.id_group = {int:id_group}',
+		array(
+			'id_group' => $_REQUEST['group'],
+		)
+	);
+	$context['group']['moderators'] = array();
+	while ($row = $smcFunc['db_fetch_assoc']($request))
+	{
+		$context['group']['moderators'][] = array(
+			'id' => $row['id_member'],
+			'name' => $row['real_name']
+		);
+
+		if ($user_info['id'] == $row['id_member'] && $context['group']['group_type'] != 1)
+			$context['group']['can_moderate'] = true;
+	}
+	$smcFunc['db_free_result']($request);
+
+	// If this group is hidden then it can only "exists" if the user can moderate it!
+	if ($context['group']['hidden'] && !$context['group']['can_moderate'])
+		fatal_lang_error('membergroup_does_not_exist', false);
+
+	// You can only assign membership if you are the moderator and/or can manage groups!
+	if (!$context['group']['can_moderate'])
+		$context['group']['assignable'] = 0;
+	// Non-admins cannot assign admins.
+	elseif ($context['group']['id'] == 1 && !allowedTo('admin_forum'))
+		$context['group']['assignable'] = 0;
+
+	// Removing member from group?
+	if (isset($_POST['remove']) && !empty($_REQUEST['rem']) && is_array($_REQUEST['rem']) && $context['group']['assignable'])
+	{
+		checkSession();
+
+		// Make sure we're dealing with integers only.
+		foreach ($_REQUEST['rem'] as $key => $group)
+			$_REQUEST['rem'][$key] = (int) $group;
+
+		require_once($sourcedir . '/Subs-Membergroups.php');
+		removeMembersFromGroups($_REQUEST['rem'], $_REQUEST['group'], true);
+	}
+	// Must be adding new members to the group...
+	elseif (isset($_REQUEST['add']) && (!empty($_REQUEST['toAdd']) || !empty($_REQUEST['member_add'])) && $context['group']['assignable'])
+	{
+		checkSession();
+
+		$member_query = array();
+		$member_parameters = array();
+
+		// Get all the members to be added... taking into account names can be quoted ;)
+		$_REQUEST['toAdd'] = strtr($smcFunc['htmlspecialchars']($_REQUEST['toAdd'], ENT_QUOTES), array('&quot;' => '"'));
+		preg_match_all('~"([^"]+)"~', $_REQUEST['toAdd'], $matches);
+		$member_names = array_unique(array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $_REQUEST['toAdd']))));
+
+		foreach ($member_names as $index => $member_name)
+		{
+			$member_names[$index] = trim($smcFunc['strtolower']($member_names[$index]));
+
+			if (strlen($member_names[$index]) == 0)
+				unset($member_names[$index]);
+		}
+
+		// Any passed by ID?
+		$member_ids = array();
+		if (!empty($_REQUEST['member_add']))
+			foreach ($_REQUEST['member_add'] as $id)
+				if ($id > 0)
+					$member_ids[] = (int) $id;
+
+		// Construct the query pelements.
+		if (!empty($member_ids))
+		{
+			$member_query[] = 'id_member IN ({array_int:member_ids})';
+			$member_parameters['member_ids'] = $member_ids;
+		}
+		if (!empty($member_names))
+		{
+			$member_query[] = 'LOWER(member_name) IN ({array_string:member_names})';
+			$member_query[] = 'LOWER(real_name) IN ({array_string:member_names})';
+			$member_parameters['member_names'] = $member_names;
+		}
+
+		$members = array();
+		if (!empty($member_query))
+		{
+			$request = $smcFunc['db_query']('', '
+				SELECT id_member
+				FROM {db_prefix}members
+				WHERE (' . implode(' OR ', $member_query) . ')
+					AND id_group != {int:id_group}
+					AND FIND_IN_SET({int:id_group}, additional_groups) = 0',
+				array_merge($member_parameters, array(
+					'id_group' => $_REQUEST['group'],
+				))
+			);
+			while ($row = $smcFunc['db_fetch_assoc']($request))
+				$members[] = $row['id_member'];
+			$smcFunc['db_free_result']($request);
+		}
+
+		// !!! Add $_POST['additional'] to templates!
+
+		// Do the updates...
+		if (!empty($members))
+		{
+			require_once($sourcedir . '/Subs-Membergroups.php');
+			addMembersToGroup($members, $_REQUEST['group'], isset($_POST['additional']) || $context['group']['hidden'] ? 'only_additional' : 'auto', true);
+		}
+	}
+
+	// Sort out the sorting!
+	$sort_methods = array(
+		'name' => 'real_name',
+		'email' => allowedTo('moderate_forum') ? 'email_address' : 'hide_email ' . (isset($_REQUEST['desc']) ? 'DESC' : 'ASC') . ', email_address',
+		'active' => 'last_login',
+		'registered' => 'date_registered',
+		'posts' => 'posts',
+	);
+
+	// They didn't pick one, default to by name..
+	if (!isset($_REQUEST['sort']) || !isset($sort_methods[$_REQUEST['sort']]))
+	{
+		$context['sort_by'] = 'name';
+		$querySort = 'real_name';
+	}
+	// Otherwise default to ascending.
+	else
+	{
+		$context['sort_by'] = $_REQUEST['sort'];
+		$querySort = $sort_methods[$_REQUEST['sort']];
+	}
+
+	$context['sort_direction'] = isset($_REQUEST['desc']) ? 'down' : 'up';
+
+	// The where on the query is interesting. Non-moderators should only see people who are in this group as primary.
+	if ($context['group']['can_moderate'])
+		$where = $context['group']['is_post_group'] ? 'id_post_group = {int:group}' : 'id_group = {int:group} OR FIND_IN_SET({int:group}, additional_groups) != 0';
+	else
+		$where = $context['group']['is_post_group'] ? 'id_post_group = {int:group}' : 'id_group = {int:group}';
+
+	// Count members of the group.
+	$request = $smcFunc['db_query']('', '
+		SELECT COUNT(*)
+		FROM {db_prefix}members
+		WHERE ' . $where,
+		array(
+			'group' => $_REQUEST['group'],
+		)
+	);
+	list ($context['total_members']) = $smcFunc['db_fetch_row']($request);
+	$smcFunc['db_free_result']($request);
+	$context['total_members'] = comma_format($context['total_members']);
+
+	// Create the page index.
+	$context['page_index'] = constructPageIndex($scripturl . '?action=' . ($context['group']['can_moderate'] ? 'moderate;area=viewgroups' : 'groups') . ';sa=members;group=' . $_REQUEST['group'] . ';sort=' . $context['sort_by'] . (isset($_REQUEST['desc']) ? ';desc' : ''), $_REQUEST['start'], $context['total_members'], $modSettings['defaultMaxMembers']);
+	$context['start'] = $_REQUEST['start'];
+	$context['can_moderate_forum'] = allowedTo('moderate_forum');
+
+	// Load up all members of this group.
+	$request = $smcFunc['db_query']('', '
+		SELECT id_member, member_name, real_name, email_address, member_ip, date_registered, last_login,
+			hide_email, posts, is_activated, real_name
+		FROM {db_prefix}members
+		WHERE ' . $where . '
+		ORDER BY ' . $querySort . ' ' . ($context['sort_direction'] == 'down' ? 'DESC' : 'ASC') . '
+		LIMIT ' . $context['start'] . ', ' . $modSettings['defaultMaxMembers'],
+		array(
+			'group' => $_REQUEST['group'],
+		)
+	);
+	$context['members'] = array();
+	while ($row = $smcFunc['db_fetch_assoc']($request))
+	{
+		$last_online = empty($row['last_login']) ? $txt['never'] : timeformat($row['last_login']);
+
+		// Italicize the online note if they aren't activated.
+		if ($row['is_activated'] % 10 != 1)
+			$last_online = '<em title="' . $txt['not_activated'] . '">' . $last_online . '</em>';
+
+		$context['members'][] = array(
+			'id' => $row['id_member'],
+			'name' => '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['real_name'] . '</a>',
+			'email' => $row['email_address'],
+			'show_email' => showEmailAddress(!empty($row['hide_email']), $row['id_member']),
+			'ip' => '<a href="' . $scripturl . '?action=trackip;searchip=' . $row['member_ip'] . '">' . $row['member_ip'] . '</a>',
+			'registered' => timeformat($row['date_registered']),
+			'last_online' => $last_online,
+			'posts' => comma_format($row['posts']),
+			'is_activated' => $row['is_activated'] % 10 == 1,
+		);
+	}
+	$smcFunc['db_free_result']($request);
+
+	// Select the template.
+	$context['sub_template'] = 'group_members';
+	$context['page_title'] = $txt['membergroups_members_title'] . ': ' . $context['group']['name'];
+}
+
+// Show and manage all group requests.
+function GroupRequests()
+{
+	global $txt, $context, $scripturl, $user_info, $sourcedir, $smcFunc, $modSettings, $language;
+
+	// Set up the template stuff...
+	$context['page_title'] = $txt['mc_group_requests'];
+	$context['sub_template'] = 'show_list';
+
+	// Verify we can be here.
+	if ($user_info['mod_cache']['gq'] == '0=1')
+		isAllowedTo('manage_membergroups');
+
+	// Normally, we act normally...
+	$where = $user_info['mod_cache']['gq'] == '1=1' || $user_info['mod_cache']['gq'] == '0=1' ? $user_info['mod_cache']['gq'] : 'lgr.' . $user_info['mod_cache']['gq'];
+	$where_parameters = array();
+
+	// We've submitted?
+	if (isset($_POST[$context['session_var']]) && !empty($_POST['groupr']) && !empty($_POST['req_action']))
+	{
+		checkSession('post');
+
+		// Clean the values.
+		foreach ($_POST['groupr'] as $k => $request)
+			$_POST['groupr'][$k] = (int) $request;
+
+		// If we are giving a reason (And why shouldn't we?), then we don't actually do much.
+		if ($_POST['req_action'] == 'reason')
+		{
+			// Different sub template...
+			$context['sub_template'] = 'group_request_reason';
+			// And a limitation. We don't care that the page number bit makes no sense, as we don't need it!
+			$where .= ' AND lgr.id_request IN ({array_int:request_ids})';
+			$where_parameters['request_ids'] = $_POST['groupr'];
+
+			$context['group_requests'] = list_getGroupRequests(0, $modSettings['defaultMaxMessages'], 'lgr.id_request', $where, $where_parameters);
+
+			// Let obExit etc sort things out.
+			obExit();
+		}
+		// Otherwise we do something!
+		else
+		{
+			// Get the details of all the members concerned...
+			$request = $smcFunc['db_query']('', '
+				SELECT lgr.id_request, lgr.id_member, lgr.id_group, mem.email_address, mem.id_group AS primary_group,
+					mem.additional_groups AS additional_groups, mem.lngfile, mem.member_name, mem.notify_types,
+					mg.hidden, mg.group_name
+				FROM {db_prefix}log_group_requests AS lgr
+					INNER JOIN {db_prefix}members AS mem ON (mem.id_member = lgr.id_member)
+					INNER JOIN {db_prefix}membergroups AS mg ON (mg.id_group = lgr.id_group)
+				WHERE ' . $where . '
+					AND lgr.id_request IN ({array_int:request_list})
+				ORDER BY mem.lngfile',
+				array(
+					'request_list' => $_POST['groupr'],
+				)
+			);
+			$email_details = array();
+			$group_changes = array();
+			while ($row = $smcFunc['db_fetch_assoc']($request))
+			{
+				$row['lngfile'] = empty($row['lngfile']) || empty($modSettings['userLanguage']) ? $language : $row['lngfile'];
+
+				// If we are approving work out what their new group is.
+				if ($_POST['req_action'] == 'approve')
+				{
+					// For people with more than one request at once.
+					if (isset($group_changes[$row['id_member']]))
+					{
+						$row['additional_groups'] = $group_changes[$row['id_member']]['add'];
+						$row['primary_group'] = $group_changes[$row['id_member']]['primary'];
+					}
+					else
+						$row['additional_groups'] = explode(',', $row['additional_groups']);
+
+					// Don't have it already?
+					if ($row['primary_group'] == $row['id_group'] || in_array($row['id_group'], $row['additional_groups']))
+						continue;
+
+					// Should it become their primary?
+					if ($row['primary_group'] == 0 && $row['hidden'] == 0)
+						$row['primary_group'] = $row['id_group'];
+					else
+						$row['additional_groups'][] = $row['id_group'];
+
+					// Add them to the group master list.
+					$group_changes[$row['id_member']] = array(
+						'primary' => $row['primary_group'],
+						'add' => $row['additional_groups'],
+					);
+				}
+
+				// Add required information to email them.
+				if ($row['notify_types'] != 4)
+					$email_details[] = array(
+						'rid' => $row['id_request'],
+						'member_id' => $row['id_member'],
+						'member_name' => $row['member_name'],
+						'group_id' => $row['id_group'],
+						'group_name' => $row['group_name'],
+						'email' => $row['email_address'],
+						'language' => $row['lngfile'],
+					);
+			}
+			$smcFunc['db_free_result']($request);
+
+			// Remove the evidence...
+			$smcFunc['db_query']('', '
+				DELETE FROM {db_prefix}log_group_requests
+				WHERE id_request IN ({array_int:request_list})',
+				array(
+					'request_list' => $_POST['groupr'],
+				)
+			);
+
+			// Ensure everyone who is online gets their changes right away.
+			updateSettings(array('settings_updated' => time()));
+
+			if (!empty($email_details))
+			{
+				require_once($sourcedir . '/Subs-Post.php');
+
+				// They are being approved?
+				if ($_POST['req_action'] == 'approve')
+				{
+					// Make the group changes.
+					foreach ($group_changes as $id => $groups)
+					{
+						// Sanity check!
+						foreach ($groups['add'] as $key => $value)
+							if ($value == 0 || trim($value) == '')
+								unset($groups['add'][$key]);
+
+						$smcFunc['db_query']('', '
+							UPDATE {db_prefix}members
+							SET id_group = {int:primary_group}, additional_groups = {string:additional_groups}
+							WHERE id_member = {int:selected_member}',
+							array(
+								'primary_group' => $groups['primary'],
+								'selected_member' => $id,
+								'additional_groups' => implode(',', $groups['add']),
+							)
+						);
+					}
+
+					$lastLng = $user_info['language'];
+					foreach ($email_details as $email)
+					{
+						$replacements = array(
+							'USERNAME' => $email['member_name'],
+							'GROUPNAME' => $email['group_name'],
+						);
+
+						$emaildata = loadEmailTemplate('mc_group_approve', $replacements, $email['language']);
+
+						sendmail($email['email'], $emaildata['subject'], $emaildata['body'], null, null, false, 2);
+					}
+				}
+				// Otherwise, they are getting rejected (With or without a reason).
+				else
+				{
+					// Same as for approving, kind of.
+					$lastLng = $user_info['language'];
+					foreach ($email_details as $email)
+					{
+						$custom_reason = isset($_POST['groupreason']) && isset($_POST['groupreason'][$email['rid']]) ? $_POST['groupreason'][$email['rid']] : '';
+
+						$replacements = array(
+							'USERNAME' => $email['member_name'],
+							'GROUPNAME' => $email['group_name'],
+						);
+
+						if (!empty($custom_reason))
+							$replacements['REASON'] = $custom_reason;
+
+						$emaildata = loadEmailTemplate(empty($custom_reason) ? 'mc_group_reject' : 'mc_group_reject_reason', $replacements, $email['language']);
+
+						sendmail($email['email'], $emaildata['subject'], $emaildata['body'], null, null, false, 2);
+					}
+				}
+			}
+
+			// Restore the current language.
+			loadLanguage('ModerationCenter');
+		}
+	}
+
+	// We're going to want this for making our list.
+	require_once($sourcedir . '/Subs-List.php');
+
+	// This is all the information required for a group listing.
+	$listOptions = array(
+		'id' => 'group_request_list',
+		'title' => $txt['mc_group_requests'],
+		'width' => '100%',
+		'items_per_page' => $modSettings['defaultMaxMessages'],
+		'no_items_label' => $txt['mc_groupr_none_found'],
+		'base_href' => $scripturl . '?action=groups;sa=requests',
+		'default_sort_col' => 'member',
+		'get_items' => array(
+			'function' => 'list_getGroupRequests',
+			'params' => array(
+				$where,
+				$where_parameters,
+			),
+		),
+		'get_count' => array(
+			'function' => 'list_getGroupRequestCount',
+			'params' => array(
+				$where,
+				$where_parameters,
+			),
+		),
+		'columns' => array(
+			'member' => array(
+				'header' => array(
+					'value' => $txt['mc_groupr_member'],
+				),
+				'data' => array(
+					'db' => 'member_link',
+				),
+				'sort' => array(
+					'default' => 'mem.member_name',
+					'reverse' => 'mem.member_name DESC',
+				),
+			),
+			'group' => array(
+				'header' => array(
+					'value' => $txt['mc_groupr_group'],
+				),
+				'data' => array(
+					'db' => 'group_link',
+				),
+				'sort' => array(
+					'default' => 'mg.group_name',
+					'reverse' => 'mg.group_name DESC',
+				),
+			),
+			'reason' => array(
+				'header' => array(
+					'value' => $txt['mc_groupr_reason'],
+				),
+				'data' => array(
+					'db' => 'reason',
+				),
+			),
+			'action' => array(
+				'header' => array(
+					'value' => '<input type="checkbox" class="input_check" onclick="invertAll(this, this.form);" />',
+					'style' => 'width: 4%;',
+				),
+				'data' => array(
+					'sprintf' => array(
+						'format' => '<input type="checkbox" name="groupr[]" value="%1$d" class="input_check" />',
+						'params' => array(
+							'id' => false,
+						),
+					),
+					'style' => 'text-align: center;',
+				),
+			),
+		),
+		'form' => array(
+			'href' => $scripturl . '?action=groups;sa=requests',
+			'include_sort' => true,
+			'include_start' => true,
+			'hidden_fields' => array(
+				$context['session_var'] => $context['session_id'],
+			),
+		),
+		'additional_rows' => array(
+			array(
+				'position' => 'bottom_of_list',
+				'value' => '
+					<select name="req_action" onchange="if (this.value != 0 &amp;&amp; (this.value == \'reason\' || confirm(\'' . $txt['mc_groupr_warning'] . '\'))) this.form.submit();">
+						<option value="0">' . $txt['with_selected'] . ':</option>
+						<option value="0">---------------------</option>
+						<option value="approve">' . $txt['mc_groupr_approve'] . '</option>
+						<option value="reject">' . $txt['mc_groupr_reject'] . '</option>
+						<option value="reason">' . $txt['mc_groupr_reject_w_reason'] . '</option>
+					</select>
+					<input type="submit" name="go" value="' . $txt['go'] . '" onclick="var sel = document.getElementById(\'req_action\'); if (sel.value != 0 &amp;&amp; sel.value != \'reason\' &amp;&amp; !confirm(\'' . $txt['mc_groupr_warning'] . '\')) return false;" class="button_submit" />',
+				'align' => 'right',
+			),
+		),
+	);
+
+	// Create the request list.
+	createList($listOptions);
+
+	$context['default_list'] = 'group_request_list';
+}
+
+function list_getGroupRequestCount($where, $where_parameters)
+{
+	global $smcFunc;
+
+	$request = $smcFunc['db_query']('', '
+		SELECT COUNT(*)
+		FROM {db_prefix}log_group_requests AS lgr
+		WHERE ' . $where,
+		array_merge($where_parameters, array(
+		))
+	);
+	list ($totalRequests) = $smcFunc['db_fetch_row']($request);
+	$smcFunc['db_free_result']($request);
+
+	return $totalRequests;
+}
+
+function list_getGroupRequests($start, $items_per_page, $sort, $where, $where_parameters)
+{
+	global $smcFunc, $txt, $scripturl;
+
+	$request = $smcFunc['db_query']('', '
+		SELECT lgr.id_request, lgr.id_member, lgr.id_group, lgr.time_applied, lgr.reason,
+			mem.member_name, mg.group_name, mg.online_color, mem.real_name
+		FROM {db_prefix}log_group_requests AS lgr
+			INNER JOIN {db_prefix}members AS mem ON (mem.id_member = lgr.id_member)
+			INNER JOIN {db_prefix}membergroups AS mg ON (mg.id_group = lgr.id_group)
+		WHERE ' . $where . '
+		ORDER BY {raw:sort}
+		LIMIT ' . $start . ', ' . $items_per_page,
+		array_merge($where_parameters, array(
+			'sort' => $sort,
+		))
+	);
+	$group_requests = array();
+	while ($row = $smcFunc['db_fetch_assoc']($request))
+	{
+		$group_requests[] = array(
+			'id' => $row['id_request'],
+			'member_link' => '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['real_name'] . '</a>',
+			'group_link' => '<span style="color: ' . $row['online_color'] . '">' . $row['group_name'] . '</span>',
+			'reason' => censorText($row['reason']),
+			'time_submitted' => timeformat($row['time_applied']),
+		);
+	}
+	$smcFunc['db_free_result']($request);
+
+	return $group_requests;
+}
+
+?>

+ 111 - 0
Sources/Help.php

@@ -0,0 +1,111 @@
+<?php
+
+/**
+ * Simple Machines Forum (SMF)
+ *
+ * @package SMF
+ * @author Simple Machines http://www.simplemachines.org
+ * @copyright 2011 Simple Machines
+ * @license http://www.simplemachines.org/about/smf/license.php BSD
+ *
+ * @version 2.0
+ */
+
+if (!defined('SMF'))
+	die('Hacking attempt...');
+
+/*	This file has the important job of taking care of help messages and the
+	help center.  It does this with two simple functions:
+
+	void ShowHelp()
+		- loads information needed for the help section.
+		- accesed by ?action=help.
+		- uses the Help template and Manual language file.
+
+	void ShowAdminHelp()
+		- shows a popup for administrative or user help.
+		- uses the help parameter to decide what string to display and where
+		  to get the string from. ($helptxt or $txt?)
+		- loads the ManagePermissions language file if the help starts with
+		  permissionhelp.
+		- uses the Help template, popup sub template, no layers.
+		- accessed via ?action=helpadmin;help=??.
+*/
+
+// Redirect to the user help ;).
+function ShowHelp()
+{
+	global $scripturl, $context, $txt;
+
+	loadTemplate('Help');
+	loadLanguage('Manual');
+
+	// We need to know where our wiki is.
+	$context['wiki_url'] = 'http://wiki.simplemachines.org/smf';
+
+	// Sections were are going to link...
+	$context['manual_sections'] = array(
+		'registering' => 'Registering',
+		'logging_in' => 'Logging_In',
+		'profile' => 'Profile',
+		'search' => 'Search',
+		'posting' => 'Posting',
+		'bbc' => 'Bulletin_board_code',
+		'personal_messages' => 'Personal_messages',
+		'memberlist' => 'Memberlist',
+		'calendar' => 'Calendar',
+		'features' => 'Features',
+	);
+
+	// Build the link tree.
+	$context['linktree'][] = array(
+		'url' => $scripturl . '?action=help',
+		'name' => $txt['help'],
+	);
+
+	// Lastly, some minor template stuff.
+	$context['page_title'] = $txt['manual_smf_user_help'];
+	$context['sub_template'] = 'manual';
+}
+
+// Show some of the more detailed help to give the admin an idea...
+function ShowAdminHelp()
+{
+	global $txt, $helptxt, $context, $scripturl;
+
+	if (!isset($_GET['help']) || !is_string($_GET['help']))
+		fatal_lang_error('no_access', false);
+
+	if (!isset($helptxt))
+		$helptxt = array();
+
+	// Load the admin help language file and template.
+	loadLanguage('Help');
+
+	// Permission specific help?
+	if (isset($_GET['help']) && substr($_GET['help'], 0, 14) == 'permissionhelp')
+		loadLanguage('ManagePermissions');
+
+	loadTemplate('Help');
+
+	// Set the page title to something relevant.
+	$context['page_title'] = $context['forum_name'] . ' - ' . $txt['help'];
+
+	// Don't show any template layers, just the popup sub template.
+	$context['template_layers'] = array();
+	$context['sub_template'] = 'popup';
+
+	// What help string should be used?
+	if (isset($helptxt[$_GET['help']]))
+		$context['help_text'] = $helptxt[$_GET['help']];
+	elseif (isset($txt[$_GET['help']]))
+		$context['help_text'] = $txt[$_GET['help']];
+	else
+		$context['help_text'] = $_GET['help'];
+
+	// Does this text contain a link that we should fill in?
+	if (preg_match('~%([0-9]+\$)?s\?~', $context['help_text'], $match))
+		$context['help_text'] = sprintf($context['help_text'], $scripturl, $context['session_id'], $context['session_var']);
+}
+
+?>

+ 204 - 0
Sources/Karma.php

@@ -0,0 +1,204 @@
+<?php
+
+/**
+ * Simple Machines Forum (SMF)
+ *
+ * @package SMF
+ * @author Simple Machines http://www.simplemachines.org
+ * @copyright 2011 Simple Machines
+ * @license http://www.simplemachines.org/about/smf/license.php BSD
+ *
+ * @version 2.0
+ */
+
+if (!defined('SMF'))
+	die('Hacking attempt...');
+
+/*	This file contains one humble function, which applauds or smites a user.
+
+	void ModifyKarma()
+		- gives or takes karma from a user.
+		- redirects back to the referrer afterward, whether by javascript or
+		  the passed parameters.
+		- requires the karma_edit permission, and that the user isn't a guest.
+		- depends on the karmaMode, karmaWaitTime, and karmaTimeRestrictAdmins
+		  settings.
+		- is accessed via ?action=modifykarma.
+*/
+
+// Modify a user's karma.
+function ModifyKarma()
+{
+	global $modSettings, $txt, $user_info, $topic, $smcFunc, $context;
+
+	// If the mod is disabled, show an error.
+	if (empty($modSettings['karmaMode']))
+		fatal_lang_error('feature_disabled', true);
+
+	// If you're a guest or can't do this, blow you off...
+	is_not_guest();
+	isAllowedTo('karma_edit');
+
+	checkSession('get');
+
+	// If you don't have enough posts, tough luck.
+	// !!! Should this be dropped in favor of post group permissions?  Should this apply to the member you are smiting/applauding?
+	if (!$user_info['is_admin'] && $user_info['posts'] < $modSettings['karmaMinPosts'])
+		fatal_lang_error('not_enough_posts_karma', true, array($modSettings['karmaMinPosts']));
+
+	// And you can't modify your own, punk! (use the profile if you need to.)
+	if (empty($_REQUEST['uid']) || (int) $_REQUEST['uid'] == $user_info['id'])
+		fatal_lang_error('cant_change_own_karma', false);
+
+	// The user ID _must_ be a number, no matter what.
+	$_REQUEST['uid'] = (int) $_REQUEST['uid'];
+
+	// Applauding or smiting?
+	$dir = $_REQUEST['sa'] != 'applaud' ? -1 : 1;
+
+	// Delete any older items from the log. (karmaWaitTime is by hour.)
+	$smcFunc['db_query']('', '
+		DELETE FROM {db_prefix}log_karma
+		WHERE {int:current_time} - log_time > {int:wait_time}',
+		array(
+			'wait_time' => (int) ($modSettings['karmaWaitTime'] * 3600),
+			'current_time' => time(),
+		)
+	);
+
+	// Start off with no change in karma.
+	$action = 0;
+
+	// Not an administrator... or one who is restricted as well.
+	if (!empty($modSettings['karmaTimeRestrictAdmins']) || !allowedTo('moderate_forum'))
+	{
+		// Find out if this user has done this recently...
+		$request = $smcFunc['db_query']('', '
+			SELECT action
+			FROM {db_prefix}log_karma
+			WHERE id_target = {int:id_target}
+				AND id_executor = {int:current_member}
+			LIMIT 1',
+			array(
+				'current_member' => $user_info['id'],
+				'id_target' => $_REQUEST['uid'],
+			)
+		);
+		if ($smcFunc['db_num_rows']($request) > 0)
+			list ($action) = $smcFunc['db_fetch_row']($request);
+		$smcFunc['db_free_result']($request);
+	}
+
+	// They haven't, not before now, anyhow.
+	if (empty($action) || empty($modSettings['karmaWaitTime']))
+	{
+		// Put it in the log.
+		$smcFunc['db_insert']('replace',
+				'{db_prefix}log_karma',
+				array('action' => 'int', 'id_target' => 'int', 'id_executor' => 'int', 'log_time' => 'int'),
+				array($dir, $_REQUEST['uid'], $user_info['id'], time()),
+				array('id_target', 'id_executor')
+			);
+
+		// Change by one.
+		updateMemberData($_REQUEST['uid'], array($dir == 1 ? 'karma_good' : 'karma_bad' => '+'));
+	}
+	else
+	{
+		// If you are gonna try to repeat.... don't allow it.
+		if ($action == $dir)
+			fatal_lang_error('karma_wait_time', false, array($modSettings['karmaWaitTime'], $txt['hours']));
+
+		// You decided to go back on your previous choice?
+		$smcFunc['db_query']('', '
+			UPDATE {db_prefix}log_karma
+			SET action = {int:action}, log_time = {int:current_time}
+			WHERE id_target = {int:id_target}
+				AND id_executor = {int:current_member}',
+			array(
+				'current_member' => $user_info['id'],
+				'action' => $dir,
+				'current_time' => time(),
+				'id_target' => $_REQUEST['uid'],
+			)
+		);
+
+		// It was recently changed the OTHER way... so... reverse it!
+		if ($dir == 1)
+			updateMemberData($_REQUEST['uid'], array('karma_good' => '+', 'karma_bad' => '-'));
+		else
+			updateMemberData($_REQUEST['uid'], array('karma_bad' => '+', 'karma_good' => '-'));
+	}
+
+	// Figure out where to go back to.... the topic?
+	if (!empty($topic))
+		redirectexit('topic=' . $topic . '.' . $_REQUEST['start'] . '#msg' . (int) $_REQUEST['m']);
+	// Hrm... maybe a personal message?
+	elseif (isset($_REQUEST['f']))
+		redirectexit('action=pm;f=' . $_REQUEST['f'] . ';start=' . $_REQUEST['start'] . (isset($_REQUEST['l']) ? ';l=' . (int) $_REQUEST['l'] : '') . (isset($_REQUEST['pm']) ? '#' . (int) $_REQUEST['pm'] : ''));
+	// JavaScript as a last resort.
+	else
+	{
+		echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"', $context['right_to_left'] ? ' dir="rtl"' : '', '>
+	<head>
+		<title>...</title>
+		<script type="text/javascript"><!-- // --><![CDATA[
+			history.go(-1);
+		// ]]></script>
+	</head>
+	<body>&laquo;</body>
+</html>';
+
+		obExit(false);
+	}
+}
+
+// What's this?  I dunno, what are you talking about?  Never seen this before, nope.  No siree.
+function BookOfUnknown()
+{
+	global $context;
+
+	if (strpos($_GET['action'], 'mozilla') !== false && !$context['browser']['is_gecko'])
+		redirectexit('http://www.getfirefox.com/');
+	elseif (strpos($_GET['action'], 'mozilla') !== false)
+		redirectexit('about:mozilla');
+
+	echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"', $context['right_to_left'] ? ' dir="rtl"' : '', '>
+	<head>
+		<title>The Book of Unknown, ', @$_GET['verse'] == '2:18' ? '2:18' : '4:16', '</title>
+		<style type="text/css">
+			em
+			{
+				font-size: 1.3em;
+				line-height: 0;
+			}
+		</style>
+	</head>
+	<body style="background-color: #444455; color: white; font-style: italic; font-family: serif;">
+		<div style="margin-top: 12%; font-size: 1.1em; line-height: 1.4; text-align: center;">';
+	if (@$_GET['verse'] == '2:18')
+		echo '
+			Woe, it was that his name wasn\'t <em>known</em>, that he came in mystery, and was recognized by none.&nbsp;And it became to be in those days <em>something</em>.&nbsp; Something not yet <em id="unknown" name="[Unknown]">unknown</em> to mankind.&nbsp; And thus what was to be known the <em>secret project</em> began into its existence.&nbsp; Henceforth the opposition was only <em>weary</em> and <em>fearful</em>, for now their match was at arms against them.';
+	else
+		echo '
+			And it came to pass that the <em>unbelievers</em> dwindled in number and saw rise of many <em>proselytizers</em>, and the opposition found fear in the face of the <em>x</em> and the <em>j</em> while those who stood with the <em>something</em> grew stronger and came together.&nbsp; Still, this was only the <em>beginning</em>, and what lay in the future was <em id="unknown" name="[Unknown]">unknown</em> to all, even those on the right side.';
+	echo '
+		</div>
+		<div style="margin-top: 2ex; font-size: 2em; text-align: right;">';
+	if (@$_GET['verse'] == '2:18')
+		echo '
+			from <span style="font-family: Georgia, serif;"><strong><a href="http://www.unknownbrackets.com/about:unknown" style="color: white; text-decoration: none; cursor: text;">The Book of Unknown</a></strong>, 2:18</span>';
+	else
+		echo '
+			from <span style="font-family: Georgia, serif;"><strong><a href="http://www.unknownbrackets.com/about:unknown" style="color: white; text-decoration: none; cursor: text;">The Book of Unknown</a></strong>, 4:16</span>';
+	echo '
+		</div>
+	</body>
+</html>';
+
+	obExit(false);
+}
+
+?>

+ 2740 - 0
Sources/Load.php

@@ -0,0 +1,2740 @@
+<?php
+
+/**
+ * Simple Machines Forum (SMF)
+ *
+ * @package SMF
+ * @author Simple Machines http://www.simplemachines.org
+ * @copyright 2011 Simple Machines
+ * @license http://www.simplemachines.org/about/smf/license.php BSD
+ *
+ * @version 2.0
+ */
+
+if (!defined('SMF'))
+	die('Hacking attempt...');
+
+/*	This file has the hefty job of loading information for the forum.  It uses
+	the following functions:
+
+	void reloadSettings()
+		- loads or reloads the $modSettings array.
+		- loads any integration settings, SMF_INTEGRATION_SETTINGS, etc.
+
+	void loadUserSettings()
+		- sets up the $user_info array
+		- assigns $user_info['query_wanna_see_board'] for what boards the user can see.
+		- first checks for cookie or intergration validation.
+		- uses the current session if no integration function or cookie is found.
+		- checks password length, if member is activated and the login span isn't over.
+		- if validation fails for the user, $id_member is set to 0.
+		- updates the last visit time when needed.
+
+	void loadBoard()
+		- sets up the $board_info array for current board information.
+		- if cache is enabled, the $board_info array is stored in cache.
+		- redirects to appropriate post if only message id is requested.
+		- is only used when inside a topic or board.
+		- determines the local moderators for the board.
+		- adds group id 3 if the user is a local moderator for the board they are in.
+		- prevents access if user is not in proper group nor a local moderator of the board.
+
+	void loadPermissions()
+		// !!!
+
+	array loadMemberData(array members, bool is_name = false, string set = 'normal')
+		// !!!
+
+	bool loadMemberContext(int id_member)
+		// !!!
+
+	void loadTheme(int id_theme = auto_detect)
+		// !!!
+
+	void loadTemplate(string template_name, array style_sheets = array(), bool fatal = true)
+		- loads a template file with the name template_name from the current,
+		  default, or base theme.
+		- uses the template_include() function to include the file.
+		- detects a wrong default theme directory and tries to work around it.
+		- if fatal is true, dies with an error message if the template cannot
+		  be found.
+
+	void loadSubTemplate(string sub_template_name, bool fatal = false)
+		- loads the sub template specified by sub_template_name, which must be
+		  in an already-loaded template.
+		- if ?debug is in the query string, shows administrators a marker after
+		  every sub template for debugging purposes.
+
+	string loadLanguage(string template_name, string language = default, bool fatal = true, bool force_reload = false)
+		// !!!
+
+	array getBoardParents(int id_parent)
+		- finds all the parents of id_parent, and that board itself.
+		- additionally detects the moderators of said boards.
+		- returns an array of information about the boards found.
+
+	string &censorText(string &text, bool force = false)
+		- censors the passed string.
+		- if the theme setting allow_no_censored is on, and the theme option
+		  show_no_censored is enabled, does not censor - unless force is set.
+		- caches the list of censored words to reduce parsing.
+
+	void template_include(string filename, bool only_once = false)
+		- loads the template or language file specified by filename.
+		- if once is true, only includes the file once (like include_once.)
+		- uses eval unless disableTemplateEval is enabled.
+		- outputs a parse error if the file did not exist or contained errors.
+		- attempts to detect the error and line, and show detailed information.
+
+	void loadSession()
+		// !!!
+
+	void loadDatabase()
+		- takes care of mysql_set_mode, if set.
+		// !!!
+
+	bool sessionOpen(string session_save_path, string session_name)
+	bool sessionClose()
+	bool sessionRead(string session_id)
+	bool sessionWrite(string session_id, string data)
+	bool sessionDestroy(string session_id)
+	bool sessionGC(int max_lifetime)
+		- implementations of PHP's session API.
+		- handle the session data in the database (more scalable.)
+		- use the databaseSession_lifetime setting for garbage collection.
+		- set by loadSession().
+
+	void cache_put_data(string key, mixed value, int ttl = 120)
+		- puts value in the cache under key for ttl seconds.
+		- may "miss" so shouldn't be depended on, and may go to any of many
+		  various caching servers.
+		- supports eAccelerator, Turck MMCache, ZPS, and memcached.
+
+	mixed cache_get_data(string key, int ttl = 120)
+		- gets the value from the cache specified by key, so long as it is not
+		  older than ttl seconds.
+		- may often "miss", so shouldn't be depended on.
+		- supports the same as cache_put_data().
+
+	void get_memcached_server(int recursion_level = 3)
+		- used by cache_get_data() and cache_put_data().
+		- attempts to connect to a random server in the cache_memcached
+		  setting.
+		- recursively calls itself up to recursion_level times.
+*/
+
+// Load the $modSettings array.
+function reloadSettings()
+{
+	global $modSettings, $boarddir, $smcFunc, $txt, $db_character_set, $context, $sourcedir;
+
+	// Most database systems have not set UTF-8 as their default input charset.
+	if (!empty($db_character_set))
+		$smcFunc['db_query']('set_character_set', '
+			SET NAMES ' . $db_character_set,
+			array(
+			)
+		);
+
+	// Try to load it from the cache first; it'll never get cached if the setting is off.
+	if (($modSettings = cache_get_data('modSettings', 90)) == null)
+	{
+		$request = $smcFunc['db_query']('', '
+			SELECT variable, value
+			FROM {db_prefix}settings',
+			array(
+			)
+		);
+		$modSettings = array();
+		if (!$request)
+			db_fatal_error();
+		while ($row = $smcFunc['db_fetch_row']($request))
+			$modSettings[$row[0]] = $row[1];
+		$smcFunc['db_free_result']($request);
+
+		// Do a few things to protect against missing settings or settings with invalid values...
+		if (empty($modSettings['defaultMaxTopics']) || $modSettings['defaultMaxTopics'] <= 0 || $modSettings['defaultMaxTopics'] > 999)
+			$modSettings['defaultMaxTopics'] = 20;
+		if (empty($modSettings['defaultMaxMessages']) || $modSettings['defaultMaxMessages'] <= 0 || $modSettings['defaultMaxMessages'] > 999)
+			$modSettings['defaultMaxMessages'] = 15;
+		if (empty($modSettings['defaultMaxMembers']) || $modSettings['defaultMaxMembers'] <= 0 || $modSettings['defaultMaxMembers'] > 999)
+			$modSettings['defaultMaxMembers'] = 30;
+
+		if (!empty($modSettings['cache_enable']))
+			cache_put_data('modSettings', $modSettings, 90);
+	}
+
+	// UTF-8 in regular expressions is unsupported on PHP(win) versions < 4.2.3.
+	$utf8 = (empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set']) === 'UTF-8' && (strpos(strtolower(PHP_OS), 'win') === false || @version_compare(PHP_VERSION, '4.2.3') != -1);
+
+	// Set a list of common functions.
+	$ent_list = empty($modSettings['disableEntityCheck']) ? '&(#\d{1,7}|quot|amp|lt|gt|nbsp);' : '&(#021|quot|amp|lt|gt|nbsp);';
+	$ent_check = empty($modSettings['disableEntityCheck']) ? array('preg_replace(\'~(&#(\d{1,7}|x[0-9a-fA-F]{1,6});)~e\', \'$smcFunc[\\\'entity_fix\\\'](\\\'\\2\\\')\', ', ')') : array('', '');
+
+	// Preg_replace can handle complex characters only for higher PHP versions.
+	$space_chars = $utf8 ? (@version_compare(PHP_VERSION, '4.3.3') != -1 ? '\x{A0}\x{AD}\x{2000}-\x{200F}\x{201F}\x{202F}\x{3000}\x{FEFF}' : "\xC2\xA0\xC2\xAD\xE2\x80\x80-\xE2\x80\x8F\xE2\x80\x9F\xE2\x80\xAF\xE2\x80\x9F\xE3\x80\x80\xEF\xBB\xBF") : '\x00-\x08\x0B\x0C\x0E-\x19\xA0';
+
+	$smcFunc += array(
+		'entity_fix' => create_function('$string', '
+			$num = substr($string, 0, 1) === \'x\' ? hexdec(substr($string, 1)) : (int) $string;
+			return $num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202E ? \'\' : \'&#\' . $num . \';\';'),
+		'htmlspecialchars' => create_function('$string, $quote_style = ENT_COMPAT, $charset = \'ISO-8859-1\'', '
+			global $smcFunc;
+			return ' . strtr($ent_check[0], array('&' => '&amp;')) . 'htmlspecialchars($string, $quote_style, ' . ($utf8 ? '\'UTF-8\'' : '$charset') . ')' . $ent_check[1] . ';'),
+		'htmltrim' => create_function('$string', '
+			global $smcFunc;
+			return preg_replace(\'~^(?:[ \t\n\r\x0B\x00' . $space_chars . ']|&nbsp;)+|(?:[ \t\n\r\x0B\x00' . $space_chars . ']|&nbsp;)+$~' . ($utf8 ? 'u' : '') . '\', \'\', ' . implode('$string', $ent_check) . ');'),
+		'strlen' => create_function('$string', '
+			global $smcFunc;
+			return strlen(preg_replace(\'~' . $ent_list . ($utf8 ? '|.~u' : '~') . '\', \'_\', ' . implode('$string', $ent_check) . '));'),
+		'strpos' => create_function('$haystack, $needle, $offset = 0', '
+			global $smcFunc;
+			$haystack_arr = preg_split(\'~(&#' . (empty($modSettings['disableEntityCheck']) ? '\d{1,7}' : '021') . ';|&quot;|&amp;|&lt;|&gt;|&nbsp;|.)~' . ($utf8 ? 'u' : '') . '\', ' . implode('$haystack', $ent_check) . ', -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
+			$haystack_size = count($haystack_arr);
+			if (strlen($needle) === 1)
+			{
+				$result = array_search($needle, array_slice($haystack_arr, $offset));
+				return is_int($result) ? $result + $offset : false;
+			}
+			else
+			{
+				$needle_arr = preg_split(\'~(&#' . (empty($modSettings['disableEntityCheck']) ? '\d{1,7}' : '021') . ';|&quot;|&amp;|&lt;|&gt;|&nbsp;|.)~' . ($utf8 ? 'u' : '') . '\',  ' . implode('$needle', $ent_check) . ', -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
+				$needle_size = count($needle_arr);
+
+				$result = array_search($needle_arr[0], array_slice($haystack_arr, $offset));
+				while (is_int($result))
+				{
+					$offset += $result;
+					if (array_slice($haystack_arr, $offset, $needle_size) === $needle_arr)
+						return $offset;
+					$result = array_search($needle_arr[0], array_slice($haystack_arr, ++$offset));
+				}
+				return false;
+			}'),
+		'substr' => create_function('$string, $start, $length = null', '
+			global $smcFunc;
+			$ent_arr = preg_split(\'~(&#' . (empty($modSettings['disableEntityCheck']) ? '\d{1,7}' : '021') . ';|&quot;|&amp;|&lt;|&gt;|&nbsp;|.)~' . ($utf8 ? 'u' : '') . '\', ' . implode('$string', $ent_check) . ', -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
+			return $length === null ? implode(\'\', array_slice($ent_arr, $start)) : implode(\'\', array_slice($ent_arr, $start, $length));'),
+		'strtolower' => $utf8 ? (function_exists('mb_strtolower') ? create_function('$string', '
+			return mb_strtolower($string, \'UTF-8\');') : create_function('$string', '
+			global $sourcedir;
+			require_once($sourcedir . \'/Subs-Charset.php\');
+			return utf8_strtolower($string);')) : 'strtolower',
+		'strtoupper' => $utf8 ? (function_exists('mb_strtoupper') ? create_function('$string', '
+			return mb_strtoupper($string, \'UTF-8\');') : create_function('$string', '
+			global $sourcedir;
+			require_once($sourcedir . \'/Subs-Charset.php\');
+			return utf8_strtoupper($string);')) : 'strtoupper',
+		'truncate' => create_function('$string, $length', (empty($modSettings['disableEntityCheck']) ? '
+			global $smcFunc;
+			$string = ' . implode('$string', $ent_check) . ';' : '') . '
+			preg_match(\'~^(' . $ent_list . '|.){\' . $smcFunc[\'strlen\'](substr($string, 0, $length)) . \'}~'.  ($utf8 ? 'u' : '') . '\', $string, $matches);
+			$string = $matches[0];
+			while (strlen($string) > $length)
+				$string = preg_replace(\'~(?:' . $ent_list . '|.)$~'.  ($utf8 ? 'u' : '') . '\', \'\', $string);
+			return $string;'),
+		'ucfirst' => $utf8 ? create_function('$string', '
+			global $smcFunc;
+			return $smcFunc[\'strtoupper\']($smcFunc[\'substr\']($string, 0, 1)) . $smcFunc[\'substr\']($string, 1);') : 'ucfirst',
+		'ucwords' => $utf8 ? create_function('$string', '
+			global $smcFunc;
+			$words = preg_split(\'~([\s\r\n\t]+)~\', $string, -1, PREG_SPLIT_DELIM_CAPTURE);
+			for ($i = 0, $n = count($words); $i < $n; $i += 2)
+				$words[$i] = $smcFunc[\'ucfirst\']($words[$i]);
+			return implode(\'\', $words);') : 'ucwords',
+	);
+
+	// Setting the timezone is a requirement for some functions in PHP >= 5.1.
+	if (isset($modSettings['default_timezone']) && function_exists('date_default_timezone_set'))
+		date_default_timezone_set($modSettings['default_timezone']);
+
+	// Check the load averages?
+	if (!empty($modSettings['loadavg_enable']))
+	{
+		if (($modSettings['load_average'] = cache_get_data('loadavg', 90)) == null)
+		{
+			$modSettings['load_average'] = @file_get_contents('/proc/loadavg');
+			if (!empty($modSettings['load_average']) && preg_match('~^([^ ]+?) ([^ ]+?) ([^ ]+)~', $modSettings['load_average'], $matches) != 0)
+				$modSettings['load_average'] = (float) $matches[1];
+			elseif (($modSettings['load_average'] = @`uptime`) != null && preg_match('~load average[s]?: (\d+\.\d+), (\d+\.\d+), (\d+\.\d+)~i', $modSettings['load_average'], $matches) != 0)
+				$modSettings['load_average'] = (float) $matches[1];
+			else
+				unset($modSettings['load_average']);
+
+			if (!empty($modSettings['load_average']))
+				cache_put_data('loadavg', $modSettings['load_average'], 90);
+		}
+
+		if (!empty($modSettings['loadavg_forum']) && !empty($modSettings['load_average']) && $modSettings['load_average'] >= $modSettings['loadavg_forum'])
+			db_fatal_error(true);
+	}
+
+	// Is post moderation alive and well?
+	$modSettings['postmod_active'] = isset($modSettings['admin_features']) ? in_array('pm', explode(',', $modSettings['admin_features'])) : true;
+
+	// Integration is cool.
+	if (defined('SMF_INTEGRATION_SETTINGS'))
+	{
+		$integration_settings = unserialize(SMF_INTEGRATION_SETTINGS);
+		foreach ($integration_settings as $hook => $function)
+			add_integration_function($hook, $function, false);
+	}
+
+	// Any files to pre include?
+	if (!empty($modSettings['integrate_pre_include']))
+	{
+		$pre_includes = explode(',', $modSettings['integrate_pre_include']);
+		foreach ($pre_includes as $include)
+		{
+			$include = strtr(trim($include), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir));
+			if (file_exists($include))
+				require_once($include);
+		}
+	}
+
+	// Call pre load integration functions.
+	call_integration_hook('integrate_pre_load');
+}
+
+// Load all the important user information...
+function loadUserSettings()
+{
+	global $modSettings, $user_settings, $sourcedir, $smcFunc;
+	global $cookiename, $user_info, $language;
+
+	// Check first the integration, then the cookie, and last the session.
+	if (count($integration_ids = call_integration_hook('integrate_verify_user')) > 0)
+	{
+		$id_member = 0;
+		foreach ($integration_ids as $integration_id)
+		{
+			$integration_id = (int) $integration_id;
+			if ($integration_id > 0)
+			{
+				$id_member = $integration_id;
+				$already_verified = true;
+				break;
+			}
+		}
+	}
+	else
+		$id_member = 0;
+
+	if (empty($id_member) && isset($_COOKIE[$cookiename]))
+	{
+		// Fix a security hole in PHP 4.3.9 and below...
+		if (preg_match('~^a:[34]:\{i:0;(i:\d{1,6}|s:[1-8]:"\d{1,8}");i:1;s:(0|40):"([a-fA-F0-9]{40})?";i:2;[id]:\d{1,14};(i:3;i:\d;)?\}$~i', $_COOKIE[$cookiename]) == 1)
+		{
+			list ($id_member, $password) = @unserialize($_COOKIE[$cookiename]);
+			$id_member = !empty($id_member) && strlen($password) > 0 ? (int) $id_member : 0;
+		}
+		else
+			$id_member = 0;
+	}
+	elseif (empty($id_member) && isset($_SESSION['login_' . $cookiename]) && ($_SESSION['USER_AGENT'] == $_SERVER['HTTP_USER_AGENT'] || !empty($modSettings['disableCheckUA'])))
+	{
+		// !!! Perhaps we can do some more checking on this, such as on the first octet of the IP?
+		list ($id_member, $password, $login_span) = @unserialize($_SESSION['login_' . $cookiename]);
+		$id_member = !empty($id_member) && strlen($password) == 40 && $login_span > time() ? (int) $id_member : 0;
+	}
+
+	// Only load this stuff if the user isn't a guest.
+	if ($id_member != 0)
+	{
+		// Is the member data cached?
+		if (empty($modSettings['cache_enable']) || $modSettings['cache_enable'] < 2 || ($user_settings = cache_get_data('user_settings-' . $id_member, 60)) == null)
+		{
+			$request = $smcFunc['db_query']('', '
+				SELECT mem.*, IFNULL(a.id_attach, 0) AS id_attach, a.filename, a.attachment_type
+				FROM {db_prefix}members AS mem
+					LEFT JOIN {db_prefix}attachments AS a ON (a.id_member = {int:id_member})
+				WHERE mem.id_member = {int:id_member}
+				LIMIT 1',
+				array(
+					'id_member' => $id_member,
+				)
+			);
+			$user_settings = $smcFunc['db_fetch_assoc']($request);
+			$smcFunc['db_free_result']($request);
+
+			if (!empty($modSettings['cache_enable']) && $modSettings['cache_enable'] >= 2)
+				cache_put_data('user_settings-' . $id_member, $user_settings, 60);
+		}
+
+		// Did we find 'im?  If not, junk it.
+		if (!empty($user_settings))
+		{
+			// As much as the password should be right, we can assume the integration set things up.
+			if (!empty($already_verified) && $already_verified === true)
+				$check = true;
+			// SHA-1 passwords should be 40 characters long.
+			elseif (strlen($password) == 40)
+				$check = sha1($user_settings['passwd'] . $user_settings['password_salt']) == $password;
+			else
+				$check = false;
+
+			// Wrong password or not activated - either way, you're going nowhere.
+			$id_member = $check && ($user_settings['is_activated'] == 1 || $user_settings['is_activated'] == 11) ? $user_settings['id_member'] : 0;
+		}
+		else
+			$id_member = 0;
+
+		// If we no longer have the member maybe they're being all hackey, stop brute force!
+		if (!$id_member)
+		{
+			require_once($sourcedir . '/LogInOut.php');
+			validatePasswordFlood(!empty($user_settings['id_member']) ? $user_settings['id_member'] : $id_member, !empty($user_settings['passwd_flood']) ? $user_settings['passwd_flood'] : false, $id_member != 0);
+		}
+	}
+
+	// Found 'im, let's set up the variables.
+	if ($id_member != 0)
+	{
+		// Let's not update the last visit time in these cases...
+		// 1. SSI doesn't count as visiting the forum.
+		// 2. RSS feeds and XMLHTTP requests don't count either.
+		// 3. If it was set within this session, no need to set it again.
+		// 4. New session, yet updated < five hours ago? Maybe cache can help.
+		if (SMF != 'SSI' && !isset($_REQUEST['xml']) && (!isset($_REQUEST['action']) || $_REQUEST['action'] != '.xml') && empty($_SESSION['id_msg_last_visit']) && (empty($modSettings['cache_enable']) || ($_SESSION['id_msg_last_visit'] = cache_get_data('user_last_visit-' . $id_member, 5 * 3600)) === null))
+		{
+			// Do a quick query to make sure this isn't a mistake.
+			$result = $smcFunc['db_query']('', '
+				SELECT poster_time
+				FROM {db_prefix}messages
+				WHERE id_msg = {int:id_msg}
+				LIMIT 1',
+				array(
+					'id_msg' => $user_settings['id_msg_last_visit'],
+				)
+			);
+			list ($visitTime) = $smcFunc['db_fetch_row']($result);
+			$smcFunc['db_free_result']($result);
+
+			$_SESSION['id_msg_last_visit'] = $user_settings['id_msg_last_visit'];
+
+			// If it was *at least* five hours ago...
+			if ($visitTime < time() - 5 * 3600)
+			{
+				updateMemberData($id_member, array('id_msg_last_visit' => (int) $modSettings['maxMsgID'], 'last_login' => time(), 'member_ip' => $_SERVER['REMOTE_ADDR'], 'member_ip2' => $_SERVER['BAN_CHECK_IP']));
+				$user_settings['last_login'] = time();
+
+				if (!empty($modSettings['cache_enable']) && $modSettings['cache_enable'] >= 2)
+					cache_put_data('user_settings-' . $id_member, $user_settings, 60);
+
+				if (!empty($modSettings['cache_enable']))
+					cache_put_data('user_last_visit-' . $id_member, $_SESSION['id_msg_last_visit'], 5 * 3600);
+			}
+		}
+		elseif (empty($_SESSION['id_msg_last_visit']))
+			$_SESSION['id_msg_last_visit'] = $user_settings['id_msg_last_visit'];
+
+		$username = $user_settings['member_name'];
+
+		if (empty($user_settings['additional_groups']))
+			$user_info = array(
+				'groups' => array($user_settings['id_group'], $user_settings['id_post_group'])
+			);
+		else
+			$user_info = array(
+				'groups' => array_merge(
+					array($user_settings['id_group'], $user_settings['id_post_group']),
+					explode(',', $user_settings['additional_groups'])
+				)
+			);
+
+		// Because history has proven that it is possible for groups to go bad - clean up in case.
+		foreach ($user_info['groups'] as $k => $v)
+			$user_info['groups'][$k] = (int) $v;
+
+		// This is a logged in user, so definitely not a spider.
+		$user_info['possibly_robot'] = false;
+	}
+	// If the user is a guest, initialize all the critical user settings.
+	else
+	{
+		// This is what a guest's variables should be.
+		$username = '';
+		$user_info = array('groups' => array(-1));
+		$user_settings = array();
+
+		if (isset($_COOKIE[$cookiename]))
+			$_COOKIE[$cookiename] = '';
+
+		// Do we perhaps think this is a search robot? Check every five minutes just in case...
+		if ((!empty($modSettings['spider_mode']) || !empty($modSettings['spider_group'])) && (!isset($_SESSION['robot_check']) || $_SESSION['robot_check'] < time() - 300))
+		{
+			require_once($sourcedir . '/ManageSearchEngines.php');
+			$user_info['possibly_robot'] = SpiderCheck();
+		}
+		elseif (!empty($modSettings['spider_mode']))
+			$user_info['possibly_robot'] = isset($_SESSION['id_robot']) ? $_SESSION['id_robot'] : 0;
+		// If we haven't turned on proper spider hunts then have a guess!
+		else
+		{
+			$ci_user_agent = strtolower($_SERVER['HTTP_USER_AGENT']);
+			$user_info['possibly_robot'] = (strpos($_SERVER['HTTP_USER_AGENT'], 'Mozilla') === false && strpos($_SERVER['HTTP_USER_AGENT'], 'Opera') === false) || strpos($ci_user_agent, 'googlebot') !== false || strpos($ci_user_agent, 'slurp') !== false || strpos($ci_user_agent, 'crawl') !== false;
+		}
+	}
+
+	// Set up the $user_info array.
+	$user_info += array(
+		'id' => $id_member,
+		'username' => $username,
+		'name' => isset($user_settings['real_name']) ? $user_settings['real_name'] : '',
+		'email' => isset($user_settings['email_address']) ? $user_settings['email_address'] : '',
+		'passwd' => isset($user_settings['passwd']) ? $user_settings['passwd'] : '',
+		'language' => empty($user_settings['lngfile']) || empty($modSettings['userLanguage']) ? $language : $user_settings['lngfile'],
+		'is_guest' => $id_member == 0,
+		'is_admin' => in_array(1, $user_info['groups']),
+		'theme' => empty($user_settings['id_theme']) ? 0 : $user_settings['id_theme'],
+		'last_login' => empty($user_settings['last_login']) ? 0 : $user_settings['last_login'],
+		'ip' => $_SERVER['REMOTE_ADDR'],
+		'ip2' => $_SERVER['BAN_CHECK_IP'],
+		'posts' => empty($user_settings['posts']) ? 0 : $user_settings['posts'],
+		'time_format' => empty($user_settings['time_format']) ? $modSettings['time_format'] : $user_settings['time_format'],
+		'time_offset' => empty($user_settings['time_offset']) ? 0 : $user_settings['time_offset'],
+		'avatar' => array(
+			'url' => isset($user_settings['avatar']) ? $user_settings['avatar'] : '',
+			'filename' => empty($user_settings['filename']) ? '' : $user_settings['filename'],
+			'custom_dir' => !empty($user_settings['attachment_type']) && $user_settings['attachment_type'] == 1,
+			'id_attach' => isset($user_settings['id_attach']) ? $user_settings['id_attach'] : 0
+		),
+		'smiley_set' => isset($user_settings['smiley_set']) ? $user_settings['smiley_set'] : '',
+		'messages' => empty($user_settings['instant_messages']) ? 0 : $user_settings['instant_messages'],
+		'unread_messages' => empty($user_settings['unread_messages']) ? 0 : $user_settings['unread_messages'],
+		'total_time_logged_in' => empty($user_settings['total_time_logged_in']) ? 0 : $user_settings['total_time_logged_in'],
+		'buddies' => !empty($modSettings['enable_buddylist']) && !empty($user_settings['buddy_list']) ? explode(',', $user_settings['buddy_list']) : array(),
+		'ignoreboards' => !empty($user_settings['ignore_boards']) && !empty($modSettings['allow_ignore_boards']) ? explode(',', $user_settings['ignore_boards']) : array(),
+		'ignoreusers' => !empty($user_settings['pm_ignore_list']) ? explode(',', $user_settings['pm_ignore_list']) : array(),
+		'warning' => isset($user_settings['warning']) ? $user_settings['warning'] : 0,
+		'permissions' => array(),
+	);
+	$user_info['groups'] = array_unique($user_info['groups']);
+	// Make sure that the last item in the ignore boards array is valid.  If the list was too long it could have an ending comma that could cause problems.
+	if (!empty($user_info['ignoreboards']) && empty($user_info['ignoreboards'][$tmp = count($user_info['ignoreboards']) - 1]))
+		unset($user_info['ignoreboards'][$tmp]);
+
+	// Do we have any languages to validate this?
+	if (!empty($modSettings['userLanguage']) && (!empty($_GET['language']) || !empty($_SESSION['language'])))
+		$languages = getLanguages();
+
+	// Allow the user to change their language if its valid.
+	if (!empty($modSettings['userLanguage']) && !empty($_GET['language']) && isset($languages[strtr($_GET['language'], './\\:', '____')]))
+	{
+		$user_info['language'] = strtr($_GET['language'], './\\:', '____');
+		$_SESSION['language'] = $user_info['language'];
+	}
+	elseif (!empty($modSettings['userLanguage']) && !empty($_SESSION['language']) && isset($languages[strtr($_SESSION['language'], './\\:', '____')]))
+		$user_info['language'] = strtr($_SESSION['language'], './\\:', '____');
+
+	// Just build this here, it makes it easier to change/use - administrators can see all boards.
+	if ($user_info['is_admin'])
+		$user_info['query_see_board'] = '1=1';
+	// Otherwise just the groups in $user_info['groups'].
+	else
+		$user_info['query_see_board'] = '(FIND_IN_SET(' . implode(', b.member_groups) != 0 OR FIND_IN_SET(', $user_info['groups']) . ', b.member_groups) != 0' . (isset($user_info['mod_cache']) ? ' OR ' . $user_info['mod_cache']['mq'] : '') . ')';
+
+	// Build the list of boards they WANT to see.
+	// This will take the place of query_see_boards in certain spots, so it better include the boards they can see also
+
+	// If they aren't ignoring any boards then they want to see all the boards they can see
+	if (empty($user_info['ignoreboards']))
+		$user_info['query_wanna_see_board'] = $user_info['query_see_board'];
+	// Ok I guess they don't want to see all the boards
+	else
+		$user_info['query_wanna_see_board'] = '(' . $user_info['query_see_board'] . ' AND b.id_board NOT IN (' . implode(',', $user_info['ignoreboards']) . '))';
+}
+
+// Check for moderators and see if they have access to the board.
+function loadBoard()
+{
+	global $txt, $scripturl, $context, $modSettings;
+	global $board_info, $board, $topic, $user_info, $smcFunc;
+
+	// Assume they are not a moderator.
+	$user_info['is_mod'] = false;
+	$context['user']['is_mod'] = &$user_info['is_mod'];
+
+	// Start the linktree off empty..
+	$context['linktree'] = array();
+
+	// Have they by chance specified a message id but nothing else?
+	if (empty($_REQUEST['action']) && empty($topic) && empty($board) && !empty($_REQUEST['msg']))
+	{
+		// Make sure the message id is really an int.
+		$_REQUEST['msg'] = (int) $_REQUEST['msg'];
+
+		// Looking through the message table can be slow, so try using the cache first.
+		if (($topic = cache_get_data('msg_topic-' . $_REQUEST['msg'], 120)) === NULL)
+		{
+			$request = $smcFunc['db_query']('', '
+				SELECT id_topic
+				FROM {db_prefix}messages
+				WHERE id_msg = {int:id_msg}
+				LIMIT 1',
+				array(
+					'id_msg' => $_REQUEST['msg'],
+				)
+			);
+
+			// So did it find anything?
+			if ($smcFunc['db_num_rows']($request))
+			{
+				list ($topic) = $smcFunc['db_fetch_row']($request);
+				$smcFunc['db_free_result']($request);
+				// Save save save.
+				cache_put_data('msg_topic-' . $_REQUEST['msg'], $topic, 120);
+			}
+		}
+
+		// Remember redirection is the key to avoiding fallout from your bosses.
+		if (!empty($topic))
+			redirectexit('topic=' . $topic . '.msg' . $_REQUEST['msg'] . '#msg' . $_REQUEST['msg']);
+		else
+		{
+			loadPermissions();
+			loadTheme();
+			fatal_lang_error('topic_gone', false);
+		}
+	}
+
+	// Load this board only if it is specified.
+	if (empty($board) && empty($topic))
+	{
+		$board_info = array('moderators' => array());
+		return;
+	}
+
+	if (!empty($modSettings['cache_enable']) && (empty($topic) || $modSettings['cache_enable'] >= 3))
+	{
+		// !!! SLOW?
+		if (!empty($topic))
+			$temp = cache_get_data('topic_board-' . $topic, 120);
+		else
+			$temp = cache_get_data('board-' . $board, 120);
+
+		if (!empty($temp))
+		{
+			$board_info = $temp;
+			$board = $board_info['id'];
+		}
+	}
+
+	if (empty($temp))
+	{
+		$request = $smcFunc['db_query']('', '
+			SELECT
+				c.id_cat, b.name AS bname, b.description, b.num_topics, b.member_groups,
+				b.id_parent, c.name AS cname, IFNULL(mem.id_member, 0) AS id_moderator,
+				mem.real_name' . (!empty($topic) ? ', b.id_board' : '') . ', b.child_level,
+				b.id_theme, b.override_theme, b.count_posts, b.id_profile, b.redirect,
+				b.unapproved_topics, b.unapproved_posts' . (!empty($topic) ? ', t.approved, t.id_member_started' : '') . '
+			FROM {db_prefix}boards AS b' . (!empty($topic) ? '
+				INNER JOIN {db_prefix}topics AS t ON (t.id_topic = {int:current_topic})' : '') . '
+				LEFT JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat)
+				LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = {raw:board_link})
+				LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = mods.id_member)
+			WHERE b.id_board = {raw:board_link}',
+			array(
+				'current_topic' => $topic,
+				'board_link' => empty($topic) ? $smcFunc['db_quote']('{int:current_board}', array('current_board' => $board)) : 't.id_board',
+			)
+		);
+		// If there aren't any, skip.
+		if ($smcFunc['db_num_rows']($request) > 0)
+		{
+			$row = $smcFunc['db_fetch_assoc']($request);
+
+			// Set the current board.
+			if (!empty($row['id_board']))
+				$board = $row['id_board'];
+
+			// Basic operating information. (globals... :/)
+			$board_info = array(
+				'id' => $board,
+				'moderators' => array(),
+				'cat' => array(
+					'id' => $row['id_cat'],
+					'name' => $row['cname']
+				),
+				'name' => $row['bname'],
+				'description' => $row['description'],
+				'num_topics' => $row['num_topics'],
+				'unapproved_topics' => $row['unapproved_topics'],
+				'unapproved_posts' => $row['unapproved_posts'],
+				'unapproved_user_topics' => 0,
+				'parent_boards' => getBoardParents($row['id_parent']),
+				'parent' => $row['id_parent'],
+				'child_level' => $row['child_level'],
+				'theme' => $row['id_theme'],
+				'override_theme' => !empty($row['override_theme']),
+				'profile' => $row['id_profile'],
+				'redirect' => $row['redirect'],
+				'posts_count' => empty($row['count_posts']),
+				'cur_topic_approved' => empty($topic) || $row['approved'],
+				'cur_topic_starter' => empty($topic) ? 0 : $row['id_member_started'],
+			);
+
+			// Load the membergroups allowed, and check permissions.
+			$board_info['groups'] = $row['member_groups'] == '' ? array() : explode(',', $row['member_groups']);
+
+			do
+			{
+				if (!empty($row['id_moderator']))
+					$board_info['moderators'][$row['id_moderator']] = array(
+						'id' => $row['id_moderator'],
+						'name' => $row['real_name'],
+						'href' => $scripturl . '?action=profile;u=' . $row['id_moderator'],
+						'link' => '<a href="' . $scripturl . '?action=profile;u=' . $row['id_moderator'] . '">' . $row['real_name'] . '</a>'
+					);
+			}
+			while ($row = $smcFunc['db_fetch_assoc']($request));
+
+			// If the board only contains unapproved posts and the user isn't an approver then they can't see any topics.
+			// If that is the case do an additional check to see if they have any topics waiting to be approved.
+			if ($board_info['num_topics'] == 0 && $modSettings['postmod_active'] && !allowedTo('approve_posts'))
+			{
+				$smcFunc['db_free_result']($request); // Free the previous result
+
+				$request = $smcFunc['db_query']('', '
+					SELECT COUNT(id_topic)
+					FROM {db_prefix}topics
+					WHERE id_member_started={int:id_member}
+						AND approved = {int:unapproved}
+						AND id_board = {int:board}',
+					array(
+						'id_member' => $user_info['id'],
+						'unapproved' => 0,
+						'board' => $board,
+					)
+				);
+
+				list ($board_info['unapproved_user_topics']) = $smcFunc['db_fetch_row']($request);
+			}
+
+			if (!empty($modSettings['cache_enable']) && (empty($topic) || $modSettings['cache_enable'] >= 3))
+			{
+				// !!! SLOW?
+				if (!empty($topic))
+					cache_put_data('topic_board-' . $topic, $board_info, 120);
+				cache_put_data('board-' . $board, $board_info, 120);
+			}
+		}
+		else
+		{
+			// Otherwise the topic is invalid, there are no moderators, etc.
+			$board_info = array(
+				'moderators' => array(),
+				'error' => 'exist'
+			);
+			$topic = null;
+			$board = 0;
+		}
+		$smcFunc['db_free_result']($request);
+	}
+
+	if (!empty($topic))
+		$_GET['board'] = (int) $board;
+
+	if (!empty($board))
+	{
+		// Now check if the user is a moderator.
+		$user_info['is_mod'] = isset($board_info['moderators'][$user_info['id']]);
+
+		if (count(array_intersect($user_info['groups'], $board_info['groups'])) == 0 && !$user_info['is_admin'])
+			$board_info['error'] = 'access';
+
+		// Build up the linktree.
+		$context['linktree'] = array_merge(
+			$context['linktree'],
+			array(array(
+				'url' => $scripturl . '#c' . $board_info['cat']['id'],
+				'name' => $board_info['cat']['name']
+			)),
+			array_reverse($board_info['parent_boards']),
+			array(array(
+				'url' => $scripturl . '?board=' . $board . '.0',
+				'name' => $board_info['name']
+			))
+		);
+	}
+
+	// Set the template contextual information.
+	$context['user']['is_mod'] = &$user_info['is_mod'];
+	$context['current_topic'] = $topic;
+	$context['current_board'] = $board;
+
+	// Hacker... you can't see this topic, I'll tell you that. (but moderators can!)
+	if (!empty($board_info['error']) && ($board_info['error'] != 'access' || !$user_info['is_mod']))
+	{
+		// The permissions and theme need loading, just to make sure everything goes smoothly.
+		loadPermissions();
+		loadTheme();
+
+		$_GET['board'] = '';
+		$_GET['topic'] = '';
+
+		// The linktree should not give the game away mate!
+		$context['linktree'] = array(
+			array(
+				'url' => $scripturl,
+				'name' => $context['forum_name_html_safe']
+			)
+		);
+
+		// If it's a prefetching agent or we're requesting an attachment.
+		if ((isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] == 'prefetch') || (!empty($_REQUEST['action']) && $_REQUEST['action'] === 'dlattach'))
+		{
+			ob_end_clean();
+			header('HTTP/1.1 403 Forbidden');
+			die;
+		}
+		elseif ($user_info['is_guest'])
+		{
+			loadLanguage('Errors');
+			is_not_guest($txt['topic_gone']);
+		}
+		else
+			fatal_lang_error('topic_gone', false);
+	}
+
+	if ($user_info['is_mod'])
+		$user_info['groups'][] = 3;
+}
+
+// Load this user's permissions.
+function loadPermissions()
+{
+	global $user_info, $board, $board_info, $modSettings, $smcFunc, $sourcedir;
+
+	if ($user_info['is_admin'])
+	{
+		banPermissions();
+		return;
+	}
+
+	if (!empty($modSettings['cache_enable']))
+	{
+		$cache_groups = $user_info['groups'];
+		asort($cache_groups);
+		$cache_groups = implode(',', $cache_groups);
+		// If it's a spider then cache it different.
+		if ($user_info['possibly_robot'])
+			$cache_groups .= '-spider';
+
+		if ($modSettings['cache_enable'] >= 2 && !empty($board) && ($temp = cache_get_data('permissions:' . $cache_groups . ':' . $board, 240)) != null && time() - 240 > $modSettings['settings_updated'])
+		{
+			list ($user_info['permissions']) = $temp;
+			banPermissions();
+
+			return;
+		}
+		elseif (($temp = cache_get_data('permissions:' . $cache_groups, 240)) != null && time() - 240 > $modSettings['settings_updated'])
+			list ($user_info['permissions'], $removals) = $temp;
+	}
+
+	// If it is detected as a robot, and we are restricting permissions as a special group - then implement this.
+	$spider_restrict = $user_info['possibly_robot'] && !empty($modSettings['spider_group']) ? ' OR (id_group = {int:spider_group} && add_deny = 0)' : '';
+
+	if (empty($user_info['permissions']))
+	{
+		// Get the general permissions.
+		$request = $smcFunc['db_query']('', '
+			SELECT permission, add_deny
+			FROM {db_prefix}permissions
+			WHERE id_group IN ({array_int:member_groups})
+				' . $spider_restrict,
+			array(
+				'member_groups' => $user_info['groups'],
+				'spider_group' => !empty($modSettings['spider_group']) ? $modSettings['spider_group'] : 0,
+			)
+		);
+		$removals = array();
+		while ($row = $smcFunc['db_fetch_assoc']($request))
+		{
+			if (empty($row['add_deny']))
+				$removals[] = $row['permission'];
+			else
+				$user_info['permissions'][] = $row['permission'];
+		}
+		$smcFunc['db_free_result']($request);
+
+		if (isset($cache_groups))
+			cache_put_data('permissions:' . $cache_groups, array($user_info['permissions'], $removals), 240);
+	}
+
+	// Get the board permissions.
+	if (!empty($board))
+	{
+		// Make sure the board (if any) has been loaded by loadBoard().
+		if (!isset($board_info['profile']))
+			fatal_lang_error('no_board');
+
+		$request = $smcFunc['db_query']('', '
+			SELECT permission, add_deny
+			FROM {db_prefix}board_permissions
+			WHERE (id_group IN ({array_int:member_groups})
+				' . $spider_restrict . ')
+				AND id_profile = {int:id_profile}',
+			array(
+				'member_groups' => $user_info['groups'],
+				'id_profile' => $board_info['profile'],
+				'spider_group' => !empty($modSettings['spider_group']) ? $modSettings['spider_group'] : 0,
+			)
+		);
+		while ($row = $smcFunc['db_fetch_assoc']($request))
+		{
+			if (empty($row['add_deny']))
+				$removals[] = $row['permission'];
+			else
+				$user_info['permissions'][] = $row['permission'];
+		}
+		$smcFunc['db_free_result']($request);
+	}
+
+	// Remove all the permissions they shouldn't have ;).
+	if (!empty($modSettings['permission_enable_deny']))
+		$user_info['permissions'] = array_diff($user_info['permissions'], $removals);
+
+	if (isset($cache_groups) && !empty($board) && $modSettings['cache_enable'] >= 2)
+		cache_put_data('permissions:' . $cache_groups . ':' . $board, array($user_info['permissions'], null), 240);
+
+	// Banned?  Watch, don't touch..
+	banPermissions();
+
+	// Load the mod cache so we can know what additional boards they should see, but no sense in doing it for guests
+	if (!$user_info['is_guest'])
+	{
+		if (!isset($_SESSION['mc']) || $_SESSION['mc']['time'] <= $modSettings['settings_updated'])
+		{
+			require_once($sourcedir . '/Subs-Auth.php');
+			rebuildModCache();
+		}
+		else
+			$user_info['mod_cache'] = $_SESSION['mc'];
+	}
+}
+
+// Loads an array of users' data by ID or member_name.
+function loadMemberData($users, $is_name = false, $set = 'normal')
+{
+	global $user_profile, $modSettings, $board_info, $smcFunc;
+
+	// Can't just look for no users :P.
+	if (empty($users))
+		return false;
+
+	// Make sure it's an array.
+	$users = !is_array($users) ? array($users) : array_unique($users);
+	$loaded_ids = array();
+
+	if (!$is_name && !empty($modSettings['cache_enable']) && $modSettings['cache_enable'] >= 3)
+	{
+		$users = array_values($users);
+		for ($i = 0, $n = count($users); $i < $n; $i++)
+		{
+			$data = cache_get_data('member_data-' . $set . '-' . $users[$i], 240);
+			if ($data == null)
+				continue;
+
+			$loaded_ids[] = $data['id_member'];
+			$user_profile[$data['id_member']] = $data;
+			unset($users[$i]);
+		}
+	}
+
+	if ($set == 'normal')
+	{
+		$select_columns = '
+			IFNULL(lo.log_time, 0) AS is_online, IFNULL(a.id_attach, 0) AS id_attach, a.filename, a.attachment_type,
+			mem.signature, mem.personal_text, mem.location, mem.gender, mem.avatar, mem.id_member, mem.member_name,
+			mem.real_name, mem.email_address, mem.hide_email, mem.date_registered, mem.website_title, mem.website_url,
+			mem.birthdate, mem.member_ip, mem.member_ip2, mem.icq, mem.aim, mem.yim, mem.msn, mem.posts, mem.last_login,
+			mem.karma_good, mem.id_post_group, mem.karma_bad, mem.lngfile, mem.id_group, mem.time_offset, mem.show_online,
+			mem.buddy_list, mg.online_color AS member_group_color, IFNULL(mg.group_name, {string:blank_string}) AS member_group,
+			pg.online_color AS post_group_color, IFNULL(pg.group_name, {string:blank_string}) AS post_group, mem.is_activated, mem.warning,
+			CASE WHEN mem.id_group = 0 OR mg.stars = {string:blank_string} THEN pg.stars ELSE mg.stars END AS stars' . (!empty($modSettings['titlesEnable']) ? ',
+			mem.usertitle' : '');
+		$select_tables = '
+			LEFT JOIN {db_prefix}log_online AS lo ON (lo.id_member = mem.id_member)
+			LEFT JOIN {db_prefix}attachments AS a ON (a.id_member = mem.id_member)
+			LEFT JOIN {db_prefix}membergroups AS pg ON (pg.id_group = mem.id_post_group)
+			LEFT JOIN {db_prefix}membergroups AS mg ON (mg.id_group = mem.id_group)';
+	}
+	elseif ($set == 'profile')
+	{
+		$select_columns = '
+			IFNULL(lo.log_time, 0) AS is_online, IFNULL(a.id_attach, 0) AS id_attach, a.filename, a.attachment_type,
+			mem.signature, mem.personal_text, mem.location, mem.gender, mem.avatar, mem.id_member, mem.member_name,
+			mem.real_name, mem.email_address, mem.hide_email, mem.date_registered, mem.website_title, mem.website_url,
+			mem.openid_uri, mem.birthdate, mem.icq, mem.aim, mem.yim, mem.msn, mem.posts, mem.last_login, mem.karma_good,
+			mem.karma_bad, mem.member_ip, mem.member_ip2, mem.lngfile, mem.id_group, mem.id_theme, mem.buddy_list,
+			mem.pm_ignore_list, mem.pm_email_notify, mem.pm_receive_from, mem.time_offset' . (!empty($modSettings['titlesEnable']) ? ', mem.usertitle' : '') . ',
+			mem.time_format, mem.secret_question, mem.is_activated, mem.additional_groups, mem.smiley_set, mem.show_online,
+			mem.total_time_logged_in, mem.id_post_group, mem.notify_announcements, mem.notify_regularity, mem.notify_send_body,
+			mem.notify_types, lo.url, mg.online_color AS member_group_color, IFNULL(mg.group_name, {string:blank_string}) AS member_group,
+			pg.online_color AS post_group_color, IFNULL(pg.group_name, {string:blank_string}) AS post_group, mem.ignore_boards, mem.warning,
+			CASE WHEN mem.id_group = 0 OR mg.stars = {string:blank_string} THEN pg.stars ELSE mg.stars END AS stars, mem.password_salt, mem.pm_prefs';
+		$select_tables = '
+			LEFT JOIN {db_prefix}log_online AS lo ON (lo.id_member = mem.id_member)
+			LEFT JOIN {db_prefix}attachments AS a ON (a.id_member = mem.id_member)
+			LEFT JOIN {db_prefix}membergroups AS pg ON (pg.id_group = mem.id_post_group)
+			LEFT JOIN {db_prefix}membergroups AS mg ON (mg.id_group = mem.id_group)';
+	}
+	elseif ($set == 'minimal')
+	{
+		$select_columns = '
+			mem.id_member, mem.member_name, mem.real_name, mem.email_address, mem.hide_email, mem.date_registered,
+			mem.posts, mem.last_login, mem.member_ip, mem.member_ip2, mem.lngfile, mem.id_group';
+		$select_tables = '';
+	}
+	else
+		trigger_error('loadMemberData(): Invalid member data set \'' . $set . '\'', E_USER_WARNING);
+
+	if (!empty($users))
+	{
+		// Load the member's data.
+		$request = $smcFunc['db_query']('', '
+			SELECT' . $select_columns . '
+			FROM {db_prefix}members AS mem' . $select_tables . '
+			WHERE mem.' . ($is_name ? 'member_name' : 'id_member') . (count($users) == 1 ? ' = {' . ($is_name ? 'string' : 'int') . ':users}' : ' IN ({' . ($is_name ? 'array_string' : 'array_int') . ':users})'),
+			array(
+				'blank_string' => '',
+				'users' => count($users) == 1 ? current($users) : $users,
+			)
+		);
+		$new_loaded_ids = array();
+		while ($row = $smcFunc['db_fetch_assoc']($request))
+		{
+			$new_loaded_ids[] = $row['id_member'];
+			$loaded_ids[] = $row['id_member'];
+			$row['options'] = array();
+			$user_profile[$row['id_member']] = $row;
+		}
+		$smcFunc['db_free_result']($request);
+	}
+
+	if (!empty($new_loaded_ids) && $set !== 'minimal')
+	{
+		$request = $smcFunc['db_query']('', '
+			SELECT *
+			FROM {db_prefix}themes
+			WHERE id_member' . (count($new_loaded_ids) == 1 ? ' = {int:loaded_ids}' : ' IN ({array_int:loaded_ids})'),
+			array(
+				'loaded_ids' => count($new_loaded_ids) == 1 ? $new_loaded_ids[0] : $new_loaded_ids,
+			)
+		);
+		while ($row = $smcFunc['db_fetch_assoc']($request))
+			$user_profile[$row['id_member']]['options'][$row['variable']] = $row['value'];
+		$smcFunc['db_free_result']($request);
+	}
+
+	if (!empty($new_loaded_ids) && !empty($modSettings['cache_enable']) && $modSettings['cache_enable'] >= 3)
+	{
+		for ($i = 0, $n = count($new_loaded_ids); $i < $n; $i++)
+			cache_put_data('member_data-' . $set . '-' . $new_loaded_ids[$i], $user_profile[$new_loaded_ids[$i]], 240);
+	}
+
+	// Are we loading any moderators?  If so, fix their group data...
+	if (!empty($loaded_ids) && !empty($board_info['moderators']) && $set === 'normal' && count($temp_mods = array_intersect($loaded_ids, array_keys($board_info['moderators']))) !== 0)
+	{
+		if (($row = cache_get_data('moderator_group_info', 480)) == null)
+		{
+			$request = $smcFunc['db_query']('', '
+				SELECT group_name AS member_group, online_color AS member_group_color, stars
+				FROM {db_prefix}membergroups
+				WHERE id_group = {int:moderator_group}
+				LIMIT 1',
+				array(
+					'moderator_group' => 3,
+				)
+			);
+			$row = $smcFunc['db_fetch_assoc']($request);
+			$smcFunc['db_free_result']($request);
+
+			cache_put_data('moderator_group_info', $row, 480);
+		}
+
+		foreach ($temp_mods as $id)
+		{
+			// By popular demand, don't show admins or global moderators as moderators.
+			if ($user_profile[$id]['id_group'] != 1 && $user_profile[$id]['id_group'] != 2)
+				$user_profile[$id]['member_group'] = $row['member_group'];
+
+			// If the Moderator group has no color or stars, but their group does... don't overwrite.
+			if (!empty($row['stars']))
+				$user_profile[$id]['stars'] = $row['stars'];
+			if (!empty($row['member_group_color']))
+				$user_profile[$id]['member_group_color'] = $row['member_group_color'];
+		}
+	}
+
+	return empty($loaded_ids) ? false : $loaded_ids;
+}
+
+// Loads the user's basic values... meant for template/theme usage.
+function loadMemberContext($user, $display_custom_fields = false)
+{
+	global $memberContext, $user_profile, $txt, $scripturl, $user_info;
+	global $context, $modSettings, $board_info, $settings;
+	global $smcFunc;
+	static $dataLoaded = array();
+
+	// If this person's data is already loaded, skip it.
+	if (isset($dataLoaded[$user]))
+		return true;
+
+	// We can't load guests or members not loaded by loadMemberData()!
+	if ($user == 0)
+		return false;
+	if (!isset($user_profile[$user]))
+	{
+		trigger_error('loadMemberContext(): member id ' . $user . ' not previously loaded by loadMemberData()', E_USER_WARNING);
+		return false;
+	}
+
+	// Well, it's loaded now anyhow.
+	$dataLoaded[$user] = true;
+	$profile = $user_profile[$user];
+
+	// Censor everything.
+	censorText($profile['signature']);
+	censorText($profile['personal_text']);
+	censorText($profile['location']);
+
+	// Set things up to be used before hand.
+	$gendertxt = $profile['gender'] == 2 ? $txt['female'] : ($profile['gender'] == 1 ? $txt['male'] : '');
+	$profile['signature'] = str_replace(array("\n", "\r"), array('<br />', ''), $profile['signature']);
+	$profile['signature'] = parse_bbc($profile['signature'], true, 'sig' . $profile['id_member']);
+
+	$profile['is_online'] = (!empty($profile['show_online']) || allowedTo('moderate_forum')) && $profile['is_online'] > 0;
+	$profile['stars'] = empty($profile['stars']) ? array('', '') : explode('#', $profile['stars']);
+	// Setup the buddy status here (One whole in_array call saved :P)
+	$profile['buddy'] = in_array($profile['id_member'], $user_info['buddies']);
+	$buddy_list = !empty($profile['buddy_list']) ? explode(',', $profile['buddy_list']) : array();
+
+	// If we're always html resizing, assume it's too large.
+	if ($modSettings['avatar_action_too_large'] == 'option_html_resize' || $modSettings['avatar_action_too_large'] == 'option_js_resize')
+	{
+		$avatar_width = !empty($modSettings['avatar_max_width_external']) ? ' width="' . $modSettings['avatar_max_width_external'] . '"' : '';
+		$avatar_height = !empty($modSettings['avatar_max_height_external']) ? ' height="' . $modSettings['avatar_max_height_external'] . '"' : '';
+	}
+	else
+	{
+		$avatar_width = '';
+		$avatar_height = '';
+	}
+
+	// What a monstrous array...
+	$memberContext[$user] = array(
+		'username' => $profile['member_name'],
+		'name' => $profile['real_name'],
+		'id' => $profile['id_member'],
+		'is_buddy' => $profile['buddy'],
+		'is_reverse_buddy' => in_array($user_info['id'], $buddy_list),
+		'buddies' => $buddy_list,
+		'title' => !empty($modSettings['titlesEnable']) ? $profile['usertitle'] : '',
+		'href' => $scripturl . '?action=profile;u=' . $profile['id_member'],
+		'link' => '<a href="' . $scripturl . '?action=profile;u=' . $profile['id_member'] . '" title="' . $txt['profile_of'] . ' ' . $profile['real_name'] . '">' . $profile['real_name'] . '</a>',
+		'email' => $profile['email_address'],
+		'show_email' => showEmailAddress(!empty($profile['hide_email']), $profile['id_member']),
+		'registered' => empty($profile['date_registered']) ? $txt['not_applicable'] : timeformat($profile['date_registered']),
+		'registered_timestamp' => empty($profile['date_registered']) ? 0 : forum_time(true, $profile['date_registered']),
+		'blurb' => $profile['personal_text'],
+		'gender' => array(
+			'name' => $gendertxt,
+			'image' => !empty($profile['gender']) ? '<img class="gender" src="' . $settings['images_url'] . '/' . ($profile['gender'] == 1 ? 'Male' : 'Female') . '.gif" alt="' . $gendertxt . '" />' : ''
+		),
+		'website' => array(
+			'title' => $profile['website_title'],
+			'url' => $profile['website_url'],
+		),
+		'birth_date' => empty($profile['birthdate']) || $profile['birthdate'] === '0001-01-01' ? '0000-00-00' : (substr($profile['birthdate'], 0, 4) === '0004' ? '0000' . substr($profile['birthdate'], 4) : $profile['birthdate']),
+		'signature' => $profile['signature'],
+		'location' => $profile['location'],
+		'icq' => $profile['icq'] != '' && (empty($modSettings['guest_hideContacts']) || !$user_info['is_guest']) ? array(
+			'name' => $profile['icq'],
+			'href' => 'http://www.icq.com/whitepages/about_me.php?uin=' . $profile['icq'],
+			'link' => '<a class="icq new_win" href="http://www.icq.com/whitepages/about_me.php?uin=' . $profile['icq'] . '" target="_blank" title="' . $txt['icq_title'] . ' - ' . $profile['icq'] . '"><img src="http://status.icq.com/online.gif?img=5&amp;icq=' . $profile['icq'] . '" alt="' . $txt['icq_title'] . ' - ' . $profile['icq'] . '" width="18" height="18" /></a>',
+			'link_text' => '<a class="icq extern" href="http://www.icq.com/whitepages/about_me.php?uin=' . $profile['icq'] . '" title="' . $txt['icq_title'] . ' - ' . $profile['icq'] . '">' . $profile['icq'] . '</a>',
+		) : array('name' => '', 'add' => '', 'href' => '', 'link' => '', 'link_text' => ''),
+		'aim' => $profile['aim'] != '' && (empty($modSettings['guest_hideContacts']) || !$user_info['is_guest']) ? array(
+			'name' => $profile['aim'],
+			'href' => 'aim:goim?screenname=' . urlencode(strtr($profile['aim'], array(' ' => '%20'))) . '&amp;message=' . $txt['aim_default_message'],
+			'link' => '<a class="aim" href="aim:goim?screenname=' . urlencode(strtr($profile['aim'], array(' ' => '%20'))) . '&amp;message=' . $txt['aim_default_message'] . '" title="' . $txt['aim_title'] . ' - ' . $profile['aim'] . '"><img src="' . $settings['images_url'] . '/aim.gif" alt="' . $txt['aim_title'] . ' - ' . $profile['aim'] . '" /></a>',
+			'link_text' => '<a class="aim" href="aim:goim?screenname=' . urlencode(strtr($profile['aim'], array(' ' => '%20'))) . '&amp;message=' . $txt['aim_default_message'] . '" title="' . $txt['aim_title'] . ' - ' . $profile['aim'] . '">' . $profile['aim'] . '</a>'
+		) : array('name' => '', 'href' => '', 'link' => '', 'link_text' => ''),
+		'yim' => $profile['yim'] != '' && (empty($modSettings['guest_hideContacts']) || !$user_info['is_guest']) ? array(
+			'name' => $profile['yim'],
+			'href' => 'http://edit.yahoo.com/config/send_webmesg?.target=' . urlencode($profile['yim']),
+			'link' => '<a class="yim" href="http://edit.yahoo.com/config/send_webmesg?.target=' . urlencode($profile['yim']) . '" title="' . $txt['yim_title'] . ' - ' . $profile['yim'] . '"><img src="http://opi.yahoo.com/online?u=' . urlencode($profile['yim']) . '&amp;m=g&amp;t=0" alt="' . $txt['yim_title'] . ' - ' . $profile['yim'] . '" /></a>',
+			'link_text' => '<a class="yim" href="http://edit.yahoo.com/config/send_webmesg?.target=' . urlencode($profile['yim']) . '" title="' . $txt['yim_title'] . ' - ' . $profile['yim'] . '">' . $profile['yim'] . '</a>'
+		) : array('name' => '', 'href' => '', 'link' => '', 'link_text' => ''),
+		'msn' => $profile['msn'] !='' && (empty($modSettings['guest_hideContacts']) || !$user_info['is_guest']) ? array(
+			'name' => $profile['msn'],
+			'href' => 'http://members.msn.com/' . $profile['msn'],
+			'link' => '<a class="msn new_win" href="http://members.msn.com/' . $profile['msn'] . '" title="' . $txt['msn_title'] . ' - ' . $profile['msn'] . '"><img src="' . $settings['images_url'] . '/msntalk.gif" alt="' . $txt['msn_title'] . ' - ' . $profile['msn'] . '" /></a>',
+			'link_text' => '<a class="msn new_win" href="http://members.msn.com/' . $profile['msn'] . '" title="' . $txt['msn_title'] . ' - ' . $profile['msn'] . '">' . $profile['msn'] . '</a>'
+		) : array('name' => '', 'href' => '', 'link' => '', 'link_text' => ''),
+		'real_posts' => $profile['posts'],
+		'posts' => $profile['posts'] > 500000 ? $txt['geek'] : comma_format($profile['posts']),
+		'avatar' => array(
+			'name' => $profile['avatar'],
+			'image' => $profile['avatar'] == '' ? ($profile['id_attach'] > 0 ? '<img class="avatar" src="' . (empty($profile['attachment_type']) ? $scripturl . '?action=dlattach;attach=' . $profile['id_attach'] . ';type=avatar' : $modSettings['custom_avatar_url'] . '/' . $profile['filename']) . '" alt="" />' : '') : (stristr($profile['avatar'], 'http://') ? '<img class="avatar" src="' . $profile['avatar'] . '"' . $avatar_width . $avatar_height . ' alt="" />' : '<img class="avatar" src="' . $modSettings['avatar_url'] . '/' . htmlspecialchars($profile['avatar']) . '" alt="" />'),
+			'href' => $profile['avatar'] == '' ? ($profile['id_attach'] > 0 ? (empty($profile['attachment_type']) ? $scripturl . '?action=dlattach;attach=' . $profile['id_attach'] . ';type=avatar' : $modSettings['custom_avatar_url'] . '/' . $profile['filename']) : '') : (stristr($profile['avatar'], 'http://') ? $profile['avatar'] : $modSettings['avatar_url'] . '/' . $profile['avatar']),
+			'url' => $profile['avatar'] == '' ? '' : (stristr($profile['avatar'], 'http://') ? $profile['avatar'] : $modSettings['avatar_url'] . '/' . $profile['avatar'])
+		),
+		'last_login' => empty($profile['last_login']) ? $txt['never'] : timeformat($profile['last_login']),
+		'last_login_timestamp' => empty($profile['last_login']) ? 0 : forum_time(0, $profile['last_login']),
+		'karma' => array(
+			'good' => $profile['karma_good'],
+			'bad' => $profile['karma_bad'],
+			'allow' => !$user_info['is_guest'] && !empty($modSettings['karmaMode']) && $user_info['id'] != $user && allowedTo('karma_edit') &&
+			($user_info['posts'] >= $modSettings['karmaMinPosts'] || $user_info['is_admin']),
+		),
+		'ip' => htmlspecialchars($profile['member_ip']),
+		'ip2' => htmlspecialchars($profile['member_ip2']),
+		'online' => array(
+			'is_online' => $profile['is_online'],
+			'text' => $txt[$profile['is_online'] ? 'online' : 'offline'],
+			'href' => $scripturl . '?action=pm;sa=send;u=' . $profile['id_member'],
+			'link' => '<a href="' . $scripturl . '?action=pm;sa=send;u=' . $profile['id_member'] . '">' . $txt[$profile['is_online'] ? 'online' : 'offline'] . '</a>',
+			'image_href' => $settings['images_url'] . '/' . ($profile['buddy'] ? 'buddy_' : '') . ($profile['is_online'] ? 'useron' : 'useroff') . '.gif',
+			'label' => $txt[$profile['is_online'] ? 'online' : 'offline']
+		),
+		'language' => $smcFunc['ucwords'](strtr($profile['lngfile'], array('_' => ' ', '-utf8' => ''))),
+		'is_activated' => isset($profile['is_activated']) ? $profile['is_activated'] : 1,
+		'is_banned' => isset($profile['is_activated']) ? $profile['is_activated'] >= 10 : 0,
+		'options' => $profile['options'],
+		'is_guest' => false,
+		'group' => $profile['member_group'],
+		'group_color' => $profile['member_group_color'],
+		'group_id' => $profile['id_group'],
+		'post_group' => $profile['post_group'],
+		'post_group_color' => $profile['post_group_color'],
+		'group_stars' => str_repeat('<img src="' . str_replace('$language', $context['user']['language'], isset($profile['stars'][1]) ? $settings['images_url'] . '/' . $profile['stars'][1] : '') . '" alt="*" />', empty($profile['stars'][0]) || empty($profile['stars'][1]) ? 0 : $profile['stars'][0]),
+		'warning' => $profile['warning'],
+		'warning_status' => !empty($modSettings['warning_mute']) && $modSettings['warning_mute'] <= $profile['warning'] ? 'mute' : (!empty($modSettings['warning_moderate']) && $modSettings['warning_moderate'] <= $profile['warning'] ? 'moderate' : (!empty($modSettings['warning_watch']) && $modSettings['warning_watch'] <= $profile['warning'] ? 'watch' : (''))),
+		'local_time' => timeformat(time() + ($profile['time_offset'] - $user_info['time_offset']) * 3600, false),
+	);
+
+	// First do a quick run through to make sure there is something to be shown.
+	$memberContext[$user]['has_messenger'] = false;
+	foreach (array('icq', 'msn', 'aim', 'yim') as $messenger)
+	{
+		if (!isset($context['disabled_fields'][$messenger]) && !empty($memberContext[$user][$messenger]['link']))
+		{
+			$memberContext[$user]['has_messenger'] = true;
+			break;
+		}
+	}
+
+	// Are we also loading the members custom fields into context?
+	if ($display_custom_fields && !empty($modSettings['displayFields']))
+	{
+		$memberContext[$user]['custom_fields'] = array();
+		if (!isset($context['display_fields']))
+			$context['display_fields'] = unserialize($modSettings['displayFields']);
+
+		foreach ($context['display_fields'] as $custom)
+		{
+			if (empty($custom['title']) || empty($profile['options'][$custom['colname']]))
+				continue;
+
+			$value = $profile['options'][$custom['colname']];
+
+			// BBC?
+			if ($custom['bbc'])
+				$value = parse_bbc($value);
+			// ... or checkbox?
+			elseif (isset($custom['type']) && $custom['type'] == 'check')
+				$value = $value ? $txt['yes'] : $txt['no'];
+
+			// Enclosing the user input within some other text?
+			if (!empty($custom['enclose']))
+				$value = strtr($custom['enclose'], array(
+					'{SCRIPTURL}' => $scripturl,
+					'{IMAGES_URL}' => $settings['images_url'],
+					'{DEFAULT_IMAGES_URL}' => $settings['default_images_url'],
+					'{INPUT}' => $value,
+				));
+
+			$memberContext[$user]['custom_fields'][] = array(
+				'title' => $custom['title'],
+				'colname' => $custom['colname'],
+				'value' => $value,
+				'placement' => !empty($custom['placement']) ? $custom['placement'] : 0,
+			);
+		}
+	}
+
+	return true;
+}
+
+function detectBrowser()
+{
+	global $context, $user_info;
+
+	// The following determines the user agent (browser) as best it can.
+	$context['browser'] = array(
+		'is_opera' => strpos($_SERVER['HTTP_USER_AGENT'], 'Opera') !== false,
+		'is_opera6' => strpos($_SERVER['HTTP_USER_AGENT'], 'Opera 6') !== false,
+		'is_opera7' => strpos($_SERVER['HTTP_USER_AGENT'], 'Opera 7') !== false || strpos($_SERVER['HTTP_USER_AGENT'], 'Opera/7') !== false,
+		'is_opera8' => strpos($_SERVER['HTTP_USER_AGENT'], 'Opera 8') !== false || strpos($_SERVER['HTTP_USER_AGENT'], 'Opera/8') !== false,
+		'is_opera9' => preg_match('~Opera[ /]9(?!\\.[89])~', $_SERVER['HTTP_USER_AGENT']) === 1,
+		'is_opera10' => preg_match('~Opera[ /]10\\.~', $_SERVER['HTTP_USER_AGENT']) === 1 || (preg_match('~Opera[ /]9\\.[89]~', $_SERVER['HTTP_USER_AGENT']) === 1 && preg_match('~Version/1[0-9]\\.~', $_SERVER['HTTP_USER_AGENT']) === 1),
+		'is_ie4' => strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE 4') !== false && strpos($_SERVER['HTTP_USER_AGENT'], 'WebTV') === false,
+		'is_webkit' => strpos($_SERVER['HTTP_USER_AGENT'], 'AppleWebKit') !== false,
+		'is_mac_ie' => strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE 5.') !== false && strpos($_SERVER['HTTP_USER_AGENT'], 'Mac') !== false,
+		'is_web_tv' => strpos($_SERVER['HTTP_USER_AGENT'], 'WebTV') !== false,
+		'is_konqueror' => strpos($_SERVER['HTTP_USER_AGENT'], 'Konqueror') !== false,
+		'is_firefox' => preg_match('~(?:Firefox|Ice[wW]easel|IceCat)/~', $_SERVER['HTTP_USER_AGENT']) === 1,
+		'is_firefox1' => preg_match('~(?:Firefox|Ice[wW]easel|IceCat)/1\\.~', $_SERVER['HTTP_USER_AGENT']) === 1,
+		'is_firefox2' => preg_match('~(?:Firefox|Ice[wW]easel|IceCat)/2\\.~', $_SERVER['HTTP_USER_AGENT']) === 1,
+		'is_firefox3' => preg_match('~(?:Firefox|Ice[wW]easel|IceCat|Shiretoko|Minefield)/3\\.~', $_SERVER['HTTP_USER_AGENT']) === 1,
+		'is_iphone' => strpos($_SERVER['HTTP_USER_AGENT'], 'iPhone') !== false || strpos($_SERVER['HTTP_USER_AGENT'], 'iPod') !== false,
+		'is_android' => strpos($_SERVER['HTTP_USER_AGENT'], 'Android') !== false,
+	);
+
+	$context['browser']['is_chrome'] = $context['browser']['is_webkit'] && strpos($_SERVER['HTTP_USER_AGENT'], 'Chrome') !== false;
+	$context['browser']['is_safari'] = !$context['browser']['is_chrome'] && strpos($_SERVER['HTTP_USER_AGENT'], 'Safari') !== false;
+	$context['browser']['is_gecko'] = strpos($_SERVER['HTTP_USER_AGENT'], 'Gecko') !== false && !$context['browser']['is_webkit'] && !$context['browser']['is_konqueror'];
+
+	// Internet Explorer 5 and 6 are often "emulated".
+	$context['browser']['is_ie8'] = !$context['browser']['is_opera'] && !$context['browser']['is_gecko'] && !$context['browser']['is_web_tv'] && strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE 8') !== false;
+	$context['browser']['is_ie7'] = !$context['browser']['is_opera'] && !$context['browser']['is_gecko'] && !$context['browser']['is_web_tv'] && strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE 7') !== false && !$context['browser']['is_ie8'];
+	$context['browser']['is_ie6'] = !$context['browser']['is_opera'] && !$context['browser']['is_gecko'] && !$context['browser']['is_web_tv'] && strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE 6') !== false && !$context['browser']['is_ie8'] && !$context['browser']['is_ie7'];
+	$context['browser']['is_ie5.5'] = !$context['browser']['is_opera'] && !$context['browser']['is_gecko'] && !$context['browser']['is_web_tv'] && strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE 5.5') !== false;
+	$context['browser']['is_ie5'] = !$context['browser']['is_opera'] && !$context['browser']['is_gecko'] && !$context['browser']['is_web_tv'] && strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE 5.0') !== false;
+
+	$context['browser']['is_ie'] = $context['browser']['is_ie4'] || $context['browser']['is_ie5'] || $context['browser']['is_ie5.5'] || $context['browser']['is_ie6'] || $context['browser']['is_ie7'] || $context['browser']['is_ie8'];
+	// Before IE8 we need to fix IE... lots!
+	$context['browser']['ie_standards_fix'] = !$context['browser']['is_ie8'];
+
+	$context['browser']['needs_size_fix'] = ($context['browser']['is_ie5'] || $context['browser']['is_ie5.5'] || $context['browser']['is_ie4'] || $context['browser']['is_opera6']) && strpos($_SERVER['HTTP_USER_AGENT'], 'Mac') === false;
+
+	// This isn't meant to be reliable, it's just meant to catch most bots to prevent PHPSESSID from showing up.
+	$context['browser']['possibly_robot'] = !empty($user_info['possibly_robot']);
+
+	// Robots shouldn't be logging in or registering.  So, they aren't a bot.  Better to be wrong than sorry (or people won't be able to log in!), anyway.
+	if ((isset($_REQUEST['action']) && in_array($_REQUEST['action'], array('login', 'login2', 'register'))) || !$user_info['is_guest'])
+		$context['browser']['possibly_robot'] = false;
+}
+
+// Load a theme, by ID.
+function loadTheme($id_theme = 0, $initialize = true)
+{
+	global $user_info, $user_settings, $board_info, $sc, $boarddir;
+	global $txt, $boardurl, $scripturl, $mbname, $modSettings, $language;
+	global $context, $settings, $options, $sourcedir, $ssi_theme, $smcFunc;
+
+	// The theme was specified by parameter.
+	if (!empty($id_theme))
+		$id_theme = (int) $id_theme;
+	// The theme was specified by REQUEST.
+	elseif (!empty($_REQUEST['theme']) && (!empty($modSettings['theme_allow']) || allowedTo('admin_forum')))
+	{
+		$id_theme = (int) $_REQUEST['theme'];
+		$_SESSION['id_theme'] = $id_theme;
+	}
+	// The theme was specified by REQUEST... previously.
+	elseif (!empty($_SESSION['id_theme']) && (!empty($modSettings['theme_allow']) || allowedTo('admin_forum')))
+		$id_theme = (int) $_SESSION['id_theme'];
+	// The theme is just the user's choice. (might use ?board=1;theme=0 to force board theme.)
+	elseif (!empty($user_info['theme']) && !isset($_REQUEST['theme']) && (!empty($modSettings['theme_allow']) || allowedTo('admin_forum')))
+		$id_theme = $user_info['theme'];
+	// The theme was specified by the board.
+	elseif (!empty($board_info['theme']))
+		$id_theme = $board_info['theme'];
+	// The theme is the forum's default.
+	else
+		$id_theme = $modSettings['theme_guests'];
+
+	// Verify the id_theme... no foul play.
+	// Always allow the board specific theme, if they are overriding.
+	if (!empty($board_info['theme']) && $board_info['override_theme'])
+		$id_theme = $board_info['theme'];
+	// If they have specified a particular theme to use with SSI allow it to be used.
+	elseif (!empty($ssi_theme) && $id_theme == $ssi_theme)
+		$id_theme = (int) $id_theme;
+	elseif (!empty($modSettings['knownThemes']) && !allowedTo('admin_forum'))
+	{
+		$themes = explode(',', $modSettings['knownThemes']);
+		if (!in_array($id_theme, $themes))
+			$id_theme = $modSettings['theme_guests'];
+		else
+			$id_theme = (int) $id_theme;
+	}
+	else
+		$id_theme = (int) $id_theme;
+
+	$member = empty($user_info['id']) ? -1 : $user_info['id'];
+
+	if (!empty($modSettings['cache_enable']) && $modSettings['cache_enable'] >= 2 && ($temp = cache_get_data('theme_settings-' . $id_theme . ':' . $member, 60)) != null && time() - 60 > $modSettings['settings_updated'])
+	{
+		$themeData = $temp;
+		$flag = true;
+	}
+	elseif (($temp = cache_get_data('theme_settings-' . $id_theme, 90)) != null && time() - 60 > $modSettings['settings_updated'])
+		$themeData = $temp + array($member => array());
+	else
+		$themeData = array(-1 => array(), 0 => array(), $member => array());
+
+	if (empty($flag))
+	{
+		// Load variables from the current or default theme, global or this user's.
+		$result = $smcFunc['db_query']('', '
+			SELECT variable, value, id_member, id_theme
+			FROM {db_prefix}themes
+			WHERE id_member' . (empty($themeData[0]) ? ' IN (-1, 0, {int:id_member})' : ' = {int:id_member}') . '
+				AND id_theme' . ($id_theme == 1 ? ' = {int:id_theme}' : ' IN ({int:id_theme}, 1)'),
+			array(
+				'id_theme' => $id_theme,
+				'id_member' => $member,
+			)
+		);
+		// Pick between $settings and $options depending on whose data it is.
+		while ($row = $smcFunc['db_fetch_assoc']($result))
+		{
+			// There are just things we shouldn't be able to change as members.
+			if ($row['id_member'] != 0 && in_array($row['variable'], array('actual_theme_url', 'actual_images_url', 'base_theme_dir', 'base_theme_url', 'default_images_url', 'default_theme_dir', 'default_theme_url', 'default_template', 'images_url', 'number_recent_posts', 'smiley_sets_default', 'theme_dir', 'theme_id', 'theme_layers', 'theme_templates', 'theme_url')))
+				continue;
+
+			// If this is the theme_dir of the default theme, store it.
+			if (in_array($row['variable'], array('theme_dir', 'theme_url', 'images_url')) && $row['id_theme'] == '1' && empty($row['id_member']))
+				$themeData[0]['default_' . $row['variable']] = $row['value'];
+
+			// If this isn't set yet, is a theme option, or is not the default theme..
+			if (!isset($themeData[$row['id_member']][$row['variable']]) || $row['id_theme'] != '1')
+				$themeData[$row['id_member']][$row['variable']] = substr($row['variable'], 0, 5) == 'show_' ? $row['value'] == '1' : $row['value'];
+		}
+		$smcFunc['db_free_result']($result);
+
+		if (!empty($themeData[-1]))
+			foreach ($themeData[-1] as $k => $v)
+			{
+				if (!isset($themeData[$member][$k]))
+					$themeData[$member][$k] = $v;
+			}
+
+		if (!empty($modSettings['cache_enable']) && $modSettings['cache_enable'] >= 2)
+			cache_put_data('theme_settings-' . $id_theme . ':' . $member, $themeData, 60);
+		// Only if we didn't already load that part of the cache...
+		elseif (!isset($temp))
+			cache_put_data('theme_settings-' . $id_theme, array(-1 => $themeData[-1], 0 => $themeData[0]), 90);
+	}
+
+	$settings = $themeData[0];
+	$options = $themeData[$member];
+
+	$settings['theme_id'] = $id_theme;
+
+	$settings['actual_theme_url'] = $settings['theme_url'];
+	$settings['actual_images_url'] = $settings['images_url'];
+	$settings['actual_theme_dir'] = $settings['theme_dir'];
+
+	$settings['template_dirs'] = array();
+	// This theme first.
+	$settings['template_dirs'][] = $settings['theme_dir'];
+
+	// Based on theme (if there is one).
+	if (!empty($settings['base_theme_dir']))
+		$settings['template_dirs'][] = $settings['base_theme_dir'];
+
+	// Lastly the default theme.
+	if ($settings['theme_dir'] != $settings['default_theme_dir'])
+		$settings['template_dirs'][] = $settings['default_theme_dir'];
+
+	if (!$initialize)
+		return;
+
+	// Check to see if they're accessing it from the wrong place.
+	if (isset($_SERVER['HTTP_HOST']) || isset($_SERVER['SERVER_NAME']))
+	{
+		$detected_url = isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on' ? 'https://' : 'http://';
+		$detected_url .= empty($_SERVER['HTTP_HOST']) ? $_SERVER['SERVER_NAME'] . (empty($_SERVER['SERVER_PORT']) || $_SERVER['SERVER_PORT'] == '80' ? '' : ':' . $_SERVER['SERVER_PORT']) : $_SERVER['HTTP_HOST'];
+		$temp = preg_replace('~/' . basename($scripturl) . '(/.+)?$~', '', strtr(dirname($_SERVER['PHP_SELF']), '\\', '/'));
+		if ($temp != '/')
+			$detected_url .= $temp;
+	}
+	if (isset($detected_url) && $detected_url != $boardurl)
+	{
+		// Try #1 - check if it's in a list of alias addresses.
+		if (!empty($modSettings['forum_alias_urls']))
+		{
+			$aliases = explode(',', $modSettings['forum_alias_urls']);
+
+			foreach ($aliases as $alias)
+			{
+				// Rip off all the boring parts, spaces, etc.
+				if ($detected_url == trim($alias) || strtr($detected_url, array('http://' => '', 'https://' => '')) == trim($alias))
+					$do_fix = true;
+			}
+		}
+
+		// Hmm... check #2 - is it just different by a www?  Send them to the correct place!!
+		if (empty($do_fix) && strtr($detected_url, array('://' => '://www.')) == $boardurl && (empty($_GET) || count($_GET) == 1) && SMF != 'SSI')
+		{
+			// Okay, this seems weird, but we don't want an endless loop - this will make $_GET not empty ;).
+			if (empty($_GET))
+				redirectexit('wwwRedirect');
+			else
+			{
+				list ($k, $v) = each($_GET);
+
+				if ($k != 'wwwRedirect')
+					redirectexit('wwwRedirect;' . $k . '=' . $v);
+			}
+		}
+
+		// #3 is just a check for SSL...
+		if (strtr($detected_url, array('https://' => 'http://')) == $boardurl)
+			$do_fix = true;
+
+		// Okay, #4 - perhaps it's an IP address?  We're gonna want to use that one, then. (assuming it's the IP or something...)
+		if (!empty($do_fix) || preg_match('~^http[s]?://(?:[\d\.:]+|\[[\d:]+\](?::\d+)?)(?:$|/)~', $detected_url) == 1)
+		{
+			// Caching is good ;).
+			$oldurl = $boardurl;
+
+			// Fix $boardurl and $scripturl.
+			$boardurl = $detected_url;
+			$scripturl = strtr($scripturl, array($oldurl => $boardurl));
+			$_SERVER['REQUEST_URL'] = strtr($_SERVER['REQUEST_URL'], array($oldurl => $boardurl));
+
+			// Fix the theme urls...
+			$settings['theme_url'] = strtr($settings['theme_url'], array($oldurl => $boardurl));
+			$settings['default_theme_url'] = strtr($settings['default_theme_url'], array($oldurl => $boardurl));
+			$settings['actual_theme_url'] = strtr($settings['actual_theme_url'], array($oldurl => $boardurl));
+			$settings['images_url'] = strtr($settings['images_url'], array($oldurl => $boardurl));
+			$settings['default_images_url'] = strtr($settings['default_images_url'], array($oldurl => $boardurl));
+			$settings['actual_images_url'] = strtr($settings['actual_images_url'], array($oldurl => $boardurl));
+
+			// And just a few mod settings :).
+			$modSettings['smileys_url'] = strtr($modSettings['smileys_url'], array($oldurl => $boardurl));
+			$modSettings['avatar_url'] = strtr($modSettings['avatar_url'], array($oldurl => $boardurl));
+
+			// Clean up after loadBoard().
+			if (isset($board_info['moderators']))
+			{
+				foreach ($board_info['moderators'] as $k => $dummy)
+				{
+					$board_info['moderators'][$k]['href'] = strtr($dummy['href'], array($oldurl => $boardurl));
+					$board_info['moderators'][$k]['link'] = strtr($dummy['link'], array('"' . $oldurl => '"' . $boardurl));
+				}
+			}
+			foreach ($context['linktree'] as $k => $dummy)
+				$context['linktree'][$k]['url'] = strtr($dummy['url'], array($oldurl => $boardurl));
+		}
+	}
+	// Set up the contextual user array.
+	$context['user'] = array(
+		'id' => $user_info['id'],
+		'is_logged' => !$user_info['is_guest'],
+		'is_guest' => &$user_info['is_guest'],
+		'is_admin' => &$user_info['is_admin'],
+		'is_mod' => &$user_info['is_mod'],
+		// A user can mod if they have permission to see the mod center, or they are a board/group/approval moderator.
+		'can_mod' => allowedTo('access_mod_center') || (!$user_info['is_guest'] && ($user_info['mod_cache']['gq'] != '0=1' || $user_info['mod_cache']['bq'] != '0=1' || ($modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap'])))),
+		'username' => $user_info['username'],
+		'language' => $user_info['language'],
+		'email' => $user_info['email'],
+		'ignoreusers' => $user_info['ignoreusers'],
+	);
+	if (!$context['user']['is_guest'])
+		$context['user']['name'] = $user_info['name'];
+	elseif ($context['user']['is_guest'] && !empty($txt['guest_title']))
+		$context['user']['name'] = $txt['guest_title'];
+
+	// Determine the current smiley set.
+	$user_info['smiley_set'] = (!in_array($user_info['smiley_set'], explode(',', $modSettings['smiley_sets_known'])) && $user_info['smiley_set'] != 'none') || empty($modSettings['smiley_sets_enable']) ? (!empty($settings['smiley_sets_default']) ? $settings['smiley_sets_default'] : $modSettings['smiley_sets_default']) : $user_info['smiley_set'];
+	$context['user']['smiley_set'] = $user_info['smiley_set'];
+
+	// Some basic information...
+	if (!isset($context['html_headers']))
+		$context['html_headers'] = '';
+
+	$context['menu_separator'] = !empty($settings['use_image_buttons']) ? ' ' : ' | ';
+	$context['session_var'] = $_SESSION['session_var'];
+	$context['session_id'] = $_SESSION['session_value'];
+	$context['forum_name'] = $mbname;
+	$context['forum_name_html_safe'] = $smcFunc['htmlspecialchars']($context['forum_name']);
+	$context['header_logo_url_html_safe'] = empty($settings['header_logo_url']) ? '' : $smcFunc['htmlspecialchars']($settings['header_logo_url']);
+	$context['current_action'] = isset($_REQUEST['action']) ? $_REQUEST['action'] : null;
+	$context['current_subaction'] = isset($_REQUEST['sa']) ? $_REQUEST['sa'] : null;
+	if (isset($modSettings['load_average']))
+		$context['load_average'] = $modSettings['load_average'];
+
+	// Set some permission related settings.
+	$context['show_login_bar'] = $user_info['is_guest'] && !empty($modSettings['enableVBStyleLogin']);
+
+	// This determines the server... not used in many places, except for login fixing.
+	$context['server'] = array(
+		'is_iis' => isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== false,
+		'is_apache' => isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'Apache') !== false,
+		'is_lighttpd' => isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'lighttpd') !== false,
+		'is_nginx' => isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'nginx') !== false,
+		'is_cgi' => isset($_SERVER['SERVER_SOFTWARE']) && strpos(php_sapi_name(), 'cgi') !== false,
+		'is_windows' => strpos(PHP_OS, 'WIN') === 0,
+		'iso_case_folding' => ord(strtolower(chr(138))) === 154,
+		'complex_preg_chars' => @version_compare(PHP_VERSION, '4.3.3') != -1,
+	);
+	// A bug in some versions of IIS under CGI (older ones) makes cookie setting not work with Location: headers.
+	$context['server']['needs_login_fix'] = $context['server']['is_cgi'] && $context['server']['is_iis'];
+
+	// Detect the browser. This is separated out because it's also used in attachment downloads
+	detectBrowser();
+
+	// Set the top level linktree up.
+	array_unshift($context['linktree'], array(
+		'url' => $scripturl,
+		'name' => $context['forum_name_html_safe']
+	));
+
+	// This allows sticking some HTML on the page output - useful for controls.
+	$context['insert_after_template'] = '';
+
+	if (!isset($txt))
+		$txt = array();
+	$simpleActions = array(
+		'findmember',
+		'helpadmin',
+		'printpage',
+		'quotefast',
+		'spellcheck',
+	);
+
+	// Wireless mode?  Load up the wireless stuff.
+	if (WIRELESS)
+	{
+		$context['template_layers'] = array(WIRELESS_PROTOCOL);
+		loadTemplate('Wireless');
+		loadLanguage('Wireless+index+Modifications');
+	}
+	// Output is fully XML, so no need for the index template.
+	elseif (isset($_REQUEST['xml']))
+	{
+		loadLanguage('index+Modifications');
+		loadTemplate('Xml');
+		$context['template_layers'] = array();
+	}
+	// These actions don't require the index template at all.
+	elseif (!empty($_REQUEST['action']) && in_array($_REQUEST['action'], $simpleActions))
+	{
+		loadLanguage('index+Modifications');
+		$context['template_layers'] = array();
+	}
+	else
+	{
+		// Custom templates to load, or just default?
+		if (isset($settings['theme_templates']))
+			$templates = explode(',', $settings['theme_templates']);
+		else
+			$templates = array('index');
+
+		// Load each template...
+		foreach ($templates as $template)
+			loadTemplate($template);
+
+		// ...and attempt to load their associated language files.
+		$required_files = implode('+', array_merge($templates, array('Modifications')));
+		loadLanguage($required_files, '', false);
+
+		// Custom template layers?
+		if (isset($settings['theme_layers']))
+			$context['template_layers'] = explode(',', $settings['theme_layers']);
+		else
+			$context['template_layers'] = array('html', 'body');
+	}
+
+	// Initialize the theme.
+	loadSubTemplate('init', 'ignore');
+
+	// Load the compatibility stylesheet if the theme hasn't been updated for 2.0 RC2 (yet).
+	if (isset($settings['theme_version']) && (version_compare($settings['theme_version'], '2.0 RC2', '<') || strpos($settings['theme_version'], '2.0 Beta') !== false))
+		loadTemplate(false, 'compat');
+
+	// Guests may still need a name.
+	if ($context['user']['is_guest'] && empty($context['user']['name']))
+		$context['user']['name'] = $txt['guest_title'];
+
+	// Any theme-related strings that need to be loaded?
+	if (!empty($settings['require_theme_strings']))
+		loadLanguage('ThemeStrings', '', false);
+
+	// We allow theme variants, because we're cool.
+	$context['theme_variant'] = '';
+	$context['theme_variant_url'] = '';
+	if (!empty($settings['theme_variants']))
+	{
+		// Overriding - for previews and that ilk.
+		if (!empty($_REQUEST['variant']))
+			$_SESSION['id_variant'] = $_REQUEST['variant'];
+		// User selection?
+		if (empty($settings['disable_user_variant']) || allowedTo('admin_forum'))
+			$context['theme_variant'] = !empty($_SESSION['id_variant']) ? $_SESSION['id_variant'] : (!empty($options['theme_variant']) ? $options['theme_variant'] : '');
+		// If not a user variant, select the default.
+		if ($context['theme_variant'] == '' || !in_array($context['theme_variant'], $settings['theme_variants']))
+			$context['theme_variant'] = !empty($settings['default_variant']) && in_array($settings['default_variant'], $settings['theme_variants']) ? $settings['default_variant'] : $settings['theme_variants'][0];
+
+		// Do this to keep things easier in the templates.
+		$context['theme_variant'] = '_' . $context['theme_variant'];
+		$context['theme_variant_url'] = $context['theme_variant'] . '/';
+	}
+
+	// Let's be compatible with old themes!
+	if (!function_exists('template_html_above') && in_array('html', $context['template_layers']))
+		$context['template_layers'] = array('main');
+
+	// Allow overriding the board wide time/number formats.
+	if (empty($user_settings['time_format']) && !empty($txt['time_format']))
+		$user_info['time_format'] = $txt['time_format'];
+	$txt['number_format'] = empty($txt['number_format']) ? empty($modSettings['number_format']) ? '' : $modSettings['number_format'] : $txt['number_format'];
+
+	if (isset($settings['use_default_images']) && $settings['use_default_images'] == 'always')
+	{
+		$settings['theme_url'] = $settings['default_theme_url'];
+		$settings['images_url'] = $settings['default_images_url'];
+		$settings['theme_dir'] = $settings['default_theme_dir'];
+	}
+	// Make a special URL for the language.
+	$settings['lang_images_url'] = $settings['images_url'] . '/' . (!empty($txt['image_lang']) ? $txt['image_lang'] : $user_info['language']);
+
+	// Set the character set from the template.
+	$context['character_set'] = empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set'];
+	$context['utf8'] = $context['character_set'] === 'UTF-8' && (strpos(strtolower(PHP_OS), 'win') === false || @version_compare(PHP_VERSION, '4.2.3') != -1);
+	$context['right_to_left'] = !empty($txt['lang_rtl']);
+
+	$context['tabindex'] = 1;
+
+	// Fix font size with HTML 4.01, etc.
+	if (isset($settings['doctype']))
+		$context['browser']['needs_size_fix'] |= $settings['doctype'] == 'html' && $context['browser']['is_ie6'];
+
+	// Compatibility.
+	if (!isset($settings['theme_version']))
+		$modSettings['memberCount'] = $modSettings['totalMembers'];
+
+	// This allows us to change the way things look for the admin.
+	$context['admin_features'] = isset($modSettings['admin_features']) ? explode(',', $modSettings['admin_features']) : array('cd,cp,k,w,rg,ml,pm');
+
+	// If we think we have mail to send, let's offer up some possibilities... robots get pain (Now with scheduled task support!)
+	if ((!empty($modSettings['mail_next_send']) && $modSettings['mail_next_send'] < time() && empty($modSettings['mail_queue_use_cron'])) || empty($modSettings['next_task_time']) || $modSettings['next_task_time'] < time())
+	{
+		if ($context['browser']['possibly_robot'])
+		{
+			//!!! Maybe move this somewhere better?!
+			require_once($sourcedir . '/ScheduledTasks.php');
+
+			// What to do, what to do?!
+			if (empty($modSettings['next_task_time']) || $modSettings['next_task_time'] < time())
+				AutoTask();
+			else
+				ReduceMailQueue();
+		}
+		else
+		{
+			$type = empty($modSettings['next_task_time']) || $modSettings['next_task_time'] < time() ? 'task' : 'mailq';
+			$ts = $type == 'mailq' ? $modSettings['mail_next_send'] : $modSettings['next_task_time'];
+
+			$context['html_headers'] .= '
+	<script type="text/javascript">
+		function smfAutoTask()
+		{
+			var tempImage = new Image();
+			tempImage.src = "' . $scripturl . '?scheduled=' . $type . ';ts=' . $ts . '";
+		}
+		window.setTimeout("smfAutoTask();", 1);
+	</script>';
+		}
+	}
+
+	// Any files to include at this point?
+	if (!empty($modSettings['integrate_theme_include']))
+	{
+		$theme_includes = explode(',', $modSettings['integrate_theme_include']);
+		foreach ($theme_includes as $include)
+		{
+			$include = strtr(trim($include), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir']));
+			if (file_exists($include))
+				require_once($include);
+		}
+	}
+
+	// Call load theme integration functions.
+	call_integration_hook('integrate_load_theme');
+
+	// We are ready to go.
+	$context['theme_loaded'] = true;
+}
+
+// Load a template - if the theme doesn't include it, use the default.
+function loadTemplate($template_name, $style_sheets = array(), $fatal = true)
+{
+	global $context, $settings, $txt, $scripturl, $boarddir, $db_show_debug;
+
+	// Do any style sheets first, cause we're easy with those.
+	if (!empty($style_sheets))
+	{
+		if (!is_array($style_sheets))
+			$style_sheets = array($style_sheets);
+
+		foreach ($style_sheets as $sheet)
+		{
+			// Prevent the style sheet from being included twice.
+			if (strpos($context['html_headers'], 'id="' . $sheet . '_css"') !== false)
+				continue;
+
+			$sheet_path = file_exists($settings['theme_dir']. '/css/' . $sheet . '.css') ? 'theme_url' : (file_exists($settings['default_theme_dir']. '/css/' . $sheet . '.css') ? 'default_theme_url' : '');
+			if ($sheet_path)
+			{
+				$context['html_headers'] .= "\n\t" . '<link rel="stylesheet" type="text/css" id="' . $sheet . '_css" href="' . $settings[$sheet_path] . '/css/' . $sheet . '.css" />';
+				if ($db_show_debug === true)
+					$context['debug']['sheets'][] = $sheet . ' (' . basename($settings[$sheet_path]) . ')';
+			}
+		}
+	}
+
+	// No template to load?
+	if ($template_name === false)
+		return true;
+
+	$loaded = false;
+	foreach ($settings['template_dirs'] as $template_dir)
+	{
+		if (file_exists($template_dir . '/' . $template_name . '.template.php'))
+		{
+			$loaded = true;
+			template_include($template_dir . '/' . $template_name . '.template.php', true);
+			break;
+		}
+	}
+
+	if ($loaded)
+	{
+		// For compatibility reasons, if this is the index template without new functions, include compatible stuff.
+		if (substr($template_name, 0, 5) == 'index' && !function_exists('template_button_strip'))
+			loadTemplate('Compat');
+
+		if ($db_show_debug === true)
+			$context['debug']['templates'][] = $template_name . ' (' . basename($template_dir) . ')';
+
+		// If they have specified an initialization function for this template, go ahead and call it now.
+		if (function_exists('template_' . $template_name . '_init'))
+			call_user_func('template_' . $template_name . '_init');
+	}
+	// Hmmm... doesn't exist?!  I don't suppose the directory is wrong, is it?
+	elseif (!file_exists($settings['default_theme_dir']) && file_exists($boarddir . '/Themes/default'))
+	{
+		$settings['default_theme_dir'] = $boarddir . '/Themes/default';
+		$settings['template_dirs'][] = $settings['default_theme_dir'];
+
+		if (!empty($context['user']['is_admin']) && !isset($_GET['th']))
+		{
+			loadLanguage('Errors');
+			echo '
+<div class="alert errorbox">
+	<a href="', $scripturl . '?action=admin;area=theme;sa=settings;th=1;' . $context['session_var'] . '=' . $context['session_id'], '" class="alert">', $txt['theme_dir_wrong'], '</a>
+</div>';
+		}
+
+		loadTemplate($template_name);
+	}
+	// Cause an error otherwise.
+	elseif ($template_name != 'Errors' && $template_name != 'index' && $fatal)
+		fatal_lang_error('theme_template_error', 'template', array((string) $template_name));
+	elseif ($fatal)
+		die(log_error(sprintf(isset($txt['theme_template_error']) ? $txt['theme_template_error'] : 'Unable to load Themes/default/%s.template.php!', (string) $template_name), 'template'));
+	else
+		return false;
+}
+
+// Load a sub template... fatal is for templates that shouldn't get a 'pretty' error screen.
+function loadSubTemplate($sub_template_name, $fatal = false)
+{
+	global $context, $settings, $options, $txt, $db_show_debug;
+
+	if ($db_show_debug === true)
+		$context['debug']['sub_templates'][] = $sub_template_name;
+
+	// Figure out what the template function is named.
+	$theme_function = 'template_' . $sub_template_name;
+	if (function_exists($theme_function))
+		$theme_function();
+	elseif ($fatal === false)
+		fatal_lang_error('theme_template_error', 'template', array((string) $sub_template_name));
+	elseif ($fatal !== 'ignore')
+		die(log_error(sprintf(isset($txt['theme_template_error']) ? $txt['theme_template_error'] : 'Unable to load the %s sub template!', (string) $sub_template_name), 'template'));
+
+	// Are we showing debugging for templates?  Just make sure not to do it before the doctype...
+	if (allowedTo('admin_forum') && isset($_REQUEST['debug']) && !in_array($sub_template_name, array('init', 'main_below')) && ob_get_length() > 0 && !isset($_REQUEST['xml']))
+	{
+		echo '
+<div style="font-size: 8pt; border: 1px dashed red; background: orange; text-align: center; font-weight: bold;">---- ', $sub_template_name, ' ends ----</div>';
+	}
+}
+
+// Load a language file.  Tries the current and default themes as well as the user and global languages.
+function loadLanguage($template_name, $lang = '', $fatal = true, $force_reload = false)
+{
+	global $user_info, $language, $settings, $context, $modSettings;
+	global $cachedir, $db_show_debug, $sourcedir, $txt;
+	static $already_loaded = array();
+
+	// Default to the user's language.
+	if ($lang == '')
+		$lang = isset($user_info['language']) ? $user_info['language'] : $language;
+
+	// Do we want the English version of language file as fallback?
+	if (empty($modSettings['disable_language_fallback']) && $lang != 'english')
+		loadLanguage($template_name, 'english', false);
+
+	if (!$force_reload && isset($already_loaded[$template_name]) && $already_loaded[$template_name] == $lang)
+		return $lang;
+
+	// Make sure we have $settings - if not we're in trouble and need to find it!
+	if (empty($settings['default_theme_dir']))
+	{
+		require_once($sourcedir . '/ScheduledTasks.php');
+		loadEssentialThemeData();
+	}
+
+	// What theme are we in?
+	$theme_name = basename($settings['theme_url']);
+	if (empty($theme_name))
+		$theme_name = 'unknown';
+
+	// For each file open it up and write it out!
+	foreach (explode('+', $template_name) as $template)
+	{
+		// Obviously, the current theme is most important to check.
+		$attempts = array(
+			array($settings['theme_dir'], $template, $lang, $settings['theme_url']),
+			array($settings['theme_dir'], $template, $language, $settings['theme_url']),
+		);
+
+		// Do we have a base theme to worry about?
+		if (isset($settings['base_theme_dir']))
+		{
+			$attempts[] = array($settings['base_theme_dir'], $template, $lang, $settings['base_theme_url']);
+			$attempts[] = array($settings['base_theme_dir'], $template, $language, $settings['base_theme_url']);
+		}
+
+		// Fall back on the default theme if necessary.
+		$attempts[] = array($settings['default_theme_dir'], $template, $lang, $settings['default_theme_url']);
+		$attempts[] = array($settings['default_theme_dir'], $template, $language, $settings['default_theme_url']);
+
+		// Fall back on the English language if none of the preferred languages can be found.
+		if (!in_array('english', array($lang, $language)))
+		{
+			$attempts[] = array($settings['theme_dir'], $template, 'english', $settings['theme_url']);
+			$attempts[] = array($settings['default_theme_dir'], $template, 'english', $settings['default_theme_url']);
+		}
+
+		// Try to find the language file.
+		$found = false;
+		foreach ($attempts as $k => $file)
+		{
+			if (file_exists($file[0] . '/languages/' . $file[1] . '.' . $file[2] . '.php'))
+			{
+				// Include it!
+				template_include($file[0] . '/languages/' . $file[1] . '.' . $file[2] . '.php');
+
+				// Note that we found it.
+				$found = true;
+
+				break;
+			}
+		}
+
+		// That couldn't be found!  Log the error, but *try* to continue normally.
+		if (!$found && $fatal)
+		{
+			log_error(sprintf($txt['theme_language_error'], $template_name . '.' . $lang, 'template'));
+			break;
+		}
+	}
+
+	// Keep track of what we're up to soldier.
+	if ($db_show_debug === true)
+		$context['debug']['language_files'][] = $template_name . '.' . $lang . ' (' . $theme_name . ')';
+
+	// Remember what we have loaded, and in which language.
+	$already_loaded[$template_name] = $lang;
+
+	// Return the language actually loaded.
+	return $lang;
+}
+
+// Get all parent boards (requires first parent as parameter)
+function getBoardParents($id_parent)
+{
+	global $scripturl, $smcFunc;
+
+	// First check if we have this cached already.
+	if (($boards = cache_get_data('board_parents-' . $id_parent, 480)) === null)
+	{
+		$boards = array();
+		$original_parent = $id_parent;
+
+		// Loop while the parent is non-zero.
+		while ($id_parent != 0)
+		{
+			$result = $smcFunc['db_query']('', '
+				SELECT
+					b.id_parent, b.name, {int:board_parent} AS id_board, IFNULL(mem.id_member, 0) AS id_moderator,
+					mem.real_name, b.child_level
+				FROM {db_prefix}boards AS b
+					LEFT JOIN {db_prefix}moderators AS mods ON (mods.id_board = b.id_board)
+					LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = mods.id_member)
+				WHERE b.id_board = {int:board_parent}',
+				array(
+					'board_parent' => $id_parent,
+				)
+			);
+			// In the EXTREMELY unlikely event this happens, give an error message.
+			if ($smcFunc['db_num_rows']($result) == 0)
+				fatal_lang_error('parent_not_found', 'critical');
+			while ($row = $smcFunc['db_fetch_assoc']($result))
+			{
+				if (!isset($boards[$row['id_board']]))
+				{
+					$id_parent = $row['id_parent'];
+					$boards[$row['id_board']] = array(
+						'url' => $scripturl . '?board=' . $row['id_board'] . '.0',
+						'name' => $row['name'],
+						'level' => $row['child_level'],
+						'moderators' => array()
+					);
+				}
+				// If a moderator exists for this board, add that moderator for all children too.
+				if (!empty($row['id_moderator']))
+					foreach ($boards as $id => $dummy)
+					{
+						$boards[$id]['moderators'][$row['id_moderator']] = array(
+							'id' => $row['id_moderator'],
+							'name' => $row['real_name'],
+							'href' => $scripturl . '?action=profile;u=' . $row['id_moderator'],
+							'link' => '<a href="' . $scripturl . '?action=profile;u=' . $row['id_moderator'] . '">' . $row['real_name'] . '</a>'
+						);
+					}
+			}
+			$smcFunc['db_free_result']($result);
+		}
+
+		cache_put_data('board_parents-' . $original_parent, $boards, 480);
+	}
+
+	return $boards;
+}
+
+// Attempt to reload our languages.
+function getLanguages($use_cache = true, $favor_utf8 = true)
+{
+	global $context, $smcFunc, $settings, $modSettings;
+
+	// Either we don't use the cache, or its expired.
+	if (!$use_cache || ($context['languages'] = cache_get_data('known_languages' . ($favor_utf8 ? '' : '_all'), !empty($modSettings['cache_enable']) && $modSettings['cache_enable'] < 1 ? 86400 : 3600)) == null)
+	{
+		// If we don't have our theme information yet, lets get it.
+		if (empty($settings['default_theme_dir']))
+			loadTheme(0, false);
+
+		// Default language directories to try.
+		$language_directories = array(
+			$settings['default_theme_dir'] . '/languages',
+			$settings['actual_theme_dir'] . '/languages',
+		);
+
+		// We possibly have a base theme directory.
+		if (!empty($settings['base_theme_dir']))
+			$language_directories[] = $settings['base_theme_dir'] . '/languages';
+
+		// Remove any duplicates.
+		$language_directories = array_unique($language_directories);
+
+		foreach ($language_directories as $language_dir)
+		{
+			// Can't look in here... doesn't exist!
+			if (!file_exists($language_dir))
+				continue;
+
+			$dir = dir($language_dir);
+			while ($entry = $dir->read())
+			{
+				// Look for the index language file....
+				if (!preg_match('~^index\.(.+)\.php$~', $entry, $matches))
+					continue;
+
+				$context['languages'][$matches[1]] = array(
+					'name' => $smcFunc['ucwords'](strtr($matches[1], array('_' => ' '))),
+					'selected' => false,
+					'filename' => $matches[1],
+					'location' => $language_dir . '/index.' . $matches[1] . '.php',
+				);
+
+			}
+			$dir->close();
+		}
+
+		// Favoring UTF8? Then prevent us from selecting non-UTF8 versions.
+		if ($favor_utf8)
+		{
+			foreach ($context['languages'] as $lang)
+				if (substr($lang['filename'], strlen($lang['filename']) - 5, 5) != '-utf8' && isset($context['languages'][$lang['filename'] . '-utf8']))
+					unset($context['languages'][$lang['filename']]);
+		}
+
+		// Lets cash in on this deal.
+		if (!empty($modSettings['cache_enable']))
+			cache_put_data('known_languages' . ($favor_utf8 ? '' : '_all'), $context['languages'], !empty($modSettings['cache_enable']) && $modSettings['cache_enable'] < 1 ? 86400 : 3600);
+	}
+
+	return $context['languages'];
+}
+
+// Replace all vulgar words with respective proper words. (substring or whole words..)
+function &censorText(&$text, $force = false)
+{
+	global $modSettings, $options, $settings, $txt;
+	static $censor_vulgar = null, $censor_proper;
+
+	if ((!empty($options['show_no_censored']) && $settings['allow_no_censored'] && !$force) || empty($modSettings['censor_vulgar']))
+		return $text;
+
+	// If they haven't yet been loaded, load them.
+	if ($censor_vulgar == null)
+	{
+		$censor_vulgar = explode("\n", $modSettings['censor_vulgar']);
+		$censor_proper = explode("\n", $modSettings['censor_proper']);
+
+		// Quote them for use in regular expressions.
+		for ($i = 0, $n = count($censor_vulgar); $i < $n; $i++)
+		{
+			$censor_vulgar[$i] = strtr(preg_quote($censor_vulgar[$i], '/'), array('\\\\\\*' => '[*]', '\\*' => '[^\s]*?', '&' => '&amp;'));
+			$censor_vulgar[$i] = (empty($modSettings['censorWholeWord']) ? '/' . $censor_vulgar[$i] . '/' : '/(?<=^|\W)' . $censor_vulgar[$i] . '(?=$|\W)/') . (empty($modSettings['censorIgnoreCase']) ? '' : 'i') . ((empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set']) === 'UTF-8' ? 'u' : '');
+
+			if (strpos($censor_vulgar[$i], '\'') !== false)
+			{
+				$censor_proper[count($censor_vulgar)] = $censor_proper[$i];
+				$censor_vulgar[count($censor_vulgar)] = strtr($censor_vulgar[$i], array('\'' => '&#039;'));
+			}
+		}
+	}
+
+	// Censoring isn't so very complicated :P.
+	$text = preg_replace($censor_vulgar, $censor_proper, $text);
+	return $text;
+}
+
+// Load the template/language file using eval or require? (with eval we can show an error message!)
+function template_include($filename, $once = false)
+{
+	global $context, $settings, $options, $txt, $scripturl, $modSettings;
+	global $user_info, $boardurl, $boarddir, $sourcedir;
+	global $maintenance, $mtitle, $mmessage;
+	static $templates = array();
+
+	// We want to be able to figure out any errors...
+	@ini_set('track_errors', '1');
+
+	// Don't include the file more than once, if $once is true.
+	if ($once && in_array($filename, $templates))
+		return;
+	// Add this file to the include list, whether $once is true or not.
+	else
+		$templates[] = $filename;
+
+	// Are we going to use eval?
+	if (empty($modSettings['disableTemplateEval']))
+	{
+		$file_found = file_exists($filename) && eval('?' . '>' . rtrim(file_get_contents($filename))) !== false;
+		$settings['current_include_filename'] = $filename;
+	}
+	else
+	{
+		$file_found = file_exists($filename);
+
+		if ($once && $file_found)
+			require_once($filename);
+		elseif ($file_found)
+			require($filename);
+	}
+
+	if ($file_found !== true)
+	{
+		ob_end_clean();
+		if (!empty($modSettings['enableCompressedOutput']))
+			@ob_start('ob_gzhandler');
+		else
+			ob_start();
+
+		if (isset($_GET['debug']) && !WIRELESS)
+			header('Content-Type: application/xhtml+xml; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
+
+		// Don't cache error pages!!
+		header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
+		header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
+		header('Cache-Control: no-cache');
+
+		if (!isset($txt['template_parse_error']))
+		{
+			$txt['template_parse_error'] = 'Template Parse Error!';
+			$txt['template_parse_error_message'] = 'It seems something has gone sour on the forum with the template system.  This problem should only be temporary, so please come back later and try again.  If you continue to see this message, please contact the administrator.<br /><br />You can also try <a href="javascript:location.reload();">refreshing this page</a>.';
+			$txt['template_parse_error_details'] = 'There was a problem loading the <tt><strong>%1$s</strong></tt> template or language file.  Please check the syntax and try again - remember, single quotes (<tt>\'</tt>) often have to be escaped with a slash (<tt>\\</tt>).  To see more specific error information from PHP, try <a href="' . $boardurl . '%1$s" class="extern">accessing the file directly</a>.<br /><br />You may want to try to <a href="javascript:location.reload();">refresh this page</a> or <a href="' . $scripturl . '?theme=1">use the default theme</a>.';
+		}
+
+		// First, let's get the doctype and language information out of the way.
+		echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"', !empty($context['right_to_left']) ? ' dir="rtl"' : '', '>
+	<head>';
+		if (isset($context['character_set']))
+			echo '
+		<meta http-equiv="Content-Type" content="text/html; charset=', $context['character_set'], '" />';
+
+		if (!empty($maintenance) && !allowedTo('admin_forum'))
+			echo '
+		<title>', $mtitle, '</title>
+	</head>
+	<body>
+		<h3>', $mtitle, '</h3>
+		', $mmessage, '
+	</body>
+</html>';
+		elseif (!allowedTo('admin_forum'))
+			echo '
+		<title>', $txt['template_parse_error'], '</title>
+	</head>
+	<body>
+		<h3>', $txt['template_parse_error'], '</h3>
+		', $txt['template_parse_error_message'], '
+	</body>
+</html>';
+		else
+		{
+			require_once($sourcedir . '/Subs-Package.php');
+
+			$error = fetch_web_data($boardurl . strtr($filename, array($boarddir => '', strtr($boarddir, '\\', '/') => '')));
+			if (empty($error))
+				$error = $php_errormsg;
+
+			$error = strtr($error, array('<b>' => '<strong>', '</b>' => '</strong>'));
+
+			echo '
+		<title>', $txt['template_parse_error'], '</title>
+	</head>
+	<body>
+		<h3>', $txt['template_parse_error'], '</h3>
+		', sprintf($txt['template_parse_error_details'], strtr($filename, array($boarddir => '', strtr($boarddir, '\\', '/') => '')));
+
+			if (!empty($error))
+				echo '
+		<hr />
+
+		<div style="margin: 0 20px;"><tt>', strtr(strtr($error, array('<strong>' . $boarddir => '<strong>...', '<strong>' . strtr($boarddir, '\\', '/') => '<strong>...')), '\\', '/'), '</tt></div>';
+
+			// I know, I know... this is VERY COMPLICATED.  Still, it's good.
+			if (preg_match('~ <strong>(\d+)</strong><br( /)?' . '>$~i', $error, $match) != 0)
+			{
+				$data = file($filename);
+				$data2 = highlight_php_code(implode('', $data));
+				$data2 = preg_split('~\<br( /)?\>~', $data2);
+
+				// Fix the PHP code stuff...
+				if ($context['browser']['is_ie4'] || $context['browser']['is_ie5'] || $context['browser']['is_ie5.5'])
+					$data2 = str_replace("\t", '<pre style="display: inline;">' . "\t" . '</pre>', $data2);
+				elseif (!$context['browser']['is_gecko'])
+					$data2 = str_replace("\t", '<span style="white-space: pre;">' . "\t" . '</span>', $data2);
+				else
+					$data2 = str_replace('<pre style="display: inline;">' . "\t" . '</pre>', "\t", $data2);
+
+				// Now we get to work around a bug in PHP where it doesn't escape <br />s!
+				$j = -1;
+				foreach ($data as $line)
+				{
+					$j++;
+
+					if (substr_count($line, '<br />') == 0)
+						continue;
+
+					$n = substr_count($line, '<br />');
+					for ($i = 0; $i < $n; $i++)
+					{
+						$data2[$j] .= '&lt;br /&gt;' . $data2[$j + $i + 1];
+						unset($data2[$j + $i + 1]);
+					}
+					$j += $n;
+				}
+				$data2 = array_values($data2);
+				array_unshift($data2, '');
+
+				echo '
+		<div style="margin: 2ex 20px; width: 96%; overflow: auto;"><pre style="margin: 0;">';
+
+				// Figure out what the color coding was before...
+				$line = max($match[1] - 9, 1);
+				$last_line = '';
+				for ($line2 = $line - 1; $line2 > 1; $line2--)
+					if (strpos($data2[$line2], '<') !== false)
+					{
+						if (preg_match('~(<[^/>]+>)[^<]*$~', $data2[$line2], $color_match) != 0)
+							$last_line = $color_match[1];
+						break;
+					}
+
+				// Show the relevant lines...
+				for ($n = min($match[1] + 4, count($data2) + 1); $line <= $n; $line++)
+				{
+					if ($line == $match[1])
+						echo '</pre><div style="background-color: #ffb0b5;"><pre style="margin: 0;">';
+
+					echo '<span style="color: black;">', sprintf('%' . strlen($n) . 's', $line), ':</span> ';
+					if (isset($data2[$line]) && $data2[$line] != '')
+						echo substr($data2[$line], 0, 2) == '</' ? preg_replace('~^</[^>]+>~', '', $data2[$line]) : $last_line . $data2[$line];
+
+					if (isset($data2[$line]) && preg_match('~(<[^/>]+>)[^<]*$~', $data2[$line], $color_match) != 0)
+					{
+						$last_line = $color_match[1];
+						echo '</', substr($last_line, 1, 4), '>';
+					}
+					elseif ($last_line != '' && strpos($data2[$line], '<') !== false)
+						$last_line = '';
+					elseif ($last_line != '' && $data2[$line] != '')
+						echo '</', substr($last_line, 1, 4), '>';
+
+					if ($line == $match[1])
+						echo '</pre></div><pre style="margin: 0;">';
+					else
+						echo "\n";
+				}
+
+				echo '</pre></div>';
+			}
+
+			echo '
+	</body>
+</html>';
+		}
+
+		die;
+	}
+}
+
+// Attempt to start the session, unless it already has been.
+function loadSession()
+{
+	global $HTTP_SESSION_VARS, $modSettings, $boardurl, $sc;
+
+	// Attempt to change a few PHP settings.
+	@ini_set('session.use_cookies', true);
+	@ini_set('session.use_only_cookies', false);
+	@ini_set('url_rewriter.tags', '');
+	@ini_set('session.use_trans_sid', false);
+	@ini_set('arg_separator.output', '&amp;');
+
+	if (!empty($modSettings['globalCookies']))
+	{
+		$parsed_url = parse_url($boardurl);
+
+		if (preg_match('~^\d{1,3}(\.\d{1,3}){3}$~', $parsed_url['host']) == 0 && preg_match('~(?:[^\.]+\.)?([^\.]{2,}\..+)\z~i', $parsed_url['host'], $parts) == 1)
+			@ini_set('session.cookie_domain', '.' . $parts[1]);
+	}
+	// !!! Set the session cookie path?
+
+	// If it's already been started... probably best to skip this.
+	if ((@ini_get('session.auto_start') == 1 && !empty($modSettings['databaseSession_enable'])) || session_id() == '')
+	{
+		// Attempt to end the already-started session.
+		if (@ini_get('session.auto_start') == 1)
+			@session_write_close();
+
+		// This is here to stop people from using bad junky PHPSESSIDs.
+		if (isset($_REQUEST[session_name()]) && preg_match('~^[A-Za-z0-9]{16,32}$~', $_REQUEST[session_name()]) == 0 && !isset($_COOKIE[session_name()]))
+		{
+			$session_id = md5(md5('smf_sess_' . time()) . mt_rand());
+			$_REQUEST[session_name()] = $session_id;
+			$_GET[session_name()] = $session_id;
+			$_POST[session_name()] = $session_id;
+		}
+
+		// Use database sessions? (they don't work in 4.1.x!)
+		if (!empty($modSettings['databaseSession_enable']) && @version_compare(PHP_VERSION, '4.2.0') != -1)
+		{
+			session_set_save_handler('sessionOpen', 'sessionClose', 'sessionRead', 'sessionWrite', 'sessionDestroy', 'sessionGC');
+			@ini_set('session.gc_probability', '1');
+		}
+		elseif (@ini_get('session.gc_maxlifetime') <= 1440 && !empty($modSettings['databaseSession_lifetime']))
+			@ini_set('session.gc_maxlifetime', max($modSettings['databaseSession_lifetime'], 60));
+
+		// Use cache setting sessions?
+		if (empty($modSettings['databaseSession_enable']) && !empty($modSettings['cache_enable']) && php_sapi_name() != 'cli')
+		{
+			if (function_exists('mmcache_set_session_handlers'))
+				mmcache_set_session_handlers();
+			elseif (function_exists('eaccelerator_set_session_handlers'))
+				eaccelerator_set_session_handlers();
+		}
+
+		session_start();
+
+		// Change it so the cache settings are a little looser than default.
+		if (!empty($modSettings['databaseSession_loose']))
+			header('Cache-Control: private');
+	}
+
+	// While PHP 4.1.x should use $_SESSION, it seems to need this to do it right.
+	if (@version_compare(PHP_VERSION, '4.2.0') == -1)
+		$HTTP_SESSION_VARS['php_412_bugfix'] = true;
+
+	// Set the randomly generated code.
+	if (!isset($_SESSION['session_var']))
+	{
+		$_SESSION['session_value'] = md5(session_id() . mt_rand());
+		$_SESSION['session_var'] = substr(preg_replace('~^\d+~', '', sha1(mt_rand() . session_id() . mt_rand())), 0, rand(7, 12));
+	}
+	$sc = $_SESSION['session_value'];
+}
+
+function sessionOpen($save_path, $session_name)
+{
+	return true;
+}
+
+function sessionClose()
+{
+	return true;
+}
+
+function sessionRead($session_id)
+{
+	global $smcFunc;
+
+	if (preg_match('~^[A-Za-z0-9]{16,32}$~', $session_id) == 0)
+		return false;
+
+	// Look for it in the database.
+	$result = $smcFunc['db_query']('', '
+		SELECT data
+		FROM {db_prefix}sessions
+		WHERE session_id = {string:session_id}
+		LIMIT 1',
+		array(
+			'session_id' => $session_id,
+		)
+	);
+	list ($sess_data) = $smcFunc['db_fetch_row']($result);
+	$smcFunc['db_free_result']($result);
+
+	return $sess_data;
+}
+
+function sessionWrite($session_id, $data)
+{
+	global $smcFunc;
+
+	if (preg_match('~^[A-Za-z0-9]{16,32}$~', $session_id) == 0)
+		return false;
+
+	// First try to update an existing row...
+	$result = $smcFunc['db_query']('', '
+		UPDATE {db_prefix}sessions
+		SET data = {string:data}, last_update = {int:last_update}
+		WHERE session_id = {string:session_id}',
+		array(
+			'last_update' => time(),
+			'data' => $data,
+			'session_id' => $session_id,
+		)
+	);
+
+	// If that didn't work, try inserting a new one.
+	if ($smcFunc['db_affected_rows']() == 0)
+		$result = $smcFunc['db_insert']('ignore',
+			'{db_prefix}sessions',
+			array('session_id' => 'string', 'data' => 'string', 'last_update' => 'int'),
+			array($session_id, $data, time()),
+			array('session_id')
+		);
+
+	return $result;
+}
+
+function sessionDestroy($session_id)
+{
+	global $smcFunc;
+
+	if (preg_match('~^[A-Za-z0-9]{16,32}$~', $session_id) == 0)
+		return false;
+
+	// Just delete the row...
+	return $smcFunc['db_query']('', '
+		DELETE FROM {db_prefix}sessions
+		WHERE session_id = {string:session_id}',
+		array(
+			'session_id' => $session_id,
+		)
+	);
+}
+
+function sessionGC($max_lifetime)
+{
+	global $modSettings, $smcFunc;
+
+	// Just set to the default or lower?  Ignore it for a higher value. (hopefully)
+	if (!empty($modSettings['databaseSession_lifetime']) && ($max_lifetime <= 1440 || $modSettings['databaseSession_lifetime'] > $max_lifetime))
+		$max_lifetime = max($modSettings['databaseSession_lifetime'], 60);
+
+	// Clean up ;).
+	return $smcFunc['db_query']('', '
+		DELETE FROM {db_prefix}sessions
+		WHERE last_update < {int:last_update}',
+		array(
+			'last_update' => time() - $max_lifetime,
+		)
+	);
+}
+
+// Load up a database connection.
+function loadDatabase()
+{
+	global $db_persist, $db_connection, $db_server, $db_user, $db_passwd;
+	global $db_type, $db_name, $ssi_db_user, $ssi_db_passwd, $sourcedir, $db_prefix;
+
+	// Figure out what type of database we are using.
+	if (empty($db_type) || !file_exists($sourcedir . '/Subs-Db-' . $db_type . '.php'))
+		$db_type = 'mysql';
+
+	// Load the file for the database.
+	require_once($sourcedir . '/Subs-Db-' . $db_type . '.php');
+
+	// If we are in SSI try them first, but don't worry if it doesn't work, we have the normal username and password we can use.
+	if (SMF == 'SSI' && !empty($ssi_db_user) && !empty($ssi_db_passwd))
+		$db_connection = smf_db_initiate($db_server, $db_name, $ssi_db_user, $ssi_db_passwd, $db_prefix, array('persist' => $db_persist, 'non_fatal' => true, 'dont_select_db' => true));
+
+	// Either we aren't in SSI mode, or it failed.
+	if (empty($db_connection))
+		$db_connection = smf_db_initiate($db_server, $db_name, $db_user, $db_passwd, $db_prefix, array('persist' => $db_persist, 'dont_select_db' => SMF == 'SSI'));
+
+	// Safe guard here, if there isn't a valid connection lets put a stop to it.
+	if (!$db_connection)
+		db_fatal_error();
+
+	// If in SSI mode fix up the prefix.
+	if (SMF == 'SSI')
+		db_fix_prefix($db_prefix, $db_name);
+}
+
+// Try to retrieve a cache entry. On failure, call the appropriate function.
+function cache_quick_get($key, $file, $function, $params, $level = 1)
+{
+	global $modSettings, $sourcedir;
+
+	// Refresh the cache if either:
+	// 1. Caching is disabled.
+	// 2. The cache level isn't high enough.
+	// 3. The item has not been cached or the cached item expired.
+	// 4. The cached item has a custom expiration condition evaluating to true.
+	// 5. The expire time set in the cache item has passed (needed for Zend).
+	if (empty($modSettings['cache_enable']) || $modSettings['cache_enable'] < $level || !is_array($cache_block = cache_get_data($key, 3600)) || (!empty($cache_block['refresh_eval']) && eval($cache_block['refresh_eval'])) || (!empty($cache_block['expires']) && $cache_block['expires'] < time()))
+	{
+		require_once($sourcedir . '/' . $file);
+		$cache_block = call_user_func_array($function, $params);
+
+		if (!empty($modSettings['cache_enable']) && $modSettings['cache_enable'] >= $level)
+			cache_put_data($key, $cache_block, $cache_block['expires'] - time());
+	}
+
+	// Some cached data may need a freshening up after retrieval.
+	if (!empty($cache_block['post_retri_eval']))
+		eval($cache_block['post_retri_eval']);
+
+	return $cache_block['data'];
+}
+
+function cache_put_data($key, $value, $ttl = 120)
+{
+	global $boardurl, $sourcedir, $modSettings, $memcached;
+	global $cache_hits, $cache_count, $db_show_debug, $cachedir;
+
+	if (empty($modSettings['cache_enable']) && !empty($modSettings))
+		return;
+
+	$cache_count = isset($cache_count) ? $cache_count + 1 : 1;
+	if (isset($db_show_debug) && $db_show_debug === true)
+	{
+		$cache_hits[$cache_count] = array('k' => $key, 'd' => 'put', 's' => $value === null ? 0 : strlen(serialize($value)));
+		$st = microtime();
+	}
+
+	$key = md5($boardurl . filemtime($sourcedir . '/Load.php')) . '-SMF-' . strtr($key, ':', '-');
+	$value = $value === null ? null : serialize($value);
+
+	// The simple yet efficient memcached.
+	if (function_exists('memcache_set') && isset($modSettings['cache_memcached']) && trim($modSettings['cache_memcached']) != '')
+	{
+		// Not connected yet?
+		if (empty($memcached))
+			get_memcached_server();
+		if (!$memcached)
+			return;
+
+		memcache_set($memcached, $key, $value, 0, $ttl);
+	}
+	// eAccelerator...
+	elseif (function_exists('eaccelerator_put'))
+	{
+		if (mt_rand(0, 10) == 1)
+			eaccelerator_gc();
+
+		if ($value === null)
+			@eaccelerator_rm($key);
+		else
+			eaccelerator_put($key, $value, $ttl);
+	}
+	// Turck MMCache?
+	elseif (function_exists('mmcache_put'))
+	{
+		if (mt_rand(0, 10) == 1)
+			mmcache_gc();
+
+		if ($value === null)
+			@mmcache_rm($key);
+		else
+			mmcache_put($key, $value, $ttl);
+	}
+	// Alternative PHP Cache, ahoy!
+	elseif (function_exists('apc_store'))
+	{
+		// An extended key is needed to counteract a bug in APC.
+		if ($value === null)
+			apc_delete($key . 'smf');
+		else
+			apc_store($key . 'smf', $value, $ttl);
+	}
+	// Zend Platform/ZPS/etc.
+	elseif (function_exists('output_cache_put'))
+		output_cache_put($key, $value);
+	elseif (function_exists('xcache_set') && ini_get('xcache.var_size') > 0)
+	{
+		if ($value === null)
+			xcache_unset($key);
+		else
+			xcache_set($key, $value, $ttl);
+	}
+	// Otherwise custom cache?
+	else
+	{
+		if ($value === null)
+			@unlink($cachedir . '/data_' . $key . '.php');
+		else
+		{
+			$cache_data = '<' . '?' . 'php if (!defined(\'SMF\')) die; if (' . (time() + $ttl) . ' < time()) $expired = true; else{$expired = false; $value = \'' . addcslashes($value, '\\\'') . '\';}' . '?' . '>';
+			$fh = @fopen($cachedir . '/data_' . $key . '.php', 'w');
+			if ($fh)
+			{
+				// Write the file.
+				set_file_buffer($fh, 0);
+				flock($fh, LOCK_EX);
+				$cache_bytes = fwrite($fh, $cache_data);
+				flock($fh, LOCK_UN);
+				fclose($fh);
+
+				// Check that the cache write was successful; all the data should be written
+				// If it fails due to low diskspace, remove the cache file
+				if ($cache_bytes != strlen($cache_data))
+					@unlink($cachedir . '/data_' . $key . '.php');
+			}
+		}
+	}
+
+	if (isset($db_show_debug) && $db_show_debug === true)
+		$cache_hits[$cache_count]['t'] = array_sum(explode(' ', microtime())) - array_sum(explode(' ', $st));
+}
+
+function cache_get_data($key, $ttl = 120)
+{
+	global $boardurl, $sourcedir, $modSettings, $memcached;
+	global $cache_hits, $cache_count, $db_show_debug, $cachedir;
+
+	if (empty($modSettings['cache_enable']) && !empty($modSettings))
+		return;
+
+	$cache_count = isset($cache_count) ? $cache_count + 1 : 1;
+	if (isset($db_show_debug) && $db_show_debug === true)
+	{
+		$cache_hits[$cache_count] = array('k' => $key, 'd' => 'get');
+		$st = microtime();
+	}
+
+	$key = md5($boardurl . filemtime($sourcedir . '/Load.php')) . '-SMF-' . strtr($key, ':', '-');
+
+	// Okay, let's go for it memcached!
+	if (function_exists('memcache_get') && isset($modSettings['cache_memcached']) && trim($modSettings['cache_memcached']) != '')
+	{
+		// Not connected yet?
+		if (empty($memcached))
+			get_memcached_server();
+		if (!$memcached)
+			return;
+
+		$value = memcache_get($memcached, $key);
+	}
+	// Again, eAccelerator.
+	elseif (function_exists('eaccelerator_get'))
+		$value = eaccelerator_get($key);
+	// The older, but ever-stable, Turck MMCache...
+	elseif (function_exists('mmcache_get'))
+		$value = mmcache_get($key);
+	// This is the free APC from PECL.
+	elseif (function_exists('apc_fetch'))
+		$value = apc_fetch($key . 'smf');
+	// Zend's pricey stuff.
+	elseif (function_exists('output_cache_get'))
+		$value = output_cache_get($key, $ttl);
+	elseif (function_exists('xcache_get') && ini_get('xcache.var_size') > 0)
+		$value = xcache_get($key);
+	// Otherwise it's SMF data!
+	elseif (file_exists($cachedir . '/data_' . $key . '.php') && filesize($cachedir . '/data_' . $key . '.php') > 10)
+	{
+		require($cachedir . '/data_' . $key . '.php');
+		if (!empty($expired) && isset($value))
+		{
+			@unlink($cachedir . '/data_' . $key . '.php');
+			unset($value);
+		}
+	}
+
+	if (isset($db_show_debug) && $db_show_debug === true)
+	{
+		$cache_hits[$cache_count]['t'] = array_sum(explode(' ', microtime())) - array_sum(explode(' ', $st));
+		$cache_hits[$cache_count]['s'] = isset($value) ? strlen($value) : 0;
+	}
+
+	if (empty($value))
+		return null;
+	// If it's broke, it's broke... so give up on it.
+	else
+		return @unserialize($value);
+}
+
+function get_memcached_server($level = 3)
+{
+	global $modSettings, $memcached, $db_persist;
+
+	$servers = explode(',', $modSettings['cache_memcached']);
+	$server = explode(':', trim($servers[array_rand($servers)]));
+
+	// Don't try more times than we have servers!
+	$level = min(count($servers), $level);
+
+	// Don't wait too long: yes, we want the server, but we might be able to run the query faster!
+	if (empty($db_persist))
+		$memcached = memcache_connect($server[0], empty($server[1]) ? 11211 : $server[1]);
+	else
+		$memcached = memcache_pconnect($server[0], empty($server[1]) ? 11211 : $server[1]);
+
+	if (!$memcached && $level > 0)
+		get_memcached_server($level - 1);
+}
+
+?>

Some files were not shown because too many files changed in this diff