mirror of
https://github.com/FreshRSS/FreshRSS.git
synced 2026-02-02 09:31:06 -05:00
This class has not been maintained for a while. Only a subset of our configuration properties are there, and some code is not relevant anymore. Furthermore, it is relying exclusively on dynamically invoked functions, making it challenging to maintain, in particular to find out what is used and what is not, what is handled and what is not. It is not well suited for changes in data formats, which have been handled in the Context class instead. It is also not able to handle configuration properties that are missing. It is the class with most errors for PHPStan level 6 (179 errors). It is also making intense use of is_callable and call_user_func_array, which are performance killers. Should the need arrise again to perform validation of our internal configuration files, I suggest an implementation with the opposite approach, namely driven by our code instead of driven by the data. In summary, at the moment, this class is costly, while not offering many guarantees.
548 lines
15 KiB
PHP
548 lines
15 KiB
PHP
<?php
|
|
|
|
/**
|
|
* The context object handles the current configuration file and different
|
|
* useful functions associated to the current view state.
|
|
*/
|
|
final class FreshRSS_Context {
|
|
|
|
/**
|
|
* @var FreshRSS_UserConfiguration|null
|
|
*/
|
|
public static $user_conf;
|
|
|
|
/**
|
|
* @var FreshRSS_SystemConfiguration|null
|
|
*/
|
|
public static $system_conf;
|
|
/**
|
|
* @var array<FreshRSS_Category>
|
|
*/
|
|
public static $categories = array();
|
|
/**
|
|
* @var array<string>
|
|
*/
|
|
public static $tags = array();
|
|
/**
|
|
* @var string
|
|
*/
|
|
public static $name = '';
|
|
/**
|
|
* @var string
|
|
*/
|
|
public static $description = '';
|
|
/**
|
|
* @var int
|
|
*/
|
|
public static $total_unread = 0;
|
|
|
|
/** @var array{'all':int,'read':int,'unread':int} */
|
|
public static $total_starred = [
|
|
'all' => 0,
|
|
'read' => 0,
|
|
'unread' => 0,
|
|
];
|
|
|
|
/**
|
|
* @var int
|
|
*/
|
|
public static $get_unread = 0;
|
|
|
|
/** @var array{'all':bool,'starred':bool,'feed':int|false,'category':int|false,'tag':int|false,'tags':bool} */
|
|
public static $current_get = [
|
|
'all' => false,
|
|
'starred' => false,
|
|
'feed' => false,
|
|
'category' => false,
|
|
'tag' => false,
|
|
'tags' => false,
|
|
];
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
public static $next_get = 'a';
|
|
/**
|
|
* @var int
|
|
*/
|
|
public static $state = 0;
|
|
/**
|
|
* @var string
|
|
*/
|
|
public static $order = 'DESC';
|
|
/**
|
|
* @var int
|
|
*/
|
|
public static $number = 0;
|
|
/** @var FreshRSS_BooleanSearch */
|
|
public static $search;
|
|
/**
|
|
* @var string
|
|
*/
|
|
public static $first_id = '';
|
|
/**
|
|
* @var string
|
|
*/
|
|
public static $next_id = '';
|
|
/**
|
|
* @var string
|
|
*/
|
|
public static $id_max = '';
|
|
/**
|
|
* @var int
|
|
*/
|
|
public static $sinceHours = 0;
|
|
/**
|
|
* @var bool
|
|
*/
|
|
public static $isCli = false;
|
|
|
|
/**
|
|
* Initialize the context for the global system.
|
|
*/
|
|
public static function initSystem(bool $reload = false): FreshRSS_SystemConfiguration {
|
|
if ($reload || FreshRSS_Context::$system_conf == null) {
|
|
//TODO: Keep in session what we need instead of always reloading from disk
|
|
FreshRSS_Context::$system_conf = FreshRSS_SystemConfiguration::init(DATA_PATH . '/config.php', FRESHRSS_PATH . '/config.default.php');
|
|
}
|
|
return FreshRSS_Context::$system_conf;
|
|
}
|
|
|
|
/**
|
|
* Initialize the context for the current user.
|
|
* @return FreshRSS_UserConfiguration|false
|
|
* @throws Minz_ConfigurationParamException
|
|
*/
|
|
public static function initUser(string $username = '', bool $userMustExist = true) {
|
|
FreshRSS_Context::$user_conf = null;
|
|
if (!isset($_SESSION)) {
|
|
Minz_Session::init('FreshRSS');
|
|
}
|
|
|
|
Minz_Session::lock();
|
|
if ($username == '') {
|
|
$username = Minz_User::name() ?? '';
|
|
}
|
|
if (($username === Minz_User::INTERNAL_USER || FreshRSS_user_Controller::checkUsername($username)) &&
|
|
(!$userMustExist || FreshRSS_user_Controller::userExists($username))) {
|
|
try {
|
|
//TODO: Keep in session what we need instead of always reloading from disk
|
|
FreshRSS_Context::$user_conf = FreshRSS_UserConfiguration::init(
|
|
USERS_PATH . '/' . $username . '/config.php',
|
|
FRESHRSS_PATH . '/config-user.default.php');
|
|
|
|
Minz_User::change($username);
|
|
} catch (Exception $ex) {
|
|
Minz_Log::warning($ex->getMessage(), USERS_PATH . '/_/' . LOG_FILENAME);
|
|
}
|
|
}
|
|
if (FreshRSS_Context::$user_conf == null) {
|
|
Minz_Session::_params([
|
|
'loginOk' => false,
|
|
Minz_User::CURRENT_USER => false,
|
|
]);
|
|
}
|
|
Minz_Session::unlock();
|
|
|
|
if (FreshRSS_Context::$user_conf == null) {
|
|
return false;
|
|
}
|
|
|
|
FreshRSS_Context::$search = new FreshRSS_BooleanSearch('');
|
|
|
|
//Legacy
|
|
$oldEntries = (int)FreshRSS_Context::$user_conf->param('old_entries', 0);
|
|
$keepMin = (int)FreshRSS_Context::$user_conf->param('keep_history_default', -5);
|
|
if ($oldEntries > 0 || $keepMin > -5) { //Freshrss < 1.15
|
|
$archiving = FreshRSS_Context::$user_conf->archiving;
|
|
$archiving['keep_max'] = false;
|
|
if ($oldEntries > 0) {
|
|
$archiving['keep_period'] = 'P' . $oldEntries . 'M';
|
|
}
|
|
if ($keepMin > 0) {
|
|
$archiving['keep_min'] = $keepMin;
|
|
} elseif ($keepMin == -1) { //Infinite
|
|
$archiving['keep_period'] = false;
|
|
$archiving['keep_min'] = false;
|
|
}
|
|
FreshRSS_Context::$user_conf->archiving = $archiving;
|
|
}
|
|
|
|
//Legacy < 1.16.1
|
|
if (!in_array(FreshRSS_Context::$user_conf->display_categories, [ 'active', 'remember', 'all', 'none' ], true)) {
|
|
FreshRSS_Context::$user_conf->display_categories = FreshRSS_Context::$user_conf->display_categories === true ? 'all' : 'active';
|
|
}
|
|
|
|
return FreshRSS_Context::$user_conf;
|
|
}
|
|
|
|
/**
|
|
* This action updates the Context object by using request parameters.
|
|
*
|
|
* Parameters are:
|
|
* - state (default: conf->default_view)
|
|
* - search (default: empty string)
|
|
* - order (default: conf->sort_order)
|
|
* - nb (default: conf->posts_per_page)
|
|
* - next (default: empty string)
|
|
* - hours (default: 0)
|
|
* @throws FreshRSS_Context_Exception
|
|
* @throws Minz_ConfigurationNamespaceException
|
|
* @throws Minz_PDOConnectionException
|
|
*/
|
|
public static function updateUsingRequest(): void {
|
|
if (empty(self::$categories)) {
|
|
$catDAO = FreshRSS_Factory::createCategoryDao();
|
|
self::$categories = $catDAO->listSortedCategories();
|
|
}
|
|
|
|
// Update number of read / unread variables.
|
|
$entryDAO = FreshRSS_Factory::createEntryDao();
|
|
self::$total_starred = $entryDAO->countUnreadReadFavorites();
|
|
self::$total_unread = FreshRSS_CategoryDAO::countUnread(
|
|
self::$categories, 1
|
|
);
|
|
|
|
self::_get(Minz_Request::paramString('get') ?: 'a');
|
|
|
|
self::$state = Minz_Request::paramInt('state') ?: self::$user_conf->default_state;
|
|
$state_forced_by_user = Minz_Request::paramString('state') !== '';
|
|
if (!$state_forced_by_user && !self::isStateEnabled(FreshRSS_Entry::STATE_READ)) {
|
|
if (self::$user_conf->default_view === 'adaptive' && self::$get_unread <= 0) {
|
|
self::$state |= FreshRSS_Entry::STATE_READ;
|
|
}
|
|
if (self::$user_conf->show_fav_unread &&
|
|
(self::isCurrentGet('s') || self::isCurrentGet('T') || self::isTag())) {
|
|
self::$state |= FreshRSS_Entry::STATE_READ;
|
|
}
|
|
}
|
|
|
|
self::$search = new FreshRSS_BooleanSearch(Minz_Request::paramString('search'));
|
|
self::$order = Minz_Request::paramString('order') ?: self::$user_conf->sort_order;
|
|
self::$number = Minz_Request::paramInt('nb') ?: self::$user_conf->posts_per_page;
|
|
if (self::$number > self::$user_conf->max_posts_per_rss) {
|
|
self::$number = max(
|
|
self::$user_conf->max_posts_per_rss,
|
|
self::$user_conf->posts_per_page);
|
|
}
|
|
self::$first_id = Minz_Request::paramString('next');
|
|
self::$sinceHours = Minz_Request::paramInt('hours');
|
|
}
|
|
|
|
/**
|
|
* Returns if the current state includes $state parameter.
|
|
*/
|
|
public static function isStateEnabled(int $state): int {
|
|
return self::$state & $state;
|
|
}
|
|
|
|
/**
|
|
* Returns the current state with or without $state parameter.
|
|
*/
|
|
public static function getRevertState(int $state): int {
|
|
if (self::$state & $state) {
|
|
return self::$state & ~$state;
|
|
}
|
|
return self::$state | $state;
|
|
}
|
|
|
|
/**
|
|
* Return the current get as a string or an array.
|
|
*
|
|
* If $array is true, the first item of the returned value is 'f' or 'c' and
|
|
* the second is the id.
|
|
* @return string|array{string,bool|int}
|
|
*/
|
|
public static function currentGet(bool $asArray = false) {
|
|
if (self::$current_get['all']) {
|
|
return 'a';
|
|
} elseif (self::$current_get['starred']) {
|
|
return 's';
|
|
} elseif (self::$current_get['feed']) {
|
|
if ($asArray) {
|
|
return array('f', self::$current_get['feed']);
|
|
} else {
|
|
return 'f_' . self::$current_get['feed'];
|
|
}
|
|
} elseif (self::$current_get['category']) {
|
|
if ($asArray) {
|
|
return array('c', self::$current_get['category']);
|
|
} else {
|
|
return 'c_' . self::$current_get['category'];
|
|
}
|
|
} elseif (self::$current_get['tag']) {
|
|
if ($asArray) {
|
|
return array('t', self::$current_get['tag']);
|
|
} else {
|
|
return 't_' . self::$current_get['tag'];
|
|
}
|
|
} elseif (self::$current_get['tags']) {
|
|
return 'T';
|
|
}
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* @return bool true if the current request targets all feeds (main view), false otherwise.
|
|
*/
|
|
public static function isAll(): bool {
|
|
return self::$current_get['all'] != false;
|
|
}
|
|
|
|
/**
|
|
* @return bool true if the current request targets a category, false otherwise.
|
|
*/
|
|
public static function isCategory(): bool {
|
|
return self::$current_get['category'] != false;
|
|
}
|
|
|
|
/**
|
|
* @return bool true if the current request targets a feed (and not a category or all articles), false otherwise.
|
|
*/
|
|
public static function isFeed(): bool {
|
|
return self::$current_get['feed'] != false;
|
|
}
|
|
|
|
/**
|
|
* @return bool true if the current request targets a tag (though not all tags), false otherwise.
|
|
*/
|
|
public static function isTag(): bool {
|
|
return self::$current_get['tag'] != false;
|
|
}
|
|
|
|
/**
|
|
* @return bool whether $get parameter corresponds to the $current_get attribute.
|
|
*/
|
|
public static function isCurrentGet(string $get): bool {
|
|
$type = substr($get, 0, 1);
|
|
$id = substr($get, 2);
|
|
|
|
switch($type) {
|
|
case 'a':
|
|
return self::$current_get['all'];
|
|
case 's':
|
|
return self::$current_get['starred'];
|
|
case 'f':
|
|
return self::$current_get['feed'] == $id;
|
|
case 'c':
|
|
return self::$current_get['category'] == $id;
|
|
case 't':
|
|
return self::$current_get['tag'] == $id;
|
|
case 'T':
|
|
return self::$current_get['tags'] || self::$current_get['tag'];
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the current $get attribute.
|
|
*
|
|
* Valid $get parameter are:
|
|
* - a
|
|
* - s
|
|
* - f_<feed id>
|
|
* - c_<category id>
|
|
* - t_<tag id>
|
|
*
|
|
* $name and $get_unread attributes are also updated as $next_get
|
|
* Raise an exception if id or $get is invalid.
|
|
* @throws FreshRSS_Context_Exception
|
|
* @throws Minz_ConfigurationNamespaceException
|
|
* @throws Minz_PDOConnectionException
|
|
*/
|
|
public static function _get(string $get): void {
|
|
$type = $get[0];
|
|
$id = (int)substr($get, 2);
|
|
|
|
if (empty(self::$categories)) {
|
|
$catDAO = FreshRSS_Factory::createCategoryDao();
|
|
self::$categories = $catDAO->listCategories();
|
|
}
|
|
|
|
switch($type) {
|
|
case 'a':
|
|
self::$current_get['all'] = true;
|
|
self::$name = _t('index.feed.title');
|
|
self::$description = self::$system_conf->meta_description;
|
|
self::$get_unread = self::$total_unread;
|
|
break;
|
|
case 's':
|
|
self::$current_get['starred'] = true;
|
|
self::$name = _t('index.feed.title_fav');
|
|
self::$description = self::$system_conf->meta_description;
|
|
self::$get_unread = self::$total_starred['unread'];
|
|
|
|
// Update state if favorite is not yet enabled.
|
|
self::$state = self::$state | FreshRSS_Entry::STATE_FAVORITE;
|
|
break;
|
|
case 'f':
|
|
// We try to find the corresponding feed. When allowing robots, always retrieve the full feed including description
|
|
$feed = FreshRSS_Context::$system_conf->allow_robots ? null : FreshRSS_CategoryDAO::findFeed(self::$categories, $id);
|
|
if ($feed === null) {
|
|
$feedDAO = FreshRSS_Factory::createFeedDao();
|
|
$feed = $feedDAO->searchById($id);
|
|
if (!$feed) {
|
|
throw new FreshRSS_Context_Exception('Invalid feed: ' . $id);
|
|
}
|
|
}
|
|
self::$current_get['feed'] = $id;
|
|
self::$current_get['category'] = $feed->categoryId();
|
|
self::$name = $feed->name();
|
|
self::$description = $feed->description();
|
|
self::$get_unread = $feed->nbNotRead();
|
|
break;
|
|
case 'c':
|
|
// We try to find the corresponding category.
|
|
self::$current_get['category'] = $id;
|
|
if (!isset(self::$categories[$id])) {
|
|
$catDAO = FreshRSS_Factory::createCategoryDao();
|
|
$cat = $catDAO->searchById($id);
|
|
if (!$cat) {
|
|
throw new FreshRSS_Context_Exception('Invalid category: ' . $id);
|
|
}
|
|
} else {
|
|
$cat = self::$categories[$id];
|
|
}
|
|
self::$name = $cat->name();
|
|
self::$get_unread = $cat->nbNotRead();
|
|
break;
|
|
case 't':
|
|
// We try to find the corresponding tag.
|
|
self::$current_get['tag'] = $id;
|
|
if (!isset(self::$tags[$id])) {
|
|
$tagDAO = FreshRSS_Factory::createTagDao();
|
|
$tag = $tagDAO->searchById($id);
|
|
if (!$tag) {
|
|
throw new FreshRSS_Context_Exception('Invalid tag: ' . $id);
|
|
}
|
|
} else {
|
|
$tag = self::$tags[$id];
|
|
}
|
|
self::$name = $tag->name();
|
|
self::$get_unread = $tag->nbUnread();
|
|
break;
|
|
case 'T':
|
|
$tagDAO = FreshRSS_Factory::createTagDao();
|
|
self::$current_get['tags'] = true;
|
|
self::$name = _t('index.menu.tags');
|
|
self::$get_unread = $tagDAO->countNotRead();
|
|
break;
|
|
default:
|
|
throw new FreshRSS_Context_Exception('Invalid getter: ' . $get);
|
|
}
|
|
|
|
self::_nextGet();
|
|
}
|
|
|
|
/**
|
|
* Set the value of $next_get attribute.
|
|
*/
|
|
private static function _nextGet(): void {
|
|
$get = self::currentGet();
|
|
// By default, $next_get == $get
|
|
self::$next_get = $get;
|
|
|
|
if (empty(self::$categories)) {
|
|
$catDAO = FreshRSS_Factory::createCategoryDao();
|
|
self::$categories = $catDAO->listCategories();
|
|
}
|
|
|
|
if (self::$user_conf->onread_jump_next && strlen($get) > 2) {
|
|
$another_unread_id = '';
|
|
$found_current_get = false;
|
|
switch ($get[0]) {
|
|
case 'f':
|
|
// We search the next unread feed with the following priorities: next in same category, or previous in same category, or next, or previous.
|
|
foreach (self::$categories as $cat) {
|
|
$sameCat = false;
|
|
foreach ($cat->feeds() as $feed) {
|
|
if ($found_current_get) {
|
|
if ($feed->nbNotRead() > 0) {
|
|
$another_unread_id = $feed->id();
|
|
break 2;
|
|
}
|
|
} elseif ($feed->id() == self::$current_get['feed']) {
|
|
$found_current_get = true;
|
|
} elseif ($feed->nbNotRead() > 0) {
|
|
$another_unread_id = $feed->id();
|
|
$sameCat = true;
|
|
}
|
|
}
|
|
if ($found_current_get && $sameCat) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If there is no more unread feed, show main stream
|
|
self::$next_get = $another_unread_id == '' ? 'a' : 'f_' . $another_unread_id;
|
|
break;
|
|
case 'c':
|
|
// We search the next category with at least one unread article.
|
|
foreach (self::$categories as $cat) {
|
|
if ($cat->id() == self::$current_get['category']) {
|
|
// Here is our current category! Next one could be our
|
|
// champion if it has unread articles.
|
|
$found_current_get = true;
|
|
continue;
|
|
}
|
|
|
|
if ($cat->nbNotRead() > 0) {
|
|
$another_unread_id = $cat->id();
|
|
if ($found_current_get) {
|
|
// Unread articles and the current category has
|
|
// already been found? Leave the loop!
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If there is no more unread category, show main stream
|
|
self::$next_get = $another_unread_id == '' ? 'a' : 'c_' . $another_unread_id;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine if the auto remove is available in the current context.
|
|
* This feature is available if:
|
|
* - it is activated in the configuration
|
|
* - the "read" state is not enable
|
|
* - the "unread" state is enable
|
|
*/
|
|
public static function isAutoRemoveAvailable(): bool {
|
|
if (!self::$user_conf->auto_remove_article) {
|
|
return false;
|
|
}
|
|
if (self::isStateEnabled(FreshRSS_Entry::STATE_READ)) {
|
|
return false;
|
|
}
|
|
if (!self::isStateEnabled(FreshRSS_Entry::STATE_NOT_READ)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Determine if the "sticky post" option is enabled. It can be enable
|
|
* by the user when it is selected in the configuration page or by the
|
|
* application when the context allows to auto-remove articles when they
|
|
* are read.
|
|
*/
|
|
public static function isStickyPostEnabled(): bool {
|
|
if (self::$user_conf->sticky_post) {
|
|
return true;
|
|
}
|
|
if (self::isAutoRemoveAvailable()) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public static function defaultTimeZone(): string {
|
|
$timezone = ini_get('date.timezone');
|
|
return $timezone != '' ? $timezone : 'UTC';
|
|
}
|
|
}
|