FeedDAO.php 22 KB

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