Skip to content

Commit d55fe05

Browse files
committed
Fix exclusion term handling in search queries: strip prefix before sanitizing, separate negative clauses, and wrap positive expressions to prevent FULLTEXT bypass
- Check exclusion prefix BEFORE stripping operators so the prefix is still present - Strip prefix before sanitizing term with preg_replace - Skip empty terms after sanitization - Collect negative clauses separately and append with AND to prevent FULLTEXT branch from bypassing exclusions - Wrap positive expression before appending negations
1 parent 31d618e commit d55fe05

17 files changed

Lines changed: 134 additions & 49 deletions

includes/class-better-search-core-query.php

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -850,14 +850,21 @@ public function posts_search( $where, $query ) {
850850
}
851851

852852
foreach ( (array) $search_terms as $term ) {
853-
$term = preg_replace( '/[+\-*"~<>()@\']/', '', $term );
854-
855853
// If there is an $exclusion_prefix, terms prefixed with it should be excluded.
854+
// Check BEFORE stripping operators so the prefix is still present.
856855
$exclude = $exclusion_prefix && ( substr( $term, 0, 1 ) === $exclusion_prefix );
856+
if ( $exclude ) {
857+
$term = substr( $term, strlen( $exclusion_prefix ) );
858+
}
859+
$term = preg_replace( '/[+\-*"~<>()@\']/', '', $term );
860+
861+
if ( '' === $term ) {
862+
continue;
863+
}
864+
857865
if ( $exclude ) {
858866
$like_op = 'NOT LIKE';
859867
$andor_op = 'AND';
860-
$term = substr( $term, 1 );
861868
} else {
862869
$like_op = 'LIKE';
863870
$andor_op = 'OR';
@@ -886,17 +893,25 @@ public function posts_search( $where, $query ) {
886893
}
887894

888895
// Let's do a LIKE search for all other fields.
889-
$searchand = '';
896+
$searchand = '';
897+
$negative_clauses = array();
890898
foreach ( (array) $search_terms as $term ) {
891-
$term = preg_replace( '/[+\-*"~<>()@\']/', '', $term );
899+
// Check exclusion BEFORE stripping operators so the prefix is still present.
900+
$exclude = $exclusion_prefix && ( substr( $term, 0, 1 ) === $exclusion_prefix );
901+
if ( $exclude ) {
902+
$term = substr( $term, strlen( $exclusion_prefix ) );
903+
}
904+
$term = preg_replace( '/[+\-*"~<>()@\']/', '', $term );
905+
906+
if ( '' === $term ) {
907+
continue;
908+
}
909+
892910
$clause = array();
893911

894-
// If there is an $exclusion_prefix, terms prefixed with it should be excluded.
895-
$exclude = $exclusion_prefix && ( substr( $term, 0, 1 ) === $exclusion_prefix );
896912
if ( $exclude ) {
897913
$like_op = 'NOT LIKE';
898914
$andor_op = ' AND ';
899-
$term = substr( $term, 1 );
900915
} else {
901916
$like_op = 'LIKE';
902917
$andor_op = ' OR ';
@@ -963,15 +978,28 @@ public function posts_search( $where, $query ) {
963978
$clause = apply_filters_ref_array( 'better_search_query_posts_search_clauses', array( $clause, $term, $like_op, $andor_op, $query, &$this ) );
964979

965980
if ( ! empty( $clause ) ) {
966-
$search_clause .= " {$searchand} (" . implode( $andor_op, (array) $clause ) . ') ';
967-
$searchand = ' AND ';
981+
if ( $exclude ) {
982+
$negative_clauses[] = '(' . implode( $andor_op, (array) $clause ) . ')';
983+
} else {
984+
$search_clause .= " {$searchand} (" . implode( $andor_op, (array) $clause ) . ') ';
985+
$searchand = ' AND ';
986+
}
968987
}
969988
}
970989

971990
if ( ! empty( $search_clause ) ) {
972991
$search .= " OR ({$search_clause}) ";
973992
}
974993

994+
if ( ! empty( $negative_clauses ) ) {
995+
// Wrap the entire positive expression before appending negations, so that
996+
// SQL AND-before-OR precedence does not let the FULLTEXT branch bypass the
997+
// exclusion clauses (e.g. "-tiger" failing to exclude posts that pass FULLTEXT).
998+
// When there are no positive terms, use 1=1 as a neutral base to avoid malformed SQL.
999+
$search = ! empty( $search ) ? '(' . $search . ')' : '1=1';
1000+
$search .= ' AND ' . implode( ' AND ', $negative_clauses );
1001+
}
1002+
9751003
if ( ! empty( $search ) ) {
9761004
$where = " AND ({$search}) ";
9771005

load-freemius.php

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,23 @@ function bsearch_freemius() {
2323
require_once __DIR__ . '/vendor/freemius/start.php';
2424
$bsearch_freemius = \fs_dynamic_init(
2525
array(
26-
'id' => '17020',
27-
'slug' => 'better-search',
28-
'premium_slug' => 'better-search-pro',
29-
'type' => 'plugin',
30-
'public_key' => 'pk_40525301bca835d9836ec4d946693',
31-
'is_premium' => false,
32-
'premium_suffix' => 'Pro',
33-
'has_addons' => false,
34-
'has_paid_plans' => true,
35-
'menu' => array(
26+
'id' => '17020',
27+
'slug' => 'better-search',
28+
'premium_slug' => 'better-search-pro',
29+
'type' => 'plugin',
30+
'public_key' => 'pk_40525301bca835d9836ec4d946693',
31+
'is_premium' => false,
32+
'premium_suffix' => 'Pro',
33+
'has_addons' => false,
34+
'has_paid_plans' => true,
35+
'menu' => array(
3636
'slug' => 'bsearch_dashboard',
3737
'contact' => false,
3838
'support' => false,
3939
'network' => true,
4040
),
41-
'is_live' => true,
41+
'is_live' => true,
42+
'is_org_compliant' => true,
4243
)
4344
);
4445
}

readme.txt

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,34 +128,50 @@ You can report security bugs through the Patchstack Vulnerability Disclosure Pro
128128
= 4.3.0 =
129129

130130
* Features:
131+
* [Pro] New: WP-CLI support with comprehensive command-line interface (search, cache, db, stats, settings, tables, status, stopwords commands).
132+
* [Pro] Dashboard chart drill-down: click any bar in the daily searches chart to view the popular searches for that day.
131133
* [Pro] New InnoDB conversion tool: convert the custom table engine with automatic FULLTEXT index recreation.
132134
* [Pro] Scheduled reconciliation cron: a twicedaily job automatically syncs any published posts missing from the custom search index table.
135+
* [Pro] New exclusion options: Exclude Front page and Exclude Posts page settings to optionally remove these pages from search results.
136+
* [Pro] Network dashboard with popular searches chart and statistics table for multisite networks, accessible from the network admin menu.
133137

134138
* Enhancements:
139+
* [Pro] Multisite admin select-all checkboxes and post-copy URL cleanup are now handled by an external JavaScript file (via `wp_enqueue_script`) instead of inline `<script>` blocks — improves compatibility with strict Content Security Policies.
140+
* [Pro] Copy-to-clipboard buttons on the tools and custom tables pages are now initialized automatically; no per-block inline script needed.
135141
* [Pro] Improved short-term (≤3 character) LIKE searches to score full-word matches higher and order results by relevance.
136142
* [Pro] Refactored fuzzy query shaping so `Query_Modifier` owns score construction and request shaping, with `Fuzzy_Search` acting as the fuzzy scoring service.
137143
* [Pro] Rewrote soundex function, removed multisite LIMIT cap, and added content scoring for fuzzy search.
138144
* [Pro] Added filters for fuzzy search truncation parameters.
139145
* [Pro] Centralized exclusion term parsing logic in Helpers class.
146+
* [Pro] Custom tables search now supports a FULLTEXT toggle, with improved LIKE-only relevance scoring when FULLTEXT is disabled.
147+
* [Pro] Improved multisite search query composition: correctly unwraps fuzzy subqueries before UNION assembly and strips only top-level ORDER BY clauses, preventing malformed SQL.
148+
* [Pro] LIKE term matching in custom tables search now uses an EXISTS subquery to avoid unbounded JOINs when the terms table is not already in scope.
149+
* [Pro] Database check results are now cached within a request, reducing redundant `SHOW TABLES` queries on pages that check table status multiple times.
150+
* [Pro] Dashboard popular searches query result is now cached within a request to avoid repeated database hits.
151+
* Refactored Media Handler with a strategy-based thumbnail resolution chain; now also supports ACF Image fields (Image Array, Image ID, Image URL) and plain text URL fields.
140152
* Hardened search sanitization and boolean mode validation for more consistent results.
141153
* Escaped output in settings forms for improved security.
142154

143155
* Bug fixes:
156+
* [Pro] Fixed localized admin script data keys: removed erroneous `.strings.` nesting that caused the cache-clear confirmation and error dialogs to display `undefined`.
157+
* Fixed spinner alignment inside action buttons (now displays inline rather than floating).
144158
* [Pro] Fixed fuzzy LIKE query SQL issues that could generate duplicate `ID` fields in wrapped sub-queries.
145159
* [Pro] Fixed fuzzy search bypassing FULLTEXT exclusions.
146160
* [Pro] Fixed inconsistent indentation and table alias qualification in multisite query composition.
147161
* [Pro] Disabled fuzzy search when boolean operators are present to prevent conflicts.
162+
* Fixed duplicate search query being executed on every non-seamless search page load.
163+
* Fixed relevance percentages on paginated search results by stabilizing topscore handling across pages, while reducing unnecessary topscore queries when minimum relevance filtering is not in use.
148164
* Fixed placeholder attribute escaping in text field rendering.
149165

150166

151167
= 4.2.4 =
152168

153169
* Features:
154-
* Better Search form: The "any" post type option label can now be customised when the post type dropdown is enabled.
170+
* Better Search form: The any post type option label can now be customised when the post type dropdown is enabled.
155171
* Media Handler now detects featured images provided by the Featured Image from URL (FIFU) plugin.
156172

157173
* Fixed:
158-
* Fixed an issue where selecting "any" post type would search through all post types instead of respecting the configured post types from settings.
174+
* Fixed an issue where selecting any post type would search through all post types instead of respecting the configured post types from settings.
159175
* [Pro] Custom table searches now include post slug matching when “Search post slug” is enabled.
160176
* [Pro] Fixed SQL syntax error in multisite search queries when custom tables are disabled, caused by malformed GROUP BY clause stripping.
161177
* Fixed improper stripping of boolean mode operators in LIKE clauses, ensuring consistent behavior between FULLTEXT and LIKE searches.
@@ -232,5 +248,5 @@ For previous changelog entries, please refer to the separate changelog.txt file
232248

233249
== Upgrade Notice ==
234250

235-
= 4.3.0 =
236-
Fixes post type selection to respect configured settings when "any" is selected.
251+
= 4.3.0 =
252+
Adds WP-CLI support, dashboard chart drill-down, an InnoDB conversion tool, scheduled index reconciliation, and a network admin dashboard for multisite. Includes a fuzzy search refactor and a long list of stability fixes.

vendor/freemius/assets/js/pricing/freemius-pricing.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/freemius/includes/class-freemius.php

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6558,10 +6558,7 @@ private function maybe_schedule_sync_cron() {
65586558
$next_schedule = $this->next_sync_cron();
65596559

65606560
// The event is properly scheduled, so no need to reschedule it.
6561-
if (
6562-
is_numeric( $next_schedule ) &&
6563-
$next_schedule > time()
6564-
) {
6561+
if ( is_numeric( $next_schedule ) ) {
65656562
return;
65666563
}
65676564

@@ -7098,7 +7095,6 @@ private function add_sticky_optin_admin_notice() {
70987095
*/
70997096
function _enqueue_connect_essentials() {
71007097
wp_enqueue_script( 'jquery' );
7101-
wp_enqueue_script( 'json2' );
71027098

71037099
fs_enqueue_local_script( 'postmessage', 'nojquery.ba-postmessage.js' );
71047100
fs_enqueue_local_script( 'fs-postmessage', 'postmessage.js' );
@@ -17436,7 +17432,7 @@ function setup_network_account(
1743617432
FS_User_Lock::instance()->unlock();
1743717433
}
1743817434

17439-
if ( 1 < count( $installs ) ) {
17435+
if ( 1 < count( $installs ) || fs_is_network_admin() ) {
1744017436
// Only network level opt-in can have more than one install.
1744117437
$is_network_level_opt_in = true;
1744217438
}

vendor/freemius/includes/class-fs-plugin-updater.php

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -540,9 +540,32 @@ function pre_set_site_transient_update_plugins_filter( $transient_data ) {
540540
return $transient_data;
541541
}
542542

543+
// Alias.
544+
$basename = $this->_fs->premium_plugin_basename();
545+
543546
global $wp_current_filter;
544547

545-
if ( ! empty( $wp_current_filter ) && in_array( 'upgrader_process_complete', $wp_current_filter ) ) {
548+
/**
549+
* During bulk updates, avoid re-injecting update data for the plugin itself once it has already been updated.
550+
*
551+
* If the custom package is re-added to the transient after the plugin update, WordPress may detect the package again and incorrectly report "The plugin is at the latest version" for a pending update, since the custom package version matches the currently installed version.
552+
*
553+
* Behavior differs depending on how the bulk update is triggered. Please refer to the inline comments for each flow below for details.
554+
*/
555+
if (
556+
! empty( $wp_current_filter ) && (
557+
/**
558+
* update-core.php and other upgrader pages:
559+
* The `upgrader_process_complete` action fires only once after all updates have finished. In this case, it is the current action (`$wp_current_filter[0]`), while `self::$_upgrade_basename` may contain any plugin basename.
560+
*/
561+
'upgrader_process_complete' === $wp_current_filter[0] ||
562+
/**
563+
* AJAX bulk updates (e.g., from the Plugins page):
564+
* The `upgrader_process_complete` action fires multiple times — once for each plugin after it finishes updating. In this flow, it is not the current action (`$wp_current_filter[0]`) because it is triggered from another action. Instead, we compare `self::$_upgrade_basename` with the basename of the plugin currently being updated, since the `upgrader_process_complete` action runs separately for each plugin.
565+
*/
566+
( in_array( 'upgrader_process_complete', $wp_current_filter ) && self::$_upgrade_basename === $basename )
567+
)
568+
) {
546569
return $transient_data;
547570
}
548571

@@ -566,9 +589,6 @@ function pre_set_site_transient_update_plugins_filter( $transient_data ) {
566589
}
567590
}
568591

569-
// Alias.
570-
$basename = $this->_fs->premium_plugin_basename();
571-
572592
if ( is_object( $this->_update_details ) ) {
573593
if ( isset( $transient_data->no_update ) ) {
574594
unset( $transient_data->no_update[ $basename ] );

vendor/freemius/includes/fs-essential-functions.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
* @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License Version 3
1111
* @since 1.1.5
1212
*/
13+
if ( ! defined( 'ABSPATH' ) ) {
14+
exit;
15+
}
1316

1417
if ( ! function_exists( 'fs_normalize_path' ) ) {
1518
if ( function_exists( 'wp_normalize_path' ) ) {

vendor/freemius/includes/managers/class-fs-contact-form-manager.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public function get_standalone_link( Freemius $fs ) {
7777
$query_params = $this->get_query_params( $fs );
7878

7979
$query_params['is_standalone'] = 'true';
80-
$query_params['parent_url'] = admin_url( add_query_arg( null, null ) );
80+
$query_params['parent_url'] = admin_url( add_query_arg( '', '' ) );
8181

8282
return WP_FS__ADDRESS . '/contact/?' . http_build_query( $query_params );
8383
}

vendor/freemius/includes/managers/class-fs-debug-manager.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
* @package Freemius
77
* @since 2.6.2
88
*/
9+
if ( ! defined( 'ABSPATH' ) ) {
10+
exit;
11+
}
912

1013
class FS_DebugManager {
1114

vendor/freemius/start.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*
1616
* @var string
1717
*/
18-
$this_sdk_version = '2.13.0';
18+
$this_sdk_version = '2.13.1';
1919

2020
#region SDK Selection Logic --------------------------------------------------------------------
2121

0 commit comments

Comments
 (0)