query.php 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  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 (Minz_Request::pathInfo() !== '') {
  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 'html':
  66. case 'rss':
  67. if (empty($raw_query['shareRss'])) {
  68. continue 2;
  69. }
  70. break;
  71. case 'greader':
  72. case 'json':
  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. if (Minz_Request::paramString('sort', plaintext: true) === '') {
  87. Minz_Request::_param('sort', $query->getSort());
  88. }
  89. Minz_Request::_param('state', (string)$query->getState());
  90. $search = $query->getSearch()->toString();
  91. // Note: we disallow references to user queries in public user search to avoid sniffing internal user queries
  92. try {
  93. $userSearch = new FreshRSS_BooleanSearch(Minz_Request::paramString('search', plaintext: true), 0, 'AND', allowUserQueries: false);
  94. } catch (Minz_BadRequestException $e) {
  95. header('HTTP/1.1 400 Bad Request');
  96. header('Content-Type: text/plain; charset=UTF-8');
  97. die($e->getMessage());
  98. }
  99. if ($userSearch->toString() !== '') {
  100. if ($search === '') {
  101. $search = $userSearch->toString();
  102. } else {
  103. $search .= ' (' . $userSearch->toString() . ')';
  104. }
  105. }
  106. Minz_Request::_param('search', $search);
  107. break;
  108. }
  109. }
  110. if ($query === null || $userSearch === null) {
  111. usleep(rand(100, 10000));
  112. header('HTTP/1.1 404 Not Found');
  113. header('Content-Type: text/plain; charset=UTF-8');
  114. die('User query not found!');
  115. }
  116. $view = new FreshRSS_View();
  117. try {
  118. FreshRSS_Context::updateUsingRequest(false);
  119. $view->entries = FreshRSS_index_Controller::listEntriesByContext();
  120. if (!$view->entries->valid()) { // Init the generator to consume the aggregated search and catch potential exceptions
  121. $view->entries = new EmptyIterator();
  122. }
  123. Minz_Request::_param('search', $userSearch->toString()); // Restore user search for display and exports
  124. FreshRSS_Context::$search = $userSearch; // Restore user search for display and exports
  125. } catch (Minz_Exception) {
  126. Minz_Error::error(400, 'Bad user query!');
  127. die();
  128. }
  129. $get = FreshRSS_Context::currentGet(true);
  130. $type = (string)$get[0];
  131. $id = (int)$get[1];
  132. switch ($type) {
  133. case 'c': // Category
  134. $cat = FreshRSS_Context::categories()[$id] ?? null;
  135. if ($cat === null) {
  136. Minz_Error::error(404, "Category {$id} not found!");
  137. die();
  138. }
  139. $view->categories = [$cat->id() => $cat];
  140. break;
  141. case 'f': // Feed
  142. $feed = FreshRSS_Category::findFeed(FreshRSS_Context::categories(), $id);
  143. if ($feed === null) {
  144. Minz_Error::error(404, "Feed {$id} not found!");
  145. die();
  146. }
  147. $view->feeds = [$id => $feed];
  148. $view->categories = [];
  149. break;
  150. default:
  151. $view->categories = FreshRSS_Context::categories();
  152. break;
  153. }
  154. $view->disable_aside = true;
  155. $view->excludeMutedFeeds = true;
  156. $view->internal_rendering = true;
  157. $view->userQuery = $query;
  158. $view->html_url = $query->sharedUrlHtml();
  159. $view->rss_url = $query->sharedUrlRss();
  160. $view->rss_title = $query->getName();
  161. $view->image_url = $query->getImageUrl();
  162. $view->description = $query->getDescription() ?: _t('index.feed.rss_of', $view->rss_title);
  163. $view->includeUserLabels = $query->includeUserLabels();
  164. $view->excludeArticleTags = $query->excludeArticleTags();
  165. $view->userLabelPrefix = $query->userLabelPrefix();
  166. $view->entryIdsTagNames = [];
  167. if ($view->includeUserLabels && in_array($format, ['rss', 'atom'], true)) {
  168. $entries = iterator_to_array($view->entries, preserve_keys: false); // TODO: Optimise: avoid iterator_to_array if possible
  169. $view->entries = $entries;
  170. if (!empty($entries)) {
  171. $tagDAO = FreshRSS_Factory::createTagDao();
  172. $view->entryIdsTagNames = $tagDAO->getEntryIdsTagNames($entries);
  173. }
  174. }
  175. if ($query->getName() != '') {
  176. FreshRSS_View::_title($query->getName());
  177. }
  178. FreshRSS_Context::systemConf()->allow_anonymous = true;
  179. header('Access-Control-Allow-Methods: GET');
  180. header('Access-Control-Allow-Origin: *');
  181. header('Access-Control-Max-Age: 600');
  182. header('Cache-Control: public, max-age=60');
  183. if (($_SERVER['REQUEST_METHOD'] ?? '') === 'OPTIONS') {
  184. header('HTTP/1.1 204 No Content');
  185. exit();
  186. }
  187. if (in_array($format, ['rss', 'atom'], true)) {
  188. header('Content-Type: application/rss+xml; charset=utf-8');
  189. header("Content-Security-Policy: default-src 'none'; sandbox; frame-ancestors " .
  190. (FreshRSS_Context::systemConf()->attributeString('csp.frame-ancestors') ?? "'none'"));
  191. $view->_layout(null);
  192. $view->_path('index/rss.phtml');
  193. } elseif (in_array($format, ['greader', 'json'], true)) {
  194. header('Content-Type: application/json; charset=utf-8');
  195. header("Content-Security-Policy: default-src 'none'; sandbox; frame-ancestors " .
  196. (FreshRSS_Context::systemConf()->attributeString('csp.frame-ancestors') ?? "'none'"));
  197. $view->_layout(null);
  198. $view->type = 'query/' . $token;
  199. $view->list_title = $query->getName();
  200. $view->entryIdsTagNames = []; // Do not export user labels for privacy
  201. $view->_path('helpers/export/articles.phtml');
  202. } elseif ($format === 'opml') {
  203. if (!$query->safeForOpml()) {
  204. Minz_Error::error(404, 'OPML not allowed for this user query!');
  205. die();
  206. }
  207. header('Content-Type: application/xml; charset=utf-8');
  208. header("Content-Security-Policy: default-src 'none'; sandbox; frame-ancestors " .
  209. (FreshRSS_Context::systemConf()->attributeString('csp.frame-ancestors') ?? "'none'"));
  210. $view->_layout(null);
  211. $view->_path('index/opml.phtml');
  212. } else {
  213. header("Content-Security-Policy: default-src 'self'; frame-src *; img-src * data:; media-src *; frame-ancestors " .
  214. (FreshRSS_Context::systemConf()->attributeString('csp.frame-ancestors') ?? "'none'"));
  215. $view->_layout('layout');
  216. $view->_path('index/html.phtml');
  217. }
  218. $view->build();