4
0

query.php 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. <?php
  2. declare(strict_types=1);
  3. require(__DIR__ . '/../../constants.php');
  4. require(LIB_PATH . '/lib_rss.php'); //Includes class autoloader
  5. Minz_Request::init();
  6. $token = Minz_Request::paramString('t');
  7. if (!ctype_alnum($token)) {
  8. header('HTTP/1.1 422 Unprocessable Entity');
  9. header('Content-Type: text/plain; charset=UTF-8');
  10. die('Invalid token `t`!' . $token);
  11. }
  12. $format = Minz_Request::paramString('f');
  13. if (!in_array($format, ['atom', 'greader', 'html', 'json', 'opml', 'rss'], true)) {
  14. header('HTTP/1.1 422 Unprocessable Entity');
  15. header('Content-Type: text/plain; charset=UTF-8');
  16. die('Invalid format `f`!');
  17. }
  18. $user = Minz_Request::paramString('user');
  19. if (!FreshRSS_user_Controller::checkUsername($user)) {
  20. header('HTTP/1.1 422 Unprocessable Entity');
  21. header('Content-Type: text/plain; charset=UTF-8');
  22. die('Invalid user!');
  23. }
  24. Minz_Session::init('FreshRSS', true);
  25. FreshRSS_Context::initSystem();
  26. if (!FreshRSS_Context::hasSystemConf() || !FreshRSS_Context::systemConf()->api_enabled) {
  27. header('HTTP/1.1 503 Service Unavailable');
  28. header('Content-Type: text/plain; charset=UTF-8');
  29. die('Service Unavailable!');
  30. }
  31. FreshRSS_Context::initUser($user);
  32. if (!FreshRSS_Context::hasUserConf() || !FreshRSS_Context::userConf()->enabled) {
  33. usleep(rand(100, 10000)); //Primitive mitigation of scanning for users
  34. header('HTTP/1.1 404 Not Found');
  35. header('Content-Type: text/plain; charset=UTF-8');
  36. die('User not found!');
  37. } else {
  38. usleep(rand(20, 200));
  39. }
  40. if (!file_exists(DATA_PATH . '/no-cache.txt')) {
  41. require(LIB_PATH . '/http-conditional.php');
  42. // TODO: Consider taking advantage of $feedMode, only for monotonous queries {all, categories, feeds} and not dynamic ones {read/unread, favourites, user labels}
  43. if (httpConditional(FreshRSS_UserDAO::mtime($user) ?: time(), 0, 0, false, PHP_COMPRESSION, false)) {
  44. exit(); //No need to send anything
  45. }
  46. }
  47. Minz_Translate::init(FreshRSS_Context::userConf()->language);
  48. Minz_ExtensionManager::init();
  49. Minz_ExtensionManager::enableByList(FreshRSS_Context::userConf()->extensions_enabled, 'user');
  50. $query = null;
  51. $userSearch = null;
  52. foreach (FreshRSS_Context::userConf()->queries as $raw_query) {
  53. if (!empty($raw_query['token']) && $raw_query['token'] === $token) {
  54. switch ($format) {
  55. case 'atom':
  56. case 'greader':
  57. case 'html':
  58. case 'json':
  59. case 'rss':
  60. if (empty($raw_query['shareRss'])) {
  61. continue 2;
  62. }
  63. break;
  64. case 'opml':
  65. if (empty($raw_query['shareOpml'])) {
  66. continue 2;
  67. }
  68. break;
  69. default:
  70. continue 2;
  71. }
  72. $query = new FreshRSS_UserQuery($raw_query, FreshRSS_Context::categories(), FreshRSS_Context::labels());
  73. Minz_Request::_param('get', $query->getGet());
  74. if (Minz_Request::paramString('order') === '') {
  75. Minz_Request::_param('order', $query->getOrder());
  76. }
  77. Minz_Request::_param('state', (string)$query->getState());
  78. $search = $query->getSearch()->getRawInput();
  79. // Note: we disallow references to user queries in public user search to avoid sniffing internal user queries
  80. $userSearch = new FreshRSS_BooleanSearch(Minz_Request::paramString('search'), 0, 'AND', false);
  81. if ($userSearch->getRawInput() !== '') {
  82. if ($search === '') {
  83. $search = $userSearch->getRawInput();
  84. } else {
  85. $search .= ' (' . $userSearch->getRawInput() . ')';
  86. }
  87. }
  88. Minz_Request::_param('search', $search);
  89. break;
  90. }
  91. }
  92. if ($query === null || $userSearch === null) {
  93. usleep(rand(100, 10000));
  94. header('HTTP/1.1 404 Not Found');
  95. header('Content-Type: text/plain; charset=UTF-8');
  96. die('User query not found!');
  97. }
  98. $view = new FreshRSS_View();
  99. try {
  100. FreshRSS_Context::updateUsingRequest(false);
  101. Minz_Request::_param('search', $userSearch->getRawInput()); // Restore user search
  102. $view->entries = FreshRSS_index_Controller::listEntriesByContext();
  103. } catch (Minz_Exception $e) {
  104. Minz_Error::error(400, 'Bad user query!');
  105. die();
  106. }
  107. $get = FreshRSS_Context::currentGet(true);
  108. $type = (string)$get[0];
  109. $id = (int)$get[1];
  110. switch ($type) {
  111. case 'c': // Category
  112. $cat = FreshRSS_Context::categories()[$id] ?? null;
  113. if ($cat === null) {
  114. Minz_Error::error(404, "Category {$id} not found!");
  115. die();
  116. }
  117. $view->categories = [ $cat->id() => $cat ];
  118. break;
  119. case 'f': // Feed
  120. $feed = FreshRSS_Category::findFeed(FreshRSS_Context::categories(), $id);
  121. if ($feed === null) {
  122. Minz_Error::error(404, "Feed {$id} not found!");
  123. die();
  124. }
  125. $view->feeds = [ $feed->id() => $feed ];
  126. $view->categories = [];
  127. break;
  128. default:
  129. $view->categories = FreshRSS_Context::categories();
  130. break;
  131. }
  132. $view->disable_aside = true;
  133. $view->excludeMutedFeeds = true;
  134. $view->internal_rendering = true;
  135. $view->userQuery = $query;
  136. $view->html_url = $query->sharedUrlHtml();
  137. $view->rss_url = $query->sharedUrlRss();
  138. $view->rss_title = $query->getName();
  139. $view->image_url = $query->getImageUrl();
  140. $view->description = $query->getDescription() ?: _t('index.feed.rss_of', $view->rss_title);
  141. if ($query->getName() != '') {
  142. FreshRSS_View::_title($query->getName());
  143. }
  144. FreshRSS_Context::systemConf()->allow_anonymous = true;
  145. header('Access-Control-Allow-Methods: GET');
  146. header('Access-Control-Allow-Origin: *');
  147. header('Access-Control-Max-Age: 600');
  148. header('Cache-Control: public, max-age=60');
  149. if (($_SERVER['REQUEST_METHOD'] ?? '') === 'OPTIONS') {
  150. header('HTTP/1.1 204 No Content');
  151. exit();
  152. }
  153. if (in_array($format, ['rss', 'atom'], true)) {
  154. header('Content-Type: application/rss+xml; charset=utf-8');
  155. $view->_layout(null);
  156. $view->_path('index/rss.phtml');
  157. } elseif (in_array($format, ['greader', 'json'], true)) {
  158. header('Content-Type: application/json; charset=utf-8');
  159. $view->_layout(null);
  160. $view->type = 'query/' . $token;
  161. $view->list_title = $query->getName();
  162. $view->entryIdsTagNames = []; // Do not export user labels for privacy
  163. $view->_path('helpers/export/articles.phtml');
  164. } elseif ($format === 'opml') {
  165. if (!$query->safeForOpml()) {
  166. Minz_Error::error(404, 'OPML not allowed for this user query!');
  167. die();
  168. }
  169. header('Content-Type: application/xml; charset=utf-8');
  170. $view->_layout(null);
  171. $view->_path('index/opml.phtml');
  172. } else {
  173. $view->_layout('layout');
  174. $view->_path('index/html.phtml');
  175. }
  176. $view->build();