mirror of
https://github.com/FreshRSS/FreshRSS.git
synced 2025-12-23 21:47:44 -05:00
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:
committed by
GitHub
parent
35a7634e68
commit
1a552bd60e
@@ -21,7 +21,7 @@ If you have to create a new ticket, try to apply the following advice:
|
||||
- We also need some information:
|
||||
- Your FreshRSS version (on about page or `constants.php` file)
|
||||
- Your server configuration: type of hosting, PHP version
|
||||
- Your storage system (SQLite, MySQL, MariaDB, PostgreSQL)
|
||||
- Your storage system (SQLite, PostgreSQL, MariaDB, MySQL)
|
||||
- If possible, the related logs (PHP logs and FreshRSS logs under `data/users/your_user/log.txt`)
|
||||
|
||||
## Fix a bug
|
||||
|
||||
@@ -66,7 +66,7 @@ FreshRSS n’est fourni avec aucune garantie.
|
||||
* Extensions requises : [cURL](https://www.php.net/curl), [DOM](https://www.php.net/dom), [JSON](https://www.php.net/json), [XML](https://www.php.net/xml), [session](https://www.php.net/session), [ctype](https://www.php.net/ctype)
|
||||
* Extensions recommandées : [PDO_SQLite](https://www.php.net/pdo-sqlite) (pour l’export/import), [GMP](https://www.php.net/gmp) (pour accès API sur plateformes < 64 bits), [IDN](https://www.php.net/intl.idn) (pour les noms de domaines internationalisés), [mbstring](https://www.php.net/mbstring) (pour le texte Unicode), [iconv](https://www.php.net/iconv) (pour conversion d’encodages), [ZIP](https://www.php.net/zip) (pour import/export), [zlib](https://www.php.net/zlib) (pour les flux compressés)
|
||||
* Extension pour base de données : [PDO_PGSQL](https://www.php.net/pdo-pgsql) ou [PDO_SQLite](https://www.php.net/pdo-sqlite) ou [PDO_MySQL](https://www.php.net/pdo-mysql)
|
||||
* PostgreSQL 10+ ou SQLite ou MySQL 5.5.3+ ou MariaDB 5.5+
|
||||
* PostgreSQL 10+ ou SQLite ou MariaDB 10.0.5+ ou MySQL 8.0+
|
||||
|
||||
# [Installation](https://freshrss.github.io/FreshRSS/fr/users/01_Installation.html)
|
||||
|
||||
|
||||
@@ -61,12 +61,12 @@ FreshRSS comes with absolutely no warranty.
|
||||
* Works on mobile (except a few features)
|
||||
* Light server running Linux or Windows
|
||||
* It even works on Raspberry Pi 1 with response time under a second (tested with 150 feeds, 22k articles)
|
||||
* A web server: Apache2.4+ (recommended), nginx, lighttpd (not tested on others)
|
||||
* A Web server: Apache2.4+ (recommended), nginx, lighttpd (not tested on others)
|
||||
* PHP 8.1+
|
||||
* Required extensions: [cURL](https://www.php.net/curl), [DOM](https://www.php.net/dom), [JSON](https://www.php.net/json), [XML](https://www.php.net/xml), [session](https://www.php.net/session), [ctype](https://www.php.net/ctype)
|
||||
* Recommended extensions: [PDO_SQLite](https://www.php.net/pdo-sqlite) (for export/import), [GMP](https://www.php.net/gmp) (for API access on 32-bit platforms), [IDN](https://www.php.net/intl.idn) (for Internationalized Domain Names), [mbstring](https://www.php.net/mbstring) (for Unicode strings), [iconv](https://www.php.net/iconv) (for charset conversion), [ZIP](https://www.php.net/zip) (for import/export), [zlib](https://www.php.net/zlib) (for compressed feeds)
|
||||
* Extension for database: [PDO_PGSQL](https://www.php.net/pdo-pgsql) or [PDO_SQLite](https://www.php.net/pdo-sqlite) or [PDO_MySQL](https://www.php.net/pdo-mysql)
|
||||
* PostgreSQL 10+ or SQLite or MySQL 5.5.3+ or MariaDB 5.5+
|
||||
* PostgreSQL 10+ or SQLite or MariaDB 10.0.5+ or MySQL 8.0+
|
||||
|
||||
# [Installation](https://freshrss.github.io/FreshRSS/en/admins/03_Installation.html)
|
||||
|
||||
|
||||
@@ -185,6 +185,30 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
|
||||
return $list;
|
||||
}
|
||||
|
||||
private static ?string $staticVersion = null;
|
||||
/**
|
||||
* To override the database version. Useful for testing.
|
||||
*/
|
||||
public static function setStaticVersion(?string $version): void {
|
||||
self::$staticVersion = $version;
|
||||
}
|
||||
|
||||
public function version(): string {
|
||||
if (self::$staticVersion !== null) {
|
||||
return self::$staticVersion;
|
||||
}
|
||||
static $version = null;
|
||||
if ($version === null) {
|
||||
$version = $this->fetchValue('SELECT version()') ?? '';
|
||||
}
|
||||
return $version;
|
||||
}
|
||||
|
||||
final public function isMariaDB(): bool {
|
||||
// MariaDB includes its name in version, but not MySQL
|
||||
return str_contains($this->version(), 'MariaDB');
|
||||
}
|
||||
|
||||
public function size(bool $all = false): int {
|
||||
$db = FreshRSS_Context::systemConf()->db;
|
||||
|
||||
@@ -237,8 +261,7 @@ SQL;
|
||||
$isMariaDB = false;
|
||||
|
||||
if ($this->pdo->dbType() === 'mysql') {
|
||||
$dbVersion = $this->fetchValue('SELECT version()') ?? '';
|
||||
$isMariaDB = stripos($dbVersion, 'MariaDB') !== false; // MariaDB includes its name in version, but not MySQL
|
||||
$isMariaDB = $this->isMariaDB();
|
||||
if (!$isMariaDB) {
|
||||
// MySQL does not support `DROP INDEX IF EXISTS` yet https://dev.mysql.com/doc/refman/8.3/en/drop-index.html
|
||||
// but MariaDB does https://mariadb.com/kb/en/drop-index/
|
||||
|
||||
@@ -631,27 +631,60 @@ HTML;
|
||||
$ok &= stripos(implode(';', $this->authors), $author) !== false;
|
||||
}
|
||||
}
|
||||
if ($ok && $filter->getAuthorRegex()) {
|
||||
foreach ($filter->getAuthorRegex() as $author) {
|
||||
$ok &= preg_match($author, implode("\n", $this->authors)) === 1;
|
||||
}
|
||||
}
|
||||
if ($ok && $filter->getNotAuthor()) {
|
||||
foreach ($filter->getNotAuthor() as $author) {
|
||||
$ok &= stripos(implode(';', $this->authors), $author) === false;
|
||||
}
|
||||
}
|
||||
if ($ok && $filter->getNotAuthorRegex()) {
|
||||
foreach ($filter->getNotAuthorRegex() as $author) {
|
||||
$ok &= preg_match($author, implode("\n", $this->authors)) === 0;
|
||||
}
|
||||
}
|
||||
if ($ok && $filter->getIntitle()) {
|
||||
foreach ($filter->getIntitle() as $title) {
|
||||
$ok &= stripos($this->title, $title) !== false;
|
||||
}
|
||||
}
|
||||
if ($ok && $filter->getIntitleRegex()) {
|
||||
foreach ($filter->getIntitleRegex() as $title) {
|
||||
$ok &= preg_match($title, $this->title) === 1;
|
||||
}
|
||||
}
|
||||
if ($ok && $filter->getNotIntitle()) {
|
||||
foreach ($filter->getNotIntitle() as $title) {
|
||||
$ok &= stripos($this->title, $title) === false;
|
||||
}
|
||||
}
|
||||
if ($ok && $filter->getNotIntitleRegex()) {
|
||||
foreach ($filter->getNotIntitleRegex() as $title) {
|
||||
$ok &= preg_match($title, $this->title) === 0;
|
||||
}
|
||||
}
|
||||
if ($ok && $filter->getTags()) {
|
||||
foreach ($filter->getTags() as $tag2) {
|
||||
$found = false;
|
||||
foreach ($this->tags as $tag1) {
|
||||
if (strcasecmp($tag1, $tag2) === 0) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$ok &= $found;
|
||||
}
|
||||
}
|
||||
if ($ok && $filter->getTagsRegex()) {
|
||||
foreach ($filter->getTagsRegex() as $tag2) {
|
||||
$found = false;
|
||||
foreach ($this->tags as $tag1) {
|
||||
if (preg_match($tag2, $tag1) === 1) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$ok &= $found;
|
||||
@@ -663,6 +696,19 @@ HTML;
|
||||
foreach ($this->tags as $tag1) {
|
||||
if (strcasecmp($tag1, $tag2) === 0) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$ok &= !$found;
|
||||
}
|
||||
}
|
||||
if ($ok && $filter->getNotTagsRegex()) {
|
||||
foreach ($filter->getNotTagsRegex() as $tag2) {
|
||||
$found = false;
|
||||
foreach ($this->tags as $tag1) {
|
||||
if (preg_match($tag2, $tag1) === 1) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$ok &= !$found;
|
||||
@@ -673,11 +719,21 @@ HTML;
|
||||
$ok &= stripos($this->link, $url) !== false;
|
||||
}
|
||||
}
|
||||
if ($ok && $filter->getInurlRegex()) {
|
||||
foreach ($filter->getInurlRegex() as $url) {
|
||||
$ok &= preg_match($url, $this->link) === 1;
|
||||
}
|
||||
}
|
||||
if ($ok && $filter->getNotInurl()) {
|
||||
foreach ($filter->getNotInurl() as $url) {
|
||||
$ok &= stripos($this->link, $url) === false;
|
||||
}
|
||||
}
|
||||
if ($ok && $filter->getNotInurlRegex()) {
|
||||
foreach ($filter->getNotInurlRegex() as $url) {
|
||||
$ok &= preg_match($url, $this->link) === 0;
|
||||
}
|
||||
}
|
||||
if ($ok && $filter->getSearch()) {
|
||||
foreach ($filter->getSearch() as $needle) {
|
||||
$ok &= (stripos($this->title, $needle) !== false || stripos($this->content, $needle) !== false);
|
||||
@@ -688,6 +744,16 @@ HTML;
|
||||
$ok &= (stripos($this->title, $needle) === false && stripos($this->content, $needle) === false);
|
||||
}
|
||||
}
|
||||
if ($ok && $filter->getSearchRegex()) {
|
||||
foreach ($filter->getSearchRegex() as $needle) {
|
||||
$ok &= (preg_match($needle, $this->title) === 1 || preg_match($needle, $this->content) === 1);
|
||||
}
|
||||
}
|
||||
if ($ok && $filter->getNotSearchRegex()) {
|
||||
foreach ($filter->getNotSearchRegex() as $needle) {
|
||||
$ok &= (preg_match($needle, $this->title) === 0 && preg_match($needle, $this->content) === 0);
|
||||
}
|
||||
}
|
||||
if ($ok) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,55 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
|
||||
return str_replace('INSERT INTO ', 'INSERT IGNORE INTO ', $sql);
|
||||
}
|
||||
|
||||
/** @return array{pattern?:string,matchType?:string} */
|
||||
protected static function regexToSql(string $regex): array {
|
||||
if (preg_match('#^/(?P<pattern>.*)/(?P<matchType>[im]*)$#', $regex, $matches)) {
|
||||
return $matches;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @param array<int|string> $values */
|
||||
protected static function sqlRegex(string $expression, string $regex, array &$values): string {
|
||||
// The implementation of this function is solely for MySQL and MariaDB
|
||||
static $databaseDAOMySQL = null;
|
||||
if ($databaseDAOMySQL === null) {
|
||||
$databaseDAOMySQL = new FreshRSS_DatabaseDAO();
|
||||
}
|
||||
|
||||
$matches = static::regexToSql($regex);
|
||||
if (isset($matches['pattern'])) {
|
||||
$matchType = $matches['matchType'] ?? '';
|
||||
if ($databaseDAOMySQL->isMariaDB()) {
|
||||
if (str_contains($matchType, 'm')) {
|
||||
// multiline mode
|
||||
$matches['pattern'] = '(?m)' . $matches['pattern'];
|
||||
}
|
||||
if (str_contains($matchType, 'i')) {
|
||||
// case-insensitive match
|
||||
$matches['pattern'] = '(?i)' . $matches['pattern'];
|
||||
} else {
|
||||
$matches['pattern'] = '(?-i)' . $matches['pattern'];
|
||||
}
|
||||
$values[] = $matches['pattern'];
|
||||
return "{$expression} REGEXP ?";
|
||||
} else { // MySQL
|
||||
if (!str_contains($matchType, 'i')) {
|
||||
// Case-sensitive matching
|
||||
$matchType .= 'c';
|
||||
}
|
||||
$values[] = $matches['pattern'];
|
||||
return "REGEXP_LIKE({$expression},?,'{$matchType}')";
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/** Register any needed SQL function for the query, e.g. application-defined functions for SQLite */
|
||||
protected function registerSqlFunctions(string $sql): void {
|
||||
// Nothing to do for MySQL
|
||||
}
|
||||
|
||||
private function updateToMediumBlob(): bool {
|
||||
if ($this->pdo->dbType() !== 'mysql') {
|
||||
return false;
|
||||
@@ -910,24 +959,44 @@ SQL;
|
||||
$values[] = "%{$author}%";
|
||||
}
|
||||
}
|
||||
if ($filter->getAuthorRegex() !== null) {
|
||||
foreach ($filter->getAuthorRegex() as $author) {
|
||||
$sub_search .= 'AND ' . static::sqlRegex("REPLACE({$alias}author, ';', '\n')", $author, $values) . ' ';
|
||||
}
|
||||
}
|
||||
if ($filter->getIntitle() !== null) {
|
||||
foreach ($filter->getIntitle() as $title) {
|
||||
$sub_search .= 'AND ' . $alias . 'title LIKE ? ';
|
||||
$values[] = "%{$title}%";
|
||||
}
|
||||
}
|
||||
if ($filter->getIntitleRegex() !== null) {
|
||||
foreach ($filter->getIntitleRegex() as $title) {
|
||||
$sub_search .= 'AND ' . static::sqlRegex($alias . 'title', $title, $values) . ' ';
|
||||
}
|
||||
}
|
||||
if ($filter->getTags() !== null) {
|
||||
foreach ($filter->getTags() as $tag) {
|
||||
$sub_search .= 'AND ' . static::sqlConcat('TRIM(' . $alias . 'tags) ', " ' #'") . ' LIKE ? ';
|
||||
$values[] = "%{$tag} #%";
|
||||
}
|
||||
}
|
||||
if ($filter->getTagsRegex() !== null) {
|
||||
foreach ($filter->getTagsRegex() as $tag) {
|
||||
$sub_search .= 'AND ' . static::sqlRegex("REPLACE(REPLACE({$alias}tags, ' #', '#'), '#', '\n')", $tag, $values) . ' ';
|
||||
}
|
||||
}
|
||||
if ($filter->getInurl() !== null) {
|
||||
foreach ($filter->getInurl() as $url) {
|
||||
$sub_search .= 'AND ' . $alias . 'link LIKE ? ';
|
||||
$values[] = "%{$url}%";
|
||||
}
|
||||
}
|
||||
if ($filter->getInurlRegex() !== null) {
|
||||
foreach ($filter->getInurlRegex() as $url) {
|
||||
$sub_search .= 'AND ' . static::sqlRegex($alias . 'link', $url, $values) . ' ';
|
||||
}
|
||||
}
|
||||
|
||||
if ($filter->getNotAuthor() !== null) {
|
||||
foreach ($filter->getNotAuthor() as $author) {
|
||||
@@ -935,29 +1004,49 @@ SQL;
|
||||
$values[] = "%{$author}%";
|
||||
}
|
||||
}
|
||||
if ($filter->getNotAuthorRegex() !== null) {
|
||||
foreach ($filter->getNotAuthorRegex() as $author) {
|
||||
$sub_search .= 'AND NOT ' . static::sqlRegex("REPLACE({$alias}author, ';', '\n')", $author, $values) . ' ';
|
||||
}
|
||||
}
|
||||
if ($filter->getNotIntitle() !== null) {
|
||||
foreach ($filter->getNotIntitle() as $title) {
|
||||
$sub_search .= 'AND ' . $alias . 'title NOT LIKE ? ';
|
||||
$values[] = "%{$title}%";
|
||||
}
|
||||
}
|
||||
if ($filter->getNotIntitleRegex() !== null) {
|
||||
foreach ($filter->getNotIntitleRegex() as $title) {
|
||||
$sub_search .= 'AND NOT ' . static::sqlRegex($alias . 'title', $title, $values) . ' ';
|
||||
}
|
||||
}
|
||||
if ($filter->getNotTags() !== null) {
|
||||
foreach ($filter->getNotTags() as $tag) {
|
||||
$sub_search .= 'AND ' . static::sqlConcat('TRIM(' . $alias . 'tags) ', " ' #'") . ' NOT LIKE ? ';
|
||||
$values[] = "%{$tag} #%";
|
||||
}
|
||||
}
|
||||
if ($filter->getNotTagsRegex() !== null) {
|
||||
foreach ($filter->getNotTagsRegex() as $tag) {
|
||||
$sub_search .= 'AND NOT ' . static::sqlRegex("REPLACE(REPLACE({$alias}tags, ' #', '#'), '#', '\n')", $tag, $values) . ' ';
|
||||
}
|
||||
}
|
||||
if ($filter->getNotInurl() !== null) {
|
||||
foreach ($filter->getNotInurl() as $url) {
|
||||
$sub_search .= 'AND ' . $alias . 'link NOT LIKE ? ';
|
||||
$values[] = "%{$url}%";
|
||||
}
|
||||
}
|
||||
if ($filter->getNotInurlRegex() !== null) {
|
||||
foreach ($filter->getNotInurlRegex() as $url) {
|
||||
$sub_search .= 'AND NOT ' . static::sqlRegex($alias . 'link', $url, $values) . ' ';
|
||||
}
|
||||
}
|
||||
|
||||
if ($filter->getSearch() !== null) {
|
||||
foreach ($filter->getSearch() as $search_value) {
|
||||
if (static::isCompressed()) { // MySQL-only
|
||||
$sub_search .= 'AND CONCAT(' . $alias . 'title, UNCOMPRESS(' . $alias . 'content_bin)) LIKE ? ';
|
||||
$sub_search .= "AND CONCAT({$alias}title, '\\n', UNCOMPRESS({$alias}content_bin)) LIKE ? ";
|
||||
$values[] = "%{$search_value}%";
|
||||
} else {
|
||||
$sub_search .= 'AND (' . $alias . 'title LIKE ? OR ' . $alias . 'content LIKE ?) ';
|
||||
@@ -966,10 +1055,21 @@ SQL;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($filter->getSearchRegex() !== null) {
|
||||
foreach ($filter->getSearchRegex() as $search_value) {
|
||||
if (static::isCompressed()) { // MySQL-only
|
||||
$sub_search .= 'AND (' . static::sqlRegex($alias . 'title', $search_value, $values) .
|
||||
' OR ' . static::sqlRegex("UNCOMPRESS({$alias}content_bin)", $search_value, $values) . ') ';
|
||||
} else {
|
||||
$sub_search .= 'AND (' . static::sqlRegex($alias . 'title', $search_value, $values) .
|
||||
' OR ' . static::sqlRegex($alias . 'content', $search_value, $values) . ') ';
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($filter->getNotSearch() !== null) {
|
||||
foreach ($filter->getNotSearch() as $search_value) {
|
||||
if (static::isCompressed()) { // MySQL-only
|
||||
$sub_search .= 'AND CONCAT(' . $alias . 'title, UNCOMPRESS(' . $alias . 'content_bin)) NOT LIKE ? ';
|
||||
$sub_search .= "AND CONCAT({$alias}title, '\\n', UNCOMPRESS({$alias}content_bin)) NOT LIKE ? ";
|
||||
$values[] = "%{$search_value}%";
|
||||
} else {
|
||||
$sub_search .= 'AND ' . $alias . 'title NOT LIKE ? AND ' . $alias . 'content NOT LIKE ? ';
|
||||
@@ -978,6 +1078,17 @@ SQL;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($filter->getNotSearchRegex() !== null) {
|
||||
foreach ($filter->getNotSearchRegex() as $search_value) {
|
||||
if (static::isCompressed()) { // MySQL-only
|
||||
$sub_search .= 'AND NOT ' . static::sqlRegex($alias . 'title', $search_value, $values) .
|
||||
' ANT NOT ' . static::sqlRegex("UNCOMPRESS({$alias}content_bin)", $search_value, $values) . ' ';
|
||||
} else {
|
||||
$sub_search .= 'AND NOT ' . static::sqlRegex($alias . 'title', $search_value, $values) .
|
||||
' AND NOT ' . static::sqlRegex($alias . 'content', $search_value, $values) . ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($sub_search != '') {
|
||||
if ($isOpen) {
|
||||
@@ -1039,6 +1150,7 @@ SQL;
|
||||
if ($filterSearch !== '') {
|
||||
$search .= 'AND (' . $filterSearch . ') ';
|
||||
$values = array_merge($values, $filterValues);
|
||||
$this->registerSqlFunctions($search);
|
||||
}
|
||||
}
|
||||
return [$values, $search];
|
||||
|
||||
@@ -23,6 +23,32 @@ class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite {
|
||||
return rtrim($sql, ' ;') . ' ON CONFLICT DO NOTHING';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected static function sqlRegex(string $expression, string $regex, array &$values): string {
|
||||
$matches = static::regexToSql($regex);
|
||||
if (isset($matches['pattern'])) {
|
||||
$matchType = $matches['matchType'] ?? '';
|
||||
if (str_contains($matchType, 'm')) {
|
||||
// newline-sensitive matching
|
||||
$matches['pattern'] = '(?m)' . $matches['pattern'];
|
||||
}
|
||||
$values[] = $matches['pattern'];
|
||||
if (str_contains($matchType, 'i')) {
|
||||
// case-insensitive matching
|
||||
return "{$expression} ~* ?";
|
||||
} else {
|
||||
// case-sensitive matching
|
||||
return "{$expression} ~ ?";
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function registerSqlFunctions(string $sql): void {
|
||||
// Nothing to do for PostgreSQL
|
||||
}
|
||||
|
||||
/** @param array<string|int> $errorInfo */
|
||||
#[\Override]
|
||||
protected function autoUpdateDb(array $errorInfo): bool {
|
||||
|
||||
@@ -28,6 +28,27 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
|
||||
return str_replace('INSERT INTO ', 'INSERT OR IGNORE INTO ', $sql);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected static function sqlRegex(string $expression, string $regex, array &$values): string {
|
||||
$values[] = $regex;
|
||||
return "{$expression} REGEXP ?";
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function registerSqlFunctions(string $sql): void {
|
||||
if (!str_contains($sql, ' REGEXP ')) {
|
||||
return;
|
||||
}
|
||||
// https://php.net/pdo.sqlitecreatefunction
|
||||
// https://www.sqlite.org/lang_expr.html#the_like_glob_regexp_match_and_extract_operators
|
||||
$this->pdo->sqliteCreateFunction('regexp',
|
||||
function (string $pattern, string $text): bool {
|
||||
return preg_match($pattern, $text) === 1;
|
||||
},
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
/** @param array<string|int> $errorInfo */
|
||||
#[\Override]
|
||||
protected function autoUpdateDb(array $errorInfo): bool {
|
||||
|
||||
@@ -27,6 +27,8 @@ class FreshRSS_Search {
|
||||
private ?array $label_names = null;
|
||||
/** @var array<string>|null */
|
||||
private ?array $intitle = null;
|
||||
/** @var array<string>|null */
|
||||
private ?array $intitle_regex = null;
|
||||
/** @var int|false|null */
|
||||
private $min_date = null;
|
||||
/** @var int|false|null */
|
||||
@@ -38,11 +40,19 @@ class FreshRSS_Search {
|
||||
/** @var array<string>|null */
|
||||
private ?array $inurl = null;
|
||||
/** @var array<string>|null */
|
||||
private ?array $inurl_regex = null;
|
||||
/** @var array<string>|null */
|
||||
private ?array $author = null;
|
||||
/** @var array<string>|null */
|
||||
private ?array $author_regex = null;
|
||||
/** @var array<string>|null */
|
||||
private ?array $tags = null;
|
||||
/** @var array<string>|null */
|
||||
private ?array $tags_regex = null;
|
||||
/** @var array<string>|null */
|
||||
private ?array $search = null;
|
||||
/** @var array<string>|null */
|
||||
private ?array $search_regex = null;
|
||||
|
||||
/** @var array<string>|null */
|
||||
private ?array $not_entry_ids = null;
|
||||
@@ -54,6 +64,8 @@ class FreshRSS_Search {
|
||||
private ?array $not_label_names = null;
|
||||
/** @var array<string>|null */
|
||||
private ?array $not_intitle = null;
|
||||
/** @var array<string>|null */
|
||||
private ?array $not_intitle_regex = null;
|
||||
/** @var int|false|null */
|
||||
private $not_min_date = null;
|
||||
/** @var int|false|null */
|
||||
@@ -65,11 +77,19 @@ class FreshRSS_Search {
|
||||
/** @var array<string>|null */
|
||||
private ?array $not_inurl = null;
|
||||
/** @var array<string>|null */
|
||||
private ?array $not_inurl_regex = null;
|
||||
/** @var array<string>|null */
|
||||
private ?array $not_author = null;
|
||||
/** @var array<string>|null */
|
||||
private ?array $not_author_regex = null;
|
||||
/** @var array<string>|null */
|
||||
private ?array $not_tags = null;
|
||||
/** @var array<string>|null */
|
||||
private ?array $not_tags_regex = null;
|
||||
/** @var array<string>|null */
|
||||
private ?array $not_search = null;
|
||||
/** @var array<string>|null */
|
||||
private ?array $not_search_regex = null;
|
||||
|
||||
public function __construct(string $input) {
|
||||
$input = self::cleanSearch($input);
|
||||
@@ -156,9 +176,17 @@ class FreshRSS_Search {
|
||||
return $this->intitle;
|
||||
}
|
||||
/** @return array<string>|null */
|
||||
public function getIntitleRegex(): ?array {
|
||||
return $this->intitle_regex;
|
||||
}
|
||||
/** @return array<string>|null */
|
||||
public function getNotIntitle(): ?array {
|
||||
return $this->not_intitle;
|
||||
}
|
||||
/** @return array<string>|null */
|
||||
public function getNotIntitleRegex(): ?array {
|
||||
return $this->not_intitle_regex;
|
||||
}
|
||||
|
||||
public function getMinDate(): ?int {
|
||||
return $this->min_date ?: null;
|
||||
@@ -199,36 +227,68 @@ class FreshRSS_Search {
|
||||
return $this->inurl;
|
||||
}
|
||||
/** @return array<string>|null */
|
||||
public function getInurlRegex(): ?array {
|
||||
return $this->inurl_regex;
|
||||
}
|
||||
/** @return array<string>|null */
|
||||
public function getNotInurl(): ?array {
|
||||
return $this->not_inurl;
|
||||
}
|
||||
/** @return array<string>|null */
|
||||
public function getNotInurlRegex(): ?array {
|
||||
return $this->not_inurl_regex;
|
||||
}
|
||||
|
||||
/** @return array<string>|null */
|
||||
public function getAuthor(): ?array {
|
||||
return $this->author;
|
||||
}
|
||||
/** @return array<string>|null */
|
||||
public function getAuthorRegex(): ?array {
|
||||
return $this->author_regex;
|
||||
}
|
||||
/** @return array<string>|null */
|
||||
public function getNotAuthor(): ?array {
|
||||
return $this->not_author;
|
||||
}
|
||||
/** @return array<string>|null */
|
||||
public function getNotAuthorRegex(): ?array {
|
||||
return $this->not_author_regex;
|
||||
}
|
||||
|
||||
/** @return array<string>|null */
|
||||
public function getTags(): ?array {
|
||||
return $this->tags;
|
||||
}
|
||||
/** @return array<string>|null */
|
||||
public function getTagsRegex(): ?array {
|
||||
return $this->tags_regex;
|
||||
}
|
||||
/** @return array<string>|null */
|
||||
public function getNotTags(): ?array {
|
||||
return $this->not_tags;
|
||||
}
|
||||
/** @return array<string>|null */
|
||||
public function getNotTagsRegex(): ?array {
|
||||
return $this->not_tags_regex;
|
||||
}
|
||||
|
||||
/** @return array<string>|null */
|
||||
public function getSearch(): ?array {
|
||||
return $this->search;
|
||||
}
|
||||
/** @return array<string>|null */
|
||||
public function getSearchRegex(): ?array {
|
||||
return $this->search_regex;
|
||||
}
|
||||
/** @return array<string>|null */
|
||||
public function getNotSearch(): ?array {
|
||||
return $this->not_search;
|
||||
}
|
||||
/** @return array<string>|null */
|
||||
public function getNotSearchRegex(): ?array {
|
||||
return $this->not_search_regex;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string>|null $anArray
|
||||
@@ -253,11 +313,19 @@ class FreshRSS_Search {
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $strings
|
||||
* @return array<string>
|
||||
*/
|
||||
private static function htmlspecialchars_decodes(array $strings): array {
|
||||
return array_map(static fn(string $s) => htmlspecialchars_decode($s, ENT_QUOTES), $strings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the search string to find entry (article) IDs.
|
||||
*/
|
||||
private function parseEntryIds(string $input): string {
|
||||
if (preg_match_all('/\be:(?P<search>[0-9,]*)/', $input, $matches)) {
|
||||
if (preg_match_all('/\\be:(?P<search>[0-9,]*)/', $input, $matches)) {
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
$ids_lists = $matches['search'];
|
||||
$this->entry_ids = [];
|
||||
@@ -273,7 +341,7 @@ class FreshRSS_Search {
|
||||
}
|
||||
|
||||
private function parseNotEntryIds(string $input): string {
|
||||
if (preg_match_all('/(?<=\s|^)[!-]e:(?P<search>[0-9,]*)/', $input, $matches)) {
|
||||
if (preg_match_all('/(?<=\\s|^)[!-]e:(?P<search>[0-9,]*)/', $input, $matches)) {
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
$ids_lists = $matches['search'];
|
||||
$this->not_entry_ids = [];
|
||||
@@ -289,7 +357,7 @@ class FreshRSS_Search {
|
||||
}
|
||||
|
||||
private function parseFeedIds(string $input): string {
|
||||
if (preg_match_all('/\bf:(?P<search>[0-9,]*)/', $input, $matches)) {
|
||||
if (preg_match_all('/\\bf:(?P<search>[0-9,]*)/', $input, $matches)) {
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
$ids_lists = $matches['search'];
|
||||
$this->feed_ids = [];
|
||||
@@ -307,7 +375,7 @@ class FreshRSS_Search {
|
||||
}
|
||||
|
||||
private function parseNotFeedIds(string $input): string {
|
||||
if (preg_match_all('/(?<=\s|^)[!-]f:(?P<search>[0-9,]*)/', $input, $matches)) {
|
||||
if (preg_match_all('/(?<=\\s|^)[!-]f:(?P<search>[0-9,]*)/', $input, $matches)) {
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
$ids_lists = $matches['search'];
|
||||
$this->not_feed_ids = [];
|
||||
@@ -328,7 +396,7 @@ class FreshRSS_Search {
|
||||
* Parse the search string to find tags (labels) IDs.
|
||||
*/
|
||||
private function parseLabelIds(string $input): string {
|
||||
if (preg_match_all('/\b[lL]:(?P<search>[0-9,]+|[*])/', $input, $matches)) {
|
||||
if (preg_match_all('/\\b[lL]:(?P<search>[0-9,]+|[*])/', $input, $matches)) {
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
$ids_lists = $matches['search'];
|
||||
$this->label_ids = [];
|
||||
@@ -350,7 +418,7 @@ class FreshRSS_Search {
|
||||
}
|
||||
|
||||
private function parseNotLabelIds(string $input): string {
|
||||
if (preg_match_all('/(?<=\s|^)[!-][lL]:(?P<search>[0-9,]+|[*])/', $input, $matches)) {
|
||||
if (preg_match_all('/(?<=\\s|^)[!-][lL]:(?P<search>[0-9,]+|[*])/', $input, $matches)) {
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
$ids_lists = $matches['search'];
|
||||
$this->not_label_ids = [];
|
||||
@@ -376,11 +444,11 @@ class FreshRSS_Search {
|
||||
*/
|
||||
private function parseLabelNames(string $input): string {
|
||||
$names_lists = [];
|
||||
if (preg_match_all('/\blabels?:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
|
||||
if (preg_match_all('/\\blabels?:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
|
||||
$names_lists = $matches['search'];
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
if (preg_match_all('/\blabels?:(?P<search>[^\s"]*)/', $input, $matches)) {
|
||||
if (preg_match_all('/\\blabels?:(?P<search>[^\s"]*)/', $input, $matches)) {
|
||||
$names_lists = array_merge($names_lists, $matches['search']);
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
@@ -402,11 +470,11 @@ class FreshRSS_Search {
|
||||
*/
|
||||
private function parseNotLabelNames(string $input): string {
|
||||
$names_lists = [];
|
||||
if (preg_match_all('/(?<=\s|^)[!-]labels?:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
|
||||
if (preg_match_all('/(?<=\\s|^)[!-]labels?:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
|
||||
$names_lists = $matches['search'];
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
if (preg_match_all('/(?<=\s|^)[!-]labels?:(?P<search>[^\s"]*)/', $input, $matches)) {
|
||||
if (preg_match_all('/(?<=\\s|^)[!-]labels?:(?P<search>[^\\s"]*)/', $input, $matches)) {
|
||||
$names_lists = array_merge($names_lists, $matches['search']);
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
@@ -428,11 +496,15 @@ class FreshRSS_Search {
|
||||
* The search is the first word following the keyword.
|
||||
*/
|
||||
private function parseIntitleSearch(string $input): string {
|
||||
if (preg_match_all('/\bintitle:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
|
||||
if (preg_match_all('#\\bintitle:(?P<search>/.*?(?<!\\\\)/[im]*)#', $input, $matches)) {
|
||||
$this->intitle_regex = self::htmlspecialchars_decodes($matches['search']);
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
if (preg_match_all('/\\bintitle:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
|
||||
$this->intitle = $matches['search'];
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
if (preg_match_all('/\bintitle:(?P<search>[^\s"]*)/', $input, $matches)) {
|
||||
if (preg_match_all('/\\bintitle:(?P<search>[^\s"]*)/', $input, $matches)) {
|
||||
$this->intitle = array_merge($this->intitle ?: [], $matches['search']);
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
@@ -444,11 +516,15 @@ class FreshRSS_Search {
|
||||
}
|
||||
|
||||
private function parseNotIntitleSearch(string $input): string {
|
||||
if (preg_match_all('/(?<=\s|^)[!-]intitle:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
|
||||
if (preg_match_all('#(?<=\\s|^)[!-]intitle:(?P<search>/.*?(?<!\\\\)/[im]*)#', $input, $matches)) {
|
||||
$this->not_intitle_regex = self::htmlspecialchars_decodes($matches['search']);
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
if (preg_match_all('/(?<=\\s|^)[!-]intitle:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
|
||||
$this->not_intitle = $matches['search'];
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
if (preg_match_all('/(?<=\s|^)[!-]intitle:(?P<search>[^\s"]*)/', $input, $matches)) {
|
||||
if (preg_match_all('/(?<=\\s|^)[!-]intitle:(?P<search>[^\s"]*)/', $input, $matches)) {
|
||||
$this->not_intitle = array_merge($this->not_intitle ?: [], $matches['search']);
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
@@ -465,11 +541,15 @@ class FreshRSS_Search {
|
||||
* a delimiter. Supported delimiters are single quote (') and double quotes (").
|
||||
*/
|
||||
private function parseAuthorSearch(string $input): string {
|
||||
if (preg_match_all('/\bauthor:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
|
||||
if (preg_match_all('#\\bauthor:(?P<search>/.*?(?<!\\\\)/[im]*)#', $input, $matches)) {
|
||||
$this->author_regex = self::htmlspecialchars_decodes($matches['search']);
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
if (preg_match_all('/\\bauthor:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
|
||||
$this->author = $matches['search'];
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
if (preg_match_all('/\bauthor:(?P<search>[^\s"]*)/', $input, $matches)) {
|
||||
if (preg_match_all('/\\bauthor:(?P<search>[^\s"]*)/', $input, $matches)) {
|
||||
$this->author = array_merge($this->author ?: [], $matches['search']);
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
@@ -481,11 +561,15 @@ class FreshRSS_Search {
|
||||
}
|
||||
|
||||
private function parseNotAuthorSearch(string $input): string {
|
||||
if (preg_match_all('/(?<=\s|^)[!-]author:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
|
||||
if (preg_match_all('#(?<=\\s|^)[!-]author:(?P<search>/.*?(?<!\\\\)/[im]*)#', $input, $matches)) {
|
||||
$this->not_author_regex = self::htmlspecialchars_decodes($matches['search']);
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
if (preg_match_all('/(?<=\\s|^)[!-]author:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
|
||||
$this->not_author = $matches['search'];
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
if (preg_match_all('/(?<=\s|^)[!-]author:(?P<search>[^\s"]*)/', $input, $matches)) {
|
||||
if (preg_match_all('/(?<=\\s|^)[!-]author:(?P<search>[^\s"]*)/', $input, $matches)) {
|
||||
$this->not_author = array_merge($this->not_author ?: [], $matches['search']);
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
@@ -501,19 +585,41 @@ class FreshRSS_Search {
|
||||
* The search is the first word following the keyword.
|
||||
*/
|
||||
private function parseInurlSearch(string $input): string {
|
||||
if (preg_match_all('/\binurl:(?P<search>[^\s]*)/', $input, $matches)) {
|
||||
if (preg_match_all('#\\binurl:(?P<search>/.*?(?<!\\\\)/[im]*)#', $input, $matches)) {
|
||||
$this->inurl_regex = self::htmlspecialchars_decodes($matches['search']);
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
if (preg_match_all('/\\binurl:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
|
||||
$this->inurl = $matches['search'];
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
$this->inurl = self::removeEmptyValues($this->inurl);
|
||||
}
|
||||
if (preg_match_all('/\\binurl:(?P<search>[^\\s]*)/', $input, $matches)) {
|
||||
$this->inurl = $matches['search'];
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
$this->inurl = self::removeEmptyValues($this->inurl);
|
||||
if (empty($this->inurl)) {
|
||||
$this->inurl = null;
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
private function parseNotInurlSearch(string $input): string {
|
||||
if (preg_match_all('/(?<=\s|^)[!-]inurl:(?P<search>[^\s]*)/', $input, $matches)) {
|
||||
if (preg_match_all('#(?<=\\s|^)[!-]inurl:(?P<search>/.*?(?<!\\\\)/[im]*)#', $input, $matches)) {
|
||||
$this->not_inurl_regex = self::htmlspecialchars_decodes($matches['search']);
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
if (preg_match_all('/(?<=\\s|^)[!-]inurl:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
|
||||
$this->not_inurl = $matches['search'];
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
$this->not_inurl = self::removeEmptyValues($this->not_inurl);
|
||||
}
|
||||
if (preg_match_all('/(?<=\\s|^)[!-]inurl:(?P<search>[^\\s]*)/', $input, $matches)) {
|
||||
$this->not_inurl = $matches['search'];
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
$this->not_inurl = self::removeEmptyValues($this->not_inurl);
|
||||
if (empty($this->not_inurl)) {
|
||||
$this->not_inurl = null;
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
@@ -523,7 +629,7 @@ class FreshRSS_Search {
|
||||
* The search is the first word following the keyword.
|
||||
*/
|
||||
private function parseDateSearch(string $input): string {
|
||||
if (preg_match_all('/\bdate:(?P<search>[^\s]*)/', $input, $matches)) {
|
||||
if (preg_match_all('/\\bdate:(?P<search>[^\\s]*)/', $input, $matches)) {
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
$dates = self::removeEmptyValues($matches['search']);
|
||||
if (!empty($dates[0])) {
|
||||
@@ -534,7 +640,7 @@ class FreshRSS_Search {
|
||||
}
|
||||
|
||||
private function parseNotDateSearch(string $input): string {
|
||||
if (preg_match_all('/(?<=\s|^)[!-]date:(?P<search>[^\s]*)/', $input, $matches)) {
|
||||
if (preg_match_all('/(?<=\\s|^)[!-]date:(?P<search>[^\\s]*)/', $input, $matches)) {
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
$dates = self::removeEmptyValues($matches['search']);
|
||||
if (!empty($dates[0])) {
|
||||
@@ -550,7 +656,7 @@ class FreshRSS_Search {
|
||||
* The search is the first word following the keyword.
|
||||
*/
|
||||
private function parsePubdateSearch(string $input): string {
|
||||
if (preg_match_all('/\bpubdate:(?P<search>[^\s]*)/', $input, $matches)) {
|
||||
if (preg_match_all('/\\bpubdate:(?P<search>[^\\s]*)/', $input, $matches)) {
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
$dates = self::removeEmptyValues($matches['search']);
|
||||
if (!empty($dates[0])) {
|
||||
@@ -561,7 +667,7 @@ class FreshRSS_Search {
|
||||
}
|
||||
|
||||
private function parseNotPubdateSearch(string $input): string {
|
||||
if (preg_match_all('/(?<=\s|^)[!-]pubdate:(?P<search>[^\s]*)/', $input, $matches)) {
|
||||
if (preg_match_all('/(?<=\\s|^)[!-]pubdate:(?P<search>[^\\s]*)/', $input, $matches)) {
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
$dates = self::removeEmptyValues($matches['search']);
|
||||
if (!empty($dates[0])) {
|
||||
@@ -577,20 +683,44 @@ class FreshRSS_Search {
|
||||
* The search is the first word following the #.
|
||||
*/
|
||||
private function parseTagsSearch(string $input): string {
|
||||
if (preg_match_all('/#(?P<search>[^\s]+)/', $input, $matches)) {
|
||||
if (preg_match_all('%#(?P<search>/.*?(?<!\\\\)/[im]*)%', $input, $matches)) {
|
||||
$this->tags_regex = self::htmlspecialchars_decodes($matches['search']);
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
if (preg_match_all('/#(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
|
||||
$this->tags = $matches['search'];
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
$this->tags = self::removeEmptyValues($this->tags);
|
||||
}
|
||||
if (preg_match_all('/#(?P<search>[^\\s]+)/', $input, $matches)) {
|
||||
$this->tags = $matches['search'];
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
$this->tags = self::removeEmptyValues($this->tags);
|
||||
if (empty($this->tags)) {
|
||||
$this->tags = null;
|
||||
} else {
|
||||
$this->tags = self::decodeSpaces($this->tags);
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
private function parseNotTagsSearch(string $input): string {
|
||||
if (preg_match_all('/(?<=\s|^)[!-]#(?P<search>[^\s]+)/', $input, $matches)) {
|
||||
if (preg_match_all('%(?<=\\s|^)[!-]#(?P<search>/.*?(?<!\\\\)/[im]*)%', $input, $matches)) {
|
||||
$this->not_tags_regex = self::htmlspecialchars_decodes($matches['search']);
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
if (preg_match_all('/(?<=\\s|^)[!-]#(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
|
||||
$this->not_tags = $matches['search'];
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
$this->not_tags = self::removeEmptyValues($this->not_tags);
|
||||
}
|
||||
if (preg_match_all('/(?<=\\s|^)[!-]#(?P<search>[^\\s]+)/', $input, $matches)) {
|
||||
$this->not_tags = $matches['search'];
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
$this->not_tags = self::removeEmptyValues($this->not_tags);
|
||||
if (empty($this->not_tags)) {
|
||||
$this->not_tags = null;
|
||||
} else {
|
||||
$this->not_tags = self::decodeSpaces($this->not_tags);
|
||||
}
|
||||
return $input;
|
||||
@@ -599,13 +729,18 @@ class FreshRSS_Search {
|
||||
/**
|
||||
* Parse the search string to find search values.
|
||||
* Every word is a distinct search value using a delimiter.
|
||||
* Supported delimiters are single quote (') and double quotes (").
|
||||
* Supported delimiters are single quote (') and double quotes (") and regex (/).
|
||||
*/
|
||||
private function parseQuotedSearch(string $input): string {
|
||||
$input = self::cleanSearch($input);
|
||||
if ($input === '') {
|
||||
return '';
|
||||
}
|
||||
if (preg_match_all('#(?<=\\s|^)(?<![!-\\\\])(?P<search>/.*?(?<!\\\\)/[im]*)#', $input, $matches)) {
|
||||
$this->search_regex = self::htmlspecialchars_decodes($matches['search']);
|
||||
//TODO: Replace all those str_replace with PREG_OFFSET_CAPTURE
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
if (preg_match_all('/(?<![!-])(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
|
||||
$this->search = $matches['search'];
|
||||
//TODO: Replace all those str_replace with PREG_OFFSET_CAPTURE
|
||||
@@ -636,7 +771,11 @@ class FreshRSS_Search {
|
||||
if ($input === '') {
|
||||
return '';
|
||||
}
|
||||
if (preg_match_all('/(?<=\s|^)[!-](?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
|
||||
if (preg_match_all('#(?<=\\s|^)[!-](?P<search>(?<!\\\\)/.*?(?<!\\\\)/[im]*)#', $input, $matches)) {
|
||||
$this->not_search_regex = self::htmlspecialchars_decodes($matches['search']);
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
if (preg_match_all('/(?<=\\s|^)[!-](?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
|
||||
$this->not_search = $matches['search'];
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
@@ -644,7 +783,7 @@ class FreshRSS_Search {
|
||||
if ($input === '') {
|
||||
return '';
|
||||
}
|
||||
if (preg_match_all('/(?<=\s|^)[!-](?P<search>[^\s]+)/', $input, $matches)) {
|
||||
if (preg_match_all('/(?<=\\s|^)[!-](?P<search>[^\\s]+)/', $input, $matches)) {
|
||||
$this->not_search = array_merge(is_array($this->not_search) ? $this->not_search : [], $matches['search']);
|
||||
$input = str_replace($matches[0], '', $input);
|
||||
}
|
||||
@@ -656,7 +795,7 @@ class FreshRSS_Search {
|
||||
* Remove all unnecessary spaces in the search
|
||||
*/
|
||||
private static function cleanSearch(string $input): string {
|
||||
$input = preg_replace('/\s+/', ' ', $input);
|
||||
$input = preg_replace('/\\s+/', ' ', $input);
|
||||
if (!is_string($input)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ You need to verify that your server can run FreshRSS before installing it. If yo
|
||||
| Web server | **Apache 2.4** | nginx, lighttpd<br />minimal compatibility with Apache 2.2 |
|
||||
| PHP | **PHP 8.1+** | FreshRSS 1.21/1.22: PHP 7.2+; FreshRSS 1.23/1.24: PHP 7.4+ |
|
||||
| PHP modules | Required: libxml, cURL, JSON, PDO_MySQL, PCRE and ctype.<br />Required (32-bit only): GMP <br />Recommended: Zlib, mbstring, iconv, ZipArchive<br />*For the whole modules list see [Dockerfile](https://github.com/FreshRSS/FreshRSS/blob/edge/Docker/Dockerfile-Alpine#L7-L9)* | |
|
||||
| Database | **PostgreSQL 10+** | SQLite, MySQL 5.5.3+, MariaDB 5.5+ |
|
||||
| Database | **PostgreSQL 10+** | SQLite, MariaDB 10.0.5+, MySQL 8.0+ |
|
||||
| Browser | **Firefox** | Chrome, Opera, Safari, or Edge |
|
||||
|
||||
## Getting the appropriate version of FreshRSS
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Database configuration
|
||||
|
||||
FreshRSS supports the databases SQLite (built-in), PostgreSQL, MySQL / MariaDB.
|
||||
FreshRSS supports the databases SQLite (built-in), PostgreSQL, MariaDB / MySQL.
|
||||
|
||||
While the default installation should be fine for most cases, additional tuning can be made.
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ Remember to give the following information if you know it:
|
||||
1. Which browser? Which version?
|
||||
2. Which server: Apache, Nginx? Which version?
|
||||
3. Which version of PHP?
|
||||
4. Which database: SQLite, MySQL, MariaDB, PostgreSQL? Which version?
|
||||
4. Which database: SQLite, PostgreSQL, MariaDB, MySQL? Which version?
|
||||
5. Which distribution runs on the server? And… which version?
|
||||
|
||||
## How to provide feed data
|
||||
|
||||
@@ -49,7 +49,7 @@ You can use the search field to further refine results:
|
||||
* by author: `author:name` or `author:'composed name'`
|
||||
* by title: `intitle:keyword` or `intitle:'composed keyword'`
|
||||
* by URL: `inurl:keyword` or `inurl:'composed keyword'`
|
||||
* by tag: `#tag` or `#tag+with+whitespace`
|
||||
* by tag: `#tag` or `#tag+with+whitespace` or or `#'tag with whitespace'`
|
||||
* by free-text: `keyword` or `'composed keyword'`
|
||||
* by date of discovery, using the [ISO 8601 time interval format](http://en.wikipedia.org/wiki/ISO_8601#Time_intervals): `date:<date-interval>`
|
||||
* From a specific day, or month, or year:
|
||||
@@ -105,6 +105,8 @@ can be used to combine several search criteria with a logical *or* instead: `aut
|
||||
You don’t have to do anything special to combine multiple negative operators. Writing `!intitle:'thing1' !intitle:'thing2'` implies AND, see above. For more pointers on how AND and OR interact with negation, see [this GitHub comment](https://github.com/FreshRSS/FreshRSS/issues/3236#issuecomment-891219460).
|
||||
Additional reading: [De Morgan’s laws](https://en.wikipedia.org/wiki/De_Morgan%27s_laws).
|
||||
|
||||
> ℹ️ Searches are applied to the raw HTML content
|
||||
|
||||
Finally, parentheses may be used to express more complex queries, with basic negation support:
|
||||
|
||||
* `(author:Alice OR intitle:hello) (author:Bob OR intitle:world)`
|
||||
@@ -115,6 +117,31 @@ Finally, parentheses may be used to express more complex queries, with basic neg
|
||||
|
||||
> ℹ️ If you need to search for a parenthesis, it needs to be escaped like `\(` or `\)`
|
||||
|
||||
### Regex
|
||||
|
||||
Text searches (including `author:`, `intitle:`, `inurl:`, `#`) may use regular expressions, which must be enclosed in `/ /`.
|
||||
|
||||
Regex searches are case-sensitive by default, but can be made case-insensitive with the `i` modifier like: `/Alice/i`
|
||||
|
||||
Supports multiline mode with `m` modifier like: `/^Alice/m`
|
||||
|
||||
> ℹ️ `author:` is working with one author per line, so the multiline mode may advantageously be used, like: `author:/^Alice Dupont$/im`
|
||||
>
|
||||
> ℹ️ `#` is likewise working with one tag per line, so the multiline mode may advantageously be used, like: `#/^Hello World$/im`
|
||||
|
||||
Example to search entries, which title starts with the *Lol* word, with any number of *o*: `intitle:/^Lo+l/i`
|
||||
|
||||
As opposed to normal searches, HTML special characters are not escaped in regex searches, to allow searching HTML code, like: `/Hello <span>world<\/span>/`
|
||||
|
||||
⚠️ Advanced regex syntax details depend on the regex engine used:
|
||||
|
||||
* FreshRSS filter actions such as auto-mark-as-read and auto-favourite use [PHP preg_match](https://php.net/function.preg-match).
|
||||
* Regex searches depend on which database you are using:
|
||||
* For SQLite, [PHP preg_match](https://php.net/function.preg-match) is used;
|
||||
* [For PostgreSQL](https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-POSIX-REGEXP);
|
||||
* [For MariaDB](https://mariadb.com/kb/en/pcre/);
|
||||
* [For MySQL](https://dev.mysql.com/doc/refman/9.0/en/regexp.html#function_regexp-like).
|
||||
|
||||
## By sorting by date
|
||||
|
||||
You can change the sort order by clicking the toggle button available in the header.
|
||||
|
||||
@@ -32,7 +32,7 @@ Nous avons aussi besoin de quelques informations :
|
||||
|
||||
* Votre version de FreshRSS (sur la page A propos) ou le fichier `constants.php`)
|
||||
* Votre configuration de serveur : type d’hébergement, version PHP
|
||||
* Quelle base de données : SQLite, MySQL, MariaDB, PostgreSQL ? Quelle version ?
|
||||
* Quelle base de données : SQLite, PostgreSQL, MariaDB, MySQL ? Quelle version ?
|
||||
* Si possible, les logs associés (logs PHP et logs FreshRSS sous `data/users/your_user/log.txt`)
|
||||
|
||||
## Corriger un bogue
|
||||
|
||||
@@ -100,7 +100,7 @@ Pensez à donner les informations suivantes si vous les connaissez :
|
||||
1. Quel navigateur ? Quelle version ?
|
||||
2. Quel serveur : Apache, Nginx ? Quelle version ?
|
||||
3. Quelle version de PHP ?
|
||||
4. Quelle base de données : SQLite, MySQL, MariaDB, PostgreSQL ? Quelle version ?
|
||||
4. Quelle base de données : SQLite, PostgreSQL, MariaDB, MySQL ? Quelle version ?
|
||||
5. Quelle distribution sur le serveur ? Et… quelle version ?
|
||||
|
||||
## Système de branches
|
||||
|
||||
@@ -9,7 +9,7 @@ Il est toutefois de votre responsabilité de vérifier que votre hébergement pe
|
||||
| Serveur web | **Apache 2.4+** | nginx, lighttpd |
|
||||
| PHP | **PHP 8.1+** | |
|
||||
| Modules PHP | Requis : libxml, cURL, JSON, PDO_MySQL, PCRE et ctype<br />Requis (32 bits seulement) : GMP<br />Recommandé : Zlib, mbstring et iconv, ZipArchive<br />*Pour une liste complète des modules nécessaires voir le [Dockerfile](https://github.com/FreshRSS/FreshRSS/blob/edge/Docker/Dockerfile-Alpine#L7-L9)* | |
|
||||
| Base de données | **PostgreSQL 10+** | SQLite, MySQL 5.5.3+, MariaDB 5.5+ |
|
||||
| Base de données | **PostgreSQL 10+** | SQLite, MariaDB 10.0.5+, MySQL 8.0+ |
|
||||
| Navigateur | **Firefox** | Chrome, Opera, Safari, or Edge |
|
||||
|
||||
## Choisir la bonne version de FreshRSS
|
||||
|
||||
@@ -208,7 +208,7 @@ Il est possible d’utiliser le champ de recherche pour raffiner les résultats
|
||||
* par auteur : `author:nom` ou `author:'nom composé'`
|
||||
* par titre : `intitle:mot` ou `intitle:'mot composé'`
|
||||
* par URL : `inurl:mot` ou `inurl:'mot composé'`
|
||||
* par tag : `#tag`
|
||||
* par tag : `#tag` ou `#'tag avec espace'`
|
||||
* par texte libre : `mot` ou `'mot composé'`
|
||||
* par date d’ajout, en utilisant le [format ISO 8601 d’intervalle entre deux dates](https://fr.wikipedia.org/wiki/ISO_8601#Intervalle_entre_deux_dates) : `date:<intervalle-de-dates>`
|
||||
* D’un jour spécifique, ou mois, ou année :
|
||||
@@ -264,6 +264,8 @@ encore plus précis, et il est autorisé d’avoir plusieurs instances de :
|
||||
Combiner plusieurs critères implique un *et* logique, mais le mot clef `OR`
|
||||
peut être utilisé pour combiner plusieurs critères avec un *ou* logique : `author:Dupont OR author:Dupond`
|
||||
|
||||
> ℹ️ Les recherches sont effectuées sur le code HTML brut
|
||||
|
||||
Enfin, les parenthèses peuvent être utilisées pour des expressions plus complexes, avec un support basique de la négation :
|
||||
|
||||
* `(author:Alice OR intitle:bonjour) (author:Bob OR intitle:monde)`
|
||||
@@ -273,3 +275,28 @@ Enfin, les parenthèses peuvent être utilisées pour des expressions plus compl
|
||||
* `!(S:1 OR S:2)`
|
||||
|
||||
> ℹ️ Si vous devez chercher une parenthèse, elle doit être *échappée* comme suit : `\(` ou `\)`
|
||||
|
||||
#### Regex
|
||||
|
||||
Les recherches de texte (incluant `author:`, `intitle:`, `inurl:`, `#`) peuvent utiliser les expressions régulières qui doivent être exprimées comme `/ /`.
|
||||
|
||||
Les recherches regex sont sensibles à la casse, mais peuvent être rendues insensibles à la casse avec l’option de recherche `i` comme : `/Alice/i`
|
||||
|
||||
Le mode multilignes peut être activé avec l’option de recherche `m` comme : `/^Alice/m`
|
||||
|
||||
> ℹ️ `author:` fonctionne avec un auteur par ligne, ce qui fait que le mode multilignes peut être avantageux, comme : `author:/^Alice Doe$/im`
|
||||
>
|
||||
> ℹ️ `#` fonctionne également avec un tag par line, ce qui fait que le mode multilignes peut être avantageux, comme : `#/^Hello World$/im`
|
||||
|
||||
Exemple pour rechercher des articles dont le titre commence par le mot *Lol* avec un nombre indéterminé de *o*: `intitle:/^Lo+l/i`
|
||||
|
||||
Contrairement aux recherches normales, les caractères spéciaux HTML ne sont pas encodés dans les recherches regex, afin de permettre de chercher du code HTML, comme : `/Bonjour <span>à tous<\/span>/`
|
||||
|
||||
⚠️ Les détails de syntaxe regex avancée dépendent du moteur regex utilisé :
|
||||
|
||||
* Les filtres d’action de FreshRSS comme marquer-automatiquement-comme-lu et mettre-automatiquement-en-favori utilisent [PHP preg_match](https://php.net/function.preg-match).
|
||||
* Les recherches regex dépendent de la base de données utilisée :
|
||||
* Pour SQLite, [PHP preg_match](https://php.net/function.preg-match) est utilisé ;
|
||||
* [Pour PostgreSQL](https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-POSIX-REGEXP) ;
|
||||
* [Pour MariaDB](https://mariadb.com/kb/en/pcre/) ;
|
||||
* [Pour MySQL](https://dev.mysql.com/doc/refman/9.0/en/regexp.html#function_regexp-like).
|
||||
|
||||
@@ -16,6 +16,11 @@ class Minz_ModelPdo {
|
||||
*/
|
||||
public static bool $usesSharedPdo = true;
|
||||
|
||||
/**
|
||||
* If true, the connection to the database will be a dummy one. Useful for unit tests.
|
||||
*/
|
||||
public static bool $dummyConnection = false;
|
||||
|
||||
private static ?Minz_Pdo $sharedPdo = null;
|
||||
|
||||
private static string $sharedCurrentUser = '';
|
||||
@@ -97,6 +102,9 @@ class Minz_ModelPdo {
|
||||
$this->pdo = $currentPdo;
|
||||
return;
|
||||
}
|
||||
if (self::$dummyConnection) {
|
||||
return;
|
||||
}
|
||||
if ($currentUser == null) {
|
||||
throw new Minz_PDOConnectionException('Current user must not be empty!', '', Minz_Exception::ERROR);
|
||||
}
|
||||
|
||||
@@ -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/']
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user