Răsfoiți Sursa

Consistent entry ID type (32-bit compatibility) (#5213)

* Remove FreshRSS_Searchable for better types
The interface was not used, and it was preventing more precise types for the different `searchById()` methods, as they each have different input and output types.

* Consistent entry ID
Entry IDs (which are 64-bit integers) must be processed as string to be compatible with 32-bit platforms

* Fix type

* A few more related types

* PHPStan level 6

* Some more casts needed

* String cast for htmlspecialchars
Alexandre Alapetite 3 ani în urmă
părinte
comite
e750448f5b

+ 1 - 1
app/Controllers/feedController.php

@@ -906,7 +906,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 		]);
 
 		//Get parameters.
-		$feed_id = Minz_Request::param('id');
+		$feed_id = (int)(Minz_Request::param('id', 0));
 		$content_selector = trim(Minz_Request::param('selector'));
 
 		if (!$content_selector) {

+ 1 - 1
app/Controllers/importExportController.php

@@ -357,7 +357,7 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
 		// For each feed, check existing GUIDs already in database.
 		$existingHashForGuids = array();
 		foreach ($newFeedGuids as $feedId => $newGuids) {
-			$existingHashForGuids[$feedId] = $this->entryDAO->listHashForFeedGuids(substr($feedId, 2), $newGuids);
+			$existingHashForGuids[$feedId] = $this->entryDAO->listHashForFeedGuids((int)substr($feedId, 2), $newGuids);
 		}
 		unset($newFeedGuids);
 

+ 2 - 2
app/Controllers/indexController.php

@@ -251,10 +251,10 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
 		$get = FreshRSS_Context::currentGet(true);
 		if (is_array($get)) {
 			$type = $get[0];
-			$id = $get[1];
+			$id = (int)($get[1]);
 		} else {
 			$type = $get;
-			$id = '';
+			$id = 0;
 		}
 
 		$limit = FreshRSS_Context::$number;

+ 77 - 52
app/Models/EntryDAO.php

@@ -10,7 +10,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 		return true;
 	}
 
-	protected static function sqlConcat($s1, $s2) {
+	protected static function sqlConcat(string $s1, string $s2): string {
 		return 'CONCAT(' . $s1 . ',' . $s2 . ')';	//MySQL
 	}
 
@@ -27,7 +27,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 	}
 
 	//TODO: Move the database auto-updates to DatabaseDAO
-	protected function createEntryTempTable() {
+	protected function createEntryTempTable(): bool {
 		$ok = false;
 		$hadTransaction = $this->pdo->inTransaction();
 		if ($hadTransaction) {
@@ -46,7 +46,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 		return $ok;
 	}
 
-	private function updateToMediumBlob() {
+	private function updateToMediumBlob(): bool {
 		if ($this->pdo->dbType() !== 'mysql') {
 			return false;
 		}
@@ -65,7 +65,7 @@ SQL;
 		return $ok;
 	}
 
-	protected function addColumn(string $name) {
+	protected function addColumn(string $name): bool {
 		if ($this->pdo->inTransaction()) {
 			$this->pdo->commit();
 		}
@@ -85,7 +85,8 @@ SQL;
 	}
 
 	//TODO: Move the database auto-updates to DatabaseDAO
-	protected function autoUpdateDb(array $errorInfo) {
+	/** @param array<string> $errorInfo */
+	protected function autoUpdateDb(array $errorInfo): bool {
 		if (isset($errorInfo[0])) {
 			if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) {
 				$errorLines = explode("\n", $errorInfo[2], 2);	// The relevant column name is on the first line, other lines are noise
@@ -119,7 +120,8 @@ SQL;
 	 */
 	private $addEntryPrepared = false;
 
-	public function addEntry(array $valuesTmp, bool $useTmpTable = true) {
+	/** @param array<string,string|int> $valuesTmp */
+	public function addEntry(array $valuesTmp, bool $useTmpTable = true): bool {
 		if ($this->addEntryPrepared == null) {
 			$sql = static::sqlIgnoreConflict(
 				'INSERT INTO `_' . ($useTmpTable ? 'entrytmp' : 'entry') . '` (id, guid, title, author, '
@@ -190,7 +192,7 @@ SQL;
 		}
 	}
 
-	public function commitNewEntries() {
+	public function commitNewEntries(): bool {
 		$sql = <<<'SQL'
 SET @rank=(SELECT MAX(id) - COUNT(*) FROM `_entrytmp`);
 
@@ -215,9 +217,11 @@ SQL;
 		return $result;
 	}
 
+	/** @var PDOStatement|null */
 	private $updateEntryPrepared = null;
 
-	public function updateEntry(array $valuesTmp) {
+	/** @param array<string,string|int> $valuesTmp */
+	public function updateEntry(array $valuesTmp): bool {
 		if (!isset($valuesTmp['is_read'])) {
 			$valuesTmp['is_read'] = null;
 		}
@@ -303,7 +307,7 @@ SQL;
 	 * @todo simplify the query by removing the str_repeat. I am pretty sure
 	 * there is an other way to do that.
 	 *
-	 * @param integer|array $ids
+	 * @param string|array<string> $ids
 	 * @return false|integer
 	 */
 	public function markFavorite($ids, bool $is_favorite = true) {
@@ -392,7 +396,7 @@ SQL;
 	 * @todo remove code duplication. It seems the code is basically the
 	 * same if it is an array or not.
 	 *
-	 * @param integer|array $ids
+	 * @param string|array<string> $ids
 	 * @param boolean $is_read
 	 * @return integer|false affected rows
 	 */
@@ -465,12 +469,10 @@ SQL;
 	 * separated.
 	 *
 	 * @param string $idMax fail safe article ID
-	 * @param boolean $onlyFavorites
-	 * @param integer $priorityMin
-	 * @param FreshRSS_BooleanSearch|null $filters
 	 * @return integer|false affected rows
 	 */
-	public function markReadEntries(string $idMax = '0', bool $onlyFavorites = false, int $priorityMin = 0, $filters = null, int $state = 0, bool $is_read = true) {
+	public function markReadEntries(string $idMax = '0', bool $onlyFavorites = false, int $priorityMin = 0,
+		?FreshRSS_BooleanSearch $filters = null, int $state = 0, bool $is_read = true) {
 		FreshRSS_UserDAO::touch();
 		if ($idMax == 0) {
 			$idMax = time() . '000000';
@@ -511,10 +513,9 @@ SQL;
 	 *
 	 * @param integer $id category ID
 	 * @param string $idMax fail safe article ID
-	 * @param FreshRSS_BooleanSearch|null $filters
 	 * @return integer|false affected rows
 	 */
-	public function markReadCat(int $id, string $idMax = '0', $filters = null, int $state = 0, bool $is_read = true) {
+	public function markReadCat(int $id, string $idMax = '0', ?FreshRSS_BooleanSearch $filters = null, int $state = 0, bool $is_read = true) {
 		FreshRSS_UserDAO::touch();
 		if ($idMax == '0') {
 			$idMax = time() . '000000';
@@ -550,10 +551,9 @@ SQL;
 	 *
 	 * @param integer $id_feed feed ID
 	 * @param string $idMax fail safe article ID
-	 * @param FreshRSS_BooleanSearch|null $filters
 	 * @return integer|false affected rows
 	 */
-	public function markReadFeed(int $id_feed, string $idMax = '0', $filters = null, int $state = 0, bool $is_read = true) {
+	public function markReadFeed(int $id_feed, string $idMax = '0', ?FreshRSS_BooleanSearch $filters = null, int $state = 0, bool $is_read = true) {
 		FreshRSS_UserDAO::touch();
 		if ($idMax == '0') {
 			$idMax = time() . '000000';
@@ -601,7 +601,8 @@ SQL;
 	 * @param string $idMax max article ID
 	 * @return integer|false affected rows
 	 */
-	public function markReadTag($id = 0, string $idMax = '0', $filters = null, int $state = 0, bool $is_read = true) {
+	public function markReadTag(int $id = 0, string $idMax = '0', ?FreshRSS_BooleanSearch $filters = null,
+		int $state = 0, bool $is_read = true) {
 		FreshRSS_UserDAO::touch();
 		if ($idMax == '0') {
 			$idMax = time() . '000000';
@@ -637,8 +638,10 @@ SQL;
 
 	/**
 	 * Remember to call updateCachedValue($id_feed) or updateCachedValues() just after.
+	 * @param array<string,int|bool|string> $options
+	 * @return int|false
 	 */
-	public function cleanOldEntries($id_feed, $options = []) {
+	public function cleanOldEntries(int $id_feed, array $options = []) {
 		$sql = 'DELETE FROM `_entry` WHERE id_feed = :id_feed1';	//No alias for MySQL / MariaDB
 		$params = [];
 		$params[':id_feed1'] = $id_feed;
@@ -697,6 +700,7 @@ SQL;
 		}
 	}
 
+	/** @return iterator<array<string,mixed>> */
 	public function selectAll() {
 		$sql = 'SELECT id, guid, title, author, '
 			. (static::isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
@@ -711,14 +715,13 @@ SQL;
 			$info = $this->pdo->errorInfo();
 			if ($this->autoUpdateDb($info)) {
 				yield from $this->selectAll();
+			} else {
+				Minz_Log::error(__method__ . ' error: ' . json_encode($info));
 			}
-			Minz_Log::error(__method__ . ' error: ' . json_encode($info));
-			yield false;
 		}
 	}
 
-	/** @return FreshRSS_Entry|null */
-	public function searchByGuid($id_feed, $guid) {
+	public function searchByGuid(int $id_feed, string $guid): ?FreshRSS_Entry {
 		// un guid est unique pour un flux donné
 		$sql = 'SELECT id, guid, title, author, '
 			. (static::isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content')
@@ -744,7 +747,7 @@ SQL;
 		return isset($res[0]) ? FreshRSS_Entry::fromArray($res[0]) : null;
 	}
 
-	public function searchIdByGuid($id_feed, $guid) {
+	public function searchIdByGuid(int $id_feed, string $guid): ?string {
 		$sql = 'SELECT id FROM `_entry` WHERE id_feed=:id_feed AND guid=:guid';
 		$stm = $this->pdo->prepare($sql);
 		$stm->bindParam(':id_feed', $id_feed, PDO::PARAM_INT);
@@ -754,8 +757,8 @@ SQL;
 		return isset($res[0]) ? $res[0] : null;
 	}
 
-	/** @param FreshRSS_BooleanSearch $filters */
-	public static function sqlBooleanSearch(string $alias, $filters, int $level = 0) {
+	/** @return array{0:array<int|string>,1:string} */
+	public static function sqlBooleanSearch(string $alias, FreshRSS_BooleanSearch $filters, int $level = 0) {
 		$search = '';
 		$values = [];
 
@@ -1021,8 +1024,9 @@ SQL;
 		return [ $values, $search ];
 	}
 
-	/** @param FreshRSS_BooleanSearch|null $filters */
-	protected function sqlListEntriesWhere(string $alias = '', $filters = null, int $state = FreshRSS_Entry::STATE_ALL,
+	/** @return array{0:array<int|string>,1:string} */
+	protected function sqlListEntriesWhere(string $alias = '', ?FreshRSS_BooleanSearch $filters = null,
+			int $state = FreshRSS_Entry::STATE_ALL,
 			string $order = 'DESC', string $firstId = '', int $date_min = 0) {
 		$search = ' ';
 		$values = array();
@@ -1067,13 +1071,17 @@ SQL;
 		return array($values, $search);
 	}
 
-	private function sqlListWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL,
-			$order = 'DESC', $limit = 1, $firstId = '', $filters = null, $date_min = 0) {
+	/**
+	 * @param int $id category/feed/tag ID
+	 * @return array{0:array<int|string>,1:string}
+	 */
+	private function sqlListWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
+			string $order = 'DESC', int $limit = 1, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null,
+			int $date_min = 0) {
 		if (!$state) {
 			$state = FreshRSS_Entry::STATE_ALL;
 		}
 		$where = '';
-		$joinFeed = false;
 		$values = array();
 		switch ($type) {
 		case 'a':	//All PRIORITY_MAIN_STREAM
@@ -1092,15 +1100,15 @@ SQL;
 		case 'c':	//Category
 			$where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_NORMAL . ' ';
 			$where .= 'AND f.category=? ';
-			$values[] = intval($id);
+			$values[] = $id;
 			break;
 		case 'f':	//Feed
 			$where .= 'e.id_feed=? ';
-			$values[] = intval($id);
+			$values[] = $id;
 			break;
 		case 't':	//Tag (label)
 			$where .= 'et.id_tag=? ';
-			$values[] = intval($id);
+			$values[] = $id;
 			break;
 		case 'T':	//Any tag (label)
 			$where .= '1=1 ';
@@ -1126,8 +1134,13 @@ SQL;
 			. ($limit > 0 ? ' LIMIT ' . intval($limit) : ''));	//TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/
 	}
 
-	private function listWhereRaw($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL,
-			$order = 'DESC', $limit = 1, $firstId = '', $filters = null, $date_min = 0) {
+	/**
+	 * @param int $id category/feed/tag ID
+	 * @return PDOStatement|false
+	 */
+	private function listWhereRaw(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
+			string $order = 'DESC', int $limit = 1, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null,
+			int $date_min = 0) {
 		list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
 
 		$sql = 'SELECT e0.id, e0.guid, e0.title, e0.author, '
@@ -1152,19 +1165,25 @@ SQL;
 		}
 	}
 
-	public function listWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL,
-			$order = 'DESC', $limit = 1, $firstId = '', $filters = null, $date_min = 0) {
+	/**
+	 * @param int $id category/feed/tag ID
+	 * @return iterable<FreshRSS_Entry>
+	 */
+	public function listWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
+			string $order = 'DESC', int $limit = 1, string $firstId = '',
+			?FreshRSS_BooleanSearch $filters = null, int $date_min = 0) {
 		$stm = $this->listWhereRaw($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
 		if ($stm) {
 			while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
 				yield FreshRSS_Entry::fromArray($row);
 			}
-		} else {
-			yield false;
 		}
 	}
 
-	/** @param array<string> $ids */
+	/**
+	 * @param array<string> $ids
+	 * @return iterable<FreshRSS_Entry>
+	 */
 	public function listByIds(array $ids, string $order = 'DESC') {
 		if (count($ids) < 1) {
 			return;
@@ -1196,10 +1215,11 @@ SQL;
 
 	/**
 	 * For API
+	 * @param int $id category/feed/tag ID
 	 * @return array<string>|false
 	 */
-	public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL,
-								 $order = 'DESC', $limit = 1, $firstId = '', $filters = null) {
+	public function listIdsWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
+		string $order = 'DESC', int $limit = 1, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null) {
 		[$values, $sql] = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters);
 
 		$stm = $this->pdo->prepare($sql);
@@ -1208,7 +1228,11 @@ SQL;
 		return $stm->fetchAll(PDO::FETCH_COLUMN, 0) ?: [];
 	}
 
-	public function listHashForFeedGuids($id_feed, $guids) {
+	/**
+	 * @param array<string> $guids
+	 * @return array<string>|false
+	 */
+	public function listHashForFeedGuids(int $id_feed, array $guids) {
 		$result = [];
 		if (count($guids) < 1) {
 			return $result;
@@ -1244,12 +1268,10 @@ SQL;
 	}
 
 	/**
-	 * @param int $id_feed
 	 * @param array<string> $guids
-	 * @param int $mtime
 	 * @return int|false The number of affected feeds, or false if error
 	 */
-	public function updateLastSeen($id_feed, $guids, $mtime = 0) {
+	public function updateLastSeen(int $id_feed, array $guids, int $mtime = 0) {
 		if (count($guids) < 1) {
 			return 0;
 		} elseif (count($guids) > FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER) {
@@ -1281,6 +1303,7 @@ SQL;
 		}
 	}
 
+	/** @return array<string,int>|false */
 	public function countUnreadRead() {
 		$sql = 'SELECT COUNT(e.id) AS count FROM `_entry` e INNER JOIN `_feed` f ON e.id_feed=f.id WHERE f.priority > 0'
 			. ' UNION SELECT COUNT(e.id) AS count FROM `_entry` e INNER JOIN `_feed` f ON e.id_feed=f.id WHERE f.priority > 0 AND e.is_read=0';
@@ -1295,11 +1318,12 @@ SQL;
 		return array('all' => $all, 'unread' => $unread, 'read' => $all - $unread);
 	}
 
-	public function count($minPriority = null) {
+	/** @return int|false */
+	public function count(?int $minPriority = null) {
 		$sql = 'SELECT COUNT(e.id) AS count FROM `_entry` e';
 		if ($minPriority !== null) {
 			$sql .= ' INNER JOIN `_feed` f ON e.id_feed=f.id';
-			$sql .= ' WHERE f.priority > ' . intval($minPriority);
+			$sql .= ' WHERE f.priority > ' . $minPriority;
 		}
 		$stm = $this->pdo->query($sql);
 		if ($stm == false) {
@@ -1309,20 +1333,21 @@ SQL;
 		return isset($res[0]) ? intval($res[0]) : 0;
 	}
 
-	public function countNotRead($minPriority = null) {
+	public function countNotRead(?int $minPriority = null): int {
 		$sql = 'SELECT COUNT(e.id) AS count FROM `_entry` e';
 		if ($minPriority !== null) {
 			$sql .= ' INNER JOIN `_feed` f ON e.id_feed=f.id';
 		}
 		$sql .= ' WHERE e.is_read=0';
 		if ($minPriority !== null) {
-			$sql .= ' AND f.priority > ' . intval($minPriority);
+			$sql .= ' AND f.priority > ' . $minPriority;
 		}
 		$stm = $this->pdo->query($sql);
 		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
 		return isset($res[0]) ? intval($res[0]) : 0;
 	}
 
+	/** @return array<string,int>|false */
 	public function countUnreadReadFavorites() {
 		$sql = <<<'SQL'
 SELECT c FROM (

+ 3 - 2
app/Models/EntryDAOPGSQL.php

@@ -18,7 +18,8 @@ class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite {
 		return rtrim($sql, ' ;') . ' ON CONFLICT DO NOTHING';
 	}
 
-	protected function autoUpdateDb(array $errorInfo) {
+	/** @param array<string> $errorInfo */
+	protected function autoUpdateDb(array $errorInfo): bool {
 		if (isset($errorInfo[0])) {
 			if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) {
 				$errorLines = explode("\n", $errorInfo[2], 2);	// The relevant column name is on the first line, other lines are noise
@@ -40,7 +41,7 @@ class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite {
 		return false;
 	}
 
-	public function commitNewEntries() {
+	public function commitNewEntries(): bool {
 		//TODO: Update to PostgreSQL 9.5+ syntax with ON CONFLICT DO NOTHING
 		$sql = 'DO $$
 DECLARE

+ 9 - 9
app/Models/EntryDAOSQLite.php

@@ -10,7 +10,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 		return false;
 	}
 
-	protected static function sqlConcat($s1, $s2) {
+	protected static function sqlConcat(string $s1, string $s2): string {
 		return $s1 . '||' . $s2;
 	}
 
@@ -22,7 +22,8 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 		return str_replace('INSERT INTO ', 'INSERT OR IGNORE INTO ', $sql);
 	}
 
-	protected function autoUpdateDb(array $errorInfo) {
+	/** @param array<string> $errorInfo */
+	protected function autoUpdateDb(array $errorInfo): bool {
 		if ($tableInfo = $this->pdo->query("PRAGMA table_info('entry')")) {
 			$columns = $tableInfo->fetchAll(PDO::FETCH_COLUMN, 1);
 			foreach (['attributes'] as $column) {
@@ -47,7 +48,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 		return false;
 	}
 
-	public function commitNewEntries() {
+	public function commitNewEntries(): bool {
 		$sql = '
 DROP TABLE IF EXISTS `tmp`;
 CREATE TEMP TABLE `tmp` AS
@@ -115,7 +116,7 @@ DROP TABLE IF EXISTS `tmp`;
 	 * @todo remove code duplication. It seems the code is basically the
 	 * same if it is an array or not.
 	 *
-	 * @param integer|array $ids
+	 * @param string|array<string> $ids
 	 * @param boolean $is_read
 	 * @return integer|false affected rows
 	 */
@@ -177,10 +178,10 @@ DROP TABLE IF EXISTS `tmp`;
 	 * @param string $idMax fail safe article ID
 	 * @param boolean $onlyFavorites
 	 * @param integer $priorityMin
-	 * @param FreshRSS_BooleanSearch|null $filters
 	 * @return integer|false affected rows
 	 */
-	public function markReadEntries(string $idMax = '0', bool $onlyFavorites = false, int $priorityMin = 0, $filters = null, int $state = 0, bool $is_read = true) {
+	public function markReadEntries(string $idMax = '0', bool $onlyFavorites = false, int $priorityMin = 0,
+		?FreshRSS_BooleanSearch $filters = null, int $state = 0, bool $is_read = true) {
 		FreshRSS_UserDAO::touch();
 		if ($idMax == '0') {
 			$idMax = time() . '000000';
@@ -219,10 +220,9 @@ DROP TABLE IF EXISTS `tmp`;
 	 *
 	 * @param integer $id category ID
 	 * @param string $idMax fail safe article ID
-	 * @param FreshRSS_BooleanSearch|null $filters
 	 * @return integer|false affected rows
 	 */
-	public function markReadCat(int $id, string $idMax = '0', $filters = null, int $state = 0, bool $is_read = true) {
+	public function markReadCat(int $id, string $idMax = '0', ?FreshRSS_BooleanSearch $filters = null, int $state = 0, bool $is_read = true) {
 		FreshRSS_UserDAO::touch();
 		if ($idMax == '0') {
 			$idMax = time() . '000000';
@@ -256,7 +256,7 @@ DROP TABLE IF EXISTS `tmp`;
 	 * @param string $idMax max article ID
 	 * @return integer|false affected rows
 	 */
-	public function markReadTag($id = 0, string $idMax = '0', $filters = null, int $state = 0, bool $is_read = true) {
+	public function markReadTag($id = 0, string $idMax = '0', ?FreshRSS_BooleanSearch $filters = null, int $state = 0, bool $is_read = true) {
 		FreshRSS_UserDAO::touch();
 		if ($idMax == 0) {
 			$idMax = time() . '000000';

+ 2 - 2
app/Services/ExportService.php

@@ -76,12 +76,12 @@ class FreshRSS_Export_Service {
 		$view->list_title = _t('sub.import_export.starred_list');
 		$view->type = 'starred';
 		$entriesId = $this->entry_dao->listIdsWhere(
-			$type, '', FreshRSS_Entry::STATE_ALL, 'ASC', -1
+			$type, 0, FreshRSS_Entry::STATE_ALL, 'ASC', -1
 		);
 		$view->entryIdsTagNames = $this->tag_dao->getEntryIdsTagNames($entriesId);
 		// The following is a streamable query, i.e. must be last
 		$view->entries = $this->entry_dao->listWhere(
-			$type, '', FreshRSS_Entry::STATE_ALL, 'ASC', -1
+			$type, 0, FreshRSS_Entry::STATE_ALL, 'ASC', -1
 		);
 
 		return [

+ 10 - 10
p/api/fever.php

@@ -227,8 +227,8 @@ final class FeverAPI
 		}
 
 		if (isset($_REQUEST['mark'], $_REQUEST['as'], $_REQUEST['id']) && ctype_digit($_REQUEST['id'])) {
-			$id = intval($_REQUEST['id']);
-			$before = intval($_REQUEST['before'] ?? '0');
+			$id = (string)$_REQUEST['id'];
+			$before = (int)($_REQUEST['before'] ?? '0');
 			switch (strtolower($_REQUEST['mark'])) {
 				case 'item':
 					switch ($_REQUEST['as']) {
@@ -249,14 +249,14 @@ final class FeverAPI
 				case 'feed':
 					switch ($_REQUEST['as']) {
 						case 'read':
-							$this->setFeedAsRead($id, $before);
+							$this->setFeedAsRead((int)$id, $before);
 							break;
 					}
 					break;
 				case 'group':
 					switch ($_REQUEST['as']) {
 						case 'read':
-							$this->setGroupAsRead($id, $before);
+							$this->setGroupAsRead((int)$id, $before);
 							break;
 					}
 					break;
@@ -420,40 +420,40 @@ final class FeverAPI
 	}
 
 	private function getUnreadItemIds(): string {
-		$entries = $this->entryDAO->listIdsWhere('a', '', FreshRSS_Entry::STATE_NOT_READ, 'ASC', 0) ?: [];
+		$entries = $this->entryDAO->listIdsWhere('a', 0, FreshRSS_Entry::STATE_NOT_READ, 'ASC', 0) ?: [];
 		return $this->entriesToIdList($entries);
 	}
 
 	private function getSavedItemIds(): string {
-		$entries = $this->entryDAO->listIdsWhere('a', '', FreshRSS_Entry::STATE_FAVORITE, 'ASC', 0) ?: [];
+		$entries = $this->entryDAO->listIdsWhere('a', 0, FreshRSS_Entry::STATE_FAVORITE, 'ASC', 0) ?: [];
 		return $this->entriesToIdList($entries);
 	}
 
 	/**
 	 * @return integer|false
 	 */
-	private function setItemAsRead(int $id) {
+	private function setItemAsRead(string $id) {
 		return $this->entryDAO->markRead($id, true);
 	}
 
 	/**
 	 * @return integer|false
 	 */
-	private function setItemAsUnread(int $id) {
+	private function setItemAsUnread(string $id) {
 		return $this->entryDAO->markRead($id, false);
 	}
 
 	/**
 	 * @return integer|false
 	 */
-	private function setItemAsSaved(int $id) {
+	private function setItemAsSaved(string $id) {
 		return $this->entryDAO->markFavorite($id, true);
 	}
 
 	/**
 	 * @return integer|false
 	 */
-	private function setItemAsUnsaved(int $id) {
+	private function setItemAsUnsaved(string $id) {
 		return $this->entryDAO->markFavorite($id, false);
 	}
 

+ 7 - 8
p/api/greader.php

@@ -385,10 +385,6 @@ final class GReaderAPI {
 			self::badRequest();
 		}
 		$addCatId = 0;
-		$categoryDAO = null;
-		if ($add != '' || $remove != '') {
-			$categoryDAO = FreshRSS_Factory::createCategoryDao();
-		}
 		$c_name = '';
 		if ($add != '' && strpos($add, 'user/') === 0) {	//user/-/label/Example ; user/username/label/Example
 			if (strpos($add, 'user/-/label/') === 0) {
@@ -403,6 +399,7 @@ final class GReaderAPI {
 				}
 			}
 			$c_name = htmlspecialchars($c_name, ENT_COMPAT, 'UTF-8');
+			$categoryDAO = FreshRSS_Factory::createCategoryDao();
 			$cat = $categoryDAO->searchByName($c_name);
 			$addCatId = $cat == null ? 0 : $cat->id();
 		} elseif ($remove != '' && strpos($remove, 'user/-/label/') === 0) {
@@ -586,13 +583,14 @@ final class GReaderAPI {
 	}
 
 	/**
-	 * @return array<string|int|FreshRSS_BooleanSearch>
+	 * @param string|int $streamId
+	 * @return array{string,int,int,FreshRSS_BooleanSearch}
 	 */
-	private static function streamContentsFilters(string $type, string $streamId,
+	private static function streamContentsFilters(string $type, $streamId,
 		string $filter_target, string $exclude_target, int $start_time, int $stop_time): array {
 		switch ($type) {
 			case 'f':	//feed
-				if ($streamId != '' && !ctype_digit($streamId)) {
+				if ($streamId != '' && is_string($streamId) && !ctype_digit($streamId)) {
 					$feedDAO = FreshRSS_Factory::createFeedDao();
 					$streamId = htmlspecialchars($streamId, ENT_COMPAT, 'UTF-8');
 					$feed = $feedDAO->searchByUrl($streamId);
@@ -601,7 +599,7 @@ final class GReaderAPI {
 				break;
 			case 'c':	//category or label
 				$categoryDAO = FreshRSS_Factory::createCategoryDao();
-				$streamId = htmlspecialchars($streamId, ENT_COMPAT, 'UTF-8');
+				$streamId = htmlspecialchars((string)$streamId, ENT_COMPAT, 'UTF-8');
 				$cat = $categoryDAO->searchByName($streamId);
 				if ($cat != null) {
 					$type = 'c';
@@ -619,6 +617,7 @@ final class GReaderAPI {
 				}
 				break;
 		}
+		$streamId = (int)$streamId;
 
 		switch ($filter_target) {
 			case 'user/-/state/com.google/read':