From 142ca20cb034b0b197b44cf1094aac10e2c7996a Mon Sep 17 00:00:00 2001 From: wn_ Date: Sun, 20 Oct 2024 16:07:50 +0000 Subject: [PATCH 1/2] Fix keyword searches with a quoted string value. Before this change curly braces wrapped the keyword and its value, making the pair get treated as leftover words. Also make the search query modification and CSV parsing a bit clearer with some comments and minor refactoring. --- classes/Feeds.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/classes/Feeds.php b/classes/Feeds.php index 5239c9f8a..2a3715211 100644 --- a/classes/Feeds.php +++ b/classes/Feeds.php @@ -2212,7 +2212,13 @@ class Feeds extends Handler_Protected { * @return array{0: string, 1: array} [$search_query_part, $search_words] */ private static function _search_to_sql(string $search, string $search_language, int $owner_uid): array { - $keywords = str_getcsv(preg_replace('/(-?\w+)\:"(\w+)/', '"{$1}:{$2}', trim($search)), ' ', '"', ''); + // Modify the search string so that 'keyword:"foo bar"' becomes '"keyword:foo bar"'. + // This is needed so potential command pairs are grouped correctly. + $search_csv_str = preg_replace('/(-?\w+)\:"(\w+)/', '"$1:$2', trim($search)); + + // $keywords will be an array like ['"title:hello world"', 'some', 'words'] + $keywords = str_getcsv($search_csv_str, ' ', '"', ''); + $query_keywords = array(); $search_words = array(); $search_query_leftover = array(); @@ -2271,7 +2277,6 @@ class Feeds extends Handler_Protected { } break; case "star": - if ($commandpair[1]) { if ($commandpair[1] == "true") array_push($query_keywords, "($not (marked = true))"); From 842e9af4cf8a0d896cb6c3ac29d0a57566e51f65 Mon Sep 17 00:00:00 2001 From: wn_ Date: Sun, 20 Oct 2024 17:57:32 +0000 Subject: [PATCH 2/2] Feeds::_search_to_sql(): updates for clarity and SQL quoting. --- classes/Feeds.php | 61 +++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/classes/Feeds.php b/classes/Feeds.php index 2a3715211..34540ff11 100644 --- a/classes/Feeds.php +++ b/classes/Feeds.php @@ -2225,11 +2225,9 @@ class Feeds extends Handler_Protected { $pdo = Db::pdo(); - if ($search_language) - $search_language = $pdo->quote(mb_strtolower($search_language)); - else - $search_language = $pdo->quote(mb_strtolower(get_pref(Prefs::DEFAULT_SEARCH_LANGUAGE, $owner_uid))); + $search_language = $pdo->quote(mb_strtolower($search_language ?: get_pref(Prefs::DEFAULT_SEARCH_LANGUAGE, $owner_uid))); + /** @var string $k a keyword pair (not yet split) or standalone value */ foreach ($keywords as $k) { if (strpos($k, "-") === 0) { $k = substr($k, 1); @@ -2238,38 +2236,42 @@ class Feeds extends Handler_Protected { $not = ""; } - $commandpair = explode(":", mb_strtolower($k), 2); + $keyword_pair = explode(':', mb_strtolower($k), 2); + $keyword_name = $keyword_pair[0]; + $keyword_value = empty($keyword_pair[1]) ? '' : trim($keyword_pair[1]); - switch ($commandpair[0]) { + // NOTE: If there's a keyword match but no keyword value we fall back to doing + // a search in article title and content. + switch ($keyword_name) { case "title": - if ($commandpair[1]) { + if ($keyword_value) { array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE ". - $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%') ."))"); + $pdo->quote("%{$keyword_value}%") ."))"); } else { - array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%') + array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").") OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))"); array_push($search_words, $k); } break; case "author": - if ($commandpair[1]) { + if ($keyword_value) { array_push($query_keywords, "($not (LOWER(author) LIKE ". - $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))"); + $pdo->quote("%{$keyword_value}%")."))"); } else { - array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%') + array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").") OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))"); array_push($search_words, $k); } break; case "note": - if ($commandpair[1]) { - if ($commandpair[1] == "true") + if ($keyword_value) { + if ($keyword_value == "true") array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))"); - else if ($commandpair[1] == "false") + else if ($keyword_value == "false") array_push($query_keywords, "($not (note IS NULL OR note = ''))"); else array_push($query_keywords, "($not (LOWER(note) LIKE ". - $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))"); + $pdo->quote("%{$keyword_value}%")."))"); } else { array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").") OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))"); @@ -2277,8 +2279,8 @@ class Feeds extends Handler_Protected { } break; case "star": - if ($commandpair[1]) { - if ($commandpair[1] == "true") + if ($keyword_value) { + if ($keyword_value == "true") array_push($query_keywords, "($not (marked = true))"); else array_push($query_keywords, "($not (marked = false))"); @@ -2289,12 +2291,11 @@ class Feeds extends Handler_Protected { } break; case "pub": - if ($commandpair[1]) { - if ($commandpair[1] == "true") + if ($keyword_value) { + if ($keyword_value == "true") array_push($query_keywords, "($not (published = true))"); else array_push($query_keywords, "($not (published = false))"); - } else { array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%') OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))"); @@ -2302,8 +2303,8 @@ class Feeds extends Handler_Protected { } break; case "label": - if ($commandpair[1]) { - $label_id = Labels::find_id($commandpair[1], $_SESSION["uid"]); + if ($keyword_value) { + $label_id = Labels::find_id($keyword_value, $_SESSION["uid"]); if ($label_id) { array_push($query_keywords, "($not @@ -2320,8 +2321,8 @@ class Feeds extends Handler_Protected { } break; case "unread": - if ($commandpair[1]) { - if ($commandpair[1] == "true") + if ($keyword_value) { + if ($keyword_value == "true") array_push($query_keywords, "($not (unread = true))"); else array_push($query_keywords, "($not (unread = false))"); @@ -2333,22 +2334,24 @@ class Feeds extends Handler_Protected { } break; default: + // @{date} handling if (strpos($k, "@") === 0) { - $user_tz_string = get_pref(Prefs::USER_TIMEZONE, $_SESSION['uid']); $orig_ts = strtotime(substr($k, 1)); $k = date("Y-m-d", TimeHelper::convert_timestamp($orig_ts, $user_tz_string, 'UTC')); //$k = date("Y-m-d", strtotime(substr($k, 1))); - array_push($query_keywords, "(".SUBSTRING_FOR_DATE."(updated,1,LENGTH('$k')) $not = '$k')"); + array_push($query_keywords, "(".SUBSTRING_FOR_DATE."(updated,1,LENGTH(".$pdo->quote($k).")) $not = ".$pdo->quote($k).")"); } else { + // treat as leftover text + + // TODO: handle multiword strings in the fulltext search + $k = mb_strtolower($k); if (Config::get(Config::DB_TYPE) == "pgsql") { - $k = mb_strtolower($k); array_push($search_query_leftover, $not ? "!$k" : $k); } else { - $k = mb_strtolower($k); array_push($search_query_leftover, $not ? "-$k" : $k); //array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")