Search::isCompact()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 1
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Utility class for search functionality.
5
 *
6
 * @package   ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
9
 *
10
 * This file contains code covered by:
11
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
12
 *
13
 * @version 2.0 dev
14
 *
15
 */
16
17
namespace ElkArte\Search;
18
19
use ElkArte\Database\AbstractResult;
20
use ElkArte\Database\QueryInterface;
21
use ElkArte\Search\API\Standard;
22
23
/**
24
 * Actually do the searches
25
 */
26
class Search
27
{
28
	/** @const the forum version but is repeated due to some people rewriting FORUM_VERSION. */
29
	public const FORUM_VERSION = 'ElkArte 2.0 dev';
30
31
	/** @var array */
32
	protected $_participants = [];
33
34
	/** @var SearchParams */
35
	protected $_searchParams;
36
37
	/** @var SearchArray Holds the words and phrases to be searched on */
38
	private $_searchArray;
39
40
	/** @var null|object Holds instance of the search api in use such as \ElkArte\Search\API\Standard_Search */
41
	private $_searchAPI;
42
43
	/** @var QueryInterface Database instance */
44
	private $_db;
45
46
	/** @var array Builds the array of words for use in the db query */
47
	private $_searchWords = [];
48
49
	/** @var array Words excluded from indexes */
50
	private $_excludedIndexWords = [];
51
52
	/** @var array Words not to be found in the subject (-word) */
53
	private $_excludedSubjectWords = [];
54
55
	/** @var array Phrases not to be found in the search results (-"some phrase") */
56
	private $_excludedPhrases = [];
57
58
	/** @var WeightFactors The weights to associate to various areas for relevancy */
59
	private $_weightFactors = [];
60
61
	/** @var bool If we are creating a tmp db table */
62
	private $_createTemporary;
63
64
	/** @var array common words that we will not index or search for */
65
	private $_blocklist_words = [];
66
67
	/**
68
	 * Constructor
69
	 * Easy enough, initialize the database objects (generic db and search db)
70
	 *
71
	 * @package Search
72
	 */
73
	public function __construct()
74
	{
75
		$this->_db = database();
76
		$db_search = db_search();
77
78
		// Create new temporary table(s) (if we can) to store preliminary results in.
79
		$db_search->skip_next_error();
80
81
		$this->_createTemporary = $db_search->createTemporaryTable(
82
				'{db_prefix}tmp_log_search_messages',
83
				[
84
					[
85
						'name' => 'id_msg',
86
						'type' => 'int',
87
						'size' => 10,
88
						'unsigned' => true,
89
						'default' => 0,
90
					]
91
				],
92
				[
93
					[
94
						'name' => 'id_msg',
95
						'columns' => ['id_msg'],
96
						'type' => 'primary'
97
					]
98
				]
99
			) !== false;
100
101
		// Skip the error as it is not uncommon for temp tables to be denied
102
		$db_search->skip_next_error();
103
		$db_search->createTemporaryTable('{db_prefix}tmp_log_search_topics',
104
			[
105
				[
106
					'name' => 'id_topic',
107
					'type' => 'mediumint',
108
					'unsigned' => true,
109
					'size' => 8,
110
					'default' => 0
111
				]
112
			],
113
			[
114
				[
115
					'name' => 'id_topic',
116
					'columns' => ['id_topic'],
117
					'type' => 'primary'
118 2
				]
119
			]
120 2
		);
121 2
	}
122 2
123
	/**
124 2
	 * Returns a search parameter.
125
	 *
126 2
	 * @param string $name - name of the search parameters
127 2
	 *
128
	 * @return bool|mixed - the value of the parameter
129
	 */
130 2
	public function param($name)
131
	{
132
		return $this->_searchParams[$name] ?? false;
133
	}
134
135
	/**
136
	 * Sets $this->_searchParams with all the search parameters.
137
	 */
138
	public function getParams()
139 2
	{
140
		$this->_searchParams->mergeWith([
141
			'min_msg_id' => $this->_searchParams->_minMsgID,
142
			'max_msg_id' => $this->_searchParams->_maxMsgID,
143
			'memberlist' => $this->_searchParams->_memberlist,
144 2
		]);
145
	}
146 2
147 2
	/**
148
	 * Returns the ignored words
149
	 */
150 2
	public function getIgnored()
151
	{
152
		return $this->_searchArray->getIgnored();
153
	}
154
155
	/**
156
	 * Set the weight factors
157
	 *
158
	 * @param WeightFactors $weight
159 2
	 */
160
	public function setWeights($weight)
161
	{
162
		$this->_weightFactors = $weight;
163
	}
164
165 2
	/**
166
	 * Set disallowed words etc.
167
	 *
168
	 * @param SearchParams $paramObject
169
	 * @param false $search_simple_fulltext
170
	 */
171
	public function setParams($paramObject, $search_simple_fulltext = false)
172
	{
173
		$this->_searchParams = $paramObject;
174
		$this->setBlockListedWords();
175
		$this->_searchArray = new SearchArray($this->_searchParams->search, $this->_blocklist_words, $search_simple_fulltext);
0 ignored issues
show
Bug Best Practice introduced by
The property search does not exist on ElkArte\Search\SearchParams. Since you implemented __get, consider adding a @property annotation.
Loading history...
176
	}
177
178
	/**
179
	 * If any block-listed word has been found
180
	 *
181
	 * @return bool
182
	 */
183
	public function foundBlockListedWords()
184
	{
185
		return $this->_searchArray->foundBlockListedWords();
186
	}
187
188
	/**
189
	 * Returns the block-listed word array
190
	 *
191
	 * @return array
192
	 */
193
	public function getBlockListedWords()
194
	{
195
		if (empty($this->_blocklist_words))
196
		{
197
			$this->setBlockListedWords();
198
		}
199
200
		return $this->_blocklist_words;
201
	}
202
203
	/**
204
	 * Sets the block-listed word array
205
	 */
206
	public function setBlockListedWords()
207
	{
208
		// Unfortunately, searching for words like these is going to result in to many hits,
209
		// so we're blocking them.
210
		$blocklist_words = ['img', 'url', 'quote', 'www', 'http', 'the', 'is', 'it', 'are', 'if', 'in'];
211
		call_integration_hook('integrate_search_blocklist_words', [&$blocklist_words]);
212
213 2
		$this->_blocklist_words = $blocklist_words;
214
	}
215 2
216 2
	/**
217
	 * Get the search array from the SearchArray object.
218 2
	 *
219
	 * @return array The search array.
220 2
	 */
221
	public function getSearchArray()
222
	{
223
		return $this->_searchArray->getSearchArray();
224
	}
225 2
226 2
	/**
227
	 * Get the list of excluded words.
228 2
	 *
229 2
	 * @return array
230
	 */
231
	public function getExcludedWords()
232
	{
233
		return $this->_searchArray->getExcludedWords();
234
	}
235
236
	/**
237
	 * Get the excluded subject words.
238
	 *
239
	 * @return array The excluded subject words.
240
	 */
241
	public function getExcludedSubjectWords()
242
	{
243
		return $this->_excludedSubjectWords;
244
	}
245
246
	/**
247
	 * Returns the search parameters.
248
	 *
249
	 * @param bool $array If true returns an array, otherwise an object
250
	 *
251
	 * @return SearchParams|string[]
252
	 */
253
	public function getSearchParams($array = false)
254
	{
255
		if ($array)
256
		{
257
			return $this->_searchParams->get();
258
		}
259
260
		return $this->_searchParams;
261
	}
262
263 2
	/**
264
	 * Get the excluded phrases.
265 2
	 *
266
	 * @return array The excluded phrases.
267
	 */
268
	public function getExcludedPhrases()
269
	{
270
		return $this->_excludedPhrases;
271 2
	}
272
273
	/**
274
	 * Tell me, do I want to see the full message or just a piece?
275
	 */
276
	public function isCompact()
277
	{
278
		return empty($this->_searchParams['show_complete']);
279
	}
280
281
	/**
282
	 * Wrapper around SearchParams::compileURL
283
	 *
284
	 * @param array $search build param index with specific search term (did you mean?)
285
	 *
286
	 * @return string - the encoded string to be appended to the URL
287
	 */
288
	public function compileURLparams($search = [])
289
	{
290
		return $this->_searchParams->compileURL($search);
291
	}
292
293
	/**
294
	 * Finds the posters of the messages
295 2
	 *
296
	 * @param int[] $msg_list - All the messages we want to find the posters
297 2
	 * @param int $limit - There are only so many topics
298
	 *
299
	 * @return int[] - array of members id
300
	 */
301
	public function loadPosters($msg_list, $limit)
302
	{
303
		// Load the posters...
304
		$posters = [];
305
		$this->_db->fetchQuery('
306
			SELECT
307
				id_member
308
			FROM {db_prefix}messages
309
			WHERE id_member != {int:no_member}
310
				AND id_msg IN ({array_int:message_list})
311
			LIMIT {int:limit}',
312
			[
313
				'message_list' => $msg_list,
314
				'limit' => $limit,
315
				'no_member' => 0,
316
			]
317
		)->fetch_callback(
318
			static function ($row) use (&$posters) {
319
				$posters[] = (int) $row['id_member'];
320
			}
321
		);
322
323
		return $posters;
324
	}
325
326
	/**
327
	 * Finds the posters of the messages
328
	 *
329
	 * @param int[] $msg_list - All the messages we want to find the posters
330
	 * @param int $limit - There are only so many topics
331
	 *
332
	 * @return bool|AbstractResult
333
	 */
334
	public function loadMessagesRequest($msg_list, $limit)
335
	{
336
		global $modSettings;
337
338
		return $this->_db->query('', '
339
			SELECT
340
				m.id_msg, m.subject, m.poster_name, m.poster_email, m.poster_time, m.id_member, m.icon, m.poster_ip,
341
				m.body, m.smileys_enabled, m.modified_time, m.modified_name, first_m.id_msg AS id_first_msg,
342
				first_m.subject AS first_subject, first_m.icon AS first_icon, first_m.poster_time AS first_poster_time,
343
				first_mem.id_member AS first_id_member,
344
				COALESCE(first_mem.real_name, first_m.poster_name) AS first_display_name,
345
				COALESCE(first_mem.member_name, first_m.poster_name) AS first_member_name,
346
				last_m.id_msg AS id_last_msg, last_m.poster_time AS last_poster_time, last_mem.id_member AS last_id_member,
347
				COALESCE(last_mem.real_name, last_m.poster_name) AS last_display_name,
348
				COALESCE(last_mem.member_name, last_m.poster_name) AS last_member_name,
349
				last_m.icon AS last_icon, last_m.subject AS last_subject,
350
				t.id_topic, t.is_sticky, t.locked, t.id_poll, t.num_replies, t.num_views, t.num_likes,
351
				b.id_board, b.name AS bname, c.id_cat, c.name AS cat_name
352
			FROM {db_prefix}messages AS m
353
				INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic)
354
				INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
355
				INNER JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat)
356
				INNER JOIN {db_prefix}messages AS first_m ON (first_m.id_msg = t.id_first_msg)
357
				INNER JOIN {db_prefix}messages AS last_m ON (last_m.id_msg = t.id_last_msg)
358
				LEFT JOIN {db_prefix}members AS first_mem ON (first_mem.id_member = first_m.id_member)
359
				LEFT JOIN {db_prefix}members AS last_mem ON (last_mem.id_member = first_m.id_member)
360
			WHERE m.id_msg IN ({array_int:message_list})' . ($modSettings['postmod_active'] ? '
361
				AND m.approved = {int:is_approved}' : '') . '
362
			ORDER BY FIND_IN_SET(m.id_msg, {string:message_list_in_set})
363
			LIMIT {int:limit}',
364
			[
365
				'message_list' => $msg_list,
366
				'is_approved' => 1,
367
				'message_list_in_set' => implode(',', $msg_list),
368
				'limit' => $limit,
369
			]
370
		);
371
	}
372
373
	/**
374
	 * Did the user find any message at all?
375
	 *
376
	 * @param AbstractResult $messages_request holds a query result
377
	 *
378
	 * @return bool
379
	 */
380
	public function noMessages($messages_request)
381
	{
382
		return $messages_request->num_rows() === 0;
383
	}
384
385
	/**
386
	 * Sets the query, calls the searchQuery method of the API in use
387
	 *
388
	 * @param Standard $searchAPI
389
	 * @return array
390
	 */
391
	public function searchQuery($searchAPI)
392
	{
393
		$this->_searchAPI = $searchAPI;
394
		$searchAPI->setExcludedPhrases($this->_excludedPhrases);
395
		$searchAPI->setWeightFactors($this->_weightFactors);
396
		$searchAPI->useTemporary($this->_createTemporary);
397
		$searchAPI->setSearchArray($this->_searchArray);
398
		if ($searchAPI->supportsExtended())
399
		{
400
			return $searchAPI->searchQuery($this->_searchArray->getSearchArray(), $this->_excludedIndexWords, $this->_participants);
401
		}
402
403 2
		return $searchAPI->searchQuery($this->searchWords(), $this->_excludedIndexWords, $this->_participants);
404
	}
405 2
406 2
	/**
407 2
	 * Builds the array of words for the query
408 2
	 */
409 2
	public function searchWords()
410
	{
411 2
		global $modSettings, $context;
412 2
413 2
		if (count($this->_searchWords) > 0)
414 2
		{
415 2
			return $this->_searchWords;
416
		}
417
418
		$orParts = [];
419
		$this->_searchWords = [];
420
		$searchArray = $this->_searchArray->getSearchArray();
421
		$excludedWords = $this->_searchArray->getExcludedWords();
422 2
423
		// All words/sentences must match.
424 2
		if (!empty($searchArray) && empty($this->_searchParams['searchtype']))
425
		{
426 2
			$orParts[0] = $searchArray;
427
		}
428
		// Any word/sentence must match.
429
		else
430
		{
431 2
			foreach ($searchArray as $index => $value)
432 2
			{
433 2
				$orParts[$index] = [$value];
434 2
			}
435
		}
436
437 2
		// Make sure the excluded words are in all or-branches.
438
		foreach (array_keys($orParts) as $orIndex)
439 2
		{
440
			foreach ($excludedWords as $word)
441
			{
442
				$orParts[$orIndex][] = $word;
443
			}
444
		}
445
446
		// Determine the or-branches and the fulltext search words.
447
		foreach (array_keys($orParts) as $orIndex)
448
		{
449
			$this->_searchWords[$orIndex] = [
450
				'indexed_words' => [],
451 2
				'words' => [],
452
				'subject_words' => [],
453 2
				'all_words' => [],
454
				'complex_words' => [],
455 1
			];
456
457
			$this->_searchAPI->setExcludedWords($excludedWords);
0 ignored issues
show
Bug introduced by
The method setExcludedWords() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

457
			$this->_searchAPI->/** @scrutinizer ignore-call */ 
458
                      setExcludedWords($excludedWords);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
458
459
			// Sort the indexed words (large words -> small words -> excluded words).
460 2
			usort($orParts[$orIndex], [$this->_searchAPI, 'searchSort']);
461
462 2
			foreach ($orParts[$orIndex] as $word)
463
			{
464
				$is_excluded = in_array($word, $excludedWords, true);
465
				$this->_searchWords[$orIndex]['all_words'][] = $word;
466
				$subjectWords = text2words($word);
467
468
				if (!$is_excluded || count($subjectWords) === 1)
469
				{
470 2
					$this->_searchWords[$orIndex]['subject_words'] = array_merge($this->_searchWords[$orIndex]['subject_words'], $subjectWords);
471
472 2
					if ($is_excluded)
473
					{
474 2
						$this->_excludedSubjectWords = array_merge($this->_excludedSubjectWords, $subjectWords);
475
					}
476 2
				}
477 2
				else
478 2
				{
479
					$this->_excludedPhrases[] = $word;
480 2
				}
481
482 2
				// Have we got indexes to prepare?
483
				$this->_searchAPI->prepareIndexes($word, $this->_searchWords[$orIndex], $this->_excludedIndexWords, $is_excluded, $this->_excludedSubjectWords);
484 2
			}
485
486 2
			// Search_force_index requires all AND parts to have at least one fulltext word.
487
			if (!empty($modSettings['search_force_index']) && empty($this->_searchWords[$orIndex]['indexed_words']))
488
			{
489
				$context['search_errors']['query_not_specific_enough'] = true;
490
				break;
491
			}
492
493
			if ($this->_searchParams->subject_only && empty($this->_searchWords[$orIndex]['subject_words']) && empty($this->_excludedSubjectWords))
0 ignored issues
show
Bug Best Practice introduced by
The property subject_only does not exist on ElkArte\Search\SearchParams. Since you implemented __get, consider adding a @property annotation.
Loading history...
494
			{
495 2
				$context['search_errors']['query_not_specific_enough'] = true;
496
				break;
497
			}
498
499 2
			// Make sure we aren't searching for too many indexed words.
500
			$this->_searchWords[$orIndex]['indexed_words'] = array_slice($this->_searchWords[$orIndex]['indexed_words'], 0, 7);
501
			$this->_searchWords[$orIndex]['subject_words'] = array_slice($this->_searchWords[$orIndex]['subject_words'], 0, 7);
502
			$this->_searchWords[$orIndex]['words'] = array_slice($this->_searchWords[$orIndex]['words'], 0, 4);
503
		}
504 2
505
		return $this->_searchWords;
506
	}
507
508
	/**
509
	 * Returns the number of results obtained from the query.
510
	 *
511
	 * @return int
512 2
	 */
513 2
	public function getNumResults()
514 2
	{
515
		return $this->_searchAPI->getNumResults();
516
	}
517
518 2
	/**
519
	 * Get the participants of the event.
520
	 *
521
	 * @return array The participants of the event.
522
	 */
523
	public function getParticipants()
524
	{
525
		return $this->_participants;
526
	}
527
}
528