query.php 7.5 KB

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