query.php 6.6 KB

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