4
0
Эх сурвалжийг харах

Search labels (#3709)

* Search labels
#fix https://github.com/FreshRSS/FreshRSS/issues/3704

* Documentation

* Allow list without quotes

* Allow boolean AND searches

* Allow searching any label

* fix labels alias
Alexandre Alapetite 4 жил өмнө
parent
commit
705be9a6a1

+ 80 - 22
app/Models/EntryDAO.php

@@ -732,25 +732,6 @@ SQL;
 				}
 				$sub_search = '';
 
-				if ($filter->getFeedIds()) {
-					$sub_search .= 'AND ' . $alias . 'id_feed IN (';
-					foreach ($filter->getFeedIds() as $feed_id) {
-						$sub_search .= '?,';
-						$values[] = $feed_id;
-					}
-					$sub_search = rtrim($sub_search, ',');
-					$sub_search .= ') ';
-				}
-				if ($filter->getNotFeedIds()) {
-					$sub_search .= 'AND ' . $alias . 'id_feed NOT IN (';
-					foreach ($filter->getNotFeedIds() as $feed_id) {
-						$sub_search .= '?,';
-						$values[] = $feed_id;
-					}
-					$sub_search = rtrim($sub_search, ',');
-					$sub_search .= ') ';
-				}
-
 				if ($filter->getMinDate()) {
 					$sub_search .= 'AND ' . $alias . 'id >= ? ';
 					$values[] = "{$filter->getMinDate()}000000";
@@ -800,6 +781,83 @@ SQL;
 					$sub_search .= ') ';
 				}
 
+				if ($filter->getFeedIds()) {
+					foreach ($filter->getFeedIds() as $feed_ids) {
+						$sub_search .= 'AND ' . $alias . 'id_feed IN (';
+						foreach ($feed_ids as $feed_id) {
+							$sub_search .= '?,';
+							$values[] = $feed_id;
+						}
+						$sub_search = rtrim($sub_search, ',');
+						$sub_search .= ') ';
+					}
+				}
+				if ($filter->getNotFeedIds()) {
+					foreach ($filter->getNotFeedIds() as $feed_ids) {
+						$sub_search .= 'AND ' . $alias . 'id_feed NOT IN (';
+						foreach ($feed_ids as $feed_id) {
+							$sub_search .= '?,';
+							$values[] = $feed_id;
+						}
+						$sub_search = rtrim($sub_search, ',');
+						$sub_search .= ') ';
+					}
+				}
+
+				if ($filter->getLabelIds()) {
+					foreach ($filter->getLabelIds() as $label_ids) {
+						if ($label_ids === '*') {
+							$sub_search .= 'AND EXISTS (SELECT et.id_tag FROM `_entrytag` et WHERE et.id_entry = ' . $alias . 'id) ';
+						} else {
+							$sub_search .= 'AND ' . $alias . 'id IN (SELECT et.id_entry FROM `_entrytag` et WHERE et.id_tag IN (';
+							foreach ($label_ids as $label_id) {
+								$sub_search .= '?,';
+								$values[] = $label_id;
+							}
+							$sub_search = rtrim($sub_search, ',');
+							$sub_search .= ')) ';
+						}
+					}
+				}
+				if ($filter->getNotLabelIds()) {
+					foreach ($filter->getNotLabelIds() as $label_ids) {
+						if ($label_ids === '*') {
+							$sub_search .= 'AND NOT EXISTS (SELECT et.id_tag FROM `_entrytag` et WHERE et.id_entry = ' . $alias . 'id) ';
+						} else {
+							$sub_search .= 'AND ' . $alias . 'id NOT IN (SELECT et.id_entry FROM `_entrytag` et WHERE et.id_tag IN (';
+							foreach ($label_ids as $label_id) {
+								$sub_search .= '?,';
+								$values[] = $label_id;
+							}
+							$sub_search = rtrim($sub_search, ',');
+							$sub_search .= ')) ';
+						}
+					}
+				}
+
+				if ($filter->getLabelNames()) {
+					foreach ($filter->getLabelNames() as $label_names) {
+						$sub_search .= 'AND ' . $alias . 'id IN (SELECT et.id_entry FROM `_entrytag` et, `_tag` t WHERE et.id_tag = t.id AND t.name IN (';
+						foreach ($label_names as $label_name) {
+							$sub_search .= '?,';
+							$values[] = $label_name;
+						}
+						$sub_search = rtrim($sub_search, ',');
+						$sub_search .= ')) ';
+					}
+				}
+				if ($filter->getNotLabelNames()) {
+					foreach ($filter->getNotLabelNames() as $label_names) {
+						$sub_search .= 'AND ' . $alias . 'id NOT IN (SELECT et.id_entry FROM `_entrytag` et, `_tag` t WHERE et.id_tag = t.id AND t.name IN (';
+						foreach ($label_names as $label_name) {
+							$sub_search .= '?,';
+							$values[] = $label_name;
+						}
+						$sub_search = rtrim($sub_search, ',');
+						$sub_search .= ')) ';
+					}
+				}
+
 				if ($filter->getAuthor()) {
 					foreach ($filter->getAuthor() as $author) {
 						$sub_search .= 'AND ' . $alias . 'author LIKE ? ';
@@ -913,14 +971,14 @@ SQL;
 			$where .= 'e.id_feed=? ';
 			$values[] = intval($id);
 			break;
-		case 't':	//Tag
+		case 't':	//Tag (label)
 			$where .= 'et.id_tag=? ';
 			$values[] = intval($id);
 			break;
-		case 'T':	//Any tag
+		case 'T':	//Any tag (label)
 			$where .= '1=1 ';
 			break;
-		case 'ST':	//Starred or tagged
+		case 'ST':	//Starred or tagged (label)
 			$where .= 'e.is_favorite=1 OR EXISTS (SELECT et2.id_tag FROM `_entrytag` et2 WHERE et2.id_entry = e.id) ';
 			break;
 		default:

+ 140 - 9
app/Models/Search.php

@@ -15,6 +15,8 @@ class FreshRSS_Search {
 
 	// The following properties are extracted from the raw input
 	private $feed_ids;
+	private $label_ids;
+	private $label_names;
 	private $intitle;
 	private $min_date;
 	private $max_date;
@@ -26,6 +28,8 @@ class FreshRSS_Search {
 	private $search;
 
 	private $not_feed_ids;
+	private $not_label_ids;
+	private $not_label_names;
 	private $not_intitle;
 	private $not_min_date;
 	private $not_max_date;
@@ -45,6 +49,8 @@ class FreshRSS_Search {
 		$input = preg_replace('/:"(.*?)"/', ':"\1"', $input);
 
 		$input = $this->parseNotFeedIds($input);
+		$input = $this->parseNotLabelIds($input);
+		$input = $this->parseNotLabelNames($input);
 
 		$input = $this->parseNotPubdateSearch($input);
 		$input = $this->parseNotDateSearch($input);
@@ -55,6 +61,8 @@ class FreshRSS_Search {
 		$input = $this->parseNotTagsSearch($input);
 
 		$input = $this->parseFeedIds($input);
+		$input = $this->parseLabelIds($input);
+		$input = $this->parseLabelNames($input);
 
 		$input = $this->parsePubdateSearch($input);
 		$input = $this->parseDateSearch($input);
@@ -83,6 +91,19 @@ class FreshRSS_Search {
 		return $this->not_feed_ids;
 	}
 
+	public function getLabelIds() {
+		return $this->label_ids;
+	}
+	public function getNotlabelIds() {
+		return $this->not_label_ids;
+	}
+	public function getLabelNames() {
+		return $this->label_names;
+	}
+	public function getNotlabelNames() {
+		return $this->not_label_names;
+	}
+
 	public function getIntitle() {
 		return $this->intitle;
 	}
@@ -175,12 +196,15 @@ class FreshRSS_Search {
 	 */
 	private function parseFeedIds($input) {
 		if (preg_match_all('/\bf:(?P<search>[0-9,]*)/', $input, $matches)) {
-			$ids_lists = $matches['search'];
 			$input = str_replace($matches[0], '', $input);
-			$ids_lists = self::removeEmptyValues($ids_lists);
-			if (!empty($ids_lists[0])) {
-				$this->feed_ids = explode(',', $ids_lists[0]);
-				array_filter($this->feed_ids, function($v) { $v != ''; });
+			$ids_lists = $matches['search'];
+			$this->feed_ids = [];
+			foreach ($ids_lists as $ids_list) {
+				$feed_ids = explode(',', $ids_list);
+				$feed_ids = self::removeEmptyValues($feed_ids);
+				if (!empty($feed_ids)) {
+					$this->feed_ids[] = $feed_ids;
+				}
 			}
 		}
 		return $input;
@@ -188,12 +212,119 @@ class FreshRSS_Search {
 
 	private function parseNotFeedIds($input) {
 		if (preg_match_all('/[!-]f:(?P<search>[0-9,]*)/', $input, $matches)) {
+			$input = str_replace($matches[0], '', $input);
+			$ids_lists = $matches['search'];
+			$this->not_feed_ids = [];
+			foreach ($ids_lists as $ids_list) {
+				$feed_ids = explode(',', $ids_list);
+				$feed_ids = self::removeEmptyValues($feed_ids);
+				if (!empty($feed_ids)) {
+					$this->not_feed_ids[] = $feed_ids;
+				}
+			}
+		}
+		return $input;
+	}
+
+	/**
+	 * Parse the search string to find tags (labels) IDs.
+	 *
+	 * @param string $input
+	 * @return string
+	 */
+	private function parseLabelIds($input) {
+		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 = [];
+			foreach ($ids_lists as $ids_list) {
+				if ($ids_list === '*') {
+					$this->label_ids[] = '*';
+					break;
+				}
+				$label_ids = explode(',', $ids_list);
+				$label_ids = self::removeEmptyValues($label_ids);
+				if (!empty($label_ids)) {
+					$this->label_ids[] = $label_ids;
+				}
+			}
+		}
+		return $input;
+	}
+
+	private function parseNotLabelIds($input) {
+		if (preg_match_all('/[!-][lL]:(?P<search>[0-9,]+|[*])/', $input, $matches)) {
+			$input = str_replace($matches[0], '', $input);
 			$ids_lists = $matches['search'];
+			$this->not_label_ids = [];
+			foreach ($ids_lists as $ids_list) {
+				if ($ids_list === '*') {
+					$this->not_label_ids[] = '*';
+					break;
+				}
+				$label_ids = explode(',', $ids_list);
+				$label_ids = self::removeEmptyValues($label_ids);
+				if (!empty($label_ids)) {
+					$this->not_label_ids[] = $label_ids;
+				}
+			}
+		}
+		return $input;
+	}
+
+	/**
+	 * Parse the search string to find tags (labels) names.
+	 *
+	 * @param string $input
+	 * @return string
+	 */
+	private function parseLabelNames($input) {
+		$names_lists = [];
+		if (preg_match_all('/\blabels?:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
+			$names_lists = $matches['search'];
 			$input = str_replace($matches[0], '', $input);
-			$ids_lists = self::removeEmptyValues($ids_lists);
-			if (!empty($ids_lists[0])) {
-				$this->not_feed_ids = explode(',', $ids_lists[0]);
-				array_filter($this->not_feed_ids, function($v) { $v != ''; });
+		}
+		if (preg_match_all('/\blabels?:(?P<search>[^\s"]*)/', $input, $matches)) {
+			$names_lists = array_merge($names_lists, $matches['search']);
+			$input = str_replace($matches[0], '', $input);
+		}
+		if (!empty($names_lists)) {
+			$this->label_names = [];
+			foreach ($names_lists as $names_list) {
+				$names_array = explode(',', $names_list);
+				$names_array = self::removeEmptyValues($names_array);
+				if (!empty($names_array)) {
+					$this->label_names[] = $names_array;
+				}
+			}
+		}
+		return $input;
+	}
+
+	/**
+	 * Parse the search string to find tags (labels) names to exclude.
+	 *
+	 * @param string $input
+	 * @return string
+	 */
+	private function parseNotLabelNames($input) {
+		$names_lists = [];
+		if (preg_match_all('/[!-]labels?:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
+			$names_lists = $matches['search'];
+			$input = str_replace($matches[0], '', $input);
+		}
+		if (preg_match_all('/[!-]labels?:(?P<search>[^\s"]*)/', $input, $matches)) {
+			$names_lists = array_merge($names_lists, $matches['search']);
+			$input = str_replace($matches[0], '', $input);
+		}
+		if (!empty($names_lists)) {
+			$this->not_label_names = [];
+			foreach ($names_lists as $names_list) {
+				$names_array = explode(',', $names_list);
+				$names_array = self::removeEmptyValues($names_array);
+				if (!empty($names_array)) {
+					$this->not_label_names[] = $names_array;
+				}
 			}
 		}
 		return $input;

+ 4 - 1
docs/en/users/03_Main_view.md

@@ -183,7 +183,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: `f:123,234,345`
+* by feed ID: `f:123` or multiple feed IDs (*or*): `f:123,234,345`
 * by author: `author:name` or `author:'composed name'`
 * by title: `intitle:keyword` or `intitle:'composed keyword'`
 * by URL: `inurl:keyword` or `inurl:'composed keyword'`
@@ -222,6 +222,9 @@ You can use the search field to further refine results:
 		* `date:PT90S/` (past ninety seconds)
 		* `date:P1DT1H/` (past one day and one hour)
 * by date of publication, using the same format: `pubdate:<date-interval>`
+* by custom label ID `L:12` or multiple label IDs: `L:12,13,14` or with any label: `L:*`
+* by custom label name `label:label`, `label:"my label"` or any label name from a list (*or*): `labels:"my label,my other label"`
+* by several label names (*and*): `label:"my label" label:"my other label"`
 
 Be careful not to enter a space between the operator and the search value.
 

+ 4 - 1
docs/fr/users/03_Main_view.md

@@ -207,7 +207,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 : `f:123,234,345`
+* par ID de flux : `f:123` ou plusieurs flux (*ou*) : `f:123,234,345`
 * par auteur : `author:nom` or `author:'nom composé'`
 * par titre : `intitle:mot` or `intitle:'mot composé'`
 * par URL: `inurl:mot` or `inurl:'mot composé'`
@@ -246,6 +246,9 @@ Il est possible d’utiliser le champ de recherche pour raffiner les résultats
 		* `date:PT90S/` (past ninety seconds)
 		* `date:P1DT1H/` (past one day and one hour)
 * par date de publication, avec la même syntaxe: `pubdate:<date-interval>`
+* par ID d’étiquette : `L:12` ou de plusieurs étiquettes : `L:12,13,14` ou avec n’importe quelle étiquette : `L:*`
+* par nom d’étiquette : `label:étiquette`, `label:"mon étiquette"` ou d’une étiquette parmis une liste (*ou*) : `labels:"mon étiquette,mon autre étiquette"`
+* par plusieurs noms d’étiquettes (*et*) : `label:"mon étiquette" label:"mon autre étiquette"`
 
 Attention à ne pas introduire d’espace entre l’opérateur et la valeur
 recherchée.