Regex search (#6706)

* Regex search
fix https://github.com/FreshRSS/FreshRSS/issues/3549

* Fix PHPStan

* Fix escape

* Fix ungreedy

* Initial support for regex search in PostgreSQL and MySQL

* Improvements, support MySQL

* Fix multiline

* Add support for SQLite

* A few tests

* Added author: and inurl: support, documentation

* author example

* Remove \b for now

* Disable regex sanitization for now

* Fix getInurlRegex

* getNotInurlRegex

* Quotes for inurl:

* Fix test

* Fix quoted tags + regex for tags
https://github.com/FreshRSS/FreshRSS/issues/6761

* Fix wrong regex detection

* Add MariaDB

* Fix logic

* Increase requirements for MySQL and MariaDB
Check support for multiline mode in MySQL

* Remove sanitizeRegexes()

* Allow searching HTML code
Allow searching for instance `/<pre>/`
Fix https://github.com/FreshRSS/FreshRSS/issues/6775#issuecomment-2331769883

* Doc regex search HTML

* Fix Doctype
This commit is contained in:
Alexandre Alapetite authored and GitHub committed 2024-09-06 09:35:58 +02:00
1 parent 35a7634e68
commit 1a552bd60e
19 files changed
+670 -53

No files matched your search

+171 -3
View File
@@ -124,10 +124,10 @@ class SearchTest extends PHPUnit\Framework\TestCase {
public static function provideInurlSearch(): array {
return [
['inurl:word1', ['word1'], null],
['inurl: word1', [], ['word1']],
['inurl: word1', null, ['word1']],
['inurl:123', ['123'], null],
['inurl:word1 word2', ['word1'], ['word2']],
['inurl:"word1 word2"', ['"word1'], ['word2"']],
['inurl:"word1 word2"', ['word1 word2'], null],
['inurl:word1 word2 inurl:word3', ['word1', 'word3'], ['word2']],
["inurl:word1 'word2 word3' word4", ['word1'], ['word2 word3', 'word4']],
['inurl:word1+word2', ['word1+word2'], null],
@@ -196,7 +196,7 @@ class SearchTest extends PHPUnit\Framework\TestCase {
['# word1', null, ['#', 'word1']],
['#123', ['123'], null],
['#word1 word2', ['word1'], ['word2']],
['#"word1 word2"', ['"word1'], ['word2"'],],
['#"word1 word2"', ['word1 word2'], null],
['#word1 #word2', ['word1', 'word2'], null],
["#word1 'word2 word3' word4", ['word1'], ['word2 word3', 'word4']],
['#word1+word2', ['word1 word2'], null]
@@ -442,4 +442,172 @@ class SearchTest extends PHPUnit\Framework\TestCase {
],
];
}
/**
* @dataProvider provideRegexPostreSQL
* @param array<string> $values
*/
public function test__regex_postgresql(string $input, string $sql, array $values): void {
[$filterValues, $filterSearch] = FreshRSS_EntryDAOPGSQL::sqlBooleanSearch('e.', new FreshRSS_BooleanSearch($input));
self::assertEquals(trim($sql), trim($filterSearch));
self::assertEquals($values, $filterValues);
}
/** @return array<array<mixed>> */
public function provideRegexPostreSQL(): array {
return [
[
'intitle:/^ab$/',
'(e.title ~ ? )',
['^ab$']
],
[
'intitle:/^ab$/i',
'(e.title ~* ? )',
['^ab$']
],
[
'intitle:/^ab$/m',
'(e.title ~ ? )',
['(?m)^ab$']
],
[
'intitle:/^ab\\M/',
'(e.title ~ ? )',
['^ab\\M']
],
[
'author:/^ab$/',
"(REPLACE(e.author, ';', '\n') ~ ? )",
['^ab$']
],
[
'inurl:/^ab$/',
'(e.link ~ ? )',
['^ab$']
],
[
'/^ab$/',
'((e.title ~ ? OR e.content ~ ?) )',
['^ab$', '^ab$']
],
[
'!/^ab$/',
'(NOT e.title ~ ? AND NOT e.content ~ ? )',
['^ab$', '^ab$']
],
[ // Not a regex
'inurl:https://example.net/test/',
'(e.link LIKE ? )',
['%https://example.net/test/%']
],
[ // Not a regex
'https://example.net/test/',
'((e.title LIKE ? OR e.content LIKE ?) )',
['%https://example.net/test/%', '%https://example.net/test/%']
],
];
}
/**
* @dataProvider provideRegexMariaDB
* @param array<string> $values
*/
public function test__regex_mariadb(string $input, string $sql, array $values): void {
FreshRSS_DatabaseDAO::$dummyConnection = true;
FreshRSS_DatabaseDAO::setStaticVersion('11.4.3-MariaDB-ubu2404');
[$filterValues, $filterSearch] = FreshRSS_EntryDAO::sqlBooleanSearch('e.', new FreshRSS_BooleanSearch($input));
self::assertEquals(trim($sql), trim($filterSearch));
self::assertEquals($values, $filterValues);
}
/** @return array<array<mixed>> */
public function provideRegexMariaDB(): array {
return [
[
'intitle:/^ab$/',
"(e.title REGEXP ? )",
['(?-i)^ab$']
],
[
'intitle:/^ab$/i',
"(e.title REGEXP ? )",
['(?i)^ab$']
],
[
'intitle:/^ab$/m',
"(e.title REGEXP ? )",
['(?-i)(?m)^ab$']
],
];
}
/**
* @dataProvider provideRegexMySQL
* @param array<string> $values
*/
public function test__regex_mysql(string $input, string $sql, array $values): void {
FreshRSS_DatabaseDAO::$dummyConnection = true;
FreshRSS_DatabaseDAO::setStaticVersion('9.0.1');
[$filterValues, $filterSearch] = FreshRSS_EntryDAO::sqlBooleanSearch('e.', new FreshRSS_BooleanSearch($input));
self::assertEquals(trim($sql), trim($filterSearch));
self::assertEquals($values, $filterValues);
}
/** @return array<array<mixed>> */
public function provideRegexMySQL(): array {
return [
[
'intitle:/^ab$/',
"(REGEXP_LIKE(e.title,?,'c') )",
['^ab$']
],
[
'intitle:/^ab$/i',
"(REGEXP_LIKE(e.title,?,'i') )",
['^ab$']
],
[
'intitle:/^ab$/m',
"(REGEXP_LIKE(e.title,?,'mc') )",
['^ab$']
],
];
}
/**
* @dataProvider provideRegexSQLite
* @param array<string> $values
*/
public function test__regex_sqlite(string $input, string $sql, array $values): void {
[$filterValues, $filterSearch] = FreshRSS_EntryDAOSQLite::sqlBooleanSearch('e.', new FreshRSS_BooleanSearch($input));
self::assertEquals(trim($sql), trim($filterSearch));
self::assertEquals($values, $filterValues);
}
/** @return array<array<mixed>> */
public function provideRegexSQLite(): array {
return [
[
'intitle:/^ab$/',
"(e.title REGEXP ? )",
['/^ab$/']
],
[
'intitle:/^ab$/i',
"(e.title REGEXP ? )",
['/^ab$/i']
],
[
'intitle:/^ab$/m',
"(e.title REGEXP ? )",
['/^ab$/m']
],
[
'intitle:/^ab\\b/',
'(e.title REGEXP ? )',
['/^ab\\b/']
],
];
}
}