FeedDAO.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  1. <?php
  2. declare(strict_types=1);
  3. class FreshRSS_FeedDAO extends Minz_ModelPdo {
  4. protected function addColumn(string $name): bool {
  5. if ($this->pdo->inTransaction()) {
  6. $this->pdo->commit();
  7. }
  8. Minz_Log::warning(__method__ . ': ' . $name);
  9. try {
  10. if ($name === 'kind') { //v1.20.0
  11. return $this->pdo->exec('ALTER TABLE `_feed` ADD COLUMN kind SMALLINT DEFAULT 0') !== false;
  12. }
  13. } catch (Exception $e) {
  14. Minz_Log::error(__method__ . ' error: ' . $e->getMessage());
  15. }
  16. return false;
  17. }
  18. /** @param array<int|string> $errorInfo */
  19. protected function autoUpdateDb(array $errorInfo): bool {
  20. if (isset($errorInfo[0])) {
  21. if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) {
  22. $errorLines = explode("\n", (string)$errorInfo[2], 2); // The relevant column name is on the first line, other lines are noise
  23. foreach (['kind'] as $column) {
  24. if (stripos($errorLines[0], $column) !== false) {
  25. return $this->addColumn($column);
  26. }
  27. }
  28. }
  29. }
  30. return false;
  31. }
  32. /**
  33. * @param array{'url':string,'kind':int,'category':int,'name':string,'website':string,'description':string,'lastUpdate':int,'priority'?:int,
  34. * 'pathEntries'?:string,'httpAuth':string,'error':int|bool,'ttl'?:int,'attributes'?:string|array<string|mixed>} $valuesTmp
  35. * @return int|false
  36. */
  37. public function addFeed(array $valuesTmp) {
  38. $sql = 'INSERT INTO `_feed` (url, kind, category, name, website, description, `lastUpdate`, priority, `pathEntries`, `httpAuth`, error, ttl, attributes)
  39. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
  40. $stm = $this->pdo->prepare($sql);
  41. $valuesTmp['url'] = safe_ascii($valuesTmp['url']);
  42. $valuesTmp['website'] = safe_ascii($valuesTmp['website']);
  43. if (!isset($valuesTmp['pathEntries'])) {
  44. $valuesTmp['pathEntries'] = '';
  45. }
  46. if (!isset($valuesTmp['attributes'])) {
  47. $valuesTmp['attributes'] = [];
  48. }
  49. $values = [
  50. $valuesTmp['url'],
  51. $valuesTmp['kind'] ?? FreshRSS_Feed::KIND_RSS,
  52. $valuesTmp['category'],
  53. mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8'),
  54. $valuesTmp['website'],
  55. sanitizeHTML($valuesTmp['description'], ''),
  56. $valuesTmp['lastUpdate'],
  57. isset($valuesTmp['priority']) ? (int)$valuesTmp['priority'] : FreshRSS_Feed::PRIORITY_MAIN_STREAM,
  58. mb_strcut($valuesTmp['pathEntries'], 0, 4096, 'UTF-8'),
  59. base64_encode($valuesTmp['httpAuth']),
  60. isset($valuesTmp['error']) ? (int)$valuesTmp['error'] : 0,
  61. isset($valuesTmp['ttl']) ? (int)$valuesTmp['ttl'] : FreshRSS_Feed::TTL_DEFAULT,
  62. is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
  63. ];
  64. if ($stm !== false && $stm->execute($values)) {
  65. $feedId = $this->pdo->lastInsertId('`_feed_id_seq`');
  66. return $feedId === false ? false : (int)$feedId;
  67. } else {
  68. $info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
  69. if ($this->autoUpdateDb($info)) {
  70. return $this->addFeed($valuesTmp);
  71. }
  72. Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
  73. return false;
  74. }
  75. }
  76. /** @return int|false */
  77. public function addFeedObject(FreshRSS_Feed $feed) {
  78. // Add feed only if we don’t find it in DB
  79. $feed_search = $this->searchByUrl($feed->url());
  80. if (!$feed_search) {
  81. $values = [
  82. 'id' => $feed->id(),
  83. 'url' => $feed->url(),
  84. 'kind' => $feed->kind(),
  85. 'category' => $feed->categoryId(),
  86. 'name' => $feed->name(true),
  87. 'website' => $feed->website(),
  88. 'description' => $feed->description(),
  89. 'lastUpdate' => 0,
  90. 'error' => false,
  91. 'pathEntries' => $feed->pathEntries(),
  92. 'httpAuth' => $feed->httpAuth(),
  93. 'ttl' => $feed->ttl(true),
  94. 'attributes' => $feed->attributes(),
  95. ];
  96. $id = $this->addFeed($values);
  97. if ($id) {
  98. $feed->_id($id);
  99. $feed->faviconPrepare();
  100. }
  101. return $id;
  102. } else {
  103. // The feed already exists so make sure it is not muted
  104. $feed->_ttl($feed_search->ttl());
  105. $feed->_mute(false);
  106. // Merge existing and import attributes
  107. $existingAttributes = $feed_search->attributes();
  108. $importAttributes = $feed->attributes();
  109. $feed->_attributes(array_replace_recursive($existingAttributes, $importAttributes));
  110. // Update some values of the existing feed using the import
  111. $values = [
  112. 'kind' => $feed->kind(),
  113. 'name' => $feed->name(true),
  114. 'website' => $feed->website(),
  115. 'description' => $feed->description(),
  116. 'pathEntries' => $feed->pathEntries(),
  117. 'ttl' => $feed->ttl(true),
  118. 'attributes' => $feed->attributes(),
  119. ];
  120. if (!$this->updateFeed($feed_search->id(), $values)) {
  121. return false;
  122. }
  123. return $feed_search->id();
  124. }
  125. }
  126. /**
  127. * @param array{'url'?:string,'kind'?:int,'category'?:int,'name'?:string,'website'?:string,'description'?:string,'lastUpdate'?:int,'priority'?:int,
  128. * 'pathEntries'?:string,'httpAuth'?:string,'error'?:int,'ttl'?:int,'attributes'?:string|array<string,mixed>} $valuesTmp $valuesTmp
  129. * @return int|false
  130. */
  131. public function updateFeed(int $id, array $valuesTmp) {
  132. $values = [];
  133. $originalValues = $valuesTmp;
  134. if (isset($valuesTmp['name'])) {
  135. $valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8');
  136. }
  137. if (isset($valuesTmp['url'])) {
  138. $valuesTmp['url'] = safe_ascii($valuesTmp['url']);
  139. }
  140. if (isset($valuesTmp['website'])) {
  141. $valuesTmp['website'] = safe_ascii($valuesTmp['website']);
  142. }
  143. $set = '';
  144. foreach ($valuesTmp as $key => $v) {
  145. $set .= '`' . $key . '`=?, ';
  146. if ($key === 'httpAuth') {
  147. $valuesTmp[$key] = base64_encode($v);
  148. } elseif ($key === 'attributes') {
  149. $valuesTmp[$key] = is_string($valuesTmp[$key]) ? $valuesTmp[$key] : json_encode($valuesTmp[$key], JSON_UNESCAPED_SLASHES);
  150. }
  151. }
  152. $set = substr($set, 0, -2);
  153. $sql = 'UPDATE `_feed` SET ' . $set . ' WHERE id=?';
  154. $stm = $this->pdo->prepare($sql);
  155. foreach ($valuesTmp as $v) {
  156. $values[] = $v;
  157. }
  158. $values[] = $id;
  159. if ($stm !== false && $stm->execute($values)) {
  160. return $stm->rowCount();
  161. } else {
  162. $info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
  163. if ($this->autoUpdateDb($info)) {
  164. return $this->updateFeed($id, $originalValues);
  165. }
  166. Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info) . ' for feed ' . $id);
  167. return false;
  168. }
  169. }
  170. /**
  171. * @param non-empty-string $key
  172. * @param string|array<mixed>|bool|int|null $value
  173. * @return int|false
  174. */
  175. public function updateFeedAttribute(FreshRSS_Feed $feed, string $key, $value) {
  176. $feed->_attribute($key, $value);
  177. return $this->updateFeed(
  178. $feed->id(),
  179. ['attributes' => $feed->attributes()]
  180. );
  181. }
  182. /**
  183. * @return int|false
  184. * @see updateCachedValues()
  185. */
  186. public function updateLastUpdate(int $id, bool $inError = false, int $mtime = 0) {
  187. $sql = 'UPDATE `_feed` SET `lastUpdate`=?, error=? WHERE id=?';
  188. $values = [
  189. $mtime <= 0 ? time() : $mtime,
  190. $inError ? 1 : 0,
  191. $id,
  192. ];
  193. $stm = $this->pdo->prepare($sql);
  194. if ($stm !== false && $stm->execute($values)) {
  195. return $stm->rowCount();
  196. } else {
  197. $info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
  198. Minz_Log::warning(__METHOD__ . ' error: ' . $sql . ' : ' . json_encode($info));
  199. return false;
  200. }
  201. }
  202. /** @return int|false */
  203. public function mute(int $id, bool $value = true) {
  204. $sql = 'UPDATE `_feed` SET ttl=' . ($value ? '-' : '') . 'ABS(ttl) WHERE id=' . intval($id);
  205. return $this->pdo->exec($sql);
  206. }
  207. /** @return int|false */
  208. public function changeCategory(int $idOldCat, int $idNewCat) {
  209. $catDAO = FreshRSS_Factory::createCategoryDao();
  210. $newCat = $catDAO->searchById($idNewCat);
  211. if ($newCat === null) {
  212. $newCat = $catDAO->getDefault();
  213. }
  214. if ($newCat === null) {
  215. return false;
  216. }
  217. $sql = 'UPDATE `_feed` SET category=? WHERE category=?';
  218. $stm = $this->pdo->prepare($sql);
  219. $values = [
  220. $newCat->id(),
  221. $idOldCat,
  222. ];
  223. if ($stm !== false && $stm->execute($values)) {
  224. return $stm->rowCount();
  225. } else {
  226. $info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
  227. Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
  228. return false;
  229. }
  230. }
  231. /** @return int|false */
  232. public function deleteFeed(int $id) {
  233. $sql = 'DELETE FROM `_feed` WHERE id=?';
  234. $stm = $this->pdo->prepare($sql);
  235. $values = [$id];
  236. if ($stm !== false && $stm->execute($values)) {
  237. return $stm->rowCount();
  238. } else {
  239. $info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
  240. Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
  241. return false;
  242. }
  243. }
  244. /**
  245. * @param bool|null $muted to include only muted feeds
  246. * @return int|false
  247. */
  248. public function deleteFeedByCategory(int $id, ?bool $muted = null) {
  249. $sql = 'DELETE FROM `_feed` WHERE category=?';
  250. if ($muted) {
  251. $sql .= ' AND ttl < 0';
  252. }
  253. $stm = $this->pdo->prepare($sql);
  254. $values = [$id];
  255. if ($stm !== false && $stm->execute($values)) {
  256. return $stm->rowCount();
  257. } else {
  258. $info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
  259. Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
  260. return false;
  261. }
  262. }
  263. /** @return Traversable<array{'id':int,'url':string,'kind':int,'category':int,'name':string,'website':string,'description':string,'lastUpdate':int,'priority'?:int,
  264. * 'pathEntries'?:string,'httpAuth':string,'error':int|bool,'ttl'?:int,'attributes'?:string}> */
  265. public function selectAll(): Traversable {
  266. $sql = <<<'SQL'
  267. SELECT id, url, kind, category, name, website, description, `lastUpdate`,
  268. priority, `pathEntries`, `httpAuth`, error, ttl, attributes
  269. FROM `_feed`
  270. SQL;
  271. $stm = $this->pdo->query($sql);
  272. if ($stm !== false) {
  273. while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
  274. /** @var array{'id':int,'url':string,'kind':int,'category':int,'name':string,'website':string,'description':string,'lastUpdate':int,'priority'?:int,
  275. * 'pathEntries'?:string,'httpAuth':string,'error':int|bool,'ttl'?:int,'attributes'?:string} $row */
  276. yield $row;
  277. }
  278. } else {
  279. $info = $this->pdo->errorInfo();
  280. if ($this->autoUpdateDb($info)) {
  281. yield from $this->selectAll();
  282. } else {
  283. Minz_Log::error(__method__ . ' error: ' . json_encode($info));
  284. }
  285. }
  286. }
  287. public function searchById(int $id): ?FreshRSS_Feed {
  288. $sql = 'SELECT * FROM `_feed` WHERE id=:id';
  289. $res = $this->fetchAssoc($sql, [':id' => $id]);
  290. if ($res == null) {
  291. return null;
  292. }
  293. /** @var array<int,array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
  294. * 'priority'?:int,'pathEntries'?:string,'httpAuth':string,'error':int,'ttl'?:int,'attributes'?:string}> $res */
  295. $feeds = self::daoToFeeds($res);
  296. return $feeds[$id] ?? null;
  297. }
  298. public function searchByUrl(string $url): ?FreshRSS_Feed {
  299. $sql = 'SELECT * FROM `_feed` WHERE url=:url';
  300. $res = $this->fetchAssoc($sql, [':url' => $url]);
  301. /** @var array<int,array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
  302. * 'priority'?:int,'pathEntries'?:string,'httpAuth':string,'error':int,'ttl'?:int,'attributes'?:string}> $res */
  303. return empty($res[0]) ? null : (current(self::daoToFeeds($res)) ?: null);
  304. }
  305. /** @return array<int> */
  306. public function listFeedsIds(): array {
  307. $sql = 'SELECT id FROM `_feed`';
  308. /** @var array<int> $res */
  309. $res = $this->fetchColumn($sql, 0) ?? [];
  310. return $res;
  311. }
  312. /**
  313. * @return array<int,FreshRSS_Feed>
  314. */
  315. public function listFeeds(): array {
  316. $sql = 'SELECT * FROM `_feed` ORDER BY name';
  317. $res = $this->fetchAssoc($sql);
  318. /** @var array<array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
  319. * 'priority':int,'pathEntries':string,'httpAuth':string,'error':int,'ttl':int,'attributes':string}>|null $res */
  320. return $res == null ? [] : self::daoToFeeds($res);
  321. }
  322. /** @return array<string,string> */
  323. public function listFeedsNewestItemUsec(?int $id_feed = null): array {
  324. $sql = 'SELECT id_feed, MAX(id) as newest_item_us FROM `_entry` ';
  325. if ($id_feed === null) {
  326. $sql .= 'GROUP BY id_feed';
  327. } else {
  328. $sql .= 'WHERE id_feed=' . intval($id_feed);
  329. }
  330. $res = $this->fetchAssoc($sql);
  331. /** @var array<array{'id_feed':int,'newest_item_us':string}>|null $res */
  332. if ($res == null) {
  333. return [];
  334. }
  335. $newestItemUsec = [];
  336. foreach ($res as $line) {
  337. $newestItemUsec['f_' . $line['id_feed']] = $line['newest_item_us'];
  338. }
  339. return $newestItemUsec;
  340. }
  341. /**
  342. * @param int $defaultCacheDuration Use -1 to return all feeds, without filtering them by TTL.
  343. * @return array<int,FreshRSS_Feed>
  344. */
  345. public function listFeedsOrderUpdate(int $defaultCacheDuration = 3600, int $limit = 0): array {
  346. $sql = 'SELECT id, url, kind, category, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, ttl, attributes, `cache_nbEntries`, `cache_nbUnreads` '
  347. . 'FROM `_feed` '
  348. . ($defaultCacheDuration < 0 ? '' : 'WHERE ttl >= ' . FreshRSS_Feed::TTL_DEFAULT
  349. . ' AND `lastUpdate` < (' . (time() + 60)
  350. . '-(CASE WHEN ttl=' . FreshRSS_Feed::TTL_DEFAULT . ' THEN ' . intval($defaultCacheDuration) . ' ELSE ttl END)) ')
  351. . 'ORDER BY `lastUpdate` '
  352. . ($limit < 1 ? '' : 'LIMIT ' . intval($limit));
  353. $stm = $this->pdo->query($sql);
  354. if ($stm !== false) {
  355. return self::daoToFeeds($stm->fetchAll(PDO::FETCH_ASSOC));
  356. } else {
  357. $info = $this->pdo->errorInfo();
  358. if ($this->autoUpdateDb($info)) {
  359. return $this->listFeedsOrderUpdate($defaultCacheDuration, $limit);
  360. }
  361. Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
  362. return [];
  363. }
  364. }
  365. /** @return array<int,string> */
  366. public function listTitles(int $id, int $limit = 0): array {
  367. $sql = 'SELECT title FROM `_entry` WHERE id_feed=:id_feed ORDER BY id DESC'
  368. . ($limit < 1 ? '' : ' LIMIT ' . intval($limit));
  369. $res = $this->fetchColumn($sql, 0, [':id_feed' => $id]) ?? [];
  370. /** @var array<int,string> $res */
  371. return $res;
  372. }
  373. /**
  374. * @param bool|null $muted to include only muted feeds
  375. * @return array<int,FreshRSS_Feed>
  376. */
  377. public function listByCategory(int $cat, ?bool $muted = null): array {
  378. $sql = 'SELECT * FROM `_feed` WHERE category=:category';
  379. if ($muted) {
  380. $sql .= ' AND ttl < 0';
  381. }
  382. $res = $this->fetchAssoc($sql, [':category' => $cat]);
  383. if ($res == null) {
  384. return [];
  385. }
  386. /**
  387. * @var array<int,array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
  388. * 'priority'?:int,'pathEntries'?:string,'httpAuth':string,'error':int,'ttl'?:int,'attributes'?:string}> $res
  389. */
  390. $feeds = self::daoToFeeds($res);
  391. uasort($feeds, static function (FreshRSS_Feed $a, FreshRSS_Feed $b) {
  392. return strnatcasecmp($a->name(), $b->name());
  393. });
  394. return $feeds;
  395. }
  396. public function countEntries(int $id): int {
  397. $sql = 'SELECT COUNT(*) AS count FROM `_entry` WHERE id_feed=:id_feed';
  398. $res = $this->fetchColumn($sql, 0, ['id_feed' => $id]);
  399. return isset($res[0]) ? (int)($res[0]) : -1;
  400. }
  401. public function countNotRead(int $id): int {
  402. $sql = 'SELECT COUNT(*) AS count FROM `_entry` WHERE id_feed=:id_feed AND is_read=0';
  403. $res = $this->fetchColumn($sql, 0, ['id_feed' => $id]);
  404. return isset($res[0]) ? (int)($res[0]) : -1;
  405. }
  406. /**
  407. * Update cached values for selected feeds, or all feeds if no feed ID is provided.
  408. * @return int|false
  409. */
  410. public function updateCachedValues(int ...$feedIds) {
  411. //2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE
  412. $sql = <<<SQL
  413. UPDATE `_feed`
  414. SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `_entry` e1 WHERE e1.id_feed=`_feed`.id),
  415. `cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `_entry` e2 WHERE e2.id_feed=`_feed`.id AND e2.is_read=0)
  416. SQL;
  417. if (count($feedIds) > 0) {
  418. $sql .= ' WHERE id IN (' . str_repeat('?,', count($feedIds) - 1) . '?)';
  419. }
  420. $stm = $this->pdo->prepare($sql);
  421. if ($stm !== false && $stm->execute($feedIds)) {
  422. return $stm->rowCount();
  423. } else {
  424. $info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
  425. Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
  426. return false;
  427. }
  428. }
  429. /**
  430. * Remember to call updateCachedValues() after calling this function
  431. * @return int|false number of lines affected or false in case of error
  432. */
  433. public function markAsReadMaxUnread(int $id, int $n) {
  434. //Double SELECT for MySQL workaround ERROR 1093 (HY000)
  435. $sql = <<<'SQL'
  436. UPDATE `_entry` SET is_read=1
  437. WHERE id_feed=:id_feed1 AND is_read=0 AND id <= (SELECT e3.id FROM (
  438. SELECT e2.id FROM `_entry` e2
  439. WHERE e2.id_feed=:id_feed2 AND e2.is_read=0
  440. ORDER BY e2.id DESC
  441. LIMIT 1
  442. OFFSET :limit) e3)
  443. SQL;
  444. if (($stm = $this->pdo->prepare($sql)) &&
  445. $stm->bindParam(':id_feed1', $id, PDO::PARAM_INT) &&
  446. $stm->bindParam(':id_feed2', $id, PDO::PARAM_INT) &&
  447. $stm->bindParam(':limit', $n, PDO::PARAM_INT) &&
  448. $stm->execute()) {
  449. return $stm->rowCount();
  450. } else {
  451. $info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
  452. Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
  453. return false;
  454. }
  455. }
  456. /**
  457. * Remember to call updateCachedValues() after calling this function
  458. * @return int|false number of lines affected or false in case of error
  459. */
  460. public function markAsReadNotSeen(int $id, int $minLastSeen) {
  461. $sql = <<<'SQL'
  462. UPDATE `_entry` SET is_read=1
  463. WHERE id_feed=:id_feed AND is_read=0 AND (`lastSeen` + 10 < :min_last_seen)
  464. SQL;
  465. if (($stm = $this->pdo->prepare($sql)) &&
  466. $stm->bindValue(':id_feed', $id, PDO::PARAM_INT) &&
  467. $stm->bindValue(':min_last_seen', $minLastSeen, PDO::PARAM_INT) &&
  468. $stm->execute()) {
  469. return $stm->rowCount();
  470. } else {
  471. $info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
  472. Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
  473. return false;
  474. }
  475. }
  476. /**
  477. * @return int|false
  478. */
  479. public function truncate(int $id) {
  480. $sql = 'DELETE FROM `_entry` WHERE id_feed=:id';
  481. $stm = $this->pdo->prepare($sql);
  482. $this->pdo->beginTransaction();
  483. if (!($stm !== false &&
  484. $stm->bindParam(':id', $id, PDO::PARAM_INT) &&
  485. $stm->execute())) {
  486. $info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
  487. Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
  488. $this->pdo->rollBack();
  489. return false;
  490. }
  491. $affected = $stm->rowCount();
  492. $sql = 'UPDATE `_feed` SET `cache_nbEntries`=0, `cache_nbUnreads`=0, `lastUpdate`=0 WHERE id=:id';
  493. $stm = $this->pdo->prepare($sql);
  494. if (!($stm !== false &&
  495. $stm->bindParam(':id', $id, PDO::PARAM_INT) &&
  496. $stm->execute())) {
  497. $info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
  498. Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
  499. $this->pdo->rollBack();
  500. return false;
  501. }
  502. $this->pdo->commit();
  503. return $affected;
  504. }
  505. public function purge(): bool {
  506. $sql = 'DELETE FROM `_entry`';
  507. $stm = $this->pdo->prepare($sql);
  508. $this->pdo->beginTransaction();
  509. if (!($stm && $stm->execute())) {
  510. $info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
  511. Minz_Log::error('SQL error ' . __METHOD__ . ' A ' . json_encode($info));
  512. $this->pdo->rollBack();
  513. return false;
  514. }
  515. $sql = 'UPDATE `_feed` SET `cache_nbEntries` = 0, `cache_nbUnreads` = 0';
  516. $stm = $this->pdo->prepare($sql);
  517. if (!($stm && $stm->execute())) {
  518. $info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
  519. Minz_Log::error('SQL error ' . __METHOD__ . ' B ' . json_encode($info));
  520. $this->pdo->rollBack();
  521. return false;
  522. }
  523. return $this->pdo->commit();
  524. }
  525. /**
  526. * @param array<int,array{'id'?:int,'url'?:string,'kind'?:int,'category'?:int,'name'?:string,'website'?:string,'description'?:string,'lastUpdate'?:int,'priority'?:int,
  527. * 'pathEntries'?:string,'httpAuth'?:string,'error'?:int|bool,'ttl'?:int,'attributes'?:string,'cache_nbUnreads'?:int,'cache_nbEntries'?:int}> $listDAO
  528. * @return array<int,FreshRSS_Feed>
  529. */
  530. public static function daoToFeeds(array $listDAO, ?int $catID = null): array {
  531. $list = [];
  532. foreach ($listDAO as $key => $dao) {
  533. FreshRSS_DatabaseDAO::pdoInt($dao, ['id', 'kind', 'category', 'lastUpdate', 'priority', 'error', 'ttl', 'cache_nbUnreads', 'cache_nbEntries']);
  534. if (!isset($dao['name'])) {
  535. continue;
  536. }
  537. if (isset($dao['id'])) {
  538. $key = (int)$dao['id'];
  539. }
  540. if ($catID === null) {
  541. $category = $dao['category'] ?? 0;
  542. } else {
  543. $category = $catID;
  544. }
  545. $myFeed = new FreshRSS_Feed($dao['url'] ?? '', false);
  546. $myFeed->_kind($dao['kind'] ?? FreshRSS_Feed::KIND_RSS);
  547. $myFeed->_categoryId($category);
  548. $myFeed->_name($dao['name']);
  549. $myFeed->_website($dao['website'] ?? '', false);
  550. $myFeed->_description($dao['description'] ?? '');
  551. $myFeed->_lastUpdate($dao['lastUpdate'] ?? 0);
  552. $myFeed->_priority($dao['priority'] ?? 10);
  553. $myFeed->_pathEntries($dao['pathEntries'] ?? '');
  554. $myFeed->_httpAuth(base64_decode($dao['httpAuth'] ?? '', true) ?: '');
  555. $myFeed->_error($dao['error'] ?? 0);
  556. $myFeed->_ttl($dao['ttl'] ?? FreshRSS_Feed::TTL_DEFAULT);
  557. $myFeed->_attributes($dao['attributes'] ?? '');
  558. $myFeed->_nbNotRead($dao['cache_nbUnreads'] ?? -1);
  559. $myFeed->_nbEntries($dao['cache_nbEntries'] ?? -1);
  560. if (isset($dao['id'])) {
  561. $myFeed->_id($dao['id']);
  562. }
  563. $list[$key] = $myFeed;
  564. }
  565. return $list;
  566. }
  567. public function count(): int {
  568. $sql = 'SELECT COUNT(e.id) AS count FROM `_feed` e';
  569. $stm = $this->pdo->query($sql);
  570. if ($stm == false) {
  571. return -1;
  572. }
  573. $res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
  574. return (int)($res[0] ?? 0);
  575. }
  576. }