<?php
/**********************************************************************************
* Search.php                                                                      *
***********************************************************************************
* SMF: Simple Machines Forum                                                      *
* Open-Source Project Inspired by Zef Hemel (zef@zefhemel.com)                    *
* =============================================================================== *
* Software Version:           SMF 1.0.10                                          *
* Software by:                Simple Machines (http://www.simplemachines.org)     *
* Copyright 2006 by:          Simple Machines LLC (http://www.simplemachines.org) *
*           2001-2006 by:     Lewis Media (http://www.lewismedia.com)             *
* Support, News, Updates at:  http://www.simplemachines.org                       *
***********************************************************************************
* This program is free software; you may redistribute it and/or modify it under   *
* the terms of the provided license as published by Simple Machines LLC.          *
*                                                                                 *
* This program is distributed in the hope that it is and will be useful, but      *
* WITHOUT ANY WARRANTIES; without even any implied warranty of MERCHANTABILITY    *
* or FITNESS FOR A PARTICULAR PURPOSE.                                            *
*                                                                                 *
* See the "license.txt" file for details of the Simple Machines license.          *
* The latest version can always be found at http://www.simplemachines.org.        *
**********************************************************************************/

if (!defined('SMF'))
	die('Hacking attempt...');

/*	These functions are here for searching, and they are:

	void PlushSearch1()
		- shows the screen to search forum posts (action=search), and uses the
		  simple version if the simpleSearch setting is enabled.
		- uses the main sub template of the Search template.
		- requires the search_posts permission.
		- decodes and loads search parameters given in the URL (if any).
		- the form redirects to index.php?action=search2.

	void PlushSearch2()
		- checks user input and searches the messages table for messages
		  matching the query.
		- requires the search_posts permission.
		- stores the results into the search cache (if enabled).
		- show the results of the search query.
		- uses the results sub template of the Search template.

	array prepareSearchContext(bool reset = false)
		- callback function for the results sub template.
		- loads the necessary contextual data to show a search result.
*/

// Ask the user what they want to search for.
function PlushSearch1()
{
	global $txt, $scripturl, $db_prefix, $modSettings, $user_info, $context;

	loadTemplate('Search');

	// Check the user's permissions.
	isAllowedTo('search_posts');

	// Link tree....
	$context['linktree'][] = array(
		'url' => $scripturl . '?action=search',
		'name' => $txt[182]
	);

	// If you got back from search2 by using the linktree, you get your original search parameters back.
	if (isset($_REQUEST['params']))
	{
		$temp_params = explode('|"|', base64_decode($_REQUEST['params']));
		$context['search_params'] = array();
		foreach ($temp_params as $i => $data)
		{
			list ($k, $v) = explode('|\'|', $data);
			$context['search_params'][$k] = stripslashes($v);
		}
		if (isset($context['search_params']['brd']))
			$context['search_params']['brd'] = $context['search_params']['brd'] == '' ? array() : explode(',', $context['search_params']['brd']);
	}

	if (isset($context['search_params']['search']))
		$context['search_params']['search'] = htmlspecialchars($context['search_params']['search']);
	if (isset($context['search_params']['userspec']))
		$context['search_params']['userspec'] = htmlspecialchars(stripslashes($context['search_params']['userspec']));
	if (!empty($context['search_params']['searchtype']))
		$context['search_params']['searchtype'] = 2;
	if (!empty($context['search_params']['minage']))
		$context['search_params']['minage'] = (int) $context['search_params']['minage'];
	if (!empty($context['search_params']['maxage']))
		$context['search_params']['maxage'] = (int) $context['search_params']['maxage'];

	$context['search_params']['show_complete'] = !empty($context['search_params']['show_complete']);
	$context['search_params']['subject_only'] = !empty($context['search_params']['subject_only']);

	// Find all the boards this user is allowed to see.
	$request = db_query("
		SELECT b.ID_CAT, c.name AS catName, b.ID_BOARD, b.name, b.childLevel
		FROM {$db_prefix}boards AS b
			LEFT JOIN {$db_prefix}categories AS c ON (c.ID_CAT = b.ID_CAT)
		WHERE $user_info[query_see_board]
		ORDER BY c.catOrder, b.boardOrder", __FILE__, __LINE__);
	$context['num_boards'] = mysql_num_rows($request);
	$context['categories'] = array();
	while ($row = mysql_fetch_assoc($request))
	{
		// This category hasn't been set up yet..
		if (!isset($context['categories'][$row['ID_CAT']]))
			$context['categories'][$row['ID_CAT']] = array(
				'id' => $row['ID_CAT'],
				'name' => $row['catName'],
				'boards' => array()
			);

		// Set this board up, and let the template know when it's a child.  (indent them..)
		$context['categories'][$row['ID_CAT']]['boards'][$row['ID_BOARD']] = array(
			'id' => $row['ID_BOARD'],
			'name' => $row['name'],
			'child_level' => $row['childLevel']
		);
	}
	mysql_free_result($request);

	// Simple or not?
	$context['simple_search'] = !empty($modSettings['simpleSearch']) && !isset($_GET['advanced']);
	$context['page_title'] = $txt[183];
}

// Gather the results and show them.
function PlushSearch2()
{
	global $modSettings, $sourcedir;
	global $scripturl, $txt, $db_prefix, $user_info, $context, $messages_request, $attachments, $boards_can;

	$weight_factors = array(
		'frequency',
		'age',
		'length',
		'subject',
		'first_message',
	);

	$weight = array();
	$weight_total = 0;
	foreach ($weight_factors as $weight_factor)
	{
		$weight[$weight_factor] = empty($modSettings['search_weight_' . $weight_factor]) ? 0 : (int) $modSettings['search_weight_' . $weight_factor];
		$weight_total += $weight[$weight_factor];
	}

	// Zero weight. Weightless.
	if (empty($weight_total))
		fatal_lang_error('search_invalid_weights');

/*
	// Fine-tune the weight of each search factor.  These factors should add up to 100.
	$weight = array(
		// The more message within a topic match the search query, the higher the ranking.
		'frequency' => 30,
		// The more recent a message is, the higher its ranking.
		'age' => 25,
		// The larger the topic is, the higher its ranking.
		'length' => 20,
		// If a search string happens to be part of the topic subject, it's probably a better match.
		'subject' => 15,
		// If the first message of a topic is a match, then the whole topic might be more interesting.
		'first_message' => 10,
	);
*/

	// These vars don't require an interface, the're just here for tweaking.
	$recentPercentage = 0.30;
	$humungousTopicPosts = 200;
	$maxMembersToSearch = 500;

	loadTemplate('Search');

	// Are you allowed?
	isAllowedTo('search_posts');

	// So, we're allowed here, let's clean up the start variable - just incase.
	$_REQUEST['start'] = (int) $_REQUEST['start'];

	require_once($sourcedir . '/Display.php');

	// $search_params will carry all settings that differ from the default search parameters.
	// That way, the URLs involved in a search page will be kept as short as possible.
	$search_params = array();

	if (isset($_REQUEST['params']))
	{
		$temp_params = explode('|"|', base64_decode($_REQUEST['params']));
		foreach ($temp_params as $i => $data)
		{
			list ($k, $v) = explode('|\'|', $data);
			$search_params[$k] = stripslashes($v);
		}
		if (isset($search_params['brd']))
			$search_params['brd'] = $search_params['brd'] == '' ? array() : explode(',', $search_params['brd']);
	}

	// 1 => 'allwords' (default, don't set as param) / 2 => 'anywords'.
	if (!empty($search_params['searchtype']) || (!empty($_REQUEST['searchtype']) && $_REQUEST['searchtype'] == 2))
		$search_params['searchtype'] = 2;

	// Minimum age of messages. Default to zero (don't set param in that case).
	if (!empty($search_params['minage']) || (!empty($_REQUEST['minage']) && $_REQUEST['minage'] > 0))
		$search_params['minage'] = !empty($search_params['minage']) ? (int) $search_params['minage'] : (int) $_REQUEST['minage'];

	// Maximum age of messages. Default to infinite (9999 days: param not set).
	if (!empty($search_params['maxage']) || (!empty($_REQUEST['maxage']) && $_REQUEST['maxage'] != 9999))
		$search_params['maxage'] = !empty($search_params['maxage']) ? (int) $search_params['maxage'] : (int) $_REQUEST['maxage'];

	$timeAddition = '';
	$timeAddition .= !empty($search_params['minage']) ? ' AND m.posterTime <= ' . (time() - $search_params['minage'] * 86400) : '';
	$timeAddition .= !empty($search_params['maxage']) ? ' AND m.posterTime >= ' . (time() - $search_params['maxage'] * 86400) : '';

	// Default the user name to a wildcard matching every user (*).
	if (!empty($search_params['userspec']) || (!empty($_REQUEST['userspec']) && $_REQUEST['userspec'] != '*'))
		$search_params['userspec'] = isset($search_params['userspec']) ? $search_params['userspec'] : $_REQUEST['userspec'];

	// If there's no specific user, then don't mention it in the main query.
	if (empty($search_params['userspec']))
		$userQuery = '';
	else
	{
		$userString = strtolower(addslashes(strtr($search_params['userspec'], array('%' => '\%', '_' => '\_', '*' => '%', '?' => '_'))));
		// Retrieve a list of possible members.
		$request = db_query("
			SELECT ID_MEMBER
			FROM {$db_prefix}members
			WHERE realName LIKE '$userString'", __FILE__, __LINE__);
		// Simply do nothing if there're too many members matching the criteria.
		if (mysql_num_rows($request) > $maxMembersToSearch)
			$userQuery = '';
		elseif (mysql_num_rows($request) == 0)
			$userQuery = "m.ID_MEMBER = 0 AND m.posterName LIKE '$userString'";
		else
		{
			$memberlist = array();
			while ($row = mysql_fetch_assoc($request))
				$memberlist[] = $row['ID_MEMBER'];
			$userQuery = "(m.ID_MEMBER IN (" . implode(', ', $memberlist) . ") OR (m.ID_MEMBER = 0 AND m.posterName LIKE '$userString'))";
		}
	}

	// If the boards were passed by URL (params=), temporarily put them back in $_REQUEST.
	if (!empty($search_params['brd']) && is_array($search_params['brd']))
		$_REQUEST['brd'] = $search_params['brd'];

	// Make sure all boards are integers.
	if (!empty($_REQUEST['brd']))
		foreach ($_REQUEST['brd'] as $id => $brd)
			$_REQUEST['brd'][$id] = (int) $brd;

	// Select all boards you've selected AND are allowed to see.
	if ($user_info['is_admin'])
		$search_params['brd'] = empty($_REQUEST['brd']) ? array() : $_REQUEST['brd'];
	else
	{
		$request = db_query("
			SELECT b.ID_BOARD
			FROM {$db_prefix}boards AS b
			WHERE $user_info[query_see_board]" . (empty($_REQUEST['brd']) ? '' : "
				AND b.ID_BOARD IN (" . implode(', ', $_REQUEST['brd']) . ")"), __FILE__, __LINE__);
		$search_params['brd'] = array();
		while ($row = mysql_fetch_assoc($request))
			$search_params['brd'][] = $row['ID_BOARD'];
		mysql_free_result($request);

		// This error should pro'bly only happen for hackers.
		if (empty($search_params['brd']))
			fatal_lang_error('search_no_boards');
	}

	// If we've selected all boards, this parameter can be left empty.
	$request = db_query("
		SELECT COUNT(ID_BOARD)
		FROM {$db_prefix}boards", __FILE__, __LINE__);
	list ($num_boards) = mysql_fetch_row($request);
	mysql_free_result($request);

	if ($num_boards == count($search_params['brd']))
		$search_params['brd'] = array();
	// Make sure these all boards are numbers.
	elseif (!empty($search_params['brd']))
	{
		foreach ($search_params['brd'] as $k => $v)
			$search_params['brd'][$k] = (int) $v;
	}

	$search_params['show_complete'] = !empty($search_params['show_complete']) || !empty($_REQUEST['show_complete']);
	$search_params['subject_only'] = !empty($search_params['subject_only']) || !empty($_REQUEST['subject_only']);

	$context['compact'] = !$search_params['show_complete'];

	// What are we searching for?
	$search_params['search'] = !empty($search_params['search']) ? $search_params['search'] : (isset($_REQUEST['search']) ? stripslashes($_REQUEST['search']) : '');
	// Nothing??
	if (!isset($search_params['search']) || $search_params['search'] == '')
		fatal_lang_error('no_valid_search_string', false);

	// Extract phrase parts first (e.g. some words "this is a phrase" some more words.)
	preg_match_all('/(?:^|\s)"([^"]+)"(?:$|\s)/u', $search_params['search'], $matches, PREG_PATTERN_ORDER);
	$searchArray = $matches[1];

	// Remove the phrase parts and extract the words.
	$searchArray = array_merge($searchArray, explode(' ', preg_replace('/(?:^|\s)"([^"]+)"(?:$|\s)/u', ' ', $search_params['search'])));

	// Trim everything and make sure there are no words that are the same.
	foreach ($searchArray as $index => $value)
	{
		$searchArray[$index] = addslashes(strtolower(trim($value)));
		if (!isset($searchArray[$index]) || $searchArray[$index] == '')
			unset($searchArray[$index]);
	}
	$searchArray = array_slice(array_unique($searchArray), 0, 10);

	if (empty($searchArray))
		fatal_lang_error('no_valid_search_string', false);

	// Each word is matched against the body and the subject.
	$searchParts = array();
	foreach ($searchArray as $word)
	{
		if (empty($modSettings['search_match_complete_words']))
			$searchParts[] = " LIKE '%" . strtr($word, array('_' => '\\_', '%' => '\\%')) . "%'";
		else
			$searchParts[] = " RLIKE '[[:<:]]" . addcslashes(preg_replace(array('/([\[\]$.+?|{}])/u', '/\*/'), array('[$1]', '.+'), $word), '') . "[[:>:]]'";
	}

	$searchQuery = 0;

	// Either all words must match (searchtype == 1) or any of the words (searchtype == 2).
	if (empty($search_params['searchtype']))
	{
		if (!$search_params['subject_only'])
			$searchQuery = 'm.body' . implode(' AND m.body', $searchParts) . $timeAddition;
		$topicQuery = 'm.subject' . implode(' AND m.subject', $searchParts) . $timeAddition;
	}
	else
	{
		if (!$search_params['subject_only'])
			$searchQuery = (count($searchParts) > 1 ? '(' : '') . 'm.body' . implode(' OR m.body', $searchParts) . (count($searchParts) > 1 ? ')' : '') . $timeAddition;
		$topicQuery = (count($searchParts) > 1 ? '(' : '') . 'm.subject' . implode(' OR m.subject', $searchParts) . (count($searchParts) > 1 ? ')' : '') . $timeAddition;
	}

	// Get the sorting parameters right. Default to sort by relevance descending.
	$sort_columns = array(
		'relevance',
		'numReplies',
		'ID_MSG',
	);
	if (empty($search_params['sort']) && !empty($_REQUEST['sort']))
		list ($search_params['sort'], $search_params['sort_dir']) = array_pad(explode('|', $_REQUEST['sort']), 2, '');
	$search_params['sort'] = !empty($search_params['sort']) && in_array($search_params['sort'], $sort_columns) ? $search_params['sort'] : 'relevance';
	$search_params['sort_dir'] = !empty($search_params['sort_dir']) && $search_params['sort_dir'] == 'asc' ? 'asc' : 'desc';

	$context['mark'] = array();
	foreach ($searchArray as $word)
		$context['mark'][$word] = '<b class="highlight">' . $word . '</b>';

	// All search params have been checked, let's compile them to a single string... made less simple by PHP 4.3.9 and below.
	$temp_params = $search_params;
	if (isset($temp_params['brd']))
		$temp_params['brd'] = implode(',', $temp_params['brd']);
	$context['params'] = array();
	foreach ($temp_params as $k => $v)
		$context['params'][] = $k . '|\'|' . addslashes($v);
	$context['params'] = base64_encode(implode('|"|', $context['params']));

	// ... and add the links to the link tree.
	$context['linktree'][] = array(
		'url' => $scripturl . '?action=search;params=' . $context['params'],
		'name' => $txt[182]
	);
	$context['linktree'][] = array(
		'url' => $scripturl . '?action=search2;params=' . $context['params'],
		'name' => $txt['search_results']
	);

	// Determine some values needed to calculate the relevance.
	$minMsg = (int) (1 - $recentPercentage) * $modSettings['maxMsgID'];
	$recentMsg = $modSettings['maxMsgID'] - $minMsg;

	$mainQuery = "
				t.ID_TOPIC,
				$weight[frequency] * IF(m.ID_MSG IS NOT NULL, COUNT(m.ID_MSG) / (t.numReplies + 1), 0) +
				$weight[age] * IF(m.ID_MSG IS NULL OR MAX(m.ID_MSG) < $minMsg, 0, (MAX(m.ID_MSG) - $minMsg) / $recentMsg) +
				$weight[length] * IF(t.numReplies < $humungousTopicPosts, t.numReplies / $humungousTopicPosts, 1) +
				$weight[subject] * t.is_subject +
				$weight[first_message] * IF(MIN(m.ID_MSG) = t.ID_FIRST_MSG, 1, 0) AS relevance,
				IF(COUNT(m.ID_MSG) = 0, t.ID_FIRST_MSG, MAX(m.ID_MSG)) AS ID_MSG, COUNT(m.ID_MSG) AS num_matches
			FROM {$db_prefix}matches AS t
				LEFT JOIN {$db_prefix}messages AS m ON (m.ID_TOPIC = t.ID_TOPIC AND $searchQuery)" . (empty($userQuery) ? '' : "
			WHERE $userQuery") ."
			GROUP BY t.ID_TOPIC";

	$context['topics'] = array();
	$use_cache = !empty($modSettings['search_cache_size']);

	// Either the results are not cached, or caching is disabled, so we need to create a temporary table.
	if (!$use_cache || empty($_SESSION['search_cache']) || $_SESSION['search_cache']['params'] != $context['params'])
	{
		// Temporary tables are preferrable, but require the right MySQL permissions.
		if (empty($modSettings['disableTemporaryTables']))
		{
			// Get rid of it if it already exists.
			mysql_query("
				DROP TABLE {$db_prefix}matches");

			$result = mysql_query("
				CREATE TEMPORARY TABLE {$db_prefix}matches (
					ID_TOPIC mediumint(8) unsigned NOT NULL default '0',
					ID_FIRST_MSG int(10) unsigned NOT NULL default '0',
					numReplies int(11) NOT NULL default '0',
					is_subject tinyint(3) unsigned NOT NULL default '0',
					PRIMARY KEY (ID_TOPIC)
				) TYPE=HEAP");

			if ($result === false)
			{
				updateSettings(array('disableTemporaryTables' => '1'));
				fatal_lang_error('unable_to_create_temporary');
			}

			if (!$search_params['subject_only'])
			{
				// Let's determine how many results we can expect.
				db_query("
					INSERT INTO {$db_prefix}matches
					SELECT DISTINCT t.ID_TOPIC, t.ID_FIRST_MSG, t.numReplies, 0 AS is_subject
					FROM {$db_prefix}topics AS t, {$db_prefix}messages AS m
					WHERE t.ID_TOPIC = m.ID_TOPIC" . (empty($search_params['brd']) ? '' : "
						AND m.ID_BOARD IN (" . implode(', ', $search_params['brd']) . ")") . (empty($userQuery) ? '' : "
						AND $userQuery") . "
						AND $searchQuery", __FILE__, __LINE__);
				$messageMatches = db_affected_rows();
			}

			// Select all topics that have subjects matching the search query.
			db_query("
				INSERT IGNORE INTO {$db_prefix}matches
				SELECT m.ID_TOPIC AS ID_TOPIC, m.ID_MSG, t.numReplies, 1 AS is_subject
				FROM {$db_prefix}topics AS t, {$db_prefix}messages AS m
				WHERE m.ID_MSG = t.ID_FIRST_MSG" . (empty($search_params['brd']) ? '' : "
					AND m.ID_BOARD IN (" . implode(', ', $search_params['brd']) . ")") . (empty($userQuery) ? '' : "
					AND $userQuery") . "
					AND $topicQuery", __FILE__, __LINE__);
			$subjectMatches = db_affected_rows();

			if (!$search_params['subject_only'])
			{
				$request = db_query("
					SELECT COUNT(ID_TOPIC)
					FROM {$db_prefix}matches", __FILE__, __LINE__);
				list ($numResults) = mysql_fetch_row($request);
				mysql_free_result($request);
			}
			else
				$numResults = $subjectMatches;

			if (empty($numResults))
				$use_cache = false;
			elseif (!$use_cache || $numResults <= $modSettings['search_results_per_page'])
			{
				$request = db_query("
					SELECT$mainQuery
					ORDER BY $search_params[sort] $search_params[sort_dir]
					LIMIT $_REQUEST[start], $modSettings[search_results_per_page]", __FILE__, __LINE__);
				while ($row = mysql_fetch_assoc($request))
					$context['topics'][$row['ID_MSG']] = array(
						'id' => $row['ID_TOPIC'],
						'relevance' => round(100 * $row['relevance'] / $weight_total, 1) . '%',
						'num_matches' => $row['num_matches'],
						'matches' => array(),
					);
				mysql_free_result($request);

				// We don't need cache, thank you.
				$use_cache = false;
			}
			// Search is not yet cached, let's cache it.
			elseif ($use_cache)
			{
				$modSettings['search_pointer'] = empty($modSettings['search_pointer']) ? 0 : (int) $modSettings['search_pointer'];

				// Increase the pointer.
				updateSettings(array('search_pointer' => $modSettings['search_pointer'] >= 255 ? 0 : $modSettings['search_pointer'] + 1));

				// Make sure this value isn't larger than 255 or the tinyint key field wouldn't be able to handle it.
				$modSettings['search_cache_size'] = empty($modSettings['search_cache_size']) || $modSettings['search_cache_size'] > 255 ? 255 : $modSettings['search_cache_size'];

				// Remove old cached results and (if set) the previous session cached result.
				db_query("
					DELETE FROM {$db_prefix}log_search
					WHERE (ID_SEARCH >= $modSettings[search_pointer]" . ($modSettings['search_pointer'] < $modSettings['search_cache_size'] ? '
						AND ID_SEARCH < ' . (256 + $modSettings['search_pointer'] - $modSettings['search_cache_size']) : '
						OR ID_SEARCH < ' . ($modSettings['search_pointer'] - $modSettings['search_cache_size'])) . ')' . (isset($_SESSION['search_cache']['ID_SEARCH']) ? "
						OR ID_SEARCH = " . $_SESSION['search_cache']['ID_SEARCH'] : ''), __FILE__, __LINE__);

				// Insert the new cached results.
				if (!empty($numResults))
					db_query("
						INSERT IGNORE INTO {$db_prefix}log_search
							(ID_SEARCH, ID_TOPIC, relevance, ID_MSG, num_matches)
						SELECT $modSettings[search_pointer], $mainQuery", __FILE__, __LINE__);
				$numResults = empty($numResults) ? 0 : db_affected_rows();

				// Store it for the session.
				$_SESSION['search_cache'] = array(
					'ID_SEARCH' => $modSettings['search_pointer'],
					'num_results' => $numResults,
					'params' => $context['params'],
				);
			}

			// Get rid of the temporary table.
			$request = db_query("
				DROP TABLE {$db_prefix}matches", __FILE__, __LINE__);
		}
		// Create temporary tables is disabled, we're gonna need to use PHP's memory and sorting.
		else
		{
			$matchingTopics = array();

			if (!$search_params['subject_only'])
			{
				// Get all the topics with a message match.
				$request = db_query("
					SELECT DISTINCT t.ID_TOPIC, t.ID_FIRST_MSG, t.numReplies, 0 AS is_subject
					FROM {$db_prefix}topics AS t, {$db_prefix}messages AS m
					WHERE t.ID_TOPIC = m.ID_TOPIC" . (empty($search_params['brd']) ? '' : "
						AND m.ID_BOARD IN (" . implode(', ', $search_params['brd']) . ")") . (empty($userQuery) ? '' : "
						AND $userQuery") . "
						AND $searchQuery", __FILE__, __LINE__);
				while ($row = mysql_fetch_assoc($request))
					$matchingTopics[$row['ID_TOPIC']] = $row;
				$messageMatches = count($matchingTopics);
			}

			// Get all the topics with a subject match.
			$request = db_query("
				SELECT m.ID_TOPIC AS ID_TOPIC, t.ID_FIRST_MSG, t.numReplies, 1 AS is_subject
				FROM {$db_prefix}topics AS t, {$db_prefix}messages AS m
				WHERE m.ID_MSG = t.ID_FIRST_MSG" . (empty($search_params['brd']) ? '' : "
					AND m.ID_BOARD IN (" . implode(', ', $search_params['brd']) . ")") . (empty($userQuery) ? '' : "
					AND $userQuery") . "
					AND $topicQuery", __FILE__, __LINE__);
			while ($row = mysql_fetch_assoc($request))
				$matchingTopics[$row['ID_TOPIC']] = $row;
			$subjectMatches = db_affected_rows();

			$numResults = $search_params['subject_only'] ? $subjectMatches : count($matchingTopics);

			if (empty($numResults))
				$use_cache = false;
			elseif (!$use_cache || $numResults <= $modSettings['search_results_per_page'])
			{
				$sort = array();
				$request = db_query("
					SELECT
						t.ID_TOPIC, COUNT(m.ID_MSG) AS numMsg, MAX(m.ID_MSG) AS lastMatch, MIN(m.ID_MSG) AS firstMatch,
						COUNT(m.ID_MSG) AS num_matches
					FROM {$db_prefix}topics AS t
						LEFT JOIN {$db_prefix}messages AS m ON (m.ID_TOPIC = t.ID_TOPIC AND $searchQuery)" . (empty($userQuery) ? '
					WHERE t.ID_TOPIC IN (' . implode(', ', array_keys($matchingTopics)) . ')' : "
					WHERE $userQuery
						AND t.ID_TOPIC IN (" . implode(', ', array_keys($matchingTopics)) . ")") . "
					GROUP BY t.ID_TOPIC
					LIMIT $_REQUEST[start], $modSettings[search_results_per_page]", __FILE__, __LINE__);
				$tmp = array();
				while ($row = mysql_fetch_assoc($request))
				{
					$relevance =
						$weight['frequency'] * ($row['numMsg'] == 0 ? 0 : $row['numMsg'] / ($matchingTopics[$row['ID_TOPIC']]['numReplies'] + 1)) +
						$weight['age'] * (!isset($row['ID_MSG']) || $row['ID_MSG'] === null || $row['lastMatch'] < $minMsg ? 0 : ($row['lastMatch'] - $minMsg) / $recentMsg) +
						$weight['length'] * ($matchingTopics[$row['ID_TOPIC']]['numReplies'] < $humungousTopicPosts ? $matchingTopics[$row['ID_TOPIC']]['numReplies'] / $humungousTopicPosts : 1) +
						$weight['subject'] * $matchingTopics[$row['ID_TOPIC']]['is_subject'] +
						$weight['first_message'] * ($row['firstMatch'] == $matchingTopics[$row['ID_TOPIC']]['ID_FIRST_MSG'] ? 1 : 0);
					$ID_MSG = $row['numMsg'] == 0 ? $matchingTopics[$row['ID_TOPIC']]['ID_FIRST_MSG'] : $row['lastMatch'];
					$sort[$ID_MSG] = $search_params['sort'] == 'relevance' ? $relevance : ($search_params['sort'] == 'numReplies' ? $matchingTopics[$row['ID_TOPIC']]['numReplies'] : $ID_MSG);
					$tmp[$ID_MSG] = array(
						'id' => $row['ID_TOPIC'],
						'relevance' => round(100 * $relevance / $weight_total, 1) . '%',
						'num_matches' => $row['num_matches'],
						'matches' => array(),
					);
				}
				mysql_free_result($request);

				// Do the manual sorting.
				if ($search_params['sort_dir'] == 'desc')
					krsort($sort);
				else
					ksort($sort);
				foreach ($sort as $ID_MSG => $value)
					$context['topics'][$ID_MSG] = $tmp[$ID_MSG];
				unset($tmp);

				// We don't need cache, thank you.
				$use_cache = false;
			}
			// Search is not yet cached, let's do that now.
			elseif ($use_cache)
			{
				$modSettings['search_pointer'] = empty($modSettings['search_pointer']) ? 0 : (int) $modSettings['search_pointer'];

				$modSettings['search_cache_size'] = empty($modSettings['search_cache_size']) || $modSettings['search_cache_size'] > 255 ? 255 : $modSettings['search_cache_size'];

				// Remove old cached results.
				db_query("
					DELETE FROM {$db_prefix}log_search
					WHERE (ID_SEARCH >= $modSettings[search_pointer]" . ($modSettings['search_pointer'] < $modSettings['search_cache_size'] ? '
						AND ID_SEARCH < ' . (256 + $modSettings['search_pointer'] - $modSettings['search_cache_size']) : '
						OR ID_SEARCH < ' . ($modSettings['search_pointer'] - $modSettings['search_cache_size'])) . ')' . (isset($_SESSION['search_cache']['ID_SEARCH']) ? "
						OR ID_SEARCH = " . $_SESSION['search_cache']['ID_SEARCH'] : ''), __FILE__, __LINE__);

				// Insert the new results into cache.
				if (!empty($numResults))
				{
					$insertRows = array();
					$sort = array();

					// Get all topics that match the search query.
					$request = db_query("
						SELECT
							t.ID_TOPIC, COUNT(m.ID_MSG) AS numMsg, MAX(m.ID_MSG) AS lastMatch, MIN(m.ID_MSG) AS firstMatch
						FROM {$db_prefix}topics AS t
							LEFT JOIN {$db_prefix}messages AS m ON (m.ID_TOPIC = t.ID_TOPIC AND $searchQuery)" . (empty($userQuery) ? '
						WHERE t.ID_TOPIC IN (' . implode(', ', array_keys($matchingTopics)) . ')' : "
						WHERE $userQuery
							AND t.ID_TOPIC IN (" . implode(', ', array_keys($matchingTopics)) . ")") . "
						GROUP BY t.ID_TOPIC", __FILE__, __LINE__);
					$tmp = array();
					while ($row = mysql_fetch_assoc($request))
					{
						$relevance =
							$weight['frequency'] * ($row['numMsg'] == 0 ? 0 : $row['numMsg'] / ($matchingTopics[$row['ID_TOPIC']]['numReplies'] + 1)) +
							$weight['age'] * (!isset($row['ID_MSG']) || $row['ID_MSG'] === null || $row['lastMatch'] < $minMsg ? 0 : ($row['lastMatch'] - $minMsg) / $recentMsg) +
							$weight['length'] * ($matchingTopics[$row['ID_TOPIC']]['numReplies'] < $humungousTopicPosts ? $matchingTopics[$row['ID_TOPIC']]['numReplies'] / $humungousTopicPosts : 1) +
							$weight['subject'] * $matchingTopics[$row['ID_TOPIC']]['is_subject'] +
							$weight['first_message'] * ($row['firstMatch'] == $matchingTopics[$row['ID_TOPIC']]['ID_FIRST_MSG'] ? 1 : 0);
						$ID_MSG = $row['numMsg'] == 0 ? $matchingTopics[$row['ID_TOPIC']]['ID_FIRST_MSG'] : $row['lastMatch'];
						$sort[$ID_MSG] = $search_params['sort'] == 'relevance' ? $relevance : ($search_params['sort'] == 'numReplies' ? $matchingTopics[$row['ID_TOPIC']]['numReplies'] : $ID_MSG);
						$tmp[$ID_MSG] = (isset($tmp[$ID_MSG]) ? $tmp[$ID_MSG] : '') . '(' . (int) $modSettings['search_pointer'] . ', ' . (int) $row['ID_TOPIC'] . ', ' . (int) $relevance . ', ' . (int) $ID_MSG . ', ' . (int) $row['numMsg'] . '),';
					}
					mysql_free_result($request);

					// Do the sorting of the rows.
					if ($search_params['sort_dir'] == 'desc')
						krsort($sort);
					else
						ksort($sort);
					foreach ($sort as $ID_MSG => $value)
						$insertRows[$ID_MSG] = mb_substr($tmp[$ID_MSG], 0, -1);
					unset($tmp);

					// Insert the matching topics into the cache.
					if (!empty($insertRows))
						db_query("
							INSERT INTO {$db_prefix}log_search
								(ID_SEARCH, ID_TOPIC, relevance, ID_MSG, num_matches)
							VALUES " . implode(', ', $insertRows), __FILE__, __LINE__);
				}
				$numResults = empty($numResults) ? 0 : db_affected_rows();

				// Store the cache information into the session.
				$_SESSION['search_cache'] = array(
					'ID_SEARCH' => $modSettings['search_pointer'],
					'num_results' => $numResults,
					'params' => $context['params'],
				);

				// Increase the cache pointer.
				updateSettings(array('search_pointer' => $modSettings['search_pointer'] >= 255 ? 0 : $modSettings['search_pointer'] + 1));
			}
		}
	}

	// Current search should be cached by now, grab it.
	if ($use_cache)
	{
		$request = db_query("
			SELECT ls.ID_TOPIC, ls.ID_MSG, ls.relevance, ls.num_matches
			FROM {$db_prefix}log_search AS ls" . ($search_params['sort'] == 'numReplies' ? ", {$db_prefix}topics AS t" : '') . "
			WHERE ID_SEARCH = " . $_SESSION['search_cache']['ID_SEARCH'] . ($search_params['sort'] == 'numReplies' ? "
				AND t.ID_TOPIC = ls.ID_TOPIC" : '') . "
			ORDER BY $search_params[sort] $search_params[sort_dir]
			LIMIT $_REQUEST[start], $modSettings[search_results_per_page]", __FILE__, __LINE__);
		while ($row = mysql_fetch_assoc($request))
			$context['topics'][$row['ID_MSG']] = array(
				'id' => $row['ID_TOPIC'],
				'relevance' => round($row['relevance'] / 10, 1) . '%',
				'num_matches' => $row['num_matches'],
				'matches' => array(),
			);
		mysql_free_result($request);

		$numResults = $_SESSION['search_cache']['num_results'];
	}

	// Now that we know how many results to expect we can start calculating the page numbers.
	$context['page_index'] = constructPageIndex($scripturl . '?action=search2;params=' . $context['params'], $_REQUEST['start'], $numResults, $modSettings['search_results_per_page'], false);

	if (!empty($context['topics']))
	{
		// Create an array for the permissions.
		$boards_can = array(
			'post_reply_own' => boardsAllowedTo('post_reply_own'),
			'post_reply_any' => boardsAllowedTo('post_reply_any'),
			'mark_any_notify' => boardsAllowedTo('mark_any_notify')
		);

		// Load the posters...
		$request = db_query("
			SELECT ID_MEMBER
			FROM {$db_prefix}messages
			WHERE ID_MEMBER != 0
				AND ID_MSG IN (" . implode(', ', array_keys($context['topics'])) . ')', __FILE__, __LINE__);
		$posters = array();
		while ($row = mysql_fetch_assoc($request))
			$posters[] = $row['ID_MEMBER'];
		mysql_free_result($request);

		if (!empty($posters))
			loadMemberData(array_unique($posters));

		// Get the messages out for the callback - select enough that it can be made to look just like Display.
		$messages_request = db_query("
			SELECT
				m.ID_MSG, m.subject, m.posterName, m.posterEmail, m.posterTime, m.ID_MEMBER,
				m.icon, m.posterIP, m.body, m.smileysEnabled, m.modifiedTime, m.modifiedName,
				a.filename, IFNULL(a.size, 0) AS filesize, a.ID_ATTACH, a.downloads,
				first_m.ID_MSG AS first_msg, first_m.subject AS first_subject, first_m.icon, first_m.posterTime AS first_posterTime,
				first_mem.ID_MEMBER AS first_member_id, IFNULL(first_mem.realName, first_m.posterName) AS first_member_name,
				last_m.ID_MSG AS last_msg, last_m.posterTime AS last_posterTime, last_mem.ID_MEMBER AS last_member_id,
				IFNULL(last_mem.realName, last_m.posterName) AS last_member_name,
				t.ID_TOPIC, t.isSticky, t.locked, t.ID_POLL, t.numReplies, t.numViews,
				b.ID_BOARD, b.name AS bName, c.ID_CAT, c.name AS cName
			FROM ({$db_prefix}messages AS m, {$db_prefix}topics AS t, {$db_prefix}boards AS b, {$db_prefix}categories AS c, {$db_prefix}messages AS first_m, {$db_prefix}messages AS last_m)
				LEFT JOIN {$db_prefix}members AS first_mem ON (first_mem.ID_MEMBER = first_m.ID_MEMBER)
				LEFT JOIN {$db_prefix}members AS last_mem ON (last_mem.ID_MEMBER = first_m.ID_MEMBER)
				LEFT JOIN {$db_prefix}attachments AS a ON (a.ID_MSG = m.ID_MSG)
			WHERE m.ID_MSG IN (" . implode(', ', array_keys($context['topics'])) . ")
				AND t.ID_TOPIC = m.ID_TOPIC
				AND b.ID_BOARD = t.ID_BOARD
				AND c.ID_CAT = b.ID_CAT
				AND first_m.ID_MSG = t.ID_FIRST_MSG
				AND last_m.ID_MSG = t.ID_LAST_MSG
			ORDER BY FIND_IN_SET(m.ID_MSG, '" . implode(',', array_keys($context['topics'])) . "')
			LIMIT " . count($context['topics']), __FILE__, __LINE__);
		// Note that the reg-exp slows things alot, but makes things make a lot more sense.
	}

	$context['key_words'] = &$searchArray;

	// Set the basic stuff for the template.
	$context['allow_hide_email'] = !empty($modSettings['allow_hideEmail']);

	$context['sub_template'] = 'results';
	$context['page_title'] = $txt[166];
	$context['get_topics'] = 'prepareSearchContext';
	$context['can_send_pm'] = allowedTo('pm_send');

	loadJumpTo();
}

// Callback to return messages - saves memory.
function prepareSearchContext($reset = false)
{
	global $txt, $modSettings, $scripturl, $ID_MEMBER;
	global $themeUser, $context, $messages_request, $db_prefix, $attachments, $boards_can;

	// Remember which message this is.  (ie. reply #83)
	static $counter = null;
	if ($counter == null || $reset)
		$counter = $_REQUEST['start'] + 1;

	// If the query returned false, bail.
	if ($messages_request == false)
		return false;

	// Start from the beginning...
	if ($reset)
		return @mysql_data_seek($messages_request, 0);

	// Attempt to get the next message.
	$message = mysql_fetch_assoc($messages_request);
	if (!$message)
		return false;

	// Can't have an empty subject can we?
	$message['subject'] = $message['subject'] != '' ? $message['subject'] : $txt[24];
	$message['first_subject'] = $message['first_subject'] != '' ? $message['first_subject'] : $txt[24];

	// If it couldn't load, or the user was a guest.... someday may be done with a guest table.
	if (!loadMemberContext($message['ID_MEMBER']))
	{
		// Notice this information isn't used anywhere else.... *cough guest table cough*
		$themeUser[$message['ID_MEMBER']]['name'] = $message['posterName'];
		$themeUser[$message['ID_MEMBER']]['id'] = 0;
		$themeUser[$message['ID_MEMBER']]['group'] = $txt[28];
		$themeUser[$message['ID_MEMBER']]['link'] = $message['posterName'];
		$themeUser[$message['ID_MEMBER']]['email'] = $message['posterEmail'];
	}
	$themeUser[$message['ID_MEMBER']]['ip'] = $message['posterIP'];

	// Do the censor thang...
	censorText($message['body']);
	censorText($message['subject']);

	censorText($message['first_subject']);

	// Shorten this message if necessary.
	if ($context['compact'])
	{
		// Set the number of characters before and after the searched keyword.
		$charLimit = 40;

		$message['body'] = strtr($message['body'], array("\n" => ' ', '<br />' => "\n"));
		$message['body'] = doUBBC($message['body'], $message['smileysEnabled']);
		$message['body'] = strip_tags($message['body']);

		if (strlen($message['body']) > $charLimit)
		{
			if (empty($context['key_words']))
				$message['body'] = htmlspecialchars(mb_substr(un_htmlspecialchars($message['body']), 0, $charLimit) . (mb_strlen($message['body']) > $charLimit ? '<b>...</b>' : ''), ENT_QUOTES);
			else
			{
				$matchString = '';
				foreach ($context['key_words'] as $keyword)
					$matchString .= strtr(preg_quote($keyword, '/'), array('\*' => '.+?')) . '|';
				$matchString = mb_substr($matchString, 0, -1);
				$message['body'] = un_htmlspecialchars(str_replace('&nbsp;', ' ', $message['body']));
				if (empty($modSettings['search_match_complete_words']))
					preg_match_all('/([^\s\W]{' . $charLimit . '}[\s\W]|[\s\W].{0,' . $charLimit . '}?|^)(' . $matchString . ')(.{0,' . $charLimit . '}[\s\W]|[^\s\W]{' . $charLimit . '})/ius', $message['body'], $matches);
				else
					preg_match_all('/([^\s\W]{' . $charLimit . '}[\s\W]|[\s\W].{0,' . $charLimit . '}?[\s\W]|^)(' . $matchString . ')([\s\W].{0,' . $charLimit . '}[\s\W]|[\s\W][^\s\W]{' . $charLimit . '})/isu', $message['body'], $matches);
				$message['body'] = '';
				foreach ($matches[0] as $index => $match)
					$message['body'] .= '<b>...</b>&nbsp;' . htmlspecialchars($match, ENT_QUOTES) . '&nbsp;<b>...</b><br />';
			}

			// Re-fix the international characters.
			$message['body'] = preg_replace('~&amp;#(\d{4,5}|[3-9]\d{2,4}|2[6-9]\d);~u', '&#$1;', $message['body']);
		}
	}
	else
	{
		// Run UBBC interpreter on the message.
		$message['body'] = doUBBC($message['body'], $message['smileysEnabled']);
	}

	$output = array_merge($context['topics'][$message['ID_MSG']], array(
		'is_sticky' => !empty($modSettings['enableStickyTopics']) && !empty($message['isSticky']),
		'is_locked' => !empty($message['locked']),
		'is_poll' => $modSettings['pollMode'] == '1' && $message['ID_POLL'] > 0,
		'is_hot' => $message['numReplies'] >= $modSettings['hotTopicPosts'],
		'is_very_hot' => $message['numReplies'] >= $modSettings['hotTopicVeryPosts'],
		'views' => $message['numViews'],
		'replies' => $message['numReplies'],
		'can_reply' => in_array($message['ID_BOARD'], $boards_can['post_reply_any']) || in_array(0, $boards_can['post_reply_any']),
		'can_mark_notify' => in_array($message['ID_BOARD'], $boards_can['mark_any_notify']) || in_array(0, $boards_can['mark_any_notify']),
		'first_post' => array(
			'id' => $message['first_msg'],
			'time' => timeformat($message['first_posterTime']),
			'subject' => $message['first_subject'],
			'href' => $scripturl . '?topic=' . $message['ID_TOPIC'] . '.0',
			'link' => '<a href="' . $scripturl . '?topic=' . $message['ID_TOPIC'] . '.0">' . $message['first_subject'] . '</a>',
			'icon' => $message['icon'],
			'member' => array(
				'id' => $message['first_member_id'],
				'name' => $message['first_member_name'],
				'href' => !empty($message['first_member_id']) ? $scripturl . '?action=profile;u=' . $message['first_member_id'] : '',
				'link' => !empty($message['first_member_id']) ? '<a href="' . $scripturl . '?action=profile;u=' . $message['first_member_id'] . '" title="' . $txt[92] . ' ' . $message['first_member_name'] . '">' . $message['first_member_name'] . '</a>' : $message['first_member_name']
			)
		),
		'last_post' => array(
			'id' => $message['last_msg'],
			'time' => timeformat($message['last_posterTime']),
			'timestamp' => $message['last_posterTime'],
			'member' => array(
				'id' => $message['last_member_id'],
				'name' => $message['last_member_name'],
				'href' => !empty($message['last_member_id']) ? $scripturl . '?action=profile;u=' . $message['last_member_id'] : '',
				'link' => !empty($message['last_member_id']) ? '<a href="' . $scripturl . '?action=profile;u=' . $message['last_member_id'] . '" title="' . $txt[92] . ' ' . $message['last_member_name'] . '">' . $message['last_member_name'] . '</a>' : $message['last_member_name']
			)
		),
		'board' => array(
			'id' => $message['ID_BOARD'],
			'name' => $message['bName'],
			'href' => $scripturl . '?board=' . $message['ID_BOARD'] . '.0',
			'link' => '<a href="' . $scripturl . '?board=' . $message['ID_BOARD'] . '.0">' . $message['bName'] . '</a>'
		),
		'category' => array(
			'id' => $message['ID_CAT'],
			'name' => $message['cName'],
			'href' => $scripturl . '#' . $message['ID_CAT'],
			'link' => '<a href="' . $scripturl . '#' . $message['ID_CAT'] . '">' . $message['cName'] . '</a>'
		)
	));
	determineTopicClass($output);

	$body_highlighted = $message['body'];
	$subject_highlighted = $message['subject'];

	foreach ($context['key_words'] as $query)
	{
		// Fix the international characters in the keyword too.
		$query = preg_replace('~&amp;#(\d{4,5}|[3-9]\d{2,4}|2[6-9]\d);~u', '&#$1;', strtr(preg_quote($query, '/'), array('&' => '&amp;')));

		$body_highlighted = preg_replace('/((<[^>]*)|' . $query . ')/iue', "'\$2' == '\$1' ? stripslashes('\$1') : '<b class=\"highlight\">\$1</b>'", $body_highlighted);
		$subject_highlighted = preg_replace('/((<[^>]*)|' . $query . ')/iue', "'$2' == '$1' ? stripslashes('$1') : '<b class=\"highlight\">$1</b>'", $subject_highlighted);
	}

	$output['matches'][] = array(
		'id' => $message['ID_MSG'],
		'attachment' => loadAttachmentContext($message['ID_MSG']),
		'alternate' => $counter % 2,
		'member' => &$themeUser[$message['ID_MEMBER']],
		'icon' => $message['icon'],
		'subject' => $message['subject'],
		'subject_highlighted' => $subject_highlighted,
		'time' => timeformat($message['posterTime']),
		'timestamp' => $message['posterTime'],
		'counter' => $counter,
		'modified' => array(
			'time' => timeformat($message['modifiedTime']),
			'timestamp' => $message['modifiedTime'],
			'name' => $message['modifiedName']
		),
		'body' => $message['body'],
		'body_highlighted' => $body_highlighted,
		'start' => 'msg' . $message['ID_MSG']
	);
	$counter++;

	return $output;
}

?>
