mirror of
https://github.com/FreshRSS/FreshRSS.git
synced 2026-10-28 05:58:40 -04:00
Drop PHP 8.1 (EOL December 2025), drop Ubuntu 22.04 (EOL May 2027, and there is now 24.04 and 26.04 LTS). PHP 8.1 is already EOL, and Debian, Alpine, Fedora, Red Hat, Synology have dropped PHP 8.1 already. See https://github.com/FreshRSS/FreshRSS/discussions/5474#discussioncomment-16791937 With PHP 8.6 around the corner and in particular a relevant `FILTER_FLAG_GLOBAL_RANGE` in PHP 8.2+. * https://www.php.net/releases/8.2 * https://www.php.net/filter.constants.validation#constant.filter-flag-global-range * https://www.php.net/class.sensitiveparameter * https://php.watch/versions/8.2/readonly-classes * https://www.php.net/function.ini-parse-quantity Allows updating to PHPUnit 11.x instead of 10.x https://phpunit.de/supported-versions.html Contributes to https://github.com/FreshRSS/FreshRSS/issues/9475 * Add regression test for FILTER_FLAG_GLOBAL_RANGE https://github.com/FreshRSS/FreshRSS/pull/9470#discussion_r4371168720
1373 lines
45 KiB
PHP
1373 lines
45 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
use PHPUnit\Framework\Attributes\DataProvider;
|
|
|
|
require_once LIB_PATH . '/lib_date.php';
|
|
|
|
final class SearchTest extends \PHPUnit\Framework\TestCase {
|
|
|
|
#[\Override]
|
|
protected function setUp(): void {
|
|
if (!FreshRSS_Context::hasSystemConf()) {
|
|
FreshRSS_Context::initSystem();
|
|
}
|
|
}
|
|
|
|
#[DataProvider('provideEmptyInput')]
|
|
public static function test__construct_whenInputIsEmpty_getsOnlyNullValues(string $input): void {
|
|
$search = new FreshRSS_Search($input);
|
|
self::assertSame('', $search->__toString());
|
|
self::assertNull($search->getIntitle());
|
|
self::assertNull($search->getMinDate());
|
|
self::assertNull($search->getMaxDate());
|
|
self::assertNull($search->getMinPubdate());
|
|
self::assertNull($search->getMaxPubdate());
|
|
self::assertNull($search->getAuthor());
|
|
self::assertNull($search->getTags());
|
|
self::assertNull($search->getSearch());
|
|
}
|
|
|
|
/**
|
|
* Return an array of values for the search object.
|
|
* Here is the description of the values
|
|
* @return array{array{''},array{' '}}
|
|
*/
|
|
public static function provideEmptyInput(): array {
|
|
return [
|
|
[''],
|
|
[' '],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string>|null $intitle_value
|
|
* @param array<string>|null $search_value
|
|
*/
|
|
#[DataProvider('provideIntitleSearch')]
|
|
public static function test__construct_whenInputContainsIntitle_setsIntitleProperty(string $input, ?array $intitle_value, ?array $search_value): void {
|
|
$search = new FreshRSS_Search($input);
|
|
self::assertSame($intitle_value, $search->getIntitle());
|
|
self::assertSame($search_value, $search->getSearch());
|
|
}
|
|
|
|
/**
|
|
* @return list<list<mixed>>
|
|
*/
|
|
public static function provideIntitleSearch(): array {
|
|
return [
|
|
['intitle:word1', ['word1'], null],
|
|
['intitle:word1-word2', ['word1-word2'], null],
|
|
['intitle:word1 word2', ['word1'], ['word2']],
|
|
['intitle:"word1 word2"', ['word1 word2'], null],
|
|
["intitle:'word1 word2'", ['word1 word2'], null],
|
|
['word1 intitle:word2', ['word2'], ['word1']],
|
|
['word1 intitle:word2 word3', ['word2'], ['word1', 'word3']],
|
|
['word1 intitle:"word2 word3"', ['word2 word3'], ['word1']],
|
|
["word1 intitle:'word2 word3'", ['word2 word3'], ['word1']],
|
|
['intitle:word1 intitle:word2', ['word1', 'word2'], null],
|
|
['intitle: word1 word2', null, ['word1', 'word2']],
|
|
['intitle:123', ['123'], null],
|
|
['intitle:"word1 word2" word3"', ['word1 word2'], ['word3"']],
|
|
["intitle:'word1 word2' word3'", ['word1 word2'], ["word3'"]],
|
|
['intitle:"word1 word2\' word3"', ["word1 word2' word3"], null],
|
|
["intitle:'word1 word2\" word3'", ['word1 word2" word3'], null],
|
|
['intitle:"< & >"', ['< & >'], null],
|
|
["intitle:word1 'word2 word3' word4", ['word1'], ['word2 word3', 'word4']],
|
|
['intitle:word1+word2', ['word1+word2'], null],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string>|null $intext_value
|
|
* @param array<string>|null $search_value
|
|
*/
|
|
#[DataProvider('provideIntextSearch')]
|
|
public static function test__construct_whenInputContainsIntext(string $input, ?array $intext_value, ?array $search_value): void {
|
|
$search = new FreshRSS_Search($input);
|
|
self::assertSame($intext_value, $search->getIntext());
|
|
self::assertSame($search_value, $search->getSearch());
|
|
}
|
|
|
|
/**
|
|
* @return list<list<mixed>>
|
|
*/
|
|
public static function provideIntextSearch(): array {
|
|
return [
|
|
['intext:word1', ['word1'], null],
|
|
['intext:"word1 word2"', ['word1 word2'], null],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string>|null $author_value
|
|
* @param array<string>|null $search_value
|
|
*/
|
|
#[DataProvider('provideAuthorSearch')]
|
|
public static function test__construct_whenInputContainsAuthor_setsAuthorValue(string $input, ?array $author_value, ?array $search_value): void {
|
|
$search = new FreshRSS_Search($input);
|
|
self::assertSame($author_value, $search->getAuthor());
|
|
self::assertSame($search_value, $search->getSearch());
|
|
}
|
|
|
|
/**
|
|
* @return list<list<mixed>>
|
|
*/
|
|
public static function provideAuthorSearch(): array {
|
|
return [
|
|
['author:word1', ['word1'], null],
|
|
['author:word1-word2', ['word1-word2'], null],
|
|
['author:word1 word2', ['word1'], ['word2']],
|
|
['author:"word1 word2"', ['word1 word2'], null],
|
|
["author:'word1 word2'", ['word1 word2'], null],
|
|
['word1 author:word2', ['word2'], ['word1']],
|
|
['word1 author:word2 word3', ['word2'], ['word1', 'word3']],
|
|
['word1 author:"word2 word3"', ['word2 word3'], ['word1']],
|
|
["word1 author:'word2 word3'", ['word2 word3'], ['word1']],
|
|
['author:word1 author:word2', ['word1', 'word2'], null],
|
|
['author: word1 word2', null, ['word1', 'word2']],
|
|
['author:123', ['123'], null],
|
|
['author:"word1 word2" word3"', ['word1 word2'], ['word3"']],
|
|
["author:'word1 word2' word3'", ['word1 word2'], ["word3'"]],
|
|
['author:"word1 word2\' word3"', ["word1 word2' word3"], null],
|
|
["author:'word1 word2\" word3'", ['word1 word2" word3'], null],
|
|
["author:word1 'word2 word3' word4", ['word1'], ['word2 word3', 'word4']],
|
|
['author:word1+word2', ['word1+word2'], null],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string>|null $inurl_value
|
|
* @param array<string>|null $search_value
|
|
*/
|
|
#[DataProvider('provideInurlSearch')]
|
|
public static function test__construct_whenInputContainsInurl_setsInurlValue(string $input, ?array $inurl_value, ?array $search_value): void {
|
|
$search = new FreshRSS_Search($input);
|
|
self::assertSame($inurl_value, $search->getInurl());
|
|
self::assertSame($search_value, $search->getSearch());
|
|
}
|
|
|
|
/**
|
|
* @return list<list<mixed>>
|
|
*/
|
|
public static function provideInurlSearch(): array {
|
|
return [
|
|
['inurl:word1', ['word1'], null],
|
|
['inurl: word1', null, ['word1']],
|
|
['inurl:123', ['123'], null],
|
|
['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],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('provideDateSearch')]
|
|
public static function test__construct_whenInputContainsDate_setsDateValues(string $input, ?int $min_date_value, ?int $max_date_value): void {
|
|
$search = new FreshRSS_Search($input);
|
|
self::assertSame($min_date_value, $search->getMinDate());
|
|
self::assertSame($max_date_value, $search->getMaxDate());
|
|
}
|
|
|
|
/**
|
|
* @return list<list<mixed>>
|
|
*/
|
|
public static function provideDateSearch(): array {
|
|
return [
|
|
['date:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z', strtotime('2007-03-01T13:00:00Z'), strtotime('2008-05-11T15:30:00Z')],
|
|
['date:2007-03-01T13:00:00Z/P1Y2M10DT2H30M', strtotime('2007-03-01T13:00:00Z'), strtotime('2008-05-11T15:29:59Z')],
|
|
['date:P1Y2M10DT2H30M/2008-05-11T15:30:00Z', strtotime('2007-03-01T13:00:01Z'), strtotime('2008-05-11T15:30:00Z')],
|
|
['date:2007-03-01/2008-05-11', strtotime('2007-03-01'), strtotime('2008-05-12') - 1],
|
|
['date:2007-03-01/', strtotime('2007-03-01'), null],
|
|
['date:/2008-05-11', null, strtotime('2008-05-12') - 1],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('providePubdateSearch')]
|
|
public static function test__construct_whenInputContainsPubdate_setsPubdateValues(string $input, ?int $min_pubdate_value, ?int $max_pubdate_value): void {
|
|
$search = new FreshRSS_Search($input);
|
|
self::assertSame($min_pubdate_value, $search->getMinPubdate());
|
|
self::assertSame($max_pubdate_value, $search->getMaxPubdate());
|
|
}
|
|
|
|
/**
|
|
* @return list<list<mixed>>
|
|
*/
|
|
public static function providePubdateSearch(): array {
|
|
return [
|
|
['pubdate:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z', strtotime('2007-03-01T13:00:00Z'), strtotime('2008-05-11T15:30:00Z')],
|
|
['pubdate:2007-03-01T13:00:00Z/P1Y2M10DT2H30M', strtotime('2007-03-01T13:00:00Z'), strtotime('2008-05-11T15:29:59Z')],
|
|
['pubdate:P1Y2M10DT2H30M/2008-05-11T15:30:00Z', strtotime('2007-03-01T13:00:01Z'), strtotime('2008-05-11T15:30:00Z')],
|
|
['pubdate:2007-03-01/2008-05-11', strtotime('2007-03-01'), strtotime('2008-05-12') - 1],
|
|
['pubdate:2007-03-01/', strtotime('2007-03-01'), null],
|
|
['pubdate:/2008-05-11', null, strtotime('2008-05-12') - 1],
|
|
];
|
|
}
|
|
|
|
|
|
|
|
#[DataProvider('provideModifiedDateSearch')]
|
|
public static function test__construct_whenInputContainsModifiedDate(string $input, ?int $min_modified_value, ?int $max_modified_value): void {
|
|
$search = new FreshRSS_Search($input);
|
|
self::assertSame($min_modified_value, $search->getMinModifiedDate());
|
|
self::assertSame($max_modified_value, $search->getMaxModifiedDate());
|
|
}
|
|
|
|
/**
|
|
* @return list<list<mixed>>
|
|
*/
|
|
public static function provideModifiedDateSearch(): array {
|
|
return [
|
|
['mdate:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z', strtotime('2007-03-01T13:00:00Z'), strtotime('2008-05-11T15:30:00Z')],
|
|
['mdate:/2008-05-11', null, strtotime('2008-05-12') - 1],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('provideUserdateSearch')]
|
|
public static function test__construct_whenInputContainsUserdate(string $input, ?int $min_userdate_value, ?int $max_userdate_value): void {
|
|
$search = new FreshRSS_Search($input);
|
|
self::assertSame($min_userdate_value, $search->getMinUserdate());
|
|
self::assertSame($max_userdate_value, $search->getMaxUserdate());
|
|
}
|
|
|
|
/**
|
|
* @return list<list<mixed>>
|
|
*/
|
|
public static function provideUserdateSearch(): array {
|
|
return [
|
|
['userdate:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z', strtotime('2007-03-01T13:00:00Z'), strtotime('2008-05-11T15:30:00Z')],
|
|
['userdate:/2008-05-11', null, strtotime('2008-05-12') - 1],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string>|null $tags_value
|
|
* @param array<string>|null $search_value
|
|
*/
|
|
#[DataProvider('provideTagsSearch')]
|
|
public static function test__construct_whenInputContainsTags_setsTagsValue(string $input, ?array $tags_value, ?array $search_value): void {
|
|
$search = new FreshRSS_Search($input);
|
|
self::assertSame($tags_value, $search->getTags());
|
|
self::assertSame($search_value, $search->getSearch());
|
|
}
|
|
|
|
/**
|
|
* @return list<list<string|list<string>|null>>
|
|
*/
|
|
public static function provideTagsSearch(): array {
|
|
return [
|
|
['#word1', ['word1'], null],
|
|
['# word1', null, ['#', 'word1']],
|
|
['#123', ['123'], null],
|
|
['#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]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param list<array{search:string,name?:string}> $queries
|
|
* @param array{0:string,1:list<string|int>} $expectedResult
|
|
*/
|
|
#[DataProvider('provideSavedQueriesExpansion')]
|
|
public static function test__construct_whenInputContainsSavedQueries_expandsSavedSearches(array $queries, string $input,
|
|
array $expectedResult, string $expectedToString): void {
|
|
$previousUserConf = FreshRSS_Context::hasUserConf() ? FreshRSS_Context::userConf() : null;
|
|
$newUserConf = $previousUserConf instanceof FreshRSS_UserConfiguration ? clone $previousUserConf : clone FreshRSS_UserConfiguration::default();
|
|
$newUserConf->queries = $queries;
|
|
FreshRSS_Context::setUserConf($newUserConf);
|
|
|
|
try {
|
|
$search = new FreshRSS_BooleanSearch($input);
|
|
[$actualValues, $actualSql] = FreshRSS_EntryDAOPGSQL::sqlBooleanSearch('e.', $search);
|
|
self::assertSame($expectedResult[0], trim($actualSql));
|
|
self::assertSame($expectedResult[1], $actualValues);
|
|
self::assertSame($expectedToString, $search->toString(expandUserQueries: false));
|
|
} finally {
|
|
FreshRSS_Context::setUserConf($previousUserConf);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return array<string,array{0:list<array{search:string}>,1:string,2:array{0:string,1:list<string|int>},3:string}>
|
|
*/
|
|
public static function provideSavedQueriesExpansion(): array {
|
|
return [
|
|
'not found ID' => [
|
|
[
|
|
['search' => 'author:Alice'],
|
|
['search' => 'intitle:World'],
|
|
],
|
|
'S:3',
|
|
[
|
|
'',
|
|
[],
|
|
],
|
|
'S:3',
|
|
],
|
|
'not found name' => [
|
|
[
|
|
['search' => 'author:Alice', 'name' => 'First'],
|
|
['search' => 'intitle:World', 'name' => 'Second'],
|
|
],
|
|
'search:Third',
|
|
[
|
|
'',
|
|
[],
|
|
],
|
|
'search:Third',
|
|
],
|
|
'expanded single group name' => [
|
|
[
|
|
['search' => 'author:Alice', 'name' => 'First'],
|
|
['search' => 'intitle:World', 'name' => 'Second'],
|
|
],
|
|
'search:First OR search:Second',
|
|
[
|
|
'((e.author LIKE ?)) OR ((e.title LIKE ?))',
|
|
['%Alice%', '%World%'],
|
|
],
|
|
'search:First OR search:Second',
|
|
],
|
|
'expanded single group name quotes' => [
|
|
[
|
|
['search' => 'author:Alice', 'name' => 'A First'],
|
|
['search' => 'intitle:World', 'name' => 'A Second'],
|
|
],
|
|
'search:"A First" OR search:\'A Second\'',
|
|
[
|
|
'((e.author LIKE ?)) OR ((e.title LIKE ?))',
|
|
['%Alice%', '%World%'],
|
|
],
|
|
'(search:"A First") OR (search:"A Second")',
|
|
],
|
|
'expanded single group name quotes special characters' => [
|
|
[
|
|
['search' => 'author:Alice', 'name' => 'A or B'],
|
|
['search' => 'intitle:World', 'name' => '(C OR D)'],
|
|
],
|
|
'search:"A or B" OR search:\'(C OR D)\'',
|
|
[
|
|
'((e.author LIKE ?)) OR ((e.title LIKE ?))',
|
|
['%Alice%', '%World%'],
|
|
],
|
|
'(search:"A or B") OR (search:"(C OR D)")',
|
|
],
|
|
'separate groups with AND' => [
|
|
[
|
|
['search' => 'author:Alice'],
|
|
['search' => 'intitle:World'],
|
|
['search' => 'inurl:Example'],
|
|
['search' => 'author:Bob'],
|
|
],
|
|
'S:0,1 S:2,3,5',
|
|
[
|
|
'(((e.author LIKE ?)) OR ((e.title LIKE ?))) AND (((e.link LIKE ?)) OR ((e.author LIKE ?)))',
|
|
['%Alice%', '%World%', '%Example%', '%Bob%'],
|
|
],
|
|
'S:0,1 S:2,3,5',
|
|
],
|
|
'separate groups with OR' => [
|
|
[
|
|
['search' => 'author:Alice'],
|
|
['search' => 'intitle:World'],
|
|
['search' => 'inurl:Example'],
|
|
['search' => 'author:Bob'],
|
|
],
|
|
'S:0,1 OR S:2,3,5',
|
|
[
|
|
'(((e.author LIKE ?)) OR ((e.title LIKE ?))) OR (((e.link LIKE ?)) OR ((e.author LIKE ?)))',
|
|
['%Alice%', '%World%', '%Example%', '%Bob%'],
|
|
],
|
|
'S:0,1 OR S:2,3,5',
|
|
],
|
|
'mixed with other clauses' => [
|
|
[
|
|
['search' => 'author:Alice'],
|
|
['search' => 'intitle:World'],
|
|
],
|
|
'date:2025-10 intitle:Hello S:0,1',
|
|
[
|
|
'((e.id >= ? AND e.id <= ? AND e.title LIKE ?)) AND (((e.author LIKE ?)) OR ((e.title LIKE ?)))',
|
|
[strtotime('2025-10-01') . '000000', (strtotime('2025-11-01') - 1) . '000000', '%Hello%', '%Alice%', '%World%'],
|
|
],
|
|
'date:2025-10 intitle:Hello S:0,1',
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string>|null $author_value
|
|
* @param array<string> $intitle_value
|
|
* @param array<string>|null $inurl_value
|
|
* @param array<string>|null $tags_value
|
|
* @param array<string>|null $search_value
|
|
*/
|
|
#[DataProvider('provideMultipleSearch')]
|
|
public static function test__construct_whenInputContainsMultipleKeywords_setsValues(string $input, ?array $author_value, ?int $min_date_value,
|
|
?int $max_date_value, ?array $intitle_value, ?array $inurl_value, ?int $min_pubdate_value,
|
|
?int $max_pubdate_value, ?array $tags_value, ?array $search_value): void {
|
|
$search = new FreshRSS_Search($input);
|
|
self::assertSame($author_value, $search->getAuthor());
|
|
self::assertSame($min_date_value, $search->getMinDate());
|
|
self::assertSame($max_date_value, $search->getMaxDate());
|
|
self::assertSame($intitle_value, $search->getIntitle());
|
|
self::assertSame($inurl_value, $search->getInurl());
|
|
self::assertSame($min_pubdate_value, $search->getMinPubdate());
|
|
self::assertSame($max_pubdate_value, $search->getMaxPubdate());
|
|
self::assertSame($tags_value, $search->getTags());
|
|
self::assertSame($search_value, $search->getSearch());
|
|
}
|
|
|
|
/** @return list<list<mixed>> */
|
|
public static function provideMultipleSearch(): array {
|
|
return [
|
|
[
|
|
'author:word1 date:2007-03-01/2008-05-11 intitle:word2 inurl:word3 pubdate:2007-03-01/2008-05-11 #word4 #word5',
|
|
['word1'],
|
|
strtotime('2007-03-01'),
|
|
strtotime('2008-05-12') - 1,
|
|
['word2'],
|
|
['word3'],
|
|
strtotime('2007-03-01'),
|
|
strtotime('2008-05-12') - 1,
|
|
['word4', 'word5'],
|
|
null
|
|
],
|
|
[
|
|
'word6 intitle:word2 inurl:word3 pubdate:2007-03-01/2008-05-11 #word4 author:word1 #word5 date:2007-03-01/2008-05-11',
|
|
['word1'],
|
|
strtotime('2007-03-01'),
|
|
strtotime('2008-05-12') - 1,
|
|
['word2'],
|
|
['word3'],
|
|
strtotime('2007-03-01'),
|
|
strtotime('2008-05-12') - 1,
|
|
['word4', 'word5'],
|
|
['word6']
|
|
],
|
|
[
|
|
'word6 intitle:word2 inurl:word3 pubdate:2007-03-01/2008-05-11 #word4 author:word1 #word5 word7 date:2007-03-01/2008-05-11',
|
|
['word1'],
|
|
strtotime('2007-03-01'),
|
|
strtotime('2008-05-12') - 1,
|
|
['word2'],
|
|
['word3'],
|
|
strtotime('2007-03-01'),
|
|
strtotime('2008-05-12') - 1,
|
|
['word4', 'word5'],
|
|
['word6', 'word7']
|
|
],
|
|
[
|
|
'word6 intitle:word2 inurl:word3 pubdate:2007-03-01/2008-05-11 #word4 author:word1 #word5 "word7 word8" date:2007-03-01/2008-05-11',
|
|
['word1'],
|
|
strtotime('2007-03-01'),
|
|
strtotime('2008-05-12') - 1,
|
|
['word2'],
|
|
['word3'],
|
|
strtotime('2007-03-01'),
|
|
strtotime('2008-05-12') - 1,
|
|
['word4', 'word5'],
|
|
['word7 word8', 'word6']
|
|
]
|
|
];
|
|
}
|
|
|
|
#[DataProvider('provideAddOrParentheses')]
|
|
public static function test__addOrParentheses(string $input, string $output): void {
|
|
self::assertSame($output, FreshRSS_BooleanSearch::addOrParentheses($input));
|
|
}
|
|
|
|
/** @return list<list{string,string}> */
|
|
public static function provideAddOrParentheses(): array {
|
|
return [
|
|
['ab', 'ab'],
|
|
['ab cd', 'ab cd'],
|
|
['!ab -cd', '!ab -cd'],
|
|
['ab OR cd', '(ab) OR (cd)'],
|
|
['!ab OR -cd', '(!ab) OR (-cd)'],
|
|
['ab cd OR ef OR "gh ij"', '(ab cd) OR (ef) OR ("gh ij")'],
|
|
['ab (!cd)', 'ab (!cd)'],
|
|
['"ab" (!"cd")', '"ab" (!"cd")'],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('provideconsistentOrParentheses')]
|
|
public static function test__consistentOrParentheses(string $input, string $output): void {
|
|
self::assertSame($output, FreshRSS_BooleanSearch::consistentOrParentheses($input));
|
|
}
|
|
|
|
/** @return list<list{string,string}> */
|
|
public static function provideconsistentOrParentheses(): array {
|
|
return [
|
|
['ab cd ef', 'ab cd ef'],
|
|
['(ab cd ef)', '(ab cd ef)'],
|
|
['("ab cd" ef)', '("ab cd" ef)'],
|
|
['"ab cd" (ef gh) "ij kl"', '"ab cd" (ef gh) "ij kl"'],
|
|
['ab (!cd)', 'ab (!cd)'],
|
|
['ab !(cd)', 'ab !(cd)'],
|
|
['(ab) -(cd)', '(ab) -(cd)'],
|
|
['ab cd OR ef OR "gh ij"', 'ab cd OR ef OR "gh ij"'],
|
|
['"plain or text" OR (cd)', '("plain or text") OR (cd)'],
|
|
['(ab) OR cd OR ef OR (gh)', '(ab) OR (cd) OR (ef) OR (gh)'],
|
|
['(ab (cd OR ef)) OR gh OR ij OR (kl)', '(ab (cd OR ef)) OR (gh) OR (ij) OR (kl)'],
|
|
['(ab (cd OR ef OR (gh))) OR ij', '(ab ((cd) OR (ef) OR (gh))) OR (ij)'],
|
|
['(ab (!cd OR ef OR (gh))) OR ij', '(ab ((!cd) OR (ef) OR (gh))) OR (ij)'],
|
|
['(ab !(cd OR ef OR !(gh))) OR ij', '(ab !((cd) OR (ef) OR !(gh))) OR (ij)'],
|
|
['"ab" OR (!"cd")', '("ab") OR (!"cd")'],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string> $values
|
|
*/
|
|
#[DataProvider('provideParentheses')]
|
|
public function test__parentheses(string $input, string $sql, array $values): void {
|
|
[$filterValues, $filterSearch] = FreshRSS_EntryDAOPGSQL::sqlBooleanSearch('e.', new FreshRSS_BooleanSearch($input));
|
|
self::assertSame(trim($sql), trim($filterSearch));
|
|
self::assertSame($values, $filterValues);
|
|
}
|
|
|
|
/** @return list<list<mixed>> */
|
|
public static function provideParentheses(): array {
|
|
return [
|
|
[
|
|
'f:1 (f:2 OR f:3 OR f:4) (f:5 OR (f:6 OR f:7))',
|
|
' ((e.id_feed IN (?))) AND ((e.id_feed IN (?)) OR (e.id_feed IN (?)) OR (e.id_feed IN (?))) AND' .
|
|
' (((e.id_feed IN (?))) OR ((e.id_feed IN (?)) OR (e.id_feed IN (?)))) ',
|
|
[1, 2, 3, 4, 5, 6, 7]
|
|
],
|
|
[
|
|
'c:1 OR c:2,3',
|
|
' (e.id_feed IN (SELECT f.id FROM `_feed` f WHERE f.category IN (?))) OR (e.id_feed IN (SELECT f.id FROM `_feed` f WHERE f.category IN (?,?))) ',
|
|
[1, 2, 3]
|
|
],
|
|
[
|
|
'#tag Hello OR (author:Alice inurl:example) OR (f:3 intitle:World) OR L:12',
|
|
" ((TRIM(e.tags) || ' #' LIKE ? AND (e.title LIKE ? OR e.content LIKE ?))) OR ((e.author LIKE ? AND e.link LIKE ?)) OR" .
|
|
' ((e.id_feed IN (?) AND e.title LIKE ?)) OR ((e.id IN (SELECT et.id_entry FROM `_entrytag` et WHERE et.id_tag IN (?)))) ',
|
|
['%tag #%', '%Hello%', '%Hello%', '%Alice%', '%example%', 3, '%World%', 12]
|
|
],
|
|
[
|
|
'#tag Hello (author:Alice inurl:example) (f:3 intitle:World) label:Bleu',
|
|
" ((TRIM(e.tags) || ' #' LIKE ? AND (e.title LIKE ? OR e.content LIKE ?))) AND" .
|
|
' ((e.author LIKE ? AND e.link LIKE ?)) AND' .
|
|
' ((e.id_feed IN (?) AND e.title LIKE ?)) AND' .
|
|
' ((e.id IN (SELECT et.id_entry FROM `_entrytag` et, `_tag` t WHERE et.id_tag = t.id AND t.name IN (?)))) ',
|
|
['%tag #%', '%Hello%', '%Hello%', '%Alice%', '%example%', 3, '%World%', 'Bleu']
|
|
],
|
|
[
|
|
'!((author:Alice intitle:hello) OR (author:Bob intitle:world))',
|
|
' NOT (((e.author LIKE ? AND e.title LIKE ?)) OR ((e.author LIKE ? AND e.title LIKE ?))) ',
|
|
['%Alice%', '%hello%', '%Bob%', '%world%'],
|
|
],
|
|
[
|
|
'(author:Alice intitle:hello) !(author:Bob intitle:world)',
|
|
' ((e.author LIKE ? AND e.title LIKE ?)) AND NOT ((e.author LIKE ? AND e.title LIKE ?)) ',
|
|
['%Alice%', '%hello%', '%Bob%', '%world%'],
|
|
],
|
|
[
|
|
'intitle:"(test)"',
|
|
'(e.title LIKE ?)',
|
|
['%(test)%'],
|
|
],
|
|
[
|
|
'intitle:\'"hello world"\'',
|
|
'(e.title LIKE ?)',
|
|
['%"hello world"%'],
|
|
],
|
|
[
|
|
'intext:\'"hello world"\'',
|
|
'(e.content LIKE ?)',
|
|
['%"hello world"%'],
|
|
],
|
|
[
|
|
'(ab) OR (cd) OR (ef)',
|
|
'(((e.title LIKE ? OR e.content LIKE ?))) OR (((e.title LIKE ? OR e.content LIKE ?))) OR (((e.title LIKE ? OR e.content LIKE ?)))',
|
|
['%ab%', '%ab%', '%cd%', '%cd%', '%ef%', '%ef%'],
|
|
],
|
|
[
|
|
'("plain or text") OR (cd)',
|
|
'(((e.title LIKE ? OR e.content LIKE ?))) OR (((e.title LIKE ? OR e.content LIKE ?)))',
|
|
['%plain or text%', '%plain or text%', '%cd%', '%cd%'],
|
|
],
|
|
[
|
|
'"plain or text" OR cd',
|
|
'((e.title LIKE ? OR e.content LIKE ?)) OR ((e.title LIKE ? OR e.content LIKE ?))',
|
|
['%plain or text%', '%plain or text%', '%cd%', '%cd%'],
|
|
],
|
|
[
|
|
'"plain OR text" OR cd',
|
|
'((e.title LIKE ? OR e.content LIKE ?)) OR ((e.title LIKE ? OR e.content LIKE ?)) ',
|
|
['%plain OR text%', '%plain OR text%', '%cd%', '%cd%'],
|
|
],
|
|
[
|
|
'ab OR cd OR (ef)',
|
|
'(((e.title LIKE ? OR e.content LIKE ?))) OR (((e.title LIKE ? OR e.content LIKE ?))) OR (((e.title LIKE ? OR e.content LIKE ?))) ',
|
|
['%ab%', '%ab%', '%cd%', '%cd%', '%ef%', '%ef%'],
|
|
],
|
|
[
|
|
'ab OR cd OR ef',
|
|
'((e.title LIKE ? OR e.content LIKE ?)) OR ((e.title LIKE ? OR e.content LIKE ?)) OR ((e.title LIKE ? OR e.content LIKE ?))',
|
|
['%ab%', '%ab%', '%cd%', '%cd%', '%ef%', '%ef%'],
|
|
],
|
|
[
|
|
'(ab) cd OR ef OR (gh)',
|
|
'(((e.title LIKE ? OR e.content LIKE ?))) AND (((e.title LIKE ? OR e.content LIKE ?))) ' .
|
|
'OR (((e.title LIKE ? OR e.content LIKE ?))) OR (((e.title LIKE ? OR e.content LIKE ?)))',
|
|
['%ab%', '%ab%', '%cd%', '%cd%', '%ef%', '%ef%', '%gh%', '%gh%'],
|
|
],
|
|
[
|
|
'(ab) OR cd OR ef OR (gh)',
|
|
'(((e.title LIKE ? OR e.content LIKE ?))) OR (((e.title LIKE ? OR e.content LIKE ?))) ' .
|
|
'OR (((e.title LIKE ? OR e.content LIKE ?))) OR (((e.title LIKE ? OR e.content LIKE ?)))',
|
|
['%ab%', '%ab%', '%cd%', '%cd%', '%ef%', '%ef%', '%gh%', '%gh%'],
|
|
],
|
|
[
|
|
'ab OR (!(cd OR ef))',
|
|
'(((e.title LIKE ? OR e.content LIKE ?))) OR (NOT (((e.title LIKE ? OR e.content LIKE ?)) OR ((e.title LIKE ? OR e.content LIKE ?))))',
|
|
['%ab%', '%ab%', '%cd%', '%cd%', '%ef%', '%ef%'],
|
|
],
|
|
[
|
|
'ab !(cd OR ef)',
|
|
'(((e.title LIKE ? OR e.content LIKE ?))) AND NOT (((e.title LIKE ? OR e.content LIKE ?)) OR ((e.title LIKE ? OR e.content LIKE ?)))',
|
|
['%ab%', '%ab%', '%cd%', '%cd%', '%ef%', '%ef%'],
|
|
],
|
|
[
|
|
'ab OR !(cd OR ef)',
|
|
'(((e.title LIKE ? OR e.content LIKE ?))) OR NOT (((e.title LIKE ? OR e.content LIKE ?)) OR ((e.title LIKE ? OR e.content LIKE ?)))',
|
|
['%ab%', '%ab%', '%cd%', '%cd%', '%ef%', '%ef%'],
|
|
],
|
|
[
|
|
'(ab (!cd OR ef OR (gh))) OR !(ij OR kl)',
|
|
'((((e.title LIKE ? OR e.content LIKE ?))) AND (((e.title NOT LIKE ? AND e.content NOT LIKE ?)) OR (((e.title LIKE ? OR e.content LIKE ?))) ' .
|
|
'OR (((e.title LIKE ? OR e.content LIKE ?))))) OR NOT (((e.title LIKE ? OR e.content LIKE ?)) OR ((e.title LIKE ? OR e.content LIKE ?)))',
|
|
['%ab%', '%ab%', '%cd%', '%cd%', '%ef%', '%ef%', '%gh%', '%gh%', '%ij%', '%ij%', '%kl%', '%kl%'],
|
|
],
|
|
[
|
|
'"ab" "cd" ("ef") intitle:"gh" !"ij" -"kl"',
|
|
'(((e.title LIKE ? OR e.content LIKE ?) AND (e.title LIKE ? OR e.content LIKE ?))) AND (((e.title LIKE ? OR e.content LIKE ?))) ' .
|
|
'AND ((e.title LIKE ? AND e.title NOT LIKE ? AND e.content NOT LIKE ? AND e.title NOT LIKE ? AND e.content NOT LIKE ?))',
|
|
['%ab%', '%ab%', '%cd%', '%cd%', '%ef%', '%ef%', '%gh%', '%ij%', '%ij%', '%kl%', '%kl%']
|
|
],
|
|
[
|
|
'intitle:"é & \' è" intext:/<&>/ \'< & " >\'',
|
|
'(e.title LIKE ? AND e.content ~ ? AND (e.title LIKE ? OR e.content LIKE ?))',
|
|
['%é & \' è%', '<&>', '%< & " >%', '%< & " >%']
|
|
],
|
|
[
|
|
'/^(ab|cd) [(] \\) (ef|gh)/',
|
|
'((e.title ~ ? OR e.content ~ ?))',
|
|
['^(ab|cd) [(] \\) (ef|gh)', '^(ab|cd) [(] \\) (ef|gh)']
|
|
],
|
|
[
|
|
'!/^(ab|cd)/',
|
|
'(NOT e.title ~ ? AND NOT e.content ~ ?)',
|
|
['^(ab|cd)', '^(ab|cd)']
|
|
],
|
|
[
|
|
'intitle:/^(ab|cd)/',
|
|
'(e.title ~ ?)',
|
|
['^(ab|cd)']
|
|
],
|
|
[
|
|
'intext:/^(ab|cd)/',
|
|
'(e.content ~ ?)',
|
|
['^(ab|cd)']
|
|
],
|
|
[
|
|
'L:1 L:2',
|
|
'(e.id IN (SELECT et.id_entry FROM `_entrytag` et WHERE et.id_tag IN (?)) AND ' .
|
|
'e.id IN (SELECT et.id_entry FROM `_entrytag` et WHERE et.id_tag IN (?)))',
|
|
[1, 2]
|
|
],
|
|
[
|
|
'L:1,2',
|
|
'(e.id IN (SELECT et.id_entry FROM `_entrytag` et WHERE et.id_tag IN (?,?)))',
|
|
[1, 2]
|
|
],
|
|
];
|
|
}
|
|
|
|
public function test__add_single_search_combines_conditions_with_and(): void {
|
|
$startTime = strtotime('2026-02-21T12:00:00Z');
|
|
$searches = new FreshRSS_BooleanSearch('');
|
|
|
|
$search = new FreshRSS_Search('');
|
|
$search->setMinDate($startTime);
|
|
$search->setMinModifiedDate($startTime);
|
|
$searches->add($search);
|
|
|
|
[$filterValues, $filterSearch] = FreshRSS_EntryDAOPGSQL::sqlBooleanSearch('e.', $searches);
|
|
|
|
$filterSearch = preg_replace('/\s+/', ' ', trim($filterSearch)) ?? '';
|
|
self::assertSame('(e.id >= ? AND e.`lastModified` >= ?)', $filterSearch);
|
|
self::assertSame([$startTime . '000000', $startTime], $filterValues);
|
|
}
|
|
|
|
public function test__add_multiple_searches_combines_conditions_with_or(): void {
|
|
$startTime = strtotime('2026-02-21T12:00:00Z');
|
|
$searches = new FreshRSS_BooleanSearch('');
|
|
|
|
$search = new FreshRSS_Search('');
|
|
$search->setMinDate($startTime);
|
|
$searches->add($search);
|
|
|
|
$search = new FreshRSS_Search('');
|
|
$search->setMinModifiedDate($startTime);
|
|
$searches->add($search);
|
|
|
|
[$filterValues, $filterSearch] = FreshRSS_EntryDAOPGSQL::sqlBooleanSearch('e.', $searches);
|
|
|
|
$filterSearch = preg_replace('/\s+/', ' ', trim($filterSearch)) ?? '';
|
|
self::assertSame('(e.id >= ?) OR (e.`lastModified` >= ?)', $filterSearch);
|
|
self::assertSame([$startTime . '000000', $startTime], $filterValues);
|
|
}
|
|
|
|
/**
|
|
* @param array<string> $values
|
|
*/
|
|
#[DataProvider('provideDateOperators')]
|
|
public function test__date_operators(string $input, string $sql, array $values): void {
|
|
[$filterValues, $filterSearch] = FreshRSS_EntryDAOPGSQL::sqlBooleanSearch('e.', new FreshRSS_BooleanSearch($input));
|
|
self::assertSame(trim($sql), trim($filterSearch));
|
|
self::assertSame($values, $filterValues);
|
|
}
|
|
|
|
/** @return list<list<mixed>> */
|
|
public static function provideDateOperators(): array {
|
|
return [
|
|
// Basic date operator tests
|
|
[
|
|
'date:2007-03-01/2008-05-11',
|
|
'(e.id >= ? AND e.id <= ?)',
|
|
[strtotime('2007-03-01T00:00:00Z') . '000000', strtotime('2008-05-11T23:59:59Z') . '000000'],
|
|
],
|
|
[
|
|
'date:2007-03-01/',
|
|
'(e.id >= ?)',
|
|
[strtotime('2007-03-01T00:00:00Z') . '000000'],
|
|
],
|
|
[
|
|
'date:/2008-05-11',
|
|
'(e.id <= ?)',
|
|
[strtotime('2008-05-11T23:59:59Z') . '000000'],
|
|
],
|
|
// Basic pubdate operator tests
|
|
[
|
|
'pubdate:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z',
|
|
'(e.date >= ? AND e.date <= ?)',
|
|
[strtotime('2007-03-01T13:00:00Z'), strtotime('2008-05-11T15:30:00Z')],
|
|
],
|
|
[
|
|
'pubdate:2007-03-01/',
|
|
'(e.date >= ?)',
|
|
[strtotime('2007-03-01T00:00:00Z')],
|
|
],
|
|
[
|
|
'pubdate:/2008-05-11',
|
|
'(e.date <= ?)',
|
|
[strtotime('2008-05-11T23:59:59Z')],
|
|
],
|
|
// Basic modified date operator tests
|
|
[
|
|
'mdate:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z',
|
|
'(e.`lastModified` >= ? AND COALESCE(e.`lastModified`, 0) <= ?)',
|
|
[strtotime('2007-03-01T13:00:00Z'), strtotime('2008-05-11T15:30:00Z')],
|
|
],
|
|
[
|
|
'mdate:2007-03-01/',
|
|
'(e.`lastModified` >= ?)',
|
|
[strtotime('2007-03-01T00:00:00Z')],
|
|
],
|
|
[
|
|
'mdate:/2008-05-11',
|
|
'(COALESCE(e.`lastModified`, 0) <= ?)',
|
|
[strtotime('2008-05-11T23:59:59Z')],
|
|
],
|
|
// Basic userdate operator tests
|
|
[
|
|
'userdate:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z',
|
|
'(e.`lastUserModified` >= ? AND e.`lastUserModified` <= ?)',
|
|
[strtotime('2007-03-01T13:00:00Z'), strtotime('2008-05-11T15:30:00Z')],
|
|
],
|
|
[
|
|
'userdate:2007-03-01/',
|
|
'(e.`lastUserModified` >= ?)',
|
|
[strtotime('2007-03-01T00:00:00Z')],
|
|
],
|
|
[
|
|
'userdate:/2008-05-11',
|
|
'(e.`lastUserModified` <= ?)',
|
|
[strtotime('2008-05-11T23:59:59Z')],
|
|
],
|
|
// Negative date operator tests
|
|
[
|
|
'-date:2007-03-01/2008-05-11',
|
|
'((e.id < ? OR e.id > ?))',
|
|
[strtotime('2007-03-01T00:00:00Z') . '000000', strtotime('2008-05-11T23:59:59Z') . '000000'],
|
|
],
|
|
[
|
|
'!pubdate:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z',
|
|
'((e.date < ? OR e.date > ?))',
|
|
[strtotime('2007-03-01T13:00:00Z'), strtotime('2008-05-11T15:30:00Z')],
|
|
],
|
|
[
|
|
'!mdate:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z',
|
|
'((COALESCE(e.`lastModified`, 0) < ? OR e.`lastModified` > ?))',
|
|
[strtotime('2007-03-01T13:00:00Z'), strtotime('2008-05-11T15:30:00Z')],
|
|
],
|
|
[
|
|
'!userdate:2007-03-01T13:00:00Z/2008-05-11T15:30:00Z',
|
|
'((COALESCE(e.`lastUserModified`, 0) < ? OR e.`lastUserModified` > ?))',
|
|
[strtotime('2007-03-01T13:00:00Z'), strtotime('2008-05-11T15:30:00Z')],
|
|
],
|
|
// Combined date operators
|
|
[
|
|
'date:2007-03-01/ pubdate:/2008-05-11',
|
|
'(e.id >= ? AND e.date <= ?)',
|
|
[strtotime('2007-03-01T00:00:00Z') . '000000', strtotime('2008-05-11T23:59:59Z')],
|
|
],
|
|
[
|
|
'pubdate:2007-03-01/ userdate:/2008-05-11',
|
|
'(e.date >= ? AND e.`lastUserModified` <= ?)',
|
|
[strtotime('2007-03-01T00:00:00Z'), strtotime('2008-05-11T23:59:59Z')],
|
|
],
|
|
[
|
|
'userdate:2007-03-01/ mdate:/2008-05-11',
|
|
'(COALESCE(e.`lastModified`, 0) <= ? AND e.`lastUserModified` >= ?)',
|
|
[strtotime('2008-05-11T23:59:59Z'), strtotime('2007-03-01T00:00:00Z')],
|
|
],
|
|
[
|
|
'date:2007-03-01/ userdate:2007-06-01/',
|
|
'(e.id >= ? AND e.`lastUserModified` >= ?)',
|
|
[strtotime('2007-03-01T00:00:00Z') . '000000', strtotime('2007-06-01T00:00:00Z')],
|
|
],
|
|
// Complex combinations with other operators
|
|
[
|
|
'intitle:test date:2007-03-01/ pubdate:/2008-05-11',
|
|
'(e.id >= ? AND e.date <= ? AND e.title LIKE ?)',
|
|
[strtotime('2007-03-01T00:00:00Z') . '000000', strtotime('2008-05-11T23:59:59Z'), '%test%'],
|
|
],
|
|
[
|
|
'author:john userdate:2007-03-01/2008-05-11',
|
|
'(e.`lastUserModified` >= ? AND e.`lastUserModified` <= ? AND e.author LIKE ?)',
|
|
[strtotime('2007-03-01T00:00:00Z'), strtotime('2008-05-11T23:59:59Z'), '%john%'],
|
|
],
|
|
// Mixed positive and negative date operators
|
|
[
|
|
'date:2007-03-01/ !pubdate:2008-01-01/2008-05-11',
|
|
'(e.id >= ? AND (e.date < ? OR e.date > ?))',
|
|
[strtotime('2007-03-01T00:00:00Z') . '000000', strtotime('2008-01-01T00:00:00Z'), strtotime('2008-05-11T23:59:59Z')],
|
|
],
|
|
];
|
|
}
|
|
|
|
/** @param array<string> $values */
|
|
#[DataProvider('provideRegexPostreSQL')]
|
|
public function test__regex_postgresql(string $input, string $sql, array $values): void {
|
|
[$filterValues, $filterSearch] = FreshRSS_EntryDAOPGSQL::sqlBooleanSearch('e.', new FreshRSS_BooleanSearch($input));
|
|
self::assertSame(trim($sql), trim($filterSearch));
|
|
self::assertSame($values, $filterValues);
|
|
}
|
|
|
|
/** @return list<list<mixed>> */
|
|
public static 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']
|
|
],
|
|
[
|
|
'intext:/^ab\\M/',
|
|
'(e.content ~ ?)',
|
|
['^ab\\M']
|
|
],
|
|
[
|
|
'intitle:/\\b\\d+/',
|
|
'(e.title ~ ?)',
|
|
['\\y\\d+']
|
|
],
|
|
[
|
|
'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$']
|
|
],
|
|
[
|
|
'#/^a(b|c)$/im',
|
|
"(REPLACE(REPLACE(e.tags, ' #', '#'), '#', '\n') ~* ?)",
|
|
['(?m)^a(b|c)$']
|
|
],
|
|
[ // 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/%']
|
|
],
|
|
[ // Not a regex
|
|
"author:'/u/Alice'",
|
|
"(e.author LIKE ?)",
|
|
['%/u/Alice%'],
|
|
],
|
|
[ // Not a regex
|
|
"inurl:'/shorts/'",
|
|
"(e.link LIKE ?)",
|
|
['%/shorts/%'],
|
|
],
|
|
[ // Not a regex
|
|
'inurl:"/shorts/" OR inurl:"/spam/"',
|
|
'(e.link LIKE ?) OR (e.link LIKE ?)',
|
|
['%/shorts/%', '%/spam/%'],
|
|
],
|
|
[ // Regex with literal 'or'
|
|
'intitle:/^A or B/i',
|
|
'(e.title ~* ?)',
|
|
['^A or B']
|
|
],
|
|
[ // Regex with literal 'OR'
|
|
'intitle:/^A B OR C D/i OR intitle:/^A B OR C D/i',
|
|
'(e.title ~* ?) OR (e.title ~* ?)',
|
|
['^A B OR C D', '^A B OR C D']
|
|
],
|
|
[ // Quote with literal 'OR'
|
|
'intitle:"A B OR C D" OR intitle:"E or F"',
|
|
'(e.title LIKE ?) OR (e.title LIKE ?)',
|
|
['%A B OR C D%', '%E or F%']
|
|
],
|
|
];
|
|
}
|
|
|
|
/** @param array<string> $values */
|
|
#[DataProvider('provideRegexMariaDB')]
|
|
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::assertSame(trim($sql), trim($filterSearch));
|
|
self::assertSame($values, $filterValues);
|
|
}
|
|
|
|
/** @return list<list<mixed>> */
|
|
public static 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$']
|
|
],
|
|
[
|
|
'intitle:/\\b\\d+/',
|
|
"(e.title REGEXP ?)",
|
|
['(?-i)\\b\\d+']
|
|
],
|
|
[
|
|
'intext:/^ab$/m',
|
|
'(UNCOMPRESS(e.content_bin) REGEXP ?)',
|
|
['(?-i)(?m)^ab$']
|
|
],
|
|
[
|
|
'-intext:/^ab$/m',
|
|
'(NOT UNCOMPRESS(e.content_bin) REGEXP ?)',
|
|
['(?-i)(?m)^ab$']
|
|
],
|
|
[
|
|
'-/^ab$/',
|
|
'(NOT e.title REGEXP ? AND NOT UNCOMPRESS(e.content_bin) REGEXP ?)',
|
|
['(?-i)^ab$', '(?-i)^ab$']
|
|
],
|
|
];
|
|
}
|
|
|
|
/** @param array<string> $values */
|
|
#[DataProvider('provideRegexMySQL')]
|
|
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::assertSame(trim($sql), trim($filterSearch));
|
|
self::assertSame($values, $filterValues);
|
|
}
|
|
|
|
/** @return list<list<mixed>> */
|
|
public static 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$']
|
|
],
|
|
[
|
|
'intext:/^ab$/m',
|
|
"(REGEXP_LIKE(UNCOMPRESS(e.content_bin),?,'mc'))",
|
|
['^ab$']
|
|
],
|
|
[
|
|
'-intext:/^ab$/m',
|
|
"(NOT REGEXP_LIKE(UNCOMPRESS(e.content_bin),?,'mc'))",
|
|
['^ab$']
|
|
],
|
|
[
|
|
'-/^ab$/',
|
|
"(NOT REGEXP_LIKE(e.title,?,'c') AND NOT REGEXP_LIKE(UNCOMPRESS(e.content_bin),?,'c'))",
|
|
['^ab$', '^ab$']
|
|
],
|
|
];
|
|
}
|
|
|
|
/** @param array<string> $values */
|
|
#[DataProvider('provideRegexSQLite')]
|
|
public function test__regex_sqlite(string $input, string $sql, array $values): void {
|
|
[$filterValues, $filterSearch] = FreshRSS_EntryDAOSQLite::sqlBooleanSearch('e.', new FreshRSS_BooleanSearch($input));
|
|
self::assertSame(trim($sql), trim($filterSearch));
|
|
self::assertSame($values, $filterValues);
|
|
}
|
|
|
|
/** @return list<list<mixed>> */
|
|
public static 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/']
|
|
],
|
|
[
|
|
'intext:/^ab\\b/',
|
|
'(e.content REGEXP ?)',
|
|
['/^ab\\b/']
|
|
],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('provideToString')]
|
|
public static function test__toString(string $input): void {
|
|
$search = new FreshRSS_Search($input);
|
|
$expected = str_replace("\n", ' ', $input);
|
|
self::assertSame($expected, $search->__toString());
|
|
}
|
|
|
|
/**
|
|
* @return array<array<string>>
|
|
*/
|
|
public static function provideToString(): array {
|
|
return [
|
|
[
|
|
<<<'EOD'
|
|
e:1,2 f:10,11 c:20,21 L:30,31 labels:"My label,My other label"
|
|
userdate:2025-01-01T00:00:00/2026-01-01T00:00:00
|
|
mdate:2025-12
|
|
pubdate:2025-02-01T00:00:00/2026-01-01T00:00:00
|
|
date:2025-03-01T00:00:00/2026-01-01T00:00:00
|
|
intitle:/<Inter&sting>/i intitle:"g ' & d\\:"
|
|
intext:/<Inter&sting>/i intext:g&d
|
|
author:/Bob/ author:"/u/Alice" author:Alice
|
|
inurl:/https/ inurl:"/shorts/" inurl:example.net
|
|
#/tag2/ #tag1
|
|
/search_regex/i "quoted search" search search:"A user search" search:U1
|
|
-e:3,4 -f:12,13 -c:22,23 -L:32,33 -labels:"Not label,Not other label"
|
|
-userdate:2025-06-01T00:00:00/2025-09-01T00:00:00
|
|
-mdate:2025-12-27
|
|
-pubdate:2025
|
|
-date:P30D
|
|
-intitle:/Spam/i -intitle:"'bad"
|
|
-intext:/Spam/i -intext:"'bad"
|
|
-author:/Dave/i -author:"/u/Charlie" -author:Charlie
|
|
-inurl:/ftp/ -inurl:"/spam/" -inurl:example.com
|
|
-#/tag4/ -#tag3
|
|
-/not_regex/i -"not quoted" -not_search -search:"Negative user search" -search:U2
|
|
EOD
|
|
],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('provideBooleanSearchToString')]
|
|
public static function testBooleanSearchToString(string $input, string $expected): void {
|
|
$search = new FreshRSS_BooleanSearch($input);
|
|
self::assertSame($expected, $search->toString());
|
|
}
|
|
|
|
/**
|
|
* @return array<array<string>>
|
|
*/
|
|
public static function provideBooleanSearchToString(): array {
|
|
return [
|
|
[
|
|
'((a OR b) (c OR d) -e) OR -(f g)',
|
|
'((a OR b) (c OR d) (-e)) OR -(f g)',
|
|
],
|
|
[
|
|
'((a OR b) ((c) OR ((d))) (-e)) OR -(((f g)))',
|
|
'((a OR b) (c OR d) (-e)) OR -(f g)',
|
|
],
|
|
[
|
|
'!((b c))',
|
|
'-(b c)',
|
|
],
|
|
[
|
|
'(a) OR !((b c))',
|
|
'a OR -(b c)',
|
|
],
|
|
[
|
|
'((a) (b))',
|
|
'a b',
|
|
],
|
|
[
|
|
'((a) OR (b))',
|
|
'a OR b',
|
|
],
|
|
[
|
|
' ( !( !( ( a ) ) ) ) ( ) ',
|
|
'-(-a)',
|
|
],
|
|
[
|
|
'-intitle:a -inurl:b',
|
|
'-intitle:a -inurl:b',
|
|
],
|
|
[
|
|
'intitle:/^A or B/i',
|
|
'intitle:/^A or B/i',
|
|
],
|
|
[
|
|
'intitle:/^A B OR C D/i',
|
|
'intitle:/^A B OR C D/i',
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param list<array{search:string,name?:string}> $queries
|
|
*/
|
|
#[DataProvider('provideBooleanSearchToStringExpansion')]
|
|
public static function testBooleanSearchToStringExpansion(array $queries, string $input,
|
|
string $expectedNotExpanded, string $expectedExpanded): void {
|
|
$previousUserConf = FreshRSS_Context::hasUserConf() ? FreshRSS_Context::userConf() : null;
|
|
$newUserConf = $previousUserConf instanceof FreshRSS_UserConfiguration ? clone $previousUserConf : clone FreshRSS_UserConfiguration::default();
|
|
$newUserConf->queries = $queries;
|
|
FreshRSS_Context::setUserConf($newUserConf);
|
|
|
|
try {
|
|
$booleanSearch = new FreshRSS_BooleanSearch($input);
|
|
self::assertSame($expectedNotExpanded, $booleanSearch->toString(expandUserQueries: false));
|
|
self::assertSame($expectedExpanded, $booleanSearch->toString());
|
|
} finally {
|
|
FreshRSS_Context::setUserConf($previousUserConf);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return array<string,array{0:list<array{search:string,name?:string}>,1:string,2:string,3:string}>
|
|
*/
|
|
public static function provideBooleanSearchToStringExpansion(): array {
|
|
return [
|
|
'Not found ID' => [
|
|
[
|
|
['search' => 'author:Alice'],
|
|
['search' => 'intitle:World'],
|
|
],
|
|
'S:3 S:4,5 ',
|
|
'S:3 S:4,5',
|
|
'',
|
|
],
|
|
'Not found name' => [
|
|
[
|
|
['search' => 'author:Alice', 'name' => 'First'],
|
|
['search' => 'intitle:World', 'name' => 'Second'],
|
|
],
|
|
'search:Third ',
|
|
'search:Third',
|
|
'',
|
|
],
|
|
'Found IDs' => [
|
|
[
|
|
['search' => 'author:Alice', 'name' => 'First'],
|
|
['search' => 'intitle:World', 'name' => 'Second'],
|
|
],
|
|
'S:0,1 ',
|
|
'S:0,1',
|
|
'author:Alice OR intitle:World',
|
|
],
|
|
'Found names' => [
|
|
[
|
|
['search' => 'author:Alice', 'name' => 'First'],
|
|
['search' => 'intitle:World', 'name' => 'Second'],
|
|
],
|
|
'search:First search:Second ',
|
|
'search:First search:Second',
|
|
'author:Alice intitle:World',
|
|
],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('provideHasSameOperators')]
|
|
public function testHasSameOperators(string $input1, string $input2, bool $expected): void {
|
|
$search1 = new FreshRSS_Search($input1);
|
|
$search2 = new FreshRSS_Search($input2);
|
|
self::assertSame($expected, $search1->hasSameOperators($search2));
|
|
}
|
|
|
|
/**
|
|
* @return array<array{string,string,bool}>
|
|
*/
|
|
public static function provideHasSameOperators(): array {
|
|
return [
|
|
['', '', true],
|
|
['intitle:a intext:b', 'intitle:c intext:d', true],
|
|
['intitle:a intext:b', 'intitle:c inurl:d', false],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('provideBooleanSearchEnforce')]
|
|
public function testBooleanSearchEnforce(string $initialInput, string $enforceInput, string $expectedOutput): void {
|
|
$booleanSearch = new FreshRSS_BooleanSearch($initialInput);
|
|
$searchToEnforce = new FreshRSS_Search($enforceInput);
|
|
$newBooleanSearch = $booleanSearch->enforce($searchToEnforce);
|
|
self::assertNotSame($booleanSearch, $newBooleanSearch);
|
|
self::assertSame($expectedOutput, $newBooleanSearch->toString());
|
|
}
|
|
|
|
/**
|
|
* @return array<array{string,string,string}>
|
|
*/
|
|
public static function provideBooleanSearchEnforce(): array {
|
|
return [
|
|
['', '', ''],
|
|
['', 'intitle:b', 'intitle:b'],
|
|
['intitle:a', '', 'intitle:a'],
|
|
['intitle:a', 'intitle:b', 'intitle:b'],
|
|
['a', 'intitle:b', 'intitle:b a'],
|
|
['intitle:a intext:a', 'intitle:b', 'intitle:b intext:a'],
|
|
['intitle:a inurl:a', 'intitle:b', 'intitle:b inurl:a'],
|
|
['intitle:a OR inurl:a', 'intitle:b', 'intitle:b (intitle:a OR inurl:a)'],
|
|
['intitle:a ((inurl:a) (intitle:c))', 'intitle:b', 'intitle:b (inurl:a intitle:c)'],
|
|
['intitle:a ((inurl:a) OR (intitle:c))', 'intitle:b', 'intitle:b (inurl:a OR intitle:c)'],
|
|
['(intitle:a) (inurl:a)', 'intitle:b', 'intitle:b inurl:a'],
|
|
['(inurl:a) (intitle:a)', 'intitle:b', 'inurl:a intitle:b'],
|
|
['(a b) OR (c d)', 'e', 'e ((a b) OR (c d))'],
|
|
['(a b) (c d)', 'e', 'e ((a b) (c d))'],
|
|
['(a b)', 'e', 'e (a b)'],
|
|
['date:2024/', 'date:/2025', 'date:/2025'],
|
|
['a', 'date:/2025', 'date:/2025 a'],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('provideBooleanSearchRemove')]
|
|
public function testBooleanSearchRemove(string $initialInput, string $removeInput, string $expectedOutput): void {
|
|
$booleanSearch = new FreshRSS_BooleanSearch($initialInput);
|
|
$searchToRemove = new FreshRSS_Search($removeInput);
|
|
$newBooleanSearch = $booleanSearch->remove($searchToRemove);
|
|
self::assertNotSame($booleanSearch, $newBooleanSearch);
|
|
self::assertSame($expectedOutput, $newBooleanSearch->toString());
|
|
}
|
|
|
|
/**
|
|
* @return array<array{string,string,string}>
|
|
*/
|
|
public static function provideBooleanSearchRemove(): array {
|
|
return [
|
|
['', 'intitle:b', ''],
|
|
['intitle:a', 'intitle:b', ''],
|
|
['intitle:a intext:a', 'intitle:b', 'intext:a'],
|
|
['intitle:a inurl:a', 'intitle:b', 'inurl:a'],
|
|
['intitle:a OR inurl:a', 'intitle:b', 'intitle:a OR inurl:a'],
|
|
['intitle:a ((inurl:a) (intitle:c))', 'intitle:b', '(inurl:a intitle:c)'],
|
|
['intitle:a ((inurl:a) OR (intitle:c))', 'intitle:b', '(inurl:a OR intitle:c)'],
|
|
['(intitle:a) (inurl:a)', 'intitle:b', 'inurl:a'],
|
|
['(inurl:a) (intitle:a)', 'intitle:b', 'inurl:a'],
|
|
['e ((a b) OR (c d))', 'e', '((a b) OR (c d))'],
|
|
['e ((a b) (c d))', 'e', '((a b) (c d))'],
|
|
['date:2024/', 'date:/2025', ''],
|
|
['date:2024/ a', 'date:/2025', 'a'],
|
|
];
|
|
}
|
|
|
|
#[DataProvider('provideNeedVisibility')]
|
|
public function testNeedVisibility(string $input, ?int $expected): void {
|
|
$search = new FreshRSS_Search($input);
|
|
self::assertSame($expected, $search->needVisibility());
|
|
}
|
|
|
|
/** @return list<list<string|int|null>> */
|
|
public static function provideNeedVisibility(): array {
|
|
return [
|
|
['', null],
|
|
['f:1', FreshRSS_Feed::PRIORITY_HIDDEN],
|
|
['c:2', FreshRSS_Feed::PRIORITY_CATEGORY],
|
|
['f:1 c:2', FreshRSS_Feed::PRIORITY_HIDDEN],
|
|
['-f:1', null],
|
|
['-c:2', null],
|
|
];
|
|
}
|
|
}
|