mirror of
https://github.com/FreshRSS/FreshRSS.git
synced 2026-09-21 18:47:17 -04:00
* Gate greader/json share formats behind Share by OPML Follow-up to #9246, per @Alkarex's suggestion on GHSA-rpmm-h4hx-5p6p. The greader and json share formats expose origin.feedUrl (the feed's subscription URL). Sharing feed URLs is what the "Share by OPML" option explicitly means, whereas "Share by RSS" is meant to share article content. This moves greader and json in p/api/query.php from the shareRss gate to the shareOpml gate, so feed URLs are only served when the owner opted into OPML sharing. Note: moved json alongside greader too, since both emit feedUrl (same toGReader path). * sharedUrlGreader: gate on shareOpml * Move greader share link under Share by OPML group
232 lines
8.0 KiB
PHP
232 lines
8.0 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
header('X-Content-Type-Options: nosniff');
|
|
|
|
require dirname(__DIR__, 2) . '/constants.php';
|
|
require LIB_PATH . '/lib_rss.php'; //Includes class autoloader
|
|
|
|
Minz_Request::init();
|
|
|
|
$token = Minz_Request::paramString('t', plaintext: true);
|
|
if (!ctype_alnum($token)) {
|
|
header('HTTP/1.1 422 Unprocessable Entity');
|
|
header('Content-Type: text/plain; charset=UTF-8');
|
|
die('Invalid token `t`!' . $token);
|
|
}
|
|
|
|
$format = Minz_Request::paramString('f', plaintext: true);
|
|
if (!in_array($format, ['atom', 'greader', 'html', 'json', 'opml', 'rss'], true)) {
|
|
header('HTTP/1.1 422 Unprocessable Entity');
|
|
header('Content-Type: text/plain; charset=UTF-8');
|
|
die('Invalid format `f`!');
|
|
}
|
|
|
|
$user = Minz_Request::paramString('user', plaintext: true);
|
|
if (!FreshRSS_user_Controller::checkUsername($user)) {
|
|
header('HTTP/1.1 422 Unprocessable Entity');
|
|
header('Content-Type: text/plain; charset=UTF-8');
|
|
die('Invalid user!');
|
|
}
|
|
|
|
Minz_Session::init('FreshRSS', true);
|
|
|
|
FreshRSS_Context::initSystem();
|
|
if (!FreshRSS_Context::hasSystemConf() || !FreshRSS_Context::systemConf()->api_enabled) {
|
|
header('HTTP/1.1 503 Service Unavailable');
|
|
header('Content-Type: text/plain; charset=UTF-8');
|
|
die('Service Unavailable!');
|
|
}
|
|
|
|
if (($_SERVER['PATH_INFO'] ?? $_SERVER['ORIG_PATH_INFO'] ?? '') !== '') {
|
|
// Do not allow trailing slashes
|
|
header('HTTP/1.1 400 Bad Request');
|
|
die('Invalid path!');
|
|
}
|
|
|
|
FreshRSS_Context::initUser($user);
|
|
if (!FreshRSS_Context::hasUserConf() || !FreshRSS_Context::userConf()->enabled) {
|
|
usleep(rand(100, 10000)); //Primitive mitigation of scanning for users
|
|
header('HTTP/1.1 404 Not Found');
|
|
header('Content-Type: text/plain; charset=UTF-8');
|
|
die('User not found!');
|
|
} else {
|
|
usleep(rand(20, 200));
|
|
}
|
|
|
|
require LIB_PATH . '/http-conditional.php';
|
|
$dateLastModification = max(
|
|
FreshRSS_UserDAO::ctime($user),
|
|
FreshRSS_UserDAO::mtime($user),
|
|
@filemtime(DATA_PATH . '/config.php') ?: 0
|
|
);
|
|
// TODO: Consider taking advantage of $feedMode, only for monotonous queries {all, categories, feeds} and not dynamic ones {read/unread, favourites, user labels}
|
|
if (!file_exists(DATA_PATH . '/no-cache.txt') && httpConditional($dateLastModification ?: time(), 0, 0, false, PHP_COMPRESSION, false)) {
|
|
exit(); //No need to send anything
|
|
}
|
|
|
|
Minz_Translate::init(FreshRSS_Context::userConf()->language);
|
|
Minz_ExtensionManager::init();
|
|
Minz_ExtensionManager::enableByList(FreshRSS_Context::userConf()->extensions_enabled, 'user');
|
|
|
|
$query = null;
|
|
$userSearch = null;
|
|
foreach (FreshRSS_Context::userConf()->queries as $raw_query) {
|
|
if (!empty($raw_query['token']) && hash_equals($raw_query['token'], $token)) {
|
|
switch ($format) {
|
|
case 'atom':
|
|
case 'html':
|
|
case 'rss':
|
|
if (empty($raw_query['shareRss'])) {
|
|
continue 2;
|
|
}
|
|
break;
|
|
case 'greader':
|
|
case 'json':
|
|
case 'opml':
|
|
if (empty($raw_query['shareOpml'])) {
|
|
continue 2;
|
|
}
|
|
break;
|
|
default:
|
|
continue 2;
|
|
}
|
|
$query = new FreshRSS_UserQuery($raw_query, FreshRSS_Context::categories(), FreshRSS_Context::labels());
|
|
Minz_Request::_param('get', $query->getGet());
|
|
if (Minz_Request::paramString('order', plaintext: true) === '') {
|
|
Minz_Request::_param('order', $query->getOrder());
|
|
}
|
|
Minz_Request::_param('state', (string)$query->getState());
|
|
|
|
$search = $query->getSearch()->toString();
|
|
// Note: we disallow references to user queries in public user search to avoid sniffing internal user queries
|
|
$userSearch = new FreshRSS_BooleanSearch(Minz_Request::paramString('search', plaintext: true), 0, 'AND', allowUserQueries: false);
|
|
if ($userSearch->toString() !== '') {
|
|
if ($search === '') {
|
|
$search = $userSearch->toString();
|
|
} else {
|
|
$search .= ' (' . $userSearch->toString() . ')';
|
|
}
|
|
}
|
|
Minz_Request::_param('search', $search);
|
|
break;
|
|
}
|
|
}
|
|
if ($query === null || $userSearch === null) {
|
|
usleep(rand(100, 10000));
|
|
header('HTTP/1.1 404 Not Found');
|
|
header('Content-Type: text/plain; charset=UTF-8');
|
|
die('User query not found!');
|
|
}
|
|
|
|
$view = new FreshRSS_View();
|
|
|
|
try {
|
|
FreshRSS_Context::updateUsingRequest(false);
|
|
$view->entries = FreshRSS_index_Controller::listEntriesByContext();
|
|
if (!$view->entries->valid()) { // Init the generator to consume the aggregated search and catch potential exceptions
|
|
$view->entries = new EmptyIterator();
|
|
}
|
|
Minz_Request::_param('search', $userSearch->toString()); // Restore user search for display and exports
|
|
FreshRSS_Context::$search = $userSearch; // Restore user search for display and exports
|
|
} catch (Minz_Exception) {
|
|
Minz_Error::error(400, 'Bad user query!');
|
|
die();
|
|
}
|
|
|
|
$get = FreshRSS_Context::currentGet(true);
|
|
$type = (string)$get[0];
|
|
$id = (int)$get[1];
|
|
|
|
switch ($type) {
|
|
case 'c': // Category
|
|
$cat = FreshRSS_Context::categories()[$id] ?? null;
|
|
if ($cat === null) {
|
|
Minz_Error::error(404, "Category {$id} not found!");
|
|
die();
|
|
}
|
|
$view->categories = [$cat->id() => $cat];
|
|
break;
|
|
case 'f': // Feed
|
|
$feed = FreshRSS_Category::findFeed(FreshRSS_Context::categories(), $id);
|
|
if ($feed === null) {
|
|
Minz_Error::error(404, "Feed {$id} not found!");
|
|
die();
|
|
}
|
|
$view->feeds = [$id => $feed];
|
|
$view->categories = [];
|
|
break;
|
|
default:
|
|
$view->categories = FreshRSS_Context::categories();
|
|
break;
|
|
}
|
|
|
|
$view->disable_aside = true;
|
|
$view->excludeMutedFeeds = true;
|
|
$view->internal_rendering = true;
|
|
$view->userQuery = $query;
|
|
$view->html_url = $query->sharedUrlHtml();
|
|
$view->rss_url = $query->sharedUrlRss();
|
|
$view->rss_title = $query->getName();
|
|
$view->image_url = $query->getImageUrl();
|
|
$view->description = $query->getDescription() ?: _t('index.feed.rss_of', $view->rss_title);
|
|
$view->includeUserLabels = $query->includeUserLabels();
|
|
$view->excludeArticleTags = $query->excludeArticleTags();
|
|
$view->userLabelPrefix = $query->userLabelPrefix();
|
|
$view->entryIdsTagNames = [];
|
|
if ($view->includeUserLabels && in_array($format, ['rss', 'atom'], true)) {
|
|
$entries = iterator_to_array($view->entries, preserve_keys: false); // TODO: Optimise: avoid iterator_to_array if possible
|
|
$view->entries = $entries;
|
|
if (!empty($entries)) {
|
|
$tagDAO = FreshRSS_Factory::createTagDao();
|
|
$view->entryIdsTagNames = $tagDAO->getEntryIdsTagNames($entries);
|
|
}
|
|
}
|
|
if ($query->getName() != '') {
|
|
FreshRSS_View::_title($query->getName());
|
|
}
|
|
FreshRSS_Context::systemConf()->allow_anonymous = true;
|
|
|
|
header('Access-Control-Allow-Methods: GET');
|
|
header('Access-Control-Allow-Origin: *');
|
|
header('Access-Control-Max-Age: 600');
|
|
header('Cache-Control: public, max-age=60');
|
|
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'OPTIONS') {
|
|
header('HTTP/1.1 204 No Content');
|
|
exit();
|
|
}
|
|
|
|
if (in_array($format, ['rss', 'atom'], true)) {
|
|
header('Content-Type: application/rss+xml; charset=utf-8');
|
|
header("Content-Security-Policy: default-src 'none'; sandbox; frame-ancestors " .
|
|
(FreshRSS_Context::systemConf()->attributeString('csp.frame-ancestors') ?? "'none'"));
|
|
$view->_layout(null);
|
|
$view->_path('index/rss.phtml');
|
|
} elseif (in_array($format, ['greader', 'json'], true)) {
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
header("Content-Security-Policy: default-src 'none'; sandbox; frame-ancestors " .
|
|
(FreshRSS_Context::systemConf()->attributeString('csp.frame-ancestors') ?? "'none'"));
|
|
$view->_layout(null);
|
|
$view->type = 'query/' . $token;
|
|
$view->list_title = $query->getName();
|
|
$view->entryIdsTagNames = []; // Do not export user labels for privacy
|
|
$view->_path('helpers/export/articles.phtml');
|
|
} elseif ($format === 'opml') {
|
|
if (!$query->safeForOpml()) {
|
|
Minz_Error::error(404, 'OPML not allowed for this user query!');
|
|
die();
|
|
}
|
|
header('Content-Type: application/xml; charset=utf-8');
|
|
header("Content-Security-Policy: default-src 'none'; sandbox; frame-ancestors " .
|
|
(FreshRSS_Context::systemConf()->attributeString('csp.frame-ancestors') ?? "'none'"));
|
|
$view->_layout(null);
|
|
$view->_path('index/opml.phtml');
|
|
} else {
|
|
header("Content-Security-Policy: default-src 'self'; frame-src *; img-src * data:; media-src *; frame-ancestors " .
|
|
(FreshRSS_Context::systemConf()->attributeString('csp.frame-ancestors') ?? "'none'"));
|
|
$view->_layout('layout');
|
|
$view->_path('index/html.phtml');
|
|
}
|
|
|
|
$view->build();
|