Failed Conditions
Branch release-2.1 (4e22cf)
by Rick
06:39
created

Sources/ManageSearch.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/**
4
 * The admin screen to change the search settings.
5
 *
6
 * Simple Machines Forum (SMF)
7
 *
8
 * @package SMF
9
 * @author Simple Machines http://www.simplemachines.org
10
 * @copyright 2017 Simple Machines and individual contributors
11
 * @license http://www.simplemachines.org/about/smf/license.php BSD
12
 *
13
 * @version 2.1 Beta 4
14
 */
15
16
if (!defined('SMF'))
17
	die('No direct access...');
18
19
/**
20
 * Main entry point for the admin search settings screen.
21
 * It checks permissions, and it forwards to the appropriate function based on
22
 * the given sub-action.
23
 * Defaults to sub-action 'settings'.
24
 * Called by ?action=admin;area=managesearch.
25
 * Requires the admin_forum permission.
26
 *
27
 * @uses ManageSearch template.
28
 * @uses Search language file.
29
 */
30
function ManageSearch()
31
{
32
	global $context, $txt;
33
34
	isAllowedTo('admin_forum');
35
36
	loadLanguage('Search');
37
	loadTemplate('ManageSearch');
38
39
	db_extend('search');
40
41
	$subActions = array(
42
		'settings' => 'EditSearchSettings',
43
		'weights' => 'EditWeights',
44
		'method' => 'EditSearchMethod',
45
		'createfulltext' => 'EditSearchMethod',
46
		'removecustom' => 'EditSearchMethod',
47
		'removefulltext' => 'EditSearchMethod',
48
		'createmsgindex' => 'CreateMessageIndex',
49
	);
50
51
	// Default the sub-action to 'edit search settings'.
52
	$_REQUEST['sa'] = isset($_REQUEST['sa']) && isset($subActions[$_REQUEST['sa']]) ? $_REQUEST['sa'] : 'weights';
53
54
	$context['sub_action'] = $_REQUEST['sa'];
55
56
	// Create the tabs for the template.
57
	$context[$context['admin_menu_name']]['tab_data'] = array(
58
		'title' => $txt['manage_search'],
59
		'help' => 'search',
60
		'description' => $txt['search_settings_desc'],
61
		'tabs' => array(
62
			'weights' => array(
63
				'description' => $txt['search_weights_desc'],
64
			),
65
			'method' => array(
66
				'description' => $txt['search_method_desc'],
67
			),
68
			'settings' => array(
69
				'description' => $txt['search_settings_desc'],
70
			),
71
		),
72
	);
73
74
	call_integration_hook('integrate_manage_search', array(&$subActions));
75
76
	// Call the right function for this sub-action.
77
	call_helper($subActions[$_REQUEST['sa']]);
78
}
79
80
/**
81
 * Edit some general settings related to the search function.
82
 * Called by ?action=admin;area=managesearch;sa=settings.
83
 * Requires the admin_forum permission.
84
 *
85
 * @param bool $return_config Whether or not to return the config_vars array (used for admin search)
86
 * @return void|array Returns nothing or returns the $config_vars array if $return_config is true
87
 * @uses ManageSearch template, 'modify_settings' sub-template.
88
 */
89
function EditSearchSettings($return_config = false)
90
{
91
	global $txt, $context, $scripturl, $sourcedir, $modSettings;
92
93
	// What are we editing anyway?
94
	$config_vars = array(
95
			// Permission...
96
			array('permissions', 'search_posts'),
97
			// Some simple settings.
98
			array('int', 'search_results_per_page'),
99
			array('int', 'search_max_results', 'subtext' => $txt['search_max_results_disable']),
100
		'',
101
			// Some limitations.
102
			array('int', 'search_floodcontrol_time', 'subtext' => $txt['search_floodcontrol_time_desc'], 6, 'postinput' => $txt['seconds']),
103
	);
104
105
	call_integration_hook('integrate_modify_search_settings', array(&$config_vars));
106
107
	// Perhaps the search method wants to add some settings?
108
	require_once($sourcedir . '/Search.php');
109
	$searchAPI = findSearchAPI();
110
	if (is_callable(array($searchAPI, 'searchSettings')))
111
		call_user_func_array(array($searchAPI, 'searchSettings'), array(&$config_vars));
112
113
	if ($return_config)
114
		return $config_vars;
115
116
	$context['page_title'] = $txt['search_settings_title'];
117
	$context['sub_template'] = 'show_settings';
118
119
	// We'll need this for the settings.
120
	require_once($sourcedir . '/ManageServer.php');
121
122
	// A form was submitted.
123
	if (isset($_REQUEST['save']))
124
	{
125
		checkSession();
126
127
		call_integration_hook('integrate_save_search_settings');
128
129
		if (empty($_POST['search_results_per_page']))
130
			$_POST['search_results_per_page'] = !empty($modSettings['search_results_per_page']) ? $modSettings['search_results_per_page'] : $modSettings['defaultMaxMessages'];
131
		saveDBSettings($config_vars);
132
		$_SESSION['adm-save'] = true;
133
		redirectexit('action=admin;area=managesearch;sa=settings;' . $context['session_var'] . '=' . $context['session_id']);
134
	}
135
136
	// Prep the template!
137
	$context['post_url'] = $scripturl . '?action=admin;area=managesearch;save;sa=settings';
138
	$context['settings_title'] = $txt['search_settings_title'];
139
140
	// We need this for the in-line permissions
141
	createToken('admin-mp');
142
143
	prepareDBSettingContext($config_vars);
144
}
145
146
/**
147
 * Edit the relative weight of the search factors.
148
 * Called by ?action=admin;area=managesearch;sa=weights.
149
 * Requires the admin_forum permission.
150
 *
151
 * @uses ManageSearch template, 'modify_weights' sub-template.
152
 */
153
function EditWeights()
154
{
155
	global $txt, $context, $modSettings;
156
157
	$context['page_title'] = $txt['search_weights_title'];
158
	$context['sub_template'] = 'modify_weights';
159
160
	$factors = array(
161
		'search_weight_frequency',
162
		'search_weight_age',
163
		'search_weight_length',
164
		'search_weight_subject',
165
		'search_weight_first_message',
166
		'search_weight_sticky',
167
	);
168
169
	call_integration_hook('integrate_modify_search_weights', array(&$factors));
170
171
	// A form was submitted.
172
	if (isset($_POST['save']))
173
	{
174
		checkSession();
175
		validateToken('admin-msw');
176
177
		call_integration_hook('integrate_save_search_weights');
178
179
		$changes = array();
180
		foreach ($factors as $factor)
181
			$changes[$factor] = (int) $_POST[$factor];
182
		updateSettings($changes);
183
	}
184
185
	$context['relative_weights'] = array('total' => 0);
186
	foreach ($factors as $factor)
187
		$context['relative_weights']['total'] += isset($modSettings[$factor]) ? $modSettings[$factor] : 0;
188
189
	foreach ($factors as $factor)
190
		$context['relative_weights'][$factor] = round(100 * (isset($modSettings[$factor]) ? $modSettings[$factor] : 0) / $context['relative_weights']['total'], 1);
191
192
	createToken('admin-msw');
193
}
194
195
/**
196
 * Edit the search method and search index used.
197
 * Calculates the size of the current search indexes in use.
198
 * Allows to create and delete a fulltext index on the messages table.
199
 * Allows to delete a custom index (that CreateMessageIndex() created).
200
 * Called by ?action=admin;area=managesearch;sa=method.
201
 * Requires the admin_forum permission.
202
 *
203
 * @uses ManageSearch template, 'select_search_method' sub-template.
204
 */
205
function EditSearchMethod()
206
{
207
	global $txt, $context, $modSettings, $smcFunc, $db_type, $db_prefix;
208
209
	$context[$context['admin_menu_name']]['current_subsection'] = 'method';
210
	$context['page_title'] = $txt['search_method_title'];
211
	$context['sub_template'] = 'select_search_method';
212
	$context['supports_fulltext'] = $smcFunc['db_search_support']('fulltext');
213
214
	// Load any apis.
215
	$context['search_apis'] = loadSearchAPIs();
216
217
	// Detect whether a fulltext index is set.
218
	if ($context['supports_fulltext'])
219
		detectFulltextIndex();
220
221
	if (!empty($_REQUEST['sa']) && $_REQUEST['sa'] == 'createfulltext')
222
	{
223
		checkSession('get');
224
		validateToken('admin-msm', 'get');
225
226
		if ($db_type == 'postgresql') {
227
			$smcFunc['db_query']('', '
228
				DROP INDEX IF EXISTS {db_prefix}messages_ftx',
229
				array(
230
					'db_error_skip' => true,
231
				)
232
			);
233
234
			$language_ftx = $smcFunc['db_search_language']();
235
236
			$smcFunc['db_query']('', '
237
				CREATE INDEX {db_prefix}messages_ftx ON {db_prefix}messages
238
				USING gin(to_tsvector({string:language},body))',
239
				array(
240
					'language' => $language_ftx
241
				)
242
			);
243
		}
244
		else
245
		{
246
			// Make sure it's gone before creating it.
247
			$smcFunc['db_query']('', '
248
				ALTER TABLE {db_prefix}messages
249
				DROP INDEX body',
250
				array(
251
					'db_error_skip' => true,
252
				)
253
			);
254
255
			$smcFunc['db_query']('', '
256
				ALTER TABLE {db_prefix}messages
257
				ADD FULLTEXT body (body)',
258
				array(
259
				)
260
			);
261
		}
262
	}
263
	elseif (!empty($_REQUEST['sa']) && $_REQUEST['sa'] == 'removefulltext' && !empty($context['fulltext_index']))
264
	{
265
		checkSession('get');
266
		validateToken('admin-msm', 'get');
267
268
		$smcFunc['db_query']('', '
269
			ALTER TABLE {db_prefix}messages
270
			DROP INDEX ' . implode(',
271
			DROP INDEX ', $context['fulltext_index']),
272
			array(
273
				'db_error_skip' => true,
274
			)
275
		);
276
277
		$context['fulltext_index'] = array();
278
279
		// Go back to the default search method.
280 View Code Duplication
		if (!empty($modSettings['search_index']) && $modSettings['search_index'] == 'fulltext')
281
			updateSettings(array(
282
				'search_index' => '',
283
			));
284
	}
285
	elseif (!empty($_REQUEST['sa']) && $_REQUEST['sa'] == 'removecustom')
286
	{
287
		checkSession('get');
288
		validateToken('admin-msm', 'get');
289
290
		db_extend();
291
		$tables = $smcFunc['db_list_tables'](false, $db_prefix . 'log_search_words');
292
		if (!empty($tables))
293
		{
294
			$smcFunc['db_search_query']('drop_words_table', '
295
				DROP TABLE {db_prefix}log_search_words',
296
				array(
297
				)
298
			);
299
		}
300
301
		updateSettings(array(
302
			'search_custom_index_config' => '',
303
			'search_custom_index_resume' => '',
304
		));
305
306
		// Go back to the default search method.
307 View Code Duplication
		if (!empty($modSettings['search_index']) && $modSettings['search_index'] == 'custom')
308
			updateSettings(array(
309
				'search_index' => '',
310
			));
311
	}
312
	elseif (isset($_POST['save']))
313
	{
314
		checkSession();
315
		validateToken('admin-msmpost');
316
317
		updateSettings(array(
318
			'search_index' => empty($_POST['search_index']) || (!in_array($_POST['search_index'], array('fulltext', 'custom')) && !isset($context['search_apis'][$_POST['search_index']])) ? '' : $_POST['search_index'],
319
			'search_force_index' => isset($_POST['search_force_index']) ? '1' : '0',
320
			'search_match_words' => isset($_POST['search_match_words']) ? '1' : '0',
321
		));
322
	}
323
324
	$context['table_info'] = array(
325
		'data_length' => 0,
326
		'index_length' => 0,
327
		'fulltext_length' => 0,
328
		'custom_index_length' => 0,
329
	);
330
331
	// Get some info about the messages table, to show its size and index size.
332
	if ($db_type == 'mysql')
333
	{
334 View Code Duplication
		if (preg_match('~^`(.+?)`\.(.+?)$~', $db_prefix, $match) !== 0)
335
			$request = $smcFunc['db_query']('', '
336
				SHOW TABLE STATUS
337
				FROM {string:database_name}
338
				LIKE {string:table_name}',
339
				array(
340
					'database_name' => '`' . strtr($match[1], array('`' => '')) . '`',
341
					'table_name' => str_replace('_', '\_', $match[2]) . 'messages',
342
				)
343
			);
344
		else
345
			$request = $smcFunc['db_query']('', '
346
				SHOW TABLE STATUS
347
				LIKE {string:table_name}',
348
				array(
349
					'table_name' => str_replace('_', '\_', $db_prefix) . 'messages',
350
				)
351
			);
352
		if ($request !== false && $smcFunc['db_num_rows']($request) == 1)
353
		{
354
			// Only do this if the user has permission to execute this query.
355
			$row = $smcFunc['db_fetch_assoc']($request);
356
			$context['table_info']['data_length'] = $row['Data_length'];
357
			$context['table_info']['index_length'] = $row['Index_length'];
358
			$context['table_info']['fulltext_length'] = $row['Index_length'];
359
			$smcFunc['db_free_result']($request);
360
		}
361
362
		// Now check the custom index table, if it exists at all.
363 View Code Duplication
		if (preg_match('~^`(.+?)`\.(.+?)$~', $db_prefix, $match) !== 0)
364
			$request = $smcFunc['db_query']('', '
365
				SHOW TABLE STATUS
366
				FROM {string:database_name}
367
				LIKE {string:table_name}',
368
				array(
369
					'database_name' => '`' . strtr($match[1], array('`' => '')) . '`',
370
					'table_name' => str_replace('_', '\_', $match[2]) . 'log_search_words',
371
				)
372
			);
373
		else
374
			$request = $smcFunc['db_query']('', '
375
				SHOW TABLE STATUS
376
				LIKE {string:table_name}',
377
				array(
378
					'table_name' => str_replace('_', '\_', $db_prefix) . 'log_search_words',
379
				)
380
			);
381
		if ($request !== false && $smcFunc['db_num_rows']($request) == 1)
382
		{
383
			// Only do this if the user has permission to execute this query.
384
			$row = $smcFunc['db_fetch_assoc']($request);
385
			$context['table_info']['index_length'] += $row['Data_length'] + $row['Index_length'];
386
			$context['table_info']['custom_index_length'] = $row['Data_length'] + $row['Index_length'];
387
			$smcFunc['db_free_result']($request);
388
		}
389
	}
390
	elseif ($db_type == 'postgresql')
391
	{
392
		// In order to report the sizes correctly we need to perform vacuum (optimize) on the tables we will be using.
393
		//db_extend();
394
		//$temp_tables = $smcFunc['db_list_tables']();
395
		//foreach ($temp_tables as $table)
396
		//	if ($table == $db_prefix. 'messages' || $table == $db_prefix. 'log_search_words')
397
		//		$smcFunc['db_optimize_table']($table);
398
399
		// PostGreSql has some hidden sizes.
400
		$request = $smcFunc['db_query']('', '
401
			SELECT
402
				indexname,
403
				pg_relation_size(quote_ident(t.tablename)::text) AS table_size,
404
				pg_relation_size(quote_ident(indexrelname)::text) AS index_size
405
			FROM pg_tables t
406
			LEFT OUTER JOIN pg_class c ON t.tablename=c.relname
407
			LEFT OUTER JOIN
408
				( SELECT c.relname AS ctablename, ipg.relname AS indexname, indexrelname FROM pg_index x
409
						JOIN pg_class c ON c.oid = x.indrelid
410
						JOIN pg_class ipg ON ipg.oid = x.indexrelid
411
						JOIN pg_stat_all_indexes psai ON x.indexrelid = psai.indexrelid )
412
				AS foo
413
				ON t.tablename = foo.ctablename
414
			WHERE t.schemaname= {string:schema} and (
415
				indexname = {string:messages_ftx} OR indexname = {string:log_search_words} )',
416
			array(
417
				'messages_ftx' => $db_prefix . 'messages_ftx',
418
				'log_search_words' => $db_prefix . 'log_search_words',
419
				'schema' => 'public',
420
			)
421
		);
422
423
		if ($request !== false && $smcFunc['db_num_rows']($request) > 0)
424
		{
425
			while ($row = $smcFunc['db_fetch_assoc']($request))
426
			{
427
				if ($row['indexname'] == $db_prefix . 'messages_ftx')
428
				{
429
					$context['table_info']['data_length'] = (int) $row['table_size'];
430
					$context['table_info']['index_length'] = (int) $row['index_size'];
431
					$context['table_info']['fulltext_length'] = (int) $row['index_size'];
432
				}
433
				elseif ($row['indexname'] == $db_prefix . 'log_search_words')
434
				{
435
					$context['table_info']['index_length'] = (int) $row['index_size'];
436
					$context['table_info']['custom_index_length'] = (int) $row['index_size'];
437
				}
438
			}
439
			$smcFunc['db_free_result']($request);
440
		}
441 View Code Duplication
		else
442
			// Didn't work for some reason...
443
			$context['table_info'] = array(
444
				'data_length' => $txt['not_applicable'],
445
				'index_length' => $txt['not_applicable'],
446
				'fulltext_length' => $txt['not_applicable'],
447
				'custom_index_length' => $txt['not_applicable'],
448
			);
449
	}
450 View Code Duplication
	else
451
		$context['table_info'] = array(
452
			'data_length' => $txt['not_applicable'],
453
			'index_length' => $txt['not_applicable'],
454
			'fulltext_length' => $txt['not_applicable'],
455
			'custom_index_length' => $txt['not_applicable'],
456
		);
457
458
	// Format the data and index length in kilobytes.
459
	foreach ($context['table_info'] as $type => $size)
460
	{
461
		// If it's not numeric then just break.  This database engine doesn't support size.
462
		if (!is_numeric($size))
463
			break;
464
465
		$context['table_info'][$type] = comma_format($context['table_info'][$type] / 1024) . ' ' . $txt['search_method_kilobytes'];
466
	}
467
468
	$context['custom_index'] = !empty($modSettings['search_custom_index_config']);
469
	$context['partial_custom_index'] = !empty($modSettings['search_custom_index_resume']) && empty($modSettings['search_custom_index_config']);
470
	$context['double_index'] = !empty($context['fulltext_index']) && $context['custom_index'];
471
472
	createToken('admin-msmpost');
473
	createToken('admin-msm', 'get');
474
}
475
476
/**
477
 * Create a custom search index for the messages table.
478
 * Called by ?action=admin;area=managesearch;sa=createmsgindex.
479
 * Linked from the EditSearchMethod screen.
480
 * Requires the admin_forum permission.
481
 * Depending on the size of the message table, the process is divided in steps.
482
 *
483
 * @uses ManageSearch template, 'create_index', 'create_index_progress', and 'create_index_done'
484
 *  sub-templates.
485
 */
486
function CreateMessageIndex()
487
{
488
	global $modSettings, $context, $smcFunc, $db_prefix, $txt;
489
490
	// Scotty, we need more time...
491
	@set_time_limit(600);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
492
	if (function_exists('apache_reset_timeout'))
493
		@apache_reset_timeout();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
494
495
	$context[$context['admin_menu_name']]['current_subsection'] = 'method';
496
	$context['page_title'] = $txt['search_index_custom'];
497
498
	$messages_per_batch = 50;
499
500
	$index_properties = array(
501
		2 => array(
502
			'column_definition' => 'small',
503
			'step_size' => 1000000,
504
		),
505
		4 => array(
506
			'column_definition' => 'medium',
507
			'step_size' => 1000000,
508
			'max_size' => 16777215,
509
		),
510
		5 => array(
511
			'column_definition' => 'large',
512
			'step_size' => 100000000,
513
			'max_size' => 2000000000,
514
		),
515
	);
516
517
	if (isset($_REQUEST['resume']) && !empty($modSettings['search_custom_index_resume']))
518
	{
519
		$context['index_settings'] = $smcFunc['json_decode']($modSettings['search_custom_index_resume'], true);
520
		$context['start'] = (int) $context['index_settings']['resume_at'];
521
		unset($context['index_settings']['resume_at']);
522
		$context['step'] = 1;
523
	}
524
	else
525
	{
526
		$context['index_settings'] = array(
527
			'bytes_per_word' => isset($_REQUEST['bytes_per_word']) && isset($index_properties[$_REQUEST['bytes_per_word']]) ? (int) $_REQUEST['bytes_per_word'] : 2,
528
		);
529
		$context['start'] = isset($_REQUEST['start']) ? (int) $_REQUEST['start'] : 0;
530
		$context['step'] = isset($_REQUEST['step']) ? (int) $_REQUEST['step'] : 0;
531
532
		// admin timeouts are painful when building these long indexes - but only if we actually have such things enabled
533
		if (empty($modSettings['securityDisable']) && $_SESSION['admin_time'] + 3300 < time() && $context['step'] >= 1)
534
			$_SESSION['admin_time'] = time();
535
	}
536
537
	if ($context['step'] !== 0)
538
		checkSession('request');
539
540
	// Step 0: let the user determine how they like their index.
541
	if ($context['step'] === 0)
542
	{
543
		$context['sub_template'] = 'create_index';
544
	}
545
546
	// Step 1: insert all the words.
547
	if ($context['step'] === 1)
548
	{
549
		$context['sub_template'] = 'create_index_progress';
550
551
		if ($context['start'] === 0)
552
		{
553
			db_extend();
554
			$tables = $smcFunc['db_list_tables'](false, $db_prefix . 'log_search_words');
555
			if (!empty($tables))
556
			{
557
				$smcFunc['db_search_query']('drop_words_table', '
558
					DROP TABLE {db_prefix}log_search_words',
559
					array(
560
					)
561
				);
562
			}
563
564
			$smcFunc['db_create_word_search']($index_properties[$context['index_settings']['bytes_per_word']]['column_definition']);
565
566
			// Temporarily switch back to not using a search index.
567 View Code Duplication
			if (!empty($modSettings['search_index']) && $modSettings['search_index'] == 'custom')
568
				updateSettings(array('search_index' => ''));
569
570
			// Don't let simultanious processes be updating the search index.
571
			if (!empty($modSettings['search_custom_index_config']))
572
				updateSettings(array('search_custom_index_config' => ''));
573
		}
574
575
		$num_messages = array(
576
			'done' => 0,
577
			'todo' => 0,
578
		);
579
580
		$request = $smcFunc['db_query']('', '
581
			SELECT id_msg >= {int:starting_id} AS todo, COUNT(*) AS num_messages
582
			FROM {db_prefix}messages
583
			GROUP BY todo',
584
			array(
585
				'starting_id' => $context['start'],
586
			)
587
		);
588
		while ($row = $smcFunc['db_fetch_assoc']($request))
589
			$num_messages[empty($row['todo']) ? 'done' : 'todo'] = $row['num_messages'];
590
591
		if (empty($num_messages['todo']))
592
		{
593
			$context['step'] = 2;
594
			$context['percentage'] = 80;
595
			$context['start'] = 0;
596
		}
597
		else
598
		{
599
			// Number of seconds before the next step.
600
			$stop = time() + 3;
601
			while (time() < $stop)
602
			{
603
				$inserts = array();
604
				$request = $smcFunc['db_query']('', '
605
					SELECT id_msg, body
606
					FROM {db_prefix}messages
607
					WHERE id_msg BETWEEN {int:starting_id} AND {int:ending_id}
608
					LIMIT {int:limit}',
609
					array(
610
						'starting_id' => $context['start'],
611
						'ending_id' => $context['start'] + $messages_per_batch - 1,
612
						'limit' => $messages_per_batch,
613
					)
614
				);
615
				$forced_break = false;
616
				$number_processed = 0;
617
				while ($row = $smcFunc['db_fetch_assoc']($request))
618
				{
619
					// In theory it's possible for one of these to take friggin ages so add more timeout protection.
620
					if ($stop < time())
621
					{
622
						$forced_break = true;
623
						break;
624
					}
625
626
					$number_processed++;
627
					foreach (text2words($row['body'], $context['index_settings']['bytes_per_word'], true) as $id_word)
628
					{
629
						$inserts[] = array($id_word, $row['id_msg']);
630
					}
631
				}
632
				$num_messages['done'] += $number_processed;
633
				$num_messages['todo'] -= $number_processed;
634
				$smcFunc['db_free_result']($request);
635
636
				$context['start'] += $forced_break ? $number_processed : $messages_per_batch;
637
638
				if (!empty($inserts))
639
					$smcFunc['db_insert']('ignore',
640
						'{db_prefix}log_search_words',
641
						array('id_word' => 'int', 'id_msg' => 'int'),
642
						$inserts,
643
						array('id_word', 'id_msg')
644
					);
645
				if ($num_messages['todo'] === 0)
646
				{
647
					$context['step'] = 2;
648
					$context['start'] = 0;
649
					break;
650
				}
651
				else
652
					updateSettings(array('search_custom_index_resume' => $smcFunc['json_encode'](array_merge($context['index_settings'], array('resume_at' => $context['start'])))));
653
			}
654
655
			// Since there are still two steps to go, 80% is the maximum here.
656
			$context['percentage'] = round($num_messages['done'] / ($num_messages['done'] + $num_messages['todo']), 3) * 80;
657
		}
658
	}
659
660
	// Step 2: removing the words that occur too often and are of no use.
661
	elseif ($context['step'] === 2)
662
	{
663
		if ($context['index_settings']['bytes_per_word'] < 4)
664
			$context['step'] = 3;
665
		else
666
		{
667
			$stop_words = $context['start'] === 0 || empty($modSettings['search_stopwords']) ? array() : explode(',', $modSettings['search_stopwords']);
668
			$stop = time() + 3;
669
			$context['sub_template'] = 'create_index_progress';
670
			$max_messages = ceil(60 * $modSettings['totalMessages'] / 100);
671
672
			while (time() < $stop)
673
			{
674
				$request = $smcFunc['db_query']('', '
675
					SELECT id_word, COUNT(id_word) AS num_words
676
					FROM {db_prefix}log_search_words
677
					WHERE id_word BETWEEN {int:starting_id} AND {int:ending_id}
678
					GROUP BY id_word
679
					HAVING COUNT(id_word) > {int:minimum_messages}',
680
					array(
681
						'starting_id' => $context['start'],
682
						'ending_id' => $context['start'] + $index_properties[$context['index_settings']['bytes_per_word']]['step_size'] - 1,
683
						'minimum_messages' => $max_messages,
684
					)
685
				);
686
				while ($row = $smcFunc['db_fetch_assoc']($request))
687
					$stop_words[] = $row['id_word'];
688
				$smcFunc['db_free_result']($request);
689
690
				updateSettings(array('search_stopwords' => implode(',', $stop_words)));
691
692
				if (!empty($stop_words))
693
					$smcFunc['db_query']('', '
694
						DELETE FROM {db_prefix}log_search_words
695
						WHERE id_word in ({array_int:stop_words})',
696
						array(
697
							'stop_words' => $stop_words,
698
						)
699
					);
700
701
				$context['start'] += $index_properties[$context['index_settings']['bytes_per_word']]['step_size'];
702
				if ($context['start'] > $index_properties[$context['index_settings']['bytes_per_word']]['max_size'])
703
				{
704
					$context['step'] = 3;
705
					break;
706
				}
707
			}
708
			$context['percentage'] = 80 + round($context['start'] / $index_properties[$context['index_settings']['bytes_per_word']]['max_size'], 3) * 20;
709
		}
710
	}
711
712
	// Step 3: remove words not distinctive enough.
713
	if ($context['step'] === 3)
714
	{
715
		$context['sub_template'] = 'create_index_done';
716
717
		updateSettings(array('search_index' => 'custom', 'search_custom_index_config' => $smcFunc['json_encode']($context['index_settings'])));
718
		$smcFunc['db_query']('', '
719
			DELETE FROM {db_prefix}settings
720
			WHERE variable = {string:search_custom_index_resume}',
721
			array(
722
				'search_custom_index_resume' => 'search_custom_index_resume',
723
			)
724
		);
725
	}
726
}
727
728
/**
729
 * Get the installed Search API implementations.
730
 * This function checks for patterns in comments on top of the Search-API files!
731
 * In addition to filenames pattern.
732
 * It loads the search API classes if identified.
733
 * This function is used by EditSearchMethod to list all installed API implementations.
734
 */
735
function loadSearchAPIs()
736
{
737
	global $sourcedir, $txt;
738
739
	$apis = array();
740
	if ($dh = opendir($sourcedir))
741
	{
742
		while (($file = readdir($dh)) !== false)
743
		{
744
			if (is_file($sourcedir . '/' . $file) && preg_match('~^SearchAPI-([A-Za-z\d_]+)\.php$~', $file, $matches))
745
			{
746
				// Check this is definitely a valid API!
747
				$fp = fopen($sourcedir . '/' . $file, 'rb');
748
				$header = fread($fp, 4096);
749
				fclose($fp);
750
751
				if (strpos($header, '* SearchAPI-' . $matches[1] . '.php') !== false)
752
				{
753
					require_once($sourcedir . '/' . $file);
754
755
					$index_name = strtolower($matches[1]);
756
					$search_class_name = $index_name . '_search';
757
					$searchAPI = new $search_class_name();
758
759
					// No Support?  NEXT!
760
					if (!$searchAPI->is_supported)
761
						continue;
762
763
					$apis[$index_name] = array(
764
						'filename' => $file,
765
						'setting_index' => $index_name,
766
						'has_template' => in_array($index_name, array('custom', 'fulltext', 'standard')),
767
						'label' => $index_name && isset($txt['search_index_' . $index_name]) ? $txt['search_index_' . $index_name] : '',
768
						'desc' => $index_name && isset($txt['search_index_' . $index_name . '_desc']) ? $txt['search_index_' . $index_name . '_desc'] : '',
769
					);
770
				}
771
			}
772
		}
773
	}
774
	closedir($dh);
775
776
	return $apis;
777
}
778
779
/**
780
 * Checks if the message table already has a fulltext index created and returns the key name
781
 * Determines if a db is capable of creating a fulltext index
782
 */
783
function detectFulltextIndex()
784
{
785
	global $smcFunc, $context, $db_prefix;
786
787
	// We need this for db_get_version
788
	db_extend();
789
790
	if ($smcFunc['db_title'] == 'PostgreSQL') {
791
		$request = $smcFunc['db_query']('', '
792
			SELECT
793
				indexname
794
			FROM pg_tables t
795
			LEFT OUTER JOIN
796
				( SELECT c.relname AS ctablename, ipg.relname AS indexname, indexrelname FROM pg_index x
797
						JOIN pg_class c ON c.oid = x.indrelid
798
						JOIN pg_class ipg ON ipg.oid = x.indexrelid
799
						JOIN pg_stat_all_indexes psai ON x.indexrelid = psai.indexrelid )
800
				AS foo
801
				ON t.tablename = foo.ctablename
802
			WHERE t.schemaname= {string:schema} and indexname = {string:messages_ftx}',
803
			array(
804
				'schema' => 'public',
805
				'messages_ftx' => $db_prefix . 'messages_ftx',
806
			)
807
		);
808
		while ($row = $smcFunc['db_fetch_assoc']($request))
809
			$context['fulltext_index'][] = $row['indexname'];
810
	}
811
	else
812
	{
813
		$request = $smcFunc['db_query']('', '
814
			SHOW INDEX
815
			FROM {db_prefix}messages',
816
			array(
817
			)
818
		);
819
		$context['fulltext_index'] = array();
820 View Code Duplication
		if ($request !== false || $smcFunc['db_num_rows']($request) != 0)
821
		{
822
			while ($row = $smcFunc['db_fetch_assoc']($request))
823
			if ($row['Column_name'] == 'body' && (isset($row['Index_type']) && $row['Index_type'] == 'FULLTEXT' || isset($row['Comment']) && $row['Comment'] == 'FULLTEXT'))
824
				$context['fulltext_index'][] = $row['Key_name'];
825
			$smcFunc['db_free_result']($request);
826
827
			if (is_array($context['fulltext_index']))
828
				$context['fulltext_index'] = array_unique($context['fulltext_index']);
829
		}
830
831 View Code Duplication
		if (preg_match('~^`(.+?)`\.(.+?)$~', $db_prefix, $match) !== 0)
832
			$request = $smcFunc['db_query']('', '
833
			SHOW TABLE STATUS
834
			FROM {string:database_name}
835
			LIKE {string:table_name}',
836
			array(
837
				'database_name' => '`' . strtr($match[1], array('`' => '')) . '`',
838
				'table_name' => str_replace('_', '\_', $match[2]) . 'messages',
839
			)
840
			);
841
		else
842
			$request = $smcFunc['db_query']('', '
843
			SHOW TABLE STATUS
844
			LIKE {string:table_name}',
845
			array(
846
				'table_name' => str_replace('_', '\_', $db_prefix) . 'messages',
847
			)
848
			);
849
850
		if ($request !== false)
851
		{
852
			while ($row = $smcFunc['db_fetch_assoc']($request))
853
			if (isset($row['Engine']) && strtolower($row['Engine']) != 'myisam' && !(strtolower($row['Engine']) == 'innodb' && version_compare($smcFunc['db_get_version'], '5.6.4', '>=')))
854
				$context['cannot_create_fulltext'] = true;
855
			$smcFunc['db_free_result']($request);
856
		}
857
	}
858
}
859
860
?>