diff --git a/composer.json b/composer.json
index ea2be30ac8..80a3513eb7 100644
--- a/composer.json
+++ b/composer.json
@@ -29,7 +29,8 @@
"require-dev": {
"phpunit/phpunit-selenium": "~4.1",
"phpunit/dbunit": "~3.0",
- "phpunit/phpunit": "6.4.4"
+ "phpunit/phpunit": "6.4.4",
+ "myclabs/deep-copy": "1.8.0"
},
"autoload": {
"files": ["sources/Autoloader.class.php"],
diff --git a/sources/admin/ManageSearch.controller.php b/sources/admin/ManageSearch.controller.php
index 3f931dd172..7aa4e88adb 100644
--- a/sources/admin/ManageSearch.controller.php
+++ b/sources/admin/ManageSearch.controller.php
@@ -176,7 +176,7 @@ public function action_searchSettings_display()
*/
private function _settings()
{
- global $txt;
+ global $txt, $modSettings;
// What are we editing anyway?
$config_vars = array(
@@ -194,13 +194,8 @@ private function _settings()
);
// Perhaps the search method wants to add some settings?
- $search = new \ElkArte\Search\Search();
- $searchAPI = $search->findSearchAPI();
-
- if (is_callable(array($searchAPI, 'searchSettings')))
- {
- call_user_func_array($searchAPI->searchSettings);
- }
+ $searchAPI = new \ElkArte\Search\SearchApiWrapper(!empty($modSettings['search_index']) ? $modSettings['search_index'] : '');
+ $searchAPI->searchSettings($config_vars);
// Add new settings with a nice hook, makes them available for admin settings search as well
call_integration_hook('integrate_modify_search_settings', array(&$config_vars));
diff --git a/sources/controllers/Display.controller.php b/sources/controllers/Display.controller.php
index 50b07d9da2..556b6967ca 100644
--- a/sources/controllers/Display.controller.php
+++ b/sources/controllers/Display.controller.php
@@ -521,14 +521,10 @@ public function action_display()
'board_name' => htmlspecialchars(strtr(strip_tags($board_info['name']), array('&' => '&')), ENT_COMPAT, 'UTF-8'),
'child_level' => $board_info['child_level'],
);
-
- // Set the callback. (do you REALIZE how much memory all the messages would take?!?)
- // This will be called from the template.
- $context['get_message'] = array($this, 'prepareDisplayContext_callback');
- $this->_icon_sources = new MessageTopicIcons(!empty($modSettings['messageIconChecks_enable']), $settings['theme_dir']);
list ($sig_limits) = explode(':', $modSettings['signature_settings']);
$signature_settings = explode(',', $sig_limits);
+ $this->_icon_sources = new MessageTopicIcons(!empty($modSettings['messageIconChecks_enable']), $settings['theme_dir']);
if ($user_info['is_guest'])
{
$this->_show_signatures = !empty($signature_settings[8]) ? (int) $signature_settings[8] : 0;
@@ -538,6 +534,19 @@ public function action_display()
$this->_show_signatures = !empty($signature_settings[9]) ? (int) $signature_settings[9] : 0;
}
+ Elk_Autoloader::instance()->register(SUBSDIR . '/MessagesCallback', '\\ElkArte\\sources\\subs\\MessagesCallback');
+
+ // Set the callback. (do you REALIZE how much memory all the messages would take?!?)
+ // This will be called from the template.
+ $bodyParser = new \ElkArte\sources\subs\MessagesCallback\BodyParser\Normal(array(), false);
+ $opt = new \ElkArte\ValuesContainer([
+ 'icon_sources' => $this->_icon_sources,
+ 'show_signatures' => $this->_show_signatures,
+ ]);
+ $renderer = new \ElkArte\sources\subs\MessagesCallback\DisplayRenderer($messages_request, $bodyParser, $opt);
+
+ $context['get_message'] = array($renderer, 'getContext');
+
// Now set all the wonderful, wonderful permissions... like moderation ones...
$common_permissions = array(
'can_approve' => 'approve_posts',
@@ -758,161 +767,4 @@ public function action_quickmod2()
redirectexit(!empty($topicGone) ? 'board=' . $board : 'topic=' . $topic . '.' . (int) $this->_req->query->start);
}
-
- /**
- * Callback for the message display.
- * It actually gets and prepares the message context.
- * This method will start over from the beginning if reset is set to true, which is
- * useful for showing an index before or after the posts.
- *
- * @param bool $reset default false.
- *
- * @return array
- */
- public function prepareDisplayContext_callback($reset = false)
- {
- global $settings, $txt, $modSettings, $scripturl, $user_info;
- global $memberContext, $context, $messages_request, $topic;
- static $counter = null;
- static $signature_shown = null;
-
- // If the query returned false, bail.
- if ($messages_request === false)
- return false;
-
- // Remember which message this is. (ie. reply #83)
- if ($counter === null || $reset)
- $counter = $context['start'];
-
- // Start from the beginning...
- if ($reset)
- return currentContext($messages_request, $reset);
-
- // Attempt to get the next message.
- $message = currentContext($messages_request);
- if (!$message)
- return false;
-
- // If you're a lazy bum, you probably didn't give a subject...
- $message['subject'] = $message['subject'] != '' ? $message['subject'] : $txt['no_subject'];
-
- // Are you allowed to remove at least a single reply?
- $context['can_remove_post'] |= allowedTo('delete_own') && (empty($modSettings['edit_disable_time']) || $message['poster_time'] + $modSettings['edit_disable_time'] * 60 >= time()) && $message['id_member'] == $user_info['id'];
-
- // Have you liked this post, can you?
- $message['you_liked'] = !empty($context['likes'][$message['id_msg']]['member'])
- && isset($context['likes'][$message['id_msg']]['member'][$user_info['id']]);
- $message['use_likes'] = allowedTo('like_posts') && empty($context['is_locked'])
- && ($message['id_member'] != $user_info['id'] || !empty($modSettings['likeAllowSelf']))
- && (empty($modSettings['likeMinPosts']) ? true : $modSettings['likeMinPosts'] <= $user_info['posts']);
- $message['like_count'] = !empty($context['likes'][$message['id_msg']]['count']) ? $context['likes'][$message['id_msg']]['count'] : 0;
-
- // If it couldn't load, or the user was a guest.... someday may be done with a guest table.
- if (!loadMemberContext($message['id_member'], true))
- {
- // Notice this information isn't used anywhere else....
- $memberContext[$message['id_member']]['name'] = $message['poster_name'];
- $memberContext[$message['id_member']]['id'] = 0;
- $memberContext[$message['id_member']]['group'] = $txt['guest_title'];
- $memberContext[$message['id_member']]['link'] = $message['poster_name'];
- $memberContext[$message['id_member']]['email'] = $message['poster_email'];
- $memberContext[$message['id_member']]['show_email'] = showEmailAddress(true, 0);
- $memberContext[$message['id_member']]['is_guest'] = true;
- }
- else
- {
- $memberContext[$message['id_member']]['can_view_profile'] = allowedTo('profile_view_any') || ($message['id_member'] == $user_info['id'] && allowedTo('profile_view_own'));
- $memberContext[$message['id_member']]['is_topic_starter'] = $message['id_member'] == $context['topic_starter_id'];
- $memberContext[$message['id_member']]['can_see_warning'] = !isset($context['disabled_fields']['warning_status']) && $memberContext[$message['id_member']]['warning_status'] && ($context['user']['can_mod'] || (!$user_info['is_guest'] && !empty($modSettings['warning_show']) && ($modSettings['warning_show'] > 1 || $message['id_member'] == $user_info['id'])));
-
- if ($this->_show_signatures === 1)
- {
- if (empty($signature_shown[$message['id_member']]))
- {
- $signature_shown[$message['id_member']] = true;
- }
- else
- {
- $memberContext[$message['id_member']]['signature'] = '';
- }
- }
- elseif ($this->_show_signatures === 2)
- {
- $memberContext[$message['id_member']]['signature'] = '';
- }
- }
-
- $memberContext[$message['id_member']]['ip'] = $message['poster_ip'];
- $memberContext[$message['id_member']]['show_profile_buttons'] = $settings['show_profile_buttons'] && (!empty($memberContext[$message['id_member']]['can_view_profile']) || (!empty($memberContext[$message['id_member']]['website']['url']) && !isset($context['disabled_fields']['website'])) || (in_array($memberContext[$message['id_member']]['show_email'], array('yes', 'yes_permission_override', 'no_through_forum'))) || $context['can_send_pm']);
-
- // Do the censor thang.
- $message['body'] = censor($message['body']);
- $message['subject'] = censor($message['subject']);
-
- // Run BBC interpreter on the message.
- $context['id_msg'] = $message['id_msg'];
- $bbc_wrapper = \BBC\ParserWrapper::instance();
- $message['body'] = $bbc_wrapper->parseMessage($message['body'], $message['smileys_enabled']);
-
- call_integration_hook('integrate_before_prepare_display_context', array(&$message));
-
- // Compose the memory eat- I mean message array.
- require_once(SUBSDIR . '/Attachments.subs.php');
- $output = array(
- 'attachment' => loadAttachmentContext($message['id_msg']),
- 'alternate' => $counter % 2,
- 'id' => $message['id_msg'],
- 'href' => $scripturl . '?topic=' . $topic . '.msg' . $message['id_msg'] . '#msg' . $message['id_msg'],
- 'link' => '' . $message['subject'] . '',
- 'member' => &$memberContext[$message['id_member']],
- 'icon' => $message['icon'],
- 'icon_url' => $this->_icon_sources->{$message['icon']},
- 'subject' => $message['subject'],
- 'time' => standardTime($message['poster_time']),
- 'html_time' => htmlTime($message['poster_time']),
- 'timestamp' => forum_time(true, $message['poster_time']),
- 'counter' => $counter,
- 'modified' => array(
- 'time' => standardTime($message['modified_time']),
- 'html_time' => htmlTime($message['modified_time']),
- 'timestamp' => forum_time(true, $message['modified_time']),
- 'name' => $message['modified_name']
- ),
- 'body' => $message['body'],
- 'new' => empty($message['is_read']),
- 'approved' => $message['approved'],
- 'first_new' => isset($context['start_from']) && $context['start_from'] == $counter,
- 'is_ignored' => !empty($modSettings['enable_buddylist']) && in_array($message['id_member'], $context['user']['ignoreusers']),
- 'is_message_author' => $message['id_member'] == $user_info['id'],
- 'can_approve' => !$message['approved'] && $context['can_approve'],
- 'can_unapprove' => !empty($modSettings['postmod_active']) && $context['can_approve'] && $message['approved'],
- 'can_modify' => (!$context['is_locked'] || allowedTo('moderate_board')) && (allowedTo('modify_any') || (allowedTo('modify_replies') && $context['user']['started']) || (allowedTo('modify_own') && $message['id_member'] == $user_info['id'] && (empty($modSettings['edit_disable_time']) || !$message['approved'] || $message['poster_time'] + $modSettings['edit_disable_time'] * 60 > time()))),
- 'can_remove' => allowedTo('delete_any') || (allowedTo('delete_replies') && $context['user']['started']) || (allowedTo('delete_own') && $message['id_member'] == $user_info['id'] && (empty($modSettings['edit_disable_time']) || $message['poster_time'] + $modSettings['edit_disable_time'] * 60 > time())),
- 'can_see_ip' => allowedTo('moderate_forum') || ($message['id_member'] == $user_info['id'] && !empty($user_info['id'])),
- 'can_like' => $message['use_likes'] && !$message['you_liked'],
- 'can_unlike' => $message['use_likes'] && $message['you_liked'],
- 'like_counter' => $message['like_count'],
- 'likes_enabled' => !empty($modSettings['likes_enabled']) && ($message['use_likes'] || ($message['like_count'] != 0)),
- 'classes' => array(),
- );
-
- if (!empty($output['modified']['name']))
- $output['modified']['last_edit_text'] = sprintf($txt['last_edit_by'], $output['modified']['time'], $output['modified']['name'], standardTime($output['modified']['timestamp']));
-
- if (!empty($output['member']['karma']['allow']))
- {
- $output['member']['karma'] += array(
- 'applaud_url' => $scripturl . '?action=karma;sa=applaud;uid=' . $output['member']['id'] . ';topic=' . $context['current_topic'] . '.' . $context['start'] . ';m=' . $output['id'] . ';' . $context['session_var'] . '=' . $context['session_id'],
- 'smite_url' => $scripturl . '?action=karma;sa=smite;uid=' . $output['member']['id'] . ';topic=' . $context['current_topic'] . '.' . $context['start'] . ';m=' . $output['id'] . ';' . $context['session_var'] . '=' . $context['session_id']
- );
- }
-
- call_integration_hook('integrate_prepare_display_context', array(&$output, &$message));
-
- $output['classes'] = implode(' ', $output['classes']);
-
- $counter++;
-
- return $output;
- }
}
diff --git a/sources/controllers/PersonalMessage.controller.php b/sources/controllers/PersonalMessage.controller.php
index e0607aea87..71659f0a7a 100644
--- a/sources/controllers/PersonalMessage.controller.php
+++ b/sources/controllers/PersonalMessage.controller.php
@@ -424,7 +424,6 @@ public function action_folder()
// Set up some basic template stuff.
$context['from_or_to'] = $context['folder'] !== 'sent' ? 'from' : 'to';
- $context['get_pmessage'] = 'preparePMContext_callback';
$context['signature_enabled'] = substr($modSettings['signature_settings'], 0, 1) == 1;
$context['disabled_fields'] = isset($modSettings['disabled_profile_fields']) ? array_flip(explode(',', $modSettings['disabled_profile_fields'])) : array();
@@ -635,12 +634,12 @@ public function action_folder()
foreach (array_reverse($pms) as $pm)
$orderBy[] = 'pm.id_pm = ' . $pm;
- // Separate query for these bits, preparePMContext_callback will use it as required
+ // Separate query for these bits, the callback will use it as required
$subjects_request = loadPMSubjectRequest($pms, $orderBy);
}
// Execute the load message query if a message has been chosen and let
- // preparePMContext_callback fetch the results. Otherwise just show the pm selection list
+ // the callback fetch the results. Otherwise just show the pm selection list
if (empty($pmsg) && empty($pmID) && $context['display_mode'] != 0)
{
$messages_request = false;
@@ -655,6 +654,16 @@ public function action_folder()
$messages_request = false;
}
+ Elk_Autoloader::instance()->register(SUBSDIR . '/MessagesCallback', '\\ElkArte\\sources\\subs\\MessagesCallback');
+
+ $bodyParser = new \ElkArte\sources\subs\MessagesCallback\BodyParser\Normal(array(), false);
+ $renderer = new \ElkArte\sources\subs\MessagesCallback\PmRenderer($messages_request, $bodyParser);
+ $srenderer = new \ElkArte\sources\subs\MessagesCallback\PmRenderer($subjects_request, $bodyParser);
+
+ $context['get_pmessage'] = array($renderer, 'getContext');
+ $context['get_psubject'] = array($srenderer, 'getContext');
+
+ $context['topic_starter_id'] = 0;
// Prepare some items for the template
$context['can_send_pm'] = allowedTo('pm_send');
$context['can_send_email'] = allowedTo('send_email_to_members');
@@ -2950,193 +2959,3 @@ private function _highlighted_callback($matches)
return isset($matches[2]) && $matches[2] == $matches[1] ? stripslashes($matches[1]) : '' . $matches[1] . '';
}
}
-
-/**
- * Get a personal message for the template. (used to save memory.)
- *
- * - This is a callback function that will fetch the actual results, as needed, of a previously run
- * subject (loadPMSubjectRequest) or message (loadPMMessageRequest) query.
- *
- * @param string $type
- * @param boolean $reset
- *
- * @return array
- */
-function preparePMContext_callback($type = 'subject', $reset = false)
-{
- global $txt, $scripturl, $modSettings, $settings, $context, $memberContext, $recipients, $user_info;
- global $subjects_request, $messages_request;
- static $counter = null, $temp_pm_selected = null;
-
- // We need this
- $db = database();
-
- // Count the current message number....
- if ($counter === null || $reset)
- {
- $counter = $context['start'];
- }
-
- if ($temp_pm_selected === null)
- {
- $temp_pm_selected = isset($_SESSION['pm_selected']) ? $_SESSION['pm_selected'] : array();
- $_SESSION['pm_selected'] = array();
- }
-
- // If we're in non-boring view do something exciting!
- if ($context['display_mode'] != 0 && $subjects_request && $type === 'subject')
- {
- $subject = $db->fetch_assoc($subjects_request);
- if (!$subject)
- {
- $db->free_result($subjects_request);
-
- return false;
- }
-
- // Make sure we have a subject
- $subject['subject'] = $subject['subject'] === '' ? $txt['no_subject'] : $subject['subject'];
- $subject['subject'] = censor($subject['subject']);
-
- $output = array(
- 'id' => $subject['id_pm'],
- 'member' => array(
- 'id' => $subject['id_member_from'],
- 'name' => $subject['from_name'],
- 'link' => $subject['not_guest'] ? '' . $subject['from_name'] . '' : $subject['from_name'],
- ),
- 'recipients' => &$recipients[$subject['id_pm']],
- 'subject' => $subject['subject'],
- 'time' => standardTime($subject['msgtime']),
- 'html_time' => htmlTime($subject['msgtime']),
- 'timestamp' => forum_time(true, $subject['msgtime']),
- 'number_recipients' => count($recipients[$subject['id_pm']]['to']),
- 'labels' => &$context['message_labels'][$subject['id_pm']],
- 'fully_labeled' => count($context['message_labels'][$subject['id_pm']]) == count($context['labels']),
- 'is_replied_to' => &$context['message_replied'][$subject['id_pm']],
- 'is_unread' => &$context['message_unread'][$subject['id_pm']],
- 'is_selected' => !empty($temp_pm_selected) && in_array($subject['id_pm'], $temp_pm_selected),
- );
-
- // In conversation view we need to indicate on the subject listing if any message inside of
- // that conversation is unread, not just if the latest is unread.
- if ($context['display_mode'] == 2 && isset($context['conversation_unread'][$output['id']]))
- {
- $output['is_unread'] = true;
- }
-
- return $output;
- }
-
- // Bail if it's false, ie. no messages.
- if ($messages_request === false)
- {
- return false;
- }
-
- // Reset the data?
- if ($reset === true)
- {
- return $db->data_seek($messages_request, 0);
- }
-
- // Get the next one... bail if anything goes wrong.
- $message = $db->fetch_assoc($messages_request);
- if (!$message)
- {
- if ($type != 'subject')
- {
- $db->free_result($messages_request);
- }
-
- return false;
- }
-
- // Use '(no subject)' if none was specified.
- $message['subject'] = $message['subject'] === '' ? $txt['no_subject'] : $message['subject'];
-
- // Load the message's information - if it's not there, load the guest information.
- if (!loadMemberContext($message['id_member_from'], true))
- {
- $memberContext[$message['id_member_from']]['name'] = $message['from_name'];
- $memberContext[$message['id_member_from']]['id'] = 0;
-
- // Sometimes the forum sends messages itself (Warnings are an example) - in this case don't label it from a guest.
- $memberContext[$message['id_member_from']]['group'] = $message['from_name'] == $context['forum_name'] ? '' : $txt['guest_title'];
- $memberContext[$message['id_member_from']]['link'] = $message['from_name'];
- $memberContext[$message['id_member_from']]['email'] = '';
- $memberContext[$message['id_member_from']]['show_email'] = showEmailAddress(true, 0);
- $memberContext[$message['id_member_from']]['is_guest'] = true;
- }
- else
- {
- $memberContext[$message['id_member_from']]['can_view_profile'] = allowedTo('profile_view_any') || ($message['id_member_from'] == $user_info['id'] && allowedTo('profile_view_own'));
- $memberContext[$message['id_member_from']]['can_see_warning'] = !isset($context['disabled_fields']['warning_status']) && $memberContext[$message['id_member_from']]['warning_status'] && ($context['user']['can_mod'] || (!empty($modSettings['warning_show']) && ($modSettings['warning_show'] > 1 || $message['id_member_from'] == $user_info['id'])));
- }
-
- $memberContext[$message['id_member_from']]['show_profile_buttons'] = $settings['show_profile_buttons'] && (!empty($memberContext[$message['id_member_from']]['can_view_profile']) || (!empty($memberContext[$message['id_member_from']]['website']['url']) && !isset($context['disabled_fields']['website'])) || (in_array($memberContext[$message['id_member_from']]['show_email'], array('yes', 'yes_permission_override', 'no_through_forum'))) || $context['can_send_pm']);
-
- // Censor all the important text...
- $message['body'] = censor($message['body']);
- $message['subject'] = censor($message['subject']);
-
- // Run BBC interpreter on the message.
- $bbc_parser = \BBC\ParserWrapper::instance();
- $message['body'] = $bbc_parser->parsePM($message['body']);
-
- // Return the array.
- $output = array(
- 'alternate' => $counter % 2,
- 'id' => $message['id_pm'],
- 'member' => &$memberContext[$message['id_member_from']],
- 'subject' => $message['subject'],
- 'time' => standardTime($message['msgtime']),
- 'html_time' => htmlTime($message['msgtime']),
- 'timestamp' => forum_time(true, $message['msgtime']),
- 'counter' => $counter,
- 'body' => $message['body'],
- 'recipients' => &$recipients[$message['id_pm']],
- 'number_recipients' => count($recipients[$message['id_pm']]['to']),
- 'labels' => &$context['message_labels'][$message['id_pm']],
- 'fully_labeled' => count($context['message_labels'][$message['id_pm']]) == count($context['labels']),
- 'is_replied_to' => &$context['message_replied'][$message['id_pm']],
- 'is_unread' => &$context['message_unread'][$message['id_pm']],
- 'is_selected' => !empty($temp_pm_selected) && in_array($message['id_pm'], $temp_pm_selected),
- 'is_message_author' => $message['id_member_from'] == $user_info['id'],
- 'can_report' => !empty($modSettings['enableReportPM']),
- 'can_see_ip' => allowedTo('moderate_forum') || ($message['id_member_from'] == $user_info['id'] && !empty($user_info['id'])),
- );
-
- $context['additional_pm_drop_buttons'] = array();
-
- // Can they report this message
- if (!empty($output['can_report']) && $context['folder'] !== 'sent' && $output['member']['id'] != $user_info['id'])
- {
- $context['additional_pm_drop_buttons']['warn_button'] = array(
- 'href' => $scripturl . '?action=pm;sa=report;l=' . $context['current_label_id'] . ';pmsg=' . $output['id'] . ';' . $context['session_var'] . '=' . $context['session_id'],
- 'text' => $txt['pm_report_to_admin']
- );
- }
-
- // Or mark it as unread
- if (empty($output['is_unread']) && $context['folder'] !== 'sent' && $output['member']['id'] != $user_info['id'])
- {
- $context['additional_pm_drop_buttons']['restore_button'] = array(
- 'href' => $scripturl . '?action=pm;sa=markunread;l=' . $context['current_label_id'] . ';pmsg=' . $output['id'] . ';' . $context['session_var'] . '=' . $context['session_id'],
- 'text' => $txt['pm_mark_unread']
- );
- }
-
- // Or give / take karma for a PM
- if (!empty($output['member']['karma']['allow']))
- {
- $output['member']['karma'] += array(
- 'applaud_url' => $scripturl . '?action=karma;sa=applaud;uid=' . $output['member']['id'] . ';f=' . $context['folder'] . ';start=' . $context['start'] . ($context['current_label_id'] != -1 ? ';l=' . $context['current_label_id'] : '') . ';pm=' . $output['id'] . ';' . $context['session_var'] . '=' . $context['session_id'],
- 'smite_url' => $scripturl . '?action=karma;sa=smite;uid=' . $output['member']['id'] . ';f=' . $context['folder'] . ';start=' . $context['start'] . ($context['current_label_id'] != -1 ? ';l=' . $context['current_label_id'] : '') . ';pm=' . $output['id'] . ';' . $context['session_var'] . '=' . $context['session_id'],
- );
- }
-
- $counter++;
-
- return $output;
-}
diff --git a/sources/controllers/Search.controller.php b/sources/controllers/Search.controller.php
index ebcf5419a8..de5ae8bb01 100644
--- a/sources/controllers/Search.controller.php
+++ b/sources/controllers/Search.controller.php
@@ -23,24 +23,6 @@
*/
class Search_Controller extends Action_Controller
{
- /**
- * Weighing factor each area, ie frequency, age, sticky, etc
- * @var array
- */
- protected $_weight = array();
-
- /**
- * Holds the total of all weight factors, should be 100
- * @var int
- */
- protected $_weight_total = 0;
-
- /**
- * Holds array of search result relevancy weigh factors
- * @var array
- */
- protected $_weight_factors = array();
-
/**
* Holds the search object
* @var \ElkArte\Search\Search
@@ -53,6 +35,12 @@ class Search_Controller extends Action_Controller
*/
protected $_icon_sources = null;
+ /**
+ *
+ * @var mixed[]
+ */
+ protected $_participants = [];
+
/**
* Called before any other action method in this class.
*
@@ -85,6 +73,7 @@ public function pre_dispatch()
{
throw new Elk_Exception('loadavg_search_disabled', false);
}
+ Elk_Autoloader::instance()->register(SUBSDIR . '/Search', '\\ElkArte\\Search');
}
/**
@@ -160,11 +149,9 @@ public function action_search()
// If you got back from search;sa=results by using the linktree, you get your original search parameters back.
if ($this->_search === null && isset($_REQUEST['params']))
{
- Elk_Autoloader::instance()->register(SUBSDIR . '/Search', '\\ElkArte\\Search');
- $this->_search = new \ElkArte\Search\Search();
- $this->_search->searchParamsFromString($_REQUEST['params']);
+ $search_params = new \ElkArte\Search\SearchParams($_REQUEST['params'] ?? '');
- $context['search_params'] = $this->_search->getParams();
+ $context['search_params'] = $search_params->get();
}
if (isset($_REQUEST['search']))
@@ -258,18 +245,16 @@ public function action_results()
{
global $scripturl, $modSettings, $txt, $settings;
global $user_info, $context, $options, $messages_request, $boards_can;
- global $participants;
// No, no, no... this is a bit hard on the server, so don't you go prefetching it!
stop_prefetching();
- // Set up the weights to help tune result relevancy
- $this->_setup_weight_factors();
-
// These vars don't require an interface, they're just here for tweaking.
$recentPercentage = 0.30;
+ // Message length used to tweak messages relevance of the results
$humungousTopicPosts = 200;
$maxMembersToSearch = 500;
+ // Maximum number of results
$maxMessageResults = empty($modSettings['search_max_results']) ? 0 : $modSettings['search_max_results'] * 5;
// Start with no errors.
@@ -292,15 +277,11 @@ public function action_results()
isAllowedTo('search_posts');
$this->_search = new \ElkArte\Search\Search();
- $this->_search->setWeights($this->_weight_factors, $this->_weight, $this->_weight_total);
-
- // Load up the search API we are going to use.
- $searchAPI = $this->_search->findSearchAPI();
+ $this->_search->setWeights(new \ElkArte\Search\WeightFactors($modSettings, $user_info['is_admin']));
+ $search_params = new \ElkArte\Search\SearchParams($_REQUEST['params'] ?? '');
+ $search_params->merge($_REQUEST, $recentPercentage, $maxMembersToSearch);
+ $this->_search->setParams($search_params, !empty($modSettings['search_simple_fulltext']));
- if (isset($_REQUEST['params']))
- $this->_search->searchParamsFromString($_REQUEST['params']);
-
- $this->_search->mergeSearchParams($_REQUEST, $recentPercentage, $maxMembersToSearch);
$context['compact'] = $this->_search->isCompact();
// Nothing??
@@ -313,7 +294,7 @@ public function action_results()
// Build the search array
// $modSettings ['search_simple_fulltext'] is an hidden setting that will
// do fulltext searching in the most basic way.
- $searchArray = $this->_search->searchArray(!empty($modSettings['search_simple_fulltext']));
+ $searchArray = $this->_search->getSearchArray();
// This is used to remember words that will be ignored (because too short usually)
$context['search_ignored'] = $this->_search->getIgnored();
@@ -331,19 +312,17 @@ public function action_results()
unset($context['search_errors']['invalid_search_string']);
}
- $searchWords = $this->_search->searchWords();
-
// *** Spell checking?
if (!empty($modSettings['enableSpellChecking']) && function_exists('pspell_new'))
{
$context['did_you_mean'] = '';
$context['did_you_mean_params'] = '';
// @todo maybe move the html to a $settings
- $this->_search->loadSuggestions($context['did_you_mean'], $context['did_you_mean_params'], '{word}');
+ $this->loadSuggestions($context['did_you_mean'], $context['did_you_mean_params'], '{word}');
}
// Let the user adjust the search query, should they wish?
- $context['search_params'] = $this->_search->getParams();
+ $context['search_params'] = (array) $this->_search->getSearchParams(true);
if (isset($context['search_params']['search']))
$context['search_params']['search'] = Util::htmlspecialchars($context['search_params']['search']);
if (isset($context['search_params']['userspec']))
@@ -355,29 +334,7 @@ public function action_results()
$context['search_params'] = $this->_fill_default_search_params($context['search_params']);
- // Do we have captcha enabled?
- if ($user_info['is_guest'] && !empty($modSettings['search_enable_captcha']) && empty($_SESSION['ss_vv_passed']) && (empty($_SESSION['last_ss']) || $_SESSION['last_ss'] != $this->_search->param('search')))
- {
- // If we come from another search box tone down the error...
- if (!isset($_REQUEST['search_vv']))
- $context['search_errors']['need_verification_code'] = true;
- else
- {
- $verificationOptions = array(
- 'id' => 'search',
- );
- $context['require_verification'] = VerificationControls_Integrate::create($verificationOptions, true);
-
- if (is_array($context['require_verification']))
- {
- foreach ($context['require_verification'] as $error)
- $context['search_errors'][$error] = true;
- }
- // Don't keep asking for it - they've proven themselves worthy.
- else
- $_SESSION['ss_vv_passed'] = true;
- }
- }
+ $this->_controlVerifications();
$context['params'] = $this->_search->compileURLparams();
@@ -401,7 +358,7 @@ public function action_results()
// One or more search errors? Go back to the first search screen.
if (!empty($context['search_errors']))
- $this->action_search();
+ return $this->action_search();
// Spam me not, Spam-a-lot?
if (empty($_SESSION['last_ss']) || $_SESSION['last_ss'] != $this->_search->param('search'))
@@ -410,46 +367,22 @@ public function action_results()
// Store the last search string to allow pages of results to be browsed.
$_SESSION['last_ss'] = $this->_search->param('search');
- // *** Reserve an ID for caching the search results.
- $query_params = $this->_search->getParams();
-
- // Can this search rely on the API given the parameters?
- if (is_callable(array($searchAPI, 'searchQuery')))
+ try
{
- $participants = array();
- $searchArray = array();
- $num_results = $searchAPI->searchQuery($query_params, $searchWords, $this->_search->getExcludedIndexWords(), $participants, $searchArray);
+ $search_config = new \ElkArte\ValuesContainer(array(
+ 'humungousTopicPosts' => $humungousTopicPosts,
+ 'maxMessageResults' => $maxMessageResults,
+ 'search_index' => !empty($modSettings['search_index']) ? $modSettings['search_index'] : '',
+ 'banned_words' => empty($modSettings['search_banned_words']) ? array() : explode(',', $modSettings['search_banned_words']),
+ ));
+ $context['topics'] = $this->_search->searchQuery(
+ new \ElkArte\Search\SearchApiWrapper($search_config, $this->_search->getSearchParams())
+ );
}
- // Update the cache if the current search term is not yet cached.
- else
+ catch (\Exception $e)
{
- $update_cache = empty($_SESSION['search_cache']) || ($_SESSION['search_cache']['params'] != $context['params']);
- if ($update_cache)
- {
- $this->_increase_pointer();
-
- // Clear the previous cache of the final results cache.
- $this->_search->clearCacheResults($_SESSION['search_cache']['id_search']);
-
- if ($this->_search->param('subject_only'))
- $_SESSION['search_cache']['num_results'] = $this->_search->getSubjectResults($_SESSION['search_cache']['id_search'], $humungousTopicPosts);
- else
- {
- $num_res = $this->_search->getResults($_SESSION['search_cache']['id_search'], $humungousTopicPosts, $maxMessageResults);
- if (empty($num_res))
- {
- $context['search_errors']['query_not_specific_enough'] = true;
- $this->action_search();
- }
-
- $_SESSION['search_cache']['num_results'] = $num_res;
- }
- }
-
- // *** Retrieve the results to be shown on the page
- $participants = $this->_search->addRelevance($context['topics'], $_SESSION['search_cache']['id_search'], (int) $_REQUEST['start'], $modSettings['search_results_per_page']);
-
- $num_results = $_SESSION['search_cache']['num_results'];
+ $context['search_errors'][$e->getMessage()] = true;
+ return $this->action_search();
}
if (!empty($context['topics']))
@@ -485,414 +418,198 @@ public function action_results()
if ($this->_search->noMessages($messages_request))
$context['topics'] = array();
- // If we want to know who participated in what then load this now.
- if (!empty($modSettings['enableParticipation']) && !$user_info['is_guest'])
- {
- require_once(SUBSDIR . '/MessageIndex.subs.php');
- $topics_participated_in = topicsParticipation($user_info['id'], array_keys($participants));
-
- foreach ($topics_participated_in as $topic)
- $participants[$topic['id_topic']] = true;
- }
+ $this->_prepareParticipants(!empty($modSettings['enableParticipation']), $user_info['is_guest'] ? $user_info['id'] : 0);
}
// Now that we know how many results to expect we can start calculating the page numbers.
- $context['page_index'] = constructPageIndex($scripturl . '?action=search;sa=results;params=' . $context['params'], $_REQUEST['start'], $num_results, $modSettings['search_results_per_page'], false);
+ $context['page_index'] = constructPageIndex($scripturl . '?action=search;sa=results;params=' . $context['params'], $_REQUEST['start'], $this->_search->getNumResults(), $modSettings['search_results_per_page'], false);
// Consider the search complete!
Cache::instance()->remove('search_start:' . ($user_info['is_guest'] ? $user_info['ip'] : $user_info['id']));
- $context['key_words'] = &$searchArray;
$context['sub_template'] = 'results';
$context['page_title'] = $txt['search_results'];
- $context['get_topics'] = array($this, 'prepareSearchContext_callback');
+
+ Elk_Autoloader::instance()->register(SUBSDIR . '/MessagesCallback', '\\ElkArte\\sources\\subs\\MessagesCallback');
+
$this->_icon_sources = new MessageTopicIcons(!empty($modSettings['messageIconChecks_enable']), $settings['theme_dir']);
+ // Set the callback. (do you REALIZE how much memory all the messages would take?!?)
+ // This will be called from the template.
+ $bodyParser = new \ElkArte\sources\subs\MessagesCallback\BodyParser\Normal($this->_search->getSearchArray(), empty($modSettings['search_method']));
+ $opt = new \ElkArte\ValuesContainer([
+ 'icon_sources' => $this->_icon_sources,
+ 'show_signatures' => false,
+ ]);
+ $renderer = new \ElkArte\sources\subs\MessagesCallback\SearchRenderer($messages_request, $bodyParser, $opt);
+ $renderer->setParticipants($this->_participants);
+
+ $context['topic_starter_id'] = 0;
+ $context['get_topics'] = array($renderer, 'getContext');
+
$context['jump_to'] = array(
'label' => addslashes(un_htmlspecialchars($txt['jump_to'])),
'board_name' => addslashes(un_htmlspecialchars($txt['select_destination'])),
);
}
- /**
- * Callback to return messages - saves memory.
- *
- * @todo Fix this, update it, whatever... from Display.controller.php mainly.
- * Note that the call to loadAttachmentContext() doesn't work:
- * this function doesn't fulfill the pre-condition to fill $attachments global...
- * So all it does is to fallback and return.
- *
- * What it does:
- *
- * - Callback function for the results sub template.
- * - Loads the necessary contextual data to show a search result.
- *
- * @param boolean $reset = false
- * @return array of messages that match the search
- */
- public function prepareSearchContext_callback($reset = false)
+ protected function _controlVerifications()
{
- global $txt, $modSettings, $scripturl, $user_info;
- global $memberContext, $context, $options, $messages_request;
- global $boards_can, $participants;
+ global $user_info, $modSettings, $context;
- // Remember which message this is. (ie. reply #83)
- static $counter = null;
- if ($counter === null || $reset)
- $counter = $_REQUEST['start'] + 1;
+ // Do we have captcha enabled?
+ if ($user_info['is_guest'] && !empty($modSettings['search_enable_captcha']) && empty($_SESSION['ss_vv_passed']) && (empty($_SESSION['last_ss']) || $_SESSION['last_ss'] != $this->_search->param('search')))
+ {
+ // If we come from another search box tone down the error...
+ if (!isset($_REQUEST['search_vv']))
+ {
+ $context['search_errors']['need_verification_code'] = true;
+ }
+ else
+ {
+ $verificationOptions = array(
+ 'id' => 'search',
+ );
+ $context['require_verification'] = VerificationControls_Integrate::create($verificationOptions, true);
- // Start from the beginning...
- if ($reset)
- return currentContext($messages_request, $reset);
+ if (is_array($context['require_verification']))
+ {
+ foreach ($context['require_verification'] as $error)
+ $context['search_errors'][$error] = true;
+ }
+ // Don't keep asking for it - they've proven themselves worthy.
+ else
+ $_SESSION['ss_vv_passed'] = true;
+ }
+ }
+ }
- // Attempt to get the next in line
- $message = currentContext($messages_request);
- if (!$message)
- return false;
+ /**
+ * Setup spellchecking suggestions and load them into the two variable
+ * passed by ref
+ *
+ * @param string $suggestion_display - the string to display in the template
+ * @param string $suggestion_param - a param string to be used in a url
+ * @param string $display_highlight - a template to enclose in each suggested word
+ */
+ protected function loadSuggestions(&$suggestion_display = '', &$suggestion_param = '', $display_highlight = '')
+ {
+ global $txt;
- // Can't have an empty subject can we?
- $message['subject'] = $message['subject'] != '' ? $message['subject'] : $txt['no_subject'];
+ // Windows fix.
+ ob_start();
+ $old = error_reporting(0);
- $message['first_subject'] = $message['first_subject'] != '' ? $message['first_subject'] : $txt['no_subject'];
- $message['last_subject'] = $message['last_subject'] != '' ? $message['last_subject'] : $txt['no_subject'];
+ pspell_new('en');
+ $pspell_link = pspell_new($txt['lang_dictionary'], $txt['lang_spelling'], '', 'utf-8', PSPELL_FAST | PSPELL_RUN_TOGETHER);
- // If it couldn't load, or the user was a guest.... someday may be done with a guest table.
- if (!loadMemberContext($message['id_member']))
+ if (!$pspell_link)
{
- // Notice this information isn't used anywhere else.... *cough guest table cough*.
- $memberContext[$message['id_member']]['name'] = $message['poster_name'];
- $memberContext[$message['id_member']]['id'] = 0;
- $memberContext[$message['id_member']]['group'] = $txt['guest_title'];
- $memberContext[$message['id_member']]['link'] = $message['poster_name'];
- $memberContext[$message['id_member']]['email'] = $message['poster_email'];
+ $pspell_link = pspell_new('en', '', '', '', PSPELL_FAST | PSPELL_RUN_TOGETHER);
}
- $memberContext[$message['id_member']]['ip'] = $message['poster_ip'];
- // Do the censor thang...
- $message['body'] = censor($message['body']);
- $message['subject'] = censor($message['subject']);
- $message['first_subject'] = censor($message['first_subject']);
- $message['last_subject'] = censor($message['last_subject']);
+ error_reporting($old);
+ @ob_end_clean();
- // Shorten this message if necessary.
- if ($context['compact'])
+ if (empty($pspell_link))
{
- // Set the number of characters before and after the searched keyword.
- $charLimit = 50;
+ return;
+ }
- $message['body'] = strtr($message['body'], array("\n" => ' ', '
' => "\n"));
- $bbc_parser = \BBC\ParserWrapper::instance();
- $message['body'] = $bbc_parser->parseMessage($message['body'], $message['smileys_enabled']);
- $message['body'] = strip_tags(strtr($message['body'], array('' => '
', '' => '
')), '
');
+ $did_you_mean = array('search' => array(), 'display' => array());
+ $found_misspelling = false;
+ foreach ($this->_search->getSearchArray() as $word)
+ {
+ // Don't check phrases.
+ if (preg_match('~^\w+$~', $word) === 0)
+ {
+ $did_you_mean['search'][] = '"' . $word . '"';
+ $did_you_mean['display'][] = '"' . \Util::htmlspecialchars($word) . '"';
+ continue;
+ }
+ // For some strange reason spell check can crash PHP on decimals.
+ elseif (preg_match('~\d~', $word) === 1)
+ {
+ $did_you_mean['search'][] = $word;
+ $did_you_mean['display'][] = \Util::htmlspecialchars($word);
+ continue;
+ }
+ elseif (pspell_check($pspell_link, $word))
+ {
+ $did_you_mean['search'][] = $word;
+ $did_you_mean['display'][] = \Util::htmlspecialchars($word);
+ continue;
+ }
- if (Util::strlen($message['body']) > $charLimit)
+ $suggestions = pspell_suggest($pspell_link, $word);
+ foreach ($suggestions as $i => $s)
{
- if (empty($context['key_words']))
- $message['body'] = Util::substr($message['body'], 0, $charLimit) . '...';
- else
+ // Search is case insensitive.
+ if (\Util::strtolower($s) == \Util::strtolower($word))
{
- $matchString = '';
- $force_partial_word = false;
- foreach ($context['key_words'] as $keyword)
- {
- $keyword = un_htmlspecialchars($keyword);
- $keyword = preg_replace_callback('~(&#(\d{1,7}|x[0-9a-fA-F]{1,6});)~', 'entity_fix__callback', strtr($keyword, array('\\\'' => '\'', '&' => '&')));
- if (preg_match('~[\'\.,/@%&;:(){}\[\]_\-+\\\\]$~', $keyword) != 0 || preg_match('~^[\'\.,/@%&;:(){}\[\]_\-+\\\\]~', $keyword) != 0)
- $force_partial_word = true;
- $matchString .= strtr(preg_quote($keyword, '/'), array('\*' => '.+?')) . '|';
- }
- $matchString = un_htmlspecialchars(substr($matchString, 0, -1));
-
- $message['body'] = un_htmlspecialchars(strtr($message['body'], array(' ' => ' ', '
' => "\n", '[' => '[', ']' => ']', ':' => ':', '@' => '@')));
-
- if (empty($modSettings['search_method']) || $force_partial_word)
- preg_match_all('/([^\s\W]{' . $charLimit . '}[\s\W]|[\s\W].{0,' . $charLimit . '}?|^)(' . $matchString . ')(.{0,' . $charLimit . '}[\s\W]|[^\s\W]{0,' . $charLimit . '})/isu', $message['body'], $matches);
- else
- preg_match_all('/([^\s\W]{' . $charLimit . '}[\s\W]|[\s\W].{0,' . $charLimit . '}?[\s\W]|^)(' . $matchString . ')([\s\W].{0,' . $charLimit . '}[\s\W]|[\s\W][^\s\W]{0,' . $charLimit . '})/isu', $message['body'], $matches);
-
- $message['body'] = '';
- foreach ($matches[0] as $match)
- {
- $match = strtr(htmlspecialchars($match, ENT_QUOTES, 'UTF-8'), array("\n" => ' '));
- $message['body'] .= '…… ' . $match . ' ……';
- }
+ unset($suggestions[$i]);
+ }
+ // Plus, don't suggest something the user thinks is rude!
+ elseif ($suggestions[$i] != censor($s))
+ {
+ unset($suggestions[$i]);
}
-
- // Re-fix the international characters.
- $message['body'] = preg_replace_callback('~(&#(\d{1,7}|x[0-9a-fA-F]{1,6});)~', 'entity_fix__callback', $message['body']);
}
- }
- else
- {
- // Run BBC interpreter on the message.
- $bbc_parser = \BBC\ParserWrapper::instance();
- $message['body'] = $bbc_parser->parseMessage($message['body'], $message['smileys_enabled']);
- }
- // Make sure we don't end up with a practically empty message body.
- $message['body'] = preg_replace('~^(?: )+$~', '', $message['body']);
-
- // Do we have quote tag enabled?
- $quote_enabled = empty($modSettings['disabledBBC']) || !in_array('quote', explode(',', $modSettings['disabledBBC']));
-
- $output = array_merge($context['topics'][$message['id_msg']], array(
- 'id' => $message['id_topic'],
- 'is_sticky' => !empty($message['is_sticky']),
- 'is_locked' => !empty($message['locked']),
- 'is_poll' => !empty($modSettings['pollMode']) && $message['id_poll'] > 0,
- 'is_hot' => !empty($modSettings['useLikesNotViews']) ? $message['num_likes'] >= $modSettings['hotTopicPosts'] : $message['num_replies'] >= $modSettings['hotTopicPosts'],
- 'is_very_hot' => !empty($modSettings['useLikesNotViews']) ? $message['num_likes'] >= $modSettings['hotTopicVeryPosts'] : $message['num_replies'] >= $modSettings['hotTopicVeryPosts'],
- 'posted_in' => !empty($participants[$message['id_topic']]),
- 'views' => $message['num_views'],
- 'replies' => $message['num_replies'],
- 'tests' => array(
- 'can_reply' => in_array($message['id_board'], $boards_can['post_reply_any']) || in_array(0, $boards_can['post_reply_any']),
- 'can_quote' => (in_array($message['id_board'], $boards_can['post_reply_any']) || in_array(0, $boards_can['post_reply_any'])) && $quote_enabled,
- 'can_mark_notify' => in_array($message['id_board'], $boards_can['mark_any_notify']) || in_array(0, $boards_can['mark_any_notify']) && !$context['user']['is_guest'],
- ),
- 'first_post' => array(
- 'id' => $message['first_msg'],
- 'time' => standardTime($message['first_poster_time']),
- 'html_time' => htmlTime($message['first_poster_time']),
- 'timestamp' => forum_time(true, $message['first_poster_time']),
- 'subject' => $message['first_subject'],
- 'href' => $scripturl . '?topic=' . $message['id_topic'] . '.0',
- 'link' => '' . $message['first_subject'] . '',
- 'icon' => $message['first_icon'],
- 'icon_url' => $this->_icon_sources->{$message['first_icon']},
- 'member' => array(
- 'id' => $message['first_member_id'],
- 'name' => $message['first_member_name'],
- 'href' => !empty($message['first_member_id']) ? $scripturl . '?action=profile;u=' . $message['first_member_id'] : '',
- 'link' => !empty($message['first_member_id']) ? '' . $message['first_member_name'] . '' : $message['first_member_name']
- )
- ),
- 'last_post' => array(
- 'id' => $message['last_msg'],
- 'time' => standardTime($message['last_poster_time']),
- 'html_time' => htmlTime($message['last_poster_time']),
- 'timestamp' => forum_time(true, $message['last_poster_time']),
- 'subject' => $message['last_subject'],
- 'href' => $scripturl . '?topic=' . $message['id_topic'] . ($message['num_replies'] == 0 ? '.0' : '.msg' . $message['last_msg']) . '#msg' . $message['last_msg'],
- 'link' => '' . $message['last_subject'] . '',
- 'icon' => $message['last_icon'],
- 'icon_url' => $this->_icon_sources->{$message['last_icon']},
- 'member' => array(
- 'id' => $message['last_member_id'],
- 'name' => $message['last_member_name'],
- 'href' => !empty($message['last_member_id']) ? $scripturl . '?action=profile;u=' . $message['last_member_id'] : '',
- 'link' => !empty($message['last_member_id']) ? '' . $message['last_member_name'] . '' : $message['last_member_name']
- )
- ),
- 'board' => array(
- 'id' => $message['id_board'],
- 'name' => $message['board_name'],
- 'href' => $scripturl . '?board=' . $message['id_board'] . '.0',
- 'link' => '' . $message['board_name'] . ''
- ),
- 'category' => array(
- 'id' => $message['id_cat'],
- 'name' => $message['cat_name'],
- 'href' => $scripturl . $modSettings['default_forum_action'] . '#c' . $message['id_cat'],
- 'link' => '' . $message['cat_name'] . ''
- )
- ));
-
- determineTopicClass($output);
-
- if ($output['posted_in'])
- $output['class'] = 'my_' . $output['class'];
-
- $body_highlighted = $message['body'];
- $subject_highlighted = $message['subject'];
-
- if (!empty($options['display_quick_mod']))
- {
- $started = $output['first_post']['member']['id'] == $user_info['id'];
-
- $output['quick_mod'] = array(
- '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']))),
- 'sticky' => (in_array(0, $boards_can['make_sticky']) || in_array($output['board']['id'], $boards_can['make_sticky'])),
- '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']))),
- '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']))),
- );
-
- $context['can_lock'] |= $output['quick_mod']['lock'];
- $context['can_sticky'] |= $output['quick_mod']['sticky'];
- $context['can_move'] |= $output['quick_mod']['move'];
- $context['can_remove'] |= $output['quick_mod']['remove'];
- $context['can_merge'] |= in_array($output['board']['id'], $boards_can['merge_any']);
- $context['can_markread'] = $context['user']['is_logged'];
-
- $context['qmod_actions'] = array('remove', 'lock', 'sticky', 'move', 'markread');
- call_integration_hook('integrate_quick_mod_actions_search');
+ // Anything found? If so, correct it!
+ if (!empty($suggestions))
+ {
+ $suggestions = array_values($suggestions);
+ $did_you_mean['search'][] = $suggestions[0];
+ $did_you_mean['display'][] = str_replace('{word}', \Util::htmlspecialchars($suggestions[0]), $display_highlight);
+ $found_misspelling = true;
+ }
+ else
+ {
+ $did_you_mean['search'][] = $word;
+ $did_you_mean['display'][] = \Util::htmlspecialchars($word);
+ }
}
- foreach ($context['key_words'] as $query)
+ if ($found_misspelling)
{
- // Fix the international characters in the keyword too.
- $query = un_htmlspecialchars($query);
- $query = trim($query, '\*+');
- $query = strtr(Util::htmlspecialchars($query), array('\\\'' => '\''));
-
- $body_highlighted = preg_replace_callback('/((<[^>]*)|' . preg_quote(strtr($query, array('\'' => ''')), '/') . ')/iu', array($this, '_highlighted_callback'), $body_highlighted);
- $subject_highlighted = preg_replace('/(' . preg_quote($query, '/') . ')/iu', '$1', $subject_highlighted);
- }
+ // Don't spell check excluded words, but add them still...
+ $temp_excluded = array('search' => array(), 'display' => array());
+ foreach ($this->_search->getExcludedWords() as $word)
+ {
+ if (preg_match('~^\w+$~', $word) == 0)
+ {
+ $temp_excluded['search'][] = '-"' . $word . '"';
+ $temp_excluded['display'][] = '-"' . \Util::htmlspecialchars($word) . '"';
+ }
+ else
+ {
+ $temp_excluded['search'][] = '-' . $word;
+ $temp_excluded['display'][] = '-' . \Util::htmlspecialchars($word);
+ }
+ }
- require_once(SUBSDIR . '/Attachments.subs.php');
- $output['matches'][] = array(
- 'id' => $message['id_msg'],
- 'attachment' => loadAttachmentContext($message['id_msg']),
- 'alternate' => $counter % 2,
- 'member' => &$memberContext[$message['id_member']],
- 'icon' => $message['icon'],
- 'icon_url' => $this->_icon_sources->{$message['icon']},
- 'subject' => $message['subject'],
- 'subject_highlighted' => $subject_highlighted,
- 'time' => standardTime($message['poster_time']),
- 'html_time' => htmlTime($message['poster_time']),
- 'timestamp' => forum_time(true, $message['poster_time']),
- 'counter' => $counter,
- 'modified' => array(
- 'time' => standardTime($message['modified_time']),
- 'html_time' => htmlTime($message['modified_time']),
- 'timestamp' => forum_time(true, $message['modified_time']),
- 'name' => $message['modified_name']
- ),
- 'body' => $message['body'],
- 'body_highlighted' => $body_highlighted,
- 'start' => 'msg' . $message['id_msg']
- );
- $counter++;
+ $did_you_mean['search'] = array_merge($did_you_mean['search'], $temp_excluded['search']);
+ $did_you_mean['display'] = array_merge($did_you_mean['display'], $temp_excluded['display']);
- if (!$context['compact'])
- {
- $output['buttons'] = array(
- // Can we request notification of topics?
- 'notify' => array(
- 'href' => $scripturl . '?action=notify;topic=' . $output['id'] . '.msg' . $message['id_msg'],
- 'text' => $txt['notify'],
- 'test' => 'can_mark_notify',
- ),
- // If they *can* reply?
- 'reply' => array(
- 'href' => $scripturl . '?action=post;topic=' . $output['id'] . '.msg' . $message['id_msg'],
- 'text' => $txt['reply'],
- 'test' => 'can_reply',
- ),
- // If they *can* quote?
- 'quote' => array(
- 'href' => $scripturl . '?action=post;topic=' . $output['id'] . '.msg' . $message['id_msg'] . ';quote=' . $message['id_msg'],
- 'text' => $txt['quote'],
- 'test' => 'can_quote',
- ),
- );
+ // Provide the potential correct spelling term in the param
+ $suggestion_param = $this->_search->compileURLparams($did_you_mean['search']);
+ $suggestion_display = implode(' ', $did_you_mean['display']);
}
-
- call_integration_hook('integrate_search_message_context', array($counter, &$output));
-
- return $output;
}
- /**
- * Used to highlight body text with strings that match the search term
- *
- * Callback function used in $body_highlighted
- *
- * @param string[] $matches
- *
- * @return string
- */
- private function _highlighted_callback($matches)
- {
- return isset($matches[2]) && $matches[2] == $matches[1] ? stripslashes($matches[1]) : '' . $matches[1] . '';
- }
-
- /**
- * Prepares the weighting factors
- */
- private function _setup_weight_factors()
+ protected function _prepareParticipants($participationEnabled, $user_id)
{
- global $user_info, $modSettings;
-
- $default_factors = $this->_weight_factors = array(
- 'frequency' => array(
- 'search' => 'COUNT(*) / (MAX(t.num_replies) + 1)',
- 'results' => '(t.num_replies + 1)',
- ),
- 'age' => array(
- 'search' => 'CASE WHEN MAX(m.id_msg) < {int:min_msg} THEN 0 ELSE (MAX(m.id_msg) - {int:min_msg}) / {int:recent_message} END',
- 'results' => 'CASE WHEN t.id_first_msg < {int:min_msg} THEN 0 ELSE (t.id_first_msg - {int:min_msg}) / {int:recent_message} END',
- ),
- 'length' => array(
- 'search' => 'CASE WHEN MAX(t.num_replies) < {int:huge_topic_posts} THEN MAX(t.num_replies) / {int:huge_topic_posts} ELSE 1 END',
- 'results' => 'CASE WHEN t.num_replies < {int:huge_topic_posts} THEN t.num_replies / {int:huge_topic_posts} ELSE 1 END',
- ),
- 'subject' => array(
- 'search' => 0,
- 'results' => 0,
- ),
- 'first_message' => array(
- 'search' => 'CASE WHEN MIN(m.id_msg) = MAX(t.id_first_msg) THEN 1 ELSE 0 END',
- ),
- 'sticky' => array(
- 'search' => 'MAX(t.is_sticky)',
- 'results' => 't.is_sticky',
- ),
- 'likes' => array(
- 'search' => 'MAX(t.num_likes)',
- 'results' => 't.num_likes',
- ),
- );
-
- // These are fallback weights in case of errors somewhere.
- // Not intended to be passed to the hook
- $default_weights = array(
- 'search_weight_frequency' => 30,
- 'search_weight_age' => 25,
- 'search_weight_length' => 20,
- 'search_weight_subject' => 15,
- 'search_weight_first_message' => 10,
- );
-
- call_integration_hook('integrate_search_weights', array(&$this->_weight_factors));
-
- // Set the weight factors for each area (frequency, age, etc) as defined in the ACP
- $this->_calculate_weights($this->_weight_factors, $modSettings);
-
- // Zero weight. Weightless :P.
- if (empty($this->_weight_total))
+ // If we want to know who participated in what then load this now.
+ if ($participationEnabled === true && $user_id !== 0)
{
- // Admins can be bothered with a failure
- if ($user_info['is_admin'])
- throw new Elk_Exception('search_invalid_weights');
-
- // Even if users will get an answer, the admin should know something is broken
- Errors::instance()->log_lang_error('search_invalid_weights');
-
- // Instead is better to give normal users and guests some kind of result
- // using our defaults.
- // Using a different variable here because it may be the hook is screwing
- // things up
- $this->_calculate_weights($default_factors, $default_weights);
- }
- }
+ $this->_participants = $this->_search->getParticipants();
- /**
- * Fill the $_weight variable and calculate the total weight
- *
- * @param mixed[] $factors
- * @param int[] $weights
- */
- private function _calculate_weights($factors, $weights)
- {
- $this->_weight = array();
- $this->_weight_total = 0;
+ require_once(SUBSDIR . '/MessageIndex.subs.php');
+ $topics_participated_in = topicsParticipation($user_id, array_keys($this->_participants));
- foreach ($factors as $weight_factor => $value)
- {
- $this->_weight[$weight_factor] = empty($weights['search_weight_' . $weight_factor]) ? 0 : (int) $weights['search_weight_' . $weight_factor];
- $this->_weight_total += $this->_weight[$weight_factor];
+ foreach ($topics_participated_in as $topic)
+ $this->_participants[$topic['id_topic']] = true;
}
}
@@ -905,53 +622,25 @@ private function _calculate_weights($factors, $weights)
*/
private function _fill_default_search_params($array)
{
- if (empty($array['search']))
- $array['search'] = '';
+ $default = array(
+ 'search' => '',
+ 'userspec' => '*',
+ 'searchtype' => 0,
+ 'show_complete' => 0,
+ 'subject_only' => 0,
+ 'minage' => 0,
+ 'maxage' => 9999,
+ 'sort' => 'relevance',
+ );
+
+ $array = array_merge($default, $array);
if (empty($array['userspec']))
+ {
$array['userspec'] = '*';
- if (empty($array['searchtype']))
- $array['searchtype'] = 0;
-
- if (!isset($array['show_complete']))
- $array['show_complete'] = 0;
- else
- $array['show_complete'] = (int) $array['show_complete'];
-
- if (!isset($array['subject_only']))
- $array['subject_only'] = 0;
- else
- $array['subject_only'] = (int) $array['subject_only'];
-
- if (empty($array['minage']))
- $array['minage'] = 0;
- if (empty($array['maxage']))
- $array['maxage'] = 9999;
- if (empty($array['sort']))
- $array['sort'] = 'relevance';
+ }
+ $array['show_complete'] = (int) $array['show_complete'];
+ $array['subject_only'] = (int) $array['subject_only'];
return $array;
}
-
- /**
- * Increase the search pointer by 1.
- *
- * - The maximum value is 255, so when it becomes bigger, the pointer is reset to 0.
- */
- private function _increase_pointer()
- {
- global $modSettings, $context;
-
- // Increase the pointer...
- $modSettings['search_pointer'] = empty($modSettings['search_pointer']) ? 0 : (int) $modSettings['search_pointer'];
-
- // ...and store it right off.
- updateSettings(array('search_pointer' => $modSettings['search_pointer'] >= 255 ? 0 : $modSettings['search_pointer'] + 1));
-
- // As long as you don't change the parameters, the cache result is yours.
- $_SESSION['search_cache'] = array(
- 'id_search' => $modSettings['search_pointer'],
- 'num_results' => -1,
- 'params' => $context['params'],
- );
- }
}
diff --git a/sources/subs/Boards.subs.php b/sources/subs/Boards.subs.php
index cf628b8ddf..f0ca92b522 100644
--- a/sources/subs/Boards.subs.php
+++ b/sources/subs/Boards.subs.php
@@ -759,10 +759,13 @@ function reorderBoards()
}
}
- $db->query(
- '',
- '
- UPDATE {db_prefix}boards
+ if (empty($update_query))
+ {
+ return;
+ }
+
+ $db->query('',
+ 'UPDATE {db_prefix}boards
SET
board_order = CASE id_board ' . $update_query . '
END',
diff --git a/sources/subs/MessagesCallback/BodyParser/BodyParserInterface.php b/sources/subs/MessagesCallback/BodyParser/BodyParserInterface.php
new file mode 100644
index 0000000000..62cbe2a103
--- /dev/null
+++ b/sources/subs/MessagesCallback/BodyParser/BodyParserInterface.php
@@ -0,0 +1,51 @@
+_searchArray = $highlight;
+ $this->_use_partial_words = $use_partial_words;
+ $this->_highlight = !empty($highlight);
+ $this->_bbc_parser = \BBC\ParserWrapper::instance();
+ }
+
+ /**
+ * {@inheritdoc }
+ */
+ public function getSearchArray()
+ {
+ return $this->_searchArray;
+ }
+
+ /**
+ * {@inheritdoc }
+ */
+ public function prepare($body, $smileys_enabled)
+ {
+ // Set the number of characters before and after the searched keyword.
+ $charLimit = 50;
+
+ $body = censor($body);
+ $body = strtr($body, array("\n" => ' ', '
' => "\n"));
+ $body = $this->_bbc_parser->parseMessage($body, $smileys_enabled);
+ $body = strip_tags(strtr($body, array('' => '
', '' => '
')), '
');
+
+ if (Util::strlen($body) > $charLimit)
+ {
+ if ($this->_highlight === false)
+ {
+ $body = Util::substr($body, 0, $charLimit) . '...';
+ }
+ else
+ {
+ $matchString = '';
+ $force_partial_word = false;
+ foreach ($this->_searchArray as $keyword)
+ {
+ $keyword = un_htmlspecialchars($keyword);
+ $keyword = preg_replace_callback('~(&#(\d{1,7}|x[0-9a-fA-F]{1,6});)~', 'entity_fix__callback', strtr($keyword, array('\\\'' => '\'', '&' => '&')));
+ if (preg_match('~[\'\.,/@%&;:(){}\[\]_\-+\\\\]$~', $keyword) != 0 || preg_match('~^[\'\.,/@%&;:(){}\[\]_\-+\\\\]~', $keyword) != 0)
+ $force_partial_word = true;
+ $matchString .= strtr(preg_quote($keyword, '/'), array('\*' => '.+?')) . '|';
+ }
+ $matchString = un_htmlspecialchars(substr($matchString, 0, -1));
+
+ $body = un_htmlspecialchars(strtr($body, array(' ' => ' ', '
' => "\n", '[' => '[', ']' => ']', ':' => ':', '@' => '@')));
+
+ if ($this->_use_partial_words || $force_partial_word)
+ preg_match_all('/([^\s\W]{' . $charLimit . '}[\s\W]|[\s\W].{0,' . $charLimit . '}?|^)(' . $matchString . ')(.{0,' . $charLimit . '}[\s\W]|[^\s\W]{0,' . $charLimit . '})/isu', $body, $matches);
+ else
+ preg_match_all('/([^\s\W]{' . $charLimit . '}[\s\W]|[\s\W].{0,' . $charLimit . '}?[\s\W]|^)(' . $matchString . ')([\s\W].{0,' . $charLimit . '}[\s\W]|[\s\W][^\s\W]{0,' . $charLimit . '})/isu', $body, $matches);
+
+ $body = '';
+ foreach ($matches[0] as $match)
+ {
+ $match = strtr(htmlspecialchars($match, ENT_QUOTES, 'UTF-8'), array("\n" => ' '));
+ $body .= '…… ' . $match . ' ……';
+ }
+ }
+
+ // Re-fix the international characters.
+ $body = preg_replace_callback('~(&#(\d{1,7}|x[0-9a-fA-F]{1,6});)~', 'entity_fix__callback', $body);
+ }
+ return $body;
+ }
+}
\ No newline at end of file
diff --git a/sources/subs/MessagesCallback/BodyParser/Normal.php b/sources/subs/MessagesCallback/BodyParser/Normal.php
new file mode 100644
index 0000000000..348fd3d64d
--- /dev/null
+++ b/sources/subs/MessagesCallback/BodyParser/Normal.php
@@ -0,0 +1,80 @@
+_searchArray = $highlight;
+ $this->_use_partial_words = $use_partial_words;
+ $this->_highlight = !empty($highlight);
+ $this->_bbc_parser = \BBC\ParserWrapper::instance();
+ }
+
+ /**
+ * {@inheritdoc }
+ */
+ public function getSearchArray()
+ {
+ return $this->_searchArray;
+ }
+
+ /**
+ * {@inheritdoc }
+ */
+ public function prepare($body, $smileys_enabled)
+ {
+ $body = censor($body);
+
+ // Run BBC interpreter on the message.
+ return $this->_bbc_parser->parseMessage($body, $smileys_enabled);
+ }
+}
\ No newline at end of file
diff --git a/sources/subs/MessagesCallback/DisplayRenderer.php b/sources/subs/MessagesCallback/DisplayRenderer.php
new file mode 100644
index 0000000000..c88e70beb6
--- /dev/null
+++ b/sources/subs/MessagesCallback/DisplayRenderer.php
@@ -0,0 +1,119 @@
+_this_message['poster_time'] + $modSettings['edit_disable_time'] * 60 >= time()) && $this->_this_message['id_member'] == $user_info['id'];
+
+ // Have you liked this post, can you?
+ $this->_this_message['you_liked'] = !empty($context['likes'][$this->_this_message['id_msg']]['member'])
+ && isset($context['likes'][$this->_this_message['id_msg']]['member'][$user_info['id']]);
+ $this->_this_message['use_likes'] = allowedTo('like_posts') && empty($context['is_locked'])
+ && ($this->_this_message['id_member'] != $user_info['id'] || !empty($modSettings['likeAllowSelf']))
+ && (empty($modSettings['likeMinPosts']) ? true : $modSettings['likeMinPosts'] <= $user_info['posts']);
+ $this->_this_message['like_count'] = !empty($context['likes'][$this->_this_message['id_msg']]['count']) ? $context['likes'][$this->_this_message['id_msg']]['count'] : 0;
+ }
+
+ /**
+ * {@inheritdoc }
+ */
+ protected function _adjustAllMembers()
+ {
+ global $memberContext, $settings, $context;
+
+ $id_member = $this->_this_message[$this->_idx_mapper->id_member];
+
+ $memberContext[$id_member]['ip'] = $this->_this_message['poster_ip'] ?? '';
+ $memberContext[$id_member]['show_profile_buttons'] = $settings['show_profile_buttons'] && (!empty($memberContext[$id_member]['can_view_profile']) || (!empty($memberContext[$id_member]['website']['url']) && !isset($context['disabled_fields']['website'])) || (in_array($memberContext[$id_member]['show_email'], array('yes', 'yes_permission_override', 'no_through_forum'))) || $context['can_send_pm']);
+
+ $context['id_msg'] = $this->_this_message['id_msg'] ?? '';
+ }
+
+ /**
+ * {@inheritdoc }
+ */
+ protected function _buildOutputArray()
+ {
+ global $scripturl, $topic, $context, $modSettings, $user_info, $txt;
+
+ require_once(SUBSDIR . '/Attachments.subs.php');
+
+ $output = parent::_buildOutputArray();
+ $output += array(
+ 'attachment' => loadAttachmentContext($this->_this_message['id_msg']),
+ 'href' => $scripturl . '?topic=' . $topic . '.msg' . $this->_this_message['id_msg'] . '#msg' . $this->_this_message['id_msg'],
+ 'link' => '' . $this->_this_message['subject'] . '',
+ 'icon' => $this->_this_message['icon'],
+ 'icon_url' => $this->_options->icon_sources->{$this->_this_message['icon']},
+ 'modified' => array(
+ 'time' => standardTime($this->_this_message['modified_time']),
+ 'html_time' => htmlTime($this->_this_message['modified_time']),
+ 'timestamp' => forum_time(true, $this->_this_message['modified_time']),
+ 'name' => $this->_this_message['modified_name']
+ ),
+ 'new' => empty($this->_this_message['is_read']),
+ 'approved' => $this->_this_message['approved'],
+ 'first_new' => isset($context['start_from']) && $context['start_from'] == $this->_counter,
+ 'is_ignored' => !empty($modSettings['enable_buddylist']) && in_array($this->_this_message['id_member'], $context['user']['ignoreusers']),
+ 'is_message_author' => $this->_this_message['id_member'] == $user_info['id'],
+ 'can_approve' => !$this->_this_message['approved'] && $context['can_approve'],
+ 'can_unapprove' => !empty($modSettings['postmod_active']) && $context['can_approve'] && $this->_this_message['approved'],
+ 'can_modify' => (!$context['is_locked'] || allowedTo('moderate_board')) && (allowedTo('modify_any') || (allowedTo('modify_replies') && $context['user']['started']) || (allowedTo('modify_own') && $this->_this_message['id_member'] == $user_info['id'] && (empty($modSettings['edit_disable_time']) || !$this->_this_message['approved'] || $this->_this_message['poster_time'] + $modSettings['edit_disable_time'] * 60 > time()))),
+ 'can_remove' => allowedTo('delete_any') || (allowedTo('delete_replies') && $context['user']['started']) || (allowedTo('delete_own') && $this->_this_message['id_member'] == $user_info['id'] && (empty($modSettings['edit_disable_time']) || $this->_this_message['poster_time'] + $modSettings['edit_disable_time'] * 60 > time())),
+ 'can_like' => $this->_this_message['use_likes'] && !$this->_this_message['you_liked'],
+ 'can_unlike' => $this->_this_message['use_likes'] && $this->_this_message['you_liked'],
+ 'like_counter' => $this->_this_message['like_count'],
+ 'likes_enabled' => !empty($modSettings['likes_enabled']) && ($this->_this_message['use_likes'] || ($this->_this_message['like_count'] != 0)),
+ 'classes' => array(),
+ );
+
+ if (!empty($output['modified']['name']))
+ $output['modified']['last_edit_text'] = sprintf($txt['last_edit_by'], $output['modified']['time'], $output['modified']['name'], standardTime($output['modified']['timestamp']));
+
+ if (!empty($output['member']['karma']['allow']))
+ {
+ $output['member']['karma'] += array(
+ 'applaud_url' => $scripturl . '?action=karma;sa=applaud;uid=' . $output['member']['id'] . ';topic=' . $context['current_topic'] . '.' . $context['start'] . ';m=' . $output['id'] . ';' . $context['session_var'] . '=' . $context['session_id'],
+ 'smite_url' => $scripturl . '?action=karma;sa=smite;uid=' . $output['member']['id'] . ';topic=' . $context['current_topic'] . '.' . $context['start'] . ';m=' . $output['id'] . ';' . $context['session_var'] . '=' . $context['session_id']
+ );
+ }
+
+ return $output;
+ }
+}
\ No newline at end of file
diff --git a/sources/subs/MessagesCallback/PmRenderer.php b/sources/subs/MessagesCallback/PmRenderer.php
new file mode 100644
index 0000000000..127d13b7a0
--- /dev/null
+++ b/sources/subs/MessagesCallback/PmRenderer.php
@@ -0,0 +1,146 @@
+_idx_mapper = new ValuesContainer([
+ 'id_msg' => 'id_pm',
+ 'id_member' => 'id_member_from',
+ 'name' => 'from_name',
+ 'time' => 'msgtime',
+ ]);
+
+ $this->_temp_pm_selected = isset($_SESSION['pm_selected']) ? $_SESSION['pm_selected'] : array();
+ $_SESSION['pm_selected'] = array();
+ }
+
+ /**
+ * {@inheritdoc }
+ */
+ protected function _setupPermissions()
+ {
+ }
+
+ /**
+ * {@inheritdoc }
+ */
+ protected function _adjustGuestContext()
+ {
+ global $memberContext, $context;
+
+ parent::_adjustGuestContext();
+
+ // Sometimes the forum sends messages itself (Warnings are an example)
+ // in this case don't label it from a guest.
+ if ($this->_this_message['from_name'] === $context['forum_name'])
+ {
+ $memberContext[$this->_this_message['id_member_from']]['group'] = '';
+ }
+ $memberContext[$this->_this_message['id_member_from']]['email'] = '';
+ }
+
+ /**
+ * {@inheritdoc }
+ */
+ protected function _adjustAllMembers()
+ {
+ global $memberContext, $context, $settings;
+
+ $memberContext[$this->_this_message['id_member_from']]['show_profile_buttons'] = $settings['show_profile_buttons'] && (!empty($memberContext[$this->_this_message['id_member_from']]['can_view_profile']) || (!empty($memberContext[$this->_this_message['id_member_from']]['website']['url']) && !isset($context['disabled_fields']['website'])) || (in_array($memberContext[$this->_this_message['id_member_from']]['show_email'], array('yes', 'yes_permission_override', 'no_through_forum'))) || $context['can_send_pm']);
+ }
+
+ /**
+ * {@inheritdoc }
+ */
+ protected function _buildOutputArray()
+ {
+ global $recipients, $context, $user_info, $modSettings, $scripturl, $txt;
+
+ $id_pm = $this->_this_message['id_pm'];
+
+ $output = parent::_buildOutputArray();
+ $output += array(
+ 'recipients' => &$recipients[$id_pm],
+ 'number_recipients' => count($recipients[$id_pm]['to']),
+ 'labels' => &$context['message_labels'][$id_pm],
+ 'fully_labeled' => count($context['message_labels'][$id_pm]) == count($context['labels']),
+ 'is_replied_to' => &$context['message_replied'][$id_pm],
+ 'is_unread' => &$context['message_unread'][$id_pm],
+ 'is_selected' => !empty($this->_temp_pm_selected) && in_array($id_pm, $this->_temp_pm_selected),
+ 'is_message_author' => $this->_this_message['id_member_from'] == $user_info['id'],
+ 'can_report' => !empty($modSettings['enableReportPM']),
+ );
+
+ $context['additional_pm_drop_buttons'] = array();
+
+ // Can they report this message
+ if (!empty($output['can_report']) && $context['folder'] !== 'sent' && $output['member']['id'] != $user_info['id'])
+ {
+ $context['additional_pm_drop_buttons']['warn_button'] = array(
+ 'href' => $scripturl . '?action=pm;sa=report;l=' . $context['current_label_id'] . ';pmsg=' . $output['id'] . ';' . $context['session_var'] . '=' . $context['session_id'],
+ 'text' => $txt['pm_report_to_admin']
+ );
+ }
+
+ // Or mark it as unread
+ if (empty($output['is_unread']) && $context['folder'] !== 'sent' && $output['member']['id'] != $user_info['id'])
+ {
+ $context['additional_pm_drop_buttons']['restore_button'] = array(
+ 'href' => $scripturl . '?action=pm;sa=markunread;l=' . $context['current_label_id'] . ';pmsg=' . $output['id'] . ';' . $context['session_var'] . '=' . $context['session_id'],
+ 'text' => $txt['pm_mark_unread']
+ );
+ }
+
+ // Or give / take karma for a PM
+ if (!empty($output['member']['karma']['allow']))
+ {
+ $output['member']['karma'] += array(
+ 'applaud_url' => $scripturl . '?action=karma;sa=applaud;uid=' . $output['member']['id'] . ';f=' . $context['folder'] . ';start=' . $context['start'] . ($context['current_label_id'] != -1 ? ';l=' . $context['current_label_id'] : '') . ';pm=' . $output['id'] . ';' . $context['session_var'] . '=' . $context['session_id'],
+ 'smite_url' => $scripturl . '?action=karma;sa=smite;uid=' . $output['member']['id'] . ';f=' . $context['folder'] . ';start=' . $context['start'] . ($context['current_label_id'] != -1 ? ';l=' . $context['current_label_id'] : '') . ';pm=' . $output['id'] . ';' . $context['session_var'] . '=' . $context['session_id'],
+ );
+ }
+
+ return $output;
+ }
+}
\ No newline at end of file
diff --git a/sources/subs/MessagesCallback/Renderer.php b/sources/subs/MessagesCallback/Renderer.php
new file mode 100644
index 0000000000..b2ebb34c7e
--- /dev/null
+++ b/sources/subs/MessagesCallback/Renderer.php
@@ -0,0 +1,332 @@
+_dbRequest = $request;
+ $this->_bodyParser = $bodyParser;
+ $this->_db = database();
+ $this->_idx_mapper = new ValuesContainer([
+ 'id_msg' => 'id_msg',
+ 'id_member' => 'id_member',
+ 'name' => 'poster_name',
+ 'time' => 'poster_time',
+ ]);
+
+ // opt:
+ // icon_sources
+ // show_signatures
+ if ($opt === null)
+ {
+ $this->_options = new ValuesContainer();
+ }
+ else
+ {
+ $this->_options = $opt;
+ }
+ }
+
+ /**
+ * The main part: reads the DB resource and returns the complex array
+ * for a certain message.
+ *
+ * @param bool $reset
+ *
+ * @return bool|mixed[]
+ */
+ public function getContext($reset = false)
+ {
+ global $settings, $txt, $modSettings, $scripturl, $user_info;
+ global $memberContext, $context, $topic;
+ static $counter = null;
+
+ // If the query returned false, bail.
+ if ($this->_dbRequest === false)
+ {
+ return false;
+ }
+
+ // Remember which message this is. (ie. reply #83)
+ if ($this->_counter === null || $reset === true)
+ {
+ $this->_counter = $context['start'];
+ }
+
+ // Start from the beginning...
+ if ($reset === true)
+ {
+ $this->_currentContext($reset);
+ }
+ // Attempt to get the next message.
+ else
+ {
+ $this->_currentContext();
+ }
+
+ if (empty($this->_this_message))
+ {
+ return false;
+ }
+
+ // If you're a lazy bum, you probably didn't give a subject...
+ $this->_this_message['subject'] = $this->_this_message['subject'] != '' ? $this->_this_message['subject'] : $txt['no_subject'];
+
+ $this->_setupPermissions();
+
+ $id_member = $this->_this_message[$this->_idx_mapper->id_member];
+
+ // If it couldn't load, or the user was a guest.... someday may be done with a guest table.
+ if (!loadMemberContext($id_member, true))
+ {
+ $this->_adjustGuestContext();
+ }
+ else
+ {
+ $this->_adjustMemberContext();
+ }
+ $this->_adjustAllMembers();
+
+ // Do the censor thang.
+ $this->_this_message['subject'] = censor($this->_this_message['subject']);
+
+ // Run BBC interpreter on the message.
+ $this->_this_message['body'] = $this->_bodyParser->prepare($this->_this_message['body'], $this->_this_message['smileys_enabled']);
+
+ call_integration_hook(static::BEFORE_PREPARE_HOOK, array(&$this->_this_message));
+
+ // Compose the memory eat- I mean message array.
+ $output = $this->_buildOutputArray();
+
+ call_integration_hook(static::CONTEXT_HOOK, array(&$output, &$this->_this_message, $this->_counter));
+
+ $output['classes'] = implode(' ', $output['classes']);
+
+ $this->_counter++;
+
+ return $output;
+ }
+
+ /**
+ * This function receives a request handle and attempts to retrieve the next result.
+ *
+ * What it does:
+ *
+ * - It is used by the controller callbacks from the template, such as
+ * posts in topic display page, posts search results page, or personal messages.
+ *
+ * @param bool $reset
+ *
+ * @return boolean
+ */
+ protected function _currentContext($reset = false)
+ {
+ // Start from the beginning...
+ if ($reset)
+ {
+ $this->_db->data_seek($this->_dbRequest, 0);
+ }
+
+ // If the query has already returned false, get out of here
+ if (empty($this->_dbRequest))
+ {
+ return false;
+ }
+
+ // Attempt to get the next message.
+ $this->_this_message = $this->_db->fetch_assoc($this->_dbRequest);
+ if (!$this->_this_message)
+ {
+ $this->_db->free_result($this->_dbRequest);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Utility function, it shall be implemented by the extending class.
+ * Run just before loadMemberContext is executed.
+ */
+ abstract protected function _setupPermissions();
+
+ /**
+ * Utility function, it can be overridden to alter something just after the
+ * members' data have been loaded from the database.
+ * Run only if loadMemberContext succeeded.
+ */
+ protected function _adjustGuestContext()
+ {
+ global $memberContext, $txt;
+
+ $member_id = $this->_this_message[$this->_idx_mapper->id_member];
+
+ // Notice this information isn't used anywhere else....
+ $memberContext[$member_id]['name'] = $this->_this_message[$this->_idx_mapper->name];
+ $memberContext[$member_id]['id'] = 0;
+ $memberContext[$member_id]['group'] = $txt['guest_title'];
+ $memberContext[$member_id]['link'] = $this->_this_message[$this->_idx_mapper->name];
+ $memberContext[$member_id]['email'] = $this->_this_message['poster_email'] ?? '';
+ $memberContext[$member_id]['show_email'] = showEmailAddress(true, 0);
+ $memberContext[$member_id]['is_guest'] = true;
+ }
+
+ /**
+ * Utility function, it can be overridden to alter something just after the
+ * members data have been set.
+ * Run only if loadMemberContext failed.
+ */
+ protected function _adjustMemberContext()
+ {
+ global $memberContext, $txt, $user_info, $context, $modSettings;
+
+ $member_id = $this->_this_message[$this->_idx_mapper->id_member];
+
+ $memberContext[$member_id]['can_view_profile'] = allowedTo('profile_view_any') || ($member_id == $user_info['id'] && allowedTo('profile_view_own'));
+ $memberContext[$member_id]['is_topic_starter'] = $member_id == $context['topic_starter_id'];
+ $memberContext[$member_id]['can_see_warning'] = !isset($context['disabled_fields']['warning_status']) && $memberContext[$member_id]['warning_status'] && (!empty($context['user']['can_mod']) || (!$user_info['is_guest'] && !empty($modSettings['warning_show']) && ($modSettings['warning_show'] > 1 || $member_id == $user_info['id'])));
+
+ if ($this->_options->show_signatures === 1)
+ {
+ if (empty($this->_signature_shown[$member_id]))
+ {
+ $this->_signature_shown[$member_id] = true;
+ }
+ else
+ {
+ $memberContext[$member_id]['signature'] = '';
+ }
+ }
+ elseif ($this->_options->show_signatures === 2)
+ {
+ $memberContext[$member_id]['signature'] = '';
+ }
+ }
+
+ /**
+ * Utility function, it can be overridden to alter something just after either
+ * the members or the guests data have been loaded from the database.
+ * Run both if loadMemberContext succeeded or failed.
+ */
+ protected function _adjustAllMembers()
+ {
+ }
+
+ /**
+ * The most important bit that differentiate the various implementations.
+ * It is supposed to prepare the $output array with all the information
+ * needed by the template to properly render the message.
+ *
+ * The method of the class extending this abstract may run
+ * parent::_buildOutputArray()
+ * as first statement in order to have a starting point and
+ * some commonly used content for the array.
+ *
+ * @return mixed[]
+ */
+ protected function _buildOutputArray()
+ {
+ global $user_info, $memberContext;
+
+ return array(
+ 'alternate' => $this->_counter % 2,
+ 'id' => $this->_this_message[$this->_idx_mapper->id_msg],
+ 'member' => &$memberContext[$this->_this_message[$this->_idx_mapper->id_member]],
+ 'subject' => $this->_this_message['subject'],
+ 'html_time' => htmlTime($this->_this_message[$this->_idx_mapper->time]),
+ 'time' => standardTime($this->_this_message[$this->_idx_mapper->time]),
+ 'timestamp' => forum_time(true, $this->_this_message[$this->_idx_mapper->time]),
+ 'counter' => $this->_counter,
+ 'body' => $this->_this_message['body'],
+ 'can_see_ip' => allowedTo('moderate_forum') || ($this->_this_message[$this->_idx_mapper->id_member] == $user_info['id'] && !empty($user_info['id'])),
+ 'classes' => array()
+ );
+
+ }
+}
\ No newline at end of file
diff --git a/sources/subs/MessagesCallback/SearchRenderer.php b/sources/subs/MessagesCallback/SearchRenderer.php
new file mode 100644
index 0000000000..59de594e2c
--- /dev/null
+++ b/sources/subs/MessagesCallback/SearchRenderer.php
@@ -0,0 +1,234 @@
+_participants = $participants;
+ }
+
+ /**
+ * {@inheritdoc }
+ */
+ protected function _setupPermissions()
+ {
+ global $txt;
+
+ $this->_this_message['first_subject'] = $this->_this_message['first_subject'] != '' ? $this->_this_message['first_subject'] : $txt['no_subject'];
+ $this->_this_message['last_subject'] = $this->_this_message['last_subject'] != '' ? $this->_this_message['last_subject'] : $txt['no_subject'];
+ }
+
+ /**
+ * {@inheritdoc }
+ */
+ protected function _adjustMemberContext()
+ {
+ }
+
+ /**
+ * {@inheritdoc }
+ */
+ protected function _adjustAllMembers()
+ {
+ global $memberContext;
+
+ $memberContext[$this->_this_message['id_member']]['ip'] = $this->_this_message['poster_ip'];
+
+ $this->_this_message['first_subject'] = censor($this->_this_message['first_subject']);
+ $this->_this_message['last_subject'] = censor($this->_this_message['last_subject']);
+ }
+
+ /**
+ * {@inheritdoc }
+ */
+ protected function _buildOutputArray()
+ {
+ global $modSettings, $context, $scripturl, $options, $user_info, $memberContext, $txt;
+ global $boards_can;
+
+ // Make sure we don't end up with a practically empty message body.
+ $this->_this_message['body'] = preg_replace('~^(?: )+$~', '', $this->_this_message['body']);
+
+ // Do we have quote tag enabled?
+ $quote_enabled = empty($modSettings['disabledBBC']) || !in_array('quote', explode(',', $modSettings['disabledBBC']));
+
+ $output_pre = \Topic_Util::prepareContext(array($this->_this_message))[$this->_this_message['id_topic']];
+
+ $output = array_merge($context['topics'][$this->_this_message['id_msg']], $output_pre);
+
+ $output['posted_in'] = !empty($this->_participants[$this->_this_message['id_topic']]);
+ $output['tests'] = array(
+ 'can_reply' => in_array($this->_this_message['id_board'], $boards_can['post_reply_any']) || in_array(0, $boards_can['post_reply_any']),
+ 'can_quote' => (in_array($this->_this_message['id_board'], $boards_can['post_reply_any']) || in_array(0, $boards_can['post_reply_any'])) && $quote_enabled,
+ 'can_mark_notify' => in_array($this->_this_message['id_board'], $boards_can['mark_any_notify']) || in_array(0, $boards_can['mark_any_notify']) && !$context['user']['is_guest'],
+ );
+ $output['board'] = array(
+ 'id' => $this->_this_message['id_board'],
+ 'name' => $this->_this_message['bname'],
+ 'href' => $scripturl . '?board=' . $this->_this_message['id_board'] . '.0',
+ 'link' => '' . $this->_this_message['bname'] . ''
+ );
+ $output['category'] = array(
+ 'id' => $this->_this_message['id_cat'],
+ 'name' => $this->_this_message['cat_name'],
+ 'href' => $scripturl . $modSettings['default_forum_action'] . '#c' . $this->_this_message['id_cat'],
+ 'link' => '' . $this->_this_message['cat_name'] . ''
+ );
+
+ determineTopicClass($output);
+
+ if ($output['posted_in'])
+ {
+ $output['class'] = 'my_' . $output['class'];
+ }
+
+ $body_highlighted = $this->_this_message['body'];
+ $subject_highlighted = $this->_this_message['subject'];
+
+ if (!empty($options['display_quick_mod']))
+ {
+ $started = $output['first_post']['member']['id'] == $user_info['id'];
+
+ $output['quick_mod'] = array(
+ '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']))),
+ 'sticky' => (in_array(0, $boards_can['make_sticky']) || in_array($output['board']['id'], $boards_can['make_sticky'])),
+ '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']))),
+ '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']))),
+ );
+
+ $context['can_lock'] |= $output['quick_mod']['lock'];
+ $context['can_sticky'] |= $output['quick_mod']['sticky'];
+ $context['can_move'] |= $output['quick_mod']['move'];
+ $context['can_remove'] |= $output['quick_mod']['remove'];
+ $context['can_merge'] |= in_array($output['board']['id'], $boards_can['merge_any']);
+ $context['can_markread'] = $context['user']['is_logged'];
+
+ $context['qmod_actions'] = array('remove', 'lock', 'sticky', 'move', 'markread');
+ call_integration_hook('integrate_quick_mod_actions_search');
+ }
+
+ foreach ($this->_bodyParser->getSearchArray() as $query)
+ {
+
+ // Fix the international characters in the keyword too.
+ $query = un_htmlspecialchars($query);
+ $query = trim($query, '\*+');
+ $query = strtr(\Util::htmlspecialchars($query), array('\\\'' => '\''));
+
+ $body_highlighted = preg_replace_callback('/((<[^>]*)|' . preg_quote(strtr($query, array('\'' => ''')), '/') . ')/iu', array($this, '_highlighted_callback'), $body_highlighted);
+ $subject_highlighted = preg_replace('/(' . preg_quote($query, '/') . ')/iu', '$1', $subject_highlighted);
+ }
+
+ $output['matches'][] = array(
+ 'id' => $this->_this_message['id_msg'],
+ 'attachment' => loadAttachmentContext($this->_this_message['id_msg']),
+ 'alternate' => $this->_counter % 2,
+ 'member' => &$memberContext[$this->_this_message['id_member']],
+ 'icon' => $this->_this_message['icon'],
+ 'icon_url' => $this->_options->icon_sources->{$this->_this_message['icon']},
+ 'subject' => $this->_this_message['subject'],
+ 'subject_highlighted' => $subject_highlighted,
+ 'time' => standardTime($this->_this_message['poster_time']),
+ 'html_time' => htmlTime($this->_this_message['poster_time']),
+ 'timestamp' => forum_time(true, $this->_this_message['poster_time']),
+ 'counter' => $this->_counter,
+ 'modified' => array(
+ 'time' => standardTime($this->_this_message['modified_time']),
+ 'html_time' => htmlTime($this->_this_message['modified_time']),
+ 'timestamp' => forum_time(true, $this->_this_message['modified_time']),
+ 'name' => $this->_this_message['modified_name']
+ ),
+ 'body' => $this->_this_message['body'],
+ 'body_highlighted' => $body_highlighted,
+ 'start' => 'msg' . $this->_this_message['id_msg']
+ );
+
+ if (!$context['compact'])
+ {
+ $output['buttons'] = array(
+ // Can we request notification of topics?
+ 'notify' => array(
+ 'href' => $scripturl . '?action=notify;topic=' . $output['id'] . '.msg' . $this->_this_message['id_msg'],
+ 'text' => $txt['notify'],
+ 'test' => 'can_mark_notify',
+ ),
+ // If they *can* reply?
+ 'reply' => array(
+ 'href' => $scripturl . '?action=post;topic=' . $output['id'] . '.msg' . $this->_this_message['id_msg'],
+ 'text' => $txt['reply'],
+ 'test' => 'can_reply',
+ ),
+ // If they *can* quote?
+ 'quote' => array(
+ 'href' => $scripturl . '?action=post;topic=' . $output['id'] . '.msg' . $this->_this_message['id_msg'] . ';quote=' . $this->_this_message['id_msg'],
+ 'text' => $txt['quote'],
+ 'test' => 'can_quote',
+ ),
+ );
+ }
+
+ return $output;
+ }
+
+ /**
+ * Used to highlight body text with strings that match the search term
+ *
+ * Callback function used in $body_highlighted
+ *
+ * @param string[] $matches
+ *
+ * @return string
+ */
+ private function _highlighted_callback($matches)
+ {
+ return isset($matches[2]) && $matches[2] == $matches[1] ? stripslashes($matches[1]) : '' . $matches[1] . '';
+ }
+}
\ No newline at end of file
diff --git a/sources/subs/PersonalMessage.subs.php b/sources/subs/PersonalMessage.subs.php
index 7ede0fb9b3..f2793144c1 100644
--- a/sources/subs/PersonalMessage.subs.php
+++ b/sources/subs/PersonalMessage.subs.php
@@ -83,7 +83,8 @@ function loadPMLabels($labels)
'not_deleted' => 0,
)
);
- $labels = array();
+
+// $labels = array();
while ($row = $db->fetch_assoc($result))
{
$this_labels = explode(',', $row['labels']);
@@ -1973,7 +1974,8 @@ function loadPMSubjectRequest($pms, $orderBy)
$subjects_request = $db->query('', '
SELECT
pm.id_pm, pm.subject, pm.id_member_from, pm.msgtime, COALESCE(mem.real_name, pm.from_name) AS from_name,
- COALESCE(mem.id_member, 0) AS not_guest
+ COALESCE(mem.id_member, 0) AS not_guest,
+ {string:empty} as body, {int:smileys_enabled} as smileys_enabled
FROM {db_prefix}personal_messages AS pm
LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = pm.id_member_from)
WHERE pm.id_pm IN ({array_int:pm_list})
@@ -1981,6 +1983,9 @@ function loadPMSubjectRequest($pms, $orderBy)
LIMIT ' . count($pms),
array(
'pm_list' => $pms,
+ 'empty' => '',
+ 'smileys_enabled' => 1,
+ 'from_time' => 0,
)
);
@@ -2006,7 +2011,8 @@ function loadPMMessageRequest($display_pms, $sort_by_query, $sort_by, $descendin
$messages_request = $db->query('', '
SELECT
- pm.id_pm, pm.subject, pm.id_member_from, pm.body, pm.msgtime, pm.from_name
+ pm.id_pm, pm.subject, pm.id_member_from, pm.body, pm.msgtime, pm.from_name,
+ {int:smileys_enabled} as smileys_enabled
FROM {db_prefix}personal_messages AS pm' . ($folder == 'sent' ? '
LEFT JOIN {db_prefix}pm_recipients AS pmr ON (pmr.id_pm = pm.id_pm)' : '') . ($sort_by == 'name' ? '
LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = {raw:id_member})' : '') . '
@@ -2017,6 +2023,7 @@ function loadPMMessageRequest($display_pms, $sort_by_query, $sort_by, $descendin
array(
'display_pms' => $display_pms,
'id_member' => $folder == 'sent' ? 'pmr.id_member' : 'pm.id_member_from',
+ 'smileys_enabled' => 1,
)
);
diff --git a/sources/subs/Post.subs.php b/sources/subs/Post.subs.php
index 29802a6576..07f348ddba 100644
--- a/sources/subs/Post.subs.php
+++ b/sources/subs/Post.subs.php
@@ -380,10 +380,8 @@ function createPost(&$msgOptions, &$topicOptions, &$posterOptions)
}
// If there's a custom search index, it may need updating...
- $search = new \ElkArte\Search\Search;
- $searchAPI = $search->findSearchAPI();
- if (is_callable(array($searchAPI, 'postCreated')))
- $searchAPI->postCreated($msgOptions, $topicOptions, $posterOptions);
+ $searchAPI = new \ElkArte\Search\SearchApiWrapper(!empty($modSettings['search_index']) ? $modSettings['search_index'] : '');
+ $searchAPI->postCreated($msgOptions, $topicOptions, $posterOptions);
// Increase the post counter for the user that created the post.
if (!empty($posterOptions['update_post_count']) && !empty($posterOptions['id']) && $msgOptions['approved'])
@@ -536,10 +534,8 @@ function modifyPost(&$msgOptions, &$topicOptions, &$posterOptions)
}
// If there's a custom search index, it needs to be modified...
- $search = new \ElkArte\Search\Search;
- $searchAPI = $search->findSearchAPI();
- if (is_callable(array($searchAPI, 'postModified')))
- $searchAPI->postModified($msgOptions, $topicOptions, $posterOptions);
+ $searchAPI = new \ElkArte\Search\SearchApiWrapper(!empty($modSettings['search_index']) ? $modSettings['search_index'] : '');
+ $searchAPI->postModified($msgOptions, $topicOptions, $posterOptions);
if (isset($msgOptions['subject']))
{
diff --git a/sources/subs/Search/API/Custom.php b/sources/subs/Search/API/Custom.php
index 3cbcd8ed1b..5d94c97a73 100644
--- a/sources/subs/Search/API/Custom.php
+++ b/sources/subs/Search/API/Custom.php
@@ -23,7 +23,7 @@
*
* @package Search
*/
-class Custom extends SearchAPI
+class Custom extends Standard
{
/**
*This is the last version of ElkArte that this was tested on, to protect against API changes.
@@ -52,7 +52,7 @@ class Custom extends SearchAPI
/**
* Custom::__construct()
*/
- public function __construct()
+ public function __construct($config, $searchParams)
{
global $modSettings;
@@ -66,6 +66,7 @@ public function __construct()
if (empty($modSettings['search_custom_index_config']))
return;
+ parent::__construct($config, $searchParams);
$this->indexSettings = \Util::unserialize($modSettings['search_custom_index_config']);
$this->bannedWords = empty($modSettings['search_stopwords']) ? array() : explode(',', $modSettings['search_stopwords']);
@@ -101,14 +102,9 @@ public function searchSort($a, $b)
}
/**
- * Do we have to do some work with the words we are searching for to prepare them?
- *
- * @param string $word A word to index
- * @param mixed[] $wordsSearch The Search words
- * @param string[] $wordsExclude Words to exclude
- * @param boolean $isExcluded
+ * {@inheritdoc }
*/
- public function prepareIndexes($word, &$wordsSearch, &$wordsExclude, $isExcluded)
+ public function prepareIndexes($word, &$wordsSearch, &$wordsExclude, $isExcluded, $excludedSubjectWords)
{
global $modSettings;
@@ -134,6 +130,11 @@ public function prepareIndexes($word, &$wordsSearch, &$wordsExclude, $isExcluded
}
}
+ public function useWordIndex()
+ {
+ return true;
+ }
+
/**
* Search for indexed words.
*
diff --git a/sources/subs/Search/API/Fulltext.php b/sources/subs/Search/API/Fulltext.php
index 1b2a42e541..910e20938f 100644
--- a/sources/subs/Search/API/Fulltext.php
+++ b/sources/subs/Search/API/Fulltext.php
@@ -22,7 +22,7 @@
*
* @package Search
*/
-class Fulltext extends SearchAPI
+class Fulltext extends Standard
{
/**
* This is the last version of ElkArte that this was tested on, to protect against API changes.
@@ -63,7 +63,7 @@ class Fulltext extends SearchAPI
/**
* Fulltext::__construct()
*/
- public function __construct()
+ public function __construct($config, $searchParams)
{
global $modSettings;
@@ -75,6 +75,7 @@ public function __construct()
return;
}
+ parent::__construct($config, $searchParams);
$this->bannedWords = empty($modSettings['search_banned_words']) ? array() : explode(',', $modSettings['search_banned_words']);
$this->min_word_length = $this->_getMinWordLength();
}
@@ -133,16 +134,9 @@ public function searchSort($a, $b)
}
/**
- * Fulltext::prepareIndexes()
- *
- * Do we have to do some work with the words we are searching for to prepare them?
- *
- * @param string $word A word to index
- * @param mixed[] $wordsSearch The Search words
- * @param string[] $wordsExclude Words to exclude
- * @param boolean $isExcluded If the $wordsSearch are those to exclude
+ * {@inheritdoc }
*/
- public function prepareIndexes($word, &$wordsSearch, &$wordsExclude, $isExcluded)
+ public function prepareIndexes($word, &$wordsSearch, &$wordsExclude, $isExcluded, $excludedSubjectWords)
{
global $modSettings;
@@ -178,6 +172,11 @@ public function prepareIndexes($word, &$wordsSearch, &$wordsExclude, $isExcluded
}
}
+ public function useWordIndex()
+ {
+ return true;
+ }
+
/**
* Fulltext::indexedWordQuery()
*
diff --git a/sources/subs/Search/API/SearchAPI.php b/sources/subs/Search/API/SearchAPI.php
index 3cb577e2b5..1d2c6fa646 100644
--- a/sources/subs/Search/API/SearchAPI.php
+++ b/sources/subs/Search/API/SearchAPI.php
@@ -44,6 +44,12 @@ abstract class SearchAPI
*/
protected $_excludedWords = array();
+ /**
+ *
+ * @var int
+ */
+ protected $_num_results = 0;
+
/**
* What words are banned?
* @var array
@@ -57,26 +63,108 @@ abstract class SearchAPI
protected $min_word_length = 3;
/**
- * What databases support the custom index?
+ * All the search configurations
+ * @var \ElkArte\ValuesContainer
+ */
+ protected $config = null;
+
+ /**
+ *
+ * @var null|\ElkArte\Search\SearchParams
+ */
+ protected $_searchParams = null;
+
+ /**
+ * The weights to associate to various areas for relevancy
+ * @var array
+ */
+ protected $_weight_factors = array();
+
+ /**
+ * Weighing factor each area, ie frequency, age, sticky, etc
+ * @var array
+ */
+ protected $_weight = array();
+
+ /**
+ * The sum of the _weight_factors, normally but not always 100
+ * @var int
+ */
+ protected $_weight_total = 0;
+
+ /**
+ * If we are creating a tmp db table
+ * @var bool
+ */
+ protected $_createTemporary = true;
+
+ /**
+ * Builds the array of words for use in the db query
+ * @var array
+ */
+ protected $_searchWords = array();
+
+ /**
+ * Phrases not to be found in the search results (-"some phrase")
+ * @var array
+ */
+ protected $_excludedPhrases = array();
+
+ /**
+ * Database instance
+ * @var \Database|null
+ */
+ protected $_db = null;
+
+ /**
+ * Search db instance
+ * @var \DbSearch|null
+ */
+ protected $_db_search = null;
+
+ /**
+ * Words excluded from indexes
+ * @var array
+ */
+ protected $_excludedIndexWords = array();
+
+ /**
+ * Words not be be found in the subject (-word)
+ * @var array
+ */
+ protected $_excludedSubjectWords = array();
+
+ /**
+ * Holds the words and phrases to be searched on
+ * @var \ElkArte\Search\SearchArray
+ */
+ protected $_searchArray = null;
+
+ /**
+ * What databases do we support? (In general.)
* @var array
*/
protected $supported_databases = array('MySQL', 'PostgreSQL');
/**
- * Fulltext::__construct()
+ * __construct()
*/
- public function __construct()
+ public function __construct($config, $searchParams)
{
- global $modSettings;
+ $this->config = $config;
+ $this->_searchParams = $searchParams;
- $this->bannedWords = empty($modSettings['search_banned_words']) ? array() : explode(',', $modSettings['search_banned_words']);
+ $this->bannedWords = $config->banned_words;
$this->min_word_length = $this->_getMinWordLength();
+
+ $this->_db = database();
+ $this->_db_search = db_search();
}
/**
- * Fulltext::_getMinWordLength()
+ * What is a sensible minimum word length?
*
- * What is the minimum word length full text supports?
+ * @return int
*/
protected function _getMinWordLength()
{
@@ -85,6 +173,8 @@ protected function _getMinWordLength()
/**
* If the settings don't exist we can't continue.
+ *
+ * @return bool
*/
public function isValid()
{
@@ -102,6 +192,105 @@ public function setExcludedWords($words)
$this->_excludedWords = $words;
}
+ /**
+ * Adds the excluded phrases list
+ *
+ * @param string[] $phrases An array of phrases to exclude
+ */
+ public function setExcludedPhrases($phrases)
+ {
+ $this->_excludedPhrases = $phrases;
+ }
+
+ /**
+ * Sets the SearchArray... heck if I know what it is.
+ *
+ * @param \ElkArte\Search\SearchArray $searchArray
+ */
+ public function setSearchArray(\ElkArte\Search\SearchArray $searchArray)
+ {
+ $this->_searchArray = $searchArray;
+ }
+
+ /**
+ * If we use a temporary table or not
+ *
+ * @param bool $use
+ */
+ public function useTemporary($use = false)
+ {
+ $this->_createTemporary = $use;
+ }
+
+ /**
+ * Adds the weight factors
+ *
+ * @param \ElkArte\Search\WeightFactors $weights
+ */
+ public function setWeightFactors(\ElkArte\Search\WeightFactors $weights)
+ {
+ $this->_weight_factors = $weights->getFactors();
+
+ $this->_weight = $weights->getWeight();
+
+ $this->_weight_total = $weights->getTotal();
+ }
+
+ /**
+ * Number of results?
+ *
+ * @return int
+ */
+ public function getNumResults()
+ {
+ return $this->_num_results;
+ }
+
+ /**
+ * Callback function for usort used to sort the fulltext results.
+ *
+ * - In the standard search ordering is not needed, so only 0 is returned.
+ *
+ * @param string $a Word A
+ * @param string $b Word B
+ *
+ * @return int An integer indicating how the words should be sorted (-1, 0 1)
+ */
+ public function searchSort($a, $b)
+ {
+ return 0;
+ }
+
+ /**
+ * Prepares the indexes
+ *
+ * @param string $word
+ * @param string $wordsSearch
+ * @param string $wordsExclude
+ * @param string $isExcluded
+ * @param string $excludedSubjectWords
+ */
+ public function prepareIndexes($word, &$wordsSearch, &$wordsExclude, $isExcluded, $excludedSubjectWords)
+ {
+ }
+
+ /**
+ * If the current API can use a word index.
+ *
+ * @return bool
+ */
+ abstract public function useWordIndex();
+
+ /**
+ * Search for indexed words.
+ *
+ * @param mixed[] $words An array of words
+ * @param mixed[] $search_data An array of search data
+ *
+ * @return resource
+ */
+ abstract public function indexedWordQuery($words, $search_data);
+
/**
* Escape words passed by the client
*
diff --git a/sources/subs/Search/API/Sphinx.php b/sources/subs/Search/API/Sphinx.php
index 41e530bac2..5aca15f0fc 100644
--- a/sources/subs/Search/API/Sphinx.php
+++ b/sources/subs/Search/API/Sphinx.php
@@ -70,7 +70,7 @@ class Sphinx extends SearchAPI
/**
* Check we support this db, set banned words
*/
- public function __construct()
+ public function __construct($config, $searchParams)
{
// Is this database supported?
if (!in_array(DB_TYPE, $this->supported_databases))
@@ -80,7 +80,7 @@ public function __construct()
return;
}
- parent::__construct();
+ parent::__construct($config, $searchParams);
}
/**
@@ -112,14 +112,16 @@ public function searchSort($a, $b)
}
/**
- * Do we have to do some work with the words we are searching for to prepare them?
- *
- * @param string Word(s) to index
- * @param mixed[] $wordsSearch The Search words
- * @param string[] $wordsExclude Words to exclude
- * @param boolean $isExcluded
+ * {@inheritdoc }
+ */
+ public function indexedWordQuery($words, $search_data)
+ {
+ }
+
+ /**
+ * {@inheritdoc }
*/
- public function prepareIndexes($word, &$wordsSearch, &$wordsExclude, $isExcluded)
+ public function prepareIndexes($word, &$wordsSearch, &$wordsExclude, $isExcluded, $excludedSubjectWords)
{
$subwords = text2words($word, null, false);
@@ -132,21 +134,13 @@ public function prepareIndexes($word, &$wordsSearch, &$wordsExclude, $isExcluded
}
/**
- * This has it's own custom search.
- *
- * @param mixed[] $search_params
- * @param mixed[] $search_words
- * @param string[] $excluded_words
- * @param int[] $participants
- * @param string[] $search_results
- *
- * @return int|mixed
+ * {@inheritdoc }
*/
- public function searchQuery($search_params, $search_words, $excluded_words, &$participants, &$search_results)
+ public function searchQuery($search_words, $excluded_words, &$participants, &$search_results)
{
global $user_info, $context, $modSettings;
- if (!$search_params['subject_only'])
+ if (!$this->_searchParams->subject_only)
{
return 0;
}
@@ -164,17 +158,17 @@ public function searchQuery($search_params, $search_words, $excluded_words, &$pa
$mySphinx->SetLimits(0, (int) $modSettings['sphinx_max_results'], (int) $modSettings['sphinx_max_results']);
// Put together a sort string; besides the main column sort (relevance, id_topic, or num_replies),
- $search_params['sort_dir'] = strtoupper($search_params['sort_dir']);
- $sphinx_sort = $search_params['sort'] === 'id_msg' ? 'id_topic' : $search_params['sort'];
+ $this->_searchParams->sort_dir = strtoupper($this->_searchParams->sort_dir);
+ $sphinx_sort = $this->_searchParams->sort === 'id_msg' ? 'id_topic' : $this->_searchParams->sort;
// Add secondary sorting based on relevance value (if not the main sort method) and age
- $sphinx_sort .= ' ' . $search_params['sort_dir'] . ($search_params['sort'] === 'relevance' ? '' : ', relevance DESC') . ', poster_time DESC';
+ $sphinx_sort .= ' ' . $this->_searchParams->sort_dir . ($this->_searchParams->sort === 'relevance' ? '' : ', relevance DESC') . ', poster_time DESC';
// Include the engines weight values in the group sort
- $sphinx_sort = str_replace('relevance ', '@weight ' . $search_params['sort_dir'] . ', relevance ', $sphinx_sort);
+ $sphinx_sort = str_replace('relevance ', '@weight ' . $this->_searchParams->sort_dir . ', relevance ', $sphinx_sort);
// Grouping by topic id makes it return only one result per topic, so don't set that for in-topic searches
- if (empty($search_params['topic']))
+ if (empty($this->_searchParams->topic))
{
$mySphinx->SetGroupBy('id_topic', SPH_GROUPBY_ATTR, $sphinx_sort);
}
@@ -186,24 +180,24 @@ public function searchQuery($search_params, $search_words, $excluded_words, &$pa
$mySphinx->SetFieldWeights(array('subject' => !empty($modSettings['search_weight_subject']) ? $modSettings['search_weight_subject'] * 10 : 100, 'body' => 100));
// Set the limits based on the search parameters.
- if (!empty($search_params['min_msg_id']) || !empty($search_params['max_msg_id']))
+ if (!empty($this->_searchParams->min_msg_id) || !empty($this->_searchParams->max_msg_id))
{
- $mySphinx->SetIDRange($search_params['min_msg_id'], empty($search_params['max_msg_id']) ? (int) $modSettings['maxMsgID'] : $search_params['max_msg_id']);
+ $mySphinx->SetIDRange($this->_searchParams->min_msg_id, empty($this->_searchParams->max_msg_id) ? (int) $modSettings['maxMsgID'] : $this->_searchParams->max_msg_id);
}
- if (!empty($search_params['topic']))
+ if (!empty($this->_searchParams->topic))
{
- $mySphinx->SetFilter('id_topic', array((int) $search_params['topic']));
+ $mySphinx->SetFilter('id_topic', array((int) $this->_searchParams->topic));
}
- if (!empty($search_params['brd']))
+ if (!empty($this->_searchParams->brd))
{
- $mySphinx->SetFilter('id_board', $search_params['brd']);
+ $mySphinx->SetFilter('id_board', $this->_searchParams->brd);
}
- if (!empty($search_params['memberlist']))
+ if (!empty($this->_searchParams->_memberlist))
{
- $mySphinx->SetFilter('id_member', $search_params['memberlist']);
+ $mySphinx->SetFilter('id_member', $this->_searchParams->_memberlist);
}
// Construct the (binary mode & |) query while accounting for excluded words
@@ -231,7 +225,7 @@ public function searchQuery($search_params, $search_words, $excluded_words, &$pa
$query = count($orResults) === 1 ? $orResults[0] : '(' . implode(') | (', $orResults) . ')';
// Subject only searches need to be specified.
- if ($search_params['subject_only'])
+ if ($this->_searchParams->subject_only)
{
$query = '@(subject) ' . $query;
}
@@ -240,7 +234,7 @@ public function searchQuery($search_params, $search_words, $excluded_words, &$pa
$mode = SPH_MATCH_ALL;
// Over two words and searching for any (since we build a binary string, this will never get set)
- if (substr_count($query, ' ') > 1 && (!empty($search_params['searchtype']) && $search_params['searchtype'] == 2))
+ if (substr_count($query, ' ') > 1 && (!empty($this->_searchParams->searchtype) && $this->_searchParams->searchtype == 2))
{
$mode = SPH_MATCH_ANY;
}
@@ -282,7 +276,7 @@ public function searchQuery($search_params, $search_words, $excluded_words, &$pa
$cached_results['matches'][$msgID] = array(
'id' => $match['attrs']['id_topic'],
'relevance' => round($match['attrs']['@count'] + $match['attrs']['relevance'] / 5000, 1) . '%',
- 'num_matches' => empty($search_params['topic']) ? $match['attrs']['@count'] : 0,
+ 'num_matches' => empty($this->_searchParams->topic) ? $match['attrs']['@count'] : 0,
'matches' => array(),
);
}
@@ -293,9 +287,10 @@ public function searchQuery($search_params, $search_words, $excluded_words, &$pa
}
$participants = array();
+ $topics = array();
foreach (array_slice(array_keys($cached_results['matches']), (int) $_REQUEST['start'], $modSettings['search_results_per_page']) as $msgID)
{
- $context['topics'][$msgID] = $cached_results['matches'][$msgID];
+ $topics[$msgID] = $cached_results['matches'][$msgID];
$participants[$cached_results['matches'][$msgID]['id']] = false;
}
@@ -305,8 +300,14 @@ public function searchQuery($search_params, $search_words, $excluded_words, &$pa
{
$search_results = array_merge($search_results, $search_words[$orIndex]['subject_words']);
}
+ $this->_num_results = $cached_results['num_results'];
- return $cached_results['num_results'];
+ return $topics;
+ }
+
+ public function useWordIndex()
+ {
+ return false;
}
/**
diff --git a/sources/subs/Search/API/Sphinxql.php b/sources/subs/Search/API/Sphinxql.php
index ca97df7073..f23cce8c76 100644
--- a/sources/subs/Search/API/Sphinxql.php
+++ b/sources/subs/Search/API/Sphinxql.php
@@ -70,7 +70,7 @@ class Sphinxql extends SearchAPI
/**
* Nothing to do ...
*/
- public function __construct()
+ public function __construct($config, $searchParams)
{
// Is this database supported?
if (!in_array(DB_TYPE, $this->supported_databases))
@@ -80,7 +80,7 @@ public function __construct()
return;
}
- parent::__construct();
+ parent::__construct($config, $searchParams);
}
/**
@@ -112,14 +112,16 @@ public function searchSort($a, $b)
}
/**
- * Do we have to do some work with the words we are searching for to prepare them?
- *
- * @param string $word word(s) to index
- * @param mixed[] $wordsSearch The Search words
- * @param string[] $wordsExclude Words to exclude
- * @param boolean $isExcluded
+ * {@inheritdoc }
+ */
+ public function indexedWordQuery($words, $search_data)
+ {
+ }
+
+ /**
+ * {@inheritdoc }
*/
- public function prepareIndexes($word, &$wordsSearch, &$wordsExclude, $isExcluded)
+ public function prepareIndexes($word, &$wordsSearch, &$wordsExclude, $isExcluded, $excludedSubjectWords)
{
$subwords = text2words($word, null, false);
@@ -132,17 +134,9 @@ public function prepareIndexes($word, &$wordsSearch, &$wordsExclude, $isExcluded
}
/**
- * This has it's own custom search.
- *
- * @param mixed[] $search_params
- * @param mixed[] $search_words Words to search
- * @param string[] $excluded_words Words to exclude, not used in this API
- * @param int[] $participants
- * @param string[] $search_results
- *
- * @return int
+ * {@inheritdoc }
*/
- public function searchQuery($search_params, $search_words, $excluded_words, &$participants, &$search_results)
+ public function searchQuery($search_words, $excluded_words, &$participants, &$search_results)
{
global $user_info, $context, $modSettings;
@@ -161,10 +155,10 @@ public function searchQuery($search_params, $search_words, $excluded_words, &$pa
// Compile different options for our query
$index = (!empty($modSettings['sphinx_index_prefix']) ? $modSettings['sphinx_index_prefix'] : 'elkarte') . '_index';
- $query = 'SELECT *' . (empty($search_params['topic']) ? ', COUNT(*) num' : '') . ', WEIGHT() weights, (weights + (relevance/10)) rank FROM ' . $index;
+ $query = 'SELECT *' . (empty($this->_searchParams->topic) ? ', COUNT(*) num' : '') . ', WEIGHT() weights, (weights + (relevance/10)) rank FROM ' . $index;
// Construct the (binary mode & |) query.
- $where_match = $this->_constructQuery($search_params['search']);
+ $where_match = $this->_constructQuery($this->_searchParams->search);
// Nothing to search, return zero results
if (trim($where_match) === '')
@@ -172,7 +166,7 @@ public function searchQuery($search_params, $search_words, $excluded_words, &$pa
return 0;
}
- if ($search_params['subject_only'])
+ if ($this->_searchParams->subject_only)
{
$where_match = '@subject ' . $where_match;
}
@@ -181,21 +175,21 @@ public function searchQuery($search_params, $search_words, $excluded_words, &$pa
// Set the limits based on the search parameters.
$extra_where = array();
- if (!empty($search_params['min_msg_id']) || !empty($search_params['max_msg_id']))
+ if (!empty($this->_searchParams->_minMsgID) || !empty($this->_searchParams->_maxMsgID))
{
- $extra_where[] = 'id >= ' . $search_params['min_msg_id'] . ' AND id <= ' . (empty($search_params['max_msg_id']) ? (int) $modSettings['maxMsgID'] : $search_params['max_msg_id']);
+ $extra_where[] = 'id >= ' . $this->_searchParams->_minMsgID . ' AND id <= ' . (empty($this->_searchParams->_maxMsgID) ? (int) $modSettings['maxMsgID'] : $this->_searchParams->_maxMsgID);
}
- if (!empty($search_params['topic']))
+ if (!empty($this->_searchParams->topic))
{
- $extra_where[] = 'id_topic = ' . (int) $search_params['topic'];
+ $extra_where[] = 'id_topic = ' . (int) $this->_searchParams->topic;
}
- if (!empty($search_params['brd']))
+ if (!empty($this->_searchParams->brd))
{
- $extra_where[] = 'id_board IN (' . implode(',', $search_params['brd']) . ')';
+ $extra_where[] = 'id_board IN (' . implode(',', $this->_searchParams->brd) . ')';
}
- if (!empty($search_params['memberlist']))
+ if (!empty($this->_searchParams->_memberlist))
{
- $extra_where[] = 'id_member IN (' . implode(',', $search_params['memberlist']) . ')';
+ $extra_where[] = 'id_member IN (' . implode(',', $this->_searchParams->_memberlist) . ')';
}
if (!empty($extra_where))
{
@@ -203,17 +197,17 @@ public function searchQuery($search_params, $search_words, $excluded_words, &$pa
}
// Put together a sort string; besides the main column sort (relevance, id_topic, or num_replies)
- $search_params['sort_dir'] = strtoupper($search_params['sort_dir']);
- $sphinx_sort = $search_params['sort'] === 'id_msg' ? 'id_topic' : $search_params['sort'];
+ $this->_searchParams->sort_dir = strtoupper($this->_searchParams->sort_dir);
+ $sphinx_sort = $this->_searchParams->sort === 'id_msg' ? 'id_topic' : $this->_searchParams->sort;
// Add secondary sorting based on relevance value (if not the main sort method) and age
- $sphinx_sort .= ' ' . $search_params['sort_dir'] . ($search_params['sort'] === 'relevance' ? '' : ', relevance DESC') . ', poster_time DESC';
+ $sphinx_sort .= ' ' . $this->_searchParams->sort_dir . ($this->_searchParams->sort === 'relevance' ? '' : ', relevance DESC') . ', poster_time DESC';
// Replace relevance with the returned rank value, rank uses sphinx weight + our own computed field weight relevance
$sphinx_sort = str_replace('relevance ', 'rank ', $sphinx_sort);
// Grouping by topic id makes it return only one result per topic, so don't set that for in-topic searches
- if (empty($search_params['topic']))
+ if (empty($this->_searchParams->topic))
{
// In the topic, base weights is the best ORDER BY param as relevance/rank is topic level
$query .= ' GROUP BY id_topic WITHIN GROUP ORDER BY ' . str_replace('rank ', 'weights ', $sphinx_sort);
@@ -253,7 +247,7 @@ public function searchQuery($search_params, $search_words, $excluded_words, &$pa
{
while ($match = mysqli_fetch_assoc($request))
{
- if (empty($search_params['topic']))
+ if (empty($this->_searchParams->topic))
{
$num = isset($match['num']) ? $match['num'] : (isset($match['@count']) ? $match['@count'] : 0);
}
@@ -282,9 +276,10 @@ public function searchQuery($search_params, $search_words, $excluded_words, &$pa
}
$participants = array();
+ $topics = array();
foreach (array_slice(array_keys($cached_results['matches']), (int) $_REQUEST['start'], $modSettings['search_results_per_page']) as $msgID)
{
- $context['topics'][$msgID] = $cached_results['matches'][$msgID];
+ $topics[$msgID] = $cached_results['matches'][$msgID];
$participants[$cached_results['matches'][$msgID]['id']] = false;
}
@@ -294,8 +289,14 @@ public function searchQuery($search_params, $search_words, $excluded_words, &$pa
{
$search_results = array_merge($search_results, $search_words[$orIndex]['subject_words']);
}
+ $this->_num_results = $cached_results['num_results'];
- return $cached_results['num_results'];
+ return $topics;
+ }
+
+ public function useWordIndex()
+ {
+ return false;
}
/**
diff --git a/sources/subs/Search/API/Standard.php b/sources/subs/Search/API/Standard.php
index 12d3032e09..ff84f99cf6 100644
--- a/sources/subs/Search/API/Standard.php
+++ b/sources/subs/Search/API/Standard.php
@@ -41,4 +41,901 @@ class Standard extends SearchAPI
* @var boolean
*/
public $is_supported = true;
+
+ /**
+ *
+ * @var object
+ */
+ protected $_search_cache = null;
+
+ /**
+ *
+ * @var int
+ */
+ protected $_num_results = 0;
+
+ /**
+ * Wrapper for searchQuery of the SearchAPI
+ * @param string[] $search_words
+ * @param string[] $excluded_words
+ * @param bool[] $participants
+ * @param string[] $search_results
+ *
+ * @return mixed[]
+ */
+ public function searchQuery($search_words, $excluded_words, &$participants, &$search_results)
+ {
+ global $context, $modSettings;
+
+ $this->_search_cache = new \ElkArte\Search\Cache\Session();
+ $this->_searchWords = $search_words;
+ $search_id = 0;
+
+ if ($this->_search_cache->existsWithParams($context['params']) === false)
+ {
+ $search_id = $this->_search_cache->increaseId($modSettings['search_pointer'] ?? 0);
+ // Store the new id right off.
+ updateSettings([
+ 'search_pointer' => $search_id
+ ]);
+
+ // Clear the previous cache of the final results cache.
+ $this->clearCacheResults($search_id);
+
+ if ($this->_searchParams['subject_only'])
+ {
+ $num_res = $this->getSubjectResults(
+ $search_id,
+ $search_words, $excluded_words
+ );
+ }
+ else
+ {
+ $num_res = $this->getResults(
+ $search_id
+ );
+ if (empty($num_res))
+ {
+ throw new \Exception('query_not_specific_enough');
+ }
+ }
+
+ $this->_search_cache->setNumResults($num_res);
+ }
+
+ $topics = array();
+ // *** Retrieve the results to be shown on the page
+ $participants = $this->addRelevance(
+ $topics,
+ $search_id,
+ (int) $_REQUEST['start'],
+ $modSettings['search_results_per_page']
+ );
+ $this->_num_results = $this->_search_cache->getNumResults();
+
+ return $topics;
+ }
+
+ /**
+ * Grabs results when the search is performed only within the subject
+ *
+ * @param int $id_search - the id of the search
+ *
+ * @return int - number of results otherwise
+ */
+ protected function getSubjectResults($id_search, $search_words, $excluded_words)
+ {
+ global $modSettings;
+
+ $numSubjectResults = 0;
+ // We do this to try and avoid duplicate keys on databases not supporting INSERT IGNORE.
+ foreach ($search_words as $words)
+ {
+ $subject_query_params = array();
+ $subject_query = array(
+ 'from' => '{db_prefix}topics AS t',
+ 'inner_join' => array(),
+ 'left_join' => array(),
+ 'where' => array(),
+ );
+
+ if ($modSettings['postmod_active'])
+ {
+ $subject_query['where'][] = 't.approved = {int:is_approved}';
+ }
+
+ $numTables = 0;
+ $prev_join = 0;
+ $numSubjectResults = 0;
+ foreach ($words['subject_words'] as $subjectWord)
+ {
+ $numTables++;
+ if (in_array($subjectWord, $excluded_words))
+ {
+ $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)';
+ $subject_query['where'][] = '(subj' . $numTables . '.word IS NULL)';
+ }
+ else
+ {
+ $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)';
+ $subject_query['where'][] = 'subj' . $numTables . '.word ' . (empty($modSettings['search_match_words']) ? 'LIKE {string:subject_words_' . $numTables . '_wild}' : '= {string:subject_words_' . $numTables . '}');
+ $prev_join = $numTables;
+ }
+
+ $subject_query_params['subject_words_' . $numTables] = $subjectWord;
+ $subject_query_params['subject_words_' . $numTables . '_wild'] = '%' . $subjectWord . '%';
+ }
+
+ if (!empty($this->_searchParams->_userQuery))
+ {
+ $subject_query['inner_join'][] = '{db_prefix}messages AS m ON (m.id_topic = t.id_topic)';
+ $subject_query['where'][] = $this->_searchParams->_userQuery;
+ }
+
+ if (!empty($this->_searchParams['topic']))
+ {
+ $subject_query['where'][] = 't.id_topic = ' . $this->_searchParams['topic'];
+ }
+
+ if (!empty($this->_searchParams->_minMsgID))
+ {
+ $subject_query['where'][] = 't.id_first_msg >= ' . $this->_searchParams->_minMsgID;
+ }
+
+ if (!empty($this->_searchParams->_maxMsgID))
+ {
+ $subject_query['where'][] = 't.id_last_msg <= ' . $this->_searchParams->_maxMsgID;
+ }
+
+ if (!empty($this->_searchParams->_boardQuery))
+ {
+ $subject_query['where'][] = 't.id_board ' . $this->_searchParams->_boardQuery;
+ }
+
+ if (!empty($this->_excludedPhrases))
+ {
+ $subject_query['inner_join'][] = '{db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)';
+
+ $count = 0;
+ foreach ($this->_excludedPhrases as $phrase)
+ {
+ $subject_query['where'][] = 'm.subject NOT ' . (empty($modSettings['search_match_words']) || $this->noRegexp() ? 'LIKE' : 'RLIKE') . ' {string:excluded_phrases_' . $count . '}';
+ $subject_query_params['excluded_phrases_' . ($count++)] = $this->prepareWord($phrase, $this->noRegexp());
+ }
+ }
+
+ // Build the search query
+ $subject_query['select'] = array(
+ 'id_search' => '{int:id_search}',
+ 'id_topic' => 't.id_topic',
+ 'relevance' => $this->_build_relevance(),
+ 'id_msg' => empty($this->_searchParams->_userQuery) ? 't.id_first_msg' : 'm.id_msg',
+ 'num_matches' => 1,
+ );
+
+ $subject_query['parameters'] = array_merge($subject_query_params, array(
+ 'id_search' => $id_search,
+ 'min_msg' => $this->_searchParams->_minMsg,
+ 'recent_message' => $this->_searchParams->_recentMsg,
+ 'huge_topic_posts' => $this->config->humungousTopicPosts,
+ 'is_approved' => 1,
+ 'limit' => empty($modSettings['search_max_results']) ? 0 : $modSettings['search_max_results'] - $numSubjectResults,
+ ));
+
+ call_integration_hook('integrate_subject_only_search_query', array(&$subject_query, &$subject_query_params));
+
+ $numSubjectResults += $this->_build_search_results_log($subject_query, 'insert_log_search_results_subject');
+
+ if (!empty($modSettings['search_max_results']) && $numSubjectResults >= $modSettings['search_max_results'])
+ {
+ break;
+ }
+ }
+
+ return $numSubjectResults;
+ }
+
+ /**
+ * If the query uses regexp or not
+ *
+ * @return bool
+ */
+ protected function noRegexp()
+ {
+ return $this->_searchArray->getNoRegexp();
+ }
+
+ /**
+ * Grabs results when the search is performed in subjects and bodies
+ *
+ * @param int $id_search - the id of the search
+ *
+ * @return bool|int - boolean (false) in case of errors, number of results otherwise
+ */
+ public function getResults($id_search)
+ {
+ global $modSettings;
+
+ $num_results = 0;
+
+ $main_query = array(
+ 'select' => array(
+ 'id_search' => $id_search,
+ 'relevance' => '0',
+ ),
+ 'weights' => array(),
+ 'from' => '{db_prefix}topics AS t',
+ 'inner_join' => array(
+ '{db_prefix}messages AS m ON (m.id_topic = t.id_topic)'
+ ),
+ 'left_join' => array(),
+ 'where' => array(),
+ 'group_by' => array(),
+ 'parameters' => array(
+ 'min_msg' => $this->_searchParams->_minMsg,
+ 'recent_message' => $this->_searchParams->_recentMsg,
+ 'huge_topic_posts' => $this->config->humungousTopicPosts,
+ 'is_approved' => 1,
+ 'limit' => $modSettings['search_max_results'],
+ ),
+ );
+
+ if (empty($this->_searchParams['topic']) && empty($this->_searchParams['show_complete']))
+ {
+ $main_query['select']['id_topic'] = 't.id_topic';
+ $main_query['select']['id_msg'] = 'MAX(m.id_msg) AS id_msg';
+ $main_query['select']['num_matches'] = 'COUNT(*) AS num_matches';
+ $main_query['weights'] = $this->_weight_factors;
+ $main_query['group_by'][] = 't.id_topic';
+ }
+ else
+ {
+ // This is outrageous!
+ $main_query['select']['id_topic'] = 'm.id_msg AS id_topic';
+ $main_query['select']['id_msg'] = 'm.id_msg';
+ $main_query['select']['num_matches'] = '1 AS num_matches';
+
+ $main_query['weights'] = array(
+ 'age' => array(
+ '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)',
+ ),
+ 'first_message' => array(
+ 'search' => 'CASE WHEN m.id_msg = t.id_first_msg THEN 1 ELSE 0 END',
+ ),
+ );
+
+ if (!empty($this->_searchParams['topic']))
+ {
+ $main_query['where'][] = 't.id_topic = {int:topic}';
+ $main_query['parameters']['topic'] = $this->_searchParams->topic;
+ }
+
+ if (!empty($this->_searchParams['show_complete']))
+ {
+ $main_query['group_by'][] = 'm.id_msg, t.id_first_msg, t.id_last_msg';
+ }
+ }
+
+ // *** Get the subject results.
+ $numSubjectResults = $this->_log_search_subjects($id_search);
+
+ if ($numSubjectResults !== 0)
+ {
+ $main_query['weights']['subject']['search'] = 'CASE WHEN MAX(lst.id_topic) IS NULL THEN 0 ELSE 1 END';
+ $main_query['left_join'][] = '{db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_topics AS lst ON (' . ($this->_createTemporary ? '' : 'lst.id_search = {int:id_search} AND ') . 'lst.id_topic = t.id_topic)';
+ if (!$this->_createTemporary)
+ {
+ $main_query['parameters']['id_search'] = $id_search;
+ }
+ }
+
+ // We building an index?
+ if ($this->useWordIndex())
+ {
+ $indexedResults = $this->_prepare_word_index($id_search);
+
+ if (empty($indexedResults) && empty($numSubjectResults) && !empty($modSettings['search_force_index']))
+ {
+ return false;
+ }
+ elseif (!empty($indexedResults))
+ {
+ $main_query['inner_join'][] = '{db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_messages AS lsm ON (lsm.id_msg = m.id_msg)';
+
+ if (!$this->_createTemporary)
+ {
+ $main_query['where'][] = 'lsm.id_search = {int:id_search}';
+ $main_query['parameters']['id_search'] = $id_search;
+ }
+ }
+ }
+ // Not using an index? All conditions have to be carried over.
+ else
+ {
+ $orWhere = array();
+ $count = 0;
+ $excludedWords = $this->_searchArray->getExcludedWords();
+ foreach ($this->_searchWords as $words)
+ {
+ $where = array();
+ foreach ($words['all_words'] as $regularWord)
+ {
+ $where[] = 'm.body' . (in_array($regularWord, $excludedWords) ? ' NOT' : '') . (empty($modSettings['search_match_words']) || $this->noRegexp() ? ' LIKE ' : ' RLIKE ') . '{string:all_word_body_' . $count . '}';
+ if (in_array($regularWord, $excludedWords))
+ {
+ $where[] = 'm.subject NOT' . (empty($modSettings['search_match_words']) || $this->noRegexp() ? ' LIKE ' : ' RLIKE ') . '{string:all_word_body_' . $count . '}';
+ }
+ $main_query['parameters']['all_word_body_' . ($count++)] = $this->prepareWord($regularWord, $this->noRegexp());
+ }
+
+ if (!empty($where))
+ {
+ $orWhere[] = count($where) > 1 ? '(' . implode(' AND ', $where) . ')' : $where[0];
+ }
+ }
+
+ if (!empty($orWhere))
+ {
+ $main_query['where'][] = count($orWhere) > 1 ? '(' . implode(' OR ', $orWhere) . ')' : $orWhere[0];
+ }
+
+ if (!empty($this->_searchParams->_userQuery))
+ {
+ $main_query['where'][] = '{raw:user_query}';
+ $main_query['parameters']['user_query'] = $this->_searchParams->_userQuery;
+ }
+
+ if (!empty($this->_searchParams['topic']))
+ {
+ $main_query['where'][] = 'm.id_topic = {int:topic}';
+ $main_query['parameters']['topic'] = $this->_searchParams->topic;
+ }
+
+ if (!empty($this->_searchParams->_minMsgID))
+ {
+ $main_query['where'][] = 'm.id_msg >= {int:min_msg_id}';
+ $main_query['parameters']['min_msg_id'] = $this->_searchParams->_minMsgID;
+ }
+
+ if (!empty($this->_searchParams->_maxMsgID))
+ {
+ $main_query['where'][] = 'm.id_msg <= {int:max_msg_id}';
+ $main_query['parameters']['max_msg_id'] = $this->_searchParams->_maxMsgID;
+ }
+
+ if (!empty($this->_searchParams->_boardQuery))
+ {
+ $main_query['where'][] = 'm.id_board {raw:board_query}';
+ $main_query['parameters']['board_query'] = $this->_searchParams->_boardQuery;
+ }
+ }
+ call_integration_hook('integrate_main_search_query', array(&$main_query));
+
+ // Did we either get some indexed results, or otherwise did not do an indexed query?
+ if (!empty($indexedResults) || $this->useWordIndex() === false)
+ {
+ $main_query['select']['relevance'] = $this->_build_relevance($main_query['weights']);
+ $num_results += $this->_build_search_results_log($main_query, 'insert_log_search_results_no_index');
+ }
+
+ // Insert subject-only matches.
+ if ($num_results < $modSettings['search_max_results'] && $numSubjectResults !== 0)
+ {
+ $subject_query = array(
+ 'select' => array(
+ 'id_search' => '{int:id_search}',
+ 'id_topic' => 't.id_topic',
+ 'relevance' => $this->_build_relevance(),
+ 'id_msg' => 't.id_first_msg',
+ 'num_matches' => 1,
+ ),
+ 'from' => '{db_prefix}topics AS t',
+ 'inner_join' => array(
+ '{db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_topics AS lst ON (lst.id_topic = t.id_topic)'
+ ),
+ 'where' => array(
+ $this->_createTemporary ? '1=1' : 'lst.id_search = {int:id_search}',
+ ),
+ 'parameters' => array(
+ 'id_search' => $id_search,
+ 'min_msg' => $this->_searchParams->_minMsg,
+ 'recent_message' => $this->_searchParams->_recentMsg,
+ 'huge_topic_posts' => $this->config->humungousTopicPosts,
+ 'limit' => empty($modSettings['search_max_results']) ? 0 : $modSettings['search_max_results'] - $num_results,
+ ),
+ );
+
+ $num_results += $this->_build_search_results_log($subject_query, 'insert_log_search_results_sub_only', true);
+ }
+ elseif ($num_results == -1)
+ {
+ $num_results = 0;
+ }
+
+ return $num_results;
+ }
+
+ /**
+ * Build the search relevance query
+ *
+ * @param null|int[] $factors - is factors are specified that array will
+ * be used to build the relevance value, otherwise the function will use
+ * $this->_weight_factors
+ *
+ * @return string
+ */
+ private function _build_relevance($factors = null)
+ {
+ $relevance = '1000 * (';
+
+ if ($factors !== null && is_array($factors))
+ {
+ $weight_total = 0;
+ foreach ($factors as $type => $value)
+ {
+ $relevance .= $this->_weight[$type];
+ if (!empty($value['search']))
+ {
+ $relevance .= ' * ' . $value['search'];
+ }
+
+ $relevance .= ' + ';
+ $weight_total += $this->_weight[$type];
+ }
+ }
+ else
+ {
+ $weight_total = $this->_weight_total;
+ foreach ($this->_weight_factors as $type => $value)
+ {
+ if (isset($value['results']))
+ {
+ $relevance .= $this->_weight[$type];
+ if (!empty($value['results']))
+ {
+ $relevance .= ' * ' . $value['results'];
+ }
+
+ $relevance .= ' + ';
+ }
+ }
+ }
+
+ $relevance = substr($relevance, 0, -3) . ') / ' . $weight_total . ' AS relevance';
+
+ return $relevance;
+ }
+
+ /**
+ * Populates log_search_messages
+ *
+ * @param int $id_search - the id of the search to delete from logs
+ *
+ * @return int - the number of indexed results
+ */
+ private function _prepare_word_index($id_search)
+ {
+ $indexedResults = 0;
+ $inserts = array();
+
+ // Clear, all clear!
+ if (!$this->_createTemporary)
+ {
+ $this->_db_search->search_query('delete_log_search_messages', '
+ DELETE FROM {db_prefix}log_search_messages
+ WHERE id_search = {int:id_search}',
+ array(
+ 'id_search' => $id_search,
+ )
+ );
+ }
+ $excludedWords = $this->_searchArray->getExcludedWords();
+
+ foreach ($this->_searchWords as $words)
+ {
+ // Search for this word, assuming we have some words!
+ if (!empty($words['indexed_words']))
+ {
+ // Variables required for the search.
+ $search_data = array(
+ 'insert_into' => ($this->_createTemporary ? 'tmp_' : '') . 'log_search_messages',
+ 'no_regexp' => $this->noRegexp(),
+ 'max_results' => $this->config->maxMessageResults,
+ 'indexed_results' => $indexedResults,
+ 'params' => array(
+ 'id_search' => !$this->_createTemporary ? $id_search : 0,
+ 'excluded_words' => $excludedWords,
+ 'user_query' => !empty($this->_searchParams->_userQuery) ? $this->_searchParams->_userQuery : '',
+ 'board_query' => !empty($this->_searchParams->_boardQuery) ? $this->_searchParams->_boardQuery : '',
+ 'topic' => (int) $this->_searchParams->topic,
+ 'min_msg_id' => (int) $this->_searchParams->_minMsgID,
+ 'max_msg_id' => (int) $this->_searchParams->_maxMsgID,
+ 'excluded_phrases' => $this->_excludedPhrases,
+ 'excluded_index_words' => $this->_excludedIndexWords,
+ 'excluded_subject_words' => $this->_excludedSubjectWords,
+ ),
+ );
+
+ $ignoreRequest = $this->indexedWordQuery($words, $search_data);
+
+ if (!$this->_db->support_ignore())
+ {
+ while ($row = $this->_db->fetch_row($ignoreRequest))
+ {
+ // No duplicates!
+ if (isset($inserts[$row[0]]))
+ {
+ continue;
+ }
+
+ $inserts[$row[0]] = $row;
+ }
+ $this->_db->free_result($ignoreRequest);
+ $indexedResults = count($inserts);
+ }
+ else
+ {
+ $indexedResults += $this->_db->affected_rows();
+ }
+
+ if (!empty($this->config->maxMessageResults) && $indexedResults >= $this->config->maxMessageResults)
+ {
+ break;
+ }
+ }
+ }
+
+ // More non-MySQL stuff needed?
+ if (!empty($inserts))
+ {
+ $this->_db->insert('',
+ '{db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_messages',
+ $this->_createTemporary ? array('id_msg' => 'int') : array('id_msg' => 'int', 'id_search' => 'int'),
+ $inserts,
+ $this->_createTemporary ? array('id_msg') : array('id_msg', 'id_search')
+ );
+ }
+
+ return $indexedResults;
+ }
+
+ /**
+ * Inserts the data into log_search_results
+ *
+ * @param mixed[] $main_query - An array holding all the query parts.
+ * Structure:
+ * 'select' => string[] - the select columns
+ * 'from' => string - the table for the FROM clause
+ * 'inner_join' => string[] - any INNER JOIN
+ * 'left_join' => string[] - any LEFT JOIN
+ * 'where' => string[] - the conditions
+ * 'group_by' => string[] - the fields to group by
+ * 'parameters' => mixed[] - any parameter required by the query
+ * @param string $query_identifier - a string to identify the query
+ * @param bool $use_old_ids - if true the topic ids retrieved by a previous
+ * call to this function will be used to identify duplicates
+ *
+ * @return int - the number of rows affected by the query
+ */
+ private function _build_search_results_log($main_query, $query_identifier, $use_old_ids = false)
+ {
+ static $usedIDs;
+
+ $ignoreRequest = $this->_db_search->search_query($query_identifier, ($this->_db->support_ignore() ? ('
+ INSERT IGNORE INTO {db_prefix}log_search_results
+ (' . implode(', ', array_keys($main_query['select'])) . ')') : '') . '
+ SELECT
+ ' . implode(',
+ ', $main_query['select']) . '
+ FROM ' . $main_query['from'] . (!empty($main_query['inner_join']) ? '
+ INNER JOIN ' . implode('
+ INNER JOIN ', array_unique($main_query['inner_join'])) : '') . (!empty($main_query['left_join']) ? '
+ LEFT JOIN ' . implode('
+ LEFT JOIN ', array_unique($main_query['left_join'])) : '') . (!empty($main_query['where']) ? '
+ WHERE ' : '') . implode('
+ AND ', array_unique($main_query['where'])) . (!empty($main_query['group_by']) ? '
+ GROUP BY ' . implode(', ', array_unique($main_query['group_by'])) : '') . (!empty($main_query['parameters']['limit']) ? '
+ LIMIT {int:limit}' : ''),
+ $main_query['parameters']
+ );
+
+ // If the database doesn't support IGNORE to make this fast we need to do some tracking.
+ if (!$this->_db->support_ignore())
+ {
+ $inserts = array();
+
+ while ($row = $this->_db->fetch_assoc($ignoreRequest))
+ {
+ // No duplicates!
+ if ($use_old_ids)
+ {
+ if (isset($usedIDs[$row['id_topic']]))
+ {
+ continue;
+ }
+ }
+ else
+ {
+ if (isset($inserts[$row['id_topic']]))
+ {
+ continue;
+ }
+ }
+
+ $usedIDs[$row['id_topic']] = true;
+ foreach ($row as $key => $value)
+ $inserts[$row['id_topic']][] = (int) $row[$key];
+ }
+ $this->_db->free_result($ignoreRequest);
+
+ // Now put them in!
+ if (!empty($inserts))
+ {
+ $query_columns = array();
+ foreach ($main_query['select'] as $k => $v)
+ $query_columns[$k] = 'int';
+
+ $this->_db->insert('',
+ '{db_prefix}log_search_results',
+ $query_columns,
+ $inserts,
+ array('id_search', 'id_topic')
+ );
+ }
+ $num_results = count($inserts);
+ }
+ else
+ {
+ $num_results = $this->_db->affected_rows();
+ }
+
+ return $num_results;
+ }
+
+ /**
+ * Determines and add the relevance to the results
+ *
+ * @param mixed[] $topics - The search results (passed by reference)
+ * @param int $id_search - the id of the search
+ * @param int $start - Results are shown starting from here
+ * @param int $limit - No more results than this
+ *
+ * @return bool[]
+ */
+ public function addRelevance(&$topics, $id_search, $start, $limit)
+ {
+ // *** Retrieve the results to be shown on the page
+ $participants = array();
+ $request = $this->_db_search->search_query('', '
+ SELECT ' . (empty($this->_searchParams['topic']) ? 'lsr.id_topic' : $this->_searchParams->topic . ' AS id_topic') . ', lsr.id_msg, lsr.relevance, lsr.num_matches
+ FROM {db_prefix}log_search_results AS lsr' . ($this->_searchParams->sort === 'num_replies' ? '
+ INNER JOIN {db_prefix}topics AS t ON (t.id_topic = lsr.id_topic)' : '') . '
+ WHERE lsr.id_search = {int:id_search}
+ ORDER BY {raw:sort} {raw:sort_dir}
+ LIMIT {int:start}, {int:limit}',
+ array(
+ 'id_search' => $id_search,
+ 'sort' => $this->_searchParams->sort,
+ 'sort_dir' => $this->_searchParams->sort_dir,
+ 'start' => $start,
+ 'limit' => $limit,
+ )
+ );
+ while ($row = $this->_db->fetch_assoc($request))
+ {
+ $topics[$row['id_msg']] = array(
+ 'relevance' => round($row['relevance'] / 10, 1) . '%',
+ 'num_matches' => $row['num_matches'],
+ 'matches' => array(),
+ );
+ // By default they didn't participate in the topic!
+ $participants[$row['id_topic']] = false;
+ }
+ $this->_db->free_result($request);
+
+ return $participants;
+ }
+
+ /**
+ * Delete logs of previous searches
+ *
+ * @param int $id_search - the id of the search to delete from logs
+ */
+ public function clearCacheResults($id_search)
+ {
+ $this->_db_search->search_query('delete_log_search_results', '
+ DELETE FROM {db_prefix}log_search_results
+ WHERE id_search = {int:search_id}',
+ array(
+ 'search_id' => $id_search,
+ )
+ );
+ }
+
+ /**
+ * If searching in topics only (?), inserts results in log_search_topics
+ *
+ * @param int $id_search - the id of the search to delete from logs
+ *
+ * @return int - the number of search results
+ */
+ private function _log_search_subjects($id_search)
+ {
+ global $modSettings;
+
+ if (!empty($this->_searchParams['topic']))
+ {
+ return 0;
+ }
+
+ $inserts = array();
+ $numSubjectResults = 0;
+
+ // Clean up some previous cache.
+ if (!$this->_createTemporary)
+ {
+ $this->_db_search->search_query('delete_log_search_topics', '
+ DELETE FROM {db_prefix}log_search_topics
+ WHERE id_search = {int:search_id}',
+ array(
+ 'search_id' => $id_search,
+ )
+ );
+ }
+
+ foreach ($this->_searchWords as $words)
+ {
+ $subject_query = array(
+ 'from' => '{db_prefix}topics AS t',
+ 'inner_join' => array(),
+ 'left_join' => array(),
+ 'where' => array(),
+ 'params' => array(),
+ );
+
+ $numTables = 0;
+ $prev_join = 0;
+ $count = 0;
+ foreach ($words['subject_words'] as $subjectWord)
+ {
+ $numTables++;
+ if (in_array($subjectWord, $this->_excludedSubjectWords))
+ {
+ $subject_query['inner_join'][] = '{db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)';
+ $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)';
+ $subject_query['params']['subject_not_' . $count] = empty($modSettings['search_match_words']) ? '%' . $subjectWord . '%' : $subjectWord;
+
+ $subject_query['where'][] = '(subj' . $numTables . '.word IS NULL)';
+ $subject_query['where'][] = 'm.body NOT ' . (empty($modSettings['search_match_words']) || $this->noRegexp() ? ' LIKE ' : ' RLIKE ') . '{string:body_not_' . $count . '}';
+ $subject_query['params']['body_not_' . ($count++)] = $this->prepareWord($subjectWord, $this->noRegexp());
+ }
+ else
+ {
+ $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)';
+ $subject_query['where'][] = 'subj' . $numTables . '.word LIKE {string:subject_like_' . $count . '}';
+ $subject_query['params']['subject_like_' . ($count++)] = empty($modSettings['search_match_words']) ? '%' . $subjectWord . '%' : $subjectWord;
+ $prev_join = $numTables;
+ }
+ }
+
+ if (!empty($this->_searchParams->_userQuery))
+ {
+ $subject_query['inner_join'][] = '{db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)';
+ $subject_query['where'][] = '{raw:user_query}';
+ $subject_query['params']['user_query'] = $this->_searchParams->_userQuery;
+ }
+
+ if (!empty($this->_searchParams['topic']))
+ {
+ $subject_query['where'][] = 't.id_topic = {int:topic}';
+ $subject_query['params']['topic'] = $this->_searchParams->topic;
+ }
+
+ if (!empty($this->_searchParams->_minMsgID))
+ {
+ $subject_query['where'][] = 't.id_first_msg >= {int:min_msg_id}';
+ $subject_query['params']['min_msg_id'] = $this->_searchParams->_minMsgID;
+ }
+
+ if (!empty($this->_searchParams->_maxMsgID))
+ {
+ $subject_query['where'][] = 't.id_last_msg <= {int:max_msg_id}';
+ $subject_query['params']['max_msg_id'] = $this->_searchParams->_maxMsgID;
+ }
+
+ if (!empty($this->_searchParams->_boardQuery))
+ {
+ $subject_query['where'][] = 't.id_board {raw:board_query}';
+ $subject_query['params']['board_query'] = $this->_searchParams->_boardQuery;
+ }
+
+ if (!empty($this->_excludedPhrases))
+ {
+ $subject_query['inner_join'][] = '{db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)';
+ $count = 0;
+ foreach ($this->_excludedPhrases as $phrase)
+ {
+ $subject_query['where'][] = 'm.subject NOT ' . (empty($modSettings['search_match_words']) || $this->noRegexp() ? 'LIKE' : 'RLIKE') . ' {string:exclude_phrase_' . $count . '}';
+ $subject_query['where'][] = 'm.body NOT ' . (empty($modSettings['search_match_words']) || $this->noRegexp() ? 'LIKE' : 'RLIKE') . ' {string:exclude_phrase_' . $count . '}';
+ $subject_query['params']['exclude_phrase_' . ($count++)] = $this->prepareWord($phrase, $this->noRegexp());
+ }
+ }
+
+ call_integration_hook('integrate_subject_search_query', array(&$subject_query));
+
+ // Nothing to search for?
+ if (empty($subject_query['where']))
+ {
+ continue;
+ }
+
+ $ignoreRequest = $this->_db_search->search_query('insert_log_search_topics', ($this->_db->support_ignore() ? ('
+ INSERT IGNORE INTO {db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_topics
+ (' . ($this->_createTemporary ? '' : 'id_search, ') . 'id_topic)') : '') . '
+ SELECT ' . ($this->_createTemporary ? '' : $id_search . ', ') . 't.id_topic
+ FROM ' . $subject_query['from'] . (empty($subject_query['inner_join']) ? '' : '
+ INNER JOIN ' . implode('
+ INNER JOIN ', array_unique($subject_query['inner_join']))) . (empty($subject_query['left_join']) ? '' : '
+ LEFT JOIN ' . implode('
+ LEFT JOIN ', array_unique($subject_query['left_join']))) . '
+ WHERE ' . implode('
+ AND ', array_unique($subject_query['where'])) . (empty($modSettings['search_max_results']) ? '' : '
+ LIMIT ' . ($modSettings['search_max_results'] - $numSubjectResults)),
+ $subject_query['params']
+ );
+
+ // Don't do INSERT IGNORE? Manually fix this up!
+ if (!$this->_db->support_ignore())
+ {
+ while ($row = $this->_db->fetch_row($ignoreRequest))
+ {
+ $ind = $this->_createTemporary ? 0 : 1;
+
+ // No duplicates!
+ if (isset($inserts[$row[$ind]]))
+ {
+ continue;
+ }
+
+ $inserts[$row[$ind]] = $row;
+ }
+ $this->_db->free_result($ignoreRequest);
+ $numSubjectResults = count($inserts);
+ }
+ else
+ {
+ $numSubjectResults += $this->_db->affected_rows();
+ }
+
+ if (!empty($modSettings['search_max_results']) && $numSubjectResults >= $modSettings['search_max_results'])
+ {
+ break;
+ }
+ }
+
+ // Got some non-MySQL data to plonk in?
+ if (!empty($inserts))
+ {
+ $this->_db->insert('',
+ ('{db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_topics'),
+ $this->_createTemporary ? array('id_topic' => 'int') : array('id_search' => 'int', 'id_topic' => 'int'),
+ $inserts,
+ $this->_createTemporary ? array('id_topic') : array('id_search', 'id_topic')
+ );
+ }
+
+ return $numSubjectResults;
+ }
+
+ public function useWordIndex()
+ {
+ return false;
+ }
+
+ /**
+ * {@inheritdoc }
+ */
+ public function indexedWordQuery($words, $search_data)
+ {
+ }
}
diff --git a/sources/subs/Search/Cache/Session.php b/sources/subs/Search/Cache/Session.php
new file mode 100644
index 0000000000..c64f35ff69
--- /dev/null
+++ b/sources/subs/Search/Cache/Session.php
@@ -0,0 +1,80 @@
+_session_index = 'search_' . $index;
+ }
+ }
+
+ public function __destruct()
+ {
+ new SessionIndex($this->_session_index, array(
+ 'id_search' => $this->_id_search,
+ 'num_results' => $this->_num_results,
+ 'params' => $this->_params,
+ ));
+ }
+
+ public function getId()
+ {
+ return $this->_id_search;
+ }
+
+ public function increaseId($pointer = 0)
+ {
+ $this->_id_search = (int) $pointer;
+ $this->_id_search += 1;
+
+ if ($this->_id_search > 255)
+ {
+ $this->_id_search = 0;
+ }
+
+ return $this->getId();
+ }
+
+ public function existsWithParams($params)
+ {
+ return $this->_params == $params;
+ }
+
+ public function setNumResults($num_results = 0)
+ {
+ $this->_num_results = (int) $num_results;
+ }
+
+ /**
+ * Returns the number of results obtained from the query.
+ *
+ * @return int
+ */
+ public function getNumResults()
+ {
+ return $this->_num_results;
+ }
+}
\ No newline at end of file
diff --git a/sources/subs/Search/Search.php b/sources/subs/Search/Search.php
index 522e68e51d..72be2c1b1c 100644
--- a/sources/subs/Search/Search.php
+++ b/sources/subs/Search/Search.php
@@ -17,36 +17,31 @@
namespace ElkArte\Search;
+use \ElkArte\Search\SearchParams;
+
/**
* Actually do the searches
*/
class Search
{
- /**
- * This is the minimum version of ElkArte that an API could have been written
- * for to work.
- * (strtr to stop accidentally updating version on release)
- */
- private $_search_version = '';
-
/**
* This is the forum version but is repeated due to some people
* rewriting FORUM_VERSION.
*/
- private $_forum_version = '';
+ const FORUM_VERSION = 'ElkArte 2.0 dev';
/**
- * $_search_params will carry all settings that differ from the default search parameters.
- *
- * That way, the URLs involved in a search page will be kept as short as possible.
+ * This is the minimum version of ElkArte that an API could have been written
+ * for to work.
+ * (strtr to stop accidentally updating version on release)
*/
- private $_search_params = array();
+ private $_search_version = '';
/**
* Holds the words and phrases to be searched on
- * @var array
+ * @var \ElkArte\Search\SearchArray
*/
- private $_searchArray = array();
+ private $_searchArray = null;
/**
* Holds instance of the search api in use such as ElkArte\Search\API\Standard_Search
@@ -66,48 +61,12 @@ class Search
*/
private $_db_search = null;
- /**
- * Holds words that will not be search on to inform the user they were skipped
- * @var array
- */
- private $_ignored = array();
-
/**
* Searching for posts from a specific user(s)
* @var array
*/
private $_memberlist = array();
- /**
- * Message "age" via ID, given bounds, needed to calculate relevance
- * @var int
- */
- private $_recentMsg = 0;
-
- /**
- * The minimum message id we will search, needed to calculate relevance
- * @var int
- */
- private $_minMsgID = 0;
-
- /**
- * Needed to calculate relevance
- * @var int
- */
- private $_minMsg = 0;
-
- /**
- * The maximum message ID we will search, needed to calculate relevance
- * @var int
- */
- private $_maxMsgID = 0;
-
- /**
- * If we are performing a boolean or simple search
- * @var bool
- */
- private $_no_regexp = false;
-
/**
* Builds the array of words for use in the db query
* @var array
@@ -120,12 +79,6 @@ class Search
*/
private $_excludedIndexWords = array();
- /**
- * Words not be be found in the search results (-word)
- * @var array
- */
- private $_excludedWords = array();
-
/**
* Words not be be found in the subject (-word)
* @var array
@@ -138,53 +91,29 @@ class Search
*/
private $_excludedPhrases = array();
- /**
- * If search words were found on the blacklist
- * @var bool
- */
- private $_foundBlackListedWords = false;
-
- /**
- * Words we do not search due to length or common terms
- * @var array
- */
- private $_blacklisted_words = array();
-
- /**
- * The db query for brd's
- * @var string
- */
- private $_boardQuery = '';
-
- /**
- * the db query for members
- * @var string
- */
- private $_userQuery = '';
-
/**
* The weights to associate to various areas for relevancy
- * @var array
+ * @var \ElkArte\Search\WeightFactors
*/
- private $_weight_factors = array();
+ private $_weightFactors = array();
/**
- * Weighing factor each area, ie frequency, age, sticky, etc
- * @var array
+ * If we are creating a tmp db table
+ * @var bool
*/
- private $_weight = array();
+ private $_createTemporary = true;
/**
- * The sum of the _weight_factors, normally but not always 100
- * @var int
+ *
+ * @var mixed[]
*/
- private $_weight_total = 0;
+ protected $_participants = [];
/**
- * If we are creating a tmp db table
- * @var bool
+ *
+ * @var null|\ElkArte\Search\SearchParams
*/
- private $_createTemporary = true;
+ protected $_searchParams = null;
/**
* Constructor
@@ -195,7 +124,6 @@ class Search
public function __construct()
{
$this->_search_version = strtr('ElkArte 1+1', array('+' => '.', '=' => ' '));
- $this->_forum_version = 'ElkArte 2.0 dev';
$this->_db = database();
$this->_db_search = db_search();
@@ -242,47 +170,6 @@ public function __construct()
);
}
- /**
- * Creates a search API and returns the object.
- *
- * @param string $searchClass
- *
- * @return API\Standard|null|object
- */
- public function findSearchAPI($searchClass = '')
- {
- global $modSettings, $txt;
-
- require_once(SUBSDIR . '/Package.subs.php');
- \Elk_Autoloader::instance()->register(SUBSDIR . '/Search', '\\ElkArte\\Search');
-
- // Load up the search API we are going to use.
- if (empty($searchClass))
- {
- $searchClass = !empty($modSettings['search_index']) ? $modSettings['search_index'] : 'standard';
- }
-
- // Try to initialize the API
- $fqcn = '\\ElkArte\\Search\\API\\' . ucfirst($searchClass);
- if (class_exists($fqcn) && is_a($fqcn, 'ElkArte\\Search\\API\\SearchAPI', true))
- {
- // Create an instance of the search API and check it is valid for this version of the software.
- $this->_searchAPI = new $fqcn;
- }
-
- // An invalid Search API? Log the error and set it to use the standard API
- if (!$this->_searchAPI || (!$this->_searchAPI->isValid()) || !matchPackageVersion($this->_forum_version, $this->_searchAPI->min_elk_version . '-' . $this->_searchAPI->version_compatible))
- {
- // Log the error.
- theme()->getTemplates()->loadLanguageFile('Errors');
- \Errors::instance()->log_error(sprintf($txt['search_api_not_compatible'], $fqcn), 'critical');
-
- $this->_searchAPI = new \ElkArte\Search\API\Standard;
- }
-
- return $this->_searchAPI;
- }
-
/**
* Returns a search parameter.
*
@@ -292,9 +179,9 @@ public function findSearchAPI($searchClass = '')
*/
public function param($name)
{
- if (isset($this->_search_params[$name]))
+ if (isset($this->_searchParams[$name]))
{
- return $this->_search_params[$name];
+ return $this->_searchParams[$name];
}
else
{
@@ -309,10 +196,10 @@ public function param($name)
*/
public function getParams()
{
- return array_merge($this->_search_params, array(
- 'min_msg_id' => (int) $this->_minMsgID,
- 'max_msg_id' => (int) $this->_maxMsgID,
- 'memberlist' => $this->_memberlist,
+ return array_merge($this->_searchParams, array(
+ 'min_msg_id' => (int) $this->_searchParams->_minMsgID,
+ 'max_msg_id' => (int) $this->_searchParams->_maxMsgID,
+ 'memberlist' => $this->_searchParams->_memberlist,
));
}
@@ -321,41 +208,30 @@ public function getParams()
*/
public function getIgnored()
{
- return $this->_ignored;
- }
-
- /**
- * Returns words excluded from indexes
- */
- public function getExcludedIndexWords()
- {
- return $this->_excludedIndexWords;
+ return $this->_searchArray->getIgnored();
}
/**
* Set the weight factors
*
- * @param mixed[] $weight_factors
- * @param mixed[] $weight - weight for each factor
- * @param int $weight_total - som of all the weights
+ * @param \ElkArte\Search\WeightFactors $weight
*/
- public function setWeights($weight_factors, $weight, $weight_total)
+ public function setWeights(\ElkArte\Search\WeightFactors $weight)
{
- $this->_weight_factors = $weight_factors;
-
- $this->_weight = $weight;
-
- $this->_weight_total = $weight_total;
+ $this->_weightFactors = $weight;
}
- /**
- * If the query uses regexp or not
- *
- * @return bool
- */
- public function noRegexp()
+ public function setParams(SearchParams $paramObject, $search_simple_fulltext = false)
{
- return $this->_no_regexp;
+ $this->_searchParams = $paramObject;
+
+ // Unfortunately, searching for words like this is going to be slow, so we're blacklisting them.
+ // @todo Setting to add more here?
+ // @todo Maybe only blacklist if they are the only word, or "any" is used?
+ $blacklisted_words = array('img', 'url', 'quote', 'www', 'http', 'the', 'is', 'it', 'are', 'if');
+ call_integration_hook('integrate_search_blacklisted_words', array(&$blacklisted_words));
+
+ $this->_searchArray = new \ElkArte\Search\SearchArray($this->_searchParams->search, $blacklisted_words, $search_simple_fulltext);
}
/**
@@ -365,132 +241,46 @@ public function noRegexp()
*/
public function foundBlackListedWords()
{
- return $this->_foundBlackListedWords;
+ return $this->_searchArray->foundBlackListedWords();
}
- /**
- * Builds the search array
- *
- * @param bool - Force splitting of strings enclosed in double quotes
- *
- * @return 0|array
- */
- public function searchArray($search_simple_fulltext = false)
+ public function getSearchArray()
{
- // Change non-word characters into spaces.
- $stripped_query = preg_replace('~(?:[\x0B\0\x{A0}\t\r\s\n(){}\\[\\]<>!@$%^*.,:+=`\~\?/\\\\]+|&(?:amp|lt|gt|quot);)+~u', ' ', $this->param('search'));
-
- // Make the query lower case. It's gonna be case insensitive anyway.
- $stripped_query = un_htmlspecialchars(\Util::strtolower($stripped_query));
-
- // This option will do fulltext searching in the most basic way.
- if ($search_simple_fulltext)
- {
- $stripped_query = strtr($stripped_query, array('"' => ''));
- }
-
- $this->_no_regexp = preg_match('~(?:\d{1,7}|x[0-9a-fA-F]{1,6});~', $stripped_query) === 1;
-
- // Extract phrase parts first (e.g. some words "this is a phrase" some more words.)
- preg_match_all('/(?:^|\s)([-]?)"([^"]+)"(?:$|\s)/', $stripped_query, $matches, PREG_PATTERN_ORDER);
- $phraseArray = $matches[2];
-
- // Remove the phrase parts and extract the words.
- $wordArray = preg_replace('~(?:^|\s)(?:[-]?)"(?:[^"]+)"(?:$|\s)~u', ' ', $this->param('search'));
- $wordArray = explode(' ', \Util::htmlspecialchars(un_htmlspecialchars($wordArray), ENT_QUOTES));
-
- // A minus sign in front of a word excludes the word.... so...
- // .. first, we check for things like -"some words", but not "-some words".
- $phraseArray = $this->_checkExcludePhrase($matches[1], $phraseArray);
-
- // Now we look for -test, etc.... normaller.
- $wordArray = $this->_checkExcludeWord($wordArray);
-
- // The remaining words and phrases are all included.
- $this->_searchArray = array_merge($phraseArray, $wordArray);
-
- // Trim everything and make sure there are no words that are the same.
- foreach ($this->_searchArray as $index => $value)
- {
- // Skip anything practically empty.
- if (($this->_searchArray[$index] = trim($value, '-_\' ')) === '')
- {
- unset($this->_searchArray[$index]);
- }
- // Skip blacklisted words. Make sure to note we skipped them in case we end up with nothing.
- elseif (in_array($this->_searchArray[$index], $this->_blacklisted_words))
- {
- $this->_foundBlackListedWords = true;
- unset($this->_searchArray[$index]);
- }
- // Don't allow very, very short words.
- elseif (\Util::strlen($value) < 2)
- {
- $this->_ignored[] = $value;
- unset($this->_searchArray[$index]);
- }
- }
-
- $this->_searchArray = array_slice(array_unique($this->_searchArray), 0, 10);
-
- return $this->_searchArray;
+ return $this->_searchArray->getSearchArray();
}
- /**
- * Looks for phrases that should be excluded from results
- *
- * - Check for things like -"some words", but not "-some words"
- * - Prevents redundancy with blacklisted words
- *
- * @param string[] $matches
- * @param string[] $phraseArray
- *
- * @return string[]
- */
- private function _checkExcludePhrase($matches, $phraseArray)
+ public function getExcludedWords()
{
- foreach ($matches as $index => $word)
- {
- if ($word === '-')
- {
- if (($word = trim($phraseArray[$index], '-_\' ')) !== '' && !in_array($word, $this->_blacklisted_words))
- {
- $this->_excludedWords[] = $word;
- }
-
- unset($phraseArray[$index]);
- }
- }
+ return $this->_searchArray->getExcludedWords();
+ }
- return $phraseArray;
+ public function getExcludedSubjectWords()
+ {
+ return $this->_excludedSubjectWords;
}
/**
- * Looks for words that should be excluded in the results (-word)
- *
- * - Look for -test, etc
- * - Prevents excluding blacklisted words since it is redundant
+ * Returns the search parameters.
*
- * @param string[] $wordArray
+ * @param bool $array If true returns an array, otherwise an object
*
- * @return string[]
+ * @return mixed
*/
- private function _checkExcludeWord($wordArray)
+ public function getSearchParams($array = false)
{
- foreach ($wordArray as $index => $word)
+ if ($array === true)
{
- if (strpos(trim($word), '-') === 0)
- {
- if (($word = trim($word, '-_\' ')) !== '' && !in_array($word, $this->_blacklisted_words))
- {
- $this->_excludedWords[] = $word;
- }
-
- unset($wordArray[$index]);
- }
+ return $this->_searchParams->get();
+ }
+ else
+ {
+ return $this->_searchParams;
}
+ }
- return $wordArray;
+ public function getExcludedPhrases()
+ {
+ return $this->_excludedPhrases;
}
/**
@@ -507,23 +297,25 @@ public function searchWords()
$orParts = array();
$this->_searchWords = array();
+ $searchArray = $this->_searchArray->getSearchArray();
+ $excludedWords = $this->_searchArray->getExcludedWords();
// All words/sentences must match.
- if (!empty($this->_searchArray) && empty($this->_search_params['searchtype']))
+ if (!empty($searchArray) && empty($this->_searchParams['searchtype']))
{
- $orParts[0] = $this->_searchArray;
+ $orParts[0] = $searchArray;
}
// Any word/sentence must match.
else
{
- foreach ($this->_searchArray as $index => $value)
+ foreach ($searchArray as $index => $value)
$orParts[$index] = array($value);
}
// Make sure the excluded words are in all or-branches.
foreach ($orParts as $orIndex => $andParts)
{
- foreach ($this->_excludedWords as $word)
+ foreach ($excludedWords as $word)
{
$orParts[$orIndex][] = $word;
}
@@ -540,16 +332,13 @@ public function searchWords()
'complex_words' => array(),
);
+ $this->_searchAPI->setExcludedWords($excludedWords);
// Sort the indexed words (large words -> small words -> excluded words).
- if (is_callable(array($this->_searchAPI, 'searchSort')))
- {
- $this->_searchAPI->setExcludedWords($this->_excludedWords);
- usort($orParts[$orIndex], array($this->_searchAPI, 'searchSort'));
- }
+ usort($orParts[$orIndex], array($this->_searchAPI, 'searchSort'));
foreach ($orParts[$orIndex] as $word)
{
- $is_excluded = in_array($word, $this->_excludedWords);
+ $is_excluded = in_array($word, $excludedWords);
$this->_searchWords[$orIndex]['all_words'][] = $word;
$subjectWords = text2words($word);
@@ -568,10 +357,7 @@ public function searchWords()
}
// Have we got indexes to prepare?
- if (is_callable(array($this->_searchAPI, 'prepareIndexes')))
- {
- $this->_searchAPI->prepareIndexes($word, $this->_searchWords[$orIndex], $this->_excludedIndexWords, $is_excluded);
- }
+ $this->_searchAPI->prepareIndexes($word, $this->_searchWords[$orIndex], $this->_excludedIndexWords, $is_excluded, $this->_excludedSubjectWords);
}
// Search_force_index requires all AND parts to have at least one fulltext word.
@@ -580,7 +366,7 @@ public function searchWords()
$context['search_errors']['query_not_specific_enough'] = true;
break;
}
- elseif ($this->param('subject_only') && empty($this->_searchWords[$orIndex]['subject_words']) && empty($this->_excludedSubjectWords))
+ elseif ($this->_searchParams->subject_only && empty($this->_searchWords[$orIndex]['subject_words']) && empty($this->_excludedSubjectWords))
{
$context['search_errors']['query_not_specific_enough'] = true;
break;
@@ -598,7 +384,15 @@ public function searchWords()
}
/**
- * Encodes search params ($this->_search_params) in an URL-compatible way
+ * Tell me, do I want to see the full message or just a piece?
+ */
+ public function isCompact()
+ {
+ return empty($this->_searchParams['show_complete']);
+ }
+
+ /**
+ * Wrapper around SearchParams::compileURL
*
* @param array $search build param index with specific search term (did you mean?)
*
@@ -606,1398 +400,134 @@ public function searchWords()
*/
public function compileURLparams($search = array())
{
- $temp_params = $this->_search_params;
- $encoded = array();
-
- if (!empty($search))
- {
- $temp_params['search'] = implode(' ', $search);
- }
-
- // *** Encode all search params
- // All search params have been checked, let's compile them to a single string... made less simple by PHP 4.3.9 and below.
- if (isset($temp_params['brd']))
- {
- $temp_params['brd'] = implode(',', $temp_params['brd']);
- }
-
- foreach ($temp_params as $k => $v)
- $encoded[] = $k . '|\'|' . $v;
-
- if (!empty($encoded))
- {
- // Due to old IE's 2083 character limit, we have to compress long search strings
- $params = @gzcompress(implode('|"|', $encoded));
-
- // Gzcompress failed, use try non-gz
- if (empty($params))
- {
- $params = implode('|"|', $encoded);
- }
+ return $this->_searchParams->compileURL($search);
+ }
- // Base64 encode, then replace +/= with uri safe ones that can be reverted
- $encoded = str_replace(array('+', '/', '='), array('-', '_', '.'), base64_encode($params));
- }
- else
- {
- $encoded = '';
- }
+ /**
+ * Finds the posters of the messages
+ *
+ * @param int[] $msg_list - All the messages we want to find the posters
+ * @param int $limit - There are only so much topics
+ *
+ * @return int[] - array of members id
+ */
+ public function loadPosters($msg_list, $limit)
+ {
+ // Load the posters...
+ $request = $this->_db->query('', '
+ SELECT
+ id_member
+ FROM {db_prefix}messages
+ WHERE id_member != {int:no_member}
+ AND id_msg IN ({array_int:message_list})
+ LIMIT {int:limit}',
+ array(
+ 'message_list' => $msg_list,
+ 'limit' => $limit,
+ 'no_member' => 0,
+ )
+ );
+ $posters = array();
+ while ($row = $this->_db->fetch_assoc($request))
+ $posters[] = $row['id_member'];
+ $this->_db->free_result($request);
- return $encoded;
+ return $posters;
}
/**
- * Extract search params from a string
+ * Finds the posters of the messages
*
- * @param string $string - the string containing encoded search params
+ * @param int[] $msg_list - All the messages we want to find the posters
+ * @param int $limit - There are only so much topics
+ *
+ * @return resource
*/
- public function searchParamsFromString($string)
+ public function loadMessagesRequest($msg_list, $limit)
{
- // Due to IE's 2083 character limit, we have to compress long search strings
- $temp_params = base64_decode(str_replace(array('-', '_', '.'), array('+', '/', '='), $string));
-
- // Test for gzuncompress failing
- $temp_params2 = @gzuncompress($temp_params);
- $temp_params = explode('|"|', (!empty($temp_params2) ? $temp_params2 : $temp_params));
+ global $modSettings;
- foreach ($temp_params as $i => $data)
- {
- list($k, $v) = array_pad(explode('|\'|', $data), 2, '');
- $this->_search_params[$k] = $v;
- }
+ $request = $this->_db->query('', '
+ SELECT
+ m.id_msg, m.subject, m.poster_name, m.poster_email, m.poster_time,
+ m.id_member, m.icon, m.poster_ip, m.body, m.smileys_enabled,
+ m.modified_time, m.modified_name, first_m.id_msg AS id_first_msg,
+ first_m.subject AS first_subject, first_m.icon AS first_icon,
+ first_m.poster_time AS first_poster_time,
+ first_mem.id_member AS first_id_member,
+ COALESCE(first_mem.real_name, first_m.poster_name) AS first_display_name,
+ COALESCE(first_mem.member_name, first_m.poster_name) AS first_member_name,
+ last_m.id_msg AS id_last_msg, last_m.poster_time AS last_poster_time,
+ last_mem.id_member AS last_id_member,
+ COALESCE(last_mem.real_name, last_m.poster_name) AS last_display_name,
+ COALESCE(last_mem.member_name, last_m.poster_name) AS last_member_name,
+ last_m.icon AS last_icon, last_m.subject AS last_subject,
+ t.id_topic, t.is_sticky, t.locked, t.id_poll, t.num_replies,
+ t.num_views, t.num_likes,
+ b.id_board, b.name AS bname, c.id_cat, c.name AS cat_name
+ FROM {db_prefix}messages AS m
+ INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic)
+ INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
+ INNER JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat)
+ INNER JOIN {db_prefix}messages AS first_m ON (first_m.id_msg = t.id_first_msg)
+ INNER JOIN {db_prefix}messages AS last_m ON (last_m.id_msg = t.id_last_msg)
+ LEFT JOIN {db_prefix}members AS first_mem ON (first_mem.id_member = first_m.id_member)
+ LEFT JOIN {db_prefix}members AS last_mem ON (last_mem.id_member = first_m.id_member)
+ WHERE m.id_msg IN ({array_int:message_list})' . ($modSettings['postmod_active'] ? '
+ AND m.approved = {int:is_approved}' : '') . '
+ ORDER BY FIND_IN_SET(m.id_msg, {string:message_list_in_set})
+ LIMIT {int:limit}',
+ array(
+ 'message_list' => $msg_list,
+ 'is_approved' => 1,
+ 'message_list_in_set' => implode(',', $msg_list),
+ 'limit' => $limit,
+ )
+ );
- if (isset($this->_search_params['brd']))
- {
- $this->_search_params['brd'] = empty($this->_search_params['brd']) ? array() : explode(',', $this->_search_params['brd']);
- }
+ return $request;
}
/**
- * Merge search params extracted with Search::searchParamsFromString
- * with those present in the $param array (usually $_REQUEST['params'])
+ * Did the user find any message at all?
*
- * @param mixed[] $params - An array of search parameters
- * @param int $recentPercentage - A coefficient to calculate the lowest
- * message id to start search from
- * @param int $maxMembersToSearch - The maximum number of members to consider
- * when multiple are found
+ * @param resource $messages_request holds a query result
*
- * @throws \Elk_Exception topic_gone
+ * @return boolean
*/
- public function mergeSearchParams($params, $recentPercentage, $maxMembersToSearch)
+ public function noMessages($messages_request)
{
- global $user_info, $modSettings, $context;
+ return $this->_db->num_rows($messages_request) == 0;
+ }
- // Store whether simple search was used (needed if the user wants to do another query).
- if (!isset($this->_search_params['advanced']))
- {
- $this->_search_params['advanced'] = empty($params['advanced']) ? 0 : 1;
- }
+ public function searchQuery(\ElkArte\Search\SearchApiWrapper $searchAPI)
+ {
+ $this->_searchAPI = $searchAPI;
+ $searchAPI->setExcludedPhrases($this->_excludedPhrases);
+ $searchAPI->setWeightFactors($this->_weightFactors);
+ $searchAPI->useTemporary($this->_createTemporary);
+ $searchAPI->setSearchArray($this->_searchArray);
+
+ return $searchAPI->searchQuery(
+ $this->searchWords(),
+ $this->_excludedIndexWords,
+ $this->_participants,
+ $this->_searchAPI
+ );
+ }
- // 1 => 'allwords' (default, don't set as param) / 2 => 'anywords'.
- if (!empty($this->_search_params['searchtype']) || (!empty($params['searchtype']) && $params['searchtype'] == 2))
- {
- $this->_search_params['searchtype'] = 2;
- }
+ /**
+ * Returns the number of results obtained from the query.
+ *
+ * @return int
+ */
+ public function getNumResults()
+ {
+ return $this->_searchAPI->getNumResults();
+ }
- // Minimum age of messages. Default to zero (don't set param in that case).
- if (!empty($this->_search_params['minage']) || (!empty($params['minage']) && $params['minage'] > 0))
- {
- $this->_search_params['minage'] = !empty($this->_search_params['minage']) ? (int) $this->_search_params['minage'] : (int) $params['minage'];
- }
-
- // Maximum age of messages. Default to infinite (9999 days: param not set).
- if (!empty($this->_search_params['maxage']) || (!empty($params['maxage']) && $params['maxage'] < 9999))
- {
- $this->_search_params['maxage'] = !empty($this->_search_params['maxage']) ? (int) $this->_search_params['maxage'] : (int) $params['maxage'];
- }
-
- // Searching a specific topic?
- if (!empty($params['topic']) || (!empty($params['search_selection']) && $params['search_selection'] === 'topic'))
- {
- $this->_search_params['topic'] = empty($params['search_selection']) ? (int) $params['topic'] : (isset($params['sd_topic']) ? (int) $params['sd_topic'] : '');
- $this->_search_params['show_complete'] = true;
- }
- elseif (!empty($this->_search_params['topic']))
- {
- $this->_search_params['topic'] = (int) $this->_search_params['topic'];
- }
-
- if (!empty($this->_search_params['minage']) || !empty($this->_search_params['maxage']))
- {
- $request = $this->_db->query('', '
- SELECT ' . (empty($this->_search_params['maxage']) ? '0, ' : 'COALESCE(MIN(id_msg), -1), ') . (empty($this->_search_params['minage']) ? '0' : 'COALESCE(MAX(id_msg), -1)') . '
- FROM {db_prefix}messages
- WHERE 1=1' . ($modSettings['postmod_active'] ? '
- AND approved = {int:is_approved_true}' : '') . (empty($this->_search_params['minage']) ? '' : '
- AND poster_time <= {int:timestamp_minimum_age}') . (empty($this->_search_params['maxage']) ? '' : '
- AND poster_time >= {int:timestamp_maximum_age}'),
- array(
- 'timestamp_minimum_age' => empty($this->_search_params['minage']) ? 0 : time() - 86400 * $this->_search_params['minage'],
- 'timestamp_maximum_age' => empty($this->_search_params['maxage']) ? 0 : time() - 86400 * $this->_search_params['maxage'],
- 'is_approved_true' => 1,
- )
- );
- list ($this->_minMsgID, $this->_maxMsgID) = $this->_db->fetch_row($request);
- if ($this->_minMsgID < 0 || $this->_maxMsgID < 0)
- {
- $context['search_errors']['no_messages_in_time_frame'] = true;
- }
- $this->_db->free_result($request);
- }
-
- // Default the user name to a wildcard matching every user (*).
- if (!empty($this->_search_params['userspec']) || (!empty($params['userspec']) && $params['userspec'] != '*'))
- {
- $this->_search_params['userspec'] = isset($this->_search_params['userspec']) ? $this->_search_params['userspec'] : $params['userspec'];
- }
-
- // If there's no specific user, then don't mention it in the main query.
- if (empty($this->_search_params['userspec']))
- {
- $this->_userQuery = '';
- }
- else
- {
- $userString = strtr(\Util::htmlspecialchars($this->_search_params['userspec'], ENT_QUOTES), array('"' => '"'));
- $userString = strtr($userString, array('%' => '\%', '_' => '\_', '*' => '%', '?' => '_'));
-
- preg_match_all('~"([^"]+)"~', $userString, $matches);
- $possible_users = array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $userString)));
-
- for ($k = 0, $n = count($possible_users); $k < $n; $k++)
- {
- $possible_users[$k] = trim($possible_users[$k]);
-
- if (strlen($possible_users[$k]) == 0)
- {
- unset($possible_users[$k]);
- }
- }
-
- // Create a list of database-escaped search names.
- $realNameMatches = array();
- foreach ($possible_users as $possible_user)
- $realNameMatches[] = $this->_db->quote(
- '{string:possible_user}',
- array(
- 'possible_user' => $possible_user
- )
- );
-
- // Retrieve a list of possible members.
- $request = $this->_db->query('', '
- SELECT
- id_member
- FROM {db_prefix}members
- WHERE {raw:match_possible_users}',
- array(
- 'match_possible_users' => 'real_name LIKE ' . implode(' OR real_name LIKE ', $realNameMatches),
- )
- );
-
- // Simply do nothing if there're too many members matching the criteria.
- if ($this->_db->num_rows($request) > $maxMembersToSearch)
- {
- $this->_userQuery = '';
- }
- elseif ($this->_db->num_rows($request) == 0)
- {
- $this->_userQuery = $this->_db->quote(
- 'm.id_member = {int:id_member_guest} AND ({raw:match_possible_guest_names})',
- array(
- 'id_member_guest' => 0,
- 'match_possible_guest_names' => 'm.poster_name LIKE ' . implode(' OR m.poster_name LIKE ', $realNameMatches),
- )
- );
- }
- else
- {
- while ($row = $this->_db->fetch_assoc($request))
- {
- $this->_memberlist[] = $row['id_member'];
- }
-
- $this->_userQuery = $this->_db->quote(
- '(m.id_member IN ({array_int:matched_members}) OR (m.id_member = {int:id_member_guest} AND ({raw:match_possible_guest_names})))',
- array(
- 'matched_members' => $this->_memberlist,
- 'id_member_guest' => 0,
- 'match_possible_guest_names' => 'm.poster_name LIKE ' . implode(' OR m.poster_name LIKE ', $realNameMatches),
- )
- );
- }
- $this->_db->free_result($request);
- }
-
- // Ensure that boards are an array of integers (or nothing).
- if (!empty($this->_search_params['brd']) && is_array($this->_search_params['brd']))
- {
- $query_boards = array_map('intval', $this->_search_params['brd']);
- }
- elseif (!empty($params['brd']) && is_array($params['brd']))
- {
- $query_boards = array_map('intval', $params['brd']);
- }
- elseif (!empty($params['brd']))
- {
- $query_boards = array_map('intval', explode(',', $params['brd']));
- }
- elseif (!empty($params['search_selection']) && $params['search_selection'] === 'board' && !empty($params['sd_brd']) && is_array($params['sd_brd']))
- {
- $query_boards = array_map('intval', $params['sd_brd']);
- }
- elseif (!empty($params['search_selection']) && $params['search_selection'] === 'board' && isset($params['sd_brd']) && (int) $params['sd_brd'] !== 0)
- {
- $query_boards = array((int) $params['sd_brd']);
- }
- else
- {
- $query_boards = array();
- }
-
- // Special case for boards: searching just one topic?
- if (!empty($this->_search_params['topic']))
- {
- $request = $this->_db->query('', '
- SELECT
- b.id_board
- FROM {db_prefix}topics AS t
- INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
- WHERE t.id_topic = {int:search_topic_id}
- AND {query_see_board}' . ($modSettings['postmod_active'] ? '
- AND t.approved = {int:is_approved_true}' : '') . '
- LIMIT 1',
- array(
- 'search_topic_id' => $this->_search_params['topic'],
- 'is_approved_true' => 1,
- )
- );
-
- if ($this->_db->num_rows($request) == 0)
- {
- throw new \Elk_Exception('topic_gone', false);
- }
-
- $this->_search_params['brd'] = array();
- list ($this->_search_params['brd'][0]) = $this->_db->fetch_row($request);
- $this->_db->free_result($request);
- }
- // Select all boards you've selected AND are allowed to see.
- elseif ($user_info['is_admin'] && (!empty($this->_search_params['advanced']) || !empty($query_boards)))
- {
- $this->_search_params['brd'] = $query_boards;
- }
- else
- {
- require_once(SUBSDIR . '/Boards.subs.php');
- $this->_search_params['brd'] = array_keys(fetchBoardsInfo(array('boards' => $query_boards), array('include_recycle' => false, 'include_redirects' => false, 'wanna_see_board' => empty($this->_search_params['advanced']))));
-
- // This error should pro'bly only happen for hackers.
- if (empty($this->_search_params['brd']))
- {
- $context['search_errors']['no_boards_selected'] = true;
- }
- }
-
- if (count($this->_search_params['brd']) != 0)
- {
- foreach ($this->_search_params['brd'] as $k => $v)
- $this->_search_params['brd'][$k] = (int) $v;
-
- // If we've selected all boards, this parameter can be left empty.
- require_once(SUBSDIR . '/Boards.subs.php');
- $num_boards = countBoards();
-
- if (count($this->_search_params['brd']) == $num_boards)
- {
- $this->_boardQuery = '';
- }
- elseif (count($this->_search_params['brd']) == $num_boards - 1 && !empty($modSettings['recycle_board']) && !in_array($modSettings['recycle_board'], $this->_search_params['brd']))
- {
- $this->_boardQuery = '!= ' . $modSettings['recycle_board'];
- }
- else
- {
- $this->_boardQuery = 'IN (' . implode(', ', $this->_search_params['brd']) . ')';
- }
- }
- else
- {
- $this->_boardQuery = '';
- }
-
- $this->_search_params['show_complete'] = !empty($this->_search_params['show_complete']) || !empty($params['show_complete']);
- $this->_search_params['subject_only'] = !empty($this->_search_params['subject_only']) || !empty($params['subject_only']);
-
- // Get the sorting parameters right. Default to sort by relevance descending.
- $sort_columns = array(
- 'relevance',
- 'num_replies',
- 'id_msg',
- );
-
- // Allow integration to add additional sort columns
- call_integration_hook('integrate_search_sort_columns', array(&$sort_columns));
-
- if (empty($this->_search_params['sort']) && !empty($params['sort']))
- {
- list ($this->_search_params['sort'], $this->_search_params['sort_dir']) = array_pad(explode('|', $params['sort']), 2, '');
- }
-
- $this->_search_params['sort'] = !empty($this->_search_params['sort']) && in_array($this->_search_params['sort'], $sort_columns) ? $this->_search_params['sort'] : 'relevance';
-
- if (!empty($this->_search_params['topic']) && $this->_search_params['sort'] === 'num_replies')
- {
- $this->_search_params['sort'] = 'id_msg';
- }
-
- // Sorting direction: descending unless stated otherwise.
- $this->_search_params['sort_dir'] = !empty($this->_search_params['sort_dir']) && $this->_search_params['sort_dir'] === 'asc' ? 'asc' : 'desc';
-
- // Determine some values needed to calculate the relevance.
- $this->_minMsg = (int) ((1 - $recentPercentage) * $modSettings['maxMsgID']);
- $this->_recentMsg = $modSettings['maxMsgID'] - $this->_minMsg;
-
- // *** Parse the search query
- call_integration_hook('integrate_search_params', array(&$this->_search_params));
-
- // Unfortunately, searching for words like this is going to be slow, so we're blacklisting them.
- // @todo Setting to add more here?
- // @todo Maybe only blacklist if they are the only word, or "any" is used?
- $this->_blacklisted_words = array('img', 'url', 'quote', 'www', 'http', 'the', 'is', 'it', 'are', 'if');
- call_integration_hook('integrate_search_blacklisted_words', array(&$this->_blacklisted_words));
-
- // What are we searching for?
- if (empty($this->_search_params['search']))
- {
- if (isset($_GET['search']))
- {
- $this->_search_params['search'] = un_htmlspecialchars($_GET['search']);
- }
- elseif (isset($_POST['search']))
- {
- $this->_search_params['search'] = $_POST['search'];
- }
- else
- {
- $this->_search_params['search'] = '';
- }
- }
- }
-
- /**
- * Tell me, do I want to see the full message or just a piece?
- */
- public function isCompact()
- {
- return empty($this->_search_params['show_complete']);
- }
-
- /**
- * Setup spellchecking suggestions and load them into the two variable
- * passed by ref
- *
- * @param string $suggestion_display - the string to display in the template
- * @param string $suggestion_param - a param string to be used in a url
- * @param string $display_highlight - a template to enclose in each suggested word
- */
- public function loadSuggestions(&$suggestion_display = '', &$suggestion_param = '', $display_highlight = '')
- {
- global $txt;
-
- // Windows fix.
- ob_start();
- $old = error_reporting(0);
-
- pspell_new('en');
- $pspell_link = pspell_new($txt['lang_dictionary'], $txt['lang_spelling'], '', 'utf-8', PSPELL_FAST | PSPELL_RUN_TOGETHER);
-
- if (!$pspell_link)
- {
- $pspell_link = pspell_new('en', '', '', '', PSPELL_FAST | PSPELL_RUN_TOGETHER);
- }
-
- error_reporting($old);
- @ob_end_clean();
-
- $did_you_mean = array('search' => array(), 'display' => array());
- $found_misspelling = false;
- foreach ($this->_searchArray as $word)
- {
- if (empty($pspell_link))
- {
- continue;
- }
-
- // Don't check phrases.
- if (preg_match('~^\w+$~', $word) === 0)
- {
- $did_you_mean['search'][] = '"' . $word . '"';
- $did_you_mean['display'][] = '"' . \Util::htmlspecialchars($word) . '"';
- continue;
- }
- // For some strange reason spell check can crash PHP on decimals.
- elseif (preg_match('~\d~', $word) === 1)
- {
- $did_you_mean['search'][] = $word;
- $did_you_mean['display'][] = \Util::htmlspecialchars($word);
- continue;
- }
- elseif (pspell_check($pspell_link, $word))
- {
- $did_you_mean['search'][] = $word;
- $did_you_mean['display'][] = \Util::htmlspecialchars($word);
- continue;
- }
-
- $suggestions = pspell_suggest($pspell_link, $word);
- foreach ($suggestions as $i => $s)
- {
- // Search is case insensitive.
- if (\Util::strtolower($s) == \Util::strtolower($word))
- {
- unset($suggestions[$i]);
- }
- // Plus, don't suggest something the user thinks is rude!
- elseif ($suggestions[$i] != censor($s))
- {
- unset($suggestions[$i]);
- }
- }
-
- // Anything found? If so, correct it!
- if (!empty($suggestions))
- {
- $suggestions = array_values($suggestions);
- $did_you_mean['search'][] = $suggestions[0];
- $did_you_mean['display'][] = str_replace('{word}', \Util::htmlspecialchars($suggestions[0]), $display_highlight);
- $found_misspelling = true;
- }
- else
- {
- $did_you_mean['search'][] = $word;
- $did_you_mean['display'][] = \Util::htmlspecialchars($word);
- }
- }
-
- if ($found_misspelling)
- {
- // Don't spell check excluded words, but add them still...
- $temp_excluded = array('search' => array(), 'display' => array());
- foreach ($this->_excludedWords as $word)
- {
- if (preg_match('~^\w+$~', $word) == 0)
- {
- $temp_excluded['search'][] = '-"' . $word . '"';
- $temp_excluded['display'][] = '-"' . \Util::htmlspecialchars($word) . '"';
- }
- else
- {
- $temp_excluded['search'][] = '-' . $word;
- $temp_excluded['display'][] = '-' . \Util::htmlspecialchars($word);
- }
- }
-
- $did_you_mean['search'] = array_merge($did_you_mean['search'], $temp_excluded['search']);
- $did_you_mean['display'] = array_merge($did_you_mean['display'], $temp_excluded['display']);
-
- // Provide the potential correct spelling term in the param
- $suggestion_param = $this->compileURLparams($did_you_mean['search']);
- $suggestion_display = implode(' ', $did_you_mean['display']);
- }
- }
-
- /**
- * Delete logs of previous searches
- *
- * @param int $id_search - the id of the search to delete from logs
- */
- public function clearCacheResults($id_search)
- {
- $this->_db_search->search_query('delete_log_search_results', '
- DELETE FROM {db_prefix}log_search_results
- WHERE id_search = {int:search_id}',
- array(
- 'search_id' => $id_search,
- )
- );
- }
-
- /**
- * Grabs results when the search is performed only within the subject
- *
- * @param int $id_search - the id of the search
- * @param int $humungousTopicPosts - Message length used to tweak messages
- * relevance of the results.
- *
- * @return int - number of results otherwise
- */
- public function getSubjectResults($id_search, $humungousTopicPosts)
- {
- global $modSettings;
-
- // We do this to try and avoid duplicate keys on databases not supporting INSERT IGNORE.
- foreach ($this->_searchWords as $words)
- {
- $subject_query_params = array();
- $subject_query = array(
- 'from' => '{db_prefix}topics AS t',
- 'inner_join' => array(),
- 'left_join' => array(),
- 'where' => array(),
- );
-
- if ($modSettings['postmod_active'])
- {
- $subject_query['where'][] = 't.approved = {int:is_approved}';
- }
-
- $numTables = 0;
- $prev_join = 0;
- $numSubjectResults = 0;
- foreach ($words['subject_words'] as $subjectWord)
- {
- $numTables++;
- if (in_array($subjectWord, $this->_excludedSubjectWords))
- {
- $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)';
- $subject_query['where'][] = '(subj' . $numTables . '.word IS NULL)';
- }
- else
- {
- $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)';
- $subject_query['where'][] = 'subj' . $numTables . '.word ' . (empty($modSettings['search_match_words']) ? 'LIKE {string:subject_words_' . $numTables . '_wild}' : '= {string:subject_words_' . $numTables . '}');
- $prev_join = $numTables;
- }
-
- $subject_query_params['subject_words_' . $numTables] = $subjectWord;
- $subject_query_params['subject_words_' . $numTables . '_wild'] = '%' . $subjectWord . '%';
- }
-
- if (!empty($this->_userQuery))
- {
- $subject_query['inner_join'][] = '{db_prefix}messages AS m ON (m.id_topic = t.id_topic)';
- $subject_query['where'][] = $this->_userQuery;
- }
-
- if (!empty($this->_search_params['topic']))
- {
- $subject_query['where'][] = 't.id_topic = ' . $this->_search_params['topic'];
- }
-
- if (!empty($this->_minMsgID))
- {
- $subject_query['where'][] = 't.id_first_msg >= ' . $this->_minMsgID;
- }
-
- if (!empty($this->_maxMsgID))
- {
- $subject_query['where'][] = 't.id_last_msg <= ' . $this->_maxMsgID;
- }
-
- if (!empty($this->_boardQuery))
- {
- $subject_query['where'][] = 't.id_board ' . $this->_boardQuery;
- }
-
- if (!empty($this->_excludedPhrases))
- {
- $subject_query['inner_join'][] = '{db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)';
-
- $count = 0;
- foreach ($this->_excludedPhrases as $phrase)
- {
- $subject_query['where'][] = 'm.subject NOT ' . (empty($modSettings['search_match_words']) || $this->noRegexp() ? 'LIKE' : 'RLIKE') . ' {string:excluded_phrases_' . $count . '}';
- $subject_query_params['excluded_phrases_' . ($count++)] = $this->_searchAPI->prepareWord($phrase, $this->noRegexp());
- }
- }
-
- // Build the search query
- $subject_query['select'] = array(
- 'id_search' => '{int:id_search}',
- 'id_topic' => 't.id_topic',
- 'relevance' => $this->_build_relevance(),
- 'id_msg' => empty($this->_userQuery) ? 't.id_first_msg' : 'm.id_msg',
- 'num_matches' => 1,
- );
-
- $subject_query['parameters'] = array_merge($subject_query_params, array(
- 'id_search' => $id_search,
- 'min_msg' => $this->_minMsg,
- 'recent_message' => $this->_recentMsg,
- 'huge_topic_posts' => $humungousTopicPosts,
- 'is_approved' => 1,
- 'limit' => empty($modSettings['search_max_results']) ? 0 : $modSettings['search_max_results'] - $numSubjectResults,
- ));
-
- call_integration_hook('integrate_subject_only_search_query', array(&$subject_query, &$subject_query_params));
-
- $numSubjectResults += $this->_build_search_results_log($subject_query, 'insert_log_search_results_subject');
-
- if (!empty($modSettings['search_max_results']) && $numSubjectResults >= $modSettings['search_max_results'])
- {
- break;
- }
- }
-
- return empty($numSubjectResults) ? 0 : $numSubjectResults;
- }
-
- /**
- * Grabs results when the search is performed in subjects and bodies
- *
- * @param int $id_search - the id of the search
- * @param int $humungousTopicPosts - Message length used to tweak messages relevance of the results.
- * @param int $maxMessageResults - Maximum number of results
- *
- * @return bool|int - boolean (false) in case of errors, number of results otherwise
- */
- public function getResults($id_search, $humungousTopicPosts, $maxMessageResults)
- {
- global $modSettings;
-
- $num_results = 0;
-
- $main_query = array(
- 'select' => array(
- 'id_search' => $id_search,
- 'relevance' => '0',
- ),
- 'weights' => array(),
- 'from' => '{db_prefix}topics AS t',
- 'inner_join' => array(
- '{db_prefix}messages AS m ON (m.id_topic = t.id_topic)'
- ),
- 'left_join' => array(),
- 'where' => array(),
- 'group_by' => array(),
- 'parameters' => array(
- 'min_msg' => $this->_minMsg,
- 'recent_message' => $this->_recentMsg,
- 'huge_topic_posts' => $humungousTopicPosts,
- 'is_approved' => 1,
- 'limit' => $modSettings['search_max_results'],
- ),
- );
-
- if (empty($this->_search_params['topic']) && empty($this->_search_params['show_complete']))
- {
- $main_query['select']['id_topic'] = 't.id_topic';
- $main_query['select']['id_msg'] = 'MAX(m.id_msg) AS id_msg';
- $main_query['select']['num_matches'] = 'COUNT(*) AS num_matches';
- $main_query['weights'] = $this->_weight_factors;
- $main_query['group_by'][] = 't.id_topic';
- }
- else
- {
- // This is outrageous!
- $main_query['select']['id_topic'] = 'm.id_msg AS id_topic';
- $main_query['select']['id_msg'] = 'm.id_msg';
- $main_query['select']['num_matches'] = '1 AS num_matches';
-
- $main_query['weights'] = array(
- 'age' => array(
- '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)',
- ),
- 'first_message' => array(
- 'search' => 'CASE WHEN m.id_msg = t.id_first_msg THEN 1 ELSE 0 END',
- ),
- );
-
- if (!empty($this->_search_params['topic']))
- {
- $main_query['where'][] = 't.id_topic = {int:topic}';
- $main_query['parameters']['topic'] = $this->param('topic');
- }
-
- if (!empty($this->_search_params['show_complete']))
- {
- $main_query['group_by'][] = 'm.id_msg, t.id_first_msg, t.id_last_msg';
- }
- }
-
- // *** Get the subject results.
- $numSubjectResults = $this->_log_search_subjects($id_search);
-
- if ($numSubjectResults !== 0)
- {
- $main_query['weights']['subject']['search'] = 'CASE WHEN MAX(lst.id_topic) IS NULL THEN 0 ELSE 1 END';
- $main_query['left_join'][] = '{db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_topics AS lst ON (' . ($this->_createTemporary ? '' : 'lst.id_search = {int:id_search} AND ') . 'lst.id_topic = t.id_topic)';
- if (!$this->_createTemporary)
- {
- $main_query['parameters']['id_search'] = $id_search;
- }
- }
-
- // We building an index?
- if (is_callable(array($this->_searchAPI, 'indexedWordQuery')))
- {
- $indexedResults = $this->_prepare_word_index($id_search, $maxMessageResults);
-
- if (empty($indexedResults) && empty($numSubjectResults) && !empty($modSettings['search_force_index']))
- {
- return false;
- }
- elseif (!empty($indexedResults))
- {
- $main_query['inner_join'][] = '{db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_messages AS lsm ON (lsm.id_msg = m.id_msg)';
-
- if (!$this->_createTemporary)
- {
- $main_query['where'][] = 'lsm.id_search = {int:id_search}';
- $main_query['parameters']['id_search'] = $id_search;
- }
- }
- }
- // Not using an index? All conditions have to be carried over.
- else
- {
- $orWhere = array();
- $count = 0;
- foreach ($this->_searchWords as $words)
- {
- $where = array();
- foreach ($words['all_words'] as $regularWord)
- {
- $where[] = 'm.body' . (in_array($regularWord, $this->_excludedWords) ? ' NOT' : '') . (empty($modSettings['search_match_words']) || $this->noRegexp() ? ' LIKE ' : ' RLIKE ') . '{string:all_word_body_' . $count . '}';
- if (in_array($regularWord, $this->_excludedWords))
- {
- $where[] = 'm.subject NOT' . (empty($modSettings['search_match_words']) || $this->noRegexp() ? ' LIKE ' : ' RLIKE ') . '{string:all_word_body_' . $count . '}';
- }
- $main_query['parameters']['all_word_body_' . ($count++)] = $this->_searchAPI->prepareWord($regularWord, $this->noRegexp());
- }
-
- if (!empty($where))
- {
- $orWhere[] = count($where) > 1 ? '(' . implode(' AND ', $where) . ')' : $where[0];
- }
- }
-
- if (!empty($orWhere))
- {
- $main_query['where'][] = count($orWhere) > 1 ? '(' . implode(' OR ', $orWhere) . ')' : $orWhere[0];
- }
-
- if (!empty($this->_userQuery))
- {
- $main_query['where'][] = '{raw:user_query}';
- $main_query['parameters']['user_query'] = $this->_userQuery;
- }
-
- if (!empty($this->_search_params['topic']))
- {
- $main_query['where'][] = 'm.id_topic = {int:topic}';
- $main_query['parameters']['topic'] = $this->param('topic');
- }
-
- if (!empty($this->_minMsgID))
- {
- $main_query['where'][] = 'm.id_msg >= {int:min_msg_id}';
- $main_query['parameters']['min_msg_id'] = $this->_minMsgID;
- }
-
- if (!empty($this->_maxMsgID))
- {
- $main_query['where'][] = 'm.id_msg <= {int:max_msg_id}';
- $main_query['parameters']['max_msg_id'] = $this->_maxMsgID;
- }
-
- if (!empty($this->_boardQuery))
- {
- $main_query['where'][] = 'm.id_board {raw:board_query}';
- $main_query['parameters']['board_query'] = $this->_boardQuery;
- }
- }
- call_integration_hook('integrate_main_search_query', array(&$main_query));
-
- // Did we either get some indexed results, or otherwise did not do an indexed query?
- if (!empty($indexedResults) || !is_callable(array($this->_searchAPI, 'indexedWordQuery')))
- {
- $relevance = $this->_build_relevance($main_query['weights']);
- $main_query['select']['relevance'] = $relevance;
- $num_results += $this->_build_search_results_log($main_query, 'insert_log_search_results_no_index');
- }
-
- // Insert subject-only matches.
- if ($num_results < $modSettings['search_max_results'] && $numSubjectResults !== 0)
- {
- $subject_query = array(
- 'select' => array(
- 'id_search' => '{int:id_search}',
- 'id_topic' => 't.id_topic',
- 'relevance' => $this->_build_relevance(),
- 'id_msg' => 't.id_first_msg',
- 'num_matches' => 1,
- ),
- 'from' => '{db_prefix}topics AS t',
- 'inner_join' => array(
- '{db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_topics AS lst ON (lst.id_topic = t.id_topic)'
- ),
- 'where' => array(
- $this->_createTemporary ? '1=1' : 'lst.id_search = {int:id_search}',
- ),
- 'parameters' => array(
- 'id_search' => $id_search,
- 'min_msg' => $this->_minMsg,
- 'recent_message' => $this->_recentMsg,
- 'huge_topic_posts' => $humungousTopicPosts,
- 'limit' => empty($modSettings['search_max_results']) ? 0 : $modSettings['search_max_results'] - $num_results,
- ),
- );
-
- $num_results += $this->_build_search_results_log($subject_query, 'insert_log_search_results_sub_only', true);
- }
- elseif ($num_results == -1)
- {
- $num_results = 0;
- }
-
- return $num_results;
- }
-
- /**
- * Determines and add the relevance to the results
- *
- * @param mixed[] $topics - The search results (passed by reference)
- * @param int $id_search - the id of the search
- * @param int $start - Results are shown starting from here
- * @param int $limit - No more results than this
- *
- * @return bool[]
- */
- public function addRelevance(&$topics, $id_search, $start, $limit)
- {
- // *** Retrieve the results to be shown on the page
- $participants = array();
- $request = $this->_db_search->search_query('', '
- SELECT ' . (empty($this->_search_params['topic']) ? 'lsr.id_topic' : $this->param('topic') . ' AS id_topic') . ', lsr.id_msg, lsr.relevance, lsr.num_matches
- FROM {db_prefix}log_search_results AS lsr' . ($this->param('sort') === 'num_replies' ? '
- INNER JOIN {db_prefix}topics AS t ON (t.id_topic = lsr.id_topic)' : '') . '
- WHERE lsr.id_search = {int:id_search}
- ORDER BY {raw:sort} {raw:sort_dir}
- LIMIT {int:start}, {int:limit}',
- array(
- 'id_search' => $id_search,
- 'sort' => $this->param('sort'),
- 'sort_dir' => $this->param('sort_dir'),
- 'start' => $start,
- 'limit' => $limit,
- )
- );
- while ($row = $this->_db->fetch_assoc($request))
- {
- $topics[$row['id_msg']] = array(
- 'relevance' => round($row['relevance'] / 10, 1) . '%',
- 'num_matches' => $row['num_matches'],
- 'matches' => array(),
- );
- // By default they didn't participate in the topic!
- $participants[$row['id_topic']] = false;
- }
- $this->_db->free_result($request);
-
- return $participants;
- }
-
- /**
- * Finds the posters of the messages
- *
- * @param int[] $msg_list - All the messages we want to find the posters
- * @param int $limit - There are only so much topics
- *
- * @return int[] - array of members id
- */
- public function loadPosters($msg_list, $limit)
- {
- // Load the posters...
- $request = $this->_db->query('', '
- SELECT
- id_member
- FROM {db_prefix}messages
- WHERE id_member != {int:no_member}
- AND id_msg IN ({array_int:message_list})
- LIMIT {int:limit}',
- array(
- 'message_list' => $msg_list,
- 'limit' => $limit,
- 'no_member' => 0,
- )
- );
- $posters = array();
- while ($row = $this->_db->fetch_assoc($request))
- $posters[] = $row['id_member'];
- $this->_db->free_result($request);
-
- return $posters;
- }
-
- /**
- * Finds the posters of the messages
- *
- * @param int[] $msg_list - All the messages we want to find the posters
- * @param int $limit - There are only so much topics
- *
- * @return resource
- */
- public function loadMessagesRequest($msg_list, $limit)
- {
- global $modSettings;
-
- $request = $this->_db->query('', '
- SELECT
- m.id_msg, m.subject, m.poster_name, m.poster_email, m.poster_time, m.id_member,
- m.icon, m.poster_ip, m.body, m.smileys_enabled, m.modified_time, m.modified_name,
- 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,
- first_mem.id_member AS first_member_id, COALESCE(first_mem.real_name, first_m.poster_name) AS first_member_name,
- last_m.id_msg AS last_msg, last_m.poster_time AS last_poster_time, last_mem.id_member AS last_member_id,
- 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,
- t.id_topic, t.is_sticky, t.locked, t.id_poll, t.num_replies, t.num_views, t.num_likes,
- b.id_board, b.name AS board_name, c.id_cat, c.name AS cat_name
- FROM {db_prefix}messages AS m
- INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic)
- INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
- INNER JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat)
- INNER JOIN {db_prefix}messages AS first_m ON (first_m.id_msg = t.id_first_msg)
- INNER JOIN {db_prefix}messages AS last_m ON (last_m.id_msg = t.id_last_msg)
- LEFT JOIN {db_prefix}members AS first_mem ON (first_mem.id_member = first_m.id_member)
- LEFT JOIN {db_prefix}members AS last_mem ON (last_mem.id_member = first_m.id_member)
- WHERE m.id_msg IN ({array_int:message_list})' . ($modSettings['postmod_active'] ? '
- AND m.approved = {int:is_approved}' : '') . '
- ORDER BY FIND_IN_SET(m.id_msg, {string:message_list_in_set})
- LIMIT {int:limit}',
- array(
- 'message_list' => $msg_list,
- 'is_approved' => 1,
- 'message_list_in_set' => implode(',', $msg_list),
- 'limit' => $limit,
- )
- );
-
- return $request;
- }
-
- /**
- * Did the user find any message at all?
- *
- * @param resource $messages_request holds a query result
- *
- * @return boolean
- */
- public function noMessages($messages_request)
- {
- return $this->_db->num_rows($messages_request) == 0;
- }
-
- /**
- * If searching in topics only (?), inserts results in log_search_topics
- *
- * @param int $id_search - the id of the search to delete from logs
- *
- * @return int - the number of search results
- */
- private function _log_search_subjects($id_search)
- {
- global $modSettings;
-
- if (!empty($this->_search_params['topic']))
- {
- return 0;
- }
-
- $inserts = array();
- $numSubjectResults = 0;
-
- // Clean up some previous cache.
- if (!$this->_createTemporary)
- {
- $this->_db_search->search_query('delete_log_search_topics', '
- DELETE FROM {db_prefix}log_search_topics
- WHERE id_search = {int:search_id}',
- array(
- 'search_id' => $id_search,
- )
- );
- }
-
- foreach ($this->_searchWords as $words)
- {
- $subject_query = array(
- 'from' => '{db_prefix}topics AS t',
- 'inner_join' => array(),
- 'left_join' => array(),
- 'where' => array(),
- 'params' => array(),
- );
-
- $numTables = 0;
- $prev_join = 0;
- $count = 0;
- foreach ($words['subject_words'] as $subjectWord)
- {
- $numTables++;
- if (in_array($subjectWord, $this->_excludedSubjectWords))
- {
- $subject_query['inner_join'][] = '{db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)';
- $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)';
- $subject_query['params']['subject_not_' . $count] = empty($modSettings['search_match_words']) ? '%' . $subjectWord . '%' : $subjectWord;
-
- $subject_query['where'][] = '(subj' . $numTables . '.word IS NULL)';
- $subject_query['where'][] = 'm.body NOT ' . (empty($modSettings['search_match_words']) || $this->noRegexp() ? ' LIKE ' : ' RLIKE ') . '{string:body_not_' . $count . '}';
- $subject_query['params']['body_not_' . ($count++)] = $this->_searchAPI->prepareWord($subjectWord, $this->noRegexp());
- }
- else
- {
- $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)';
- $subject_query['where'][] = 'subj' . $numTables . '.word LIKE {string:subject_like_' . $count . '}';
- $subject_query['params']['subject_like_' . ($count++)] = empty($modSettings['search_match_words']) ? '%' . $subjectWord . '%' : $subjectWord;
- $prev_join = $numTables;
- }
- }
-
- if (!empty($this->_userQuery))
- {
- $subject_query['inner_join'][] = '{db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)';
- $subject_query['where'][] = '{raw:user_query}';
- $subject_query['params']['user_query'] = $this->_userQuery;
- }
-
- if (!empty($this->_search_params['topic']))
- {
- $subject_query['where'][] = 't.id_topic = {int:topic}';
- $subject_query['params']['topic'] = $this->param('topic');
- }
-
- if (!empty($this->_minMsgID))
- {
- $subject_query['where'][] = 't.id_first_msg >= {int:min_msg_id}';
- $subject_query['params']['min_msg_id'] = $this->_minMsgID;
- }
-
- if (!empty($this->_maxMsgID))
- {
- $subject_query['where'][] = 't.id_last_msg <= {int:max_msg_id}';
- $subject_query['params']['max_msg_id'] = $this->_maxMsgID;
- }
-
- if (!empty($this->_boardQuery))
- {
- $subject_query['where'][] = 't.id_board {raw:board_query}';
- $subject_query['params']['board_query'] = $this->_boardQuery;
- }
-
- if (!empty($this->_excludedPhrases))
- {
- $subject_query['inner_join'][] = '{db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)';
- $count = 0;
- foreach ($this->_excludedPhrases as $phrase)
- {
- $subject_query['where'][] = 'm.subject NOT ' . (empty($modSettings['search_match_words']) || $this->noRegexp() ? 'LIKE' : 'RLIKE') . ' {string:exclude_phrase_' . $count . '}';
- $subject_query['where'][] = 'm.body NOT ' . (empty($modSettings['search_match_words']) || $this->noRegexp() ? 'LIKE' : 'RLIKE') . ' {string:exclude_phrase_' . $count . '}';
- $subject_query['params']['exclude_phrase_' . ($count++)] = $this->_searchAPI->prepareWord($phrase, $this->noRegexp());
- }
- }
-
- call_integration_hook('integrate_subject_search_query', array(&$subject_query));
-
- // Nothing to search for?
- if (empty($subject_query['where']))
- {
- continue;
- }
-
- $ignoreRequest = $this->_db_search->search_query('insert_log_search_topics', ($this->_db->support_ignore() ? ('
- INSERT IGNORE INTO {db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_topics
- (' . ($this->_createTemporary ? '' : 'id_search, ') . 'id_topic)') : '') . '
- SELECT ' . ($this->_createTemporary ? '' : $id_search . ', ') . 't.id_topic
- FROM ' . $subject_query['from'] . (empty($subject_query['inner_join']) ? '' : '
- INNER JOIN ' . implode('
- INNER JOIN ', array_unique($subject_query['inner_join']))) . (empty($subject_query['left_join']) ? '' : '
- LEFT JOIN ' . implode('
- LEFT JOIN ', array_unique($subject_query['left_join']))) . '
- WHERE ' . implode('
- AND ', array_unique($subject_query['where'])) . (empty($modSettings['search_max_results']) ? '' : '
- LIMIT ' . ($modSettings['search_max_results'] - $numSubjectResults)),
- $subject_query['params']
- );
-
- // Don't do INSERT IGNORE? Manually fix this up!
- if (!$this->_db->support_ignore())
- {
- while ($row = $this->_db->fetch_row($ignoreRequest))
- {
- $ind = $this->_createTemporary ? 0 : 1;
-
- // No duplicates!
- if (isset($inserts[$row[$ind]]))
- {
- continue;
- }
-
- $inserts[$row[$ind]] = $row;
- }
- $this->_db->free_result($ignoreRequest);
- $numSubjectResults = count($inserts);
- }
- else
- {
- $numSubjectResults += $this->_db->affected_rows();
- }
-
- if (!empty($modSettings['search_max_results']) && $numSubjectResults >= $modSettings['search_max_results'])
- {
- break;
- }
- }
-
- // Got some non-MySQL data to plonk in?
- if (!empty($inserts))
- {
- $this->_db->insert('',
- ('{db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_topics'),
- $this->_createTemporary ? array('id_topic' => 'int') : array('id_search' => 'int', 'id_topic' => 'int'),
- $inserts,
- $this->_createTemporary ? array('id_topic') : array('id_search', 'id_topic')
- );
- }
-
- return $numSubjectResults;
- }
-
- /**
- * Populates log_search_messages
- *
- * @param int $id_search - the id of the search to delete from logs
- * @param int $maxMessageResults - the maximum number of messages to index
- *
- * @return int - the number of indexed results
- */
- private function _prepare_word_index($id_search, $maxMessageResults)
- {
- $indexedResults = 0;
- $inserts = array();
-
- // Clear, all clear!
- if (!$this->_createTemporary)
- {
- $this->_db_search->search_query('delete_log_search_messages', '
- DELETE FROM {db_prefix}log_search_messages
- WHERE id_search = {int:id_search}',
- array(
- 'id_search' => $id_search,
- )
- );
- }
-
- foreach ($this->_searchWords as $words)
- {
- // Search for this word, assuming we have some words!
- if (!empty($words['indexed_words']))
- {
- // Variables required for the search.
- $search_data = array(
- 'insert_into' => ($this->_createTemporary ? 'tmp_' : '') . 'log_search_messages',
- 'no_regexp' => $this->noRegexp(),
- 'max_results' => $maxMessageResults,
- 'indexed_results' => $indexedResults,
- 'params' => array(
- 'id_search' => !$this->_createTemporary ? $id_search : 0,
- 'excluded_words' => $this->_excludedWords,
- 'user_query' => !empty($this->_userQuery) ? $this->_userQuery : '',
- 'board_query' => !empty($this->_boardQuery) ? $this->_boardQuery : '',
- 'topic' => !empty($this->_search_params['topic']) ? $this->param('topic') : 0,
- 'min_msg_id' => (int) $this->_minMsgID,
- 'max_msg_id' => (int) $this->_maxMsgID,
- 'excluded_phrases' => $this->_excludedPhrases,
- 'excluded_index_words' => $this->_excludedIndexWords,
- 'excluded_subject_words' => $this->_excludedSubjectWords,
- ),
- );
-
- $ignoreRequest = $this->_searchAPI->indexedWordQuery($words, $search_data);
-
- if (!$this->_db->support_ignore())
- {
- while ($row = $this->_db->fetch_row($ignoreRequest))
- {
- // No duplicates!
- if (isset($inserts[$row[0]]))
- {
- continue;
- }
-
- $inserts[$row[0]] = $row;
- }
- $this->_db->free_result($ignoreRequest);
- $indexedResults = count($inserts);
- }
- else
- {
- $indexedResults += $this->_db->affected_rows();
- }
-
- if (!empty($maxMessageResults) && $indexedResults >= $maxMessageResults)
- {
- break;
- }
- }
- }
-
- // More non-MySQL stuff needed?
- if (!empty($inserts))
- {
- $this->_db->insert('',
- '{db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_messages',
- $this->_createTemporary ? array('id_msg' => 'int') : array('id_msg' => 'int', 'id_search' => 'int'),
- $inserts,
- $this->_createTemporary ? array('id_msg') : array('id_msg', 'id_search')
- );
- }
-
- return $indexedResults;
- }
-
- /**
- * Build the search relevance query
- *
- * @param null|int[] $factors - is factors are specified that array will
- * be used to build the relevance value, otherwise the function will use
- * $this->_weight_factors
- *
- * @return string
- */
- private function _build_relevance($factors = null)
- {
- $relevance = '1000 * (';
-
- if ($factors !== null && is_array($factors))
- {
- $weight_total = 0;
- foreach ($factors as $type => $value)
- {
- $relevance .= $this->_weight[$type];
- if (!empty($value['search']))
- {
- $relevance .= ' * ' . $value['search'];
- }
-
- $relevance .= ' + ';
- $weight_total += $this->_weight[$type];
- }
- }
- else
- {
- $weight_total = $this->_weight_total;
- foreach ($this->_weight_factors as $type => $value)
- {
- if (isset($value['results']))
- {
- $relevance .= $this->_weight[$type];
- if (!empty($value['results']))
- {
- $relevance .= ' * ' . $value['results'];
- }
-
- $relevance .= ' + ';
- }
- }
- }
-
- $relevance = substr($relevance, 0, -3) . ') / ' . $weight_total . ' AS relevance';
-
- return $relevance;
- }
-
- /**
- * Inserts the data into log_search_results
- *
- * @param mixed[] $main_query - An array holding all the query parts.
- * Structure:
- * 'select' => string[] - the select columns
- * 'from' => string - the table for the FROM clause
- * 'inner_join' => string[] - any INNER JOIN
- * 'left_join' => string[] - any LEFT JOIN
- * 'where' => string[] - the conditions
- * 'group_by' => string[] - the fields to group by
- * 'parameters' => mixed[] - any parameter required by the query
- * @param string $query_identifier - a string to identify the query
- * @param bool $use_old_ids - if true the topic ids retrieved by a previous
- * call to this function will be used to identify duplicates
- *
- * @return int - the number of rows affected by the query
- */
- private function _build_search_results_log($main_query, $query_identifier, $use_old_ids = false)
+ public function getParticipants()
{
- static $usedIDs;
-
- $ignoreRequest = $this->_db_search->search_query($query_identifier, ($this->_db->support_ignore() ? ('
- INSERT IGNORE INTO {db_prefix}log_search_results
- (' . implode(', ', array_keys($main_query['select'])) . ')') : '') . '
- SELECT
- ' . implode(',
- ', $main_query['select']) . '
- FROM ' . $main_query['from'] . (!empty($main_query['inner_join']) ? '
- INNER JOIN ' . implode('
- INNER JOIN ', array_unique($main_query['inner_join'])) : '') . (!empty($main_query['left_join']) ? '
- LEFT JOIN ' . implode('
- LEFT JOIN ', array_unique($main_query['left_join'])) : '') . (!empty($main_query['where']) ? '
- WHERE ' : '') . implode('
- AND ', array_unique($main_query['where'])) . (!empty($main_query['group_by']) ? '
- GROUP BY ' . implode(', ', array_unique($main_query['group_by'])) : '') . (!empty($main_query['parameters']['limit']) ? '
- LIMIT {int:limit}' : ''),
- $main_query['parameters']
- );
-
- // If the database doesn't support IGNORE to make this fast we need to do some tracking.
- if (!$this->_db->support_ignore())
- {
- $inserts = array();
-
- while ($row = $this->_db->fetch_assoc($ignoreRequest))
- {
- // No duplicates!
- if ($use_old_ids)
- {
- if (isset($usedIDs[$row['id_topic']]))
- {
- continue;
- }
- }
- else
- {
- if (isset($inserts[$row['id_topic']]))
- {
- continue;
- }
- }
-
- $usedIDs[$row['id_topic']] = true;
- foreach ($row as $key => $value)
- $inserts[$row['id_topic']][] = (int) $row[$key];
- }
- $this->_db->free_result($ignoreRequest);
-
- // Now put them in!
- if (!empty($inserts))
- {
- $query_columns = array();
- foreach ($main_query['select'] as $k => $v)
- $query_columns[$k] = 'int';
-
- $this->_db->insert('',
- '{db_prefix}log_search_results',
- $query_columns,
- $inserts,
- array('id_search', 'id_topic')
- );
- }
- $num_results = count($inserts);
- }
- else
- {
- $num_results = $this->_db->affected_rows();
- }
-
- return $num_results;
+ return $this->_participants;
}
}
diff --git a/sources/subs/Search/SearchApiWrapper.php b/sources/subs/Search/SearchApiWrapper.php
new file mode 100644
index 0000000000..c2af76705e
--- /dev/null
+++ b/sources/subs/Search/SearchApiWrapper.php
@@ -0,0 +1,267 @@
+register(SUBSDIR . '/Search', '\\ElkArte\\Search');
+ if (!is_object($config))
+ {
+ $config = new \ElkArte\ValuesContainer((array) $config);
+ }
+ $this->load($config->search_index, $config, $searchParams);
+ }
+
+ /**
+ * Wrapper for postCreated of the SearchAPI
+ *
+ * @param mixed[] $msgOptions
+ * @param mixed[] $topicOptions
+ * @param mixed[] $posterOptions
+ */
+ public function postCreated($msgOptions, $topicOptions, $posterOptions)
+ {
+ if (is_callable(array($this->_searchAPI, 'postCreated')))
+ {
+ $this->_searchAPI->postCreated($msgOptions, $topicOptions, $posterOptions);
+ }
+ }
+
+ /**
+ * Wrapper for postModified of the SearchAPI
+ *
+ * @param mixed[] $msgOptions
+ * @param mixed[] $topicOptions
+ * @param mixed[] $posterOptions
+ */
+ public function postModified($msgOptions, $topicOptions, $posterOptions)
+ {
+ if (is_callable(array($this->_searchAPI, 'postModified')))
+ {
+ $this->_searchAPI->postModified($msgOptions, $topicOptions, $posterOptions);
+ }
+ }
+
+ /**
+ * Wrapper for topicSplit of the SearchAPI
+ *
+ * @param int $split2_ID_TOPIC
+ * @param int[] $splitMessages
+ */
+ public function topicSplit($split2_ID_TOPIC, $splitMessages)
+ {
+ if (is_callable(array($this->_searchAPI, 'topicSplit')))
+ {
+ $this->_searchAPI->topicSplit($split2_ID_TOPIC, $splitMessages);
+ }
+ }
+
+ /**
+ * Wrapper for topicMerge of the SearchAPI
+ *
+ * @param int $id_topic
+ * @param mixed[] $topics
+ * @param int[] $affected_msgs
+ * @param string[] $subject array($response_prefix, $target_subject)
+ */
+ public function topicMerge($id_topic, $topics, $affected_msgs, $subject)
+ {
+ if (is_callable(array($this->_searchAPI, 'topicMerge')))
+ {
+ $this->_searchAPI->topicMerge($id_topic, $topics, $affected_msgs, $subject);
+ }
+ }
+
+ /**
+ * Wrapper for searchSettings of the SearchAPI
+ *
+ * @param mixed[] $config_vars
+ */
+ public function searchSettings(&$config_vars)
+ {
+ if (is_callable(array($this->_searchAPI, 'searchSettings')))
+ {
+ $this->_searchAPI->searchSettings($config_vars);
+ }
+ }
+
+ /**
+ * Wrapper for searchQuery of the SearchAPI
+ * @param string[] $search_words
+ * @param string[] $excluded_words
+ * @param bool[] $participants
+ * @param string[] $search_results
+ *
+ * @return mixed[]
+ */
+ public function searchQuery($search_words, $excluded_words, &$participants, &$search_results)
+ {
+ return $this->_searchAPI->searchQuery($search_words, $excluded_words, $participants, $search_results);
+ }
+
+ /**
+ * Wrapper for prepareWord of the SearchAPI
+ *
+ * @return string
+ */
+ public function prepareWord($phrase, $no_regexp)
+ {
+ return $this->_searchAPI->prepareWord($phrase, $no_regexp);
+ }
+
+ /**
+ * Wrapper for setExcludedPhrases of the SearchAPI
+ *
+ * @param string[] $phrases An array of phrases to exclude
+ */
+ public function setExcludedPhrases($phrase)
+ {
+ $this->_searchAPI->setExcludedPhrases($phrase);
+ }
+
+ /**
+ * Wrapper for setExcludedWords of the SearchAPI
+ *
+ * @param string[] $words An array of words to exclude
+ */
+ public function setExcludedWords($words)
+ {
+ $this->_searchAPI->setExcludedWords($words);
+ }
+
+ /**
+ * Wrapper for setSearchArray of the SearchAPI
+ *
+ * @param \ElkArte\Search\SearchArray $searchArray
+ */
+ public function setSearchArray(\ElkArte\Search\SearchArray $searchArray)
+ {
+ $this->_searchAPI->setSearchArray($searchArray);
+ }
+
+ /**
+ * Wrapper for prepareIndexes of the SearchAPI
+ *
+ * @param string $word
+ * @param string $wordsSearch
+ * @param string $wordsExclude
+ * @param string $isExcluded
+ * @param string $excludedSubjectWords
+ */
+ public function prepareIndexes($word, &$wordsSearch, &$wordsExclude, $isExcluded, $excludedSubjectWords)
+ {
+ $this->_searchAPI->prepareIndexes($word, $wordsSearch, $wordsExclude, $isExcluded, $excludedSubjectWords);
+ }
+
+ /**
+ * Wrapper for searchSort of the SearchAPI
+ *
+ * @param string $a Word A
+ * @param string $b Word B
+ * @return int An integer indicating how the words should be sorted (-1, 0 1)
+ */
+ public function searchSort($a, $b)
+ {
+ return $this->_searchAPI->searchSort($a, $b);
+ }
+
+ /**
+ * Wrapper for setWeightFactors of the SearchAPI
+ *
+ * @param \ElkArte\Search\WeightFactors $weights
+ */
+ public function setWeightFactors(\ElkArte\Search\WeightFactors $weights)
+ {
+ $this->_searchAPI->setWeightFactors($weights);
+ }
+
+ /**
+ * Wrapper for useTemporary of the SearchAPI
+ *
+ * @param bool $use
+ */
+ public function useTemporary($use = false)
+ {
+ $this->_searchAPI->useTemporary($use);
+ }
+
+ /**
+ * Returns the number of results obtained from the query.
+ *
+ * @return int
+ */
+ public function getNumResults()
+ {
+ return $this->_searchAPI->getNumResults();
+ }
+
+ /**
+ * Creates a search API and returns the object.
+ *
+ * @param string $searchClass
+ * @param \ElkArte\ValuesContainer $config
+ */
+ protected function load($searchClass, $config, $searchParams)
+ {
+ global $txt;
+
+ require_once(SUBSDIR . '/Package.subs.php');
+
+ // Load up the search API we are going to use.
+ if (empty($searchClass))
+ {
+ $searchClass = self::DEFAULT_API;
+ }
+
+ // Try to initialize the API
+ $fqcn = '\\ElkArte\\Search\\API\\' . ucfirst($searchClass);
+ if (class_exists($fqcn) && is_a($fqcn, '\\ElkArte\\Search\\API\\SearchAPI', true))
+ {
+ // Create an instance of the search API and check it is valid for this version of the software.
+ $this->_searchAPI = new $fqcn($config, $searchParams);
+ }
+
+ // An invalid Search API? Log the error and set it to use the standard API
+ if (!$this->_searchAPI || (!$this->_searchAPI->isValid()) || !matchPackageVersion(Search::FORUM_VERSION, $this->_searchAPI->min_elk_version . '-' . $this->_searchAPI->version_compatible))
+ {
+ // Log the error.
+ theme()->getTemplates()->loadLanguageFile('Errors');
+ \Errors::instance()->log_error(sprintf($txt['search_api_not_compatible'], $fqcn), 'critical');
+
+ $this->_searchAPI = new API\Standard($config, $searchParams);
+ }
+ }
+}
diff --git a/sources/subs/Search/SearchArray.php b/sources/subs/Search/SearchArray.php
new file mode 100644
index 0000000000..543c1d1d2d
--- /dev/null
+++ b/sources/subs/Search/SearchArray.php
@@ -0,0 +1,236 @@
+_search_string = $search_string;
+ $this->_search_simple_fulltext = $search_simple_fulltext;
+ $this->_blacklisted_words = $blacklisted_words;
+ $this->searchArray();
+ }
+
+ /**
+ * Builds the search array
+ *
+ * @return array
+ */
+ protected function searchArray()
+ {
+ // Change non-word characters into spaces.
+ $stripped_query = preg_replace('~(?:[\x0B\0\x{A0}\t\r\s\n(){}\\[\\]<>!@$%^*.,:+=`\~\?/\\\\]+|&(?:amp|lt|gt|quot);)+~u', ' ', $this->_search_string);
+
+ // Make the query lower case. It's gonna be case insensitive anyway.
+ $stripped_query = un_htmlspecialchars(\Util::strtolower($stripped_query));
+
+ // This option will do fulltext searching in the most basic way.
+ if ($this->_search_simple_fulltext)
+ {
+ $stripped_query = strtr($stripped_query, array('"' => ''));
+ }
+
+ $this->_no_regexp = preg_match('~(?:\d{1,7}|x[0-9a-fA-F]{1,6});~', $stripped_query) === 1;
+
+ // Extract phrase parts first (e.g. some words "this is a phrase" some more words.)
+ preg_match_all('/(?:^|\s)([-]?)"([^"]+)"(?:$|\s)/', $stripped_query, $matches, PREG_PATTERN_ORDER);
+ $phraseArray = $matches[2];
+
+ // Remove the phrase parts and extract the words.
+ $wordArray = preg_replace('~(?:^|\s)(?:[-]?)"(?:[^"]+)"(?:$|\s)~u', ' ', $this->_search_string);
+ $wordArray = explode(' ', \Util::htmlspecialchars(un_htmlspecialchars($wordArray), ENT_QUOTES));
+
+ // A minus sign in front of a word excludes the word.... so...
+ // .. first, we check for things like -"some words", but not "-some words".
+ $phraseArray = $this->_checkExcludePhrase($matches[1], $phraseArray);
+
+ // Now we look for -test, etc.... normaller.
+ $wordArray = $this->_checkExcludeWord($wordArray);
+
+ // The remaining words and phrases are all included.
+ $this->_searchArray = array_merge($phraseArray, $wordArray);
+
+ // Trim everything and make sure there are no words that are the same.
+ foreach ($this->_searchArray as $index => $value)
+ {
+ // Skip anything practically empty.
+ if (($this->_searchArray[$index] = trim($value, '-_\' ')) === '')
+ {
+ unset($this->_searchArray[$index]);
+ }
+ // Skip blacklisted words. Make sure to note we skipped them in case we end up with nothing.
+ elseif (in_array($this->_searchArray[$index], $this->_blacklisted_words))
+ {
+ $this->_foundBlackListedWords = true;
+ unset($this->_searchArray[$index]);
+ }
+ // Don't allow very, very short words.
+ elseif (\Util::strlen($value) < 2)
+ {
+ $this->_ignored[] = $value;
+ unset($this->_searchArray[$index]);
+ }
+ }
+
+ $this->_searchArray = array_slice(array_unique($this->_searchArray), 0, 10);
+
+ return $this->_searchArray;
+ }
+
+ public function getSearchArray()
+ {
+ return $this->_searchArray;
+ }
+
+ public function getExcludedWords()
+ {
+ return $this->_excludedWords;
+ }
+
+ public function getNoRegexp()
+ {
+ return $this->_no_regexp;
+ }
+
+ public function foundBlackListedWords()
+ {
+ return $this->_foundBlackListedWords;
+ }
+
+ public function getIgnored()
+ {
+ return $this->_ignored;
+ }
+
+ /**
+ * Looks for phrases that should be excluded from results
+ *
+ * - Check for things like -"some words", but not "-some words"
+ * - Prevents redundancy with blacklisted words
+ *
+ * @param string[] $matches
+ * @param string[] $phraseArray
+ *
+ * @return string[]
+ */
+ private function _checkExcludePhrase($matches, $phraseArray)
+ {
+ foreach ($matches as $index => $word)
+ {
+ if ($word === '-')
+ {
+ if (($word = trim($phraseArray[$index], '-_\' ')) !== '' && !in_array($word, $this->_blacklisted_words))
+ {
+ $this->_excludedWords[] = $word;
+ }
+
+ unset($phraseArray[$index]);
+ }
+ }
+
+ return $phraseArray;
+ }
+
+ /**
+ * Looks for words that should be excluded in the results (-word)
+ *
+ * - Look for -test, etc
+ * - Prevents excluding blacklisted words since it is redundant
+ *
+ * @param string[] $wordArray
+ *
+ * @return string[]
+ */
+ private function _checkExcludeWord($wordArray)
+ {
+ foreach ($wordArray as $index => $word)
+ {
+ if (strpos(trim($word), '-') === 0)
+ {
+ if (($word = trim($word, '-_\' ')) !== '' && !in_array($word, $this->_blacklisted_words))
+ {
+ $this->_excludedWords[] = $word;
+ }
+
+ unset($wordArray[$index]);
+ }
+ }
+
+ return $wordArray;
+ }
+}
\ No newline at end of file
diff --git a/sources/subs/Search/SearchParams.php b/sources/subs/Search/SearchParams.php
new file mode 100644
index 0000000000..9fd500e390
--- /dev/null
+++ b/sources/subs/Search/SearchParams.php
@@ -0,0 +1,489 @@
+_db = database();
+ $this->_search_string = $string;
+ $this->prepare();
+ $this->data = &$this->_search_params;
+ }
+
+ /**
+ * Encodes search params ($this->_search_params) in an URL-compatible way
+ *
+ * @param array $search build param index with specific search term (did you mean?)
+ *
+ * @return string - the encoded string to be appended to the URL
+ */
+ public function compileURL($search = array())
+ {
+ $temp_params = $this->_search_params;
+ $encoded = array();
+
+ if (!empty($search))
+ {
+ $temp_params['search'] = implode(' ', $search);
+ }
+
+ // *** Encode all search params
+ // All search params have been checked, let's compile them to a single string... made less simple by PHP 4.3.9 and below.
+ if (isset($temp_params['brd']))
+ {
+ $temp_params['brd'] = implode(',', $temp_params['brd']);
+ }
+
+ foreach ($temp_params as $k => $v)
+ $encoded[] = $k . '|\'|' . $v;
+
+ if (!empty($encoded))
+ {
+ // Due to old IE's 2083 character limit, we have to compress long search strings
+ $params = @gzcompress(implode('|"|', $encoded));
+
+ // Gzcompress failed, use try non-gz
+ if (empty($params))
+ {
+ $params = implode('|"|', $encoded);
+ }
+
+ // Base64 encode, then replace +/= with uri safe ones that can be reverted
+ $encoded = str_replace(array('+', '/', '='), array('-', '_', '.'), base64_encode($params));
+ }
+ else
+ {
+ $encoded = '';
+ }
+
+ return $encoded;
+ }
+
+ /**
+ * Extract search params from a string
+ */
+ protected function prepare()
+ {
+ // Due to IE's 2083 character limit, we have to compress long search strings
+ $temp_params = base64_decode(str_replace(array('-', '_', '.'), array('+', '/', '='), $this->_search_string));
+
+ // Test for gzuncompress failing
+ $temp_params2 = @gzuncompress($temp_params);
+ $temp_params = explode('|"|', (!empty($temp_params2) ? $temp_params2 : $temp_params));
+
+ foreach ($temp_params as $i => $data)
+ {
+ list($k, $v) = array_pad(explode('|\'|', $data), 2, '');
+ $this->_search_params[$k] = $v;
+ }
+
+ if (isset($this->_search_params['brd']))
+ {
+ $this->_search_params['brd'] = empty($this->_search_params['brd']) ? array() : explode(',', $this->_search_params['brd']);
+ }
+ }
+
+ /**
+ * Merge search params extracted with SearchParams::prepare
+ * with those present in the $param array (usually $_REQUEST['params'])
+ *
+ * @param mixed[] $params - An array of search parameters
+ * @param int $recentPercentage - A coefficient to calculate the lowest
+ * message id to start search from
+ * @param int $maxMembersToSearch - The maximum number of members to consider
+ * when multiple are found
+ *
+ * @throws \Elk_Exception topic_gone
+ */
+ public function merge($params, $recentPercentage, $maxMembersToSearch)
+ {
+ global $user_info, $modSettings, $context;
+
+ // Store whether simple search was used (needed if the user wants to do another query).
+ if (!isset($this->_search_params['advanced']))
+ {
+ $this->_search_params['advanced'] = empty($params['advanced']) ? 0 : 1;
+ }
+
+ // 1 => 'allwords' (default, don't set as param) / 2 => 'anywords'.
+ if (!empty($this->_search_params['searchtype']) || (!empty($params['searchtype']) && $params['searchtype'] == 2))
+ {
+ $this->_search_params['searchtype'] = 2;
+ }
+
+ // Minimum age of messages. Default to zero (don't set param in that case).
+ if (!empty($this->_search_params['minage']) || (!empty($params['minage']) && $params['minage'] > 0))
+ {
+ $this->_search_params['minage'] = !empty($this->_search_params['minage']) ? (int) $this->_search_params['minage'] : (int) $params['minage'];
+ }
+
+ // Maximum age of messages. Default to infinite (9999 days: param not set).
+ if (!empty($this->_search_params['maxage']) || (!empty($params['maxage']) && $params['maxage'] < 9999))
+ {
+ $this->_search_params['maxage'] = !empty($this->_search_params['maxage']) ? (int) $this->_search_params['maxage'] : (int) $params['maxage'];
+ }
+
+ // Searching a specific topic?
+ if (!empty($params['topic']) || (!empty($params['search_selection']) && $params['search_selection'] === 'topic'))
+ {
+ $this->_search_params['topic'] = empty($params['search_selection']) ? (int) $params['topic'] : (isset($params['sd_topic']) ? (int) $params['sd_topic'] : '');
+ $this->_search_params['show_complete'] = true;
+ }
+ elseif (!empty($this->_search_params['topic']))
+ {
+ $this->_search_params['topic'] = (int) $this->_search_params['topic'];
+ }
+
+ if (!empty($this->_search_params['minage']) || !empty($this->_search_params['maxage']))
+ {
+ $request = $this->_db->query('', '
+ SELECT ' . (empty($this->_search_params['maxage']) ? '0, ' : 'COALESCE(MIN(id_msg), -1), ') . (empty($this->_search_params['minage']) ? '0' : 'COALESCE(MAX(id_msg), -1)') . '
+ FROM {db_prefix}messages
+ WHERE 1=1' . ($modSettings['postmod_active'] ? '
+ AND approved = {int:is_approved_true}' : '') . (empty($this->_search_params['minage']) ? '' : '
+ AND poster_time <= {int:timestamp_minimum_age}') . (empty($this->_search_params['maxage']) ? '' : '
+ AND poster_time >= {int:timestamp_maximum_age}'),
+ array(
+ 'timestamp_minimum_age' => empty($this->_search_params['minage']) ? 0 : time() - 86400 * $this->_search_params['minage'],
+ 'timestamp_maximum_age' => empty($this->_search_params['maxage']) ? 0 : time() - 86400 * $this->_search_params['maxage'],
+ 'is_approved_true' => 1,
+ )
+ );
+ list ($this->_minMsgID, $this->_maxMsgID) = $this->_db->fetch_row($request);
+ if ($this->_minMsgID < 0 || $this->_maxMsgID < 0)
+ {
+ $context['search_errors']['no_messages_in_time_frame'] = true;
+ }
+ $this->_db->free_result($request);
+ }
+
+ // Default the user name to a wildcard matching every user (*).
+ if (!empty($this->_search_params['userspec']) || (!empty($params['userspec']) && $params['userspec'] != '*'))
+ {
+ $this->_search_params['userspec'] = isset($this->_search_params['userspec']) ? $this->_search_params['userspec'] : $params['userspec'];
+ }
+
+ // If there's no specific user, then don't mention it in the main query.
+ if (empty($this->_search_params['userspec']))
+ {
+ $this->_userQuery = '';
+ }
+ else
+ {
+ $userString = strtr(\Util::htmlspecialchars($this->_search_params['userspec'], ENT_QUOTES), array('"' => '"'));
+ $userString = strtr($userString, array('%' => '\%', '_' => '\_', '*' => '%', '?' => '_'));
+
+ preg_match_all('~"([^"]+)"~', $userString, $matches);
+ $possible_users = array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $userString)));
+
+ for ($k = 0, $n = count($possible_users); $k < $n; $k++)
+ {
+ $possible_users[$k] = trim($possible_users[$k]);
+
+ if (strlen($possible_users[$k]) == 0)
+ {
+ unset($possible_users[$k]);
+ }
+ }
+
+ // Create a list of database-escaped search names.
+ $realNameMatches = array();
+ foreach ($possible_users as $possible_user)
+ $realNameMatches[] = $this->_db->quote(
+ '{string:possible_user}',
+ array(
+ 'possible_user' => $possible_user
+ )
+ );
+
+ // Retrieve a list of possible members.
+ $request = $this->_db->query('', '
+ SELECT
+ id_member
+ FROM {db_prefix}members
+ WHERE {raw:match_possible_users}',
+ array(
+ 'match_possible_users' => 'real_name LIKE ' . implode(' OR real_name LIKE ', $realNameMatches),
+ )
+ );
+
+ // Simply do nothing if there're too many members matching the criteria.
+ if ($this->_db->num_rows($request) > $maxMembersToSearch)
+ {
+ $this->_userQuery = '';
+ }
+ elseif ($this->_db->num_rows($request) == 0)
+ {
+ $this->_userQuery = $this->_db->quote(
+ 'm.id_member = {int:id_member_guest} AND ({raw:match_possible_guest_names})',
+ array(
+ 'id_member_guest' => 0,
+ 'match_possible_guest_names' => 'm.poster_name LIKE ' . implode(' OR m.poster_name LIKE ', $realNameMatches),
+ )
+ );
+ }
+ else
+ {
+ while ($row = $this->_db->fetch_assoc($request))
+ {
+ $this->_memberlist[] = $row['id_member'];
+ }
+
+ $this->_userQuery = $this->_db->quote(
+ '(m.id_member IN ({array_int:matched_members}) OR (m.id_member = {int:id_member_guest} AND ({raw:match_possible_guest_names})))',
+ array(
+ 'matched_members' => $this->_memberlist,
+ 'id_member_guest' => 0,
+ 'match_possible_guest_names' => 'm.poster_name LIKE ' . implode(' OR m.poster_name LIKE ', $realNameMatches),
+ )
+ );
+ }
+ $this->_db->free_result($request);
+ }
+
+ // Ensure that boards are an array of integers (or nothing).
+ if (!empty($this->_search_params['brd']) && is_array($this->_search_params['brd']))
+ {
+ $query_boards = array_map('intval', $this->_search_params['brd']);
+ }
+ elseif (!empty($params['brd']) && is_array($params['brd']))
+ {
+ $query_boards = array_map('intval', $params['brd']);
+ }
+ elseif (!empty($params['brd']))
+ {
+ $query_boards = array_map('intval', explode(',', $params['brd']));
+ }
+ elseif (!empty($params['search_selection']) && $params['search_selection'] === 'board' && !empty($params['sd_brd']) && is_array($params['sd_brd']))
+ {
+ $query_boards = array_map('intval', $params['sd_brd']);
+ }
+ elseif (!empty($params['search_selection']) && $params['search_selection'] === 'board' && isset($params['sd_brd']) && (int) $params['sd_brd'] !== 0)
+ {
+ $query_boards = array((int) $params['sd_brd']);
+ }
+ else
+ {
+ $query_boards = array();
+ }
+
+ // Special case for boards: searching just one topic?
+ if (!empty($this->_search_params['topic']))
+ {
+ $request = $this->_db->query('', '
+ SELECT
+ b.id_board
+ FROM {db_prefix}topics AS t
+ INNER JOIN {db_prefix}boards AS b ON (b.id_board = t.id_board)
+ WHERE t.id_topic = {int:search_topic_id}
+ AND {query_see_board}' . ($modSettings['postmod_active'] ? '
+ AND t.approved = {int:is_approved_true}' : '') . '
+ LIMIT 1',
+ array(
+ 'search_topic_id' => $this->_search_params['topic'],
+ 'is_approved_true' => 1,
+ )
+ );
+
+ if ($this->_db->num_rows($request) == 0)
+ {
+ throw new \Elk_Exception('topic_gone', false);
+ }
+
+ $this->_search_params['brd'] = array();
+ list ($this->_search_params['brd'][0]) = $this->_db->fetch_row($request);
+ $this->_db->free_result($request);
+ }
+ // Select all boards you've selected AND are allowed to see.
+ elseif ($user_info['is_admin'] && (!empty($this->_search_params['advanced']) || !empty($query_boards)))
+ {
+ $this->_search_params['brd'] = $query_boards;
+ }
+ else
+ {
+ require_once(SUBSDIR . '/Boards.subs.php');
+ $this->_search_params['brd'] = array_keys(fetchBoardsInfo(array('boards' => $query_boards), array('include_recycle' => false, 'include_redirects' => false, 'wanna_see_board' => empty($this->_search_params['advanced']))));
+
+ // This error should pro'bly only happen for hackers.
+ if (empty($this->_search_params['brd']))
+ {
+ $context['search_errors']['no_boards_selected'] = true;
+ }
+ }
+
+ if (count($this->_search_params['brd']) != 0)
+ {
+ foreach ($this->_search_params['brd'] as $k => $v)
+ $this->_search_params['brd'][$k] = (int) $v;
+
+ // If we've selected all boards, this parameter can be left empty.
+ require_once(SUBSDIR . '/Boards.subs.php');
+ $num_boards = countBoards();
+
+ if (count($this->_search_params['brd']) == $num_boards)
+ {
+ $this->_boardQuery = '';
+ }
+ elseif (count($this->_search_params['brd']) == $num_boards - 1 && !empty($modSettings['recycle_board']) && !in_array($modSettings['recycle_board'], $this->_search_params['brd']))
+ {
+ $this->_boardQuery = '!= ' . $modSettings['recycle_board'];
+ }
+ else
+ {
+ $this->_boardQuery = 'IN (' . implode(', ', $this->_search_params['brd']) . ')';
+ }
+ }
+ else
+ {
+ $this->_boardQuery = '';
+ }
+
+ $this->_search_params['show_complete'] = !empty($this->_search_params['show_complete']) || !empty($params['show_complete']);
+ $this->_search_params['subject_only'] = !empty($this->_search_params['subject_only']) || !empty($params['subject_only']);
+
+ // Get the sorting parameters right. Default to sort by relevance descending.
+ $sort_columns = array(
+ 'relevance',
+ 'num_replies',
+ 'id_msg',
+ );
+
+ // Allow integration to add additional sort columns
+ call_integration_hook('integrate_search_sort_columns', array(&$sort_columns));
+
+ if (empty($this->_search_params['sort']) && !empty($params['sort']))
+ {
+ list ($this->_search_params['sort'], $this->_search_params['sort_dir']) = array_pad(explode('|', $params['sort']), 2, '');
+ }
+
+ $this->_search_params['sort'] = !empty($this->_search_params['sort']) && in_array($this->_search_params['sort'], $sort_columns) ? $this->_search_params['sort'] : 'relevance';
+
+ if (!empty($this->_search_params['topic']) && $this->_search_params['sort'] === 'num_replies')
+ {
+ $this->_search_params['sort'] = 'id_msg';
+ }
+
+ // Sorting direction: descending unless stated otherwise.
+ $this->_search_params['sort_dir'] = !empty($this->_search_params['sort_dir']) && $this->_search_params['sort_dir'] === 'asc' ? 'asc' : 'desc';
+
+ // Determine some values needed to calculate the relevance.
+ $this->_minMsg = (int) ((1 - $recentPercentage) * $modSettings['maxMsgID']);
+ $this->_recentMsg = $modSettings['maxMsgID'] - $this->_minMsg;
+
+ // *** Parse the search query
+ call_integration_hook('integrate_search_params', array(&$this->_search_params));
+
+ // What are we searching for?
+ if (empty($this->_search_params['search']))
+ {
+ if (isset($_GET['search']))
+ {
+ $this->_search_params['search'] = un_htmlspecialchars($_GET['search']);
+ }
+ elseif (isset($_POST['search']))
+ {
+ $this->_search_params['search'] = $_POST['search'];
+ }
+ else
+ {
+ $this->_search_params['search'] = '';
+ }
+ }
+ }
+
+ public function get()
+ {
+ return $this->_search_params;
+ }
+}
diff --git a/sources/subs/Search/WeightFactors.php b/sources/subs/Search/WeightFactors.php
new file mode 100644
index 0000000000..47ede6ab5c
--- /dev/null
+++ b/sources/subs/Search/WeightFactors.php
@@ -0,0 +1,137 @@
+_input_weights = $weights;
+ $this->_is_admin = (bool) $is_admin;
+
+ $this->_setup_weight_factors();
+ }
+
+ public function getFactors()
+ {
+ return $this->_weight_factors;
+ }
+
+ public function getWeight()
+ {
+ return $this->_weight;
+ }
+
+ public function getTotal()
+ {
+ return $this->_weight_total;
+ }
+
+ /**
+ * Prepares the weighting factors
+ */
+ private function _setup_weight_factors()
+ {
+ global $modSettings;
+
+ $default_factors = $this->_weight_factors = array(
+ 'frequency' => array(
+ 'search' => 'COUNT(*) / (MAX(t.num_replies) + 1)',
+ 'results' => '(t.num_replies + 1)',
+ ),
+ 'age' => array(
+ 'search' => 'CASE WHEN MAX(m.id_msg) < {int:min_msg} THEN 0 ELSE (MAX(m.id_msg) - {int:min_msg}) / {int:recent_message} END',
+ 'results' => 'CASE WHEN t.id_first_msg < {int:min_msg} THEN 0 ELSE (t.id_first_msg - {int:min_msg}) / {int:recent_message} END',
+ ),
+ 'length' => array(
+ 'search' => 'CASE WHEN MAX(t.num_replies) < {int:huge_topic_posts} THEN MAX(t.num_replies) / {int:huge_topic_posts} ELSE 1 END',
+ 'results' => 'CASE WHEN t.num_replies < {int:huge_topic_posts} THEN t.num_replies / {int:huge_topic_posts} ELSE 1 END',
+ ),
+ 'subject' => array(
+ 'search' => 0,
+ 'results' => 0,
+ ),
+ 'first_message' => array(
+ 'search' => 'CASE WHEN MIN(m.id_msg) = MAX(t.id_first_msg) THEN 1 ELSE 0 END',
+ ),
+ 'sticky' => array(
+ 'search' => 'MAX(t.is_sticky)',
+ 'results' => 't.is_sticky',
+ ),
+ 'likes' => array(
+ 'search' => 'MAX(t.num_likes)',
+ 'results' => 't.num_likes',
+ ),
+ );
+
+ // These are fallback weights in case of errors somewhere.
+ // Not intended to be passed to the hook
+ $default_weights = array(
+ 'search_weight_frequency' => 30,
+ 'search_weight_age' => 25,
+ 'search_weight_length' => 20,
+ 'search_weight_subject' => 15,
+ 'search_weight_first_message' => 10,
+ );
+
+ call_integration_hook('integrate_search_weights', array(&$this->_weight_factors));
+
+ // Set the weight factors for each area (frequency, age, etc) as defined in the ACP
+ $this->_calculate_weights($this->_weight_factors, $modSettings);
+
+ // Zero weight. Weightless :P.
+ if (empty($this->_weight_total))
+ {
+ // Admins can be bothered with a failure
+ if ($this->_is_admin)
+ {
+ throw new Elk_Exception('search_invalid_weights');
+ }
+
+ // Even if users will get an answer, the admin should know something is broken
+ Errors::instance()->log_lang_error('search_invalid_weights');
+
+ // Instead is better to give normal users and guests some kind of result
+ // using our defaults.
+ // Using a different variable here because it may be the hook is screwing
+ // things up
+ $this->_calculate_weights($default_factors, $default_weights);
+ }
+ }
+
+ /**
+ * Fill the $_weight variable and calculate the total weight
+ *
+ * @param mixed[] $factors
+ * @param int[] $weights
+ */
+ private function _calculate_weights($factors, $weights)
+ {
+ foreach ($factors as $weight_factor => $value)
+ {
+ $this->_weight[$weight_factor] = (int) ($weights['search_weight_' . $weight_factor] ?? 0);
+ $this->_weight_total += $this->_weight[$weight_factor];
+ }
+ }
+}
\ No newline at end of file
diff --git a/sources/subs/Topic.subs.php b/sources/subs/Topic.subs.php
index 9a65057267..ad89f0a3b1 100644
--- a/sources/subs/Topic.subs.php
+++ b/sources/subs/Topic.subs.php
@@ -2393,7 +2393,7 @@ function postSplitRedirect($reason, $subject, $board_info, $new_topic)
*/
function splitTopic($split1_ID_TOPIC, $splitMessages, $new_subject)
{
- global $txt;
+ global $txt, $modSettings;
$db = database();
@@ -2631,10 +2631,8 @@ function splitTopic($split1_ID_TOPIC, $splitMessages, $new_subject)
sendNotifications($split1_ID_TOPIC, 'split');
// If there's a search index that needs updating, update it...
- $search = new \ElkArte\Search\Search;
- $searchAPI = $search->findSearchAPI();
- if (is_callable(array($searchAPI, 'topicSplit')))
- $searchAPI->topicSplit($split2_ID_TOPIC, $splitMessages);
+ $searchAPI = new \ElkArte\Search\SearchApiWrapper(!empty($modSettings['search_index']) ? $modSettings['search_index'] : '');
+ $searchAPI->topicSplit($split2_ID_TOPIC, $splitMessages);
// Return the ID of the newly created topic.
return $split2_ID_TOPIC;
diff --git a/sources/subs/TopicUtil.class.php b/sources/subs/TopicUtil.class.php
index 75afce8b47..239c92ce88 100644
--- a/sources/subs/TopicUtil.class.php
+++ b/sources/subs/TopicUtil.class.php
@@ -118,6 +118,8 @@ public static function prepareContext($topics_info, $topicseen = false, $preview
$url_fragment = ($row['num_replies'] == 0 ? '.0' : '.msg' . $row['id_last_msg']) . $topicseen . '#new';
}
+ $row['new_from'] = $row['new_from'] ?? 0;
+
// And build the array.
$topics[$row['id_topic']] = array(
'id' => $row['id_topic'],
@@ -134,7 +136,7 @@ public static function prepareContext($topics_info, $topicseen = false, $preview
'html_time' => htmlTime($row['first_poster_time']),
'timestamp' => forum_time(true, $row['first_poster_time']),
'subject' => $row['first_subject'],
- 'preview' => trim($row['first_body']),
+ 'preview' => isset($row['first_body']) ? trim($row['first_body']) : '',
'icon' => $row['first_icon'],
'icon_url' => $icon_sources->{$row['first_icon']},
'href' => $scripturl . '?topic=' . $row['id_topic'] . '.0' . $topicseen,
@@ -153,7 +155,7 @@ public static function prepareContext($topics_info, $topicseen = false, $preview
'html_time' => htmlTime($row['last_poster_time']),
'timestamp' => forum_time(true, $row['last_poster_time']),
'subject' => $row['last_subject'],
- 'preview' => trim($row['last_body']),
+ 'preview' => isset($row['last_body']) ? trim($row['last_body']) : '',
'icon' => $row['last_icon'],
'icon_url' => $icon_sources->{$row['last_icon']},
'href' => $scripturl . '?topic=' . $row['id_topic'] . $url_fragment,
@@ -180,8 +182,9 @@ public static function prepareContext($topics_info, $topicseen = false, $preview
'replies' => comma_format($row['num_replies']),
'views' => comma_format($row['num_views']),
'likes' => comma_format($row['num_likes']),
- 'approved' => $row['approved'],
+ 'approved' => $row['approved'] ?? 1,
'unapproved_posts' => !empty($row['unapproved_posts']) ? $row['unapproved_posts'] : 0,
+ 'classes' => array(),
);
if (!empty($row['id_board']))
diff --git a/sources/subs/TopicsMerge.class.php b/sources/subs/TopicsMerge.class.php
index 1b6a313a3a..caa1e155ff 100644
--- a/sources/subs/TopicsMerge.class.php
+++ b/sources/subs/TopicsMerge.class.php
@@ -380,6 +380,8 @@ public function doMerge($details = array())
*/
protected function _updateStats($affected_msgs, $id_topic, $target_subject, $enforce_subject)
{
+ global $modSettings;
+
// Cycle through each board...
foreach ($this->_boardTotals as $id_board => $stats)
decrementBoard($id_board, $stats);
@@ -406,10 +408,8 @@ protected function _updateStats($affected_msgs, $id_topic, $target_subject, $enf
$response_prefix = response_prefix();
// If there's a search index that needs updating, update it...
- $search = new \ElkArte\Search\Search;
- $searchAPI = $search->findSearchAPI();
- if (is_callable(array($searchAPI, 'topicMerge')))
- $searchAPI->topicMerge($id_topic, $this->_topics, $affected_msgs, empty($enforce_subject) ? null : array($response_prefix, $target_subject));
+ $searchAPI = new \ElkArte\Search\SearchApiWrapper(!empty($modSettings['search_index']) ? $modSettings['search_index'] : '');
+ $searchAPI->topicMerge($id_topic, $this->_topics, $affected_msgs, empty($enforce_subject) ? null : array($response_prefix, $target_subject));
}
/**
diff --git a/sources/subs/VerificationControl/Captcha.php b/sources/subs/VerificationControl/Captcha.php
index 69cb11081d..b693b07806 100644
--- a/sources/subs/VerificationControl/Captcha.php
+++ b/sources/subs/VerificationControl/Captcha.php
@@ -107,8 +107,10 @@ public function showVerification($sessionVal, $isNew, $force_refresh = true)
{
global $context, $modSettings, $scripturl;
+ $show_captcha = !empty($this->_options['override_visual']) || (!empty($modSettings['visual_verification_type']) && !isset($this->_options['override_visual']));
+
// Some javascript ma'am? (But load it only once)
- if (!empty($this->_options['override_visual']) || (!empty($modSettings['visual_verification_type']) && !isset($this->_options['override_visual'])) && empty($context['captcha_js_loaded']))
+ if ($show_captcha && empty($context['captcha_js_loaded']))
{
theme()->getTemplates()->load('VerificationControls');
loadJavascriptFile('jquery.captcha.js');
@@ -120,7 +122,7 @@ public function showVerification($sessionVal, $isNew, $force_refresh = true)
// Requesting a new challenge, build the image link, seed the JS
if ($isNew)
{
- $this->_show_captcha = !empty($this->_options['override_visual']) || (!empty($modSettings['visual_verification_type']) && !isset($this->_options['override_visual']));
+ $this->_show_captcha = $show_captcha;
if ($this->_show_captcha)
{
@@ -130,7 +132,9 @@ public function showVerification($sessionVal, $isNew, $force_refresh = true)
}
if ($isNew || $force_refresh)
+ {
$this->createTest($sessionVal, $force_refresh);
+ }
return $this->_show_captcha;
}
diff --git a/sources/subs/VerificationControl/Questions.php b/sources/subs/VerificationControl/Questions.php
index 1ce56aea02..4972d26b0a 100644
--- a/sources/subs/VerificationControl/Questions.php
+++ b/sources/subs/VerificationControl/Questions.php
@@ -64,6 +64,13 @@ class Questions implements ControlInterface
*/
private $_incorrectQuestions = null;
+ /**
+ * Filters to use to load the questions
+ *
+ * @var null|\ElkArte\ValuesContainer
+ */
+ private $_filter = null;
+
/**
* On your mark
*
@@ -72,7 +79,10 @@ class Questions implements ControlInterface
public function __construct($verificationOptions = null)
{
if (!empty($verificationOptions))
+ {
$this->_options = $verificationOptions;
+ }
+ $this->_filter = new \ElkArte\ValuesContainer();
}
/**
@@ -88,7 +98,9 @@ public function showVerification($sessionVal, $isNew, $force_refresh = true)
// If we want questions do we have a cache of all the IDs?
if (!empty($this->_number_questions) && empty($modSettings['question_id_cache']))
+ {
$this->_refreshQuestionsCache();
+ }
// Let's deal with languages
// First thing we need to know what language the user wants and if there is at least one question
@@ -99,10 +111,14 @@ public function showVerification($sessionVal, $isNew, $force_refresh = true)
{
// Not even in the forum default? What the heck are you doing?!
if (empty($modSettings['question_id_cache'][$language]))
+ {
$this->_number_questions = 0;
+ }
// Fall back to the default
else
+ {
$this->_questions_language = $language;
+ }
}
// Do we have enough questions?
@@ -113,7 +129,9 @@ public function showVerification($sessionVal, $isNew, $force_refresh = true)
$this->_questionIDs = array();
if ($isNew || $force_refresh)
+ {
$this->createTest($sessionVal, $force_refresh);
+ }
}
}
@@ -135,17 +153,27 @@ public function createTest($sessionVal, $refresh = true)
// Pick some random IDs
if ($this->_number_questions == 1)
+ {
$this->_questionIDs[] = $this->_possible_questions[array_rand($this->_possible_questions, $this->_number_questions)];
+ }
else
+ {
foreach (array_rand($this->_possible_questions, $this->_number_questions) as $index)
+ {
$this->_questionIDs[] = $this->_possible_questions[$index];
+ }
+ }
}
// Same questions as before.
else
+ {
$this->_questionIDs = !empty($sessionVal['q']) ? $sessionVal['q'] : array();
+ }
if (empty($this->_questionIDs) && !$refresh)
+ {
$this->createTest($sessionVal, true);
+ }
}
/**
@@ -156,8 +184,9 @@ public function prepareContext($sessionVal)
theme()->getTemplates()->load('VerificationControls');
$sessionVal['q'] = array();
+ $this->_filter = new \ElkArte\ValuesContainer(array('type' => 'id_question', 'value' => $this->_questionIDs));
- $questions = $this->_loadAntispamQuestions(array('type' => 'id_question', 'value' => $this->_questionIDs));
+ $questions = $this->_loadAntispamQuestions();
$asked_questions = array();
$parser = \BBC\ParserWrapper::instance();
@@ -186,10 +215,14 @@ public function prepareContext($sessionVal)
public function doTest($sessionVal)
{
if ($this->_number_questions && (!isset($sessionVal['q']) || !isset($_REQUEST[$this->_options['id'] . '_vv']['q'])))
+ {
throw new Elk_Exception('no_access', false);
+ }
if (!$this->_verifyAnswers($sessionVal))
+ {
return 'wrong_verification_answer';
+ }
return true;
}
@@ -210,15 +243,13 @@ public function settings()
global $txt, $context, $language;
// Load any question and answers!
- $filter = null;
if (isset($_GET['language']))
{
- $filter = array(
+ $this->_filter = new \ElkArte\ValuesContainer(array(
'type' => 'language',
'value' => $_GET['language'],
- );
+ ));
}
- $context['question_answers'] = $this->_loadAntispamQuestions($filter);
$languages = getLanguages();
// Languages dropdown only if we have more than a lang installed, otherwise is plain useless
@@ -226,66 +257,27 @@ public function settings()
{
$context['languages'] = $languages;
foreach ($context['languages'] as &$lang)
+ {
if ($lang['filename'] === $language)
+ {
$lang['selected'] = true;
+ }
+ }
}
// Saving them?
if (isset($_GET['save']))
{
- // Handle verification questions.
- $questionInserts = array();
- $count_questions = 0;
-
- foreach ($_POST['question'] as $id => $question)
- {
- $question = trim(\Util::htmlspecialchars($question, ENT_COMPAT));
- $answers = array();
- $question_lang = isset($_POST['language'][$id]) && isset($languages[$_POST['language'][$id]]) ? $_POST['language'][$id] : $language;
- if (!empty($_POST['answer'][$id]))
- foreach ($_POST['answer'][$id] as $answer)
- {
- $answer = trim(\Util::strtolower(\Util::htmlspecialchars($answer, ENT_COMPAT)));
- if ($answer != '')
- $answers[] = $answer;
- }
-
- // Already existed?
- if (isset($context['question_answers'][$id]))
- {
- $count_questions++;
-
- // Changed?
- if ($question == '' || empty($answers))
- {
- $this->_delete($id);
- $count_questions--;
- }
- else
- $this->_update($id, $question, $answers, $question_lang);
- }
- // It's so shiney and new!
- elseif ($question != '' && !empty($answers))
- {
- $questionInserts[] = array(
- 'question' => $question,
- // @todo: remotely possible that the serialized value is longer than 65535 chars breaking the update/insertion
- 'answer' => serialize($answers),
- 'language' => $question_lang,
- );
- $count_questions++;
- }
- }
-
- // Any questions to insert?
- if (!empty($questionInserts))
- $this->_insert($questionInserts);
+ $count_questions = $this->_saveSettings($_POST['question'], $_POST['answer'], $_POST['language']);
if (empty($count_questions) || $_POST['qa_verification_number'] > $count_questions)
+ {
$_POST['qa_verification_number'] = $count_questions;
-
+ }
}
+ $context['question_answers'] = $this->_loadAntispamQuestions();
+
return array(
// Clever Thomas, who is looking sheepy now? Not I, the mighty sword swinger did say.
array('title', 'setup_verification_questions'),
@@ -295,6 +287,73 @@ public function settings()
);
}
+ /**
+ * Prepares the questions coming from the UI to be saved into the database.
+ *
+ * @param string[] $save_question
+ * @return boolean
+ */
+ protected function _saveSettings($save_question, $save_answer, $save_language)
+ {
+ $existing_question_answers = $this->_loadAntispamQuestions();
+
+ // Handle verification questions.
+ $questionInserts = array();
+ $count_questions = 0;
+
+ foreach ($save_question as $id => $question)
+ {
+ $question = trim(\Util::htmlspecialchars($question, ENT_COMPAT));
+ $answers = array();
+ $question_lang = isset($save_language[$id]) && isset($languages[$save_language[$id]]) ? $save_language[$id] : $language;
+ if (!empty($save_answer[$id]))
+ {
+ foreach ($save_answer[$id] as $answer)
+ {
+ $answer = trim(\Util::strtolower(\Util::htmlspecialchars($answer, ENT_COMPAT)));
+ if ($answer != '')
+ $answers[] = $answer;
+ }
+ }
+
+ // Already existed?
+ if (isset($existing_question_answers[$id]))
+ {
+ $count_questions++;
+
+ // Changed?
+ if ($question == '' || empty($answers))
+ {
+ $this->_delete($id);
+ $count_questions--;
+ }
+ else
+ {
+ $this->_update($id, $question, $answers, $question_lang);
+ }
+ }
+ // It's so shiney and new!
+ elseif ($question != '' && !empty($answers))
+ {
+ $questionInserts[] = array(
+ 'question' => $question,
+ // @todo: remotely possible that the serialized value is longer than 65535 chars breaking the update/insertion
+ 'answer' => serialize($answers),
+ 'language' => $question_lang,
+ );
+ $count_questions++;
+ }
+ }
+
+ // Any questions to insert?
+ if (!empty($questionInserts))
+ {
+ $this->_insert($questionInserts);
+ }
+
+ return $count_questions;
+ }
+
/**
* Checks if an the answers to anti-spam questions are correct
*
@@ -302,8 +361,9 @@ public function settings()
*/
private function _verifyAnswers($sessionVal)
{
+ $this->_filter = new \ElkArte\ValuesContainer(array('type' => 'id_question', 'value' => $sessionVal['q']));
// Get the answers and see if they are all right!
- $questions = $this->_loadAntispamQuestions(array('type' => 'id_question', 'value' => $sessionVal['q']));
+ $questions = $this->_loadAntispamQuestions();
$this->_incorrectQuestions = array();
foreach ($questions as $row)
{
@@ -349,30 +409,28 @@ private function _refreshQuestionsCache()
/**
* Loads all the available antispam questions, or a subset based on a filter
*
- * @param array|null $filter if specified it myst be an array with two indexes:
- * - 'type' => a valid filter, it can be 'language' or 'id_question'
- * - 'value' => the value of the filter (i.e. the language)
- *
* @return array
*/
- private function _loadAntispamQuestions($filter = null)
+ private function _loadAntispamQuestions()
{
$db = database();
- $available_filters = array(
- 'language' => 'language = {string:current_filter}',
- 'id_question' => 'id_question IN ({array_int:current_filter})',
- );
+ $available_filters = new \ElkArte\ValuesContainer(array(
+ 'language' => '
+ WHERE language = {string:current_filter}',
+ 'id_question' => '
+ WHERE id_question IN ({array_int:current_filter})',
+ ));
+ $condition = (string) $available_filters[$this->_filter['type']];
// Load any question and answers!
$question_answers = array();
$request = $db->query('', '
SELECT
id_question, question, answer, language
- FROM {db_prefix}antispam_questions' . ($filter === null || !isset($available_filters[$filter['type']]) ? '' : '
- WHERE ' . $available_filters[$filter['type']]),
+ FROM {db_prefix}antispam_questions' . $condition,
array(
- 'current_filter' => $filter['value'],
+ 'current_filter' => $this->_filter['value'],
)
);
while ($row = $db->fetch_assoc($request))
diff --git a/sources/subs/VerificationControls.integrate.php b/sources/subs/VerificationControls.integrate.php
index 974acf42d3..45a78c6197 100644
--- a/sources/subs/VerificationControls.integrate.php
+++ b/sources/subs/VerificationControls.integrate.php
@@ -97,7 +97,7 @@ public static function create(&$verificationOptions, $do_test = false)
// Start with any testing.
if ($do_test)
{
- $force_refresh = $instances->test($verification_errors);
+ $force_refresh = $instances->test($verification_errors, $max_errors);
}
// Are we refreshing then?
diff --git a/sources/subs/VerificationControls.php b/sources/subs/VerificationControls.php
index 064c026b63..6950e88586 100644
--- a/sources/subs/VerificationControls.php
+++ b/sources/subs/VerificationControls.php
@@ -50,7 +50,7 @@ public function __construct(SessionIndex $sessionVal, $settings = array(), $veri
{
$settings['known_verifications'] = self::discoverControls();
}
- $this->_known_verifications = json_decode($settings['known_verifications']);
+ $this->_known_verifications = json_decode($settings['known_verifications'], true);
$this->_verification_options = $verificationOptions;
$this->_verification_options['render'] = false;
$this->_sessionVal = $sessionVal;
@@ -137,8 +137,9 @@ protected static function loadFSControls()
* Runs the tests and populates the errors (if any)
*
* @param \ElkArte\Errors\ErrorContext $verification_errors
+ * @param int $max_errors
*/
- public function test($verification_errors)
+ public function test($verification_errors, $max_errors)
{
$increase_error_count = false;
$force_refresh = false;
@@ -167,7 +168,7 @@ public function test($verification_errors)
$this->_sessionVal['errors'] = 0;
}
// Too many errors?
- elseif ($this->_sessionVal['errors'] > $thisVerification['max_errors'])
+ elseif ($this->_sessionVal['errors'] > $max_errors)
{
$force_refresh = true;
}
diff --git a/tests/sources/subs/Search.class.Test.php b/tests/sources/subs/Search.class.Test.php
new file mode 100644
index 0000000000..a43a2f9e19
--- /dev/null
+++ b/tests/sources/subs/Search.class.Test.php
@@ -0,0 +1,277 @@
+run_tests = false;
+ return;
+ }
+
+ // This is here to cheat with allowedTo
+ $user_info['is_admin'] = true;
+
+ // Create 2 boards
+ require_once(SUBSDIR . '/Boards.subs.php');
+ $this->board_all_access = createBoard([
+ 'board_name' => 'Search 1',
+ 'redirect' => '',
+ 'move_to' => 'bottom',
+ 'target_category' => 1,
+ 'access_groups' => [0,1],
+ 'deny_groups' => [],
+ ]);
+ $this->board_limited_access = createBoard([
+ 'board_name' => 'Search 2',
+ 'redirect' => '',
+ 'move_to' => 'bottom',
+ 'target_category' => 1,
+ 'access_groups' => [1],
+ 'deny_groups' => [],
+ ]);
+
+ // Create 3 users (actually 2)
+ // One user must have access to all the boards
+ // One user must have access to one board
+ // One user must have access to no boards (guests)
+ require_once(SUBSDIR . '/Members.subs.php');
+
+ $regOptions1 = [
+ 'interface' => 'admin',
+ 'username' => 'Search User 1',
+ 'email' => 'search@email1.tld',
+ 'password' => 'password',
+ 'password_check' => 'password',
+ 'check_reserved_name' => false,
+ 'check_password_strength' => false,
+ 'check_email_ban' => false,
+ 'send_welcome_email' => false,
+ 'require' => 'nothing',
+ 'memberGroup' => 1,
+ 'ip' => '127.0.0.1',
+ 'ip2' => '127.0.0.1',
+ 'auth_method' => 'password',
+ ];
+ $this->member_full_access = registerMember($regOptions1);
+
+ $regOptions2 = [
+ 'interface' => 'admin',
+ 'username' => 'Search User 2',
+ 'email' => 'search@email2.tld',
+ 'password' => 'password',
+ 'password_check' => 'password',
+ 'check_reserved_name' => false,
+ 'check_password_strength' => false,
+ 'check_email_ban' => false,
+ 'send_welcome_email' => false,
+ 'require' => 'nothing',
+ 'memberGroup' => 0,
+ 'ip' => '127.0.0.1',
+ 'ip2' => '127.0.0.1',
+ 'auth_method' => 'password',
+ ];
+ $this->member_limited_access = registerMember($regOptions2);
+
+ // Hopefully a guest doesn't have any access
+ $this->member_no_access = 0;
+
+ // Create 2 topics, one for each board
+ require_once(SUBSDIR . '/Post.subs.php');
+ $msgOptions = [
+ 'id' => 0,
+ 'subject' => 'Visible search topic',
+ 'body' => 'Visible search topic',
+ 'icon' => '',
+ 'smileys_enabled' => true,
+ 'approved' => true,
+ ];
+ $topicOptions = [
+ 'id' => 0,
+ 'board' => $this->board_all_access,
+ 'lock_mode' => null,
+ 'sticky_mode' => null,
+ 'mark_as_read' => true,
+ 'is_approved' => true,
+ ];
+ $posterOptions = [
+ 'id' => $this->member_full_access,
+ 'name' => 'guestname',
+ 'email' => $regOptions1['email'],
+ 'update_post_count' => false,
+ ];
+ createPost($msgOptions, $topicOptions, $posterOptions);
+
+ $msgOptions = [
+ 'id' => 0,
+ 'subject' => 'Hidden search topic',
+ 'body' => 'Hidden search topic',
+ 'icon' => '',
+ 'smileys_enabled' => true,
+ 'approved' => true,
+ ];
+ $topicOptions = [
+ 'id' => 0,
+ 'board' => $this->board_limited_access,
+ 'lock_mode' => null,
+ 'sticky_mode' => null,
+ 'mark_as_read' => true,
+ 'is_approved' => true,
+ ];
+ $posterOptions = [
+ 'id' => $this->member_full_access,
+ 'name' => 'guestname',
+ 'email' => $regOptions1['email'],
+ 'update_post_count' => false,
+ ];
+ createPost($msgOptions, $topicOptions, $posterOptions);
+
+ Elk_Autoloader::instance()->register(SUBSDIR . '/Search', '\\ElkArte\\Search');
+ }
+
+ /**
+ * Admins are able to find both topics
+ */
+ public function testBasicAdminSearch()
+ {
+ global $cookiename;
+
+ if ($this->run_tests === false)
+ {
+ return;
+ }
+
+ $_COOKIE[$cookiename] = json_encode([$this->member_full_access, $this->_getSalt($this->member_full_access)]);
+ loadUserSettings();
+
+ $topics = $this->_performSearch();
+ $this->assertEquals(2, count($topics), 'Admin search results not correct, found ' . count($topics) . ' instead of 2');
+ }
+
+ /**
+ * Normal users with access to one of the two boards are able to find
+ * only one topic
+ */
+ public function testBasicMemberSearch()
+ {
+ global $cookiename;
+
+ if ($this->run_tests === false)
+ {
+ return;
+ }
+
+ $_COOKIE[$cookiename] = json_encode([$this->member_limited_access, $this->_getSalt($this->member_limited_access)]);
+ loadUserSettings();
+
+ $topics = $this->_performSearch();
+ $this->assertEquals(1, count($topics), 'Normal member search results not correct, found ' . count($topics) . ' instead of 1');
+ }
+
+ /**
+ * Guests do not have access to the boards, so they cannot find any result
+ *
+ * @expectedException \Exception
+ * @expectedExceptionMessage query_not_specific_enough
+ */
+ public function testBasicGuestSearch()
+ {
+ global $cookiename;
+
+ if ($this->run_tests === false)
+ {
+ throw new \Exception('query_not_specific_enough');
+ }
+
+ $_COOKIE[$cookiename] = null;
+ loadUserSettings();
+
+ $topics = $this->_performSearch();
+ $this->assertEquals(0, count($topics), 'Guest search results not correct, found ' . count($topics) . ' instead of 0');
+
+ }
+
+ protected function _performSearch()
+ {
+ global $modSettings, $context;
+
+ $recentPercentage = 0.30;
+ $maxMembersToSearch = 500;
+ $humungousTopicPosts = 200;
+ $maxMessageResults = empty($modSettings['search_max_results']) ? 0 :
+ $search_terms = [
+ 'search' => 'search',
+ 'search_selection' => 'all',
+ 'advanced' => 0
+ ];
+ $_GET['search'] = $search_terms['search'];
+ $search = new \ElkArte\Search\Search();
+ $search->setWeights(new \ElkArte\Search\WeightFactors($modSettings, 1));
+ $search_params = new \ElkArte\Search\SearchParams('');
+ $search_params->merge($search_terms, $recentPercentage, $maxMembersToSearch);
+ $search->setParams($search_params, !empty($modSettings['search_simple_fulltext']));
+ $context['params'] = $search->compileURLparams();
+
+ $search_config = new \ElkArte\ValuesContainer(array(
+ 'humungousTopicPosts' => $humungousTopicPosts,
+ 'maxMessageResults' => $maxMessageResults,
+ 'search_index' => !empty($modSettings['search_index']) ? $modSettings['search_index'] : '',
+ 'banned_words' => empty($modSettings['search_banned_words']) ? array() : explode(',', $modSettings['search_banned_words']),
+ ));
+ $topics = $search->searchQuery(
+ new \ElkArte\Search\SearchApiWrapper($search_config, $search->getSearchParams())
+ );
+
+ return $topics;
+ }
+
+ protected function _getSalt($member)
+ {
+ $db = database();
+
+ if (empty($member))
+ {
+ return '';
+ }
+
+ $res = $db->fetchQuery('
+ SELECT passwd, password_salt
+ FROM {db_prefix}members
+ where id_member = {int:id_member}',
+ array(
+ 'id_member' => $member
+ )
+ );
+
+ if (empty($res))
+ {
+ return '';
+ }
+ return hash('sha256', ($res[0]['passwd'] . $res[0]['password_salt']));
+ }
+
+ /**
+ * cleanup data we no longer need at the end of the tests in this class.
+ *
+ * tearDown() is run automatically by the testing framework after each test method.
+ */
+ public function tearDown()
+ {
+ }
+}
diff --git a/themes/default/PersonalMessage.template.php b/themes/default/PersonalMessage.template.php
index 4f598dcee9..344db1695a 100644
--- a/themes/default/PersonalMessage.template.php
+++ b/themes/default/PersonalMessage.template.php
@@ -84,13 +84,14 @@ function template_folder()
$start = true;
+ $controller = $context['get_pmessage'][0];
// Do we have some messages to display?
- if ($context['get_pmessage']('message', true))
+ if ($controller->{$context['get_pmessage'][1]}(true))
{
echo '