SearchParams::setTopicBoardLimit()   B
last analyzed

Complexity

Conditions 8
Paths 5

Size

Total Lines 57
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 12.096

Importance

Changes 0
Metric Value
cc 8
eloc 25
dl 0
loc 57
rs 8.4444
c 0
b 0
f 0
nc 5
nop 1
ccs 9
cts 15
cp 0.6
crap 12.096

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\QueryInterface;
20
use ElkArte\Exceptions\Exception;
21
use ElkArte\Helper\DataValidator;
22
use ElkArte\Helper\HttpReq;
23
use ElkArte\Helper\Util;
24
use ElkArte\Helper\ValuesContainer;
25
use ElkArte\User;
26
27
/**
28
 * Actually do the searches
29
 */
30
class SearchParams extends ValuesContainer
31
{
32
	/** @var string the db query for members */
33
	public $_userQuery = '';
34
35
	/** @var string The db query for brd's */
36
	public $_boardQuery = '';
37
38
	/** @var int Needed to calculate relevance */
39
	public $_minMsg = 0;
40
41
	/** @var int The minimum message id we will search, needed to calculate relevance */
42
	public $_minMsgID = 0;
43
44
	/** @var int The maximum message ID we will search, needed to calculate relevance */
45
	public $_maxMsgID = 0;
46
47
	/** @var int Message "age" via ID, given bounds, needed to calculate relevance */
48
	public $_recentMsg = 0;
49
50
	/** @var int[] */
51
	public $_memberlist = [];
52
53
	/** @var QueryInterface|null */
54
	protected $_db;
55
56
	/** @var HttpReq HttpReq instance */
57
	protected $_req;
58
59
	/**
60
	 * $_search_params will carry all settings that differ from the default search parameters.
61
	 * That way, the URLs involved in a search page will be kept as short as possible.
62
	 *
63
	 * @var mixed
64
	 */
65
	protected $_search_params = [];
66
67
	/**
68
	 * Constructor
69
	 *
70
	 * @param string $_search_string The string containing encoded search params
71
	 * @package Search
72
	 */
73
	public function __construct(protected $_search_string)
74
	{
75
		$this->_db = database();
76
		$this->prepare();
77
		$this->data = &$this->_search_params;
78
		$this->_req = HttpReq::instance();
79
	}
80
81
	/**
82
	 * Extract search params from a string
83
	 */
84
	protected function prepare()
85
	{
86
		// Due to IE's 2083 character limit, we have to compress long search strings
87
		$temp_params = base64_decode(str_replace(['-', '_', '.'], ['+', '/', '='], $this->_search_string));
88
89
		// Test for gzuncompress failing, our ErrorException will die on any E_WARNING with no
90
		// Exception, so turn it off/on for this check.
91
		set_error_handler(static function () { /* ignore errors */ });
92
		try
93
		{
94
			$check = gzuncompress($temp_params);
95
		}
96
		catch (\Exception)
97
		{
98
			$check = $temp_params;
99 2
		}
100
		finally
101 2
		{
102 2
			restore_error_handler();
103 2
		}
104 2
105 2
		$this->_search_params = json_decode($check, true);
106
	}
107
108
	/**
109
	 * Encodes search params ($this->_search_params) in an URL-compatible way
110 2
	 *
111
	 * @param array $search build param index with specific search term (did you mean?)
112
	 *
113 2
	 * @return string - the encoded string to be appended to the URL
114
	 */
115
	public function compileURL($search = [])
116 2
	{
117 2
		$temp_params = $this->_search_params;
118
119 2
		if (!empty($search))
120
		{
121 2
			$temp_params['search'] = implode(' ', $search);
122 2
		}
123
124
		// *** Encode all search params
125 2
		// All search params have been checked, let's compile them to a single string.
126
		$encoded = json_encode($temp_params);
127
128
		// Due to some potential browser/server limitations, attempt to compress
129 2
		// old IE's 2083 character limit, we have to compress long search
130
		set_error_handler(static function () { /* ignore errors */ });
131
		try
132
		{
133
			$compressed = gzcompress($encoded);
134
		}
135
		catch (\Exception)
136
		{
137
			$compressed = $encoded;
138 2
		}
139
		finally
140 2
		{
141 2
			restore_error_handler();
142
		}
143 2
144
		return str_replace(['+', '/', '='], ['-', '_', '.'], base64_encode($compressed));
145
	}
146
147
	/**
148
	 * Merge search params extracted with SearchParams::prepare
149
	 * with those present in the $param array (usually $_REQUEST['params'])
150 2
	 *
151
	 * @param array $params - An array of search parameters
152 2
	 * @param int $recentPercentage - A coefficient to calculate the lowest message id to start search from
153
	 * @param int $maxMembersToSearch - The maximum number of members to consider when multiple are found
154
	 */
155 2
	public function merge($params, $recentPercentage, $maxMembersToSearch)
156
	{
157 2
		global $modSettings;
158
159
		// Determine the search settings from the form or get params
160 2
		$this->cleanParams($params);
161
		$this->setAdvanced($params);
162
		$this->setSearchType($params);
163 2
		$this->setMinMaxAge($params);
164
		$this->setTopic($params);
165
		$this->setUser($params);
166 2
167
		// If there's no specific user, then don't mention it in the main query.
168
		if (empty($this->_search_params['userspec']))
169
		{
170
			$this->_userQuery = '';
171
		}
172 2
		else
173
		{
174
			$this->buildUserQuery($maxMembersToSearch);
175
		}
176
177
		// Ensure that boards are an array of integers (or nothing).
178
		$query_boards = $this->setBoards($params);
179 2
180
		// What boards are we searching in, all, selected, limited due to a topic?
181
		$this->_search_params['brd'] = $this->setTopicBoardLimit($query_boards);
182
		$this->_boardQuery = $this->setBoardQuery();
183
184
		$this->_search_params['show_complete'] = !empty($this->_search_params['show_complete']) || !empty($params['show_complete']);
185
		$this->_search_params['subject_only'] = !empty($this->_search_params['subject_only']) || !empty($params['subject_only']);
186
187
		// Get the sorting parameters right. Default to sort by relevance descending.
188
		$this->setSortAndDirection($params);
189
190
		// Determine some values needed to calculate the relevance.
191
		$this->_minMsg = (int) ceil((1 - $recentPercentage) * $modSettings['maxMsgID']);
192
		$this->_recentMsg = $modSettings['maxMsgID'] - $this->_minMsg;
193
194 2
		// *** Parse the search query
195
		call_integration_hook('integrate_search_params', [&$this->_search_params]);
196 2
197
		// What are we searching for?
198
		$this->_search_params['search'] = $this->setSearchTerm();
199 2
	}
200
201 2
	/**
202
	 * Cast the passed params to what we demand they be
203
	 *
204
	 * @param mixed $params
205 2
	 */
206
	public function cleanParams(&$params)
207
	{
208
		$validator = new DataValidator();
209
210
		// Convert dates to days between now and ...
211 2
		$params['minage'] = $this->daysBetween($params['minage'] ?? null, 0);
212
		$params['maxage'] = $this->daysBetween($params['maxage'] ?? null, 9999);
213
214
		$validator->sanitation_rules([
215
			'advanced' => 'intval',
216
			'searchtype' => 'intval',
217 2
			'minage' => 'intval',
218
			'maxage' => 'intval',
219
			'search_selection' => 'intval',
220
			'topic' => 'intval',
221
			'sd_topic' => 'intval',
222
			'userspec' => 'trim',
223 2
			'brd' => 'intval',
224
			'sort' => 'trim',
225
			'show_complete' => 'boolval',
226
			'sd_brd' => 'intval'
227
		]);
228 2
		$validator->input_processing([
229
			'brd' => 'array',
230
			'sd_brd' => 'array'
231
		]);
232
		$validator->validate($params);
233 2
234
		$params = array_replace((array) $params, $validator->validation_data());
235
	}
236
237
	/**
238
	 * Sets if using the advanced search mode
239
	 *
240
	 * @param mixed $params
241
	 */
242
	public function setAdvanced($params)
243
	{
244
		// Store whether simple search was used (needed if the user wants to do another query).
245
		if (!isset($this->_search_params['advanced']))
246
		{
247
			$this->_search_params['advanced'] = empty($params['advanced']) ? 0 : 1;
248
		}
249
	}
250
251
	/**
252
	 * Set the search type to all or any
253
	 *
254
	 * @param mixed $params
255
	 */
256
	public function setSearchType($params)
257
	{
258 2
		// 1 => 'allwords' (default, don't set as param) / 2 => 'anywords'.
259
		if (!empty($this->_search_params['searchtype']) || (!empty($params['searchtype']) && $params['searchtype'] === 2))
260
		{
261
			$this->_search_params['searchtype'] = 2;
262
		}
263
	}
264 2
265
	/**
266 2
	 * Sets the timeline to search in, if any, for messages
267
	 *
268
	 * @param mixed $params
269
	 */
270
	public function setMinMaxAge($params)
271
	{
272
		// Minimum age of messages. Default to zero (don't set param in that case).
273
		if (!empty($this->_search_params['minage']) || (!empty($params['minage']) && $params['minage'] > 0))
274
		{
275
			$this->_search_params['minage'] = empty($this->_search_params['minage']) ? $params['minage'] : $this->_search_params['minage'];
276
		}
277
278
		// Maximum age of messages. Default to infinite (9999 days: param not set).
279
		if (!empty($this->_search_params['maxage']) || (!empty($params['maxage']) && $params['maxage'] < 9999))
280
		{
281
			$this->_search_params['maxage'] = empty($this->_search_params['maxage']) ? $params['maxage'] : $this->_search_params['maxage'];
282
		}
283
284
		if (!empty($this->_search_params['minage']) || !empty($this->_search_params['maxage']))
285
		{
286
			$this->getMinMaxLimits();
287
		}
288
	}
289
290
	/**
291
	 * Determines and sets the min and max message ID based on timelines of min/max
292
	 */
293
	private function getMinMaxLimits()
294
	{
295
		global $modSettings, $context;
296
297
		$request = $this->_db->query('', '
0 ignored issues
show
Bug introduced by
The method query() 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

297
		/** @scrutinizer ignore-call */ 
298
  $request = $this->_db->query('', '

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...
298
			SELECT ' .
299
			(empty($this->_search_params['maxage']) ? '0, ' : 'COALESCE(MIN(id_msg), -1), ') . (empty($this->_search_params['minage']) ? '0' : 'COALESCE(MAX(id_msg), -1)') . '
300
			FROM {db_prefix}messages
301
			WHERE 1=1' . ($modSettings['postmod_active'] ? '
302
				AND approved = {int:is_approved_true}' : '') . (empty($this->_search_params['minage']) ? '' : '
303
				AND poster_time <= {int:timestamp_minimum_age}') . (empty($this->_search_params['maxage']) ? '' : '
304
				AND poster_time >= {int:timestamp_maximum_age}'),
305
			[
306
				'timestamp_minimum_age' => empty($this->_search_params['minage']) ? 0 : time() - 86400 * $this->_search_params['minage'],
307
				'timestamp_maximum_age' => empty($this->_search_params['maxage']) ? 0 : time() - 86400 * $this->_search_params['maxage'],
308
				'is_approved_true' => 1,
309
			]
310
		);
311
		[$this->_minMsgID, $this->_maxMsgID] = $request->fetch_row();
312
		$this->_minMsgID = (int) $this->_minMsgID;
313
		$this->_maxMsgID = (int) $this->_maxMsgID;
314
		if ($this->_minMsgID < 0 || $this->_maxMsgID < 0)
315
		{
316
			$context['search_errors']['no_messages_in_time_frame'] = true;
317
		}
318
	}
319
320
	/**
321
	 * Set the topic to search in, if any
322
	 *
323
	 * @param mixed $params
324
	 */
325
	public function setTopic($params)
326
	{
327
		// Searching a specific topic?
328
		if (!empty($params['topic']) || (!empty($params['search_selection']) && $params['search_selection'] === 'topic'))
329
		{
330
			$this->_search_params['topic'] = empty($params['search_selection']) ? (int) $params['topic'] : ($params['sd_topic'] ?? '');
331
			$this->_search_params['show_complete'] = true;
332
		}
333
	}
334
335
	/**
336
	 * Set the search by user value, if any
337
	 *
338
	 * @param mixed $params
339
	 */
340
	public function setUser($params)
341
	{
342
		// Default the user name to a wildcard matching every user (*).
343 2
		if (!empty($this->_search_params['userspec']) || (!empty($params['userspec']) && $params['userspec'] !== '*'))
344
		{
345
			$this->_search_params['userspec'] = $this->_search_params['userspec'] ?? $params['userspec'];
346
		}
347 2
	}
348
349
	/**
350
	 * So you want to search for items based on a specific user, or group of users or wildcard users?
351 2
	 *
352
	 * Will use real_name first and if nothing found, backup to member_name
353
	 *
354
	 * @param int $maxMembersToSearch
355 2
	 */
356
	public function buildUserQuery($maxMembersToSearch)
357
	{
358
		$userString = strtr(Util::htmlspecialchars($this->_search_params['userspec'], ENT_QUOTES), ['&quot;' => '"']);
359 2
		$userString = strtr($userString, ['%' => '\%', '_' => '\_', '*' => '%', '?' => '_']);
360
361
		preg_match_all('~"([^"]+)"~', $userString, $matches);
362
		$possible_users = array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $userString)));
363
		$possible_users = array_map('trim', $possible_users);
364
		$possible_users = array_filter($possible_users);
365 2
366
		// Create a list of database-escaped search names.
367
		$realNameMatches = [];
368
		foreach ($possible_users as $possible_user)
369 2
		{
370
			$realNameMatches[] = $this->_db->quote(
371
				'{string:possible_user}',
372
				[
373
					'possible_user' => $possible_user
374
				]
375
			);
376
		}
377
378
		// Retrieve a list of possible members.
379
		$request = $this->_db->query('', '
380
			SELECT
381
				id_member
382
			FROM {db_prefix}members
383
			WHERE {raw:match_possible_users}',
384
			[
385
				'match_possible_users' => 'real_name LIKE ' . implode(' OR real_name LIKE ', $realNameMatches),
386
			]
387
		);
388
389
		// Simply do nothing if there're too many members matching the criteria.
390
		if ($request->num_rows() > $maxMembersToSearch)
391
		{
392
			$this->_userQuery = '';
393
		}
394
		// Nothing? lets try the poster name instead since that is what they go by
395
		elseif ($request->num_rows() === 0)
396 2
		{
397
			$this->_userQuery = $this->_db->quote(
398
				'm.id_member = {int:id_member_guest} AND ({raw:match_possible_guest_names})',
399
				[
400
					'id_member_guest' => 0,
401
					'match_possible_guest_names' => 'm.poster_name LIKE ' . implode(' OR m.poster_name LIKE ', $realNameMatches),
402 2
				]
403 2
			);
404
		}
405
		// We have some users!
406 2
		else
407
		{
408
			while (($row = $request->fetch_assoc()))
409
			{
410
				$this->_memberlist[] = $row['id_member'];
411
			}
412 2
413
			$this->_userQuery = $this->_db->quote(
414 2
				'(m.id_member IN ({array_int:matched_members}) OR (m.id_member = {int:id_member_guest} AND ({raw:match_possible_guest_names})))',
415
				[
416 2
					'matched_members' => $this->_memberlist,
417
					'id_member_guest' => 0,
418
					'match_possible_guest_names' => 'm.poster_name LIKE ' . implode(' OR m.poster_name LIKE ', $realNameMatches),
419
				]
420 2
			);
421 2
		}
422
423 2
		$request->free_result();
424
	}
425 2
426
	/**
427
	 * Figures what boards we have been requested to search in.  Does not check
428
	 * permissions at this point.
429
	 *
430
	 * @param $params
431
	 * @return int[]
432
	 */
433 2
	public function setBoards($params)
434
	{
435
		if (!empty($this->_search_params['brd']) && is_array($this->_search_params['brd']))
436
		{
437
			return $this->_search_params['brd'];
438
		}
439
440
		if (!empty($params['brd']) && is_array($params['brd']))
441 2
		{
442 2
			return $params['brd'];
443
		}
444
445
		if (!empty($params['search_selection']) && $params['search_selection'] === 'board' && !empty($params['sd_brd']) && is_array($params['sd_brd']))
446 2
		{
447
			return $params['sd_brd'];
448
		}
449
450
		return [];
451
	}
452 2
453
	/**
454 2
	 * Determines what boards you can or should be searching
455
	 *
456
	 * @param $query_boards
457
	 * @return int[] array of boards to search in
458
	 * @throws Exception topic_gone
459 2
	 */
460
	public function setTopicBoardLimit($query_boards)
461 2
	{
462
		global $modSettings, $context;
463
464
		// Searching by topic means the board is set as well.
465
		if (!empty($this->_search_params['topic']))
466
		{
467 2
			$request = $this->_db->query('', '
468
				SELECT
469
					b.id_board
470 2
				FROM {db_prefix}topics AS t
471 2
					INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
472
				WHERE t.id_topic = {int:search_topic_id}
473
					AND {query_see_board}' . ($modSettings['postmod_active'] ? '
474 2
					AND t.approved = {int:is_approved_true}' : '') . '
475
				LIMIT 1',
476
				[
477 2
					'search_topic_id' => $this->_search_params['topic'],
478
					'is_approved_true' => 1,
479 2
				]
480
			);
481 2
			if ($request->num_rows() === 0)
482
			{
483
				throw new Exception('topic_gone', false);
484
			}
485
486
			$this->_search_params['brd'] = [];
487
			$brd = (int) $request->fetch_row()[0];
488
489
			$request->free_result();
490
491
			return [$brd];
492 2
		}
493
494
		// Select all boards you've selected AND are allowed to see.
495
		if (User::$info->is_admin && (!empty($this->_search_params['advanced']) || !empty($query_boards)))
0 ignored issues
show
Bug Best Practice introduced by
The property is_admin does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
496
		{
497
			return $query_boards;
498
		}
499
500
		require_once(SUBSDIR . '/Boards.subs.php');
501
		$brd = array_keys(fetchBoardsInfo([
502
			'boards' => $query_boards], [
503
				'include_recycle' => false,
504
				'include_redirects' => false,
505
				'wanna_see_board' => empty($this->_search_params['advanced'])
506
			]
507
		));
508
509
		// This error should pro'bly only happen for hackers.
510
		if (empty($brd))
511
		{
512
			$context['search_errors']['no_boards_selected'] = true;
513
			$brd = [];
514
		}
515
516
		return $brd;
517
	}
518
519
	/**
520
	 * Builds the query for the boards we are searching in.
521
	 *
522
	 * @return string
523
	 */
524
	public function setBoardQuery()
525
	{
526
		if (count($this->_search_params['brd']) !== 0)
527
		{
528
			array_map('intval', $this->_search_params['brd']);
529
530
			// If we've selected all boards, this parameter can be left empty.
531
			require_once(SUBSDIR . '/Boards.subs.php');
532
			$num_boards = countBoards();
533
534
			if (count($this->_search_params['brd']) === $num_boards)
535
			{
536
				return $this->_boardQuery = '';
537
			}
538
539
			if (count($this->_search_params['brd']) === $num_boards - 1
540
				&& !empty($modSettings['recycle_board'])
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $modSettings seems to never exist and therefore empty should always be true.
Loading history...
541
				&& !in_array((int) $modSettings['recycle_board'], $this->_search_params['brd'], true))
542
			{
543
				return $this->_boardQuery = '!= ' . $modSettings['recycle_board'];
544
			}
545
546
			return $this->_boardQuery = 'IN (' . implode(', ', $this->_search_params['brd']) . ')';
547
		}
548
549
		return '';
550
	}
551
552
	/**
553
	 * Sets the sort column and direction
554
	 *
555
	 * @event integrate_search_sort_columns
556
	 * @param mixed $params
557
	 */
558
	public function setSortAndDirection($params)
559
	{
560
		$sort_columns = ['relevance', 'num_replies', 'id_msg',];
561
562
		// Allow integration to add additional sort columns
563
		call_integration_hook('integrate_search_sort_columns', [&$sort_columns]);
564
565
		if (empty($this->_search_params['sort']) && !empty($params['sort']))
566
		{
567
			[$this->_search_params['sort'], $this->_search_params['sort_dir']] = array_pad(explode('|', $params['sort']), 2, '');
568
		}
569
570
		$this->_search_params['sort'] = !empty($this->_search_params['sort']) && in_array($this->_search_params['sort'], $sort_columns, true) ? $this->_search_params['sort'] : 'relevance';
571
572
		if (!empty($this->_search_params['topic']) && $this->_search_params['sort'] === 'num_replies')
573
		{
574
			$this->_search_params['sort'] = 'id_msg';
575
		}
576
577
		// Sorting direction: descending unless stated otherwise.
578
		$this->_search_params['sort_dir'] = !empty($this->_search_params['sort_dir']) && $this->_search_params['sort_dir'] === 'asc' ? 'asc' : 'desc';
579
	}
580
581
	/**
582
	 * Set the search term from wherever we can find it!
583
	 *
584
	 * @return string
585
	 */
586
	public function setSearchTerm()
587
	{
588
		if (!empty($this->_search_params['search']))
589
		{
590
			return $this->_search_params['search'];
591
		}
592
593
		return $this->_req->getRequest('search', 'un_htmlspecialchars', '');
594
	}
595
596
	/**
597
	 * Return the current set of search details
598
	 *
599
	 * @return string[]
600
	 */
601
	public function get()
602
	{
603
		return $this->_search_params;
604
	}
605
606
	/**
607
	 * Number of days between today/now and some date.
608
	 *
609
	 * @param string $date
610
	 * @param int $default
611
	 * @return int
612
	 */
613
	private function daysBetween($date, $default)
614
	{
615
		// Already a number, validate
616
		if (is_numeric($date))
617
		{
618
			return (max(min(0, $date), 9999));
619
		}
620
621
		// Nothing, then full range
622
		if (empty($date))
623
		{
624
			return $default;
625
		}
626
627
		$startTimeStamp = time();
628
		$endTimeStamp = strtotime($date);
629
		$timeDiff = $startTimeStamp - ($endTimeStamp !== false ? $endTimeStamp : $startTimeStamp);
630
631
		// Can't search into the future
632
		if ($timeDiff < 1)
633
		{
634
			$timeDiff = 0;
635
		}
636
637
		return $timeDiff / 86400;
638
	}
639
}
640