mirror of
https://github.com/FreshRSS/FreshRSS.git
synced 2026-02-24 18:48:16 -05:00
As discussed on https://github.com/marienfressinaud/FreshRSS/issues/493#issuecomment-56266415 For performance, but this is also a good simplification. Will help with PostgreSQL https://github.com/marienfressinaud/FreshRSS/issues/416 and SQLite too https://github.com/marienfressinaud/FreshRSS/issues/100 because the main query becomes simpler. Me may need to introduce another system to hide old articles, if this is a problem for some users. Also, the feature showing "empty feeds" in another colour may need to be adapted.
493 lines
16 KiB
PHP
Executable File
493 lines
16 KiB
PHP
Executable File
<?php
|
|
|
|
class FreshRSS_index_Controller extends Minz_ActionController {
|
|
private $nb_not_read_cat = 0;
|
|
|
|
public function indexAction () {
|
|
$output = Minz_Request::param ('output');
|
|
$token = $this->view->conf->token;
|
|
|
|
// check if user is logged in
|
|
if (!$this->view->loginOk && !Minz_Configuration::allowAnonymous()) {
|
|
$token_param = Minz_Request::param ('token', '');
|
|
$token_is_ok = ($token != '' && $token === $token_param);
|
|
if ($output === 'rss' && !$token_is_ok) {
|
|
Minz_Error::error (
|
|
403,
|
|
array ('error' => array (Minz_Translate::t ('access_denied')))
|
|
);
|
|
return;
|
|
} elseif ($output !== 'rss') {
|
|
// "hard" redirection is not required, just ask dispatcher to
|
|
// forward to the login form without 302 redirection
|
|
Minz_Request::forward(array('c' => 'index', 'a' => 'formLogin'));
|
|
return;
|
|
}
|
|
}
|
|
|
|
$params = Minz_Request::params ();
|
|
if (isset ($params['search'])) {
|
|
$params['search'] = urlencode ($params['search']);
|
|
}
|
|
|
|
$this->view->url = array (
|
|
'c' => 'index',
|
|
'a' => 'index',
|
|
'params' => $params
|
|
);
|
|
|
|
if ($output === 'rss') {
|
|
// no layout for RSS output
|
|
$this->view->_useLayout (false);
|
|
header('Content-Type: application/rss+xml; charset=utf-8');
|
|
} elseif ($output === 'global') {
|
|
Minz_View::appendScript (Minz_Url::display ('/scripts/global_view.js?' . @filemtime(PUBLIC_PATH . '/scripts/global_view.js')));
|
|
}
|
|
|
|
$catDAO = new FreshRSS_CategoryDAO();
|
|
$entryDAO = FreshRSS_Factory::createEntryDao();
|
|
|
|
$this->view->cat_aside = $catDAO->listCategories ();
|
|
$this->view->nb_favorites = $entryDAO->countUnreadReadFavorites ();
|
|
$this->view->nb_not_read = FreshRSS_CategoryDAO::CountUnreads($this->view->cat_aside, 1);
|
|
$this->view->currentName = '';
|
|
|
|
$this->view->get_c = '';
|
|
$this->view->get_f = '';
|
|
|
|
$get = Minz_Request::param ('get', 'a');
|
|
$getType = $get[0];
|
|
$getId = substr ($get, 2);
|
|
if (!$this->checkAndProcessType ($getType, $getId)) {
|
|
Minz_Log::record ('Not found [' . $getType . '][' . $getId . ']', Minz_Log::DEBUG);
|
|
Minz_Error::error (
|
|
404,
|
|
array ('error' => array (Minz_Translate::t ('page_not_found')))
|
|
);
|
|
return;
|
|
}
|
|
|
|
// mise à jour des titres
|
|
$this->view->rss_title = $this->view->currentName . ' | ' . Minz_View::title();
|
|
Minz_View::prependTitle(
|
|
($this->nb_not_read_cat > 0 ? '(' . formatNumber($this->nb_not_read_cat) . ') ' : '') .
|
|
$this->view->currentName .
|
|
' · '
|
|
);
|
|
|
|
// On récupère les différents éléments de filtrage
|
|
$this->view->state = Minz_Request::param('state', $this->view->conf->default_view);
|
|
$state_param = Minz_Request::param ('state', null);
|
|
$filter = Minz_Request::param ('search', '');
|
|
$this->view->order = $order = Minz_Request::param ('order', $this->view->conf->sort_order);
|
|
$nb = Minz_Request::param ('nb', $this->view->conf->posts_per_page);
|
|
$first = Minz_Request::param ('next', '');
|
|
|
|
$ajax_request = Minz_Request::param('ajax', false);
|
|
if ($output === 'reader') {
|
|
$nb = max(1, round($nb / 2));
|
|
}
|
|
|
|
if ($this->view->state === FreshRSS_Entry::STATE_NOT_READ) { //Any unread article in this category at all?
|
|
switch ($getType) {
|
|
case 'a':
|
|
$hasUnread = $this->view->nb_not_read > 0;
|
|
break;
|
|
case 's':
|
|
// This is deprecated. The favorite button does not exist anymore
|
|
$hasUnread = $this->view->nb_favorites['unread'] > 0;
|
|
break;
|
|
case 'c':
|
|
$hasUnread = (!isset($this->view->cat_aside[$getId])) || ($this->view->cat_aside[$getId]->nbNotRead() > 0);
|
|
break;
|
|
case 'f':
|
|
$myFeed = FreshRSS_CategoryDAO::findFeed($this->view->cat_aside, $getId);
|
|
$hasUnread = ($myFeed === null) || ($myFeed->nbNotRead() > 0);
|
|
break;
|
|
default:
|
|
$hasUnread = true;
|
|
break;
|
|
}
|
|
if (!$hasUnread && ($state_param === null)) {
|
|
$this->view->state = FreshRSS_Entry::STATE_ALL;
|
|
}
|
|
}
|
|
|
|
$this->view->today = @strtotime('today');
|
|
|
|
try {
|
|
$entries = $entryDAO->listWhere($getType, $getId, $this->view->state, $order, $nb + 1, $first, $filter);
|
|
|
|
// Si on a récupéré aucun article "non lus"
|
|
// on essaye de récupérer tous les articles
|
|
if ($this->view->state === FreshRSS_Entry::STATE_NOT_READ && empty($entries) && ($state_param === null) && ($filter == '')) {
|
|
Minz_Log::record('Conflicting information about nbNotRead!', Minz_Log::DEBUG);
|
|
$feedDAO = FreshRSS_Factory::createFeedDao();
|
|
try {
|
|
$feedDAO->updateCachedValues();
|
|
} catch (Exception $ex) {
|
|
Minz_Log::record('Failed to automatically correct nbNotRead! ' + $ex->getMessage(), Minz_Log::NOTICE);
|
|
}
|
|
$this->view->state = FreshRSS_Entry::STATE_ALL;
|
|
$entries = $entryDAO->listWhere($getType, $getId, $this->view->state, $order, $nb, $first, $filter);
|
|
}
|
|
Minz_Request::_param('state', $this->view->state);
|
|
|
|
if (count($entries) <= $nb) {
|
|
$this->view->nextId = '';
|
|
} else { //We have more elements for pagination
|
|
$lastEntry = array_pop($entries);
|
|
$this->view->nextId = $lastEntry->id();
|
|
}
|
|
|
|
$this->view->entries = $entries;
|
|
} catch (FreshRSS_EntriesGetter_Exception $e) {
|
|
Minz_Log::record ($e->getMessage (), Minz_Log::NOTICE);
|
|
Minz_Error::error (
|
|
404,
|
|
array ('error' => array (Minz_Translate::t ('page_not_found')))
|
|
);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Vérifie que la catégorie / flux sélectionné existe
|
|
* + Initialise correctement les variables de vue get_c et get_f
|
|
* + Met à jour la variable $this->nb_not_read_cat
|
|
*/
|
|
private function checkAndProcessType ($getType, $getId) {
|
|
switch ($getType) {
|
|
case 'a':
|
|
$this->view->currentName = Minz_Translate::t ('your_rss_feeds');
|
|
$this->nb_not_read_cat = $this->view->nb_not_read;
|
|
$this->view->get_c = $getType;
|
|
return true;
|
|
case 's':
|
|
$this->view->currentName = Minz_Translate::t ('your_favorites');
|
|
$this->nb_not_read_cat = $this->view->nb_favorites['unread'];
|
|
$this->view->get_c = $getType;
|
|
return true;
|
|
case 'c':
|
|
$cat = isset($this->view->cat_aside[$getId]) ? $this->view->cat_aside[$getId] : null;
|
|
if ($cat === null) {
|
|
$catDAO = new FreshRSS_CategoryDAO();
|
|
$cat = $catDAO->searchById($getId);
|
|
}
|
|
if ($cat) {
|
|
$this->view->currentName = $cat->name ();
|
|
$this->nb_not_read_cat = $cat->nbNotRead ();
|
|
$this->view->get_c = $getId;
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
case 'f':
|
|
$feed = FreshRSS_CategoryDAO::findFeed($this->view->cat_aside, $getId);
|
|
if (empty($feed)) {
|
|
$feedDAO = FreshRSS_Factory::createFeedDao();
|
|
$feed = $feedDAO->searchById($getId);
|
|
}
|
|
if ($feed) {
|
|
$this->view->currentName = $feed->name ();
|
|
$this->nb_not_read_cat = $feed->nbNotRead ();
|
|
$this->view->get_f = $getId;
|
|
$this->view->get_c = $feed->category ();
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public function aboutAction () {
|
|
Minz_View::prependTitle (Minz_Translate::t ('about') . ' · ');
|
|
}
|
|
|
|
public function logsAction () {
|
|
if (!$this->view->loginOk) {
|
|
Minz_Error::error (
|
|
403,
|
|
array ('error' => array (Minz_Translate::t ('access_denied')))
|
|
);
|
|
}
|
|
|
|
Minz_View::prependTitle (Minz_Translate::t ('logs') . ' · ');
|
|
|
|
if (Minz_Request::isPost ()) {
|
|
FreshRSS_LogDAO::truncate();
|
|
}
|
|
|
|
$logs = FreshRSS_LogDAO::lines(); //TODO: ask only the necessary lines
|
|
|
|
//gestion pagination
|
|
$page = Minz_Request::param ('page', 1);
|
|
$this->view->logsPaginator = new Minz_Paginator ($logs);
|
|
$this->view->logsPaginator->_nbItemsPerPage (50);
|
|
$this->view->logsPaginator->_currentPage ($page);
|
|
}
|
|
|
|
public function loginAction () {
|
|
$this->view->_useLayout (false);
|
|
|
|
$url = 'https://verifier.login.persona.org/verify';
|
|
$assert = Minz_Request::param ('assertion');
|
|
$params = 'assertion=' . $assert . '&audience=' .
|
|
urlencode (Minz_Url::display (null, 'php', true));
|
|
$ch = curl_init ();
|
|
$options = array (
|
|
CURLOPT_URL => $url,
|
|
CURLOPT_RETURNTRANSFER => TRUE,
|
|
CURLOPT_POST => 2,
|
|
CURLOPT_POSTFIELDS => $params
|
|
);
|
|
curl_setopt_array ($ch, $options);
|
|
$result = curl_exec ($ch);
|
|
curl_close ($ch);
|
|
|
|
$res = json_decode ($result, true);
|
|
|
|
$loginOk = false;
|
|
$reason = '';
|
|
if ($res['status'] === 'okay') {
|
|
$email = filter_var($res['email'], FILTER_VALIDATE_EMAIL);
|
|
if ($email != '') {
|
|
$personaFile = DATA_PATH . '/persona/' . $email . '.txt';
|
|
if (($currentUser = @file_get_contents($personaFile)) !== false) {
|
|
$currentUser = trim($currentUser);
|
|
if (ctype_alnum($currentUser)) {
|
|
try {
|
|
$this->conf = new FreshRSS_Configuration($currentUser);
|
|
$loginOk = strcasecmp($email, $this->conf->mail_login) === 0;
|
|
} catch (Minz_Exception $e) {
|
|
$reason = 'Invalid configuration for user [' . $currentUser . ']! ' . $e->getMessage(); //Permission denied or conf file does not exist
|
|
}
|
|
} else {
|
|
$reason = 'Invalid username format [' . $currentUser . ']!';
|
|
}
|
|
}
|
|
} else {
|
|
$reason = 'Invalid email format [' . $res['email'] . ']!';
|
|
}
|
|
}
|
|
if ($loginOk) {
|
|
Minz_Session::_param('currentUser', $currentUser);
|
|
Minz_Session::_param ('mail', $email);
|
|
$this->view->loginOk = true;
|
|
invalidateHttpCache();
|
|
} else {
|
|
$res = array ();
|
|
$res['status'] = 'failure';
|
|
$res['reason'] = $reason == '' ? Minz_Translate::t ('invalid_login') : $reason;
|
|
Minz_Log::record ('Persona: ' . $res['reason'], Minz_Log::WARNING);
|
|
}
|
|
|
|
header('Content-Type: application/json; charset=UTF-8');
|
|
$this->view->res = json_encode ($res);
|
|
}
|
|
|
|
public function logoutAction () {
|
|
$this->view->_useLayout(false);
|
|
invalidateHttpCache();
|
|
Minz_Session::_param('currentUser');
|
|
Minz_Session::_param('mail');
|
|
Minz_Session::_param('passwordHash');
|
|
}
|
|
|
|
private static function makeLongTermCookie($username, $passwordHash) {
|
|
do {
|
|
$token = sha1(Minz_Configuration::salt() . $username . uniqid(mt_rand(), true));
|
|
$tokenFile = DATA_PATH . '/tokens/' . $token . '.txt';
|
|
} while (file_exists($tokenFile));
|
|
if (@file_put_contents($tokenFile, $username . "\t" . $passwordHash) === false) {
|
|
return false;
|
|
}
|
|
$expire = time() + 2629744; //1 month //TODO: Use a configuration instead
|
|
Minz_Session::setLongTermCookie('FreshRSS_login', $token, $expire);
|
|
Minz_Session::_param('token', $token);
|
|
return $token;
|
|
}
|
|
|
|
private static function deleteLongTermCookie() {
|
|
Minz_Session::deleteLongTermCookie('FreshRSS_login');
|
|
$token = Minz_Session::param('token', null);
|
|
if (ctype_alnum($token)) {
|
|
@unlink(DATA_PATH . '/tokens/' . $token . '.txt');
|
|
}
|
|
Minz_Session::_param('token');
|
|
if (rand(0, 10) === 1) {
|
|
self::purgeTokens();
|
|
}
|
|
}
|
|
|
|
private static function purgeTokens() {
|
|
$oldest = time() - 2629744; //1 month //TODO: Use a configuration instead
|
|
foreach (new DirectoryIterator(DATA_PATH . '/tokens/') as $fileInfo) {
|
|
if ($fileInfo->getExtension() === 'txt' && $fileInfo->getMTime() < $oldest) {
|
|
@unlink($fileInfo->getPathname());
|
|
}
|
|
}
|
|
}
|
|
|
|
public function formLoginAction () {
|
|
if ($this->view->loginOk) {
|
|
Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
|
|
}
|
|
|
|
if (Minz_Request::isPost()) {
|
|
$ok = false;
|
|
$nonce = Minz_Session::param('nonce');
|
|
$username = Minz_Request::param('username', '');
|
|
$c = Minz_Request::param('challenge', '');
|
|
if (ctype_alnum($username) && ctype_graph($c) && ctype_alnum($nonce)) {
|
|
if (!function_exists('password_verify')) {
|
|
include_once(LIB_PATH . '/password_compat.php');
|
|
}
|
|
try {
|
|
$conf = new FreshRSS_Configuration($username);
|
|
$s = $conf->passwordHash;
|
|
$ok = password_verify($nonce . $s, $c);
|
|
if ($ok) {
|
|
Minz_Session::_param('currentUser', $username);
|
|
Minz_Session::_param('passwordHash', $s);
|
|
if (Minz_Request::param('keep_logged_in', false)) {
|
|
self::makeLongTermCookie($username, $s);
|
|
} else {
|
|
self::deleteLongTermCookie();
|
|
}
|
|
} else {
|
|
Minz_Log::record('Password mismatch for user ' . $username . ', nonce=' . $nonce . ', c=' . $c, Minz_Log::WARNING);
|
|
}
|
|
} catch (Minz_Exception $me) {
|
|
Minz_Log::record('Login failure: ' . $me->getMessage(), Minz_Log::WARNING);
|
|
}
|
|
} else {
|
|
Minz_Log::record('Invalid credential parameters: user=' . $username . ' challenge=' . $c . ' nonce=' . $nonce, Minz_Log::DEBUG);
|
|
}
|
|
if (!$ok) {
|
|
$notif = array(
|
|
'type' => 'bad',
|
|
'content' => Minz_Translate::t('invalid_login')
|
|
);
|
|
Minz_Session::_param('notification', $notif);
|
|
}
|
|
$this->view->_useLayout(false);
|
|
Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
|
|
} elseif (Minz_Configuration::unsafeAutologinEnabled() && isset($_GET['u']) && isset($_GET['p'])) {
|
|
Minz_Session::_param('currentUser');
|
|
Minz_Session::_param('mail');
|
|
Minz_Session::_param('passwordHash');
|
|
$username = ctype_alnum($_GET['u']) ? $_GET['u'] : '';
|
|
$passwordPlain = $_GET['p'];
|
|
Minz_Request::_param('p'); //Discard plain-text password ASAP
|
|
$_GET['p'] = '';
|
|
if (!function_exists('password_verify')) {
|
|
include_once(LIB_PATH . '/password_compat.php');
|
|
}
|
|
try {
|
|
$conf = new FreshRSS_Configuration($username);
|
|
$s = $conf->passwordHash;
|
|
$ok = password_verify($passwordPlain, $s);
|
|
unset($passwordPlain);
|
|
if ($ok) {
|
|
Minz_Session::_param('currentUser', $username);
|
|
Minz_Session::_param('passwordHash', $s);
|
|
} else {
|
|
Minz_Log::record('Unsafe password mismatch for user ' . $username, Minz_Log::WARNING);
|
|
}
|
|
} catch (Minz_Exception $me) {
|
|
Minz_Log::record('Unsafe login failure: ' . $me->getMessage(), Minz_Log::WARNING);
|
|
}
|
|
Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
|
|
} elseif (!Minz_Configuration::canLogIn()) {
|
|
Minz_Error::error (
|
|
403,
|
|
array ('error' => array (Minz_Translate::t ('access_denied')))
|
|
);
|
|
}
|
|
invalidateHttpCache();
|
|
}
|
|
|
|
public function formLogoutAction () {
|
|
$this->view->_useLayout(false);
|
|
invalidateHttpCache();
|
|
Minz_Session::_param('currentUser');
|
|
Minz_Session::_param('mail');
|
|
Minz_Session::_param('passwordHash');
|
|
self::deleteLongTermCookie();
|
|
Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
|
|
}
|
|
|
|
public function resetAuthAction() {
|
|
Minz_View::prependTitle(_t('auth_reset') . ' · ');
|
|
Minz_View::appendScript(Minz_Url::display(
|
|
'/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js')
|
|
));
|
|
|
|
$this->view->no_form = false;
|
|
// Enable changement of auth only if Persona!
|
|
if (Minz_Configuration::authType() != 'persona') {
|
|
$this->view->message = array(
|
|
'status' => 'bad',
|
|
'title' => _t('damn'),
|
|
'body' => _t('auth_not_persona')
|
|
);
|
|
$this->view->no_form = true;
|
|
return;
|
|
}
|
|
|
|
$conf = new FreshRSS_Configuration(Minz_Configuration::defaultUser());
|
|
// Admin user must have set its master password.
|
|
if (!$conf->passwordHash) {
|
|
$this->view->message = array(
|
|
'status' => 'bad',
|
|
'title' => _t('damn'),
|
|
'body' => _t('auth_no_password_set')
|
|
);
|
|
$this->view->no_form = true;
|
|
return;
|
|
}
|
|
|
|
invalidateHttpCache();
|
|
|
|
if (Minz_Request::isPost()) {
|
|
$nonce = Minz_Session::param('nonce');
|
|
$username = Minz_Request::param('username', '');
|
|
$c = Minz_Request::param('challenge', '');
|
|
if (!(ctype_alnum($username) && ctype_graph($c) && ctype_alnum($nonce))) {
|
|
Minz_Log::debug('Invalid credential parameters:' .
|
|
' user=' . $username .
|
|
' challenge=' . $c .
|
|
' nonce=' . $nonce);
|
|
Minz_Request::bad(_t('invalid_login'),
|
|
array('c' => 'index', 'a' => 'resetAuth'));
|
|
}
|
|
|
|
if (!function_exists('password_verify')) {
|
|
include_once(LIB_PATH . '/password_compat.php');
|
|
}
|
|
|
|
$s = $conf->passwordHash;
|
|
$ok = password_verify($nonce . $s, $c);
|
|
if ($ok) {
|
|
Minz_Configuration::_authType('form');
|
|
$ok = Minz_Configuration::writeFile();
|
|
|
|
if ($ok) {
|
|
Minz_Request::good(_t('auth_form_set'));
|
|
} else {
|
|
Minz_Request::bad(_t('auth_form_not_set'),
|
|
array('c' => 'index', 'a' => 'resetAuth'));
|
|
}
|
|
} else {
|
|
Minz_Log::debug('Password mismatch for user ' . $username .
|
|
', nonce=' . $nonce . ', c=' . $c);
|
|
|
|
Minz_Request::bad(_t('invalid_login'),
|
|
array('c' => 'index', 'a' => 'resetAuth'));
|
|
}
|
|
}
|
|
}
|
|
}
|