aboutsummaryrefslogtreecommitdiff
path: root/app/Models
diff options
context:
space:
mode:
Diffstat (limited to 'app/Models')
-rw-r--r--app/Models/DatabaseDAO.php27
-rw-r--r--app/Models/Entry.php66
-rw-r--r--app/Models/EntryDAO.php116
-rw-r--r--app/Models/EntryDAOPGSQL.php26
-rw-r--r--app/Models/EntryDAOSQLite.php21
-rw-r--r--app/Models/Search.php207
6 files changed, 425 insertions, 38 deletions
diff --git a/app/Models/DatabaseDAO.php b/app/Models/DatabaseDAO.php
index 363225cdb..ba0ee3e79 100644
--- a/app/Models/DatabaseDAO.php
+++ b/app/Models/DatabaseDAO.php
@@ -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/
diff --git a/app/Models/Entry.php b/app/Models/Entry.php
index 4b331419b..415bc0235 100644
--- a/app/Models/Entry.php
+++ b/app/Models/Entry.php
@@ -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;
}
diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php
index 175df15c3..6a00e2108 100644
--- a/app/Models/EntryDAO.php
+++ b/app/Models/EntryDAO.php
@@ -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];
diff --git a/app/Models/EntryDAOPGSQL.php b/app/Models/EntryDAOPGSQL.php
index 8adeffe9e..fe157308c 100644
--- a/app/Models/EntryDAOPGSQL.php
+++ b/app/Models/EntryDAOPGSQL.php
@@ -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 {
diff --git a/app/Models/EntryDAOSQLite.php b/app/Models/EntryDAOSQLite.php
index 9c2b37623..6d604f25a 100644
--- a/app/Models/EntryDAOSQLite.php
+++ b/app/Models/EntryDAOSQLite.php
@@ -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 {
diff --git a/app/Models/Search.php b/app/Models/Search.php
index 7eaf741c3..755cf6b59 100644
--- a/app/Models/Search.php
+++ b/app/Models/Search.php
@@ -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
@@ -254,10 +314,18 @@ class FreshRSS_Search {
}
/**
+ * @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);
+ }
+ 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);
+ }
+ $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 '';
}