diff options
Diffstat (limited to 'app/Controllers')
| -rw-r--r-- | app/Controllers/authController.php | 349 | ||||
| -rw-r--r-- | app/Controllers/categoryController.php | 194 | ||||
| -rwxr-xr-x | app/Controllers/configureController.php | 322 | ||||
| -rwxr-xr-x | app/Controllers/entryController.php | 240 | ||||
| -rw-r--r-- | app/Controllers/errorController.php | 69 | ||||
| -rw-r--r-- | app/Controllers/extensionController.php | 215 | ||||
| -rwxr-xr-x | app/Controllers/feedController.php | 704 | ||||
| -rw-r--r-- | app/Controllers/importExportController.php | 291 | ||||
| -rwxr-xr-x | app/Controllers/indexController.php | 619 | ||||
| -rwxr-xr-x | app/Controllers/javascriptController.php | 17 | ||||
| -rw-r--r-- | app/Controllers/statsController.php | 32 | ||||
| -rw-r--r-- | app/Controllers/subscriptionController.php | 116 | ||||
| -rw-r--r-- | app/Controllers/updateController.php | 91 | ||||
| -rw-r--r-- | app/Controllers/userController.php (renamed from app/Controllers/usersController.php) | 149 |
14 files changed, 2080 insertions, 1328 deletions
diff --git a/app/Controllers/authController.php b/app/Controllers/authController.php new file mode 100644 index 000000000..937c0759d --- /dev/null +++ b/app/Controllers/authController.php @@ -0,0 +1,349 @@ +<?php + +/** + * This controller handles action about authentication. + */ +class FreshRSS_auth_Controller extends Minz_ActionController { + /** + * This action handles authentication management page. + * + * Parameters are: + * - token (default: current token) + * - anon_access (default: false) + * - anon_refresh (default: false) + * - auth_type (default: none) + * - unsafe_autologin (default: false) + * - api_enabled (default: false) + * + * @todo move unsafe_autologin in an extension. + */ + public function indexAction() { + if (!FreshRSS_Auth::hasAccess('admin')) { + Minz_Error::error(403); + } + + Minz_View::prependTitle(_t('admin.auth.title') . ' · '); + + if (Minz_Request::isPost()) { + $ok = true; + + $current_token = FreshRSS_Context::$user_conf->token; + $token = Minz_Request::param('token', $current_token); + FreshRSS_Context::$user_conf->token = $token; + $ok &= FreshRSS_Context::$user_conf->save(); + + $anon = Minz_Request::param('anon_access', false); + $anon = ((bool)$anon) && ($anon !== 'no'); + $anon_refresh = Minz_Request::param('anon_refresh', false); + $anon_refresh = ((bool)$anon_refresh) && ($anon_refresh !== 'no'); + $auth_type = Minz_Request::param('auth_type', 'none'); + $unsafe_autologin = Minz_Request::param('unsafe_autologin', false); + $api_enabled = Minz_Request::param('api_enabled', false); + if ($anon != FreshRSS_Context::$system_conf->allow_anonymous || + $auth_type != FreshRSS_Context::$system_conf->auth_type || + $anon_refresh != FreshRSS_Context::$system_conf->allow_anonymous_refresh || + $unsafe_autologin != FreshRSS_Context::$system_conf->unsafe_autologin_enabled || + $api_enabled != FreshRSS_Context::$system_conf->api_enabled) { + + // TODO: test values from form + FreshRSS_Context::$system_conf->auth_type = $auth_type; + FreshRSS_Context::$system_conf->allow_anonymous = $anon; + FreshRSS_Context::$system_conf->allow_anonymous_refresh = $anon_refresh; + FreshRSS_Context::$system_conf->unsafe_autologin_enabled = $unsafe_autologin; + FreshRSS_Context::$system_conf->api_enabled = $api_enabled; + + $ok &= FreshRSS_Context::$system_conf->save(); + } + + invalidateHttpCache(); + + if ($ok) { + Minz_Request::good(_t('feedback.conf.updated'), + array('c' => 'auth', 'a' => 'index')); + } else { + Minz_Request::bad(_t('feedback.conf.error'), + array('c' => 'auth', 'a' => 'index')); + } + } + } + + /** + * This action handles the login page. + * + * It forwards to the correct login page (form or Persona) or main page if + * the user is already connected. + */ + public function loginAction() { + if (FreshRSS_Auth::hasAccess()) { + Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true); + } + + $auth_type = FreshRSS_Context::$system_conf->auth_type; + switch ($auth_type) { + case 'form': + Minz_Request::forward(array('c' => 'auth', 'a' => 'formLogin')); + break; + case 'persona': + Minz_Request::forward(array('c' => 'auth', 'a' => 'personaLogin')); + break; + case 'http_auth': + case 'none': + // It should not happened! + Minz_Error::error(404); + default: + // TODO load plugin instead + Minz_Error::error(404); + } + } + + /** + * This action handles form login page. + * + * If this action is reached through a POST request, username and password + * are compared to login the current user. + * + * Parameters are: + * - nonce (default: false) + * - username (default: '') + * - challenge (default: '') + * - keep_logged_in (default: false) + * + * @todo move unsafe autologin in an extension. + */ + public function formLoginAction() { + invalidateHttpCache(); + + $file_mtime = @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js'); + Minz_View::appendScript(Minz_Url::display('/scripts/bcrypt.min.js?' . $file_mtime)); + + if (Minz_Request::isPost()) { + $nonce = Minz_Session::param('nonce'); + $username = Minz_Request::param('username', ''); + $challenge = Minz_Request::param('challenge', ''); + + $conf = get_user_configuration($username); + if (is_null($conf)) { + Minz_Request::bad(_t('feedback.auth.login.invalid'), + array('c' => 'auth', 'a' => 'login')); + } + + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $conf->passwordHash, $nonce, $challenge + ); + if ($ok) { + // Set session parameter to give access to the user. + Minz_Session::_param('currentUser', $username); + Minz_Session::_param('passwordHash', $conf->passwordHash); + FreshRSS_Auth::giveAccess(); + + // Set cookie parameter if nedded. + if (Minz_Request::param('keep_logged_in')) { + FreshRSS_FormAuth::makeCookie($username, $conf->passwordHash); + } else { + FreshRSS_FormAuth::deleteCookie(); + } + + // All is good, go back to the index. + Minz_Request::good(_t('feedback.auth.login.success'), + array('c' => 'index', 'a' => 'index')); + } else { + Minz_Log::warning('Password mismatch for' . + ' user=' . $username . + ', nonce=' . $nonce . + ', c=' . $challenge); + Minz_Request::bad(_t('feedback.auth.login.invalid'), + array('c' => 'auth', 'a' => 'login')); + } + } elseif (FreshRSS_Context::$system_conf->unsafe_autologin_enabled) { + $username = Minz_Request::param('u', ''); + $password = Minz_Request::param('p', ''); + Minz_Request::_param('p'); + + if (!$username) { + return; + } + + $conf = get_user_configuration($username); + if (is_null($conf)) { + return; + } + + if (!function_exists('password_verify')) { + include_once(LIB_PATH . '/password_compat.php'); + } + + $s = $conf->passwordHash; + $ok = password_verify($password, $s); + unset($password); + if ($ok) { + Minz_Session::_param('currentUser', $username); + Minz_Session::_param('passwordHash', $s); + FreshRSS_Auth::giveAccess(); + + Minz_Request::good(_t('feedback.auth.login.success'), + array('c' => 'index', 'a' => 'index')); + } else { + Minz_Log::warning('Unsafe password mismatch for user ' . $username); + Minz_Request::bad(_t('feedback.auth.login.invalid'), + array('c' => 'auth', 'a' => 'login')); + } + } + } + + /** + * This action handles Persona login page. + * + * If this action is reached through a POST request, assertion from Persona + * is verificated and user connected if all is ok. + * + * Parameter is: + * - assertion (default: false) + * + * @todo: Persona system should be moved to a plugin + */ + public function personaLoginAction() { + $this->view->res = false; + + if (Minz_Request::isPost()) { + $this->view->_useLayout(false); + + $assert = Minz_Request::param('assertion'); + $url = 'https://verifier.login.persona.org/verify'; + $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); + + $login_ok = false; + $reason = ''; + if ($res['status'] === 'okay') { + $email = filter_var($res['email'], FILTER_VALIDATE_EMAIL); + if ($email != '') { + $persona_file = DATA_PATH . '/persona/' . $email . '.txt'; + if (($current_user = @file_get_contents($persona_file)) !== false) { + $current_user = trim($current_user); + $conf = get_user_configuration($current_user); + if (!is_null($conf)) { + $login_ok = strcasecmp($email, $conf->mail_login) === 0; + } else { + $reason = 'Invalid configuration for user ' . + '[' . $current_user . ']'; + } + } + } else { + $reason = 'Invalid email format [' . $res['email'] . ']'; + } + } else { + $reason = $res['reason']; + } + + if ($login_ok) { + Minz_Session::_param('currentUser', $current_user); + Minz_Session::_param('mail', $email); + FreshRSS_Auth::giveAccess(); + invalidateHttpCache(); + } else { + Minz_Log::error($reason); + + $res = array(); + $res['status'] = 'failure'; + $res['reason'] = _t('feedback.auth.login.invalid'); + } + + header('Content-Type: application/json; charset=UTF-8'); + $this->view->res = $res; + } + } + + /** + * This action removes all accesses of the current user. + */ + public function logoutAction() { + invalidateHttpCache(); + FreshRSS_Auth::removeAccess(); + Minz_Request::good(_t('feedback.auth.logout.success'), + array('c' => 'index', 'a' => 'index')); + } + + /** + * This action resets the authentication system. + * + * After reseting, form auth is set by default. + */ + public function resetAction() { + Minz_View::prependTitle(_t('admin.auth.title_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 (FreshRSS_Context::$system_conf->auth_type != 'persona') { + $this->view->message = array( + 'status' => 'bad', + 'title' => _t('gen.short.damn'), + 'body' => _t('feedback.auth.not_persona') + ); + $this->view->no_form = true; + return; + } + + $conf = get_user_configuration(FreshRSS_Context::$system_conf->default_user); + if (is_null($conf)) { + return; + } + + // Admin user must have set its master password. + if (!$conf->passwordHash) { + $this->view->message = array( + 'status' => 'bad', + 'title' => _t('gen.short.damn'), + 'body' => _t('feedback.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', ''); + $challenge = Minz_Request::param('challenge', ''); + + $ok = FreshRSS_FormAuth::checkCredentials( + $username, $conf->passwordHash, $nonce, $challenge + ); + + if ($ok) { + FreshRSS_Context::$system_conf->auth_type = 'form'; + $ok = FreshRSS_Context::$system_conf->save(); + + if ($ok) { + Minz_Request::good(_t('feedback.auth.form.set')); + } else { + Minz_Request::bad(_t('feedback.auth.form.not_set'), + array('c' => 'auth', 'a' => 'reset')); + } + } else { + Minz_Log::warning('Password mismatch for' . + ' user=' . $username . + ', nonce=' . $nonce . + ', c=' . $challenge); + Minz_Request::bad(_t('feedback.auth.login.invalid'), + array('c' => 'auth', 'a' => 'reset')); + } + } + } +} diff --git a/app/Controllers/categoryController.php b/app/Controllers/categoryController.php new file mode 100644 index 000000000..e65c146de --- /dev/null +++ b/app/Controllers/categoryController.php @@ -0,0 +1,194 @@ +<?php + +/** + * Controller to handle actions relative to categories. + * User needs to be connected. + */ +class FreshRSS_category_Controller extends Minz_ActionController { + /** + * This action is called before every other action in that class. It is + * the common boiler plate for every action. It is triggered by the + * underlying framework. + * + */ + public function firstAction() { + if (!FreshRSS_Auth::hasAccess()) { + Minz_Error::error(403); + } + + $catDAO = new FreshRSS_CategoryDAO(); + $catDAO->checkDefault(); + } + + /** + * This action creates a new category. + * + * Request parameter is: + * - new-category + */ + public function createAction() { + $catDAO = new FreshRSS_CategoryDAO(); + $url_redirect = array('c' => 'subscription', 'a' => 'index'); + + $limits = FreshRSS_Context::$system_conf->limits; + $this->view->categories = $catDAO->listCategories(false); + + if (count($this->view->categories) >= $limits['max_categories']) { + Minz_Request::bad(_t('feedback.sub.category.over_max', $limits['max_categories']), + $url_redirect); + } + + if (Minz_Request::isPost()) { + invalidateHttpCache(); + + $cat_name = Minz_Request::param('new-category'); + if (!$cat_name) { + Minz_Request::bad(_t('feedback.sub.category.no_name'), $url_redirect); + } + + $cat = new FreshRSS_Category($cat_name); + + if ($catDAO->searchByName($cat->name()) != null) { + Minz_Request::bad(_t('feedback.sub.category.name_exists'), $url_redirect); + } + + $values = array( + 'id' => $cat->id(), + 'name' => $cat->name(), + ); + + if ($catDAO->addCategory($values)) { + Minz_Request::good(_t('feedback.sub.category.created', $cat->name()), $url_redirect); + } else { + Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect); + } + } + + Minz_Request::forward($url_redirect, true); + } + + /** + * This action updates the given category. + * + * Request parameters are: + * - id + * - name + */ + public function updateAction() { + $catDAO = new FreshRSS_CategoryDAO(); + $url_redirect = array('c' => 'subscription', 'a' => 'index'); + + if (Minz_Request::isPost()) { + invalidateHttpCache(); + + $id = Minz_Request::param('id'); + $name = Minz_Request::param('name', ''); + if (strlen($name) <= 0) { + Minz_Request::bad(_t('feedback.sub.category.no_name'), $url_redirect); + } + + if ($catDAO->searchById($id) == null) { + Minz_Request::bad(_t('feedback.sub.category.not_exist'), $url_redirect); + } + + $cat = new FreshRSS_Category($name); + $values = array( + 'name' => $cat->name(), + ); + + if ($catDAO->updateCategory($id, $values)) { + Minz_Request::good(_t('feedback.sub.category.updated'), $url_redirect); + } else { + Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect); + } + } + + Minz_Request::forward($url_redirect, true); + } + + /** + * This action deletes a category. + * Feeds in the given category are moved in the default category. + * Related user queries are deleted too. + * + * Request parameter is: + * - id (of a category) + */ + public function deleteAction() { + $feedDAO = FreshRSS_Factory::createFeedDao(); + $catDAO = new FreshRSS_CategoryDAO(); + $default_category = $catDAO->getDefault(); + $url_redirect = array('c' => 'subscription', 'a' => 'index'); + + if (Minz_Request::isPost()) { + invalidateHttpCache(); + + $id = Minz_Request::param('id'); + if (!$id) { + Minz_Request::bad(_t('feedback.sub.category.no_id'), $url_redirect); + } + + if ($id === $default_category->id()) { + Minz_Request::bad(_t('feedback.sub.category.not_delete_default'), $url_redirect); + } + + if ($feedDAO->changeCategory($id, $default_category->id()) === false) { + Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect); + } + + if ($catDAO->deleteCategory($id) === false) { + Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect); + } + + // Remove related queries. + FreshRSS_Context::$user_conf->queries = remove_query_by_get( + 'c_' . $id, FreshRSS_Context::$user_conf->queries); + FreshRSS_Context::$user_conf->save(); + + Minz_Request::good(_t('feedback.sub.category.deleted'), $url_redirect); + } + + Minz_Request::forward($url_redirect, true); + } + + /** + * This action deletes all the feeds relative to a given category. + * Feed-related queries are deleted. + * + * Request parameter is: + * - id (of a category) + */ + public function emptyAction() { + $feedDAO = FreshRSS_Factory::createFeedDao(); + $url_redirect = array('c' => 'subscription', 'a' => 'index'); + + if (Minz_Request::isPost()) { + invalidateHttpCache(); + + $id = Minz_Request::param('id'); + if (!$id) { + Minz_Request::bad(_t('feedback.sub.category.no_id'), $url_redirect); + } + + // List feeds to remove then related user queries. + $feeds = $feedDAO->listByCategory($id); + + if ($feedDAO->deleteFeedByCategory($id)) { + // TODO: Delete old favicons + + // Remove related queries + foreach ($feeds as $feed) { + FreshRSS_Context::$user_conf->queries = remove_query_by_get( + 'f_' . $feed->id(), FreshRSS_Context::$user_conf->queries); + } + FreshRSS_Context::$user_conf->save(); + + Minz_Request::good(_t('feedback.sub.category.emptied'), $url_redirect); + } else { + Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect); + } + } + + Minz_Request::forward($url_redirect, true); + } +} diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php index 231865bd7..38ccd2b2d 100755 --- a/app/Controllers/configureController.php +++ b/app/Controllers/configureController.php @@ -8,175 +8,10 @@ class FreshRSS_configure_Controller extends Minz_ActionController { * This action is called before every other action in that class. It is * the common boiler plate for every action. It is triggered by the * underlying framework. - * - * @todo see if the category default configuration is needed here or if - * we can move it to the categorize action */ public function firstAction() { - if (!$this->view->loginOk) { - Minz_Error::error( - 403, - array('error' => array(_t('access_denied'))) - ); - } - - $catDAO = new FreshRSS_CategoryDAO(); - $catDAO->checkDefault(); - } - - /** - * This action handles the category configuration page - * - * It displays the category configuration page. - * If this action is reached through a POST request, it loops through - * every category to check for modification then add a new category if - * needed then sends a notification to the user. - * If a category name is emptied, the category is deleted and all - * related feeds are moved to the default category. Related user queries - * are deleted too. - * If a category name is changed, it is updated. - */ - public function categorizeAction() { - $feedDAO = FreshRSS_Factory::createFeedDao(); - $catDAO = new FreshRSS_CategoryDAO(); - $defaultCategory = $catDAO->getDefault(); - $defaultId = $defaultCategory->id(); - - if (Minz_Request::isPost()) { - $cats = Minz_Request::param('categories', array()); - $ids = Minz_Request::param('ids', array()); - $newCat = trim(Minz_Request::param('new_category', '')); - - foreach ($cats as $key => $name) { - if (strlen($name) > 0) { - $cat = new FreshRSS_Category($name); - $values = array( - 'name' => $cat->name(), - ); - $catDAO->updateCategory($ids[$key], $values); - } elseif ($ids[$key] != $defaultId) { - $feedDAO->changeCategory($ids[$key], $defaultId); - $catDAO->deleteCategory($ids[$key]); - - // Remove related queries. - $this->view->conf->remove_query_by_get('c_' . $ids[$key]); - $this->view->conf->save(); - } - } - - if ($newCat != '') { - $cat = new FreshRSS_Category($newCat); - $values = array( - 'id' => $cat->id(), - 'name' => $cat->name(), - ); - - if ($catDAO->searchByName($newCat) == null) { - $catDAO->addCategory($values); - } - } - invalidateHttpCache(); - - Minz_Request::good(_t('categories_updated'), - array('c' => 'configure', 'a' => 'categorize')); - } - - $this->view->categories = $catDAO->listCategories(false); - $this->view->defaultCategory = $catDAO->getDefault(); - $this->view->feeds = $feedDAO->listFeeds(); - - Minz_View::prependTitle(_t('categories_management') . ' · '); - } - - /** - * This action handles the feed configuration page. - * - * It displays the feed configuration page. - * If this action is reached through a POST request, it stores all new - * configuraiton values then sends a notification to the user. - * - * The options available on the page are: - * - name - * - description - * - website URL - * - feed URL - * - category id (default: default category id) - * - CSS path to article on website - * - display in main stream (default: 0) - * - HTTP authentication - * - number of article to retain (default: -2) - * - refresh frequency (default: -2) - * Default values are empty strings unless specified. - */ - public function feedAction() { - $catDAO = new FreshRSS_CategoryDAO(); - $this->view->categories = $catDAO->listCategories(false); - - $feedDAO = FreshRSS_Factory::createFeedDao(); - $this->view->feeds = $feedDAO->listFeeds(); - - $id = Minz_Request::param('id'); - if ($id == false && !empty($this->view->feeds)) { - $id = current($this->view->feeds)->id(); - } - - $this->view->flux = false; - if ($id != false) { - $this->view->flux = $this->view->feeds[$id]; - - if (!$this->view->flux) { - Minz_Error::error( - 404, - array('error' => array(_t('page_not_found'))) - ); - } else { - if (Minz_Request::isPost() && $this->view->flux) { - $user = Minz_Request::param('http_user', ''); - $pass = Minz_Request::param('http_pass', ''); - - $httpAuth = ''; - if ($user != '' || $pass != '') { - $httpAuth = $user . ':' . $pass; - } - - $cat = intval(Minz_Request::param('category', 0)); - - $values = array( - 'name' => Minz_Request::param('name', ''), - 'description' => sanitizeHTML(Minz_Request::param('description', '', true)), - 'website' => Minz_Request::param('website', ''), - 'url' => Minz_Request::param('url', ''), - 'category' => $cat, - 'pathEntries' => Minz_Request::param('path_entries', ''), - 'priority' => intval(Minz_Request::param('priority', 0)), - 'httpAuth' => $httpAuth, - 'keep_history' => intval(Minz_Request::param('keep_history', -2)), - 'ttl' => intval(Minz_Request::param('ttl', -2)), - ); - - if ($feedDAO->updateFeed($id, $values)) { - $this->view->flux->_category($cat); - $this->view->flux->faviconPrepare(); - $notif = array( - 'type' => 'good', - 'content' => _t('feed_updated') - ); - } else { - $notif = array( - 'type' => 'bad', - 'content' => _t('error_occurred_update') - ); - } - invalidateHttpCache(); - - Minz_Session::_param('notification', $notif); - Minz_Request::forward(array('c' => 'configure', 'a' => 'feed', 'params' => array('id' => $id)), true); - } - - Minz_View::prependTitle(_t('rss_feed_management') . ' — ' . $this->view->flux->name() . ' · '); - } - } else { - Minz_View::prependTitle(_t('rss_feed_management') . ' · '); + if (!FreshRSS_Auth::hasAccess()) { + Minz_Error::error(403); } } @@ -206,33 +41,33 @@ class FreshRSS_configure_Controller extends Minz_ActionController { */ public function displayAction() { if (Minz_Request::isPost()) { - $this->view->conf->_language(Minz_Request::param('language', 'en')); - $this->view->conf->_theme(Minz_Request::param('theme', FreshRSS_Themes::$defaultTheme)); - $this->view->conf->_content_width(Minz_Request::param('content_width', 'thin')); - $this->view->conf->_topline_read(Minz_Request::param('topline_read', false)); - $this->view->conf->_topline_favorite(Minz_Request::param('topline_favorite', false)); - $this->view->conf->_topline_date(Minz_Request::param('topline_date', false)); - $this->view->conf->_topline_link(Minz_Request::param('topline_link', false)); - $this->view->conf->_bottomline_read(Minz_Request::param('bottomline_read', false)); - $this->view->conf->_bottomline_favorite(Minz_Request::param('bottomline_favorite', false)); - $this->view->conf->_bottomline_sharing(Minz_Request::param('bottomline_sharing', false)); - $this->view->conf->_bottomline_tags(Minz_Request::param('bottomline_tags', false)); - $this->view->conf->_bottomline_date(Minz_Request::param('bottomline_date', false)); - $this->view->conf->_bottomline_link(Minz_Request::param('bottomline_link', false)); - $this->view->conf->_html5_notif_timeout(Minz_Request::param('html5_notif_timeout', 0)); - $this->view->conf->save(); - - Minz_Session::_param('language', $this->view->conf->language); - Minz_Translate::reset(); + FreshRSS_Context::$user_conf->language = Minz_Request::param('language', 'en'); + FreshRSS_Context::$user_conf->theme = Minz_Request::param('theme', FreshRSS_Themes::$defaultTheme); + FreshRSS_Context::$user_conf->content_width = Minz_Request::param('content_width', 'thin'); + FreshRSS_Context::$user_conf->topline_read = Minz_Request::param('topline_read', false); + FreshRSS_Context::$user_conf->topline_favorite = Minz_Request::param('topline_favorite', false); + FreshRSS_Context::$user_conf->topline_date = Minz_Request::param('topline_date', false); + FreshRSS_Context::$user_conf->topline_link = Minz_Request::param('topline_link', false); + FreshRSS_Context::$user_conf->bottomline_read = Minz_Request::param('bottomline_read', false); + FreshRSS_Context::$user_conf->bottomline_favorite = Minz_Request::param('bottomline_favorite', false); + FreshRSS_Context::$user_conf->bottomline_sharing = Minz_Request::param('bottomline_sharing', false); + FreshRSS_Context::$user_conf->bottomline_tags = Minz_Request::param('bottomline_tags', false); + FreshRSS_Context::$user_conf->bottomline_date = Minz_Request::param('bottomline_date', false); + FreshRSS_Context::$user_conf->bottomline_link = Minz_Request::param('bottomline_link', false); + FreshRSS_Context::$user_conf->html5_notif_timeout = Minz_Request::param('html5_notif_timeout', 0); + FreshRSS_Context::$user_conf->save(); + + Minz_Session::_param('language', FreshRSS_Context::$user_conf->language); + Minz_Translate::reset(FreshRSS_Context::$user_conf->language); invalidateHttpCache(); - Minz_Request::good(_t('configuration_updated'), + Minz_Request::good(_t('feedback.conf.updated'), array('c' => 'configure', 'a' => 'display')); } $this->view->themes = FreshRSS_Themes::get(); - Minz_View::prependTitle(_t('display_configuration') . ' · '); + Minz_View::prependTitle(_t('conf.display.title') . ' · '); } /** @@ -254,6 +89,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController { * - image lazy loading * - stick open articles to the top * - display a confirmation when reading all articles + * - auto remove article after reading * - article order (default: DESC) * - mark articles as read when: * - displayed @@ -264,35 +100,33 @@ class FreshRSS_configure_Controller extends Minz_ActionController { */ public function readingAction() { if (Minz_Request::isPost()) { - $this->view->conf->_posts_per_page(Minz_Request::param('posts_per_page', 10)); - $this->view->conf->_view_mode(Minz_Request::param('view_mode', 'normal')); - $this->view->conf->_default_view((int)Minz_Request::param('default_view', FreshRSS_Entry::STATE_ALL)); - $this->view->conf->_auto_load_more(Minz_Request::param('auto_load_more', false)); - $this->view->conf->_display_posts(Minz_Request::param('display_posts', false)); - $this->view->conf->_display_categories(Minz_Request::param('display_categories', false)); - $this->view->conf->_hide_read_feeds(Minz_Request::param('hide_read_feeds', false)); - $this->view->conf->_onread_jump_next(Minz_Request::param('onread_jump_next', false)); - $this->view->conf->_lazyload(Minz_Request::param('lazyload', false)); - $this->view->conf->_sticky_post(Minz_Request::param('sticky_post', false)); - $this->view->conf->_reading_confirm(Minz_Request::param('reading_confirm', false)); - $this->view->conf->_sort_order(Minz_Request::param('sort_order', 'DESC')); - $this->view->conf->_mark_when(array( + FreshRSS_Context::$user_conf->posts_per_page = Minz_Request::param('posts_per_page', 10); + FreshRSS_Context::$user_conf->view_mode = Minz_Request::param('view_mode', 'normal'); + FreshRSS_Context::$user_conf->default_view = Minz_Request::param('default_view', 'adaptive'); + FreshRSS_Context::$user_conf->auto_load_more = Minz_Request::param('auto_load_more', false); + FreshRSS_Context::$user_conf->display_posts = Minz_Request::param('display_posts', false); + FreshRSS_Context::$user_conf->display_categories = Minz_Request::param('display_categories', false); + FreshRSS_Context::$user_conf->hide_read_feeds = Minz_Request::param('hide_read_feeds', false); + FreshRSS_Context::$user_conf->onread_jump_next = Minz_Request::param('onread_jump_next', false); + FreshRSS_Context::$user_conf->lazyload = Minz_Request::param('lazyload', false); + FreshRSS_Context::$user_conf->sticky_post = Minz_Request::param('sticky_post', false); + FreshRSS_Context::$user_conf->reading_confirm = Minz_Request::param('reading_confirm', false); + FreshRSS_Context::$user_conf->auto_remove_article = Minz_Request::param('auto_remove_article', false); + FreshRSS_Context::$user_conf->sort_order = Minz_Request::param('sort_order', 'DESC'); + FreshRSS_Context::$user_conf->mark_when = array( 'article' => Minz_Request::param('mark_open_article', false), 'site' => Minz_Request::param('mark_open_site', false), 'scroll' => Minz_Request::param('mark_scroll', false), 'reception' => Minz_Request::param('mark_upon_reception', false), - )); - $this->view->conf->save(); - - Minz_Session::_param('language', $this->view->conf->language); - Minz_Translate::reset(); + ); + FreshRSS_Context::$user_conf->save(); invalidateHttpCache(); - Minz_Request::good(_t('configuration_updated'), + Minz_Request::good(_t('feedback.conf.updated'), array('c' => 'configure', 'a' => 'reading')); } - Minz_View::prependTitle(_t('reading_configuration') . ' · '); + Minz_View::prependTitle(_t('conf.reading.title') . ' · '); } /** @@ -305,15 +139,15 @@ class FreshRSS_configure_Controller extends Minz_ActionController { public function sharingAction() { if (Minz_Request::isPost()) { $params = Minz_Request::params(); - $this->view->conf->_sharing($params['share']); - $this->view->conf->save(); + FreshRSS_Context::$user_conf->sharing = $params['share']; + FreshRSS_Context::$user_conf->save(); invalidateHttpCache(); - Minz_Request::good(_t('configuration_updated'), + Minz_Request::good(_t('feedback.conf.updated'), array('c' => 'configure', 'a' => 'sharing')); } - Minz_View::prependTitle(_t('sharing') . ' · '); + Minz_View::prependTitle(_t('conf.sharing.title') . ' · '); } /** @@ -330,11 +164,11 @@ class FreshRSS_configure_Controller extends Minz_ActionController { */ public function shortcutAction() { $list_keys = array('a', 'b', 'backspace', 'c', 'd', 'delete', 'down', 'e', 'end', 'enter', - 'escape', 'f', 'g', 'h', 'home', 'i', 'insert', 'j', 'k', 'l', 'left', - 'm', 'n', 'o', 'p', 'page_down', 'page_up', 'q', 'r', 'return', 'right', - 's', 'space', 't', 'tab', 'u', 'up', 'v', 'w', 'x', 'y', - 'z', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', - 'f10', 'f11', 'f12'); + 'escape', 'f', 'g', 'h', 'home', 'i', 'insert', 'j', 'k', 'l', 'left', + 'm', 'n', 'o', 'p', 'page_down', 'page_up', 'q', 'r', 'return', 'right', + 's', 'space', 't', 'tab', 'u', 'up', 'v', 'w', 'x', 'y', + 'z', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', + 'f10', 'f11', 'f12'); $this->view->list_keys = $list_keys; if (Minz_Request::isPost()) { @@ -347,24 +181,15 @@ class FreshRSS_configure_Controller extends Minz_ActionController { } } - $this->view->conf->_shortcuts($shortcuts_ok); - $this->view->conf->save(); + FreshRSS_Context::$user_conf->shortcuts = $shortcuts_ok; + FreshRSS_Context::$user_conf->save(); invalidateHttpCache(); - Minz_Request::good(_t('shortcuts_updated'), + Minz_Request::good(_t('feedback.conf.shortcuts_updated'), array('c' => 'configure', 'a' => 'shortcut')); } - Minz_View::prependTitle(_t('shortcuts') . ' · '); - } - - /** - * This action display the user configuration page - * - * @todo move that action in the user controller - */ - public function usersAction() { - Minz_View::prependTitle(_t('users') . ' · '); + Minz_View::prependTitle(_t('conf.shortcut.title') . ' · '); } /** @@ -384,23 +209,23 @@ class FreshRSS_configure_Controller extends Minz_ActionController { */ public function archivingAction() { if (Minz_Request::isPost()) { - $this->view->conf->_old_entries(Minz_Request::param('old_entries', 3)); - $this->view->conf->_keep_history_default(Minz_Request::param('keep_history_default', 0)); - $this->view->conf->_ttl_default(Minz_Request::param('ttl_default', -2)); - $this->view->conf->save(); + FreshRSS_Context::$user_conf->old_entries = Minz_Request::param('old_entries', 3); + FreshRSS_Context::$user_conf->keep_history_default = Minz_Request::param('keep_history_default', 0); + FreshRSS_Context::$user_conf->ttl_default = Minz_Request::param('ttl_default', -2); + FreshRSS_Context::$user_conf->save(); invalidateHttpCache(); - Minz_Request::good(_t('configuration_updated'), + Minz_Request::good(_t('feedback.conf.updated'), array('c' => 'configure', 'a' => 'archiving')); } - Minz_View::prependTitle(_t('archiving_configuration') . ' · '); + Minz_View::prependTitle(_t('conf.archiving.title') . ' · '); $entryDAO = FreshRSS_Factory::createEntryDao(); $this->view->nb_total = $entryDAO->count(); $this->view->size_user = $entryDAO->size(); - if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) { + if (FreshRSS_Auth::hasAccess('admin')) { $this->view->size_total = $entryDAO->size(true); } } @@ -421,19 +246,19 @@ class FreshRSS_configure_Controller extends Minz_ActionController { foreach ($queries as $key => $query) { if (!$query['name']) { - $query['name'] = _t('query_number', $key + 1); + $query['name'] = _t('conf.query.number', $key + 1); } } - $this->view->conf->_queries($queries); - $this->view->conf->save(); + FreshRSS_Context::$user_conf->queries = $queries; + FreshRSS_Context::$user_conf->save(); - Minz_Request::good(_t('configuration_updated'), + Minz_Request::good(_t('feedback.conf.updated'), array('c' => 'configure', 'a' => 'queries')); } else { $this->view->query_get = array(); $cat_dao = new FreshRSS_CategoryDAO(); $feed_dao = FreshRSS_Factory::createFeedDao(); - foreach ($this->view->conf->queries as $key => $query) { + foreach (FreshRSS_Context::$user_conf->queries as $key => $query) { if (!isset($query['get'])) { continue; } @@ -489,7 +314,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController { } } - Minz_View::prependTitle(_t('queries') . ' · '); + Minz_View::prependTitle(_t('conf.query.title') . ' · '); } /** @@ -501,22 +326,19 @@ class FreshRSS_configure_Controller extends Minz_ActionController { */ public function addQueryAction() { $whitelist = array('get', 'order', 'name', 'search', 'state'); - $queries = $this->view->conf->queries; + $queries = FreshRSS_Context::$user_conf->queries; $query = Minz_Request::params(); - $query['name'] = _t('query_number', count($queries) + 1); + $query['name'] = _t('conf.query.number', count($queries) + 1); foreach ($query as $key => $value) { if (!in_array($key, $whitelist)) { unset($query[$key]); } } - if (!empty($query['state']) && $query['state'] & FreshRSS_Entry::STATE_STRICT) { - $query['state'] -= FreshRSS_Entry::STATE_STRICT; - } $queries[] = $query; - $this->view->conf->_queries($queries); - $this->view->conf->save(); + FreshRSS_Context::$user_conf->queries = $queries; + FreshRSS_Context::$user_conf->save(); - Minz_Request::good(_t('query_created', $query['name']), + Minz_Request::good(_t('feedback.conf.query_created', $query['name']), array('c' => 'configure', 'a' => 'queries')); } } diff --git a/app/Controllers/entryController.php b/app/Controllers/entryController.php index ab66d9198..1d9989f40 100755 --- a/app/Controllers/entryController.php +++ b/app/Controllers/entryController.php @@ -1,150 +1,181 @@ <?php +/** + * Controller to handle every entry actions. + */ class FreshRSS_entry_Controller extends Minz_ActionController { - public function firstAction () { - if (!$this->view->loginOk) { - Minz_Error::error ( - 403, - array ('error' => array (Minz_Translate::t ('access_denied'))) - ); + /** + * This action is called before every other action in that class. It is + * the common boiler plate for every action. It is triggered by the + * underlying framework. + */ + public function firstAction() { + if (!FreshRSS_Auth::hasAccess()) { + Minz_Error::error(403); } - $this->params = array (); - $output = Minz_Request::param('output', ''); - if (($output != '') && ($this->view->conf->view_mode !== $output)) { - $this->params['output'] = $output; - } - - $this->redirect = false; - $ajax = Minz_Request::param ('ajax'); - if ($ajax) { - $this->view->_useLayout (false); - } - } - - public function lastAction () { - $ajax = Minz_Request::param ('ajax'); - if (!$ajax && $this->redirect) { - Minz_Request::forward (array ( - 'c' => 'index', - 'a' => 'index', - 'params' => $this->params - ), true); - } else { - Minz_Request::_param ('ajax'); + // If ajax request, we do not print layout + $this->ajax = Minz_Request::param('ajax'); + if ($this->ajax) { + $this->view->_useLayout(false); + Minz_Request::_param('ajax'); } } - public function readAction () { - $this->redirect = true; - - $id = Minz_Request::param ('id'); - $get = Minz_Request::param ('get'); - $nextGet = Minz_Request::param ('nextGet', $get); - $idMax = Minz_Request::param ('idMax', 0); + /** + * Mark one or several entries as read (or not!). + * + * If request concerns several entries, it MUST be a POST request. + * If request concerns several entries, only mark them as read is available. + * + * Parameters are: + * - id (default: false) + * - get (default: false) /(c_\d+|f_\d+|s|a)/ + * - nextGet (default: $get) + * - idMax (default: 0) + * - is_read (default: true) + */ + public function readAction() { + $id = Minz_Request::param('id'); + $get = Minz_Request::param('get'); + $next_get = Minz_Request::param('nextGet', $get); + $id_max = Minz_Request::param('idMax', 0); + $params = array(); $entryDAO = FreshRSS_Factory::createEntryDao(); - if ($id == false) { + if ($id === false) { + // id is false? It MUST be a POST request! if (!Minz_Request::isPost()) { return; } if (!$get) { - $entryDAO->markReadEntries ($idMax); + // No get? Mark all entries as read (from $id_max) + $entryDAO->markReadEntries($id_max); } else { - $typeGet = $get[0]; - $get = substr ($get, 2); - switch ($typeGet) { - case 'c': - $entryDAO->markReadCat ($get, $idMax); - break; - case 'f': - $entryDAO->markReadFeed ($get, $idMax); - break; - case 's': - $entryDAO->markReadEntries ($idMax, true); - break; - case 'a': - $entryDAO->markReadEntries ($idMax); - break; + $type_get = $get[0]; + $get = substr($get, 2); + switch($type_get) { + case 'c': + $entryDAO->markReadCat($get, $id_max); + break; + case 'f': + $entryDAO->markReadFeed($get, $id_max); + break; + case 's': + $entryDAO->markReadEntries($id_max, true); + break; + case 'a': + $entryDAO->markReadEntries($id_max); + break; } - if ($nextGet !== 'a') { - $this->params['get'] = $nextGet; + + if ($next_get !== 'a') { + // Redirect to the correct page (category, feed or starred) + // Not "a" because it is the default value if nothing is + // given. + $params['get'] = $next_get; } } - - $notif = array ( - 'type' => 'good', - 'content' => Minz_Translate::t ('feeds_marked_read') - ); - Minz_Session::_param ('notification', $notif); } else { - $is_read = (bool)(Minz_Request::param ('is_read', true)); - $entryDAO->markRead ($id, $is_read); + $is_read = (bool)(Minz_Request::param('is_read', true)); + $entryDAO->markRead($id, $is_read); } - } - public function bookmarkAction () { - $this->redirect = true; + if (!$this->ajax) { + Minz_Request::good(_t('feedback.sub.feed.marked_read'), array( + 'c' => 'index', + 'a' => 'index', + 'params' => $params, + ), true); + } + } - $id = Minz_Request::param ('id'); - if ($id) { + /** + * This action marks an entry as favourite (bookmark) or not. + * + * Parameter is: + * - id (default: false) + * - is_favorite (default: true) + * If id is false, nothing happened. + */ + public function bookmarkAction() { + $id = Minz_Request::param('id'); + $is_favourite = (bool)Minz_Request::param('is_favorite', true); + if ($id !== false) { $entryDAO = FreshRSS_Factory::createEntryDao(); - $entryDAO->markFavorite ($id, (bool)(Minz_Request::param ('is_favorite', true))); + $entryDAO->markFavorite($id, $is_favourite); + } + + if (!$this->ajax) { + Minz_Request::forward(array( + 'c' => 'index', + 'a' => 'index', + ), true); } } + /** + * This action optimizes database to reduce its size. + * + * This action shouldbe reached by a POST request. + * + * @todo move this action in configure controller. + * @todo call this action through web-cron when available + */ public function optimizeAction() { - if (Minz_Request::isPost()) { - @set_time_limit(300); + $url_redirect = array( + 'c' => 'configure', + 'a' => 'archiving', + ); - // La table des entrées a tendance à grossir énormément - // Cette action permet d'optimiser cette table permettant de grapiller un peu de place - // Cette fonctionnalité n'est à appeler qu'occasionnellement - $entryDAO = FreshRSS_Factory::createEntryDao(); - $entryDAO->optimizeTable(); + if (!Minz_Request::isPost()) { + Minz_Request::forward($url_redirect, true); + } - $feedDAO = FreshRSS_Factory::createFeedDao(); - $feedDAO->updateCachedValues(); + @set_time_limit(300); - invalidateHttpCache(); + $entryDAO = FreshRSS_Factory::createEntryDao(); + $entryDAO->optimizeTable(); - $notif = array ( - 'type' => 'good', - 'content' => Minz_Translate::t ('optimization_complete') - ); - Minz_Session::_param ('notification', $notif); - } + $feedDAO = FreshRSS_Factory::createFeedDao(); + $feedDAO->updateCachedValues(); - Minz_Request::forward(array( - 'c' => 'configure', - 'a' => 'archiving' - ), true); + invalidateHttpCache(); + Minz_Request::good(_t('feedback.admin.optimization_complete'), $url_redirect); } + /** + * This action purges old entries from feeds. + * + * @todo should be a POST request + * @todo should be in feedController + */ public function purgeAction() { @set_time_limit(300); - $nb_month_old = max($this->view->conf->old_entries, 1); + $nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1); $date_min = time() - (3600 * 24 * 30 * $nb_month_old); $feedDAO = FreshRSS_Factory::createFeedDao(); $feeds = $feedDAO->listFeeds(); - $nbTotal = 0; + $nb_total = 0; invalidateHttpCache(); foreach ($feeds as $feed) { - $feedHistory = $feed->keepHistory(); - if ($feedHistory == -2) { //default - $feedHistory = $this->view->conf->keep_history_default; + $feed_history = $feed->keepHistory(); + if ($feed_history == -2) { + // TODO: -2 must be a constant! + // -2 means we take the default value from configuration + $feed_history = FreshRSS_Context::$user_conf->keep_history_default; } - if ($feedHistory >= 0) { - $nb = $feedDAO->cleanOldEntries($feed->id(), $date_min, $feedHistory); + + if ($feed_history >= 0) { + $nb = $feedDAO->cleanOldEntries($feed->id(), $date_min, $feed_history); if ($nb > 0) { - $nbTotal += $nb; - Minz_Log::record($nb . ' old entries cleaned in feed [' . $feed->url() . ']', Minz_Log::DEBUG); - //$feedDAO->updateLastUpdate($feed->id()); + $nb_total += $nb; + Minz_Log::debug($nb . ' old entries cleaned in feed [' . $feed->url() . ']'); } } } @@ -152,16 +183,9 @@ class FreshRSS_entry_Controller extends Minz_ActionController { $feedDAO->updateCachedValues(); invalidateHttpCache(); - - $notif = array( - 'type' => 'good', - 'content' => Minz_Translate::t('purge_completed', $nbTotal) - ); - Minz_Session::_param('notification', $notif); - - Minz_Request::forward(array( + Minz_Request::good(_t('feedback.sub.purge_completed', $nb_total), array( 'c' => 'configure', 'a' => 'archiving' - ), true); + )); } } diff --git a/app/Controllers/errorController.php b/app/Controllers/errorController.php index 922650b3d..b0bafda72 100644 --- a/app/Controllers/errorController.php +++ b/app/Controllers/errorController.php @@ -1,36 +1,51 @@ <?php +/** + * Controller to handle error page. + */ class FreshRSS_error_Controller extends Minz_ActionController { + /** + * This action is the default one for the controller. + * + * It is called by Minz_Error::error() method. + * + * Parameters are passed by Minz_Session to have a proper url: + * - error_code (default: 404) + * - error_logs (default: array()) + */ public function indexAction() { - switch (Minz_Request::param('code')) { - case 403: - $this->view->code = 'Error 403 - Forbidden'; - break; - case 404: - $this->view->code = 'Error 404 - Not found'; - break; - case 500: - $this->view->code = 'Error 500 - Internal Server Error'; - break; - case 503: - $this->view->code = 'Error 503 - Service Unavailable'; - break; - default: - $this->view->code = 'Error 404 - Not found'; + $code_int = Minz_Session::param('error_code', 404); + $error_logs = Minz_Session::param('error_logs', array()); + Minz_Session::_param('error_code'); + Minz_Session::_param('error_logs'); + + switch ($code_int) { + case 200 : + header('HTTP/1.1 200 OK'); + break; + case 403: + header('HTTP/1.1 403 Forbidden'); + $this->view->code = 'Error 403 - Forbidden'; + $this->view->errorMessage = _t('feedback.access.denied'); + break; + case 500: + header('HTTP/1.1 500 Internal Server Error'); + $this->view->code = 'Error 500 - Internal Server Error'; + break; + case 503: + header('HTTP/1.1 503 Service Unavailable'); + $this->view->code = 'Error 503 - Service Unavailable'; + break; + case 404: + default: + header('HTTP/1.1 404 Not Found'); + $this->view->code = 'Error 404 - Not found'; + $this->view->errorMessage = _t('feedback.access.not_found'); } - $errors = Minz_Request::param('logs', array()); - $this->view->errorMessage = trim(implode($errors)); - if ($this->view->errorMessage == '') { - switch(Minz_Request::param('code')) { - case 403: - $this->view->errorMessage = Minz_Translate::t('forbidden_access'); - break; - case 404: - default: - $this->view->errorMessage = Minz_Translate::t('page_not_found'); - break; - } + $error_message = trim(implode($error_logs)); + if ($error_message !== '') { + $this->view->errorMessage = $error_message; } Minz_View::prependTitle($this->view->code . ' · '); diff --git a/app/Controllers/extensionController.php b/app/Controllers/extensionController.php new file mode 100644 index 000000000..b6d2d3fe4 --- /dev/null +++ b/app/Controllers/extensionController.php @@ -0,0 +1,215 @@ +<?php + +/** + * The controller to manage extensions. + */ +class FreshRSS_extension_Controller extends Minz_ActionController { + /** + * This action is called before every other action in that class. It is + * the common boiler plate for every action. It is triggered by the + * underlying framework. + */ + public function firstAction() { + if (!FreshRSS_Auth::hasAccess()) { + Minz_Error::error(403); + } + } + + /** + * This action lists all the extensions available to the current user. + */ + public function indexAction() { + Minz_View::prependTitle(_t('admin.extensions.title') . ' · '); + $this->view->extension_list = array( + 'system' => array(), + 'user' => array(), + ); + + $extensions = Minz_ExtensionManager::listExtensions(); + foreach ($extensions as $ext) { + $this->view->extension_list[$ext->getType()][] = $ext; + } + } + + /** + * This action handles configuration of a given extension. + * + * Only administrator can configure a system extension. + * + * Parameters are: + * - e: the extension name (urlencoded) + * - additional parameters which should be handle by the extension + * handleConfigureAction() method (POST request). + */ + public function configureAction() { + if (Minz_Request::param('ajax')) { + $this->view->_useLayout(false); + } else { + $this->indexAction(); + $this->view->change_view('extension', 'index'); + } + + $ext_name = urldecode(Minz_Request::param('e')); + $ext = Minz_ExtensionManager::findExtension($ext_name); + + if (is_null($ext)) { + Minz_Error::error(404); + } + if ($ext->getType() === 'system' && !FreshRSS_Auth::hasAccess('admin')) { + Minz_Error::error(403); + } + + $this->view->extension = $ext; + $this->view->extension->handleConfigureAction(); + } + + /** + * This action enables a disabled extension for the current user. + * + * System extensions can only be enabled by an administrator. + * This action must be reached by a POST request. + * + * Parameter is: + * - e: the extension name (urlencoded). + */ + public function enableAction() { + $url_redirect = array('c' => 'extension', 'a' => 'index'); + + if (Minz_Request::isPost()) { + $ext_name = urldecode(Minz_Request::param('e')); + $ext = Minz_ExtensionManager::findExtension($ext_name); + + if (is_null($ext)) { + Minz_Request::bad(_t('feedback.extensions.not_found', $ext_name), + $url_redirect); + } + + if ($ext->isEnabled()) { + Minz_Request::bad(_t('feedback.extensions.already_enabled', $ext_name), + $url_redirect); + } + + $conf = null; + if ($ext->getType() === 'system' && FreshRSS_Auth::hasAccess('admin')) { + $conf = FreshRSS_Context::$system_conf; + } elseif ($ext->getType() === 'user') { + $conf = FreshRSS_Context::$user_conf; + } else { + Minz_Request::bad(_t('feedback.extensions.no_access', $ext_name), + $url_redirect); + } + + $res = $ext->install(); + + if ($res === true) { + $ext_list = $conf->extensions_enabled; + array_push_unique($ext_list, $ext_name); + $conf->extensions_enabled = $ext_list; + $conf->save(); + + Minz_Request::good(_t('feedback.extensions.enable.ok', $ext_name), + $url_redirect); + } else { + Minz_Log::warning('Can not enable extension ' . $ext_name . ': ' . $res); + Minz_Request::bad(_t('feedback.extensions.enable.ko', $ext_name, _url('index', 'logs')), + $url_redirect); + } + } + + Minz_Request::forward($url_redirect, true); + } + + /** + * This action disables an enabled extension for the current user. + * + * System extensions can only be disabled by an administrator. + * This action must be reached by a POST request. + * + * Parameter is: + * - e: the extension name (urlencoded). + */ + public function disableAction() { + $url_redirect = array('c' => 'extension', 'a' => 'index'); + + if (Minz_Request::isPost()) { + $ext_name = urldecode(Minz_Request::param('e')); + $ext = Minz_ExtensionManager::findExtension($ext_name); + + if (is_null($ext)) { + Minz_Request::bad(_t('feedback.extensions.not_found', $ext_name), + $url_redirect); + } + + if (!$ext->isEnabled()) { + Minz_Request::bad(_t('feedback.extensions.not_enabled', $ext_name), + $url_redirect); + } + + $conf = null; + if ($ext->getType() === 'system' && FreshRSS_Auth::hasAccess('admin')) { + $conf = FreshRSS_Context::$system_conf; + } elseif ($ext->getType() === 'user') { + $conf = FreshRSS_Context::$user_conf; + } else { + Minz_Request::bad(_t('feedback.extensions.no_access', $ext_name), + $url_redirect); + } + + $res = $ext->uninstall(); + + if ($res === true) { + $ext_list = $conf->extensions_enabled; + array_remove($ext_list, $ext_name); + $conf->extensions_enabled = $ext_list; + $conf->save(); + + Minz_Request::good(_t('feedback.extensions.disable.ok', $ext_name), + $url_redirect); + } else { + Minz_Log::warning('Can not unable extension ' . $ext_name . ': ' . $res); + Minz_Request::bad(_t('feedback.extensions.disable.ko', $ext_name, _url('index', 'logs')), + $url_redirect); + } + } + + Minz_Request::forward($url_redirect, true); + } + + /** + * This action handles deletion of an extension. + * + * Only administrator can remove an extension. + * This action must be reached by a POST request. + * + * Parameter is: + * -e: extension name (urlencoded) + */ + public function removeAction() { + if (!FreshRSS_Auth::hasAccess('admin')) { + Minz_Error::error(403); + } + + $url_redirect = array('c' => 'extension', 'a' => 'index'); + + if (Minz_Request::isPost()) { + $ext_name = urldecode(Minz_Request::param('e')); + $ext = Minz_ExtensionManager::findExtension($ext_name); + + if (is_null($ext)) { + Minz_Request::bad(_t('feedback.extensions.not_found', $ext_name), + $url_redirect); + } + + $res = recursive_unlink($ext->getPath()); + if ($res) { + Minz_Request::good(_t('feedback.extensions.removed', $ext_name), + $url_redirect); + } else { + Minz_Request::bad(_t('feedback.extensions.cannot_delete', $ext_name), + $url_redirect); + } + } + + Minz_Request::forward($url_redirect, true); + } +} diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index f75c969d9..6f544d834 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -1,180 +1,217 @@ <?php +/** + * Controller to handle every feed actions. + */ class FreshRSS_feed_Controller extends Minz_ActionController { - public function firstAction () { - if (!$this->view->loginOk) { + /** + * This action is called before every other action in that class. It is + * the common boiler plate for every action. It is triggered by the + * underlying framework. + */ + public function firstAction() { + if (!FreshRSS_Auth::hasAccess()) { // Token is useful in the case that anonymous refresh is forbidden // and CRON task cannot be used with php command so the user can // set a CRON task to refresh his feeds by using token inside url - $token = $this->view->conf->token; - $token_param = Minz_Request::param ('token', ''); + $token = FreshRSS_Context::$user_conf->token; + $token_param = Minz_Request::param('token', ''); $token_is_ok = ($token != '' && $token == $token_param); - $action = Minz_Request::actionName (); - if (!(($token_is_ok || Minz_Configuration::allowAnonymousRefresh()) && - $action === 'actualize') - ) { - Minz_Error::error ( - 403, - array ('error' => array (Minz_Translate::t ('access_denied'))) - ); + $action = Minz_Request::actionName(); + $allow_anonymous_refresh = FreshRSS_Context::$system_conf->allow_anonymous_refresh; + if ($action !== 'actualize' || + !($allow_anonymous_refresh || $token_is_ok)) { + Minz_Error::error(403); } } } - public function addAction () { - $url = Minz_Request::param('url_rss', false); + /** + * This action subscribes to a feed. + * + * It can be reached by both GET and POST requests. + * + * GET request displays a form to add and configure a feed. + * Request parameter is: + * - url_rss (default: false) + * + * POST request adds a feed in database. + * Parameters are: + * - url_rss (default: false) + * - category (default: false) + * - new_category (required if category == 'nc') + * - http_user (default: false) + * - http_pass (default: false) + * It tries to get website information from RSS feed. + * If no category is given, feed is added to the default one. + * + * If url_rss is false, nothing happened. + */ + public function addAction() { + $url = Minz_Request::param('url_rss'); if ($url === false) { + // No url, do nothing Minz_Request::forward(array( - 'c' => 'configure', - 'a' => 'feed' + 'c' => 'subscription', + 'a' => 'index' ), true); } $feedDAO = FreshRSS_Factory::createFeedDao(); - $this->catDAO = new FreshRSS_CategoryDAO (); - $this->catDAO->checkDefault (); + $this->catDAO = new FreshRSS_CategoryDAO(); + $url_redirect = array( + 'c' => 'subscription', + 'a' => 'index', + 'params' => array(), + ); + + $limits = FreshRSS_Context::$system_conf->limits; + $this->view->feeds = $feedDAO->listFeeds(); + if (count($this->view->feeds) >= $limits['max_feeds']) { + Minz_Request::bad(_t('feedback.sub.feed.over_max', $limits['max_feeds']), + $url_redirect); + } if (Minz_Request::isPost()) { @set_time_limit(300); - - $cat = Minz_Request::param ('category', false); + $cat = Minz_Request::param('category'); if ($cat === 'nc') { - $new_cat = Minz_Request::param ('new_category'); + // User want to create a new category, new_category parameter + // must exist + $new_cat = Minz_Request::param('new_category'); if (empty($new_cat['name'])) { $cat = false; } else { $cat = $this->catDAO->addCategory($new_cat); } } + if ($cat === false) { - $def_cat = $this->catDAO->getDefault (); - $cat = $def_cat->id (); + // If category was not given or if creating new category failed, + // get the default category + $this->catDAO->checkDefault(); + $def_cat = $this->catDAO->getDefault(); + $cat = $def_cat->id(); } - $user = Minz_Request::param ('http_user'); - $pass = Minz_Request::param ('http_pass'); - $params = array (); + // HTTP information are useful if feed is protected behind a + // HTTP authentication + $user = Minz_Request::param('http_user'); + $pass = Minz_Request::param('http_pass'); + $http_auth = ''; + if ($user != '' || $pass != '') { + $http_auth = $user . ':' . $pass; + } - $transactionStarted = false; + $transaction_started = false; try { - $feed = new FreshRSS_Feed ($url); - $feed->_category ($cat); - - $httpAuth = ''; - if ($user != '' || $pass != '') { - $httpAuth = $user . ':' . $pass; - } - $feed->_httpAuth ($httpAuth); + $feed = new FreshRSS_Feed($url); + } catch (FreshRSS_BadUrl_Exception $e) { + // Given url was not a valid url! + Minz_Log::warning($e->getMessage()); + Minz_Request::bad(_t('feedback.sub.feed.invalid_url', $url), $url_redirect); + } + try { $feed->load(true); - - $values = array ( - 'url' => $feed->url (), - 'category' => $feed->category (), - 'name' => $feed->name (), - 'website' => $feed->website (), - 'description' => $feed->description (), - 'lastUpdate' => time (), - 'httpAuth' => $feed->httpAuth (), - ); - - if ($feedDAO->searchByUrl ($values['url'])) { - // on est déjà abonné à ce flux - $notif = array ( - 'type' => 'bad', - 'content' => Minz_Translate::t ('already_subscribed', $feed->name ()) - ); - Minz_Session::_param ('notification', $notif); - } else { - $id = $feedDAO->addFeed ($values); - if (!$id) { - // problème au niveau de la base de données - $notif = array ( - 'type' => 'bad', - 'content' => Minz_Translate::t ('feed_not_added', $feed->name ()) - ); - Minz_Session::_param ('notification', $notif); - } else { - $feed->_id ($id); - $feed->faviconPrepare(); - - $is_read = $this->view->conf->mark_when['reception'] ? 1 : 0; - - $entryDAO = FreshRSS_Factory::createEntryDao(); - $entries = array_reverse($feed->entries()); //We want chronological order and SimplePie uses reverse order - - // on calcule la date des articles les plus anciens qu'on accepte - $nb_month_old = $this->view->conf->old_entries; - $date_min = time () - (3600 * 24 * 30 * $nb_month_old); - - //MySQL: http://docs.oracle.com/cd/E17952_01/refman-5.5-en/optimizing-innodb-transaction-management.html - //SQLite: http://stackoverflow.com/questions/1711631/how-do-i-improve-the-performance-of-sqlite - $preparedStatement = $entryDAO->addEntryPrepare(); - $transactionStarted = true; - $feedDAO->beginTransaction(); - // on ajoute les articles en masse sans vérification - foreach ($entries as $entry) { - $values = $entry->toArray(); - $values['id_feed'] = $feed->id(); - $values['id'] = min(time(), $entry->date(true)) . uSecString(); - $values['is_read'] = $is_read; - $entryDAO->addEntry($values, $preparedStatement); - } - $feedDAO->updateLastUpdate($feed->id()); - if ($transactionStarted) { - $feedDAO->commit(); - } - $transactionStarted = false; - - // ok, ajout terminé - $notif = array ( - 'type' => 'good', - 'content' => Minz_Translate::t ('feed_added', $feed->name ()) - ); - Minz_Session::_param ('notification', $notif); - - // permet de rediriger vers la page de conf du flux - $params['id'] = $feed->id (); - } - } - } catch (FreshRSS_BadUrl_Exception $e) { - Minz_Log::record ($e->getMessage (), Minz_Log::WARNING); - $notif = array ( - 'type' => 'bad', - 'content' => Minz_Translate::t ('invalid_url', $url) - ); - Minz_Session::_param ('notification', $notif); } catch (FreshRSS_Feed_Exception $e) { - Minz_Log::record ($e->getMessage (), Minz_Log::WARNING); - $notif = array ( - 'type' => 'bad', - 'content' => Minz_Translate::t ('internal_problem_feed', Minz_Url::display(array('a' => 'logs'))) + // Something went bad (timeout, server not found, etc.) + Minz_Log::warning($e->getMessage()); + Minz_Request::bad( + _t('feedback.sub.feed.internal_problem', _url('index', 'logs')), + $url_redirect ); - Minz_Session::_param ('notification', $notif); } catch (Minz_FileNotExistException $e) { - // Répertoire de cache n'existe pas - Minz_Log::record ($e->getMessage (), Minz_Log::ERROR); - $notif = array ( - 'type' => 'bad', - 'content' => Minz_Translate::t ('internal_problem_feed', Minz_Url::display(array('a' => 'logs'))) + // Cache directory doesn't exist! + Minz_Log::error($e->getMessage()); + Minz_Request::bad( + _t('feedback.sub.feed.internal_problem', _url('index', 'logs')), + $url_redirect ); - Minz_Session::_param ('notification', $notif); } - if ($transactionStarted) { - $feedDAO->rollBack (); + + if ($feedDAO->searchByUrl($feed->url())) { + Minz_Request::bad( + _t('feedback.sub.feed.already_subscribed', $feed->name()), + $url_redirect + ); + } + + $feed->_category($cat); + $feed->_httpAuth($http_auth); + + // Call the extension hook + $name = $feed->name(); + $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed); + if (is_null($feed)) { + Minz_Request::bad(_t('feed_not_added', $name), $url_redirect); + } + + $values = array( + 'url' => $feed->url(), + 'category' => $feed->category(), + 'name' => $feed->name(), + 'website' => $feed->website(), + 'description' => $feed->description(), + 'lastUpdate' => time(), + 'httpAuth' => $feed->httpAuth(), + ); + + $id = $feedDAO->addFeed($values); + if (!$id) { + // There was an error in database... we cannot say what here. + Minz_Request::bad(_t('feedback.sub.feed.not_added', $feed->name()), $url_redirect); + } + + // Ok, feed has been added in database. Now we have to refresh entries. + $feed->_id($id); + $feed->faviconPrepare(); + + $is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0; + + $entryDAO = FreshRSS_Factory::createEntryDao(); + // We want chronological order and SimplePie uses reverse order. + $entries = array_reverse($feed->entries()); + + // Calculate date of oldest entries we accept in DB. + $nb_month_old = FreshRSS_Context::$user_conf->old_entries; + $date_min = time() - (3600 * 24 * 30 * $nb_month_old); + + // Use a shared statement and a transaction to improve a LOT the + // performances. + $prepared_statement = $entryDAO->addEntryPrepare(); + $feedDAO->beginTransaction(); + foreach ($entries as $entry) { + // Entries are added without any verification. + $entry->_feed($feed->id()); + $entry->_id(min(time(), $entry->date(true)) . uSecString()); + $entry->_isRead($is_read); + + $entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry); + if (is_null($entry)) { + // An extension has returned a null value, there is nothing to insert. + continue; + } + + $values = $entry->toArray(); + $entryDAO->addEntry($values, $prepared_statement); } + $feedDAO->updateLastUpdate($feed->id()); + $feedDAO->commit(); - Minz_Request::forward (array ('c' => 'configure', 'a' => 'feed', 'params' => $params), true); + // Entries are in DB, we redirect to feed configuration page. + $url_redirect['params']['id'] = $feed->id(); + Minz_Request::good(_t('feedback.sub.feed.added', $feed->name()), $url_redirect); } else { + // GET request: we must ask confirmation to user before adding feed. + Minz_View::prependTitle(_t('sub.feed.title_add') . ' · '); - // GET request so we must ask confirmation to user - Minz_View::prependTitle(Minz_Translate::t('add_rss_feed') . ' · '); $this->view->categories = $this->catDAO->listCategories(false); $this->view->feed = new FreshRSS_Feed($url); try { - // We try to get some more information about the feed + // We try to get more information about the feed. $this->view->feed->load(true); $this->view->load_ok = true; } catch (Exception $e) { @@ -183,256 +220,291 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $feed = $feedDAO->searchByUrl($this->view->feed->url()); if ($feed) { - // Already subscribe so we redirect to the feed configuration page - $notif = array( - 'type' => 'bad', - 'content' => Minz_Translate::t( - 'already_subscribed', $feed->name() - ) - ); - Minz_Session::_param('notification', $notif); - - Minz_Request::forward(array( - 'c' => 'configure', - 'a' => 'feed', - 'params' => array( - 'id' => $feed->id() - ) - ), true); + // Already subscribe so we redirect to the feed configuration page. + $url_redirect['params']['id'] = $feed->id(); + Minz_Request::good(_t('feedback.sub.feed.already_subscribed', $feed->name()), $url_redirect); } } } - public function truncateAction () { - if (Minz_Request::isPost ()) { - $id = Minz_Request::param ('id'); - $feedDAO = FreshRSS_Factory::createFeedDao(); - $n = $feedDAO->truncate($id); - $notif = array( - 'type' => $n === false ? 'bad' : 'good', - 'content' => Minz_Translate::t ('n_entries_deleted', $n) - ); - Minz_Session::_param ('notification', $notif); - invalidateHttpCache(); - Minz_Request::forward (array ('c' => 'configure', 'a' => 'feed', 'params' => array('id' => $id)), true); + /** + * This action remove entries from a given feed. + * + * It should be reached by a POST action. + * + * Parameter is: + * - id (default: false) + */ + public function truncateAction() { + $id = Minz_Request::param('id'); + $url_redirect = array( + 'c' => 'subscription', + 'a' => 'index', + 'params' => array('id' => $id) + ); + + if (!Minz_Request::isPost()) { + Minz_Request::forward($url_redirect, true); + } + + $feedDAO = FreshRSS_Factory::createFeedDao(); + $n = $feedDAO->truncate($id); + + invalidateHttpCache(); + if ($n === false) { + Minz_Request::bad(_t('feedback.sub.feed.error'), $url_redirect); + } else { + Minz_Request::good(_t('feedback.sub.feed.n_entries_deleted', $n), $url_redirect); } } - public function actualizeAction () { + /** + * This action actualizes entries from one or several feeds. + * + * Parameters are: + * - id (default: false) + * - force (default: false) + * If id is not specified, all the feeds are actualized. But if force is + * false, process stops at 10 feeds to avoid time execution problem. + */ + public function actualizeAction() { @set_time_limit(300); $feedDAO = FreshRSS_Factory::createFeedDao(); $entryDAO = FreshRSS_Factory::createEntryDao(); Minz_Session::_param('actualize_feeds', false); - $id = Minz_Request::param ('id'); - $force = Minz_Request::param ('force', false); + $id = Minz_Request::param('id'); + $force = Minz_Request::param('force'); - // on créé la liste des flux à mettre à actualiser - // si on veut mettre un flux à jour spécifiquement, on le met - // dans la liste, mais seul (permet d'automatiser le traitement) - $feeds = array (); + // Create a list of feeds to actualize. + // If id is set and valid, corresponding feed is added to the list but + // alone in order to automatize further process. + $feeds = array(); if ($id) { - $feed = $feedDAO->searchById ($id); + $feed = $feedDAO->searchById($id); if ($feed) { - $feeds = array ($feed); + $feeds[] = $feed; } } else { - $feeds = $feedDAO->listFeedsOrderUpdate($this->view->conf->ttl_default); + $feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::$user_conf->ttl_default); } - // on calcule la date des articles les plus anciens qu'on accepte - $nb_month_old = max($this->view->conf->old_entries, 1); - $date_min = time () - (3600 * 24 * 30 * $nb_month_old); + // Calculate date of oldest entries we accept in DB. + $nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1); + $date_min = time() - (3600 * 24 * 30 * $nb_month_old); - $i = 0; - $flux_update = 0; - $is_read = $this->view->conf->mark_when['reception'] ? 1 : 0; + $updated_feeds = 0; + $is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0; foreach ($feeds as $feed) { if (!$feed->lock()) { - Minz_Log::record('Feed already being actualized: ' . $feed->url(), Minz_Log::NOTICE); + Minz_Log::notice('Feed already being actualized: ' . $feed->url()); continue; } - try { - $url = $feed->url(); - $feedHistory = $feed->keepHistory(); + try { + // Load entries $feed->load(false); - $entries = array_reverse($feed->entries()); //We want chronological order and SimplePie uses reverse order - $hasTransaction = false; + } catch (FreshRSS_Feed_Exception $e) { + Minz_Log::notice($e->getMessage()); + $feedDAO->updateLastUpdate($feed->id(), 1); + $feed->unlock(); + continue; + } - if (count($entries) > 0) { - //For this feed, check last n entry GUIDs already in database - $existingGuids = array_fill_keys ($entryDAO->listLastGuidsByFeed ($feed->id (), count($entries) + 10), 1); - $useDeclaredDate = empty($existingGuids); + $url = $feed->url(); + $feed_history = $feed->keepHistory(); + if ($feed_history == -2) { + // TODO: -2 must be a constant! + // -2 means we take the default value from configuration + $feed_history = FreshRSS_Context::$user_conf->keep_history_default; + } - if ($feedHistory == -2) { //default - $feedHistory = $this->view->conf->keep_history_default; + // We want chronological order and SimplePie uses reverse order. + $entries = array_reverse($feed->entries()); + if (count($entries) > 0) { + // For this feed, check last n entry GUIDs already in database. + $existing_guids = array_fill_keys($entryDAO->listLastGuidsByFeed( + $feed->id(), count($entries) + 10 + ), 1); + $use_declared_date = empty($existing_guids); + + // Add entries in database if possible. + $prepared_statement = $entryDAO->addEntryPrepare(); + $feedDAO->beginTransaction(); + foreach ($entries as $entry) { + $entry_date = $entry->date(true); + if (isset($existing_guids[$entry->guid()]) || + ($feed_history == 0 && $entry_date < $date_min)) { + // This entry already exists in DB or should not be added + // considering configuration and date. + continue; } - $preparedStatement = $entryDAO->addEntryPrepare(); - $hasTransaction = true; - $feedDAO->beginTransaction(); - - // On ne vérifie pas strictement que l'article n'est pas déjà en BDD - // La BDD refusera l'ajout car (id_feed, guid) doit être unique - foreach ($entries as $entry) { - $eDate = $entry->date(true); - if ((!isset($existingGuids[$entry->guid()])) && - (($feedHistory != 0) || ($eDate >= $date_min))) { - $values = $entry->toArray(); - //Use declared date at first import, otherwise use discovery date - $values['id'] = ($useDeclaredDate || $eDate < $date_min) ? - min(time(), $eDate) . uSecString() : - uTimeString(); - $values['is_read'] = $is_read; - $entryDAO->addEntry($values, $preparedStatement); - } + $id = uTimeString(); + if ($use_declared_date || $entry_date < $date_min) { + // Use declared date at first import. + $id = min(time(), $entry_date) . uSecString(); } - } - if (($feedHistory >= 0) && (rand(0, 30) === 1)) { - if (!$hasTransaction) { - $feedDAO->beginTransaction(); - } - $nb = $feedDAO->cleanOldEntries ($feed->id (), $date_min, max($feedHistory, count($entries) + 10)); - if ($nb > 0) { - Minz_Log::record ($nb . ' old entries cleaned in feed [' . $feed->url() . ']', Minz_Log::DEBUG); + $entry->_id($id); + $entry->_isRead($is_read); + + $entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry); + if (is_null($entry)) { + // An extension has returned a null value, there is nothing to insert. + continue; } + + $values = $entry->toArray(); + $entryDAO->addEntry($values, $prepared_statement); } + } - // on indique que le flux vient d'être mis à jour en BDD - $feedDAO->updateLastUpdate ($feed->id (), 0, $hasTransaction); - if ($hasTransaction) { - $feedDAO->commit(); + if ($feed_history >= 0 && rand(0, 30) === 1) { + // TODO: move this function in web cron when available (see entry::purge) + // Remove old entries once in 30. + if (!$feedDAO->hasTransaction()) { + $feedDAO->beginTransaction(); } - $flux_update++; - if (($feed->url() !== $url)) { //HTTP 301 Moved Permanently - Minz_Log::record('Feed ' . $url . ' moved permanently to ' . $feed->url(), Minz_Log::NOTICE); - $feedDAO->updateFeed($feed->id(), array('url' => $feed->url())); + + $nb = $feedDAO->cleanOldEntries($feed->id(), + $date_min, + max($feed_history, count($entries) + 10)); + if ($nb > 0) { + Minz_Log::debug($nb . ' old entries cleaned in feed [' . + $feed->url() . ']'); } - } catch (FreshRSS_Feed_Exception $e) { - Minz_Log::record ($e->getMessage (), Minz_Log::NOTICE); - $feedDAO->updateLastUpdate ($feed->id (), 1); + } + + $feedDAO->updateLastUpdate($feed->id(), 0, $feedDAO->hasTransaction()); + if ($feedDAO->hasTransaction()) { + $feedDAO->commit(); + } + + if ($feed->url() !== $url) { + // HTTP 301 Moved Permanently + Minz_Log::notice('Feed ' . $url . ' moved permanently to ' . $feed->url()); + $feedDAO->updateFeed($feed->id(), array('url' => $feed->url())); } $feed->faviconPrepare(); $feed->unlock(); + $updated_feeds++; unset($feed); - // On arrête à 10 flux pour ne pas surcharger le serveur - // sauf si le paramètre $force est à vrai - $i++; - if ($i >= 10 && !$force) { + // No more than 10 feeds unless $force is true to avoid overloading + // the server. + if ($updated_feeds >= 10 && !$force) { break; } } - $url = array (); - if ($flux_update === 1) { - // on a mis un seul flux à jour - $feed = reset ($feeds); - $notif = array ( - 'type' => 'good', - 'content' => Minz_Translate::t ('feed_actualized', $feed->name ()) - ); - } elseif ($flux_update > 1) { - // plusieurs flux on été mis à jour - $notif = array ( + if (Minz_Request::param('ajax')) { + // Most of the time, ajax request is for only one feed. But since + // there are several parallel requests, we should return that there + // are several updated feeds. + $notif = array( 'type' => 'good', - 'content' => Minz_Translate::t ('n_feeds_actualized', $flux_update) + 'content' => _t('feedback.sub.feed.actualizeds') ); + Minz_Session::_param('notification', $notif); + // No layout in ajax request. + $this->view->_useLayout(false); + return; + } + + // Redirect to the main page with correct notification. + if ($updated_feeds === 1) { + $feed = reset($feeds); + Minz_Request::good(_t('feedback.sub.feed.actualized', $feed->name()), array( + 'params' => array('get' => 'f_' . $feed->id()) + )); + } elseif ($updated_feeds > 1) { + Minz_Request::good(_t('feedback.sub.feed.n_actualized', $updated_feeds), array()); } else { - // aucun flux n'a été mis à jour, oups - $notif = array ( - 'type' => 'good', - 'content' => Minz_Translate::t ('no_feed_to_refresh') - ); + Minz_Request::good(_t('feedback.sub.feed.no_refresh'), array()); + } + } + + /** + * This action changes the category of a feed. + * + * This page must be reached by a POST request. + * + * Parameters are: + * - f_id (default: false) + * - c_id (default: false) + * If c_id is false, default category is used. + * + * @todo should handle order of the feed inside the category. + */ + public function moveAction() { + if (!Minz_Request::isPost()) { + Minz_Request::forward(array('c' => 'subscription'), true); } - if ($i === 1) { - // Si on a voulu mettre à jour qu'un flux - // on filtre l'affichage par ce flux - $feed = reset ($feeds); - $url['params'] = array ('get' => 'f_' . $feed->id ()); + $feed_id = Minz_Request::param('f_id'); + $cat_id = Minz_Request::param('c_id'); + + if ($cat_id === false) { + // If category was not given get the default one. + $catDAO = new FreshRSS_CategoryDAO(); + $catDAO->checkDefault(); + $def_cat = $catDAO->getDefault(); + $cat_id = $def_cat->id(); } - if (Minz_Request::param ('ajax', 0) === 0) { - Minz_Session::_param ('notification', $notif); - Minz_Request::forward ($url, true); + $feedDAO = FreshRSS_Factory::createFeedDao(); + $values = array('category' => $cat_id); + + $feed = $feedDAO->searchById($feed_id); + if ($feed && ($feed->category() == $cat_id || + $feedDAO->updateFeed($feed_id, $values))) { + // TODO: return something useful } else { - // Une requête Ajax met un seul flux à jour. - // Comme en principe plusieurs requêtes ont lieu, - // on indique que "plusieurs flux ont été mis à jour". - // Cela permet d'avoir une notification plus proche du - // ressenti utilisateur - $notif = array ( - 'type' => 'good', - 'content' => Minz_Translate::t ('feeds_actualized') - ); - Minz_Session::_param ('notification', $notif); - // et on désactive le layout car ne sert à rien - $this->view->_useLayout (false); + Minz_Log::warning('Cannot move feed `' . $feed_id . '` ' . + 'in the category `' . $cat_id . '`'); + Minz_Error::error(404); } } - public function deleteAction () { - if (Minz_Request::isPost ()) { - $type = Minz_Request::param ('type', 'feed'); - $id = Minz_Request::param ('id'); - - $feedDAO = FreshRSS_Factory::createFeedDao(); - if ($type == 'category') { - // List feeds to remove then related user queries. - $feeds = $feedDAO->listByCategory($id); + /** + * This action deletes a feed. + * + * This page must be reached by a POST request. + * If there are related queries, they are deleted too. + * + * Parameters are: + * - id (default: false) + * - r (default: false) + * r permits to redirect to a given page at the end of this action. + * + * @todo handle "r" redirection in Minz_Request::forward()? + */ + public function deleteAction() { + $redirect_url = Minz_Request::param('r', false, true); + if (!$redirect_url) { + $redirect_url = array('c' => 'subscription', 'a' => 'index'); + } - if ($feedDAO->deleteFeedByCategory ($id)) { - // Remove related queries - foreach ($feeds as $feed) { - $this->view->conf->remove_query_by_get('f_' . $feed->id()); - } - $this->view->conf->save(); + if (!Minz_Request::isPost()) { + Minz_Request::forward($redirect_url, true); + } - $notif = array ( - 'type' => 'good', - 'content' => Minz_Translate::t ('category_emptied') - ); - //TODO: Delete old favicons - } else { - $notif = array ( - 'type' => 'bad', - 'content' => Minz_Translate::t ('error_occured') - ); - } - } else { - if ($feedDAO->deleteFeed ($id)) { - // Remove related queries - $this->view->conf->remove_query_by_get('f_' . $id); - $this->view->conf->save(); - - $notif = array ( - 'type' => 'good', - 'content' => Minz_Translate::t ('feed_deleted') - ); - //TODO: Delete old favicon - } else { - $notif = array ( - 'type' => 'bad', - 'content' => Minz_Translate::t ('error_occured') - ); - } - } + $id = Minz_Request::param('id'); + $feedDAO = FreshRSS_Factory::createFeedDao(); + if ($feedDAO->deleteFeed($id)) { + // TODO: Delete old favicon - Minz_Session::_param ('notification', $notif); + // Remove related queries + FreshRSS_Context::$user_conf->queries = remove_query_by_get( + 'f_' . $id, FreshRSS_Context::$user_conf->queries); + FreshRSS_Context::$user_conf->save(); - $redirect_url = Minz_Request::param('r', false, true); - if ($redirect_url) { - Minz_Request::forward($redirect_url); - } elseif ($type == 'category') { - Minz_Request::forward(array ('c' => 'configure', 'a' => 'categorize'), true); - } else { - Minz_Request::forward(array ('c' => 'configure', 'a' => 'feed'), true); - } + Minz_Request::good(_t('feedback.sub.feed.deleted'), $redirect_url); + } else { + Minz_Request::bad(_t('feedback.sub.feed.error'), $redirect_url); } } } diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php index f329766b8..589777b2a 100644 --- a/app/Controllers/importExportController.php +++ b/app/Controllers/importExportController.php @@ -1,12 +1,17 @@ <?php +/** + * Controller to handle every import and export actions. + */ class FreshRSS_importExport_Controller extends Minz_ActionController { + /** + * This action is called before every other action in that class. It is + * the common boiler plate for every action. It is triggered by the + * underlying framework. + */ public function firstAction() { - if (!$this->view->loginOk) { - Minz_Error::error( - 403, - array('error' => array(_t('access_denied'))) - ); + if (!FreshRSS_Auth::hasAccess()) { + Minz_Error::error(403); } require_once(LIB_PATH . '/lib_opml.php'); @@ -16,13 +21,23 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $this->feedDAO = FreshRSS_Factory::createFeedDao(); } + /** + * This action displays the main page for import / export system. + */ public function indexAction() { - $this->view->categories = $this->catDAO->listCategories(); $this->view->feeds = $this->feedDAO->listFeeds(); - - Minz_View::prependTitle(_t('import_export') . ' · '); + Minz_View::prependTitle(_t('sub.import_export.title') . ' · '); } + /** + * This action handles import action. + * + * It must be reached by a POST request. + * + * Parameter is: + * - file (default: nothing!) + * Available file types are: zip, json or xml. + */ public function importAction() { if (!Minz_Request::isPost()) { Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true); @@ -33,7 +48,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { if ($status_file !== 0) { Minz_Log::error('File cannot be uploaded. Error code: ' . $status_file); - Minz_Request::bad(_t('file_cannot_be_uploaded'), + Minz_Request::bad(_t('feedback.import_export.file_cannot_be_uploaded'), array('c' => 'importExport', 'a' => 'index')); } @@ -55,7 +70,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { if (!is_resource($zip)) { // zip_open cannot open file: something is wrong Minz_Log::error('Zip archive cannot be imported. Error code: ' . $zip); - Minz_Request::bad(_t('zip_error'), + Minz_Request::bad(_t('feedback.import_export.zip_error'), array('c' => 'importExport', 'a' => 'index')); } @@ -77,7 +92,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { zip_close($zip); } elseif ($type_file === 'zip') { // Zip extension is not loaded - Minz_Request::bad(_t('no_zip_extension'), + Minz_Request::bad(_t('feedback.import_export.no_zip_extension'), array('c' => 'importExport', 'a' => 'index')); } elseif ($type_file !== 'unknown') { $list_files[$type_file][] = file_get_contents($file['tmp_name']); @@ -92,24 +107,26 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $error = $this->importOpml($opml_file); } foreach ($list_files['json_starred'] as $article_file) { - $error = $this->importArticles($article_file, true); + $error = $this->importJson($article_file, true); } foreach ($list_files['json_feed'] as $article_file) { - $error = $this->importArticles($article_file); + $error = $this->importJson($article_file); } // And finally, we get import status and redirect to the home page Minz_Session::_param('actualize_feeds', true); - $content_notif = $error === true ? _t('feeds_imported_with_errors') : - _t('feeds_imported'); + $content_notif = $error === true ? _t('feedback.import_export.feeds_imported_with_errors') : + _t('feedback.import_export.feeds_imported'); Minz_Request::good($content_notif); } + /** + * This method tries to guess the file type based on its name. + * + * Itis a *very* basic guess file type function. Only based on filename. + * That's could be improved but should be enough for what we have to do. + */ private function guessFileType($filename) { - // A *very* basic guess file type function. Only based on filename - // That's could be improved but should be enough, at least for a first - // implementation. - if (substr_compare($filename, '.zip', -4) === 0) { return 'zip'; } elseif (substr_compare($filename, '.opml', -5) === 0 || @@ -125,10 +142,16 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { } } + /** + * This method parses and imports an OPML file. + * + * @param string $opml_file the OPML file content. + * @return boolean true if an error occured, false else. + */ private function importOpml($opml_file) { $opml_array = array(); try { - $opml_array = libopml_parse_string($opml_file); + $opml_array = libopml_parse_string($opml_file, false); } catch (LibOPML_Exception $e) { Minz_Log::warning($e->getMessage()); return true; @@ -139,35 +162,78 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { return $this->addOpmlElements($opml_array['body']); } + /** + * This method imports an OPML file based on its body. + * + * @param array $opml_elements an OPML element (body or outline). + * @param string $parent_cat the name of the parent category. + * @return boolean true if an error occured, false else. + */ private function addOpmlElements($opml_elements, $parent_cat = null) { $error = false; + + $nb_feeds = count($this->feedDAO->listFeeds()); + $nb_cats = count($this->catDAO->listCategories(false)); + $limits = FreshRSS_Context::$system_conf->limits; + foreach ($opml_elements as $elt) { - $res = false; + $is_error = false; if (isset($elt['xmlUrl'])) { - $res = $this->addFeedOpml($elt, $parent_cat); + // If xmlUrl exists, it means it is a feed + if ($nb_feeds >= $limits['max_feeds']) { + Minz_Log::warning(_t('feedback.sub.feed.over_max', + $limits['max_feeds'])); + $is_error = true; + continue; + } + + $is_error = $this->addFeedOpml($elt, $parent_cat); + if (!$is_error) { + $nb_feeds += 1; + } } else { - $res = $this->addCategoryOpml($elt, $parent_cat); + // No xmlUrl? It should be a category! + $limit_reached = ($nb_cats >= $limits['max_categories']); + if ($limit_reached) { + Minz_Log::warning(_t('feedback.sub.category.over_max', + $limits['max_categories'])); + } + + $is_error = $this->addCategoryOpml($elt, $parent_cat, $limit_reached); + if (!$is_error) { + $nb_cats += 1; + } } - if (!$error && $res) { + if (!$error && $is_error) { // oops: there is at least one error! - $error = $res; + $error = $is_error; } } return $error; } + /** + * This method imports an OPML feed element. + * + * @param array $feed_elt an OPML element (must be a feed element). + * @param string $parent_cat the name of the parent category. + * @return boolean true if an error occured, false else. + */ private function addFeedOpml($feed_elt, $parent_cat) { + $default_cat = $this->catDAO->getDefault(); if (is_null($parent_cat)) { // This feed has no parent category so we get the default one - $parent_cat = $this->catDAO->getDefault()->name(); + $parent_cat = $default_cat->name(); } $cat = $this->catDAO->searchByName($parent_cat); - - if (!$cat) { - return true; + if (is_null($cat)) { + // If there is not $cat, it means parent category does not exist in + // database. + // If it happens, take the default category. + $cat = $default_cat; } // We get different useful information @@ -191,10 +257,16 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $feed->_website($website); $feed->_description($description); - // addFeedObject checks if feed is already in DB so nothing else to - // check here - $id = $this->feedDAO->addFeedObject($feed); - $error = ($id === false); + // Call the extension hook + $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed); + if (!is_null($feed)) { + // addFeedObject checks if feed is already in DB so nothing else to + // check here + $id = $this->feedDAO->addFeedObject($feed); + $error = ($id === false); + } else { + $error = true; + } } catch (FreshRSS_Feed_Exception $e) { Minz_Log::warning($e->getMessage()); $error = true; @@ -203,12 +275,24 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { return $error; } - private function addCategoryOpml($cat_elt, $parent_cat) { + /** + * This method imports an OPML category element. + * + * @param array $cat_elt an OPML element (must be a category element). + * @param string $parent_cat the name of the parent category. + * @param boolean $cat_limit_reached indicates if category limit has been reached. + * if yes, category is not added (but we try for feeds!) + * @return boolean true if an error occured, false else. + */ + private function addCategoryOpml($cat_elt, $parent_cat, $cat_limit_reached) { // Create a new Category object $cat = new FreshRSS_Category(Minz_Helper::htmlspecialchars_utf8($cat_elt['text'])); - $id = $this->catDAO->addCategoryObject($cat); - $error = ($id === false); + $error = true; + if (!$cat_limit_reached) { + $id = $this->catDAO->addCategoryObject($cat); + $error = ($id === false); + } if (isset($cat_elt['@outlines'])) { // Our cat_elt contains more categories or more feeds, so we @@ -223,28 +307,55 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { return $error; } - private function importArticles($article_file, $starred = false) { + /** + * This method import a JSON-based file (Google Reader format). + * + * @param string $article_file the JSON file content. + * @param boolean $starred true if articles from the file must be starred. + * @return boolean true if an error occured, false else. + */ + private function importJson($article_file, $starred = false) { $article_object = json_decode($article_file, true); if (is_null($article_object)) { Minz_Log::warning('Try to import a non-JSON file'); return true; } - $is_read = $this->view->conf->mark_when['reception'] ? 1 : 0; + $is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0; - $google_compliant = ( - strpos($article_object['id'], 'com.google') !== false - ); + $google_compliant = strpos($article_object['id'], 'com.google') !== false; $error = false; $article_to_feed = array(); + $nb_feeds = count($this->feedDAO->listFeeds()); + $limits = FreshRSS_Context::$system_conf->limits; + // First, we check feeds of articles are in DB (and add them if needed). foreach ($article_object['items'] as $item) { - $feed = $this->addFeedArticles($item['origin'], $google_compliant); + $key = $google_compliant ? 'htmlUrl' : 'feedUrl'; + $feed = new FreshRSS_Feed($item['origin'][$key]); + $feed = $this->feedDAO->searchByUrl($feed->url()); + if (is_null($feed)) { - $error = true; - } else { + // Feed does not exist in DB,we should to try to add it. + if ($nb_feeds >= $limits['max_feeds']) { + // Oops, no more place! + Minz_Log::warning(_t('feedback.sub.feed.over_max', $limits['max_feeds'])); + } else { + $feed = $this->addFeedJson($item['origin'], $google_compliant); + } + + if (is_null($feed)) { + // Still null? It means something went wrong. + $error = true; + } else { + // Nice! Increase the counter. + $nb_feeds += 1; + } + } + + if (!is_null($feed)) { $article_to_feed[$item['id']] = $feed->id(); } } @@ -254,6 +365,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $this->entryDAO->beginTransaction(); foreach ($article_object['items'] as $item) { if (!isset($article_to_feed[$item['id']])) { + // Related feed does not exist for this entry, do nothing. continue; } @@ -263,6 +375,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { 'summary' : 'content'; $tags = $item['categories']; if ($google_compliant) { + // Remove tags containing "/state/com.google" which are useless. $tags = array_filter($tags, function($var) { return strpos($var, '/state/com.google') === false; }); @@ -276,6 +389,12 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $entry->_id(min(time(), $entry->date(true)) . uSecString()); $entry->_tags($tags); + $entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry); + if (is_null($entry)) { + // An extension has returned a null value, there is nothing to insert. + continue; + } + $values = $entry->toArray(); $id = $this->entryDAO->addEntry($values, $prepared_statement); @@ -288,7 +407,15 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { return $error; } - private function addFeedArticles($origin, $google_compliant) { + /** + * This method import a JSON-based feed (Google Reader format). + * + * @param array $origin represents a feed. + * @param boolean $google_compliant takes care of some specific values if true. + * @return FreshRSS_Feed if feed is in database at the end of the process, + * else null. + */ + private function addFeedJson($origin, $google_compliant) { $default_cat = $this->catDAO->getDefault(); $return = null; @@ -298,19 +425,23 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $website = $origin['htmlUrl']; try { - // Create a Feed object and add it in DB + // Create a Feed object and add it in database. $feed = new FreshRSS_Feed($url); $feed->_category($default_cat->id()); $feed->_name($name); $feed->_website($website); - // addFeedObject checks if feed is already in DB so nothing else to - // check here - $id = $this->feedDAO->addFeedObject($feed); + // Call the extension hook + $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed); + if (!is_null($feed)) { + // addFeedObject checks if feed is already in DB so nothing else to + // check here. + $id = $this->feedDAO->addFeedObject($feed); - if ($id !== false) { - $feed->_id($id); - $return = $feed; + if ($id !== false) { + $feed->_id($id); + $return = $feed; + } } } catch (FreshRSS_Feed_Exception $e) { Minz_Log::warning($e->getMessage()); @@ -319,6 +450,16 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { return $return; } + /** + * This action handles export action. + * + * This action must be reached by a POST request. + * + * Parameters are: + * - export_opml (default: false) + * - export_starred (default: false) + * - export_feeds (default: array()) a list of feed ids + */ public function exportAction() { if (!Minz_Request::isPost()) { Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true); @@ -336,7 +477,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { } if ($export_starred) { - $export_files['starred.json'] = $this->generateArticles('starred'); + $export_files['starred.json'] = $this->generateEntries('starred'); } foreach ($export_feeds as $feed_id) { @@ -344,9 +485,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { if ($feed) { $filename = 'feed_' . $feed->category() . '_' . $feed->id() . '.json'; - $export_files[$filename] = $this->generateArticles( - 'feed', $feed - ); + $export_files[$filename] = $this->generateEntries('feed', $feed); } } @@ -357,7 +496,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $this->exportZip($export_files); } catch (Exception $e) { # Oops, there is no Zip extension! - Minz_Request::bad(_t('export_no_zip_extension'), + Minz_Request::bad(_t('feedback.import_export.export_no_zip_extension'), array('c' => 'importExport', 'a' => 'index')); } } elseif ($nb_files === 1) { @@ -366,10 +505,16 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $type = $this->guessFileType($filename); $this->exportFile('freshrss_' . $filename, $export_files[$filename], $type); } else { + // Nothing to do... Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true); } } + /** + * This method returns the OPML file based on user subscriptions. + * + * @return string the OPML file content. + */ private function generateOpml() { $list = array(); foreach ($this->catDAO->listCategories() as $key => $cat) { @@ -381,23 +526,29 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { return $this->view->helperToString('export/opml'); } - private function generateArticles($type, $feed = NULL) { + /** + * This method returns a JSON file content. + * + * @param string $type must be "starred" or "feed" + * @param FreshRSS_Feed $feed feed of which we want to get entries. + * @return string the JSON file content. + */ + private function generateEntries($type, $feed = NULL) { $this->view->categories = $this->catDAO->listCategories(); if ($type == 'starred') { - $this->view->list_title = _t('starred_list'); + $this->view->list_title = _t('sub.import_export.starred_list'); $this->view->type = 'starred'; $unread_fav = $this->entryDAO->countUnreadReadFavorites(); $this->view->entries = $this->entryDAO->listWhere( - 's', '', FreshRSS_Entry::STATE_ALL, 'ASC', - $unread_fav['all'] + 's', '', FreshRSS_Entry::STATE_ALL, 'ASC', $unread_fav['all'] ); } elseif ($type == 'feed' && !is_null($feed)) { - $this->view->list_title = _t('feed_list', $feed->name()); + $this->view->list_title = _t('sub.import_export.feed_list', $feed->name()); $this->view->type = 'feed/' . $feed->id(); $this->view->entries = $this->entryDAO->listWhere( 'f', $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC', - $this->view->conf->posts_per_page + FreshRSS_Context::$user_conf->posts_per_page ); $this->view->feed = $feed; } @@ -405,6 +556,12 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { return $this->view->helperToString('export/articles'); } + /** + * This method zips a list of files and returns it by HTTP. + * + * @param array $files list of files where key is filename and value the content. + * @throws Exception if Zip extension is not loaded. + */ private function exportZip($files) { if (!extension_loaded('zip')) { throw new Exception(); @@ -428,6 +585,14 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { unlink($zip_file); } + /** + * This method returns a single file (OPML or JSON) by HTTP. + * + * @param string $filename + * @param string $content + * @param string $type the file type (opml, json_feed or json_starred). + * If equals to unknown, nothing happens. + */ private function exportFile($filename, $content, $type) { if ($type === 'unknown') { return; diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php index d3b299f4e..ddcf0b4e4 100755 --- a/app/Controllers/indexController.php +++ b/app/Controllers/indexController.php @@ -1,499 +1,236 @@ <?php +/** + * This class handles main actions of FreshRSS. + */ 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 ( + /** + * This action only redirect on the default view mode (normal or global) + */ + public function indexAction() { + $prefered_output = FreshRSS_Context::$user_conf->view_mode; + Minz_Request::forward(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(); + 'a' => $prefered_output + )); + } - $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'))) - ); + /** + * This action displays the normal view of FreshRSS. + */ + public function normalAction() { + $allow_anonymous = FreshRSS_Context::$system_conf->allow_anonymous; + if (!FreshRSS_Auth::hasAccess() && !$allow_anonymous) { + Minz_Request::forward(array('c' => 'auth', 'a' => 'login')); 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; - } + try { + $this->updateContext(); + } catch (FreshRSS_Context_Exception $e) { + Minz_Error::error(404); } - $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); + $entries = $this->listEntriesByContext(); + + $nb_entries = count($entries); + if ($nb_entries > FreshRSS_Context::$number) { + // We have more elements for pagination + $last_entry = array_pop($entries); + FreshRSS_Context::$next_id = $last_entry->id(); } - 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(); + $first_entry = $nb_entries > 0 ? $entries[0] : null; + FreshRSS_Context::$id_max = $first_entry === null ? + (time() - 1) . '000000' : + $first_entry->id(); + if (FreshRSS_Context::$order === 'ASC') { + // In this case we do not know but we guess id_max + $id_max = (time() - 1) . '000000'; + if (strcmp($id_max, FreshRSS_Context::$id_max) > 0) { + FreshRSS_Context::$id_max = $id_max; + } } $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'))) - ); + Minz_Log::notice($e->getMessage()); + Minz_Error::error(404); } - } - /* - * 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; + $this->view->categories = FreshRSS_Context::$categories; + + $this->view->rss_title = FreshRSS_Context::$name . ' | ' . Minz_View::title(); + $title = FreshRSS_Context::$name; + if (FreshRSS_Context::$get_unread > 0) { + $title = '(' . FreshRSS_Context::$get_unread . ') ' . $title; } + Minz_View::prependTitle($title . ' · '); } - - public function aboutAction () { - Minz_View::prependTitle (Minz_Translate::t ('about') . ' · '); + + /** + * This action displays the reader view of FreshRSS. + * + * @todo: change this view into specific CSS rules? + */ + public function readerAction() { + $this->normalAction(); } - public function logsAction () { - if (!$this->view->loginOk) { - Minz_Error::error ( - 403, - array ('error' => array (Minz_Translate::t ('access_denied'))) - ); + /** + * This action displays the global view of FreshRSS. + */ + public function globalAction() { + $allow_anonymous = FreshRSS_Context::$system_conf->allow_anonymous; + if (!FreshRSS_Auth::hasAccess() && !$allow_anonymous) { + Minz_Request::forward(array('c' => 'auth', 'a' => 'login')); + return; } - Minz_View::prependTitle (Minz_Translate::t ('logs') . ' · '); + Minz_View::appendScript(Minz_Url::display('/scripts/global_view.js?' . @filemtime(PUBLIC_PATH . '/scripts/global_view.js'))); - if (Minz_Request::isPost ()) { - FreshRSS_LogDAO::truncate(); + try { + $this->updateContext(); + } catch (FreshRSS_Context_Exception $e) { + Minz_Error::error(404); } - $logs = FreshRSS_LogDAO::lines(); //TODO: ask only the necessary lines + $this->view->categories = FreshRSS_Context::$categories; - //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); + $this->view->rss_title = FreshRSS_Context::$name . ' | ' . Minz_View::title(); + $title = _t('index.feed.title_global'); + if (FreshRSS_Context::$get_unread > 0) { + $title = '(' . FreshRSS_Context::$get_unread . ') ' . $title; + } + Minz_View::prependTitle($title . ' · '); } - 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'] . ']!'; - } + /** + * This action displays the RSS feed of FreshRSS. + */ + public function rssAction() { + $allow_anonymous = FreshRSS_Context::$system_conf->allow_anonymous; + $token = FreshRSS_Context::$user_conf->token; + $token_param = Minz_Request::param('token', ''); + $token_is_ok = ($token != '' && $token === $token_param); + + // Check if user has access. + if (!FreshRSS_Auth::hasAccess() && + !$allow_anonymous && + !$token_is_ok) { + Minz_Error::error(403); } - 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); + + try { + $this->updateContext(); + } catch (FreshRSS_Context_Exception $e) { + Minz_Error::error(404); } - header('Content-Type: application/json; charset=UTF-8'); - $this->view->res = json_encode ($res); - } + try { + $this->view->entries = $this->listEntriesByContext(); + } catch (FreshRSS_EntriesGetter_Exception $e) { + Minz_Log::notice($e->getMessage()); + Minz_Error::error(404); + } - public function logoutAction () { + // No layout for RSS output. + $this->view->rss_title = FreshRSS_Context::$name . ' | ' . Minz_View::title(); $this->view->_useLayout(false); - invalidateHttpCache(); - Minz_Session::_param('currentUser'); - Minz_Session::_param('mail'); - Minz_Session::_param('passwordHash'); + header('Content-Type: application/rss+xml; charset=utf-8'); } - 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; - } + /** + * 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) + */ + private function updateContext() { + // Update number of read / unread variables. + $entryDAO = FreshRSS_Factory::createEntryDao(); + FreshRSS_Context::$total_starred = $entryDAO->countUnreadReadFavorites(); + FreshRSS_Context::$total_unread = FreshRSS_CategoryDAO::CountUnreads( + FreshRSS_Context::$categories, 1 + ); - 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(); + FreshRSS_Context::_get(Minz_Request::param('get', 'a')); + FreshRSS_Context::$state = Minz_Request::param( + 'state', FreshRSS_Context::$user_conf->default_state + ); + $state_forced_by_user = Minz_Request::param('state', false) !== false; + if (FreshRSS_Context::$user_conf->default_view === 'adaptive' && + FreshRSS_Context::$get_unread <= 0 && + !FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_READ) && + !$state_forced_by_user) { + FreshRSS_Context::$state |= FreshRSS_Entry::STATE_READ; } - } - 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()); - } - } + FreshRSS_Context::$search = Minz_Request::param('search', ''); + FreshRSS_Context::$order = Minz_Request::param( + 'order', FreshRSS_Context::$user_conf->sort_order + ); + FreshRSS_Context::$number = Minz_Request::param( + 'nb', FreshRSS_Context::$user_conf->posts_per_page + ); + FreshRSS_Context::$first_id = Minz_Request::param('next', ''); } - public function formLoginAction () { - if ($this->view->loginOk) { - Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true); - } + /** + * This method returns a list of entries based on the Context object. + */ + private function listEntriesByContext() { + $entryDAO = FreshRSS_Factory::createEntryDao(); - 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'))) - ); + $get = FreshRSS_Context::currentGet(true); + if (count($get) > 1) { + $type = $get[0]; + $id = $get[1]; + } else { + $type = $get; + $id = ''; } - 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); + return $entryDAO->listWhere( + $type, $id, FreshRSS_Context::$state, FreshRSS_Context::$order, + FreshRSS_Context::$number + 1, FreshRSS_Context::$first_id, + FreshRSS_Context::$search + ); } - 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; - } + /** + * This action displays the about page of FreshRSS. + */ + public function aboutAction() { + Minz_View::prependTitle(_t('index.about.title') . ' · '); + } - $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; + /** + * This action displays logs of FreshRSS for the current user. + */ + public function logsAction() { + if (!FreshRSS_Auth::hasAccess()) { + Minz_Error::error(403); } - invalidateHttpCache(); + Minz_View::prependTitle(_t('index.log.title') . ' · '); 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'); - } + FreshRSS_LogDAO::truncate(); + } - $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); + $logs = FreshRSS_LogDAO::lines(); //TODO: ask only the necessary lines - Minz_Request::bad(_t('invalid_login'), - array('c' => 'index', 'a' => 'resetAuth')); - } - } + //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); } } diff --git a/app/Controllers/javascriptController.php b/app/Controllers/javascriptController.php index 67148350f..421cf6f72 100755 --- a/app/Controllers/javascriptController.php +++ b/app/Controllers/javascriptController.php @@ -1,14 +1,14 @@ <?php class FreshRSS_javascript_Controller extends Minz_ActionController { - public function firstAction () { - $this->view->_useLayout (false); + public function firstAction() { + $this->view->_useLayout(false); } - public function actualizeAction () { + public function actualizeAction() { header('Content-Type: text/javascript; charset=UTF-8'); $feedDAO = FreshRSS_Factory::createFeedDao(); - $this->view->feeds = $feedDAO->listFeedsOrderUpdate($this->view->conf->ttl_default); + $this->view->feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::$user_conf->ttl_default); } public function nbUnreadsPerFeedAction() { @@ -28,17 +28,20 @@ class FreshRSS_javascript_Controller extends Minz_ActionController { $user = isset($_GET['user']) ? $_GET['user'] : ''; if (ctype_alnum($user)) { try { - $conf = new FreshRSS_Configuration($user); + $salt = FreshRSS_Context::$system_conf->salt; + $conf = get_user_configuration($user); $s = $conf->passwordHash; if (strlen($s) >= 60) { $this->view->salt1 = substr($s, 0, 29); //CRYPT_BLOWFISH Salt: "$2a$", a two digit cost parameter, "$", and 22 characters from the alphabet "./0-9A-Za-z". - $this->view->nonce = sha1(Minz_Configuration::salt() . uniqid(mt_rand(), true)); + $this->view->nonce = sha1($salt . uniqid(mt_rand(), true)); Minz_Session::_param('nonce', $this->view->nonce); return; //Success } } catch (Minz_Exception $me) { - Minz_Log::record('Nonce failure: ' . $me->getMessage(), Minz_Log::WARNING); + Minz_Log::warning('Nonce failure: ' . $me->getMessage()); } + } else { + Minz_Log::notice('Nonce failure due to invalid username!'); } $this->view->nonce = ''; //Failure $this->view->salt1 = ''; diff --git a/app/Controllers/statsController.php b/app/Controllers/statsController.php index 3069be34d..4a597ae7d 100644 --- a/app/Controllers/statsController.php +++ b/app/Controllers/statsController.php @@ -6,6 +6,19 @@ class FreshRSS_stats_Controller extends Minz_ActionController { /** + * This action is called before every other action in that class. It is + * the common boiler plate for every action. It is triggered by the + * underlying framework. + */ + public function firstAction() { + if (!FreshRSS_Auth::hasAccess()) { + Minz_Error::error(403); + } + + Minz_View::prependTitle(_t('admin.stats.title') . ' · '); + } + + /** * This action handles the statistic main page. * * It displays the statistic main page. @@ -99,11 +112,12 @@ class FreshRSS_stats_Controller extends Minz_ActionController { $categoryDAO = new FreshRSS_CategoryDAO(); $feedDAO = FreshRSS_Factory::createFeedDao(); Minz_View::appendScript(Minz_Url::display('/scripts/flotr2.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/flotr2.min.js'))); - $id = Minz_Request::param ('id', null); + $id = Minz_Request::param('id', null); $this->view->categories = $categoryDAO->listCategories(); $this->view->feed = $feedDAO->searchById($id); $this->view->days = $statsDAO->getDays(); $this->view->months = $statsDAO->getMonths(); + $this->view->repartition = $statsDAO->calculateEntryRepartitionPerFeed($id); $this->view->repartitionHour = $statsDAO->calculateEntryRepartitionPerFeedPerHour($id); $this->view->averageHour = $statsDAO->calculateEntryAveragePerFeedPerHour($id); $this->view->repartitionDayOfWeek = $statsDAO->calculateEntryRepartitionPerFeedPerDayOfWeek($id); @@ -111,20 +125,4 @@ class FreshRSS_stats_Controller extends Minz_ActionController { $this->view->repartitionMonth = $statsDAO->calculateEntryRepartitionPerFeedPerMonth($id); $this->view->averageMonth = $statsDAO->calculateEntryAveragePerFeedPerMonth($id); } - - /** - * This action is called before every other action in that class. It is - * the common boiler plate for every action. It is triggered by the - * underlying framework. - */ - public function firstAction() { - if (!$this->view->loginOk) { - Minz_Error::error( - 403, array('error' => array(Minz_Translate::t('access_denied'))) - ); - } - - Minz_View::prependTitle(Minz_Translate::t('stats') . ' · '); - } - } diff --git a/app/Controllers/subscriptionController.php b/app/Controllers/subscriptionController.php new file mode 100644 index 000000000..333565faf --- /dev/null +++ b/app/Controllers/subscriptionController.php @@ -0,0 +1,116 @@ +<?php + +/** + * Controller to handle subscription actions. + */ +class FreshRSS_subscription_Controller extends Minz_ActionController { + /** + * This action is called before every other action in that class. It is + * the common boiler plate for every action. It is triggered by the + * underlying framework. + */ + public function firstAction() { + if (!FreshRSS_Auth::hasAccess()) { + Minz_Error::error(403); + } + + $catDAO = new FreshRSS_CategoryDAO(); + + $catDAO->checkDefault(); + $this->view->categories = $catDAO->listCategories(false); + $this->view->default_category = $catDAO->getDefault(); + } + + /** + * This action handles the main subscription page + * + * It displays categories and associated feeds. + */ + public function indexAction() { + Minz_View::appendScript(Minz_Url::display('/scripts/category.js?' . + @filemtime(PUBLIC_PATH . '/scripts/category.js'))); + Minz_View::prependTitle(_t('sub.title') . ' · '); + + $id = Minz_Request::param('id'); + if ($id !== false) { + $feedDAO = FreshRSS_Factory::createFeedDao(); + $this->view->feed = $feedDAO->searchById($id); + } + } + + /** + * This action handles the feed configuration page. + * + * It displays the feed configuration page. + * If this action is reached through a POST request, it stores all new + * configuraiton values then sends a notification to the user. + * + * The options available on the page are: + * - name + * - description + * - website URL + * - feed URL + * - category id (default: default category id) + * - CSS path to article on website + * - display in main stream (default: 0) + * - HTTP authentication + * - number of article to retain (default: -2) + * - refresh frequency (default: -2) + * Default values are empty strings unless specified. + */ + public function feedAction() { + if (Minz_Request::param('ajax')) { + $this->view->_useLayout(false); + } + + $feedDAO = FreshRSS_Factory::createFeedDao(); + $this->view->feeds = $feedDAO->listFeeds(); + + $id = Minz_Request::param('id'); + if ($id === false || !isset($this->view->feeds[$id])) { + Minz_Error::error(404); + return; + } + + $this->view->feed = $this->view->feeds[$id]; + + Minz_View::prependTitle(_t('sub.title.feed_management') . ' · ' . $this->view->feed->name() . ' · '); + + if (Minz_Request::isPost()) { + $user = Minz_Request::param('http_user', ''); + $pass = Minz_Request::param('http_pass', ''); + + $httpAuth = ''; + if ($user != '' || $pass != '') { + $httpAuth = $user . ':' . $pass; + } + + $cat = intval(Minz_Request::param('category', 0)); + + $values = array( + 'name' => Minz_Request::param('name', ''), + 'description' => sanitizeHTML(Minz_Request::param('description', '', true)), + 'website' => Minz_Request::param('website', ''), + 'url' => Minz_Request::param('url', ''), + 'category' => $cat, + 'pathEntries' => Minz_Request::param('path_entries', ''), + 'priority' => intval(Minz_Request::param('priority', 0)), + 'httpAuth' => $httpAuth, + 'keep_history' => intval(Minz_Request::param('keep_history', -2)), + 'ttl' => intval(Minz_Request::param('ttl', -2)), + ); + + invalidateHttpCache(); + + $url_redirect = array('c' => 'subscription', 'params' => array('id' => $id)); + if ($feedDAO->updateFeed($id, $values) !== false) { + $this->view->feed->_category($cat); + $this->view->feed->faviconPrepare(); + + Minz_Request::good(_t('feedback.sub.feed.updated'), $url_redirect); + } else { + Minz_Request::bad(_t('feedback.sub.feed.error'), $url_redirect); + } + } + } +} diff --git a/app/Controllers/updateController.php b/app/Controllers/updateController.php index da5bddc65..4797a3486 100644 --- a/app/Controllers/updateController.php +++ b/app/Controllers/updateController.php @@ -1,42 +1,42 @@ <?php class FreshRSS_update_Controller extends Minz_ActionController { + public function firstAction() { - $current_user = Minz_Session::param('currentUser', ''); - if (!$this->view->loginOk && Minz_Configuration::isAdmin($current_user)) { - Minz_Error::error( - 403, - array('error' => array(_t('access_denied'))) - ); + if (!FreshRSS_Auth::hasAccess('admin')) { + Minz_Error::error(403); } invalidateHttpCache(); - Minz_View::prependTitle(_t('update_system') . ' · '); $this->view->update_to_apply = false; $this->view->last_update_time = 'unknown'; - $this->view->check_last_hour = false; - $timestamp = (int)@file_get_contents(DATA_PATH . '/last_update.txt'); - if (is_numeric($timestamp) && $timestamp > 0) { + $timestamp = @filemtime(join_path(DATA_PATH, 'last_update.txt')); + if ($timestamp !== false) { $this->view->last_update_time = timestamptodate($timestamp); - $this->view->check_last_hour = (time() - 3600) <= $timestamp; } } public function indexAction() { + Minz_View::prependTitle(_t('admin.update.title') . ' · '); + if (file_exists(UPDATE_FILENAME) && !is_writable(FRESHRSS_PATH)) { $this->view->message = array( 'status' => 'bad', - 'title' => _t('damn'), - 'body' => _t('file_is_nok', FRESHRSS_PATH) + 'title' => _t('gen.short.damn'), + 'body' => _t('feedback.update.file_is_nok', FRESHRSS_PATH) ); } elseif (file_exists(UPDATE_FILENAME)) { // There is an update file to apply! + $version = @file_get_contents(join_path(DATA_PATH, 'last_update.txt')); + if (empty($version)) { + $version = 'unknown'; + } $this->view->update_to_apply = true; $this->view->message = array( 'status' => 'good', - 'title' => _t('ok'), - 'body' => _t('update_can_apply') + 'title' => _t('gen.short.ok'), + 'body' => _t('feedback.update.can_apply', $version) ); } } @@ -44,11 +44,11 @@ class FreshRSS_update_Controller extends Minz_ActionController { public function checkAction() { $this->view->change_view('update', 'index'); - if (file_exists(UPDATE_FILENAME) || $this->view->check_last_hour) { + if (file_exists(UPDATE_FILENAME)) { // There is already an update file to apply: we don't need to check // the webserver! // Or if already check during the last hour, do nothing. - Minz_Request::forward(array('c' => 'update')); + Minz_Request::forward(array('c' => 'update'), true); return; } @@ -69,8 +69,8 @@ class FreshRSS_update_Controller extends Minz_ActionController { $this->view->message = array( 'status' => 'bad', - 'title' => _t('damn'), - 'body' => _t('update_server_not_found', FRESHRSS_UPDATE_WEBSITE) + 'title' => _t('gen.short.damn'), + 'body' => _t('feedback.update.server_not_found', FRESHRSS_UPDATE_WEBSITE) ); return; } @@ -80,23 +80,27 @@ class FreshRSS_update_Controller extends Minz_ActionController { if (strpos($status, 'UPDATE') !== 0) { $this->view->message = array( 'status' => 'bad', - 'title' => _t('damn'), - 'body' => _t('no_update') + 'title' => _t('gen.short.damn'), + 'body' => _t('feedback.update.none') ); - @file_put_contents(DATA_PATH . '/last_update.txt', time()); + @touch(join_path(DATA_PATH, 'last_update.txt')); return; } $script = $res_array[1]; if (file_put_contents(UPDATE_FILENAME, $script) !== false) { - Minz_Request::forward(array('c' => 'update')); + $version = explode(' ', $status, 2); + $version = $version[1]; + @file_put_contents(join_path(DATA_PATH, 'last_update.txt'), $version); + + Minz_Request::forward(array('c' => 'update'), true); } else { $this->view->message = array( 'status' => 'bad', - 'title' => _t('damn'), - 'body' => _t('update_problem', 'Cannot save the update script') + 'title' => _t('gen.short.damn'), + 'body' => _t('feedback.update.error', 'Cannot save the update script') ); } } @@ -108,6 +112,21 @@ class FreshRSS_update_Controller extends Minz_ActionController { require(UPDATE_FILENAME); + if (Minz_Request::param('post_conf', false)) { + $res = do_post_update(); + + Minz_ExtensionManager::callHook('post_update'); + + if ($res === true) { + @unlink(UPDATE_FILENAME); + @file_put_contents(join_path(DATA_PATH, 'last_update.txt'), ''); + Minz_Request::good(_t('feedback.update.finished')); + } else { + Minz_Request::bad(_t('feedback.update.error', $res), + array('c' => 'update', 'a' => 'index')); + } + } + if (Minz_Request::isPost()) { save_info_update(); } @@ -116,14 +135,26 @@ class FreshRSS_update_Controller extends Minz_ActionController { $res = apply_update(); if ($res === true) { - @unlink(UPDATE_FILENAME); - @file_put_contents(DATA_PATH . '/last_update.txt', time()); - - Minz_Request::good(_t('update_finished')); + Minz_Request::forward(array( + 'c' => 'update', + 'a' => 'apply', + 'params' => array('post_conf' => true) + ), true); } else { - Minz_Request::bad(_t('update_problem', $res), + Minz_Request::bad(_t('feedback.update.error', $res), array('c' => 'update', 'a' => 'index')); } } } + + /** + * This action displays information about installation. + */ + public function checkInstallAction() { + Minz_View::prependTitle(_t('admin.check_install.title') . ' · '); + + $this->view->status_php = check_install_php(); + $this->view->status_files = check_install_files(); + $this->view->status_database = check_install_database(); + } } diff --git a/app/Controllers/usersController.php b/app/Controllers/userController.php index a9e6c32bc..ed01b83c5 100644 --- a/app/Controllers/usersController.php +++ b/app/Controllers/userController.php @@ -1,19 +1,30 @@ <?php -class FreshRSS_users_Controller extends Minz_ActionController { - - const BCRYPT_COST = 9; //Will also have to be computed client side on mobile devices, so do not use a too high cost - +/** + * Controller to handle user actions. + */ +class FreshRSS_user_Controller extends Minz_ActionController { + // Will also have to be computed client side on mobile devices, + // so do not use a too high cost + const BCRYPT_COST = 9; + + /** + * This action is called before every other action in that class. It is + * the common boiler plate for every action. It is triggered by the + * underlying framework. + */ public function firstAction() { - if (!$this->view->loginOk) { - Minz_Error::error( - 403, - array('error' => array(Minz_Translate::t('access_denied'))) - ); + if (!FreshRSS_Auth::hasAccess()) { + Minz_Error::error(403); } } - public function authAction() { + /** + * This action displays the user profile page. + */ + public function profileAction() { + Minz_View::prependTitle(_t('conf.profile.title') . ' · '); + if (Minz_Request::isPost()) { $ok = true; @@ -28,9 +39,9 @@ class FreshRSS_users_Controller extends Minz_ActionController { $passwordPlain = ''; $passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js $ok &= ($passwordHash != ''); - $this->view->conf->_passwordHash($passwordHash); + FreshRSS_Context::$user_conf->passwordHash = $passwordHash; } - Minz_Session::_param('passwordHash', $this->view->conf->passwordHash); + Minz_Session::_param('passwordHash', FreshRSS_Context::$user_conf->passwordHash); $passwordPlain = Minz_Request::param('apiPasswordPlain', '', true); if ($passwordPlain != '') { @@ -41,16 +52,17 @@ class FreshRSS_users_Controller extends Minz_ActionController { $passwordPlain = ''; $passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js $ok &= ($passwordHash != ''); - $this->view->conf->_apiPasswordHash($passwordHash); + FreshRSS_Context::$user_conf->apiPasswordHash = $passwordHash; } - if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) { - $this->view->conf->_mail_login(Minz_Request::param('mail_login', '', true)); + // TODO: why do we need of hasAccess here? + if (FreshRSS_Auth::hasAccess('admin')) { + FreshRSS_Context::$user_conf->mail_login = Minz_Request::param('mail_login', '', true); } - $email = $this->view->conf->mail_login; + $email = FreshRSS_Context::$user_conf->mail_login; Minz_Session::_param('mail', $email); - $ok &= $this->view->conf->save(); + $ok &= FreshRSS_Context::$user_conf->save(); if ($email != '') { $personaFile = DATA_PATH . '/persona/' . $email . '.txt'; @@ -58,68 +70,63 @@ class FreshRSS_users_Controller extends Minz_ActionController { $ok &= (file_put_contents($personaFile, Minz_Session::param('currentUser', '_')) !== false); } - if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) { - $current_token = $this->view->conf->token; - $token = Minz_Request::param('token', $current_token); - $this->view->conf->_token($token); - $ok &= $this->view->conf->save(); - - $anon = Minz_Request::param('anon_access', false); - $anon = ((bool)$anon) && ($anon !== 'no'); - $anon_refresh = Minz_Request::param('anon_refresh', false); - $anon_refresh = ((bool)$anon_refresh) && ($anon_refresh !== 'no'); - $auth_type = Minz_Request::param('auth_type', 'none'); - $unsafe_autologin = Minz_Request::param('unsafe_autologin', false); - $api_enabled = Minz_Request::param('api_enabled', false); - if ($anon != Minz_Configuration::allowAnonymous() || - $auth_type != Minz_Configuration::authType() || - $anon_refresh != Minz_Configuration::allowAnonymousRefresh() || - $unsafe_autologin != Minz_Configuration::unsafeAutologinEnabled() || - $api_enabled != Minz_Configuration::apiEnabled()) { - - Minz_Configuration::_authType($auth_type); - Minz_Configuration::_allowAnonymous($anon); - Minz_Configuration::_allowAnonymousRefresh($anon_refresh); - Minz_Configuration::_enableAutologin($unsafe_autologin); - Minz_Configuration::_enableApi($api_enabled); - $ok &= Minz_Configuration::writeFile(); - } + if ($ok) { + Minz_Request::good(_t('feedback.profile.updated'), + array('c' => 'user', 'a' => 'profile')); + } else { + Minz_Request::bad(_t('feedback.profile.error'), + array('c' => 'user', 'a' => 'profile')); } + } + } - invalidateHttpCache(); + /** + * This action displays the user management page. + */ + public function manageAction() { + if (!FreshRSS_Auth::hasAccess('admin')) { + Minz_Error::error(403); + } - $notif = array( - 'type' => $ok ? 'good' : 'bad', - 'content' => Minz_Translate::t($ok ? 'configuration_updated' : 'error_occurred') - ); - Minz_Session::_param('notification', $notif); + Minz_View::prependTitle(_t('admin.user.title') . ' · '); + + // Get the correct current user. + $username = Minz_Request::param('u', Minz_Session::param('currentUser')); + if (!FreshRSS_UserDAO::exist($username)) { + $username = Minz_Session::param('currentUser'); } - Minz_Request::forward(array('c' => 'configure', 'a' => 'users'), true); + $this->view->current_user = $username; + + // Get information about the current user. + $entryDAO = FreshRSS_Factory::createEntryDao($this->view->current_user); + $this->view->nb_articles = $entryDAO->count(); + $this->view->size_user = $entryDAO->size(); } public function createAction() { - if (Minz_Request::isPost() && Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) { - $db = Minz_Configuration::dataBase(); + if (Minz_Request::isPost() && FreshRSS_Auth::hasAccess('admin')) { + $db = FreshRSS_Context::$system_conf->db; require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php'); - $new_user_language = Minz_Request::param('new_user_language', $this->view->conf->language); - if (!in_array($new_user_language, $this->view->conf->availableLanguages())) { - $new_user_language = $this->view->conf->language; + $new_user_language = Minz_Request::param('new_user_language', FreshRSS_Context::$user_conf->language); + $languages = Minz_Translate::availableLanguages(); + if (!isset($languages[$new_user_language])) { + $new_user_language = FreshRSS_Context::$user_conf->language; } $new_user_name = Minz_Request::param('new_user_name'); $ok = ($new_user_name != '') && ctype_alnum($new_user_name); if ($ok) { - $ok &= (strcasecmp($new_user_name, Minz_Configuration::defaultUser()) !== 0); //It is forbidden to alter the default user + $default_user = FreshRSS_Context::$system_conf->default_user; + $ok &= (strcasecmp($new_user_name, $default_user) !== 0); //It is forbidden to alter the default user $ok &= !in_array(strtoupper($new_user_name), array_map('strtoupper', listUsers())); //Not an existing user, case-insensitive - $configPath = DATA_PATH . '/' . $new_user_name . '_user.php'; + $configPath = join_path(DATA_PATH, 'users', $new_user_name, 'config.php'); $ok &= !file_exists($configPath); } if ($ok) { - $passwordPlain = Minz_Request::param('new_user_passwordPlain', '', true); $passwordHash = ''; if ($passwordPlain != '') { @@ -141,12 +148,13 @@ class FreshRSS_users_Controller extends Minz_ActionController { if (empty($new_user_email)) { $new_user_email = ''; } else { - $personaFile = DATA_PATH . '/persona/' . $new_user_email . '.txt'; + $personaFile = join_path(DATA_PATH, 'persona', $new_user_email . '.txt'); @unlink($personaFile); $ok &= (file_put_contents($personaFile, $new_user_name) !== false); } } if ($ok) { + mkdir(join_path(DATA_PATH, 'users', $new_user_name)); $config_array = array( 'language' => $new_user_language, 'passwordHash' => $passwordHash, @@ -162,42 +170,45 @@ class FreshRSS_users_Controller extends Minz_ActionController { $notif = array( 'type' => $ok ? 'good' : 'bad', - 'content' => Minz_Translate::t($ok ? 'user_created' : 'error_occurred', $new_user_name) + 'content' => _t('feedback.user.created' . (!$ok ? '.error' : ''), $new_user_name) ); Minz_Session::_param('notification', $notif); } - Minz_Request::forward(array('c' => 'configure', 'a' => 'users'), true); + + Minz_Request::forward(array('c' => 'user', 'a' => 'manage'), true); } public function deleteAction() { - if (Minz_Request::isPost() && Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) { - $db = Minz_Configuration::dataBase(); + if (Minz_Request::isPost() && FreshRSS_Auth::hasAccess('admin')) { + $db = FreshRSS_Context::$system_conf->db; require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php'); $username = Minz_Request::param('username'); $ok = ctype_alnum($username); + $user_data = join_path(DATA_PATH, 'users', $username); if ($ok) { - $ok &= (strcasecmp($username, Minz_Configuration::defaultUser()) !== 0); //It is forbidden to delete the default user + $default_user = FreshRSS_Context::$system_conf->default_user; + $ok &= (strcasecmp($username, $default_user) !== 0); //It is forbidden to delete the default user } if ($ok) { - $configPath = DATA_PATH . '/' . $username . '_user.php'; - $ok &= file_exists($configPath); + $ok &= is_dir($user_data); } if ($ok) { $userDAO = new FreshRSS_UserDAO(); $ok &= $userDAO->deleteUser($username); - $ok &= unlink($configPath); + $ok &= recursive_unlink($user_data); //TODO: delete Persona file } invalidateHttpCache(); $notif = array( 'type' => $ok ? 'good' : 'bad', - 'content' => Minz_Translate::t($ok ? 'user_deleted' : 'error_occurred', $username) + 'content' => _t('feedback.user.deleted' . (!$ok ? '.error' : ''), $username) ); Minz_Session::_param('notification', $notif); } - Minz_Request::forward(array('c' => 'configure', 'a' => 'users'), true); + + Minz_Request::forward(array('c' => 'user', 'a' => 'manage'), true); } } |
