Jelajahi Sumber

Fix serialisation of dates in searches (#8341)

Dates should not be expanded/resolved in string serialisations of search expressions, as it otherwise break relative user queries such as `P30D`.
Fix of https://github.com/FreshRSS/FreshRSS/pull/8293
Alexandre Alapetite 3 bulan lalu
induk
melakukan
00c61cf34c
2 mengubah file dengan 73 tambahan dan 60 penghapusan
  1. 69 56
      app/Models/Search.php
  2. 4 4
      tests/app/Models/SearchTest.php

+ 69 - 56
app/Models/Search.php

@@ -35,18 +35,15 @@ class FreshRSS_Search implements \Stringable {
 	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 */
-	private $max_date = null;
-	/** @var int|false|null */
-	private $min_pubdate = null;
-	/** @var int|false|null */
-	private $max_pubdate = null;
-	/** @var int|false|null */
-	private $min_userdate = null;
-	/** @var int|false|null */
-	private $max_userdate = null;
+	private ?string $input_date = null;
+	private int|false|null $min_date = null;
+	private int|false|null $max_date = null;
+	private ?string $input_pubdate = null;
+	private int|false|null $min_pubdate = null;
+	private int|false|null $max_pubdate = null;
+	private ?string $input_userdate = null;
+	private int|false|null $min_userdate = null;
+	private int|false|null $max_userdate = null;
 	/** @var list<string>|null */
 	private ?array $inurl = null;
 	/** @var list<string>|null */
@@ -82,18 +79,15 @@ class FreshRSS_Search implements \Stringable {
 	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 */
-	private $not_max_date = null;
-	/** @var int|false|null */
-	private $not_min_pubdate = null;
-	/** @var int|false|null */
-	private $not_max_pubdate = null;
-	/** @var int|false|null */
-	private $not_min_userdate = null;
-	/** @var int|false|null */
-	private $not_max_userdate = null;
+	private ?string $input_not_date = null;
+	private int|false|null $not_min_date = null;
+	private int|false|null $not_max_date = null;
+	private ?string $input_not_pubdate = null;
+	private int|false|null $not_min_pubdate = null;
+	private int|false|null $not_max_pubdate = null;
+	private ?string $input_not_userdate = null;
+	private int|false|null $not_min_userdate = null;
+	private int|false|null $not_max_userdate = null;
 	/** @var list<string>|null */
 	private ?array $not_inurl = null;
 	/** @var list<string>|null */
@@ -161,26 +155,27 @@ class FreshRSS_Search implements \Stringable {
 		return $s;
 	}
 
-	private static function dateIntervalToString(int|false|null $min, int|false|null $max): string {
-		if ($min === false) {
-			$min = null;
-		}
-		if ($max === false) {
-			$max = null;
-		}
-		if ($min === null && $max === null) {
-			return '';
-		}
-		$s = '';
-		if ($min !== null) {
-			$s .= date('Y-m-d\\TH:i:s', $min);
-		}
-		$s .= '/';
-		if ($max !== null) {
-			$s .= date('Y-m-d\\TH:i:s', $max);
-		}
-		return $s;
-	}
+	// TODO: Reuse as option for a string representation resolving and expanding date intervals
+	// private static function dateIntervalToString(int|false|null $min, int|false|null $max): string {
+	// 	if ($min === false) {
+	// 		$min = null;
+	// 	}
+	// 	if ($max === false) {
+	// 		$max = null;
+	// 	}
+	// 	if ($min === null && $max === null) {
+	// 		return '';
+	// 	}
+	// 	$s = '';
+	// 	if ($min !== null) {
+	// 		$s .= date('Y-m-d\\TH:i:s', $min);
+	// 	}
+	// 	$s .= '/';
+	// 	if ($max !== null) {
+	// 		$s .= date('Y-m-d\\TH:i:s', $max);
+	// 	}
+	// 	return $s;
+	// }
 
 	/**
 	 * Return true if both searches have the same constraint parameters (even if the values differ), false otherwise.
@@ -287,14 +282,14 @@ class FreshRSS_Search implements \Stringable {
 			}
 		}
 
-		if ($this->min_userdate !== null || $this->max_userdate !== null) {
-			$result .= ' userdate:' . self::dateIntervalToString($this->min_userdate, $this->max_userdate);
+		if ($this->input_userdate !== null) {
+			$result .= ' userdate:' . $this->input_userdate;
 		}
-		if ($this->min_pubdate !== null || $this->max_pubdate !== null) {
-			$result .= ' pubdate:' . self::dateIntervalToString($this->min_pubdate, $this->max_pubdate);
+		if ($this->input_pubdate !== null) {
+			$result .= ' pubdate:' . $this->input_pubdate;
 		}
-		if ($this->min_date !== null || $this->max_date !== null) {
-			$result .= ' date:' . self::dateIntervalToString($this->min_date, $this->max_date);
+		if ($this->input_date !== null) {
+			$result .= ' date:' . $this->input_date;
 		}
 
 		if ($this->intitle_regex !== null) {
@@ -378,14 +373,14 @@ class FreshRSS_Search implements \Stringable {
 			}
 		}
 
-		if ($this->not_min_userdate !== null || $this->not_max_userdate !== null) {
-			$result .= ' -userdate:' . self::dateIntervalToString($this->not_min_userdate, $this->not_max_userdate);
+		if ($this->input_not_userdate !== null) {
+			$result .= ' -userdate:' . $this->input_not_userdate;
 		}
-		if ($this->not_min_pubdate !== null || $this->not_max_pubdate !== null) {
-			$result .= ' -pubdate:' . self::dateIntervalToString($this->not_min_pubdate, $this->not_max_pubdate);
+		if ($this->input_not_pubdate !== null) {
+			$result .= ' -pubdate:' . $this->input_not_pubdate;
 		}
-		if ($this->not_min_date !== null || $this->not_max_date !== null) {
-			$result .= ' -date:' . self::dateIntervalToString($this->not_min_date, $this->not_max_date);
+		if ($this->input_not_date !== null) {
+			$result .= ' -date:' . $this->input_not_date;
 		}
 
 		if ($this->not_intitle_regex !== null) {
@@ -1069,6 +1064,9 @@ class FreshRSS_Search implements \Stringable {
 			$dates = self::removeEmptyValues($matches['search']);
 			if (!empty($dates[0])) {
 				[$this->min_date, $this->max_date] = parseDateInterval($dates[0]);
+				if (is_int($this->min_date) || is_int($this->max_date)) {
+					$this->input_date = $dates[0];
+				}
 			}
 		}
 		return $input;
@@ -1080,6 +1078,9 @@ class FreshRSS_Search implements \Stringable {
 			$dates = self::removeEmptyValues($matches['search']);
 			if (!empty($dates[0])) {
 				[$this->not_min_date, $this->not_max_date] = parseDateInterval($dates[0]);
+				if (is_int($this->not_min_date) || is_int($this->not_max_date)) {
+					$this->input_not_date = $dates[0];
+				}
 			}
 		}
 		return $input;
@@ -1096,6 +1097,9 @@ class FreshRSS_Search implements \Stringable {
 			$dates = self::removeEmptyValues($matches['search']);
 			if (!empty($dates[0])) {
 				[$this->min_pubdate, $this->max_pubdate] = parseDateInterval($dates[0]);
+				if (is_int($this->min_pubdate) || is_int($this->max_pubdate)) {
+					$this->input_pubdate = $dates[0];
+				}
 			}
 		}
 		return $input;
@@ -1107,6 +1111,9 @@ class FreshRSS_Search implements \Stringable {
 			$dates = self::removeEmptyValues($matches['search']);
 			if (!empty($dates[0])) {
 				[$this->not_min_pubdate, $this->not_max_pubdate] = parseDateInterval($dates[0]);
+				if (is_int($this->not_min_pubdate) || is_int($this->not_max_pubdate)) {
+					$this->input_not_pubdate = $dates[0];
+				}
 			}
 		}
 		return $input;
@@ -1122,6 +1129,9 @@ class FreshRSS_Search implements \Stringable {
 			$dates = self::removeEmptyValues($matches['search']);
 			if (!empty($dates[0])) {
 				[$this->min_userdate, $this->max_userdate] = parseDateInterval($dates[0]);
+				if (is_int($this->min_userdate) || is_int($this->max_userdate)) {
+					$this->input_userdate = $dates[0];
+				}
 			}
 		}
 		return $input;
@@ -1133,6 +1143,9 @@ class FreshRSS_Search implements \Stringable {
 			$dates = self::removeEmptyValues($matches['search']);
 			if (!empty($dates[0])) {
 				[$this->not_min_userdate, $this->not_max_userdate] = parseDateInterval($dates[0]);
+				if (is_int($this->not_min_userdate) || is_int($this->not_max_userdate)) {
+					$this->input_not_userdate = $dates[0];
+				}
 			}
 		}
 		return $input;

+ 4 - 4
tests/app/Models/SearchTest.php

@@ -956,8 +956,8 @@ final class SearchTest extends \PHPUnit\Framework\TestCase {
 					/search_regex/i "quoted search" search
 					-e:3,4 -f:12,13 -c:22,23 -L:32,33 -labels:"Not label,Not other label"
 					-userdate:2025-06-01T00:00:00/2025-09-01T00:00:00
-					-pubdate:2025-06-01T00:00:00/2025-09-01T00:00:00
-					-date:2025-06-01T00:00:00/2025-09-01T00:00:00
+					-pubdate:2025
+					-date:P30D
 					-intitle:/Spam/i -intitle:"'bad"
 					-intext:/Spam/i -intext:"'bad"
 					-author:/Dave/i -author:Charlie
@@ -1070,8 +1070,8 @@ final class SearchTest extends \PHPUnit\Framework\TestCase {
 			['(a b) OR (c d)', 'e', 'e ((a b) OR (c d))'],
 			['(a b) (c d)', 'e', 'e ((a b) (c d))'],
 			['(a b)', 'e', 'e (a b)'],
-			['date:2024/', 'date:/2025', 'date:/2025-12-31T23:59:59'],
-			['a', 'date:/2025', 'date:/2025-12-31T23:59:59 a'],
+			['date:2024/', 'date:/2025', 'date:/2025'],
+			['a', 'date:/2025', 'date:/2025 a'],
 		];
 	}