Przeglądaj źródła

Fix overlapping saved search references (#9484)

Match the longest saved query reference before shorter references so
expansion does not depend on their order. Apply the same replacement
behaviour when preserving unexpanded searches, including negated ones.

Add regression coverage for overlapping IDs, names, and ID groups.

Refs #9483

Co-authored-by: Busy-SweetPotato <346315145+Busy-SweetPotato@users.noreply.github.com>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
Busy SweetPotato 1 dzień temu
rodzic
commit
73b47af405

+ 1 - 0
CHANGELOG.md

@@ -10,6 +10,7 @@ See also [the FreshRSS releases](https://github.com/FreshRSS/FreshRSS/releases).
 	* Fix HTTP Retry-After cache when proxy is used [#9508](https://github.com/FreshRSS/FreshRSS/pull/9508)
 	* Fix HTTP Retry-After cache when proxy is used [#9508](https://github.com/FreshRSS/FreshRSS/pull/9508)
 	* Fix 32-bit systems provide a string for SQL BIGINT [#9486](https://github.com/FreshRSS/FreshRSS/pull/9486)
 	* Fix 32-bit systems provide a string for SQL BIGINT [#9486](https://github.com/FreshRSS/FreshRSS/pull/9486)
 	* Keep JSON feed attachments as enclosures [#9510](https://github.com/FreshRSS/FreshRSS/pull/9510)
 	* Keep JSON feed attachments as enclosures [#9510](https://github.com/FreshRSS/FreshRSS/pull/9510)
+	* Fix saved search references with overlapping IDs or names [#9484](https://github.com/FreshRSS/FreshRSS/pull/9484)
 	* Fix non-ASCII characters in scripts of fetched HTML pages [#9384](https://github.com/FreshRSS/FreshRSS/pull/9384)
 	* Fix non-ASCII characters in scripts of fetched HTML pages [#9384](https://github.com/FreshRSS/FreshRSS/pull/9384)
 * Security
 * Security
 	* 💥 Replace bcrypt challenge auth with direct password comparison [#9358](https://github.com/FreshRSS/FreshRSS/pull/9358)
 	* 💥 Replace bcrypt challenge auth with direct password comparison [#9358](https://github.com/FreshRSS/FreshRSS/pull/9358)

+ 1 - 0
CREDITS.md

@@ -57,6 +57,7 @@ People are sorted by name so please keep this order.
 * [Brewal Bouvet](https://github.com/Jucgshu): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Jucgshu)
 * [Brewal Bouvet](https://github.com/Jucgshu): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Jucgshu)
 * [Brooke.](https://github.com/BrookeDot): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:BrookeDot), [Web](https://brooke.codes/)
 * [Brooke.](https://github.com/BrookeDot): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:BrookeDot), [Web](https://brooke.codes/)
 * [Bryan Butler](https://github.com/BryanButlerGit): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:BryanButlerGit)
 * [Bryan Butler](https://github.com/BryanButlerGit): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:BryanButlerGit)
+* [Busy-SweetPotato](https://github.com/Busy-SweetPotato): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Busy-SweetPotato)
 * [caminsha](https://github.com/caminsha): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:caminsha)
 * [caminsha](https://github.com/caminsha): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:caminsha)
 * [CarelessCaution](https://github.com/ghost): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:CarelessCaution)
 * [CarelessCaution](https://github.com/ghost): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:CarelessCaution)
 * [Carey Metcalfe](https://github.com/pR0Ps): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:pR0Ps), [Web](https://cmetcalfe.ca/)
 * [Carey Metcalfe](https://github.com/pR0Ps): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:pR0Ps), [Web](https://cmetcalfe.ca/)

+ 2 - 2
app/Models/BooleanSearch.php

@@ -97,7 +97,7 @@ class FreshRSS_BooleanSearch implements \Stringable {
 				}
 				}
 			}
 			}
 
 
-			$input = str_replace($fromS, $toS, $input);
+			$input = strtr($input, array_combine($fromS, $toS));
 		}
 		}
 		return $input;
 		return $input;
 	}
 	}
@@ -145,7 +145,7 @@ class FreshRSS_BooleanSearch implements \Stringable {
 				}
 				}
 			}
 			}
 
 
-			$input = str_replace($fromS, $toS, $input);
+			$input = strtr($input, array_combine($fromS, $toS));
 		}
 		}
 		return $input;
 		return $input;
 	}
 	}

+ 6 - 6
app/Models/Search.php

@@ -873,7 +873,7 @@ class FreshRSS_Search implements \Stringable {
 	 */
 	 */
 	private function parseUserQueryIds(string $input): string {
 	private function parseUserQueryIds(string $input): string {
 		if (preg_match_all('/\\b[S]:(?P<search>[0-9,]+|[*])/', $input, $matches)) {
 		if (preg_match_all('/\\b[S]:(?P<search>[0-9,]+|[*])/', $input, $matches)) {
-			$input = str_replace($matches[0], '', $input);
+			$input = strtr($input, array_fill_keys($matches[0], ''));
 			$ids_lists = $matches['search'];
 			$ids_lists = $matches['search'];
 			$this->user_query_ids = [];
 			$this->user_query_ids = [];
 			foreach ($ids_lists as $ids_list) {
 			foreach ($ids_lists as $ids_list) {
@@ -895,7 +895,7 @@ class FreshRSS_Search implements \Stringable {
 
 
 	private function parseNotUserQueryIds(string $input): string {
 	private function parseNotUserQueryIds(string $input): string {
 		if (preg_match_all('/(?<=[\\s(]|^)[!-][S]:(?P<search>[0-9,]+|[*])/', $input, $matches)) {
 		if (preg_match_all('/(?<=[\\s(]|^)[!-][S]:(?P<search>[0-9,]+|[*])/', $input, $matches)) {
-			$input = str_replace($matches[0], '', $input);
+			$input = strtr($input, array_fill_keys($matches[0], ''));
 			$ids_lists = $matches['search'];
 			$ids_lists = $matches['search'];
 			$this->not_user_query_ids = [];
 			$this->not_user_query_ids = [];
 			foreach ($ids_lists as $ids_list) {
 			foreach ($ids_lists as $ids_list) {
@@ -922,11 +922,11 @@ class FreshRSS_Search implements \Stringable {
 		$names_lists = [];
 		$names_lists = [];
 		if (preg_match_all('/\\bsearch?:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
 		if (preg_match_all('/\\bsearch?:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
 			$names_lists = $matches['search'];
 			$names_lists = $matches['search'];
-			$input = str_replace($matches[0], '', $input);
+			$input = strtr($input, array_fill_keys($matches[0], ''));
 		}
 		}
 		if (preg_match_all('/\\bsearch?:(?P<search>[^\s"]*)/', $input, $matches)) {
 		if (preg_match_all('/\\bsearch?:(?P<search>[^\s"]*)/', $input, $matches)) {
 			$names_lists = array_merge($names_lists, $matches['search']);
 			$names_lists = array_merge($names_lists, $matches['search']);
-			$input = str_replace($matches[0], '', $input);
+			$input = strtr($input, array_fill_keys($matches[0], ''));
 		}
 		}
 		if (!empty($names_lists)) {
 		if (!empty($names_lists)) {
 			$this->user_query_names = [];
 			$this->user_query_names = [];
@@ -948,11 +948,11 @@ class FreshRSS_Search implements \Stringable {
 		$names_lists = [];
 		$names_lists = [];
 		if (preg_match_all('/(?<=[\\s(]|^)[!-]search?:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
 		if (preg_match_all('/(?<=[\\s(]|^)[!-]search?:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
 			$names_lists = $matches['search'];
 			$names_lists = $matches['search'];
-			$input = str_replace($matches[0], '', $input);
+			$input = strtr($input, array_fill_keys($matches[0], ''));
 		}
 		}
 		if (preg_match_all('/(?<=[\\s(]|^)[!-]search?:(?P<search>[^\\s"]*)/', $input, $matches)) {
 		if (preg_match_all('/(?<=[\\s(]|^)[!-]search?:(?P<search>[^\\s"]*)/', $input, $matches)) {
 			$names_lists = array_merge($names_lists, $matches['search']);
 			$names_lists = array_merge($names_lists, $matches['search']);
-			$input = str_replace($matches[0], '', $input);
+			$input = strtr($input, array_fill_keys($matches[0], ''));
 		}
 		}
 		if (!empty($names_lists)) {
 		if (!empty($names_lists)) {
 			$this->not_user_query_names = [];
 			$this->not_user_query_names = [];

+ 58 - 0
tests/app/Models/SearchTest.php

@@ -1230,6 +1230,10 @@ final class SearchTest extends \PHPUnit\Framework\TestCase {
 	 * @return array<string,array{0:list<array{search:string,name?:string}>,1:string,2:string,3:string}>
 	 * @return array<string,array{0:list<array{search:string,name?:string}>,1:string,2:string,3:string}>
 	 */
 	 */
 	public static function provideBooleanSearchToStringExpansion(): array {
 	public static function provideBooleanSearchToStringExpansion(): array {
+		$overlappingQueries = array_fill(0, 11, ['search' => 'author:Unused']);
+		$overlappingQueries[1] = ['name' => 'News', 'search' => 'author:Alice'];
+		$overlappingQueries[10] = ['name' => 'Newsletter', 'search' => 'author:Bob'];
+
 		return [
 		return [
 			'Not found ID' => [
 			'Not found ID' => [
 				[
 				[
@@ -1267,6 +1271,60 @@ final class SearchTest extends \PHPUnit\Framework\TestCase {
 				'search:First search:Second',
 				'search:First search:Second',
 				'author:Alice intitle:World',
 				'author:Alice intitle:World',
 			],
 			],
+			'Longer ID first' => [
+				$overlappingQueries,
+				'S:10 OR S:1',
+				'S:10 OR S:1',
+				'author:Bob OR author:Alice',
+			],
+			'Shorter ID first' => [
+				$overlappingQueries,
+				'S:1 OR S:10',
+				'S:1 OR S:10',
+				'author:Alice OR author:Bob',
+			],
+			'Overlapping IDs with AND' => [
+				$overlappingQueries,
+				'S:1 S:10',
+				'S:1 S:10',
+				'author:Alice author:Bob',
+			],
+			'Overlapping ID groups' => [
+				$overlappingQueries,
+				'S:1,10 S:1',
+				'S:1,10 S:1',
+				'(author:Alice OR author:Bob) author:Alice',
+			],
+			'Longer name first' => [
+				$overlappingQueries,
+				'search:Newsletter OR search:News',
+				'search:Newsletter OR search:News',
+				'author:Bob OR author:Alice',
+			],
+			'Shorter name first' => [
+				$overlappingQueries,
+				'search:News OR search:Newsletter',
+				'search:News OR search:Newsletter',
+				'author:Alice OR author:Bob',
+			],
+			'Overlapping names with AND' => [
+				$overlappingQueries,
+				'search:News search:Newsletter',
+				'search:News search:Newsletter',
+				'author:Alice author:Bob',
+			],
+			'Negated overlapping IDs' => [
+				$overlappingQueries,
+				'-S:1 -S:10',
+				'-S:1 -S:10',
+				'-author:Alice -author:Bob',
+			],
+			'Negated overlapping names' => [
+				$overlappingQueries,
+				'-search:News -search:Newsletter',
+				'-search:News -search:Newsletter',
+				'-author:Alice -author:Bob',
+			],
 		];
 		];
 	}
 	}