FeedDAO.php 19 KB

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