entryController.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  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. * - maxPubDate (default: 0)
  40. * - is_read (default: true)
  41. */
  42. public function readAction(): void {
  43. $get = Minz_Request::paramString('get', plaintext: true);
  44. $next_get = Minz_Request::paramString('nextGet', plaintext: true) ?: $get;
  45. $id_max = Minz_Request::paramString('idMax', plaintext: true);
  46. if (!ctype_digit($id_max)) {
  47. $id_max = '0';
  48. }
  49. $is_read = Minz_Request::paramTernary('is_read') ?? true;
  50. FreshRSS_Context::$search = new FreshRSS_BooleanSearch(Minz_Request::paramString('search', plaintext: true));
  51. $maxPubDate = Minz_Request::paramInt('maxPubDate');
  52. if ($maxPubDate > 0) {
  53. $search = new FreshRSS_Search('');
  54. $search->setMaxPubdate($maxPubDate);
  55. FreshRSS_Context::$search->prepend($search);
  56. }
  57. FreshRSS_Context::$state = Minz_Request::paramInt('state');
  58. if (FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_FAVORITE)) {
  59. if (!FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_NOT_FAVORITE)) {
  60. FreshRSS_Context::$state = FreshRSS_Entry::STATE_FAVORITE;
  61. }
  62. } elseif (FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_NOT_FAVORITE)) {
  63. FreshRSS_Context::$state = FreshRSS_Entry::STATE_NOT_FAVORITE;
  64. } else {
  65. FreshRSS_Context::$state = 0;
  66. }
  67. $params = [];
  68. $this->view->tagsForEntries = [];
  69. $entryDAO = FreshRSS_Factory::createEntryDao();
  70. if (!Minz_Request::hasParam('id')) {
  71. // No id, then it MUST be a POST request
  72. if (!Minz_Request::isPost()) {
  73. Minz_Request::bad(_t('feedback.access.not_found'), ['c' => 'index', 'a' => 'index']);
  74. return;
  75. }
  76. if ($get === '') {
  77. // No get? Mark all entries as read (from $id_max)
  78. $entryDAO->markReadEntries($id_max, false, FreshRSS_Feed::PRIORITY_MAIN_STREAM, FreshRSS_Feed::PRIORITY_IMPORTANT, null, 0, $is_read);
  79. } else {
  80. $type_get = $get[0];
  81. $get = (int)substr($get, 2);
  82. switch ($type_get) {
  83. case 'c': // Category
  84. $entryDAO->markReadCat($get, $id_max,
  85. priorityMin: min(FreshRSS_Feed::PRIORITY_CATEGORY, FreshRSS_Context::$search->needVisibility() ?? FreshRSS_Feed::PRIORITY_IMPORTANT),
  86. filters: FreshRSS_Context::$search, state: FreshRSS_Context::$state, is_read: $is_read);
  87. break;
  88. case 'f': // Feed
  89. $entryDAO->markReadFeed($get, $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
  90. break;
  91. case 's': // Starred. Deprecated: use $state instead
  92. $entryDAO->markReadEntries($id_max, onlyFavorites: true,
  93. priorityMin: null,
  94. priorityMax: null,
  95. filters: FreshRSS_Context::$search, state: FreshRSS_Context::$state, is_read: $is_read);
  96. break;
  97. case 'a': // All PRIORITY_MAIN_STREAM
  98. $entryDAO->markReadEntries($id_max, onlyFavorites: false,
  99. priorityMin: min(FreshRSS_Feed::PRIORITY_MAIN_STREAM, FreshRSS_Context::$search->needVisibility() ?? FreshRSS_Feed::PRIORITY_IMPORTANT),
  100. priorityMax: null,
  101. filters: FreshRSS_Context::$search, state: FreshRSS_Context::$state, is_read: $is_read);
  102. break;
  103. case 'A': // All except PRIORITY_HIDDEN
  104. $entryDAO->markReadEntries($id_max, onlyFavorites: false,
  105. priorityMin: min(FreshRSS_Feed::PRIORITY_CATEGORY, FreshRSS_Context::$search->needVisibility() ?? FreshRSS_Feed::PRIORITY_IMPORTANT),
  106. priorityMax: null,
  107. filters: FreshRSS_Context::$search, state: FreshRSS_Context::$state, is_read: $is_read);
  108. break;
  109. case 'Z': // All including PRIORITY_HIDDEN
  110. $entryDAO->markReadEntries($id_max, onlyFavorites: false,
  111. priorityMin: FreshRSS_Feed::PRIORITY_HIDDEN,
  112. priorityMax: null,
  113. filters: FreshRSS_Context::$search, state: FreshRSS_Context::$state, is_read: $is_read);
  114. break;
  115. case 'i': // Priority important feeds
  116. $entryDAO->markReadEntries($id_max, onlyFavorites: false,
  117. priorityMin: min(FreshRSS_Feed::PRIORITY_IMPORTANT, FreshRSS_Context::$search->needVisibility() ?? FreshRSS_Feed::PRIORITY_IMPORTANT),
  118. priorityMax: null,
  119. filters: FreshRSS_Context::$search, state: FreshRSS_Context::$state, is_read: $is_read);
  120. break;
  121. case 't': // Tag (label)
  122. $entryDAO->markReadTag($get, $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
  123. // Marking all entries in a tag as read can result in other tags also having all entries marked as read,
  124. // so the next unread tag calculation is deferred by passing next_get = 'a' instead of the current get ID.
  125. if ($next_get === 'a' && $is_read) {
  126. $tagDAO = FreshRSS_Factory::createTagDao();
  127. $tagsList = $tagDAO->listTags();
  128. $found_tag = false;
  129. foreach ($tagsList as $tag) {
  130. if ($found_tag) {
  131. // Found the tag matching our current ID already, so now we're just looking for the first unread
  132. if ($tag->nbUnread() > 0) {
  133. $next_get = 't_' . $tag->id();
  134. break;
  135. }
  136. } else {
  137. // Still looking for the tag ID matching our $get that was just marked as read
  138. if ($tag->id() === $get) {
  139. $found_tag = true;
  140. }
  141. }
  142. }
  143. // Didn't find any unread tags after the current one? Start over from the beginning.
  144. if ($next_get === 'a') {
  145. foreach ($tagsList as $tag) {
  146. // Check this first so we can return to the current tag if it's the only one that's unread
  147. if ($tag->nbUnread() > 0) {
  148. $next_get = 't_' . $tag->id();
  149. break;
  150. }
  151. // Give up if reached our first tag again
  152. if ($tag->id() === $get) {
  153. break;
  154. }
  155. }
  156. }
  157. // If we still haven't found any unread tags, fallback to the full tag list
  158. if ($next_get === 'a') {
  159. $next_get = 'T';
  160. }
  161. }
  162. break;
  163. case 'T': // Any tag (label)
  164. $entryDAO->markReadTag(0, $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
  165. break;
  166. }
  167. if ($next_get !== 'a') {
  168. // Redirect to the correct page (category, feed or starred)
  169. // Not "a" because it is the default value if nothing is given.
  170. $params['get'] = $next_get;
  171. }
  172. }
  173. } else {
  174. /** @var list<numeric-string> $idArray */
  175. $idArray = Minz_Request::paramArrayString('id', plaintext: true);
  176. $idString = Minz_Request::paramString('id', plaintext: true);
  177. if (count($idArray) > 0) {
  178. $ids = $idArray;
  179. } elseif (ctype_digit($idString)) {
  180. $ids = [$idString];
  181. } else {
  182. $ids = [];
  183. }
  184. $entryDAO->markRead($ids, $is_read);
  185. $tagDAO = FreshRSS_Factory::createTagDao();
  186. $tagsForEntries = $tagDAO->getTagsForEntries($ids) ?? [];
  187. $tags = [];
  188. foreach ($tagsForEntries as $line) {
  189. $tags['t_' . $line['id_tag']][] = (string)$line['id_entry'];
  190. }
  191. $this->view->tagsForEntries = $tags;
  192. }
  193. if (!$this->ajax) {
  194. if (Minz_Request::hasParam('order')) {
  195. $params['order'] = Minz_Request::paramString('order', plaintext: true);
  196. }
  197. if (Minz_Request::hasParam('sort')) {
  198. $params['sort'] = Minz_Request::paramString('sort', plaintext: true);
  199. }
  200. Minz_Request::good(
  201. $is_read ? _t('feedback.sub.articles.marked_read') : _t('feedback.sub.articles.marked_unread'),
  202. [
  203. 'c' => 'index',
  204. 'a' => Minz_Request::paramStringNull('from') ?? 'index',
  205. 'params' => $params,
  206. ],
  207. notificationName: 'readAction ',
  208. showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
  209. );
  210. }
  211. }
  212. /**
  213. * This action marks an entry as favourite (bookmark) or not.
  214. *
  215. * Parameter is:
  216. * - id (default: false)
  217. * - is_favorite (default: true)
  218. * If id is false, nothing happened.
  219. */
  220. public function bookmarkAction(): void {
  221. $id = Minz_Request::paramString('id', plaintext: true);
  222. $is_favourite = Minz_Request::paramTernary('is_favorite') ?? true;
  223. if ($id != '' && ctype_digit($id)) {
  224. $entryDAO = FreshRSS_Factory::createEntryDao();
  225. $entryDAO->markFavorite($id, $is_favourite);
  226. }
  227. if (!$this->ajax) {
  228. Minz_Request::forward([
  229. 'c' => 'index',
  230. 'a' => 'index',
  231. ], true);
  232. }
  233. }
  234. /**
  235. * This action optimizes database to reduce its size.
  236. *
  237. * This action should be reached by a POST request.
  238. *
  239. * @todo move this action in configure controller.
  240. * @todo call this action through web-cron when available
  241. */
  242. public function optimizeAction(): void {
  243. $url_redirect = [
  244. 'c' => 'configure',
  245. 'a' => 'archiving',
  246. ];
  247. if (!Minz_Request::isPost()) {
  248. Minz_Request::forward($url_redirect, true);
  249. }
  250. if (function_exists('set_time_limit')) {
  251. @set_time_limit(300);
  252. }
  253. $databaseDAO = FreshRSS_Factory::createDatabaseDAO();
  254. $databaseDAO->optimize();
  255. $feedDAO = FreshRSS_Factory::createFeedDao();
  256. $feedDAO->updateCachedValues();
  257. invalidateHttpCache();
  258. Minz_Request::good(
  259. _t('feedback.admin.optimization_complete'),
  260. $url_redirect,
  261. showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
  262. );
  263. }
  264. /**
  265. * This action purges old entries from feeds.
  266. *
  267. * @todo should be in feedController
  268. */
  269. public function purgeAction(): void {
  270. if (!Minz_Request::isPost()) {
  271. Minz_Error::error(403);
  272. return;
  273. }
  274. if (function_exists('set_time_limit')) {
  275. @set_time_limit(300);
  276. }
  277. $feedDAO = FreshRSS_Factory::createFeedDao();
  278. $feeds = $feedDAO->listFeeds();
  279. $nb_total = 0;
  280. invalidateHttpCache();
  281. $feedDAO->beginTransaction();
  282. foreach ($feeds as $feed) {
  283. $nb_total += ($feed->cleanOldEntries() ?: 0);
  284. }
  285. $feedDAO->updateCachedValues();
  286. $feedDAO->commit();
  287. $databaseDAO = FreshRSS_Factory::createDatabaseDAO();
  288. $databaseDAO->minorDbMaintenance();
  289. invalidateHttpCache();
  290. Minz_Request::good(
  291. _t('feedback.sub.purge_completed', $nb_total),
  292. ['c' => 'configure', 'a' => 'archiving'],
  293. showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
  294. );
  295. }
  296. }