4
0

query.php 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  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. $dateLastModification = max(
  43. FreshRSS_UserDAO::ctime($user),
  44. FreshRSS_UserDAO::mtime($user),
  45. @filemtime(DATA_PATH . '/config.php') ?: 0
  46. );
  47. // TODO: Consider taking advantage of $feedMode, only for monotonous queries {all, categories, feeds} and not dynamic ones {read/unread, favourites, user labels}
  48. if (httpConditional($dateLastModification ?: time(), 0, 0, false, PHP_COMPRESSION, false)) {
  49. exit(); //No need to send anything
  50. }
  51. }
  52. Minz_Translate::init(FreshRSS_Context::userConf()->language);
  53. Minz_ExtensionManager::init();
  54. Minz_ExtensionManager::enableByList(FreshRSS_Context::userConf()->extensions_enabled, 'user');
  55. $query = null;
  56. $userSearch = null;
  57. foreach (FreshRSS_Context::userConf()->queries as $raw_query) {
  58. if (!empty($raw_query['token']) && $raw_query['token'] === $token) {
  59. switch ($format) {
  60. case 'atom':
  61. case 'greader':
  62. case 'html':
  63. case 'json':
  64. case 'rss':
  65. if (empty($raw_query['shareRss'])) {
  66. continue 2;
  67. }
  68. break;
  69. case 'opml':
  70. if (empty($raw_query['shareOpml'])) {
  71. continue 2;
  72. }
  73. break;
  74. default:
  75. continue 2;
  76. }
  77. $query = new FreshRSS_UserQuery($raw_query, FreshRSS_Context::categories(), FreshRSS_Context::labels());
  78. Minz_Request::_param('get', $query->getGet());
  79. if (Minz_Request::paramString('order') === '') {
  80. Minz_Request::_param('order', $query->getOrder());
  81. }
  82. Minz_Request::_param('state', (string)$query->getState());
  83. $search = $query->getSearch()->getRawInput();
  84. // Note: we disallow references to user queries in public user search to avoid sniffing internal user queries
  85. $userSearch = new FreshRSS_BooleanSearch(Minz_Request::paramString('search'), 0, 'AND', false);
  86. if ($userSearch->getRawInput() !== '') {
  87. if ($search === '') {
  88. $search = $userSearch->getRawInput();
  89. } else {
  90. $search .= ' (' . $userSearch->getRawInput() . ')';
  91. }
  92. }
  93. Minz_Request::_param('search', $search);
  94. break;
  95. }
  96. }
  97. if ($query === null || $userSearch === null) {
  98. usleep(rand(100, 10000));
  99. header('HTTP/1.1 404 Not Found');
  100. header('Content-Type: text/plain; charset=UTF-8');
  101. die('User query not found!');
  102. }
  103. $view = new FreshRSS_View();
  104. try {
  105. FreshRSS_Context::updateUsingRequest(false);
  106. Minz_Request::_param('search', $userSearch->getRawInput()); // Restore user search
  107. $view->entries = FreshRSS_index_Controller::listEntriesByContext();
  108. } catch (Minz_Exception $e) {
  109. Minz_Error::error(400, 'Bad user query!');
  110. die();
  111. }
  112. $get = FreshRSS_Context::currentGet(true);
  113. $type = (string)$get[0];
  114. $id = (int)$get[1];
  115. switch ($type) {
  116. case 'c': // Category
  117. $cat = FreshRSS_Context::categories()[$id] ?? null;
  118. if ($cat === null) {
  119. Minz_Error::error(404, "Category {$id} not found!");
  120. die();
  121. }
  122. $view->categories = [ $cat->id() => $cat ];
  123. break;
  124. case 'f': // Feed
  125. $feed = FreshRSS_Category::findFeed(FreshRSS_Context::categories(), $id);
  126. if ($feed === null) {
  127. Minz_Error::error(404, "Feed {$id} not found!");
  128. die();
  129. }
  130. $view->feeds = [ $feed->id() => $feed ];
  131. $view->categories = [];
  132. break;
  133. default:
  134. $view->categories = FreshRSS_Context::categories();
  135. break;
  136. }
  137. $view->disable_aside = true;
  138. $view->excludeMutedFeeds = true;
  139. $view->internal_rendering = true;
  140. $view->userQuery = $query;
  141. $view->html_url = $query->sharedUrlHtml();
  142. $view->rss_url = $query->sharedUrlRss();
  143. $view->rss_title = $query->getName();
  144. $view->image_url = $query->getImageUrl();
  145. $view->description = $query->getDescription() ?: _t('index.feed.rss_of', $view->rss_title);
  146. if ($query->getName() != '') {
  147. FreshRSS_View::_title($query->getName());
  148. }
  149. FreshRSS_Context::systemConf()->allow_anonymous = true;
  150. header('Access-Control-Allow-Methods: GET');
  151. header('Access-Control-Allow-Origin: *');
  152. header('Access-Control-Max-Age: 600');
  153. header('Cache-Control: public, max-age=60');
  154. if (($_SERVER['REQUEST_METHOD'] ?? '') === 'OPTIONS') {
  155. header('HTTP/1.1 204 No Content');
  156. exit();
  157. }
  158. if (in_array($format, ['rss', 'atom'], true)) {
  159. header('Content-Type: application/rss+xml; charset=utf-8');
  160. $view->_layout(null);
  161. $view->_path('index/rss.phtml');
  162. } elseif (in_array($format, ['greader', 'json'], true)) {
  163. header('Content-Type: application/json; charset=utf-8');
  164. $view->_layout(null);
  165. $view->type = 'query/' . $token;
  166. $view->list_title = $query->getName();
  167. $view->entryIdsTagNames = []; // Do not export user labels for privacy
  168. $view->_path('helpers/export/articles.phtml');
  169. } elseif ($format === 'opml') {
  170. if (!$query->safeForOpml()) {
  171. Minz_Error::error(404, 'OPML not allowed for this user query!');
  172. die();
  173. }
  174. header('Content-Type: application/xml; charset=utf-8');
  175. $view->_layout(null);
  176. $view->_path('index/opml.phtml');
  177. } else {
  178. $view->_layout('layout');
  179. $view->_path('index/html.phtml');
  180. }
  181. $view->build();