4
0

query.php 7.9 KB

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