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; } } $today = @strtotime('today'); $this->view->today = $today; // on calcule la date des articles les plus anciens qu'on affiche $nb_month_old = $this->view->conf->old_entries; $date_min = $today - (3600 * 24 * 30 * $nb_month_old); //Do not use a fast changing value such as time() to allow SQL caching $keepHistoryDefault = $this->view->conf->keep_history_default; try { $entries = $entryDAO->listWhere($getType, $getId, $this->view->state, $order, $nb + 1, $first, $filter, $date_min, true, $keepHistoryDefault); // 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, $date_min, true, $keepHistoryDefault); } 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) { $extension = pathinfo($fileInfo->getFilename(), PATHINFO_EXTENSION); if ($extension === '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')); } } } }