entryController.php 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * Controller to handle every entry actions.
  5. */
  6. class FreshRSS_entry_Controller extends FreshRSS_ActionController {
  7. /**
  8. * JavaScript request or not.
  9. */
  10. private bool $ajax = false;
  11. /**
  12. * This action is called before every other action in that class. It is
  13. * the common boilerplate for every action. It is triggered by the
  14. * underlying framework.
  15. */
  16. #[\Override]
  17. public function firstAction(): void {
  18. if (!FreshRSS_Auth::hasAccess()) {
  19. Minz_Error::error(403);
  20. }
  21. // If ajax request, we do not print layout
  22. $this->ajax = Minz_Request::paramBoolean('ajax');
  23. if ($this->ajax) {
  24. $this->view->_layout(null);
  25. Minz_Request::_param('ajax');
  26. }
  27. }
  28. /**
  29. * Mark one or several entries as read (or not!).
  30. *
  31. * If request concerns several entries, it MUST be a POST request.
  32. * If request concerns several entries, only mark them as read is available.
  33. *
  34. * Parameters are:
  35. * - id (default: false)
  36. * - get (default: false) /(c_\d+|f_\d+|s|a)/
  37. * - nextGet (default: $get)
  38. * - idMax (default: 0)
  39. * - is_read (default: true)
  40. */
  41. public function readAction(): void {
  42. $get = Minz_Request::paramString('get');
  43. $next_get = Minz_Request::paramString('nextGet') ?: $get;
  44. $id_max = Minz_Request::paramString('idMax');
  45. if (!ctype_digit($id_max)) {
  46. $id_max = '0';
  47. }
  48. $is_read = Minz_Request::paramTernary('is_read') ?? true;
  49. FreshRSS_Context::$search = new FreshRSS_BooleanSearch(Minz_Request::paramString('search'));
  50. FreshRSS_Context::$state = Minz_Request::paramInt('state');
  51. if (FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_FAVORITE)) {
  52. if (!FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_NOT_FAVORITE)) {
  53. FreshRSS_Context::$state = FreshRSS_Entry::STATE_FAVORITE;
  54. }
  55. } elseif (FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_NOT_FAVORITE)) {
  56. FreshRSS_Context::$state = FreshRSS_Entry::STATE_NOT_FAVORITE;
  57. } else {
  58. FreshRSS_Context::$state = 0;
  59. }
  60. $params = [];
  61. $this->view->tagsForEntries = [];
  62. $entryDAO = FreshRSS_Factory::createEntryDao();
  63. if (!Minz_Request::hasParam('id')) {
  64. // No id, then it MUST be a POST request
  65. if (!Minz_Request::isPost()) {
  66. Minz_Request::bad(_t('feedback.access.not_found'), ['c' => 'index', 'a' => 'index']);
  67. return;
  68. }
  69. if ($get === '') {
  70. // No get? Mark all entries as read (from $id_max)
  71. $entryDAO->markReadEntries($id_max, false, FreshRSS_Feed::PRIORITY_MAIN_STREAM, FreshRSS_Feed::PRIORITY_IMPORTANT, null, 0, $is_read);
  72. } else {
  73. $type_get = $get[0];
  74. $get = (int)substr($get, 2);
  75. switch ($type_get) {
  76. case 'c':
  77. $entryDAO->markReadCat($get, $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
  78. break;
  79. case 'f':
  80. $entryDAO->markReadFeed($get, $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
  81. break;
  82. case 's':
  83. $entryDAO->markReadEntries($id_max, true, null, FreshRSS_Feed::PRIORITY_IMPORTANT,
  84. FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
  85. break;
  86. case 'a':
  87. $entryDAO->markReadEntries($id_max, false, FreshRSS_Feed::PRIORITY_MAIN_STREAM, FreshRSS_Feed::PRIORITY_IMPORTANT,
  88. FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
  89. break;
  90. case 'A':
  91. $entryDAO->markReadEntries($id_max, false, FreshRSS_Feed::PRIORITY_CATEGORY, FreshRSS_Feed::PRIORITY_IMPORTANT,
  92. FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
  93. break;
  94. case 'Z':
  95. $entryDAO->markReadEntries($id_max, false, FreshRSS_Feed::PRIORITY_ARCHIVED, FreshRSS_Feed::PRIORITY_IMPORTANT,
  96. FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
  97. break;
  98. case 'i':
  99. $entryDAO->markReadEntries($id_max, false, FreshRSS_Feed::PRIORITY_IMPORTANT, null,
  100. FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
  101. break;
  102. case 't':
  103. $entryDAO->markReadTag($get, $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
  104. // Marking all entries in a tag as read can result in other tags also having all entries marked as read,
  105. // so the next unread tag calculation is deferred by passing next_get = 'a' instead of the current get ID.
  106. if ($next_get === 'a' && $is_read) {
  107. $tagDAO = FreshRSS_Factory::createTagDao();
  108. $tagsList = $tagDAO->listTags() ?: [];
  109. $found_tag = false;
  110. foreach ($tagsList as $tag) {
  111. if ($found_tag) {
  112. // Found the tag matching our current ID already, so now we're just looking for the first unread
  113. if ($tag->nbUnread() > 0) {
  114. $next_get = 't_' . $tag->id();
  115. break;
  116. }
  117. } else {
  118. // Still looking for the tag ID matching our $get that was just marked as read
  119. if ($tag->id() === $get) {
  120. $found_tag = true;
  121. }
  122. }
  123. }
  124. // Didn't find any unread tags after the current one? Start over from the beginning.
  125. if ($next_get === 'a') {
  126. foreach ($tagsList as $tag) {
  127. // Check this first so we can return to the current tag if it's the only one that's unread
  128. if ($tag->nbUnread() > 0) {
  129. $next_get = 't_' . $tag->id();
  130. break;
  131. }
  132. // Give up if reached our first tag again
  133. if ($tag->id() === $get) {
  134. break;
  135. }
  136. }
  137. }
  138. // If we still haven't found any unread tags, fallback to the full tag list
  139. if ($next_get === 'a') {
  140. $next_get = 'T';
  141. }
  142. }
  143. break;
  144. case 'T':
  145. $entryDAO->markReadTag(0, $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
  146. break;
  147. }
  148. if ($next_get !== 'a') {
  149. // Redirect to the correct page (category, feed or starred)
  150. // Not "a" because it is the default value if nothing is given.
  151. $params['get'] = $next_get;
  152. }
  153. }
  154. } else {
  155. /** @var list<numeric-string> $idArray */
  156. $idArray = Minz_Request::paramArrayString('id');
  157. $idString = Minz_Request::paramString('id');
  158. if (count($idArray) > 0) {
  159. $ids = $idArray;
  160. } elseif (ctype_digit($idString)) {
  161. $ids = [$idString];
  162. } else {
  163. $ids = [];
  164. }
  165. $entryDAO->markRead($ids, $is_read);
  166. $tagDAO = FreshRSS_Factory::createTagDao();
  167. $tagsForEntries = $tagDAO->getTagsForEntries($ids) ?: [];
  168. $tags = [];
  169. foreach ($tagsForEntries as $line) {
  170. $tags['t_' . $line['id_tag']][] = (string)$line['id_entry'];
  171. }
  172. $this->view->tagsForEntries = $tags;
  173. }
  174. if (!$this->ajax) {
  175. Minz_Request::good(
  176. $is_read ? _t('feedback.sub.articles.marked_read') : _t('feedback.sub.articles.marked_unread'),
  177. [
  178. 'c' => 'index',
  179. 'a' => 'index',
  180. 'params' => $params,
  181. ]
  182. );
  183. }
  184. }
  185. /**
  186. * This action marks an entry as favourite (bookmark) or not.
  187. *
  188. * Parameter is:
  189. * - id (default: false)
  190. * - is_favorite (default: true)
  191. * If id is false, nothing happened.
  192. */
  193. public function bookmarkAction(): void {
  194. $id = Minz_Request::paramString('id');
  195. $is_favourite = Minz_Request::paramTernary('is_favorite') ?? true;
  196. if ($id != '' && ctype_digit($id)) {
  197. $entryDAO = FreshRSS_Factory::createEntryDao();
  198. $entryDAO->markFavorite($id, $is_favourite);
  199. }
  200. if (!$this->ajax) {
  201. Minz_Request::forward([
  202. 'c' => 'index',
  203. 'a' => 'index',
  204. ], true);
  205. }
  206. }
  207. /**
  208. * This action optimizes database to reduce its size.
  209. *
  210. * This action should be reached by a POST request.
  211. *
  212. * @todo move this action in configure controller.
  213. * @todo call this action through web-cron when available
  214. */
  215. public function optimizeAction(): void {
  216. $url_redirect = [
  217. 'c' => 'configure',
  218. 'a' => 'archiving',
  219. ];
  220. if (!Minz_Request::isPost()) {
  221. Minz_Request::forward($url_redirect, true);
  222. }
  223. if (function_exists('set_time_limit')) {
  224. @set_time_limit(300);
  225. }
  226. $databaseDAO = FreshRSS_Factory::createDatabaseDAO();
  227. $databaseDAO->optimize();
  228. $feedDAO = FreshRSS_Factory::createFeedDao();
  229. $feedDAO->updateCachedValues();
  230. invalidateHttpCache();
  231. Minz_Request::good(_t('feedback.admin.optimization_complete'), $url_redirect);
  232. }
  233. /**
  234. * This action purges old entries from feeds.
  235. *
  236. * @todo should be a POST request
  237. * @todo should be in feedController
  238. */
  239. public function purgeAction(): void {
  240. if (function_exists('set_time_limit')) {
  241. @set_time_limit(300);
  242. }
  243. $feedDAO = FreshRSS_Factory::createFeedDao();
  244. $feeds = $feedDAO->listFeeds();
  245. $nb_total = 0;
  246. invalidateHttpCache();
  247. $feedDAO->beginTransaction();
  248. foreach ($feeds as $feed) {
  249. $nb_total += ($feed->cleanOldEntries() ?: 0);
  250. }
  251. $feedDAO->updateCachedValues();
  252. $feedDAO->commit();
  253. $databaseDAO = FreshRSS_Factory::createDatabaseDAO();
  254. $databaseDAO->minorDbMaintenance();
  255. invalidateHttpCache();
  256. Minz_Request::good(_t('feedback.sub.purge_completed', $nb_total), [
  257. 'c' => 'configure',
  258. 'a' => 'archiving',
  259. ]);
  260. }
  261. }