Issues (1061)

Sources/Search.php (3 issues)

1
<?php
2
3
/**
4
 * Handle all of the searching from here.
5
 *
6
 * Simple Machines Forum (SMF)
7
 *
8
 * @package SMF
9
 * @author Simple Machines https://www.simplemachines.org
10
 * @copyright 2020 Simple Machines and individual contributors
11
 * @license https://www.simplemachines.org/about/smf/license.php BSD
12
 *
13
 * @version 2.1 RC2
14
 */
15
16
if (!defined('SMF'))
17
	die('No direct access...');
18
19
/**
20
 * Ask the user what they want to search for.
21
 * What it does:
22
 * - shows the screen to search forum posts (action=search)
23
 * - uses the main sub template of the Search template.
24
 * - uses the Search language file.
25
 * - requires the search_posts permission.
26
 * - decodes and loads search parameters given in the URL (if any).
27
 * - the form redirects to index.php?action=search2.
28
 */
29
function PlushSearch1()
30
{
31
	global $txt, $scripturl, $modSettings, $user_info, $context, $smcFunc, $sourcedir;
32
33
	// Is the load average too high to allow searching just now?
34
	if (!empty($context['load_average']) && !empty($modSettings['loadavg_search']) && $context['load_average'] >= $modSettings['loadavg_search'])
35
		fatal_lang_error('loadavg_search_disabled', false);
36
37
	loadLanguage('Search');
38
	// Don't load this in XML mode.
39
	if (!isset($_REQUEST['xml']))
40
	{
41
		loadTemplate('Search');
42
		loadJavaScriptFile('suggest.js', array('defer' => false, 'minimize' => true), 'smf_suggest');
43
	}
44
45
	// Check the user's permissions.
46
	isAllowedTo('search_posts');
47
48
	// Link tree....
49
	$context['linktree'][] = array(
50
		'url' => $scripturl . '?action=search',
51
		'name' => $txt['search']
52
	);
53
54
	// This is hard coded maximum string length.
55
	$context['search_string_limit'] = 100;
56
57
	$context['require_verification'] = $user_info['is_guest'] && !empty($modSettings['search_enable_captcha']) && empty($_SESSION['ss_vv_passed']);
58
	if ($context['require_verification'])
59
	{
60
		require_once($sourcedir . '/Subs-Editor.php');
61
		$verificationOptions = array(
62
			'id' => 'search',
63
		);
64
		$context['require_verification'] = create_control_verification($verificationOptions);
65
		$context['visual_verification_id'] = $verificationOptions['id'];
66
	}
67
68
	// If you got back from search2 by using the linktree, you get your original search parameters back.
69
	if (isset($_REQUEST['params']))
70
	{
71
		// Due to IE's 2083 character limit, we have to compress long search strings
72
		$temp_params = base64_decode(str_replace(array('-', '_', '.'), array('+', '/', '='), $_REQUEST['params']));
73
		// Test for gzuncompress failing
74
		$temp_params2 = @gzuncompress($temp_params);
75
		$temp_params = explode('|"|', !empty($temp_params2) ? $temp_params2 : $temp_params);
76
77
		$context['search_params'] = array();
78
		foreach ($temp_params as $i => $data)
79
		{
80
			@list ($k, $v) = explode('|\'|', $data);
81
			$context['search_params'][$k] = $v;
82
		}
83
		if (isset($context['search_params']['brd']))
84
			$context['search_params']['brd'] = $context['search_params']['brd'] == '' ? array() : explode(',', $context['search_params']['brd']);
85
	}
86
87
	if (isset($_REQUEST['search']))
88
		$context['search_params']['search'] = un_htmlspecialchars($_REQUEST['search']);
89
90
	if (isset($context['search_params']['search']))
91
		$context['search_params']['search'] = $smcFunc['htmlspecialchars']($context['search_params']['search']);
92
	if (isset($context['search_params']['userspec']))
93
		$context['search_params']['userspec'] = $smcFunc['htmlspecialchars']($context['search_params']['userspec']);
94
	if (!empty($context['search_params']['searchtype']))
95
		$context['search_params']['searchtype'] = 2;
96
	if (!empty($context['search_params']['minage']))
97
		$context['search_params']['minage'] = (int) $context['search_params']['minage'];
98
	if (!empty($context['search_params']['maxage']))
99
		$context['search_params']['maxage'] = (int) $context['search_params']['maxage'];
100
101
	$context['search_params']['show_complete'] = !empty($context['search_params']['show_complete']);
102
	$context['search_params']['subject_only'] = !empty($context['search_params']['subject_only']);
103
104
	// Load the error text strings if there were errors in the search.
105
	if (!empty($context['search_errors']))
106
	{
107
		loadLanguage('Errors');
108
		$context['search_errors']['messages'] = array();
109
		foreach ($context['search_errors'] as $search_error => $dummy)
110
		{
111
			if ($search_error === 'messages')
112
				continue;
113
114
			if ($search_error == 'string_too_long')
115
				$txt['error_string_too_long'] = sprintf($txt['error_string_too_long'], $context['search_string_limit']);
116
117
			$context['search_errors']['messages'][] = $txt['error_' . $search_error];
118
		}
119
	}
120
121
	// Find all the boards this user is allowed to see.
122
	$request = $smcFunc['db_query']('order_by_board_order', '
123
		SELECT b.id_cat, c.name AS cat_name, b.id_board, b.name, b.child_level
124
		FROM {db_prefix}boards AS b
125
			LEFT JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat)
126
		WHERE {query_see_board}
127
			AND redirect = {string:empty_string}',
128
		array(
129
			'empty_string' => '',
130
		)
131
	);
132
	$context['num_boards'] = $smcFunc['db_num_rows']($request);
133
	$context['boards_check_all'] = true;
134
	$context['categories'] = array();
135
	while ($row = $smcFunc['db_fetch_assoc']($request))
136
	{
137
		// This category hasn't been set up yet..
138
		if (!isset($context['categories'][$row['id_cat']]))
139
			$context['categories'][$row['id_cat']] = array(
140
				'id' => $row['id_cat'],
141
				'name' => $row['cat_name'],
142
				'boards' => array()
143
			);
144
145
		// Set this board up, and let the template know when it's a child.  (indent them..)
146
		$context['categories'][$row['id_cat']]['boards'][$row['id_board']] = array(
147
			'id' => $row['id_board'],
148
			'name' => $row['name'],
149
			'child_level' => $row['child_level'],
150
			'selected' => (empty($context['search_params']['brd']) && (empty($modSettings['recycle_enable']) || $row['id_board'] != $modSettings['recycle_board']) && !in_array($row['id_board'], $user_info['ignoreboards'])) || (!empty($context['search_params']['brd']) && in_array($row['id_board'], $context['search_params']['brd']))
151
		);
152
153
		// If a board wasn't checked that probably should have been ensure the board selection is selected, yo!
154
		if (!$context['categories'][$row['id_cat']]['boards'][$row['id_board']]['selected'] && (empty($modSettings['recycle_enable']) || $row['id_board'] != $modSettings['recycle_board']))
155
			$context['boards_check_all'] = false;
156
	}
157
	$smcFunc['db_free_result']($request);
158
159
	require_once($sourcedir . '/Subs-Boards.php');
160
	sortCategories($context['categories']);
161
162
	// Now, let's sort the list of categories into the boards for templates that like that.
163
	$temp_boards = array();
164
	foreach ($context['categories'] as $category)
165
	{
166
		$temp_boards[] = array(
167
			'name' => $category['name'],
168
			'child_ids' => array_keys($category['boards'])
169
		);
170
		$temp_boards = array_merge($temp_boards, array_values($category['boards']));
171
172
		// Include a list of boards per category for easy toggling.
173
		$context['categories'][$category['id']]['child_ids'] = array_keys($category['boards']);
174
	}
175
176
	$max_boards = ceil(count($temp_boards) / 2);
177
	if ($max_boards == 1)
178
		$max_boards = 2;
179
180
	// Now, alternate them so they can be shown left and right ;).
181
	$context['board_columns'] = array();
182
	for ($i = 0; $i < $max_boards; $i++)
183
	{
184
		$context['board_columns'][] = $temp_boards[$i];
185
		if (isset($temp_boards[$i + $max_boards]))
186
			$context['board_columns'][] = $temp_boards[$i + $max_boards];
187
		else
188
			$context['board_columns'][] = array();
189
	}
190
191
	if (!empty($_REQUEST['topic']))
192
	{
193
		$context['search_params']['topic'] = (int) $_REQUEST['topic'];
194
		$context['search_params']['show_complete'] = true;
195
	}
196
	if (!empty($context['search_params']['topic']))
197
	{
198
		$context['search_params']['topic'] = (int) $context['search_params']['topic'];
199
200
		$context['search_topic'] = array(
201
			'id' => $context['search_params']['topic'],
202
			'href' => $scripturl . '?topic=' . $context['search_params']['topic'] . '.0',
203
		);
204
205
		$request = $smcFunc['db_query']('', '
206
			SELECT subject
207
			FROM {db_prefix}topics AS t
208
				INNER JOIN {db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)
209
			WHERE t.id_topic = {int:search_topic_id}
210
				AND {query_see_message_board} ' . ($modSettings['postmod_active'] ? '
211
				AND t.approved = {int:is_approved_true}' : '') . '
212
			LIMIT 1',
213
			array(
214
				'is_approved_true' => 1,
215
				'search_topic_id' => $context['search_params']['topic'],
216
			)
217
		);
218
219
		if ($smcFunc['db_num_rows']($request) == 0)
220
			fatal_lang_error('topic_gone', false);
221
222
		list ($context['search_topic']['subject']) = $smcFunc['db_fetch_row']($request);
223
		$smcFunc['db_free_result']($request);
224
225
		$context['search_topic']['link'] = '<a href="' . $context['search_topic']['href'] . '">' . $context['search_topic']['subject'] . '</a>';
226
	}
227
228
	$context['page_title'] = $txt['set_parameters'];
229
230
	call_integration_hook('integrate_search');
231
}
232
233
/**
234
 * Gather the results and show them.
235
 * What it does:
236
 * - checks user input and searches the messages table for messages matching the query.
237
 * - requires the search_posts permission.
238
 * - uses the results sub template of the Search template.
239
 * - uses the Search language file.
240
 * - stores the results into the search cache.
241
 * - show the results of the search query.
242
 */
243
function PlushSearch2()
244
{
245
	global $scripturl, $modSettings, $sourcedir, $txt;
246
	global $user_info, $context, $options, $messages_request, $boards_can;
247
	global $excludedWords, $participants, $smcFunc, $cache_enable;
248
249
	// if comming from the quick search box, and we want to search on members, well we need to do that ;)
250
	if (isset($_REQUEST['search_selection']) && $_REQUEST['search_selection'] === 'members')
251
		redirectexit($scripturl . '?action=mlist;sa=search;fields=name,email;search=' . urlencode($_REQUEST['search']));
252
253
	if (!empty($context['load_average']) && !empty($modSettings['loadavg_search']) && $context['load_average'] >= $modSettings['loadavg_search'])
254
		fatal_lang_error('loadavg_search_disabled', false);
255
256
	// No, no, no... this is a bit hard on the server, so don't you go prefetching it!
257
	if (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] == 'prefetch')
258
	{
259
		ob_end_clean();
260
		send_http_status(403);
261
		die;
262
	}
263
264
	$weight_factors = array(
265
		'frequency' => array(
266
			'search' => 'COUNT(*) / (MAX(t.num_replies) + 1)',
267
			'results' => '(t.num_replies + 1)',
268
		),
269
		'age' => array(
270
			'search' => 'CASE WHEN MAX(m.id_msg) < {int:min_msg} THEN 0 ELSE (MAX(m.id_msg) - {int:min_msg}) / {int:recent_message} END',
271
			'results' => 'CASE WHEN t.id_first_msg < {int:min_msg} THEN 0 ELSE (t.id_first_msg - {int:min_msg}) / {int:recent_message} END',
272
		),
273
		'length' => array(
274
			'search' => 'CASE WHEN MAX(t.num_replies) < {int:huge_topic_posts} THEN MAX(t.num_replies) / {int:huge_topic_posts} ELSE 1 END',
275
			'results' => 'CASE WHEN t.num_replies < {int:huge_topic_posts} THEN t.num_replies / {int:huge_topic_posts} ELSE 1 END',
276
		),
277
		'subject' => array(
278
			'search' => 0,
279
			'results' => 0,
280
		),
281
		'first_message' => array(
282
			'search' => 'CASE WHEN MIN(m.id_msg) = MAX(t.id_first_msg) THEN 1 ELSE 0 END',
283
		),
284
		'sticky' => array(
285
			'search' => 'MAX(t.is_sticky)',
286
			'results' => 't.is_sticky',
287
		),
288
	);
289
290
	call_integration_hook('integrate_search_weights', array(&$weight_factors));
291
292
	$weight = array();
293
	$weight_total = 0;
294
	foreach ($weight_factors as $weight_factor => $value)
295
	{
296
		$weight[$weight_factor] = empty($modSettings['search_weight_' . $weight_factor]) ? 0 : (int) $modSettings['search_weight_' . $weight_factor];
297
		$weight_total += $weight[$weight_factor];
298
	}
299
300
	// Zero weight.  Weightless :P.
301
	if (empty($weight_total))
302
		fatal_lang_error('search_invalid_weights');
303
304
	// These vars don't require an interface, they're just here for tweaking.
305
	$recentPercentage = 0.30;
306
	$humungousTopicPosts = 200;
307
	$maxMembersToSearch = 500;
308
	$maxMessageResults = empty($modSettings['search_max_results']) ? 0 : $modSettings['search_max_results'] * 5;
309
310
	// Start with no errors.
311
	$context['search_errors'] = array();
312
313
	// Number of pages hard maximum - normally not set at all.
314
	$modSettings['search_max_results'] = empty($modSettings['search_max_results']) ? 200 * $modSettings['search_results_per_page'] : (int) $modSettings['search_max_results'];
315
316
	// Maximum length of the string.
317
	$context['search_string_limit'] = 100;
318
319
	loadLanguage('Search');
320
	if (!isset($_REQUEST['xml']))
321
		loadTemplate('Search');
322
	//If we're doing XML we need to use the results template regardless really.
323
	else
324
		$context['sub_template'] = 'results';
325
326
	// Are you allowed?
327
	isAllowedTo('search_posts');
328
329
	require_once($sourcedir . '/Display.php');
330
	require_once($sourcedir . '/Subs-Package.php');
331
332
	// Search has a special database set.
333
	db_extend('search');
334
335
	// Load up the search API we are going to use.
336
	$searchAPI = findSearchAPI();
337
338
	// $search_params will carry all settings that differ from the default search parameters.
339
	// That way, the URLs involved in a search page will be kept as short as possible.
340
	$search_params = array();
341
342
	if (isset($_REQUEST['params']))
343
	{
344
		// Due to IE's 2083 character limit, we have to compress long search strings
345
		$temp_params = base64_decode(str_replace(array('-', '_', '.'), array('+', '/', '='), $_REQUEST['params']));
346
347
		// Test for gzuncompress failing
348
		$temp_params2 = @gzuncompress($temp_params);
349
		$temp_params = explode('|"|', (!empty($temp_params2) ? $temp_params2 : $temp_params));
350
351
		foreach ($temp_params as $i => $data)
352
		{
353
			@list($k, $v) = explode('|\'|', $data);
354
			$search_params[$k] = $v;
355
		}
356
357
		if (isset($search_params['brd']))
358
			$search_params['brd'] = empty($search_params['brd']) ? array() : explode(',', $search_params['brd']);
359
	}
360
361
	// Store whether simple search was used (needed if the user wants to do another query).
362
	if (!isset($search_params['advanced']))
363
		$search_params['advanced'] = empty($_REQUEST['advanced']) ? 0 : 1;
364
365
	// 1 => 'allwords' (default, don't set as param) / 2 => 'anywords'.
366
	if (!empty($search_params['searchtype']) || (!empty($_REQUEST['searchtype']) && $_REQUEST['searchtype'] == 2))
367
		$search_params['searchtype'] = 2;
368
369
	// Minimum age of messages. Default to zero (don't set param in that case).
370
	if (!empty($search_params['minage']) || (!empty($_REQUEST['minage']) && $_REQUEST['minage'] > 0))
371
		$search_params['minage'] = !empty($search_params['minage']) ? (int) $search_params['minage'] : (int) $_REQUEST['minage'];
372
373
	// Maximum age of messages. Default to infinite (9999 days: param not set).
374
	if (!empty($search_params['maxage']) || (!empty($_REQUEST['maxage']) && $_REQUEST['maxage'] < 9999))
375
		$search_params['maxage'] = !empty($search_params['maxage']) ? (int) $search_params['maxage'] : (int) $_REQUEST['maxage'];
376
377
	// Searching a specific topic?
378
	if (!empty($_REQUEST['topic']) || (!empty($_REQUEST['search_selection']) && $_REQUEST['search_selection'] == 'topic'))
379
	{
380
		$search_params['topic'] = empty($_REQUEST['search_selection']) ? (int) $_REQUEST['topic'] : (isset($_REQUEST['sd_topic']) ? (int) $_REQUEST['sd_topic'] : '');
381
		$search_params['show_complete'] = true;
382
	}
383
	elseif (!empty($search_params['topic']))
384
		$search_params['topic'] = (int) $search_params['topic'];
385
386
	if (!empty($search_params['minage']) || !empty($search_params['maxage']))
387
	{
388
		$request = $smcFunc['db_query']('', '
389
			SELECT ' . (empty($search_params['maxage']) ? '0, ' : 'COALESCE(MIN(id_msg), -1), ') . (empty($search_params['minage']) ? '0' : 'COALESCE(MAX(id_msg), -1)') . '
390
			FROM {db_prefix}messages
391
			WHERE 1=1' . ($modSettings['postmod_active'] ? '
392
				AND approved = {int:is_approved_true}' : '') . (empty($search_params['minage']) ? '' : '
393
				AND poster_time <= {int:timestamp_minimum_age}') . (empty($search_params['maxage']) ? '' : '
394
				AND poster_time >= {int:timestamp_maximum_age}'),
395
			array(
396
				'timestamp_minimum_age' => empty($search_params['minage']) ? 0 : time() - 86400 * $search_params['minage'],
397
				'timestamp_maximum_age' => empty($search_params['maxage']) ? 0 : time() - 86400 * $search_params['maxage'],
398
				'is_approved_true' => 1,
399
			)
400
		);
401
		list ($minMsgID, $maxMsgID) = $smcFunc['db_fetch_row']($request);
402
		if ($minMsgID < 0 || $maxMsgID < 0)
403
			$context['search_errors']['no_messages_in_time_frame'] = true;
404
		$smcFunc['db_free_result']($request);
405
	}
406
407
	// Default the user name to a wildcard matching every user (*).
408
	if (!empty($search_params['userspec']) || (!empty($_REQUEST['userspec']) && $_REQUEST['userspec'] != '*'))
409
		$search_params['userspec'] = isset($search_params['userspec']) ? $search_params['userspec'] : $_REQUEST['userspec'];
410
411
	// If there's no specific user, then don't mention it in the main query.
412
	if (empty($search_params['userspec']))
413
		$userQuery = '';
414
	else
415
	{
416
		$userString = strtr($smcFunc['htmlspecialchars']($search_params['userspec'], ENT_QUOTES), array('&quot;' => '"', '%' => '\%', '_' => '\_', '*' => '%', '?' => '_'));
417
418
		preg_match_all('~"([^"]+)"~', $userString, $matches);
419
		$possible_users = array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $userString)));
420
421
		for ($k = 0, $n = count($possible_users); $k < $n; $k++)
422
		{
423
			$possible_users[$k] = trim($possible_users[$k]);
424
425
			if (strlen($possible_users[$k]) == 0)
426
				unset($possible_users[$k]);
427
		}
428
429
		if (empty($possible_users))
430
		{
431
			$userQuery = '';
432
		}
433
		else
434
		{
435
			// Create a list of database-escaped search names.
436
			$realNameMatches = array();
437
438
			foreach ($possible_users as $possible_user)
439
				$realNameMatches[] = $smcFunc['db_quote'](
440
					'{string:possible_user}',
441
					array(
442
						'possible_user' => $possible_user
443
					)
444
				);
445
446
			// Retrieve a list of possible members.
447
			$request = $smcFunc['db_query']('', '
448
				SELECT id_member
449
				FROM {db_prefix}members
450
				WHERE {raw:match_possible_users}',
451
				array(
452
					'match_possible_users' => 'real_name LIKE ' . implode(' OR real_name LIKE ', $realNameMatches),
453
				)
454
			);
455
456
			// Simply do nothing if there're too many members matching the criteria.
457
			if ($smcFunc['db_num_rows']($request) > $maxMembersToSearch)
458
			{
459
				$userQuery = '';
460
			}
461
			elseif ($smcFunc['db_num_rows']($request) == 0)
462
			{
463
				$userQuery = $smcFunc['db_quote'](
464
					'm.id_member = {int:id_member_guest} AND ({raw:match_possible_guest_names})',
465
					array(
466
						'id_member_guest' => 0,
467
						'match_possible_guest_names' => 'm.poster_name LIKE ' . implode(' OR m.poster_name LIKE ', $realNameMatches),
468
					)
469
				);
470
			}
471
			else
472
			{
473
				$memberlist = array();
474
475
				while ($row = $smcFunc['db_fetch_assoc']($request))
476
					$memberlist[] = $row['id_member'];
477
478
				$userQuery = $smcFunc['db_quote'](
479
					'(m.id_member IN ({array_int:matched_members}) OR (m.id_member = {int:id_member_guest} AND ({raw:match_possible_guest_names})))',
480
					array(
481
						'matched_members' => $memberlist,
482
						'id_member_guest' => 0,
483
						'match_possible_guest_names' => 'm.poster_name LIKE ' . implode(' OR m.poster_name LIKE ', $realNameMatches),
484
					)
485
				);
486
			}
487
			$smcFunc['db_free_result']($request);
488
		}
489
	}
490
491
	// If the boards were passed by URL (params=), temporarily put them back in $_REQUEST.
492
	if (!empty($search_params['brd']) && is_array($search_params['brd']))
493
		$_REQUEST['brd'] = $search_params['brd'];
494
495
	// Ensure that brd is an array.
496
	if ((!empty($_REQUEST['brd']) && !is_array($_REQUEST['brd'])) || (!empty($_REQUEST['search_selection']) && $_REQUEST['search_selection'] == 'board'))
497
	{
498
		if (!empty($_REQUEST['brd']))
499
		{
500
			$_REQUEST['brd'] = strpos($_REQUEST['brd'], ',') !== false ? explode(',', $_REQUEST['brd']) : array($_REQUEST['brd']);
501
		}
502
		else
503
			$_REQUEST['brd'] = isset($_REQUEST['sd_brd']) ? array($_REQUEST['sd_brd']) : array();
504
	}
505
506
	// Make sure all boards are integers.
507
	if (!empty($_REQUEST['brd']))
508
	{
509
		foreach ($_REQUEST['brd'] as $id => $brd)
510
			$_REQUEST['brd'][$id] = (int) $brd;
511
	}
512
513
	// Special case for boards: searching just one topic?
514
	if (!empty($search_params['topic']))
515
	{
516
		$request = $smcFunc['db_query']('', '
517
			SELECT t.id_board
518
			FROM {db_prefix}topics AS t
519
			WHERE t.id_topic = {int:search_topic_id}
520
				AND {query_see_topic_board}' . ($modSettings['postmod_active'] ? '
521
				AND t.approved = {int:is_approved_true}' : '') . '
522
			LIMIT 1',
523
			array(
524
				'search_topic_id' => $search_params['topic'],
525
				'is_approved_true' => 1,
526
			)
527
		);
528
529
		if ($smcFunc['db_num_rows']($request) == 0)
530
			fatal_lang_error('topic_gone', false);
531
532
		$search_params['brd'] = array();
533
		list ($search_params['brd'][0]) = $smcFunc['db_fetch_row']($request);
534
		$smcFunc['db_free_result']($request);
535
	}
536
	// Select all boards you've selected AND are allowed to see.
537
	elseif ($user_info['is_admin'] && (!empty($search_params['advanced']) || !empty($_REQUEST['brd'])))
538
	{
539
		$search_params['brd'] = empty($_REQUEST['brd']) ? array() : $_REQUEST['brd'];
540
	}
541
	else
542
	{
543
		$see_board = empty($search_params['advanced']) ? 'query_wanna_see_board' : 'query_see_board';
544
545
		$request = $smcFunc['db_query']('', '
546
			SELECT b.id_board
547
			FROM {db_prefix}boards AS b
548
			WHERE {raw:boards_allowed_to_see}
549
				AND redirect = {string:empty_string}' . (empty($_REQUEST['brd']) ? (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? '
550
				AND b.id_board != {int:recycle_board_id}' : '') : '
551
				AND b.id_board IN ({array_int:selected_search_boards})'),
552
			array(
553
				'boards_allowed_to_see' => $user_info[$see_board],
554
				'empty_string' => '',
555
				'selected_search_boards' => empty($_REQUEST['brd']) ? array() : $_REQUEST['brd'],
556
				'recycle_board_id' => $modSettings['recycle_board'],
557
			)
558
		);
559
560
		$search_params['brd'] = array();
561
562
		while ($row = $smcFunc['db_fetch_assoc']($request))
563
			$search_params['brd'][] = $row['id_board'];
564
565
		$smcFunc['db_free_result']($request);
566
567
		// This error should pro'bly only happen for hackers.
568
		if (empty($search_params['brd']))
569
			$context['search_errors']['no_boards_selected'] = true;
570
	}
571
572
	if (count($search_params['brd']) != 0)
573
	{
574
		foreach ($search_params['brd'] as $k => $v)
575
			$search_params['brd'][$k] = (int) $v;
576
577
		// If we've selected all boards, this parameter can be left empty.
578
		$request = $smcFunc['db_query']('', '
579
			SELECT COUNT(*)
580
			FROM {db_prefix}boards
581
			WHERE redirect = {string:empty_string}',
582
			array(
583
				'empty_string' => '',
584
			)
585
		);
586
587
		list ($num_boards) = $smcFunc['db_fetch_row']($request);
588
589
		$smcFunc['db_free_result']($request);
590
591
		if (count($search_params['brd']) == $num_boards)
592
		{
593
			$boardQuery = '';
594
		}
595
		elseif (count($search_params['brd']) == $num_boards - 1 && !empty($modSettings['recycle_board']) && !in_array($modSettings['recycle_board'], $search_params['brd']))
596
		{
597
			$boardQuery = '!= ' . $modSettings['recycle_board'];
598
		}
599
		else
600
			$boardQuery = 'IN (' . implode(', ', $search_params['brd']) . ')';
601
	}
602
	else
603
		$boardQuery = '';
604
605
	$search_params['show_complete'] = !empty($search_params['show_complete']) || !empty($_REQUEST['show_complete']);
606
607
	$search_params['subject_only'] = !empty($search_params['subject_only']) || !empty($_REQUEST['subject_only']);
608
609
	$context['compact'] = !$search_params['show_complete'];
610
611
	// Get the sorting parameters right. Default to sort by relevance descending.
612
	$sort_columns = array(
613
		'relevance',
614
		'num_replies',
615
		'id_msg',
616
	);
617
618
	call_integration_hook('integrate_search_sort_columns', array(&$sort_columns));
619
620
	if (empty($search_params['sort']) && !empty($_REQUEST['sort']))
621
	{
622
		list ($search_params['sort'], $search_params['sort_dir']) = array_pad(explode('|', $_REQUEST['sort']), 2, '');
623
	}
624
625
	$search_params['sort'] = !empty($search_params['sort']) && in_array($search_params['sort'], $sort_columns) ? $search_params['sort'] : 'relevance';
626
627
	if (!empty($search_params['topic']) && $search_params['sort'] === 'num_replies')
628
		$search_params['sort'] = 'id_msg';
629
630
	// Sorting direction: descending unless stated otherwise.
631
	$search_params['sort_dir'] = !empty($search_params['sort_dir']) && $search_params['sort_dir'] == 'asc' ? 'asc' : 'desc';
632
633
	// Determine some values needed to calculate the relevance.
634
	$minMsg = (int) ((1 - $recentPercentage) * $modSettings['maxMsgID']);
635
	$recentMsg = $modSettings['maxMsgID'] - $minMsg;
636
637
	// *** Parse the search query
638
	call_integration_hook('integrate_search_params', array(&$search_params));
639
640
	/*
641
	 * Unfortunately, searching for words like this is going to be slow, so we're blacklisting them.
642
	 *
643
	 * @todo Setting to add more here?
644
	 * @todo Maybe only blacklist if they are the only word, or "any" is used?
645
	 */
646
	$blacklisted_words = array('img', 'url', 'quote', 'www', 'http', 'the', 'is', 'it', 'are', 'if');
647
648
	call_integration_hook('integrate_search_blacklisted_words', array(&$blacklisted_words));
649
650
	// What are we searching for?
651
	if (empty($search_params['search']))
652
	{
653
		if (isset($_GET['search']))
654
			$search_params['search'] = un_htmlspecialchars($_GET['search']);
655
656
		elseif (isset($_POST['search']))
657
			$search_params['search'] = $_POST['search'];
658
659
		else
660
			$search_params['search'] = '';
661
	}
662
663
	// Nothing??
664
	if (!isset($search_params['search']) || $search_params['search'] == '')
665
	{
666
		$context['search_errors']['invalid_search_string'] = true;
667
	}
668
	// Too long?
669
	elseif ($smcFunc['strlen']($search_params['search']) > $context['search_string_limit'])
670
	{
671
		$context['search_errors']['string_too_long'] = true;
672
	}
673
674
	// Change non-word characters into spaces.
675
	$stripped_query = preg_replace('~(?:[\x0B\0' . ($context['utf8'] ? '\x{A0}' : '\xA0') . '\t\r\s\n(){}\\[\\]<>!@$%^*.,:+=`\~\?/\\\\]+|&(?:amp|lt|gt|quot);)+~' . ($context['utf8'] ? 'u' : ''), ' ', $search_params['search']);
676
677
	// Make the query lower case. It's gonna be case insensitive anyway.
678
	$stripped_query = un_htmlspecialchars($smcFunc['strtolower']($stripped_query));
679
680
	// This (hidden) setting will do fulltext searching in the most basic way.
681
	if (!empty($modSettings['search_simple_fulltext']))
682
		$stripped_query = strtr($stripped_query, array('"' => ''));
683
684
	$no_regexp = preg_match('~&#(?:\d{1,7}|x[0-9a-fA-F]{1,6});~', $stripped_query) === 1;
685
686
	// Extract phrase parts first (e.g. some words "this is a phrase" some more words.)
687
	preg_match_all('/(?:^|\s)([-]?)"([^"]+)"(?:$|\s)/', $stripped_query, $matches, PREG_PATTERN_ORDER);
688
689
	$phraseArray = $matches[2];
690
691
	// Remove the phrase parts and extract the words.
692
	$wordArray = preg_replace('~(?:^|\s)(?:[-]?)"(?:[^"]+)"(?:$|\s)~' . ($context['utf8'] ? 'u' : ''), ' ', $search_params['search']);
693
694
	$wordArray = explode(' ', $smcFunc['htmlspecialchars'](un_htmlspecialchars($wordArray), ENT_QUOTES));
695
696
	// A minus sign in front of a word excludes the word.... so...
697
	$excludedWords = array();
698
	$excludedIndexWords = array();
699
	$excludedSubjectWords = array();
700
	$excludedPhrases = array();
701
702
	// .. first, we check for things like -"some words", but not "-some words".
703
	foreach ($matches[1] as $index => $word)
704
	{
705
		if ($word === '-')
706
		{
707
			if (($word = trim($phraseArray[$index], '-_\' ')) !== '' && !in_array($word, $blacklisted_words))
708
				$excludedWords[] = $word;
709
			unset($phraseArray[$index]);
710
		}
711
	}
712
713
	// Now we look for -test, etc.... normaller.
714
	foreach ($wordArray as $index => $word)
715
	{
716
		if (strpos(trim($word), '-') === 0)
717
		{
718
			if (($word = trim($word, '-_\' ')) !== '' && !in_array($word, $blacklisted_words))
719
				$excludedWords[] = $word;
720
721
			unset($wordArray[$index]);
722
		}
723
	}
724
725
	// The remaining words and phrases are all included.
726
	$searchArray = array_merge($phraseArray, $wordArray);
727
728
	$context['search_ignored'] = array();
729
	// Trim everything and make sure there are no words that are the same.
730
	foreach ($searchArray as $index => $value)
731
	{
732
		// Skip anything practically empty.
733
		if (($searchArray[$index] = trim($value, '-_\' ')) === '')
734
		{
735
			unset($searchArray[$index]);
736
		}
737
		// Skip blacklisted words. Make sure to note we skipped them in case we end up with nothing.
738
		elseif (in_array($searchArray[$index], $blacklisted_words))
739
		{
740
			$foundBlackListedWords = true;
741
			unset($searchArray[$index]);
742
		}
743
		// Don't allow very, very short words.
744
		elseif ($smcFunc['strlen']($value) < 2)
745
		{
746
			$context['search_ignored'][] = $value;
747
			unset($searchArray[$index]);
748
		}
749
	}
750
	$searchArray = array_slice(array_unique($searchArray), 0, 10);
751
752
	// Create an array of replacements for highlighting.
753
	$context['mark'] = array();
754
755
	foreach ($searchArray as $word)
756
		$context['mark'][$word] = '<strong class="highlight">' . $word . '</strong>';
757
758
	// Initialize two arrays storing the words that have to be searched for.
759
	$orParts = array();
760
	$searchWords = array();
761
762
	// Make sure at least one word is being searched for.
763
	if (empty($searchArray))
764
	{
765
		$context['search_errors']['invalid_search_string' . (!empty($foundBlackListedWords) ? '_blacklist' : '')] = true;
766
	}
767
	// All words/sentences must match.
768
	elseif (empty($search_params['searchtype']))
769
	{
770
		$orParts[0] = $searchArray;
771
	}
772
	// Any word/sentence must match.
773
	else
774
	{
775
		foreach ($searchArray as $index => $value)
776
			$orParts[$index] = array($value);
777
	}
778
779
	// Don't allow duplicate error messages if one string is too short.
780
	if (isset($context['search_errors']['search_string_small_words'], $context['search_errors']['invalid_search_string']))
781
		unset($context['search_errors']['invalid_search_string']);
782
783
	// Make sure the excluded words are in all or-branches.
784
	foreach ($orParts as $orIndex => $andParts)
785
		foreach ($excludedWords as $word)
786
			$orParts[$orIndex][] = $word;
787
788
	// Determine the or-branches and the fulltext search words.
789
	foreach ($orParts as $orIndex => $andParts)
790
	{
791
		$searchWords[$orIndex] = array(
792
			'indexed_words' => array(),
793
			'words' => array(),
794
			'subject_words' => array(),
795
			'all_words' => array(),
796
			'complex_words' => array(),
797
		);
798
799
		// Sort the indexed words (large words -> small words -> excluded words).
800
		if ($searchAPI->supportsMethod('searchSort'))
801
			usort($orParts[$orIndex], 'searchSort');
802
803
		foreach ($orParts[$orIndex] as $word)
804
		{
805
			$is_excluded = in_array($word, $excludedWords);
806
807
			$searchWords[$orIndex]['all_words'][] = $word;
808
809
			$subjectWords = text2words($word);
810
			if (!$is_excluded || count($subjectWords) === 1)
811
			{
812
				$searchWords[$orIndex]['subject_words'] = array_merge($searchWords[$orIndex]['subject_words'], $subjectWords);
813
				if ($is_excluded)
814
					$excludedSubjectWords = array_merge($excludedSubjectWords, $subjectWords);
815
			}
816
			else
817
				$excludedPhrases[] = $word;
818
819
			// Have we got indexes to prepare?
820
			if ($searchAPI->supportsMethod('prepareIndexes'))
821
				$searchAPI->prepareIndexes($word, $searchWords[$orIndex], $excludedIndexWords, $is_excluded);
822
		}
823
824
		// Search_force_index requires all AND parts to have at least one fulltext word.
825
		if (!empty($modSettings['search_force_index']) && empty($searchWords[$orIndex]['indexed_words']))
826
		{
827
			$context['search_errors']['query_not_specific_enough'] = true;
828
			break;
829
		}
830
		elseif ($search_params['subject_only'] && empty($searchWords[$orIndex]['subject_words']) && empty($excludedSubjectWords))
831
		{
832
			$context['search_errors']['query_not_specific_enough'] = true;
833
			break;
834
		}
835
836
		// Make sure we aren't searching for too many indexed words.
837
		else
838
		{
839
			$searchWords[$orIndex]['indexed_words'] = array_slice($searchWords[$orIndex]['indexed_words'], 0, 7);
840
			$searchWords[$orIndex]['subject_words'] = array_slice($searchWords[$orIndex]['subject_words'], 0, 7);
841
			$searchWords[$orIndex]['words'] = array_slice($searchWords[$orIndex]['words'], 0, 4);
842
		}
843
	}
844
845
	// *** Spell checking
846
	$context['show_spellchecking'] = !empty($modSettings['enableSpellChecking']) && (function_exists('pspell_new') || (function_exists('enchant_broker_init') && ($txt['lang_character_set'] == 'UTF-8' || function_exists('iconv'))));
847
	if ($context['show_spellchecking'])
848
	{
849
		require_once($sourcedir . '/Subs-Post.php');
850
851
		// Don't hardcode spellchecking functions!
852
		$link = spell_init();
853
854
		$did_you_mean = array('search' => array(), 'display' => array());
855
		$found_misspelling = false;
856
		foreach ($searchArray as $word)
857
		{
858
			if (empty($link))
859
				continue;
860
861
			// Don't check phrases.
862
			if (preg_match('~^\w+$~', $word) === 0)
863
			{
864
				$did_you_mean['search'][] = '"' . $word . '"';
865
				$did_you_mean['display'][] = '&quot;' . $smcFunc['htmlspecialchars']($word) . '&quot;';
866
				continue;
867
			}
868
			// For some strange reason spell check can crash PHP on decimals.
869
			elseif (preg_match('~\d~', $word) === 1)
870
			{
871
				$did_you_mean['search'][] = $word;
872
				$did_you_mean['display'][] = $smcFunc['htmlspecialchars']($word);
873
				continue;
874
			}
875
			elseif (spell_check($link, $word))
876
			{
877
				$did_you_mean['search'][] = $word;
878
				$did_you_mean['display'][] = $smcFunc['htmlspecialchars']($word);
879
				continue;
880
			}
881
882
			$suggestions = spell_suggest($link, $word);
883
			foreach ($suggestions as $i => $s)
884
			{
885
				// Search is case insensitive.
886
				if ($smcFunc['strtolower']($s) == $smcFunc['strtolower']($word))
887
					unset($suggestions[$i]);
888
889
				// Plus, don't suggest something the user thinks is rude!
890
				elseif ($suggestions[$i] != censorText($s))
891
					unset($suggestions[$i]);
892
			}
893
894
			// Anything found?  If so, correct it!
895
			if (!empty($suggestions))
896
			{
897
				$suggestions = array_values($suggestions);
898
				$did_you_mean['search'][] = $suggestions[0];
899
				$did_you_mean['display'][] = '<em><strong>' . $smcFunc['htmlspecialchars']($suggestions[0]) . '</strong></em>';
900
				$found_misspelling = true;
901
			}
902
			else
903
			{
904
				$did_you_mean['search'][] = $word;
905
				$did_you_mean['display'][] = $smcFunc['htmlspecialchars']($word);
906
			}
907
		}
908
909
		if ($found_misspelling)
910
		{
911
			// Don't spell check excluded words, but add them still...
912
			$temp_excluded = array('search' => array(), 'display' => array());
913
914
			foreach ($excludedWords as $word)
915
			{
916
				if (preg_match('~^\w+$~', $word) == 0)
917
				{
918
					$temp_excluded['search'][] = '-"' . $word . '"';
919
					$temp_excluded['display'][] = '-&quot;' . $smcFunc['htmlspecialchars']($word) . '&quot;';
920
				}
921
				else
922
				{
923
					$temp_excluded['search'][] = '-' . $word;
924
					$temp_excluded['display'][] = '-' . $smcFunc['htmlspecialchars']($word);
925
				}
926
			}
927
928
			$did_you_mean['search'] = array_merge($did_you_mean['search'], $temp_excluded['search']);
929
			$did_you_mean['display'] = array_merge($did_you_mean['display'], $temp_excluded['display']);
930
931
			$temp_params = $search_params;
932
			$temp_params['search'] = implode(' ', $did_you_mean['search']);
933
934
			if (isset($temp_params['brd']))
935
				$temp_params['brd'] = implode(',', $temp_params['brd']);
936
937
			$context['params'] = array();
938
939
			foreach ($temp_params as $k => $v)
940
				$context['did_you_mean_params'][] = $k . '|\'|' . $v;
941
942
			$context['did_you_mean_params'] = base64_encode(implode('|"|', $context['did_you_mean_params']));
943
			$context['did_you_mean'] = implode(' ', $did_you_mean['display']);
944
		}
945
	}
946
947
	// Let the user adjust the search query, should they wish?
948
	$context['search_params'] = $search_params;
949
950
	if (isset($context['search_params']['search']))
951
		$context['search_params']['search'] = $smcFunc['htmlspecialchars']($context['search_params']['search']);
952
953
	if (isset($context['search_params']['userspec']))
954
		$context['search_params']['userspec'] = $smcFunc['htmlspecialchars']($context['search_params']['userspec']);
955
956
	// Do we have captcha enabled?
957
	if ($user_info['is_guest'] && !empty($modSettings['search_enable_captcha']) && empty($_SESSION['ss_vv_passed']) && (empty($_SESSION['last_ss']) || $_SESSION['last_ss'] != $search_params['search']))
958
	{
959
		require_once($sourcedir . '/Subs-Editor.php');
960
961
		$verificationOptions = array(
962
			'id' => 'search',
963
		);
964
965
		$context['require_verification'] = create_control_verification($verificationOptions, true);
966
967
		if (is_array($context['require_verification']))
968
		{
969
			foreach ($context['require_verification'] as $error)
970
				$context['search_errors'][$error] = true;
971
		}
972
		// Don't keep asking for it - they've proven themselves worthy.
973
		else
974
			$_SESSION['ss_vv_passed'] = true;
975
	}
976
977
	// *** Encode all search params
978
979
	// All search params have been checked, let's compile them to a single string... made less simple by PHP 4.3.9 and below.
980
	$temp_params = $search_params;
981
	if (isset($temp_params['brd']))
982
		$temp_params['brd'] = implode(',', $temp_params['brd']);
983
984
	$context['params'] = array();
985
986
	foreach ($temp_params as $k => $v)
987
		$context['params'][] = $k . '|\'|' . $v;
988
989
	if (!empty($context['params']))
990
	{
991
		// Due to old IE's 2083 character limit, we have to compress long search strings
992
		$params = @gzcompress(implode('|"|', $context['params']));
993
994
		// Gzcompress failed, use try non-gz
995
		if (empty($params))
996
			$params = implode('|"|', $context['params']);
997
998
		// Base64 encode, then replace +/= with uri safe ones that can be reverted
999
		$context['params'] = str_replace(array('+', '/', '='), array('-', '_', '.'), base64_encode($params));
1000
	}
1001
1002
	// ... and add the links to the link tree.
1003
	$context['linktree'][] = array(
1004
		'url' => $scripturl . '?action=search;params=' . $context['params'],
1005
		'name' => $txt['search']
1006
	);
1007
	$context['linktree'][] = array(
1008
		'url' => $scripturl . '?action=search2;params=' . $context['params'],
1009
		'name' => $txt['search_results']
1010
	);
1011
1012
	// *** A last error check
1013
	call_integration_hook('integrate_search_errors');
1014
1015
	// One or more search errors? Go back to the first search screen.
1016
	if (!empty($context['search_errors']))
1017
	{
1018
		$_REQUEST['params'] = $context['params'];
1019
		return PlushSearch1();
1020
	}
1021
1022
	// Spam me not, Spam-a-lot?
1023
	if (empty($_SESSION['last_ss']) || $_SESSION['last_ss'] != $search_params['search'])
1024
		spamProtection('search');
1025
1026
	// Store the last search string to allow pages of results to be browsed.
1027
	$_SESSION['last_ss'] = $search_params['search'];
1028
1029
	// *** Reserve an ID for caching the search results.
1030
	$query_params = array_merge($search_params, array(
1031
		'min_msg_id' => isset($minMsgID) ? (int) $minMsgID : 0,
1032
		'max_msg_id' => isset($maxMsgID) ? (int) $maxMsgID : 0,
1033
		'memberlist' => !empty($memberlist) ? $memberlist : array(),
1034
	));
1035
1036
	// Can this search rely on the API given the parameters?
1037
	if ($searchAPI->supportsMethod('searchQuery', $query_params))
1038
	{
1039
		$participants = array();
1040
		$searchArray = array();
1041
1042
		$searchAPI->searchQuery($query_params, $searchWords, $excludedIndexWords, $participants, $searchArray);
1043
	}
1044
1045
	// Update the cache if the current search term is not yet cached.
1046
	else
1047
	{
1048
		$update_cache = empty($_SESSION['search_cache']) || ($_SESSION['search_cache']['params'] != $context['params']);
1049
1050
		// Are the result fresh?
1051
		if (!$update_cache && !empty($_SESSION['search_cache']['id_search']))
1052
		{
1053
			$request = $smcFunc['db_query']('', '
1054
				SELECT id_search
1055
				FROM {db_prefix}log_search_results
1056
				WHERE id_search = {int:search_id}
1057
				LIMIT 1',
1058
				array(
1059
					'search_id' => $_SESSION['search_cache']['id_search'],
1060
				)
1061
			);
1062
1063
			if ($smcFunc['db_num_rows']($request) === 0)
1064
				$update_cache = true;
1065
		}
1066
1067
		if ($update_cache)
1068
		{
1069
			// Increase the pointer...
1070
			$modSettings['search_pointer'] = empty($modSettings['search_pointer']) ? 0 : (int) $modSettings['search_pointer'];
1071
1072
			// ...and store it right off.
1073
			updateSettings(array('search_pointer' => $modSettings['search_pointer'] >= 255 ? 0 : $modSettings['search_pointer'] + 1));
1074
1075
			// As long as you don't change the parameters, the cache result is yours.
1076
			$_SESSION['search_cache'] = array(
1077
				'id_search' => $modSettings['search_pointer'],
1078
				'num_results' => -1,
1079
				'params' => $context['params'],
1080
			);
1081
1082
			// Clear the previous cache of the final results cache.
1083
			$smcFunc['db_search_query']('delete_log_search_results', '
1084
				DELETE FROM {db_prefix}log_search_results
1085
				WHERE id_search = {int:search_id}',
1086
				array(
1087
					'search_id' => $_SESSION['search_cache']['id_search'],
1088
				)
1089
			);
1090
1091
			if ($search_params['subject_only'])
1092
			{
1093
				// We do this to try and avoid duplicate keys on databases not supporting INSERT IGNORE.
1094
				$inserts = array();
1095
1096
				foreach ($searchWords as $orIndex => $words)
1097
				{
1098
					$subject_query_params = array();
1099
					$subject_query = array(
1100
						'from' => '{db_prefix}topics AS t',
1101
						'inner_join' => array(),
1102
						'left_join' => array(),
1103
						'where' => array(),
1104
					);
1105
1106
					if ($modSettings['postmod_active'])
1107
						$subject_query['where'][] = 't.approved = {int:is_approved}';
1108
1109
					$numTables = 0;
1110
					$prev_join = 0;
1111
					$numSubjectResults = 0;
1112
1113
					foreach ($words['subject_words'] as $subjectWord)
1114
					{
1115
						$numTables++;
1116
1117
						if (in_array($subjectWord, $excludedSubjectWords))
1118
						{
1119
							$subject_query['left_join'][] = '{db_prefix}log_search_subjects AS subj' . $numTables . ' ON (subj' . $numTables . '.word ' . (empty($modSettings['search_match_words']) ? 'LIKE {string:subject_words_' . $numTables . '_wild}' : '= {string:subject_words_' . $numTables . '}') . ' AND subj' . $numTables . '.id_topic = t.id_topic)';
1120
1121
							$subject_query['where'][] = '(subj' . $numTables . '.word IS NULL)';
1122
						}
1123
						else
1124
						{
1125
							$subject_query['inner_join'][] = '{db_prefix}log_search_subjects AS subj' . $numTables . ' ON (subj' . $numTables . '.id_topic = ' . ($prev_join === 0 ? 't' : 'subj' . $prev_join) . '.id_topic)';
1126
1127
							$subject_query['where'][] = 'subj' . $numTables . '.word ' . (empty($modSettings['search_match_words']) ? 'LIKE {string:subject_words_' . $numTables . '_wild}' : '= {string:subject_words_' . $numTables . '}');
1128
1129
							$prev_join = $numTables;
1130
						}
1131
1132
						$subject_query_params['subject_words_' . $numTables] = $subjectWord;
1133
						$subject_query_params['subject_words_' . $numTables . '_wild'] = '%' . $subjectWord . '%';
1134
					}
1135
1136
					if (!empty($userQuery))
1137
					{
1138
						if ($subject_query['from'] != '{db_prefix}messages AS m')
1139
						{
1140
							$subject_query['inner_join'][] = '{db_prefix}messages AS m ON (m.id_topic = t.id_topic)';
1141
						}
1142
1143
						$subject_query['where'][] = $userQuery;
1144
					}
1145
1146
					if (!empty($search_params['topic']))
1147
						$subject_query['where'][] = 't.id_topic = ' . $search_params['topic'];
1148
1149
					if (!empty($minMsgID))
1150
						$subject_query['where'][] = 't.id_first_msg >= ' . $minMsgID;
1151
1152
					if (!empty($maxMsgID))
1153
						$subject_query['where'][] = 't.id_last_msg <= ' . $maxMsgID;
1154
1155
					if (!empty($boardQuery))
1156
						$subject_query['where'][] = 't.id_board ' . $boardQuery;
1157
1158
					if (!empty($excludedPhrases))
1159
					{
1160
						if ($subject_query['from'] != '{db_prefix}messages AS m')
1161
						{
1162
							$subject_query['inner_join'][] = '{db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)';
1163
						}
1164
1165
						$count = 0;
1166
1167
						foreach ($excludedPhrases as $phrase)
1168
						{
1169
							$subject_query['where'][] = 'm.subject NOT ' . (empty($modSettings['search_match_words']) || $no_regexp ? ' LIKE ' : ' RLIKE ') . '{string:excluded_phrases_' . $count . '}';
1170
1171
							$subject_query_params['excluded_phrases_' . $count++] = empty($modSettings['search_match_words']) || $no_regexp ? '%' . strtr($phrase, array('_' => '\\_', '%' => '\\%')) . '%' : '[[:<:]]' . addcslashes(preg_replace(array('/([\[\]$.+*?|{}()])/'), array('[$1]'), $phrase), '\\\'') . '[[:>:]]';
1172
						}
1173
					}
1174
1175
					call_integration_hook('integrate_subject_only_search_query', array(&$subject_query, &$subject_query_params));
1176
1177
					$relevance = '1000 * (';
1178
1179
					foreach ($weight_factors as $type => $value)
1180
					{
1181
						$relevance .= $weight[$type];
1182
1183
						if (!empty($value['results']))
1184
							$relevance .= ' * ' . $value['results'];
1185
1186
						$relevance .= ' + ';
1187
					}
1188
1189
					$relevance = substr($relevance, 0, -3) . ') / ' . $weight_total . ' AS relevance';
1190
1191
					$ignoreRequest = $smcFunc['db_search_query']('insert_log_search_results_subject',
1192
						($smcFunc['db_support_ignore'] ? '
1193
						INSERT IGNORE INTO {db_prefix}log_search_results
1194
							(id_search, id_topic, relevance, id_msg, num_matches)' : '') . '
1195
						SELECT
1196
							{int:id_search},
1197
							t.id_topic,
1198
							' . $relevance . ',
1199
							' . (empty($userQuery) ? 't.id_first_msg' : 'm.id_msg') . ',
1200
							1
1201
						FROM ' . $subject_query['from'] . (empty($subject_query['inner_join']) ? '' : '
1202
							INNER JOIN ' . implode('
1203
							INNER JOIN ', $subject_query['inner_join'])) . (empty($subject_query['left_join']) ? '' : '
1204
							LEFT JOIN ' . implode('
1205
							LEFT JOIN ', $subject_query['left_join'])) . '
1206
						WHERE ' . implode('
1207
							AND ', $subject_query['where']) . (empty($modSettings['search_max_results']) ? '' : '
1208
						LIMIT ' . ($modSettings['search_max_results'] - $numSubjectResults)),
1209
						array_merge($subject_query_params, array(
1210
							'id_search' => $_SESSION['search_cache']['id_search'],
1211
							'min_msg' => $minMsg,
1212
							'recent_message' => $recentMsg,
1213
							'huge_topic_posts' => $humungousTopicPosts,
1214
							'is_approved' => 1,
1215
						))
1216
					);
1217
1218
					// If the database doesn't support IGNORE to make this fast we need to do some tracking.
1219
					if (!$smcFunc['db_support_ignore'])
1220
					{
1221
						while ($row = $smcFunc['db_fetch_row']($ignoreRequest))
1222
						{
1223
							// No duplicates!
1224
							if (isset($inserts[$row[1]]))
1225
								continue;
1226
1227
							foreach ($row as $key => $value)
1228
								$inserts[$row[1]][] = (int) $row[$key];
1229
						}
1230
						$smcFunc['db_free_result']($ignoreRequest);
1231
						$numSubjectResults = count($inserts);
1232
					}
1233
					else
1234
						$numSubjectResults += $smcFunc['db_affected_rows']();
1235
1236
					if (!empty($modSettings['search_max_results']) && $numSubjectResults >= $modSettings['search_max_results'])
1237
						break;
1238
				}
1239
1240
				// If there's data to be inserted for non-IGNORE databases do it here!
1241
				if (!empty($inserts))
1242
				{
1243
					$smcFunc['db_insert']('',
1244
						'{db_prefix}log_search_results',
1245
						array('id_search' => 'int', 'id_topic' => 'int', 'relevance' => 'int', 'id_msg' => 'int', 'num_matches' => 'int'),
1246
						$inserts,
1247
						array('id_search', 'id_topic')
1248
					);
1249
				}
1250
1251
				$_SESSION['search_cache']['num_results'] = $numSubjectResults;
1252
			}
1253
			else
1254
			{
1255
				$main_query = array(
1256
					'select' => array(
1257
						'id_search' => $_SESSION['search_cache']['id_search'],
1258
						'relevance' => '0',
1259
					),
1260
					'weights' => array(),
1261
					'from' => '{db_prefix}topics AS t',
1262
					'inner_join' => array(
1263
						'{db_prefix}messages AS m ON (m.id_topic = t.id_topic)'
1264
					),
1265
					'left_join' => array(),
1266
					'where' => array(),
1267
					'group_by' => array(),
1268
					'parameters' => array(
1269
						'min_msg' => $minMsg,
1270
						'recent_message' => $recentMsg,
1271
						'huge_topic_posts' => $humungousTopicPosts,
1272
						'is_approved' => 1,
1273
					),
1274
				);
1275
1276
				if (empty($search_params['topic']) && empty($search_params['show_complete']))
1277
				{
1278
					$main_query['select']['id_topic'] = 't.id_topic';
1279
					$main_query['select']['id_msg'] = 'MAX(m.id_msg) AS id_msg';
1280
					$main_query['select']['num_matches'] = 'COUNT(*) AS num_matches';
1281
1282
					$main_query['weights'] = $weight_factors;
1283
1284
					$main_query['group_by'][] = 't.id_topic';
1285
				}
1286
				else
1287
				{
1288
					// This is outrageous!
1289
					$main_query['select']['id_topic'] = 'm.id_msg AS id_topic';
1290
					$main_query['select']['id_msg'] = 'm.id_msg';
1291
					$main_query['select']['num_matches'] = '1 AS num_matches';
1292
1293
					$main_query['weights'] = array(
1294
						'age' => array(
1295
							'search' => '((m.id_msg - t.id_first_msg) / CASE WHEN t.id_last_msg = t.id_first_msg THEN 1 ELSE t.id_last_msg - t.id_first_msg END)',
1296
						),
1297
						'first_message' => array(
1298
							'search' => 'CASE WHEN m.id_msg = t.id_first_msg THEN 1 ELSE 0 END',
1299
						),
1300
					);
1301
1302
					if (!empty($search_params['topic']))
1303
					{
1304
						$main_query['where'][] = 't.id_topic = {int:topic}';
1305
						$main_query['parameters']['topic'] = $search_params['topic'];
1306
					}
1307
					if (!empty($search_params['show_complete']))
1308
						$main_query['group_by'][] = 'm.id_msg, t.id_first_msg, t.id_last_msg';
1309
				}
1310
1311
				// *** Get the subject results.
1312
				$numSubjectResults = 0;
1313
				if (empty($search_params['topic']))
1314
				{
1315
					$inserts = array();
1316
					// Create a temporary table to store some preliminary results in.
1317
					$smcFunc['db_search_query']('drop_tmp_log_search_topics', '
1318
						DROP TABLE IF EXISTS {db_prefix}tmp_log_search_topics',
1319
						array(
1320
						)
1321
					);
1322
					$createTemporary = $smcFunc['db_search_query']('create_tmp_log_search_topics', '
1323
						CREATE TEMPORARY TABLE {db_prefix}tmp_log_search_topics (
1324
							id_topic int NOT NULL default {string:string_zero},
1325
							PRIMARY KEY (id_topic)
1326
						) ENGINE=MEMORY',
1327
						array(
1328
							'string_zero' => '0',
1329
						)
1330
					) !== false;
1331
1332
					// Clean up some previous cache.
1333
					if (!$createTemporary)
1334
						$smcFunc['db_search_query']('delete_log_search_topics', '
1335
							DELETE FROM {db_prefix}log_search_topics
1336
							WHERE id_search = {int:search_id}',
1337
							array(
1338
								'search_id' => $_SESSION['search_cache']['id_search'],
1339
							)
1340
						);
1341
1342
					foreach ($searchWords as $orIndex => $words)
1343
					{
1344
						$subject_query = array(
1345
							'from' => '{db_prefix}topics AS t',
1346
							'inner_join' => array(),
1347
							'left_join' => array(),
1348
							'where' => array(),
1349
							'params' => array(),
1350
						);
1351
1352
						$numTables = 0;
1353
						$prev_join = 0;
1354
						$count = 0;
1355
						$excluded = false;
1356
						foreach ($words['subject_words'] as $subjectWord)
1357
						{
1358
							$numTables++;
1359
							if (in_array($subjectWord, $excludedSubjectWords))
1360
							{
1361
								if (($subject_query['from'] != '{db_prefix}messages AS m') && !$excluded)
1362
								{
1363
									$subject_query['inner_join'][] = '{db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)';
1364
									$excluded = true;
1365
								}
1366
								$subject_query['left_join'][] = '{db_prefix}log_search_subjects AS subj' . $numTables . ' ON (subj' . $numTables . '.word ' . (empty($modSettings['search_match_words']) ? 'LIKE {string:subject_not_' . $count . '}' : '= {string:subject_not_' . $count . '}') . ' AND subj' . $numTables . '.id_topic = t.id_topic)';
1367
								$subject_query['params']['subject_not_' . $count] = empty($modSettings['search_match_words']) ? '%' . $subjectWord . '%' : $subjectWord;
1368
1369
								$subject_query['where'][] = '(subj' . $numTables . '.word IS NULL)';
1370
								$subject_query['where'][] = 'm.body NOT ' . (empty($modSettings['search_match_words']) || $no_regexp ? ' LIKE ' : ' RLIKE ') . '{string:body_not_' . $count . '}';
1371
								$subject_query['params']['body_not_' . $count++] = empty($modSettings['search_match_words']) || $no_regexp ? '%' . strtr($subjectWord, array('_' => '\\_', '%' => '\\%')) . '%' : '[[:<:]]' . addcslashes(preg_replace(array('/([\[\]$.+*?|{}()])/'), array('[$1]'), $subjectWord), '\\\'') . '[[:>:]]';
1372
							}
1373
							else
1374
							{
1375
								$subject_query['inner_join'][] = '{db_prefix}log_search_subjects AS subj' . $numTables . ' ON (subj' . $numTables . '.id_topic = ' . ($prev_join === 0 ? 't' : 'subj' . $prev_join) . '.id_topic)';
1376
								$subject_query['where'][] = 'subj' . $numTables . '.word LIKE {string:subject_like_' . $count . '}';
1377
								$subject_query['params']['subject_like_' . $count++] = empty($modSettings['search_match_words']) ? '%' . $subjectWord . '%' : $subjectWord;
1378
								$prev_join = $numTables;
1379
							}
1380
						}
1381
1382
						if (!empty($userQuery))
1383
						{
1384
							if ($subject_query['from'] != '{db_prefix}messages AS m')
1385
							{
1386
								$subject_query['inner_join'][] = '{db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)';
1387
							}
1388
							$subject_query['where'][] = '{raw:user_query}';
1389
							$subject_query['params']['user_query'] = $userQuery;
1390
						}
1391
						if (!empty($search_params['topic']))
1392
						{
1393
							$subject_query['where'][] = 't.id_topic = {int:topic}';
1394
							$subject_query['params']['topic'] = $search_params['topic'];
1395
						}
1396
						if (!empty($minMsgID))
1397
						{
1398
							$subject_query['where'][] = 't.id_first_msg >= {int:min_msg_id}';
1399
							$subject_query['params']['min_msg_id'] = $minMsgID;
1400
						}
1401
						if (!empty($maxMsgID))
1402
						{
1403
							$subject_query['where'][] = 't.id_last_msg <= {int:max_msg_id}';
1404
							$subject_query['params']['max_msg_id'] = $maxMsgID;
1405
						}
1406
						if (!empty($boardQuery))
1407
						{
1408
							$subject_query['where'][] = 't.id_board {raw:board_query}';
1409
							$subject_query['params']['board_query'] = $boardQuery;
1410
						}
1411
						if (!empty($excludedPhrases))
1412
						{
1413
							if ($subject_query['from'] != '{db_prefix}messages AS m')
1414
							{
1415
								$subject_query['inner_join'][] = '{db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)';
1416
							}
1417
							$count = 0;
1418
							foreach ($excludedPhrases as $phrase)
1419
							{
1420
								$subject_query['where'][] = 'm.subject NOT ' . (empty($modSettings['search_match_words']) || $no_regexp ? ' LIKE ' : ' RLIKE ') . '{string:exclude_phrase_' . $count . '}';
1421
								$subject_query['where'][] = 'm.body NOT ' . (empty($modSettings['search_match_words']) || $no_regexp ? ' LIKE ' : ' RLIKE ') . '{string:exclude_phrase_' . $count . '}';
1422
								$subject_query['params']['exclude_phrase_' . $count++] = empty($modSettings['search_match_words']) || $no_regexp ? '%' . strtr($phrase, array('_' => '\\_', '%' => '\\%')) . '%' : '[[:<:]]' . addcslashes(preg_replace(array('/([\[\]$.+*?|{}()])/'), array('[$1]'), $phrase), '\\\'') . '[[:>:]]';
1423
							}
1424
						}
1425
						call_integration_hook('integrate_subject_search_query', array(&$subject_query));
1426
1427
						// Nothing to search for?
1428
						if (empty($subject_query['where']))
1429
							continue;
1430
1431
						$ignoreRequest = $smcFunc['db_search_query']('insert_log_search_topics', ($smcFunc['db_support_ignore'] ? ('
1432
							INSERT IGNORE INTO {db_prefix}' . ($createTemporary ? 'tmp_' : '') . 'log_search_topics
1433
								(' . ($createTemporary ? '' : 'id_search, ') . 'id_topic)') : '') . '
1434
							SELECT ' . ($createTemporary ? '' : $_SESSION['search_cache']['id_search'] . ', ') . 't.id_topic
1435
							FROM ' . $subject_query['from'] . (empty($subject_query['inner_join']) ? '' : '
1436
								INNER JOIN ' . implode('
1437
								INNER JOIN ', $subject_query['inner_join'])) . (empty($subject_query['left_join']) ? '' : '
1438
								LEFT JOIN ' . implode('
1439
								LEFT JOIN ', $subject_query['left_join'])) . '
1440
							WHERE ' . implode('
1441
								AND ', $subject_query['where']) . (empty($modSettings['search_max_results']) ? '' : '
1442
							LIMIT ' . ($modSettings['search_max_results'] - $numSubjectResults)),
1443
							$subject_query['params']
1444
						);
1445
						// Don't do INSERT IGNORE? Manually fix this up!
1446
						if (!$smcFunc['db_support_ignore'])
1447
						{
1448
							while ($row = $smcFunc['db_fetch_row']($ignoreRequest))
1449
							{
1450
								$ind = $createTemporary ? 0 : 1;
1451
								// No duplicates!
1452
								if (isset($inserts[$row[$ind]]))
1453
									continue;
1454
1455
								$inserts[$row[$ind]] = $row;
1456
							}
1457
							$smcFunc['db_free_result']($ignoreRequest);
1458
							$numSubjectResults = count($inserts);
1459
						}
1460
						else
1461
							$numSubjectResults += $smcFunc['db_affected_rows']();
1462
1463
						if (!empty($modSettings['search_max_results']) && $numSubjectResults >= $modSettings['search_max_results'])
1464
							break;
1465
					}
1466
1467
					// Got some non-MySQL data to plonk in?
1468
					if (!empty($inserts))
1469
					{
1470
						$smcFunc['db_insert']('',
1471
							('{db_prefix}' . ($createTemporary ? 'tmp_' : '') . 'log_search_topics'),
1472
							$createTemporary ? array('id_topic' => 'int') : array('id_search' => 'int', 'id_topic' => 'int'),
1473
							$inserts,
1474
							$createTemporary ? array('id_topic') : array('id_search', 'id_topic')
1475
						);
1476
					}
1477
1478
					if ($numSubjectResults !== 0)
1479
					{
1480
						$main_query['weights']['subject']['search'] = 'CASE WHEN MAX(lst.id_topic) IS NULL THEN 0 ELSE 1 END';
1481
						$main_query['left_join'][] = '{db_prefix}' . ($createTemporary ? 'tmp_' : '') . 'log_search_topics AS lst ON (' . ($createTemporary ? '' : 'lst.id_search = {int:id_search} AND ') . 'lst.id_topic = t.id_topic)';
1482
						if (!$createTemporary)
1483
							$main_query['parameters']['id_search'] = $_SESSION['search_cache']['id_search'];
1484
					}
1485
				}
1486
1487
				$indexedResults = 0;
1488
				// We building an index?
1489
				if ($searchAPI->supportsMethod('indexedWordQuery', $query_params))
1490
				{
1491
					$inserts = array();
1492
					$smcFunc['db_search_query']('drop_tmp_log_search_messages', '
1493
						DROP TABLE IF EXISTS {db_prefix}tmp_log_search_messages',
1494
						array(
1495
						)
1496
					);
1497
1498
					$createTemporary = $smcFunc['db_search_query']('create_tmp_log_search_messages', '
1499
						CREATE TEMPORARY TABLE {db_prefix}tmp_log_search_messages (
1500
							id_msg int NOT NULL default {string:string_zero},
1501
							PRIMARY KEY (id_msg)
1502
						) ENGINE=MEMORY',
1503
						array(
1504
							'string_zero' => '0',
1505
						)
1506
					) !== false;
1507
1508
					// Clear, all clear!
1509
					if (!$createTemporary)
1510
						$smcFunc['db_search_query']('delete_log_search_messages', '
1511
							DELETE FROM {db_prefix}log_search_messages
1512
							WHERE id_search = {int:id_search}',
1513
							array(
1514
								'id_search' => $_SESSION['search_cache']['id_search'],
1515
							)
1516
						);
1517
1518
					foreach ($searchWords as $orIndex => $words)
1519
					{
1520
						// Search for this word, assuming we have some words!
1521
						if (!empty($words['indexed_words']))
1522
						{
1523
							// Variables required for the search.
1524
							$search_data = array(
1525
								'insert_into' => ($createTemporary ? 'tmp_' : '') . 'log_search_messages',
1526
								'no_regexp' => $no_regexp,
1527
								'max_results' => $maxMessageResults,
1528
								'indexed_results' => $indexedResults,
1529
								'params' => array(
1530
									'id_search' => !$createTemporary ? $_SESSION['search_cache']['id_search'] : 0,
1531
									'excluded_words' => $excludedWords,
1532
									'user_query' => !empty($userQuery) ? $userQuery : '',
1533
									'board_query' => !empty($boardQuery) ? $boardQuery : '',
1534
									'topic' => !empty($search_params['topic']) ? $search_params['topic'] : 0,
1535
									'min_msg_id' => !empty($minMsgID) ? $minMsgID : 0,
1536
									'max_msg_id' => !empty($maxMsgID) ? $maxMsgID : 0,
1537
									'excluded_phrases' => !empty($excludedPhrases) ? $excludedPhrases : array(),
1538
									'excluded_index_words' => !empty($excludedIndexWords) ? $excludedIndexWords : array(),
1539
									'excluded_subject_words' => !empty($excludedSubjectWords) ? $excludedSubjectWords : array(),
1540
								),
1541
							);
1542
1543
							$ignoreRequest = $searchAPI->indexedWordQuery($words, $search_data);
1544
1545
							if (!$smcFunc['db_support_ignore'])
1546
							{
1547
								while ($row = $smcFunc['db_fetch_row']($ignoreRequest))
1548
								{
1549
									// No duplicates!
1550
									if (isset($inserts[$row[0]]))
1551
										continue;
1552
1553
									$inserts[$row[0]] = $row;
1554
								}
1555
								$smcFunc['db_free_result']($ignoreRequest);
1556
								$indexedResults = count($inserts);
1557
							}
1558
							else
1559
								$indexedResults += $smcFunc['db_affected_rows']();
1560
1561
							if (!empty($maxMessageResults) && $indexedResults >= $maxMessageResults)
1562
								break;
1563
						}
1564
					}
1565
1566
					// More non-MySQL stuff needed?
1567
					if (!empty($inserts))
1568
					{
1569
						$smcFunc['db_insert']('',
1570
							'{db_prefix}' . ($createTemporary ? 'tmp_' : '') . 'log_search_messages',
1571
							$createTemporary ? array('id_msg' => 'int') : array('id_msg' => 'int', 'id_search' => 'int'),
1572
							$inserts,
1573
							$createTemporary ? array('id_msg') : array('id_msg', 'id_search')
1574
						);
1575
					}
1576
1577
					if (empty($indexedResults) && empty($numSubjectResults) && !empty($modSettings['search_force_index']))
1578
					{
1579
						$context['search_errors']['query_not_specific_enough'] = true;
1580
						$_REQUEST['params'] = $context['params'];
1581
						return PlushSearch1();
1582
					}
1583
					elseif (!empty($indexedResults))
1584
					{
1585
						$main_query['inner_join'][] = '{db_prefix}' . ($createTemporary ? 'tmp_' : '') . 'log_search_messages AS lsm ON (lsm.id_msg = m.id_msg)';
1586
						if (!$createTemporary)
1587
						{
1588
							$main_query['where'][] = 'lsm.id_search = {int:id_search}';
1589
							$main_query['parameters']['id_search'] = $_SESSION['search_cache']['id_search'];
1590
						}
1591
					}
1592
				}
1593
1594
				// Not using an index? All conditions have to be carried over.
1595
				else
1596
				{
1597
					$orWhere = array();
1598
					$count = 0;
1599
					foreach ($searchWords as $orIndex => $words)
1600
					{
1601
						$where = array();
1602
						foreach ($words['all_words'] as $regularWord)
1603
						{
1604
							$where[] = 'm.body' . (in_array($regularWord, $excludedWords) ? ' NOT' : '') . (empty($modSettings['search_match_words']) || $no_regexp ? ' LIKE ' : ' RLIKE ') . '{string:all_word_body_' . $count . '}';
1605
							if (in_array($regularWord, $excludedWords))
1606
								$where[] = 'm.subject NOT' . (empty($modSettings['search_match_words']) || $no_regexp ? ' LIKE ' : ' RLIKE ') . '{string:all_word_body_' . $count . '}';
1607
							$main_query['parameters']['all_word_body_' . $count++] = empty($modSettings['search_match_words']) || $no_regexp ? '%' . strtr($regularWord, array('_' => '\\_', '%' => '\\%')) . '%' : '[[:<:]]' . addcslashes(preg_replace(array('/([\[\]$.+*?|{}()])/'), array('[$1]'), $regularWord), '\\\'') . '[[:>:]]';
1608
						}
1609
						if (!empty($where))
1610
							$orWhere[] = count($where) > 1 ? '(' . implode(' AND ', $where) . ')' : $where[0];
1611
					}
1612
					if (!empty($orWhere))
1613
						$main_query['where'][] = count($orWhere) > 1 ? '(' . implode(' OR ', $orWhere) . ')' : $orWhere[0];
1614
1615
					if (!empty($userQuery))
1616
					{
1617
						$main_query['where'][] = '{raw:user_query}';
1618
						$main_query['parameters']['user_query'] = $userQuery;
1619
					}
1620
					if (!empty($search_params['topic']))
1621
					{
1622
						$main_query['where'][] = 'm.id_topic = {int:topic}';
1623
						$main_query['parameters']['topic'] = $search_params['topic'];
1624
					}
1625
					if (!empty($minMsgID))
1626
					{
1627
						$main_query['where'][] = 'm.id_msg >= {int:min_msg_id}';
1628
						$main_query['parameters']['min_msg_id'] = $minMsgID;
1629
					}
1630
					if (!empty($maxMsgID))
1631
					{
1632
						$main_query['where'][] = 'm.id_msg <= {int:max_msg_id}';
1633
						$main_query['parameters']['max_msg_id'] = $maxMsgID;
1634
					}
1635
					if (!empty($boardQuery))
1636
					{
1637
						$main_query['where'][] = 'm.id_board {raw:board_query}';
1638
						$main_query['parameters']['board_query'] = $boardQuery;
1639
					}
1640
				}
1641
				call_integration_hook('integrate_main_search_query', array(&$main_query));
1642
1643
				// Did we either get some indexed results, or otherwise did not do an indexed query?
1644
				if (!empty($indexedResults) || !$searchAPI->supportsMethod('indexedWordQuery', $query_params))
1645
				{
1646
					$relevance = '1000 * (';
1647
					$new_weight_total = 0;
1648
					foreach ($main_query['weights'] as $type => $value)
1649
					{
1650
						$relevance .= $weight[$type];
1651
						if (!empty($value['search']))
1652
							$relevance .= ' * ' . $value['search'];
1653
						$relevance .= ' + ';
1654
						$new_weight_total += $weight[$type];
1655
					}
1656
					$main_query['select']['relevance'] = substr($relevance, 0, -3) . ') / ' . $new_weight_total . ' AS relevance';
1657
1658
					$ignoreRequest = $smcFunc['db_search_query']('insert_log_search_results_no_index', ($smcFunc['db_support_ignore'] ? ('
1659
						INSERT IGNORE INTO ' . '{db_prefix}log_search_results
1660
							(' . implode(', ', array_keys($main_query['select'])) . ')') : '') . '
1661
						SELECT
1662
							' . implode(',
1663
							', $main_query['select']) . '
1664
						FROM ' . $main_query['from'] . (empty($main_query['inner_join']) ? '' : '
1665
							INNER JOIN ' . implode('
1666
							INNER JOIN ', $main_query['inner_join'])) . (empty($main_query['left_join']) ? '' : '
1667
							LEFT JOIN ' . implode('
1668
							LEFT JOIN ', $main_query['left_join'])) . (!empty($main_query['where']) ? '
1669
						WHERE ' : '') . implode('
1670
							AND ', $main_query['where']) . (empty($main_query['group_by']) ? '' : '
1671
						GROUP BY ' . implode(', ', $main_query['group_by'])) . (empty($modSettings['search_max_results']) ? '' : '
1672
						LIMIT ' . $modSettings['search_max_results']),
1673
						$main_query['parameters']
1674
					);
1675
1676
					// We love to handle non-good databases that don't support our ignore!
1677
					if (!$smcFunc['db_support_ignore'])
1678
					{
1679
						$inserts = array();
1680
						while ($row = $smcFunc['db_fetch_row']($ignoreRequest))
1681
						{
1682
							// No duplicates!
1683
							if (isset($inserts[$row[2]]))
1684
								continue;
1685
1686
							foreach ($row as $key => $value)
1687
								$inserts[$row[2]][] = (int) $row[$key];
1688
						}
1689
						$smcFunc['db_free_result']($ignoreRequest);
1690
1691
						// Now put them in!
1692
						if (!empty($inserts))
1693
						{
1694
							$query_columns = array();
1695
							foreach ($main_query['select'] as $k => $v)
1696
								$query_columns[$k] = 'int';
1697
1698
							$smcFunc['db_insert']('',
1699
								'{db_prefix}log_search_results',
1700
								$query_columns,
1701
								$inserts,
1702
								array('id_search', 'id_topic')
1703
							);
1704
						}
1705
						$_SESSION['search_cache']['num_results'] += count($inserts);
1706
					}
1707
					else
1708
						$_SESSION['search_cache']['num_results'] = $smcFunc['db_affected_rows']();
1709
				}
1710
1711
				// Insert subject-only matches.
1712
				if ($_SESSION['search_cache']['num_results'] < $modSettings['search_max_results'] && $numSubjectResults !== 0)
1713
				{
1714
					$relevance = '1000 * (';
1715
					foreach ($weight_factors as $type => $value)
1716
						if (isset($value['results']))
1717
						{
1718
							$relevance .= $weight[$type];
1719
							if (!empty($value['results']))
1720
								$relevance .= ' * ' . $value['results'];
1721
							$relevance .= ' + ';
1722
						}
1723
					$relevance = substr($relevance, 0, -3) . ') / ' . $weight_total . ' AS relevance';
1724
1725
					$usedIDs = array_flip(empty($inserts) ? array() : array_keys($inserts));
1726
					$ignoreRequest = $smcFunc['db_search_query']('insert_log_search_results_sub_only', ($smcFunc['db_support_ignore'] ? ('
1727
						INSERT IGNORE INTO {db_prefix}log_search_results
1728
							(id_search, id_topic, relevance, id_msg, num_matches)') : '') . '
1729
						SELECT
1730
							{int:id_search},
1731
							t.id_topic,
1732
							' . $relevance . ',
1733
							t.id_first_msg,
1734
							1
1735
						FROM {db_prefix}topics AS t
1736
							INNER JOIN {db_prefix}' . ($createTemporary ? 'tmp_' : '') . 'log_search_topics AS lst ON (lst.id_topic = t.id_topic)'
1737
						. ($createTemporary ? '' : ' WHERE lst.id_search = {int:id_search}')
1738
						. (empty($modSettings['search_max_results']) ? '' : '
1739
						LIMIT ' . ($modSettings['search_max_results'] - $_SESSION['search_cache']['num_results'])),
1740
						array(
1741
							'id_search' => $_SESSION['search_cache']['id_search'],
1742
							'min_msg' => $minMsg,
1743
							'recent_message' => $recentMsg,
1744
							'huge_topic_posts' => $humungousTopicPosts,
1745
						)
1746
					);
1747
					// Once again need to do the inserts if the database don't support ignore!
1748
					if (!$smcFunc['db_support_ignore'])
1749
					{
1750
						$inserts = array();
1751
						while ($row = $smcFunc['db_fetch_row']($ignoreRequest))
1752
						{
1753
							// No duplicates!
1754
							if (isset($usedIDs[$row[1]]))
1755
								continue;
1756
1757
							$usedIDs[$row[1]] = true;
1758
							$inserts[] = $row;
1759
						}
1760
						$smcFunc['db_free_result']($ignoreRequest);
1761
1762
						// Now put them in!
1763
						if (!empty($inserts))
1764
						{
1765
							$smcFunc['db_insert']('',
1766
								'{db_prefix}log_search_results',
1767
								array('id_search' => 'int', 'id_topic' => 'int', 'relevance' => 'float', 'id_msg' => 'int', 'num_matches' => 'int'),
1768
								$inserts,
1769
								array('id_search', 'id_topic')
1770
							);
1771
						}
1772
						$_SESSION['search_cache']['num_results'] += count($inserts);
1773
					}
1774
					else
1775
						$_SESSION['search_cache']['num_results'] += $smcFunc['db_affected_rows']();
1776
				}
1777
				elseif ($_SESSION['search_cache']['num_results'] == -1)
1778
					$_SESSION['search_cache']['num_results'] = 0;
1779
			}
1780
		}
1781
1782
		$approve_query = '';
1783
		if (!empty($modSettings['postmod_active']))
1784
		{
1785
			// Exclude unapproved topics, but show ones they started.
1786
			if (empty($user_info['mod_cache']['ap']))
1787
				$approve_query = '
1788
				AND (t.approved = {int:is_approved} OR t.id_member_started = {int:current_member})';
1789
1790
			// Show unapproved topics in boards they have access to.
1791
			elseif ($user_info['mod_cache']['ap'] !== array(0))
1792
				$approve_query = '
1793
				AND (t.approved = {int:is_approved} OR t.id_member_started = {int:current_member} OR t.id_board IN ({array_int:approve_boards}))';
1794
		}
1795
1796
		// *** Retrieve the results to be shown on the page
1797
		$participants = array();
1798
		$request = $smcFunc['db_search_query']('', '
1799
			SELECT ' . (empty($search_params['topic']) ? 'lsr.id_topic' : $search_params['topic'] . ' AS id_topic') . ', lsr.id_msg, lsr.relevance, lsr.num_matches
1800
			FROM {db_prefix}log_search_results AS lsr' . ($search_params['sort'] == 'num_replies' || !empty($approve_query) ? '
1801
				INNER JOIN {db_prefix}topics AS t ON (t.id_topic = lsr.id_topic)' : '') . '
1802
			WHERE lsr.id_search = {int:id_search}' . $approve_query . '
1803
			ORDER BY {raw:sort} {raw:sort_dir}
1804
			LIMIT {int:start}, {int:max}',
1805
			array(
1806
				'id_search' => $_SESSION['search_cache']['id_search'],
1807
				'sort' => $search_params['sort'],
1808
				'sort_dir' => $search_params['sort_dir'],
1809
				'start' => $_REQUEST['start'],
1810
				'max' => $modSettings['search_results_per_page'],
1811
				'is_approved' => 1,
1812
				'current_member' => $user_info['id'],
1813
				'approve_boards' => !empty($modSettings['postmod_active']) ? $user_info['mod_cache']['ap'] : array(),
1814
			)
1815
		);
1816
		while ($row = $smcFunc['db_fetch_assoc']($request))
1817
		{
1818
			$context['topics'][$row['id_msg']] = array(
1819
				'relevance' => round($row['relevance'] / 10, 1) . '%',
1820
				'num_matches' => $row['num_matches'],
1821
				'matches' => array(),
1822
			);
1823
			// By default they didn't participate in the topic!
1824
			$participants[$row['id_topic']] = false;
1825
		}
1826
		$smcFunc['db_free_result']($request);
1827
	}
1828
1829
	$num_results = 0;
1830
	if (!empty($context['topics']))
1831
	{
1832
		// Create an array for the permissions.
1833
		$perms = array('post_reply_own', 'post_reply_any');
1834
1835
		if (!empty($options['display_quick_mod']))
1836
			$perms = array_merge($perms, array('lock_any', 'lock_own', 'make_sticky', 'move_any', 'move_own', 'remove_any', 'remove_own', 'merge_any'));
1837
1838
		$boards_can = boardsAllowedTo($perms, true, false);
1839
1840
		// How's about some quick moderation?
1841
		if (!empty($options['display_quick_mod']))
1842
		{
1843
			$context['can_lock'] = in_array(0, $boards_can['lock_any']);
1844
			$context['can_sticky'] = in_array(0, $boards_can['make_sticky']);
1845
			$context['can_move'] = in_array(0, $boards_can['move_any']);
1846
			$context['can_remove'] = in_array(0, $boards_can['remove_any']);
1847
			$context['can_merge'] = in_array(0, $boards_can['merge_any']);
1848
		}
1849
1850
		$approve_query = '';
1851
		if (!empty($modSettings['postmod_active']))
1852
		{
1853
			if (empty($user_info['mod_cache']['ap']))
1854
				$approve_query = '
1855
				AND (m.approved = {int:is_approved} OR m.id_member = {int:current_member})';
1856
1857
			elseif ($user_info['mod_cache']['ap'] !== array(0))
1858
				$approve_query = '
1859
				AND (m.approved = {int:is_approved} OR m.id_member = {int:current_member} OR m.id_board IN ({array_int:approve_boards}))';
1860
		}
1861
1862
		// What messages are we using?
1863
		$msg_list = array_keys($context['topics']);
1864
1865
		// Load the posters...
1866
		$request = $smcFunc['db_query']('', '
1867
			SELECT id_member
1868
			FROM {db_prefix}messages
1869
			WHERE id_member != {int:no_member}
1870
				AND id_msg IN ({array_int:message_list})
1871
			LIMIT {int:limit}',
1872
			array(
1873
				'message_list' => $msg_list,
1874
				'no_member' => 0,
1875
				'limit' => count($context['topics']),
1876
			)
1877
		);
1878
		$posters = array();
1879
		while ($row = $smcFunc['db_fetch_assoc']($request))
1880
			$posters[] = $row['id_member'];
1881
		$smcFunc['db_free_result']($request);
1882
1883
		call_integration_hook('integrate_search_message_list', array(&$msg_list, &$posters));
1884
1885
		if (!empty($posters))
1886
			loadMemberData(array_unique($posters));
1887
1888
		// Get the messages out for the callback - select enough that it can be made to look just like Display.
1889
		$messages_request = $smcFunc['db_query']('', '
1890
			SELECT
1891
				m.id_msg, m.subject, m.poster_name, m.poster_email, m.poster_time, m.id_member,
1892
				m.icon, m.poster_ip, m.body, m.smileys_enabled, m.modified_time, m.modified_name,
1893
				first_m.id_msg AS first_msg, first_m.subject AS first_subject, first_m.icon AS first_icon, first_m.poster_time AS first_poster_time,
1894
				first_mem.id_member AS first_member_id, COALESCE(first_mem.real_name, first_m.poster_name) AS first_member_name,
1895
				last_m.id_msg AS last_msg, last_m.poster_time AS last_poster_time, last_mem.id_member AS last_member_id,
1896
				COALESCE(last_mem.real_name, last_m.poster_name) AS last_member_name, last_m.icon AS last_icon, last_m.subject AS last_subject,
1897
				t.id_topic, t.is_sticky, t.locked, t.id_poll, t.num_replies, t.num_views,
1898
				b.id_board, b.name AS board_name, c.id_cat, c.name AS cat_name
1899
			FROM {db_prefix}messages AS m
1900
				INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic)
1901
				INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
1902
				INNER JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat)
1903
				INNER JOIN {db_prefix}messages AS first_m ON (first_m.id_msg = t.id_first_msg)
1904
				INNER JOIN {db_prefix}messages AS last_m ON (last_m.id_msg = t.id_last_msg)
1905
				LEFT JOIN {db_prefix}members AS first_mem ON (first_mem.id_member = first_m.id_member)
1906
				LEFT JOIN {db_prefix}members AS last_mem ON (last_mem.id_member = first_m.id_member)
1907
			WHERE m.id_msg IN ({array_int:message_list})' . $approve_query . '
1908
			ORDER BY ' . $smcFunc['db_custom_order']('m.id_msg', $msg_list) . '
1909
			LIMIT {int:limit}',
1910
			array(
1911
				'message_list' => $msg_list,
1912
				'is_approved' => 1,
1913
				'current_member' => $user_info['id'],
1914
				'approve_boards' => !empty($modSettings['postmod_active']) ? $user_info['mod_cache']['ap'] : array(),
1915
				'limit' => count($context['topics']),
1916
			)
1917
		);
1918
1919
		// How many results will the user be able to see?
1920
		$num_results = $smcFunc['db_num_rows']($messages_request);
1921
1922
		// If there are no results that means the things in the cache got deleted, so pretend we have no topics anymore.
1923
		if ($num_results == 0)
1924
			$context['topics'] = array();
1925
1926
		// If we want to know who participated in what then load this now.
1927
		if (!empty($modSettings['enableParticipation']) && !$user_info['is_guest'])
1928
		{
1929
			$result = $smcFunc['db_query']('', '
1930
				SELECT id_topic
1931
				FROM {db_prefix}messages
1932
				WHERE id_topic IN ({array_int:topic_list})
1933
					AND id_member = {int:current_member}
1934
				GROUP BY id_topic
1935
				LIMIT {int:limit}',
1936
				array(
1937
					'current_member' => $user_info['id'],
1938
					'topic_list' => array_keys($participants),
1939
					'limit' => count($participants),
1940
				)
1941
			);
1942
			while ($row = $smcFunc['db_fetch_assoc']($result))
1943
				$participants[$row['id_topic']] = true;
1944
1945
			$smcFunc['db_free_result']($result);
1946
		}
1947
	}
1948
1949
	// Now that we know how many results to expect we can start calculating the page numbers.
1950
	$context['page_index'] = constructPageIndex($scripturl . '?action=search2;params=' . $context['params'], $_REQUEST['start'], $num_results, $modSettings['search_results_per_page'], false);
1951
1952
	// Consider the search complete!
1953
	if (!empty($cache_enable) && $cache_enable >= 2)
1954
		cache_put_data('search_start:' . ($user_info['is_guest'] ? $user_info['ip'] : $user_info['id']), null, 90);
1955
1956
	$context['key_words'] = &$searchArray;
1957
1958
	// Setup the default topic icons... for checking they exist and the like!
1959
	$context['icon_sources'] = array();
1960
	foreach ($context['stable_icons'] as $icon)
1961
		$context['icon_sources'][$icon] = 'images_url';
1962
1963
	$context['sub_template'] = 'results';
1964
	$context['page_title'] = $txt['search_results'];
1965
	$context['get_topics'] = 'prepareSearchContext';
1966
	$context['can_restore_perm'] = allowedTo('move_any') && !empty($modSettings['recycle_enable']);
1967
	$context['can_restore'] = false; // We won't know until we handle the context later whether we can actually restore...
1968
1969
	$context['jump_to'] = array(
1970
		'label' => addslashes(un_htmlspecialchars($txt['jump_to'])),
1971
		'board_name' => addslashes(un_htmlspecialchars($txt['select_destination'])),
1972
	);
1973
}
1974
1975
/**
1976
 * Callback to return messages - saves memory.
1977
 *
1978
 * What it does:
1979
 * - callback function for the results sub template.
1980
 * - loads the necessary contextual data to show a search result.
1981
 *
1982
 * @param bool $reset Whether to reset the counter
1983
 * @return array An array of contextual info related to this search
1984
 */
1985
function prepareSearchContext($reset = false)
1986
{
1987
	global $txt, $modSettings, $scripturl, $user_info;
1988
	global $memberContext, $context, $settings, $options, $messages_request;
1989
	global $boards_can, $participants, $smcFunc;
1990
	static $recycle_board = null;
1991
1992
	if ($recycle_board === null)
1993
		$recycle_board = !empty($modSettings['recycle_enable']) && !empty($modSettings['recycle_board']) ? (int) $modSettings['recycle_board'] : 0;
1994
1995
	// Remember which message this is.  (ie. reply #83)
1996
	static $counter = null;
1997
	if ($counter == null || $reset)
1998
		$counter = $_REQUEST['start'] + 1;
1999
2000
	// If the query returned false, bail.
2001
	if ($messages_request == false)
2002
		return false;
2003
2004
	// Start from the beginning...
2005
	if ($reset)
2006
		return @$smcFunc['db_data_seek']($messages_request, 0);
2007
2008
	// Attempt to get the next message.
2009
	$message = $smcFunc['db_fetch_assoc']($messages_request);
2010
	if (!$message)
2011
		return false;
2012
2013
	// Can't have an empty subject can we?
2014
	$message['subject'] = $message['subject'] != '' ? $message['subject'] : $txt['no_subject'];
2015
2016
	$message['first_subject'] = $message['first_subject'] != '' ? $message['first_subject'] : $txt['no_subject'];
2017
	$message['last_subject'] = $message['last_subject'] != '' ? $message['last_subject'] : $txt['no_subject'];
2018
2019
	// If it couldn't load, or the user was a guest.... someday may be done with a guest table.
2020
	if (!loadMemberContext($message['id_member']))
2021
	{
2022
		// Notice this information isn't used anywhere else.... *cough guest table cough*.
2023
		$memberContext[$message['id_member']]['name'] = $message['poster_name'];
2024
		$memberContext[$message['id_member']]['id'] = 0;
2025
		$memberContext[$message['id_member']]['group'] = $txt['guest_title'];
2026
		$memberContext[$message['id_member']]['link'] = $message['poster_name'];
2027
		$memberContext[$message['id_member']]['email'] = $message['poster_email'];
2028
	}
2029
	$memberContext[$message['id_member']]['ip'] = inet_dtop($message['poster_ip']);
2030
2031
	// Do the censor thang...
2032
	censorText($message['body']);
2033
	censorText($message['subject']);
2034
2035
	censorText($message['first_subject']);
2036
	censorText($message['last_subject']);
2037
2038
	// Shorten this message if necessary.
2039
	if ($context['compact'])
2040
	{
2041
		// Set the number of characters before and after the searched keyword.
2042
		$charLimit = 50;
2043
2044
		$message['body'] = strtr($message['body'], array("\n" => ' ', '<br>' => "\n"));
2045
		$message['body'] = parse_bbc($message['body'], $message['smileys_enabled'], $message['id_msg']);
2046
		$message['body'] = strip_tags(strtr($message['body'], array('</div>' => '<br>', '</li>' => '<br>')), '<br>');
2047
2048
		if ($smcFunc['strlen']($message['body']) > $charLimit)
2049
		{
2050
			if (empty($context['key_words']))
2051
				$message['body'] = $smcFunc['substr']($message['body'], 0, $charLimit) . '<strong>...</strong>';
2052
			else
2053
			{
2054
				$matchString = '';
2055
				$force_partial_word = false;
2056
				foreach ($context['key_words'] as $keyword)
2057
				{
2058
					$keyword = un_htmlspecialchars($keyword);
2059
					$keyword = preg_replace_callback('~(&amp;#(\d{1,7}|x[0-9a-fA-F]{1,6});)~', 'entity_fix__callback', strtr($keyword, array('\\\'' => '\'', '&' => '&amp;')));
2060
2061
					if (preg_match('~[\'\.,/@%&;:(){}\[\]_\-+\\\\]$~', $keyword) != 0 || preg_match('~^[\'\.,/@%&;:(){}\[\]_\-+\\\\]~', $keyword) != 0)
2062
						$force_partial_word = true;
2063
					$matchString .= strtr(preg_quote($keyword, '/'), array('\*' => '.+?')) . '|';
2064
				}
2065
				$matchString = un_htmlspecialchars(substr($matchString, 0, -1));
2066
2067
				$message['body'] = un_htmlspecialchars(strtr($message['body'], array('&nbsp;' => ' ', '<br>' => "\n", '&#91;' => '[', '&#93;' => ']', '&#58;' => ':', '&#64;' => '@')));
2068
2069
				if (empty($modSettings['search_method']) || $force_partial_word)
2070
					preg_match_all('/([^\s\W]{' . $charLimit . '}[\s\W]|[\s\W].{0,' . $charLimit . '}?|^)(' . $matchString . ')(.{0,' . $charLimit . '}[\s\W]|[^\s\W]{0,' . $charLimit . '})/is' . ($context['utf8'] ? 'u' : ''), $message['body'], $matches);
2071
				else
2072
					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]{0,' . $charLimit . '})/is' . ($context['utf8'] ? 'u' : ''), $message['body'], $matches);
2073
2074
				$message['body'] = '';
2075
				foreach ($matches[0] as $index => $match)
2076
				{
2077
					$match = strtr($smcFunc['htmlspecialchars']($match, ENT_QUOTES), array("\n" => '&nbsp;'));
2078
					$message['body'] .= '<strong>......</strong>&nbsp;' . $match . '&nbsp;<strong>......</strong>';
2079
				}
2080
			}
2081
2082
			// Re-fix the international characters.
2083
			$message['body'] = preg_replace_callback('~(&amp;#(\d{1,7}|x[0-9a-fA-F]{1,6});)~', 'entity_fix__callback', $message['body']);
2084
		}
2085
		$message['subject_highlighted'] = highlight($message['subject'], $context['key_words']);
2086
		$message['body_highlighted'] = highlight($message['body'], $context['key_words']);
2087
	}
2088
	else
2089
	{
2090
		$message['subject_highlighted'] = highlight($message['subject'], $context['key_words']);
2091
		$message['body_highlighted'] = highlight($message['body'], $context['key_words']);
2092
2093
		// Run BBC interpreter on the message.
2094
		$message['body'] = parse_bbc($message['body'], $message['smileys_enabled'], $message['id_msg']);
2095
	}
2096
2097
	// Make sure we don't end up with a practically empty message body.
2098
	$message['body'] = preg_replace('~^(?:&nbsp;)+$~', '', $message['body']);
2099
2100
	if (!empty($recycle_board) && $message['id_board'] == $recycle_board)
2101
	{
2102
		$message['first_icon'] = 'recycled';
2103
		$message['last_icon'] = 'recycled';
2104
		$message['icon'] = 'recycled';
2105
	}
2106
2107
	// Sadly, we need to check the icon ain't broke.
2108
	if (!empty($modSettings['messageIconChecks_enable']))
2109
	{
2110
		if (!isset($context['icon_sources'][$message['first_icon']]))
2111
			$context['icon_sources'][$message['first_icon']] = file_exists($settings['theme_dir'] . '/images/post/' . $message['first_icon'] . '.png') ? 'images_url' : 'default_images_url';
2112
		if (!isset($context['icon_sources'][$message['last_icon']]))
2113
			$context['icon_sources'][$message['last_icon']] = file_exists($settings['theme_dir'] . '/images/post/' . $message['last_icon'] . '.png') ? 'images_url' : 'default_images_url';
2114
		if (!isset($context['icon_sources'][$message['icon']]))
2115
			$context['icon_sources'][$message['icon']] = file_exists($settings['theme_dir'] . '/images/post/' . $message['icon'] . '.png') ? 'images_url' : 'default_images_url';
2116
	}
2117
	else
2118
	{
2119
		if (!isset($context['icon_sources'][$message['first_icon']]))
2120
			$context['icon_sources'][$message['first_icon']] = 'images_url';
2121
		if (!isset($context['icon_sources'][$message['last_icon']]))
2122
			$context['icon_sources'][$message['last_icon']] = 'images_url';
2123
		if (!isset($context['icon_sources'][$message['icon']]))
2124
			$context['icon_sources'][$message['icon']] = 'images_url';
2125
	}
2126
2127
	// Do we have quote tag enabled?
2128
	$quote_enabled = empty($modSettings['disabledBBC']) || !in_array('quote', explode(',', $modSettings['disabledBBC']));
2129
2130
	// Reference the main color class.
2131
	$colorClass = 'windowbg';
2132
2133
	// Sticky topics should get a different color, too.
2134
	if ($message['is_sticky'])
2135
		$colorClass .= ' sticky';
2136
2137
	// Locked topics get special treatment as well.
2138
	if ($message['locked'])
2139
		$colorClass .= ' locked';
2140
2141
	$output = array_merge($context['topics'][$message['id_msg']], array(
2142
		'id' => $message['id_topic'],
2143
		'is_sticky' => !empty($message['is_sticky']),
2144
		'is_locked' => !empty($message['locked']),
2145
		'css_class' => $colorClass,
2146
		'is_poll' => $modSettings['pollMode'] == '1' && $message['id_poll'] > 0,
2147
		'posted_in' => !empty($participants[$message['id_topic']]),
2148
		'views' => $message['num_views'],
2149
		'replies' => $message['num_replies'],
2150
		'can_reply' => in_array($message['id_board'], $boards_can['post_reply_any']) || in_array(0, $boards_can['post_reply_any']),
2151
		'can_quote' => (in_array($message['id_board'], $boards_can['post_reply_any']) || in_array(0, $boards_can['post_reply_any'])) && $quote_enabled,
2152
		'first_post' => array(
2153
			'id' => $message['first_msg'],
2154
			'time' => timeformat($message['first_poster_time']),
2155
			'timestamp' => forum_time(true, $message['first_poster_time']),
2156
			'subject' => $message['first_subject'],
2157
			'href' => $scripturl . '?topic=' . $message['id_topic'] . '.0',
2158
			'link' => '<a href="' . $scripturl . '?topic=' . $message['id_topic'] . '.0">' . $message['first_subject'] . '</a>',
2159
			'icon' => $message['first_icon'],
2160
			'icon_url' => $settings[$context['icon_sources'][$message['first_icon']]] . '/post/' . $message['first_icon'] . '.png',
2161
			'member' => array(
2162
				'id' => $message['first_member_id'],
2163
				'name' => $message['first_member_name'],
2164
				'href' => !empty($message['first_member_id']) ? $scripturl . '?action=profile;u=' . $message['first_member_id'] : '',
2165
				'link' => !empty($message['first_member_id']) ? '<a href="' . $scripturl . '?action=profile;u=' . $message['first_member_id'] . '" title="' . $txt['profile_of'] . ' ' . $message['first_member_name'] . '">' . $message['first_member_name'] . '</a>' : $message['first_member_name']
2166
			)
2167
		),
2168
		'last_post' => array(
2169
			'id' => $message['last_msg'],
2170
			'time' => timeformat($message['last_poster_time']),
2171
			'timestamp' => forum_time(true, $message['last_poster_time']),
2172
			'subject' => $message['last_subject'],
2173
			'href' => $scripturl . '?topic=' . $message['id_topic'] . ($message['num_replies'] == 0 ? '.0' : '.msg' . $message['last_msg']) . '#msg' . $message['last_msg'],
2174
			'link' => '<a href="' . $scripturl . '?topic=' . $message['id_topic'] . ($message['num_replies'] == 0 ? '.0' : '.msg' . $message['last_msg']) . '#msg' . $message['last_msg'] . '">' . $message['last_subject'] . '</a>',
2175
			'icon' => $message['last_icon'],
2176
			'icon_url' => $settings[$context['icon_sources'][$message['last_icon']]] . '/post/' . $message['last_icon'] . '.png',
2177
			'member' => array(
2178
				'id' => $message['last_member_id'],
2179
				'name' => $message['last_member_name'],
2180
				'href' => !empty($message['last_member_id']) ? $scripturl . '?action=profile;u=' . $message['last_member_id'] : '',
2181
				'link' => !empty($message['last_member_id']) ? '<a href="' . $scripturl . '?action=profile;u=' . $message['last_member_id'] . '" title="' . $txt['profile_of'] . ' ' . $message['last_member_name'] . '">' . $message['last_member_name'] . '</a>' : $message['last_member_name']
2182
			)
2183
		),
2184
		'board' => array(
2185
			'id' => $message['id_board'],
2186
			'name' => $message['board_name'],
2187
			'href' => $scripturl . '?board=' . $message['id_board'] . '.0',
2188
			'link' => '<a href="' . $scripturl . '?board=' . $message['id_board'] . '.0">' . $message['board_name'] . '</a>'
2189
		),
2190
		'category' => array(
2191
			'id' => $message['id_cat'],
2192
			'name' => $message['cat_name'],
2193
			'href' => $scripturl . '#c' . $message['id_cat'],
2194
			'link' => '<a href="' . $scripturl . '#c' . $message['id_cat'] . '">' . $message['cat_name'] . '</a>'
2195
		)
2196
	));
2197
2198
	if (!empty($options['display_quick_mod']))
2199
	{
2200
		$started = $output['first_post']['member']['id'] == $user_info['id'];
2201
2202
		$output['quick_mod'] = array(
2203
			'lock' => in_array(0, $boards_can['lock_any']) || in_array($output['board']['id'], $boards_can['lock_any']) || ($started && (in_array(0, $boards_can['lock_own']) || in_array($output['board']['id'], $boards_can['lock_own']))),
2204
			'sticky' => (in_array(0, $boards_can['make_sticky']) || in_array($output['board']['id'], $boards_can['make_sticky'])),
2205
			'move' => in_array(0, $boards_can['move_any']) || in_array($output['board']['id'], $boards_can['move_any']) || ($started && (in_array(0, $boards_can['move_own']) || in_array($output['board']['id'], $boards_can['move_own']))),
2206
			'remove' => in_array(0, $boards_can['remove_any']) || in_array($output['board']['id'], $boards_can['remove_any']) || ($started && (in_array(0, $boards_can['remove_own']) || in_array($output['board']['id'], $boards_can['remove_own']))),
2207
			'restore' => $context['can_restore_perm'] && ($modSettings['recycle_board'] == $output['board']['id']),
2208
		);
2209
2210
		$context['can_lock'] |= $output['quick_mod']['lock'];
2211
		$context['can_sticky'] |= $output['quick_mod']['sticky'];
2212
		$context['can_move'] |= $output['quick_mod']['move'];
2213
		$context['can_remove'] |= $output['quick_mod']['remove'];
2214
		$context['can_merge'] |= in_array($output['board']['id'], $boards_can['merge_any']);
2215
		$context['can_restore'] |= $output['quick_mod']['restore'];
2216
		$context['can_markread'] = $context['user']['is_logged'];
2217
2218
		$context['qmod_actions'] = array('remove', 'lock', 'sticky', 'move', 'merge', 'restore', 'markread');
2219
		call_integration_hook('integrate_quick_mod_actions_search');
2220
	}
2221
2222
	$output['matches'][] = array(
2223
		'id' => $message['id_msg'],
2224
		'attachment' => array(),
2225
		'member' => &$memberContext[$message['id_member']],
2226
		'icon' => $message['icon'],
2227
		'icon_url' => $settings[$context['icon_sources'][$message['icon']]] . '/post/' . $message['icon'] . '.png',
2228
		'subject' => $message['subject'],
2229
		'subject_highlighted' => $message['subject_highlighted'],
2230
		'time' => timeformat($message['poster_time']),
2231
		'timestamp' => forum_time(true, $message['poster_time']),
2232
		'counter' => $counter,
2233
		'modified' => array(
2234
			'time' => timeformat($message['modified_time']),
2235
			'timestamp' => forum_time(true, $message['modified_time']),
2236
			'name' => $message['modified_name']
2237
		),
2238
		'body' => $message['body'],
2239
		'body_highlighted' => $message['body_highlighted'],
2240
		'start' => 'msg' . $message['id_msg']
2241
	);
2242
	$counter++;
2243
2244
	call_integration_hook('integrate_search_message_context', array(&$output, &$message, $counter));
2245
2246
	return $output;
2247
}
2248
2249
/**
2250
 * Creates a search API and returns the object.
2251
 *
2252
 * @return search_api_interface An instance of the search API interface
2253
 */
2254
function findSearchAPI()
2255
{
2256
	global $sourcedir, $modSettings, $searchAPI, $txt;
2257
2258
	require_once($sourcedir . '/Subs-Package.php');
2259
	require_once($sourcedir . '/Class-SearchAPI.php');
2260
2261
	// Search has a special database set.
2262
	db_extend('search');
2263
2264
	// Load up the search API we are going to use.
2265
	$modSettings['search_index'] = empty($modSettings['search_index']) ? 'standard' : $modSettings['search_index'];
2266
	if (!file_exists($sourcedir . '/SearchAPI-' . ucwords($modSettings['search_index']) . '.php'))
2267
		fatal_lang_error('search_api_missing');
2268
	require_once($sourcedir . '/SearchAPI-' . ucwords($modSettings['search_index']) . '.php');
2269
2270
	// Create an instance of the search API and check it is valid for this version of SMF.
2271
	$search_class_name = $modSettings['search_index'] . '_search';
2272
	$searchAPI = new $search_class_name();
2273
2274
	// An invalid Search API.
2275
	if (!$searchAPI || !($searchAPI instanceof search_api_interface) || ($searchAPI->supportsMethod('isValid') && !$searchAPI->isValid()) || !matchPackageVersion(SMF_VERSION, $searchAPI->min_smf_version . '-' . $searchAPI->version_compatible))
0 ignored issues
show
$searchAPI is of type object, thus it always evaluated to true.
Loading history...
Accessing version_compatible on the interface search_api_interface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
Accessing min_smf_version on the interface search_api_interface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
2276
	{
2277
		// Log the error.
2278
		loadLanguage('Errors');
2279
		log_error(sprintf($txt['search_api_not_compatible'], 'SearchAPI-' . ucwords($modSettings['search_index']) . '.php'), 'critical');
2280
2281
		require_once($sourcedir . '/SearchAPI-Standard.php');
2282
		$searchAPI = new standard_search();
2283
	}
2284
2285
	return $searchAPI;
2286
}
2287
2288
/**
2289
 * This function compares the length of two strings plus a little.
2290
 * What it does:
2291
 * - callback function for usort used to sort the fulltext results.
2292
 * - passes sorting duty to the current API.
2293
 *
2294
 * @param string $a
2295
 * @param string $b
2296
 * @return int
2297
 */
2298
function searchSort($a, $b)
2299
{
2300
	global $searchAPI;
2301
2302
	return $searchAPI->searchSort($a, $b);
2303
}
2304
2305
/**
2306
 * Highlighting matching string
2307
 *
2308
 * @param string $text Text to search through
2309
 * @param array $words List of keywords to search
2310
 *
2311
 * @return string Text with highlighted keywords
2312
 */
2313
function highlight($text, array $words)
2314
{
2315
	$words = implode('|', array_map('preg_quote', $words));
2316
	$highlighted = preg_filter('/' . $words . '/i', '<span class="highlight">$0</span>', $text);
2317
2318
	if (!empty($highlighted))
2319
		$text = $highlighted;
2320
2321
	return $text;
2322
}
2323
2324
?>