FeedDAO.php 20 KB

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