aboutsummaryrefslogtreecommitdiff
path: root/app/Models
diff options
context:
space:
mode:
authorGravatar Alexandre Alapetite <alexandre@alapetite.fr> 2025-01-25 09:16:13 +0100
committerGravatar GitHub <noreply@github.com> 2025-01-25 09:16:13 +0100
commitd6c2daee51fa90f000c106492141baf3824931d2 (patch)
tree98357a95438a55c9399cc1e1520c96996536b9c6 /app/Models
parent22b74b0a5790360d81088a83addab1f98b7f7947 (diff)
Add search operator intext: (#7228)
* Add search operator intext: fix https://github.com/FreshRSS/FreshRSS/issues/6188 https://github.com/FreshRSS/FreshRSS/discussions/7220 * Add example to doc
Diffstat (limited to 'app/Models')
-rw-r--r--app/Models/Entry.php20
-rw-r--r--app/Models/EntryDAO.php48
-rw-r--r--app/Models/Search.php71
3 files changed, 138 insertions, 1 deletions
diff --git a/app/Models/Entry.php b/app/Models/Entry.php
index f86948122..fe0cf7429 100644
--- a/app/Models/Entry.php
+++ b/app/Models/Entry.php
@@ -673,6 +673,26 @@ HTML;
$ok &= preg_match($title, $this->title) === 0;
}
}
+ if ($ok && $filter->getIntext() !== null) {
+ foreach ($filter->getIntext() as $content) {
+ $ok &= stripos($this->content, $content) !== false;
+ }
+ }
+ if ($ok && $filter->getIntextRegex() !== null) {
+ foreach ($filter->getIntextRegex() as $content) {
+ $ok &= preg_match($content, $this->content) === 1;
+ }
+ }
+ if ($ok && $filter->getNotIntext() !== null) {
+ foreach ($filter->getNotIntext() as $content) {
+ $ok &= stripos($this->content, $content) === false;
+ }
+ }
+ if ($ok && $filter->getNotIntextRegex() !== null) {
+ foreach ($filter->getNotIntextRegex() as $content) {
+ $ok &= preg_match($content, $this->content) === 0;
+ }
+ }
if ($ok && $filter->getTags() !== null) {
foreach ($filter->getTags() as $tag2) {
$found = false;
diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php
index af229df54..a234dce91 100644
--- a/app/Models/EntryDAO.php
+++ b/app/Models/EntryDAO.php
@@ -981,6 +981,30 @@ SQL;
$sub_search .= 'AND ' . static::sqlRegex($alias . 'title', $title, $values) . ' ';
}
}
+ if ($filter->getIntext() !== null) {
+ if (static::isCompressed()) { // MySQL-only
+ foreach ($filter->getIntext() as $content) {
+ $sub_search .= "AND UNCOMPRESS({$alias}content_bin) LIKE ? ";
+ $values[] = "%{$content}%";
+ }
+ } else {
+ foreach ($filter->getIntext() as $content) {
+ $sub_search .= 'AND ' . $alias . 'content LIKE ? ';
+ $values[] = "%{$content}%";
+ }
+ }
+ }
+ if ($filter->getIntextRegex() !== null) {
+ if (static::isCompressed()) { // MySQL-only
+ foreach ($filter->getIntextRegex() as $content) {
+ $sub_search .= 'AND ' . static::sqlRegex("UNCOMPRESS({$alias}content_bin)", $content, $values) . ') ';
+ }
+ } else {
+ foreach ($filter->getIntextRegex() as $content) {
+ $sub_search .= 'AND ' . static::sqlRegex($alias . 'content', $content, $values) . ' ';
+ }
+ }
+ }
if ($filter->getTags() !== null) {
foreach ($filter->getTags() as $tag) {
$sub_search .= 'AND ' . static::sqlConcat('TRIM(' . $alias . 'tags) ', " ' #'") . ' LIKE ? ';
@@ -1026,6 +1050,30 @@ SQL;
$sub_search .= 'AND NOT ' . static::sqlRegex($alias . 'title', $title, $values) . ' ';
}
}
+ if ($filter->getNotIntext() !== null) {
+ if (static::isCompressed()) { // MySQL-only
+ foreach ($filter->getNotIntext() as $content) {
+ $sub_search .= "AND UNCOMPRESS({$alias}content_bin) NOT LIKE ? ";
+ $values[] = "%{$content}%";
+ }
+ } else {
+ foreach ($filter->getNotIntext() as $content) {
+ $sub_search .= 'AND ' . $alias . 'content NOT LIKE ? ';
+ $values[] = "%{$content}%";
+ }
+ }
+ }
+ if ($filter->getNotIntextRegex() !== null) {
+ if (static::isCompressed()) { // MySQL-only
+ foreach ($filter->getNotIntextRegex() as $content) {
+ $sub_search .= 'AND NOT ' . static::sqlRegex("UNCOMPRESS({$alias}content_bin)", $content, $values) . ') ';
+ }
+ } else {
+ foreach ($filter->getNotIntextRegex() as $content) {
+ $sub_search .= 'AND NOT ' . static::sqlRegex($alias . 'content', $content, $values) . ' ';
+ }
+ }
+ }
if ($filter->getNotTags() !== null) {
foreach ($filter->getNotTags() as $tag) {
$sub_search .= 'AND ' . static::sqlConcat('TRIM(' . $alias . 'tags) ', " ' #'") . ' NOT LIKE ? ';
diff --git a/app/Models/Search.php b/app/Models/Search.php
index 5b88b1f3b..d425fcee8 100644
--- a/app/Models/Search.php
+++ b/app/Models/Search.php
@@ -29,6 +29,10 @@ class FreshRSS_Search implements \Stringable {
private ?array $intitle = null;
/** @var list<string>|null */
private ?array $intitle_regex = null;
+ /** @var list<string>|null */
+ private ?array $intext = null;
+ /** @var list<string>|null */
+ private ?array $intext_regex = null;
/** @var int|false|null */
private $min_date = null;
/** @var int|false|null */
@@ -66,6 +70,10 @@ class FreshRSS_Search implements \Stringable {
private ?array $not_intitle = null;
/** @var list<string>|null */
private ?array $not_intitle_regex = null;
+ /** @var list<string>|null */
+ private ?array $not_intext = null;
+ /** @var list<string>|null */
+ private ?array $not_intext_regex = null;
/** @var int|false|null */
private $not_min_date = null;
/** @var int|false|null */
@@ -106,6 +114,7 @@ class FreshRSS_Search implements \Stringable {
$input = $this->parseNotDateSearch($input);
$input = $this->parseNotIntitleSearch($input);
+ $input = $this->parseNotIntextSearch($input);
$input = $this->parseNotAuthorSearch($input);
$input = $this->parseNotInurlSearch($input);
$input = $this->parseNotTagsSearch($input);
@@ -119,6 +128,7 @@ class FreshRSS_Search implements \Stringable {
$input = $this->parseDateSearch($input);
$input = $this->parseIntitleSearch($input);
+ $input = $this->parseIntextSearch($input);
$input = $this->parseAuthorSearch($input);
$input = $this->parseInurlSearch($input);
$input = $this->parseTagsSearch($input);
@@ -189,6 +199,23 @@ class FreshRSS_Search implements \Stringable {
return $this->not_intitle_regex;
}
+ /** @return list<string>|null */
+ public function getIntext(): ?array {
+ return $this->intext;
+ }
+ /** @return list<string>|null */
+ public function getIntextRegex(): ?array {
+ return $this->intext_regex;
+ }
+ /** @return list<string>|null */
+ public function getNotIntext(): ?array {
+ return $this->not_intext;
+ }
+ /** @return list<string>|null */
+ public function getNotIntextRegex(): ?array {
+ return $this->not_intext_regex;
+ }
+
public function getMinDate(): ?int {
return $this->min_date ?: null;
}
@@ -494,7 +521,6 @@ class FreshRSS_Search implements \Stringable {
/**
* Parse the search string to find intitle keyword and the search related to it.
- * The search is the first word following the keyword.
*/
private function parseIntitleSearch(string $input): string {
if (preg_match_all('#\\bintitle:(?P<search>/.*?(?<!\\\\)/[im]*)#', $input, $matches)) {
@@ -537,6 +563,49 @@ class FreshRSS_Search implements \Stringable {
}
/**
+ * Parse the search string to find intext keyword and the search related to it.
+ */
+ private function parseIntextSearch(string $input): string {
+ if (preg_match_all('#\\bintext:(?P<search>/.*?(?<!\\\\)/[im]*)#', $input, $matches)) {
+ $this->intext_regex = self::htmlspecialchars_decodes($matches['search']);
+ $input = str_replace($matches[0], '', $input);
+ }
+ if (preg_match_all('/\\bintext:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
+ $this->intext = $matches['search'];
+ $input = str_replace($matches[0], '', $input);
+ }
+ if (preg_match_all('/\\bintext:(?P<search>[^\s"]*)/', $input, $matches)) {
+ $this->intext = array_merge($this->intext ?? [], $matches['search']);
+ $input = str_replace($matches[0], '', $input);
+ }
+ $this->intext = self::removeEmptyValues($this->intext);
+ if (empty($this->intext)) {
+ $this->intext = null;
+ }
+ return $input;
+ }
+
+ private function parseNotIntextSearch(string $input): string {
+ if (preg_match_all('#(?<=[\\s(]|^)[!-]intext:(?P<search>/.*?(?<!\\\\)/[im]*)#', $input, $matches)) {
+ $this->not_intext_regex = self::htmlspecialchars_decodes($matches['search']);
+ $input = str_replace($matches[0], '', $input);
+ }
+ if (preg_match_all('/(?<=[\\s(]|^)[!-]intext:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
+ $this->not_intext = $matches['search'];
+ $input = str_replace($matches[0], '', $input);
+ }
+ if (preg_match_all('/(?<=[\\s(]|^)[!-]intext:(?P<search>[^\s"]*)/', $input, $matches)) {
+ $this->not_intext = array_merge($this->not_intext ?? [], $matches['search']);
+ $input = str_replace($matches[0], '', $input);
+ }
+ $this->not_intext = self::removeEmptyValues($this->not_intext);
+ if (empty($this->not_intext)) {
+ $this->not_intext = null;
+ }
+ return $input;
+ }
+
+ /**
* Parse the search string to find author keyword and the search related to it.
* The search is the first word following the keyword except when using
* a delimiter. Supported delimiters are single quote (') and double quotes (").