aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Alexandre Alapetite <alexandre@alapetite.fr> 2025-06-29 11:09:08 +0200
committerGravatar GitHub <noreply@github.com> 2025-06-29 11:09:08 +0200
commitc8bbf355342985c83054c6c36c6538a780ab509e (patch)
tree87286cac4e98769b3a14308e042abdd4d83153e4
parent7c57f38008136202ba7b38e3154ac87be4eefb68 (diff)
Add search operator `c:` for categories (#7696)
* Add search operator `c:` for categories fix https://github.com/FreshRSS/FreshRSS/discussions/7692 Allow searching for e.g. `c:23,34`
-rw-r--r--app/Models/Entry.php6
-rw-r--r--app/Models/EntryDAO.php19
-rw-r--r--app/Models/Search.php51
-rw-r--r--docs/en/users/10_filter.md1
-rw-r--r--docs/fr/users/03_Main_view.md1
-rw-r--r--tests/app/Models/SearchTest.php5
6 files changed, 83 insertions, 0 deletions
diff --git a/app/Models/Entry.php b/app/Models/Entry.php
index 66c05a830..190b435c8 100644
--- a/app/Models/Entry.php
+++ b/app/Models/Entry.php
@@ -642,6 +642,12 @@ HTML;
if ($ok && $filter->getNotFeedIds() !== null) {
$ok &= !in_array($this->feedId, $filter->getNotFeedIds(), true);
}
+ if ($ok && $filter->getCategoryIds() !== null) {
+ $ok &= in_array($this->feed()?->categoryId(), $filter->getCategoryIds(), true);
+ }
+ if ($ok && $filter->getNotCategoryIds() !== null) {
+ $ok &= !in_array($this->feed()?->categoryId(), $filter->getNotCategoryIds(), true);
+ }
if ($ok && $filter->getAuthor() !== null) {
foreach ($filter->getAuthor() as $author) {
$ok &= stripos(implode(';', $this->authors), $author) !== false;
diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php
index 68746c380..fecf02cf8 100644
--- a/app/Models/EntryDAO.php
+++ b/app/Models/EntryDAO.php
@@ -917,6 +917,25 @@ SQL;
$sub_search .= ') ';
}
+ if ($filter->getCategoryIds() !== null) {
+ $sub_search .= 'AND ' . $alias . 'id_feed IN (SELECT f.id FROM `_feed` f WHERE f.category IN (';
+ foreach ($filter->getCategoryIds() as $category_id) {
+ $sub_search .= '?,';
+ $values[] = $category_id;
+ }
+ $sub_search = rtrim($sub_search, ',');
+ $sub_search .= ')) ';
+ }
+ if ($filter->getNotCategoryIds() !== null) {
+ $sub_search .= 'AND ' . $alias . 'id_feed NOT IN (SELECT f.id FROM `_feed` f WHERE f.category IN (';
+ foreach ($filter->getNotCategoryIds() as $category_id) {
+ $sub_search .= '?,';
+ $values[] = $category_id;
+ }
+ $sub_search = rtrim($sub_search, ',');
+ $sub_search .= ')) ';
+ }
+
if ($filter->getLabelIds() !== null) {
if ($filter->getLabelIds() === '*') {
$sub_search .= 'AND EXISTS (SELECT et.id_tag FROM `_entrytag` et WHERE et.id_entry = ' . $alias . 'id) ';
diff --git a/app/Models/Search.php b/app/Models/Search.php
index d425fcee8..68f0a6bce 100644
--- a/app/Models/Search.php
+++ b/app/Models/Search.php
@@ -21,6 +21,8 @@ class FreshRSS_Search implements \Stringable {
private ?array $entry_ids = null;
/** @var list<int>|null */
private ?array $feed_ids = null;
+ /** @var list<int>|null */
+ private ?array $category_ids = null;
/** @var list<int>|'*'|null */
private $label_ids = null;
/** @var list<string>|null */
@@ -62,6 +64,8 @@ class FreshRSS_Search implements \Stringable {
private ?array $not_entry_ids = null;
/** @var list<int>|null */
private ?array $not_feed_ids = null;
+ /** @var list<int>|null */
+ private ?array $not_category_ids = null;
/** @var list<int>|'*'|null */
private $not_label_ids = null;
/** @var list<string>|null */
@@ -107,6 +111,7 @@ class FreshRSS_Search implements \Stringable {
$input = $this->parseNotEntryIds($input);
$input = $this->parseNotFeedIds($input);
+ $input = $this->parseNotCategoryIds($input);
$input = $this->parseNotLabelIds($input);
$input = $this->parseNotLabelNames($input);
@@ -121,6 +126,7 @@ class FreshRSS_Search implements \Stringable {
$input = $this->parseEntryIds($input);
$input = $this->parseFeedIds($input);
+ $input = $this->parseCategoryIds($input);
$input = $this->parseLabelIds($input);
$input = $this->parseLabelNames($input);
@@ -165,6 +171,15 @@ class FreshRSS_Search implements \Stringable {
return $this->not_feed_ids;
}
+ /** @return list<int>|null */
+ public function getCategoryIds(): ?array {
+ return $this->category_ids;
+ }
+ /** @return list<int>|null */
+ public function getNotCategoryIds(): ?array {
+ return $this->not_category_ids;
+ }
+
/** @return list<int>|'*'|null */
public function getLabelIds(): array|string|null {
return $this->label_ids;
@@ -420,6 +435,42 @@ class FreshRSS_Search implements \Stringable {
return $input;
}
+ private function parseCategoryIds(string $input): string {
+ if (preg_match_all('/\\bc:(?P<search>[0-9,]*)/', $input, $matches)) {
+ $input = str_replace($matches[0], '', $input);
+ $ids_lists = $matches['search'];
+ $this->category_ids = [];
+ foreach ($ids_lists as $ids_list) {
+ $category_ids = explode(',', $ids_list);
+ $category_ids = self::removeEmptyValues($category_ids);
+ /** @var list<int> $category_ids */
+ $category_ids = array_map('intval', $category_ids);
+ if (!empty($category_ids)) {
+ $this->category_ids = array_merge($this->category_ids, $category_ids);
+ }
+ }
+ }
+ return $input;
+ }
+
+ private function parseNotCategoryIds(string $input): string {
+ if (preg_match_all('/(?<=[\\s(]|^)[!-]c:(?P<search>[0-9,]*)/', $input, $matches)) {
+ $input = str_replace($matches[0], '', $input);
+ $ids_lists = $matches['search'];
+ $this->not_category_ids = [];
+ foreach ($ids_lists as $ids_list) {
+ $category_ids = explode(',', $ids_list);
+ $category_ids = self::removeEmptyValues($category_ids);
+ /** @var list<int> $category_ids */
+ $category_ids = array_map('intval', $category_ids);
+ if (!empty($category_ids)) {
+ $this->not_category_ids = array_merge($this->not_category_ids, $category_ids);
+ }
+ }
+ }
+ return $input;
+ }
+
/**
* Parse the search string to find tags (labels) IDs.
*/
diff --git a/docs/en/users/10_filter.md b/docs/en/users/10_filter.md
index 7d37e312e..42e44339b 100644
--- a/docs/en/users/10_filter.md
+++ b/docs/en/users/10_filter.md
@@ -46,6 +46,7 @@ It is possible to filter articles by their content by inputting a string in the
You can use the search field to further refine results:
* by feed ID: `f:123` or multiple feed IDs (*or*): `f:123,234,345`
+* by category ID: `c:23` or multiple category IDs (*or*): `c:23,34,45`
* by author: `author:name` or `author:'composed name'`
* by title: `intitle:keyword` or `intitle:'composed keyword'`
* by text (content): `intext:keyword` or `intext:'composed keyword'`
diff --git a/docs/fr/users/03_Main_view.md b/docs/fr/users/03_Main_view.md
index 5d874124b..67f03ee93 100644
--- a/docs/fr/users/03_Main_view.md
+++ b/docs/fr/users/03_Main_view.md
@@ -205,6 +205,7 @@ the search field.
Il est possible d’utiliser le champ de recherche pour raffiner les résultats :
* par ID de flux : `f:123` ou plusieurs flux (*ou*) : `f:123,234,345`
+* by ID de catégorie : `c:23` ou plusieurs catégories (*ou*): `c:23,34,45`
* par auteur : `author:nom` ou `author:'nom composé'`
* par titre : `intitle:mot` ou `intitle:'mot composé'`
* par texte (contenu) : `intext:mot` ou `intext:'mot composé'`
diff --git a/tests/app/Models/SearchTest.php b/tests/app/Models/SearchTest.php
index a62e255de..1e8855aa3 100644
--- a/tests/app/Models/SearchTest.php
+++ b/tests/app/Models/SearchTest.php
@@ -367,6 +367,11 @@ class SearchTest extends PHPUnit\Framework\TestCase {
[1, 2, 3, 4, 5, 6, 7]
],
[
+ 'c:1 OR c:2,3',
+ ' (e.id_feed IN (SELECT f.id FROM `_feed` f WHERE f.category IN (?)) ) OR (e.id_feed IN (SELECT f.id FROM `_feed` f WHERE f.category IN (?,?)) ) ',
+ [1, 2, 3]
+ ],
+ [
'#tag Hello OR (author:Alice inurl:example) OR (f:3 intitle:World) OR L:12',
" ((TRIM(e.tags) || ' #' LIKE ? AND (e.title LIKE ? OR e.content LIKE ?) )) OR ((e.author LIKE ? AND e.link LIKE ? )) OR" .
' ((e.id_feed IN (?) AND e.title LIKE ? )) OR ((e.id IN (SELECT et.id_entry FROM `_entrytag` et WHERE et.id_tag IN (?)) )) ',