aboutsummaryrefslogtreecommitdiff
path: root/app/Controllers
diff options
context:
space:
mode:
authorGravatar Clément <clement@selfhost.fr> 2017-02-15 14:14:03 +0100
committerGravatar Clément <clement@selfhost.fr> 2017-02-15 14:14:03 +0100
commit5a1bb1393b4496eb35a2ffb3cc63d41c9dc1e2e5 (patch)
tree67028e45792c575c25c92616633f64cc7a4a13eb /app/Controllers
parent7e949d50320317b5c3b5a2da2bdaf324e794b2f7 (diff)
parent5f637bd816b7323885bfe1751a1724ee59a822f6 (diff)
Merge remote-tracking branch 'FreshRSS/master'
Diffstat (limited to 'app/Controllers')
-rw-r--r--app/Controllers/authController.php212
-rw-r--r--app/Controllers/categoryController.php193
-rwxr-xr-xapp/Controllers/configureController.php563
-rwxr-xr-xapp/Controllers/entryController.php259
-rw-r--r--app/Controllers/errorController.php45
-rw-r--r--app/Controllers/extensionController.php215
-rwxr-xr-xapp/Controllers/feedController.php860
-rw-r--r--app/Controllers/importExportController.php716
-rwxr-xr-xapp/Controllers/indexController.php545
-rwxr-xr-xapp/Controllers/javascriptController.php30
-rw-r--r--app/Controllers/statsController.php150
-rw-r--r--app/Controllers/subscriptionController.php116
-rw-r--r--app/Controllers/updateController.php226
-rw-r--r--app/Controllers/userController.php257
-rw-r--r--app/Controllers/usersController.php183
15 files changed, 3285 insertions, 1285 deletions
diff --git a/app/Controllers/authController.php b/app/Controllers/authController.php
new file mode 100644
index 000000000..1398e4e49
--- /dev/null
+++ b/app/Controllers/authController.php
@@ -0,0 +1,212 @@
+<?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 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 '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));
+
+ $conf = Minz_Configuration::get('system');
+ $limits = $conf->limits;
+ $this->view->cookie_days = round($limits['cookie_duration'] / 86400, 1);
+
+ 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_Error::error(403, array(_t('feedback.auth.login.invalid')), false);
+ return;
+ }
+
+ $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_Error::error(403, array(_t('feedback.auth.login.invalid')), false);
+ }
+ } 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_Error::error(403, array(_t('feedback.auth.login.invalid')), false);
+ }
+ }
+ }
+
+ /**
+ * 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 gives possibility to a user to create an account.
+ */
+ public function registerAction() {
+ if (max_registrations_reached()) {
+ Minz_Error::error(403);
+ }
+
+ Minz_View::prependTitle(_t('gen.auth.registration.title') . ' · ');
+ }
+}
diff --git a/app/Controllers/categoryController.php b/app/Controllers/categoryController.php
new file mode 100644
index 000000000..922f92844
--- /dev/null
+++ b/app/Controllers/categoryController.php
@@ -0,0 +1,193 @@
+<?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();
+ $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 === FreshRSS_CategoryDAO::defaultCategoryId) {
+ Minz_Request::bad(_t('feedback.sub.category.not_delete_default'), $url_redirect);
+ }
+
+ if ($feedDAO->changeCategory($id, FreshRSS_CategoryDAO::defaultCategoryId) === 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 ad8bc546a..e73f106a6 100755
--- a/app/Controllers/configureController.php
+++ b/app/Controllers/configureController.php
@@ -1,354 +1,331 @@
<?php
+/**
+ * Controller to handle every configuration options.
+ */
class FreshRSS_configure_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);
}
-
- $catDAO = new FreshRSS_CategoryDAO ();
- $catDAO->checkDefault ();
}
- public function categorizeAction () {
- $feedDAO = new FreshRSS_FeedDAO ();
- $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]);
- }
- }
-
- if ($newCat != '') {
- $cat = new FreshRSS_Category ($newCat);
- $values = array (
- 'id' => $cat->id (),
- 'name' => $cat->name (),
- );
-
- if ($catDAO->searchByName ($newCat) == false) {
- $catDAO->addCategory ($values);
- }
- }
+ /**
+ * This action handles the display configuration page.
+ *
+ * It displays the display configuration page.
+ * If this action is reached through a POST request, it stores all new
+ * configuration values then sends a notification to the user.
+ *
+ * The options available on the page are:
+ * - language (default: en)
+ * - theme (default: Origin)
+ * - content width (default: thin)
+ * - display of read action in header
+ * - display of favorite action in header
+ * - display of date in header
+ * - display of open action in header
+ * - display of read action in footer
+ * - display of favorite action in footer
+ * - display of sharing action in footer
+ * - display of tags in footer
+ * - display of date in footer
+ * - display of open action in footer
+ * - html5 notification timeout (default: 0)
+ * Default values are false unless specified.
+ */
+ public function displayAction() {
+ if (Minz_Request::isPost()) {
+ 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();
- $notif = array (
- 'type' => 'good',
- 'content' => Minz_Translate::t ('categories_updated')
- );
- Minz_Session::_param ('notification', $notif);
-
- Minz_Request::forward (array ('c' => 'configure', 'a' => 'categorize'), true);
+ Minz_Request::good(_t('feedback.conf.updated'),
+ array('c' => 'configure', 'a' => 'display'));
}
- $this->view->categories = $catDAO->listCategories (false);
- $this->view->defaultCategory = $catDAO->getDefault ();
- $this->view->feeds = $feedDAO->listFeeds ();
- $this->view->flux = false;
+ $this->view->themes = FreshRSS_Themes::get();
- Minz_View::prependTitle (Minz_Translate::t ('categories_management') . ' · ');
+ Minz_View::prependTitle(_t('conf.display.title') . ' · ');
}
- public function feedAction () {
- $catDAO = new FreshRSS_CategoryDAO ();
- $this->view->categories = $catDAO->listCategories (false);
-
- $feedDAO = new FreshRSS_FeedDAO ();
- $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 (Minz_Translate::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)),
- );
-
- if ($feedDAO->updateFeed ($id, $values)) {
- $this->view->flux->_category ($cat);
- $this->view->flux->faviconPrepare();
- $notif = array (
- 'type' => 'good',
- 'content' => Minz_Translate::t ('feed_updated')
- );
- } else {
- $notif = array (
- 'type' => 'bad',
- 'content' => Minz_Translate::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 (Minz_Translate::t ('rss_feed_management') . ' — ' . $this->view->flux->name () . ' · ');
- }
- } else {
- Minz_View::prependTitle (Minz_Translate::t ('rss_feed_management') . ' · ');
- }
- }
-
- public function displayAction () {
+ /**
+ * This action handles the reading configuration page.
+ *
+ * It displays the reading configuration page.
+ * If this action is reached through a POST request, it stores all new
+ * configuration values then sends a notification to the user.
+ *
+ * The options available on the page are:
+ * - number of posts per page (default: 10)
+ * - view mode (default: normal)
+ * - default article view (default: all)
+ * - load automatically articles
+ * - display expanded articles
+ * - display expanded categories
+ * - hide categories and feeds without unread articles
+ * - jump on next category or feed when marked as read
+ * - 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
+ * - opened on site
+ * - scrolled
+ * - received
+ * Default values are false unless specified.
+ */
+ public function readingAction() {
if (Minz_Request::isPost()) {
- $this->view->conf->_language(Minz_Request::param('language', 'en'));
- $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 (Minz_Request::param('default_view', 'a'));
- $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->_onread_jump_next(Minz_Request::param('onread_jump_next', false));
- $this->view->conf->_lazyload (Minz_Request::param('lazyload', 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->mark_updated_article_unread = Minz_Request::param('mark_updated_article_unread', 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),
- ));
- $themeId = Minz_Request::param('theme', '');
- if ($themeId == '') {
- $themeId = FreshRSS_Themes::defaultTheme;
- }
- $this->view->conf->_theme($themeId);
- $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->save();
-
- Minz_Session::_param ('language', $this->view->conf->language);
- Minz_Translate::reset ();
- invalidateHttpCache();
-
- $notif = array (
- 'type' => 'good',
- 'content' => Minz_Translate::t ('configuration_updated')
);
- Minz_Session::_param ('notification', $notif);
+ FreshRSS_Context::$user_conf->save();
+ invalidateHttpCache();
- Minz_Request::forward (array ('c' => 'configure', 'a' => 'display'), true);
+ Minz_Request::good(_t('feedback.conf.updated'),
+ array('c' => 'configure', 'a' => 'reading'));
}
- $this->view->themes = FreshRSS_Themes::get();
-
- Minz_View::prependTitle (Minz_Translate::t ('reading_configuration') . ' · ');
+ Minz_View::prependTitle(_t('conf.reading.title') . ' · ');
}
- public function sharingAction () {
- if (Minz_Request::isPost ()) {
- $this->view->conf->_sharing (array(
- 'shaarli' => Minz_Request::param ('shaarli', false),
- 'wallabag' => Minz_Request::param ('wallabag', false),
- 'diaspora' => Minz_Request::param ('diaspora', false),
- 'twitter' => Minz_Request::param ('twitter', false),
- 'g+' => Minz_Request::param ('g+', false),
- 'facebook' => Minz_Request::param ('facebook', false),
- 'email' => Minz_Request::param ('email', false),
- 'print' => Minz_Request::param ('print', false),
- ));
- $this->view->conf->save();
+ /**
+ * This action handles the sharing configuration page.
+ *
+ * It displays the sharing configuration page.
+ * If this action is reached through a POST request, it stores all
+ * configuration values then sends a notification to the user.
+ */
+ public function sharingAction() {
+ if (Minz_Request::isPost()) {
+ $params = Minz_Request::fetchPOST();
+ FreshRSS_Context::$user_conf->sharing = $params['share'];
+ FreshRSS_Context::$user_conf->save();
invalidateHttpCache();
- $notif = array (
- 'type' => 'good',
- 'content' => Minz_Translate::t ('configuration_updated')
- );
- Minz_Session::_param ('notification', $notif);
-
- Minz_Request::forward (array ('c' => 'configure', 'a' => 'sharing'), true);
+ Minz_Request::good(_t('feedback.conf.updated'),
+ array('c' => 'configure', 'a' => 'sharing'));
}
- Minz_View::prependTitle (Minz_Translate::t ('sharing') . ' · ');
+ Minz_View::prependTitle(_t('conf.sharing.title') . ' · ');
}
- public function importExportAction () {
- require_once(LIB_PATH . '/lib_opml.php');
- $catDAO = new FreshRSS_CategoryDAO ();
- $this->view->categories = $catDAO->listCategories ();
+ /**
+ * This action handles the shortcut configuration page.
+ *
+ * It displays the shortcut configuration page.
+ * If this action is reached through a POST request, it stores all new
+ * configuration values then sends a notification to the user.
+ *
+ * The authorized values for shortcuts are letters (a to z), numbers (0
+ * to 9), function keys (f1 to f12), backspace, delete, down, end, enter,
+ * escape, home, insert, left, page down, page up, return, right, space,
+ * tab and up.
+ */
+ 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');
+ $this->view->list_keys = $list_keys;
- $this->view->req = Minz_Request::param ('q');
+ if (Minz_Request::isPost()) {
+ $shortcuts = Minz_Request::param('shortcuts');
+ $shortcuts_ok = array();
+
+ foreach ($shortcuts as $key => $value) {
+ if (in_array($value, $list_keys)) {
+ $shortcuts_ok[$key] = $value;
+ }
+ }
- if ($this->view->req == 'export') {
- Minz_View::_title ('freshrss_feeds.opml');
+ FreshRSS_Context::$user_conf->shortcuts = $shortcuts_ok;
+ FreshRSS_Context::$user_conf->save();
+ invalidateHttpCache();
- $this->view->_useLayout (false);
- header('Content-Type: application/xml; charset=utf-8');
- header('Content-disposition: attachment; filename=freshrss_feeds.opml');
+ Minz_Request::good(_t('feedback.conf.shortcuts_updated'),
+ array('c' => 'configure', 'a' => 'shortcut'));
+ }
- $feedDAO = new FreshRSS_FeedDAO ();
- $catDAO = new FreshRSS_CategoryDAO ();
+ Minz_View::prependTitle(_t('conf.shortcut.title') . ' · ');
+ }
- $list = array ();
- foreach ($catDAO->listCategories () as $key => $cat) {
- $list[$key]['name'] = $cat->name ();
- $list[$key]['feeds'] = $feedDAO->listByCategory ($cat->id ());
- }
+ /**
+ * This action handles the archive configuration page.
+ *
+ * It displays the archive configuration page.
+ * If this action is reached through a POST request, it stores all new
+ * configuration values then sends a notification to the user.
+ *
+ * The options available on that page are:
+ * - duration to retain old article (default: 3)
+ * - number of article to retain per feed (default: 0)
+ * - refresh frequency (default: -2)
+ *
+ * @todo explain why the default value is -2 but this value does not
+ * exist in the drop-down list
+ */
+ public function archivingAction() {
+ if (Minz_Request::isPost()) {
+ 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();
- $this->view->categories = $list;
- } elseif ($this->view->req == 'import' && Minz_Request::isPost ()) {
- if ($_FILES['file']['error'] == 0) {
- invalidateHttpCache();
- // on parse le fichier OPML pour récupérer les catégories et les flux associés
- try {
- list ($categories, $feeds) = opml_import (
- file_get_contents ($_FILES['file']['tmp_name'])
- );
-
- // On redirige vers le controller feed qui va se charger d'insérer les flux en BDD
- // les flux sont mis au préalable dans des variables de Request
- Minz_Request::_param ('q', 'null');
- Minz_Request::_param ('categories', $categories);
- Minz_Request::_param ('feeds', $feeds);
- Minz_Request::forward (array ('c' => 'feed', 'a' => 'massiveImport'));
- } catch (FreshRSS_Opml_Exception $e) {
- Minz_Log::record ($e->getMessage (), Minz_Log::WARNING);
-
- $notif = array (
- 'type' => 'bad',
- 'content' => Minz_Translate::t ('bad_opml_file')
- );
- Minz_Session::_param ('notification', $notif);
-
- Minz_Request::forward (array (
- 'c' => 'configure',
- 'a' => 'importExport'
- ), true);
- }
- }
+ Minz_Request::good(_t('feedback.conf.updated'),
+ array('c' => 'configure', 'a' => 'archiving'));
}
- $feedDAO = new FreshRSS_FeedDAO ();
- $this->view->feeds = $feedDAO->listFeeds ();
+ Minz_View::prependTitle(_t('conf.archiving.title') . ' · ');
- // au niveau de la vue, permet de ne pas voir un flux sélectionné dans la liste
- $this->view->flux = false;
+ $entryDAO = FreshRSS_Factory::createEntryDao();
+ $this->view->nb_total = $entryDAO->count();
+ $this->view->size_user = $entryDAO->size();
- Minz_View::prependTitle (Minz_Translate::t ('import_export_opml') . ' · ');
+ if (FreshRSS_Auth::hasAccess('admin')) {
+ $this->view->size_total = $entryDAO->size(true);
+ }
}
- 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', '0', '1', '2', '3', '4', '5', '6', '7', '8',
- '9', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9',
- 'f10', 'f11', 'f12');
- $this->view->list_keys = $list_keys;
-
- if (Minz_Request::isPost ()) {
- $shortcuts = Minz_Request::param ('shortcuts');
- $shortcuts_ok = array ();
+ /**
+ * This action handles the user queries configuration page.
+ *
+ * If this action is reached through a POST request, it stores all new
+ * configuration values then sends a notification to the user then
+ * redirect to the same page.
+ * If this action is not reached through a POST request, it displays the
+ * configuration page and verifies that every user query is runable by
+ * checking if categories and feeds are still in use.
+ */
+ public function queriesAction() {
+ $category_dao = new FreshRSS_CategoryDAO();
+ $feed_dao = FreshRSS_Factory::createFeedDao();
+ if (Minz_Request::isPost()) {
+ $params = Minz_Request::param('queries', array());
- foreach ($shortcuts as $key => $value) {
- if (in_array($value, $list_keys)) {
- $shortcuts_ok[$key] = $value;
+ foreach ($params as $key => $query) {
+ if (!$query['name']) {
+ $query['name'] = _t('conf.query.number', $key + 1);
}
+ $queries[] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao);
}
+ FreshRSS_Context::$user_conf->queries = $queries;
+ FreshRSS_Context::$user_conf->save();
- $this->view->conf->_shortcuts ($shortcuts_ok);
- $this->view->conf->save();
- invalidateHttpCache();
+ Minz_Request::good(_t('feedback.conf.updated'),
+ array('c' => 'configure', 'a' => 'queries'));
+ } else {
+ $this->view->queries = array();
+ foreach (FreshRSS_Context::$user_conf->queries as $key => $query) {
+ $this->view->queries[$key] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao);
+ }
+ }
- $notif = array (
- 'type' => 'good',
- 'content' => Minz_Translate::t ('shortcuts_updated')
- );
- Minz_Session::_param ('notification', $notif);
+ Minz_View::prependTitle(_t('conf.query.title') . ' · ');
+ }
- Minz_Request::forward (array ('c' => 'configure', 'a' => 'shortcut'), true);
+ /**
+ * This action handles the creation of a user query.
+ *
+ * It gets the GET parameters and stores them in the configuration query
+ * storage. Before it is saved, the unwanted parameters are unset to keep
+ * lean data.
+ */
+ public function addQueryAction() {
+ $category_dao = new FreshRSS_CategoryDAO();
+ $feed_dao = FreshRSS_Factory::createFeedDao();
+ $queries = array();
+ foreach (FreshRSS_Context::$user_conf->queries as $key => $query) {
+ $queries[$key] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao);
}
+ $params = Minz_Request::fetchGET();
+ $params['url'] = Minz_Url::display(array('params' => $params));
+ $params['name'] = _t('conf.query.number', count($queries) + 1);
+ $queries[] = new FreshRSS_UserQuery($params, $feed_dao, $category_dao);
- Minz_View::prependTitle (Minz_Translate::t ('shortcuts') . ' · ');
- }
+ FreshRSS_Context::$user_conf->queries = $queries;
+ FreshRSS_Context::$user_conf->save();
- public function usersAction() {
- Minz_View::prependTitle(Minz_Translate::t ('users') . ' · ');
+ Minz_Request::good(_t('feedback.conf.query_created', $query['name']),
+ array('c' => 'configure', 'a' => 'queries'));
}
- public function archivingAction () {
+ /**
+ * This action handles the system configuration page.
+ *
+ * It displays the system configuration page.
+ * If this action is reach through a POST request, it stores all new
+ * configuration values then sends a notification to the user.
+ *
+ * The options available on the page are:
+ * - user limit (default: 1)
+ * - user category limit (default: 16384)
+ * - user feed limit (default: 16384)
+ */
+ public function systemAction() {
+ if (!FreshRSS_Auth::hasAccess('admin')) {
+ Minz_Error::error(403);
+ }
if (Minz_Request::isPost()) {
- $old = Minz_Request::param('old_entries', 3);
- $keepHistoryDefault = Minz_Request::param('keep_history_default', 0);
+ $limits = FreshRSS_Context::$system_conf->limits;
+ $limits['max_registrations'] = Minz_Request::param('max-registrations', 1);
+ $limits['max_feeds'] = Minz_Request::param('max-feeds', 16384);
+ $limits['max_categories'] = Minz_Request::param('max-categories', 16384);
+ FreshRSS_Context::$system_conf->limits = $limits;
+ FreshRSS_Context::$system_conf->title = Minz_Request::param('instance-name', 'FreshRSS');
+ FreshRSS_Context::$system_conf->auto_update_url = Minz_Request::param('auto-update-url', false);
+ FreshRSS_Context::$system_conf->save();
- $this->view->conf->_old_entries($old);
- $this->view->conf->_keep_history_default($keepHistoryDefault);
- $this->view->conf->save();
invalidateHttpCache();
- $notif = array(
+ Minz_Session::_param('notification', array(
'type' => 'good',
- 'content' => Minz_Translate::t('configuration_updated')
- );
- Minz_Session::_param('notification', $notif);
-
- Minz_Request::forward(array('c' => 'configure', 'a' => 'archiving'), true);
- }
-
- Minz_View::prependTitle(Minz_Translate::t('archiving_configuration') . ' · ');
-
- $entryDAO = new FreshRSS_EntryDAO();
- $this->view->nb_total = $entryDAO->count();
- $this->view->size_user = $entryDAO->size();
-
- if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
- $this->view->size_total = $entryDAO->size(true);
+ 'content' => _t('feedback.conf.updated')
+ ));
}
}
}
diff --git a/app/Controllers/entryController.php b/app/Controllers/entryController.php
index 1756c91e5..c40588105 100755
--- a/app/Controllers/entryController.php
+++ b/app/Controllers/entryController.php
@@ -1,158 +1,203 @@
<?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);
+ // 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 lastAction () {
- $ajax = Minz_Request::param ('ajax');
- if (!$ajax && $this->redirect) {
- Minz_Request::forward (array (
- 'c' => 'index',
- 'a' => 'index',
- 'params' => $this->params
- ), true);
+ /**
+ * 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);
+ FreshRSS_Context::$search = new FreshRSS_Search(Minz_Request::param('search', ''));
+
+ FreshRSS_Context::$state = Minz_Request::param('state', 0);
+ if (FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_FAVORITE)) {
+ FreshRSS_Context::$state = FreshRSS_Entry::STATE_FAVORITE;
+ } elseif (FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_NOT_FAVORITE)) {
+ FreshRSS_Context::$state = FreshRSS_Entry::STATE_NOT_FAVORITE;
} else {
- Minz_Request::_param ('ajax');
+ FreshRSS_Context::$state = 0;
}
- }
- public function readAction () {
- $this->redirect = true;
+ $params = array();
- $id = Minz_Request::param ('id');
- $get = Minz_Request::param ('get');
- $nextGet = Minz_Request::param ('nextGet', $get);
- $idMax = Minz_Request::param ('idMax', 0);
+ $entryDAO = FreshRSS_Factory::createEntryDao();
+ if ($id === false) {
+ // id is false? It MUST be a POST request!
+ if (!Minz_Request::isPost()) {
+ Minz_Request::bad(_t('feedback.access.not_found'), array('c' => 'index', 'a' => 'index'));
+ return;
+ }
- $entryDAO = new FreshRSS_EntryDAO ();
- if ($id == false) {
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, FreshRSS_Context::$search, FreshRSS_Context::$state);
+ break;
+ case 'f':
+ $entryDAO->markReadFeed($get, $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state);
+ break;
+ case 's':
+ $entryDAO->markReadEntries($id_max, true, 0, FreshRSS_Context::$search);
+ break;
+ case 'a':
+ $entryDAO->markReadEntries($id_max, false, 0, FreshRSS_Context::$search, FreshRSS_Context::$state);
+ 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);
+ }
+
+ if (!$this->ajax) {
+ Minz_Request::good(_t('feedback.sub.feed.marked_read'), array(
+ 'c' => 'index',
+ 'a' => 'index',
+ 'params' => $params,
+ ), true);
}
}
- public function bookmarkAction () {
- $this->redirect = true;
+ /**
+ * 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, $is_favourite);
+ }
- $id = Minz_Request::param ('id');
- if ($id) {
- $entryDAO = new FreshRSS_EntryDAO ();
- $entryDAO->markFavorite ($id, (bool)(Minz_Request::param ('is_favorite', true)));
+ 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);
-
- // 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 = new FreshRSS_EntryDAO();
- $entryDAO->optimizeTable();
-
- invalidateHttpCache();
-
- $notif = array (
- 'type' => 'good',
- 'content' => Minz_Translate::t ('optimization_complete')
- );
- Minz_Session::_param ('notification', $notif);
+ $url_redirect = array(
+ 'c' => 'configure',
+ 'a' => 'archiving',
+ );
+
+ if (!Minz_Request::isPost()) {
+ Minz_Request::forward($url_redirect, true);
}
- Minz_Request::forward(array(
- 'c' => 'configure',
- 'a' => 'archiving'
- ), true);
+ @set_time_limit(300);
+
+ $entryDAO = FreshRSS_Factory::createEntryDao();
+ $entryDAO->optimizeTable();
+
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ $feedDAO->updateCachedValues();
+
+ 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 = new FreshRSS_FeedDAO();
- $feeds = $feedDAO->listFeedsOrderUpdate();
- $nbTotal = 0;
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ $feeds = $feedDAO->listFeeds();
+ $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() . ']');
}
}
}
- invalidateHttpCache();
-
- $notif = array(
- 'type' => 'good',
- 'content' => Minz_Translate::t('purge_completed', $nbTotal)
- );
- Minz_Session::_param('notification', $notif);
+ $feedDAO->updateCachedValues();
- Minz_Request::forward(array(
+ invalidateHttpCache();
+ 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 dc9a2ee25..b0bafda72 100644
--- a/app/Controllers/errorController.php
+++ b/app/Controllers/errorController.php
@@ -1,26 +1,53 @@
<?php
+/**
+ * Controller to handle error page.
+ */
class FreshRSS_error_Controller extends Minz_ActionController {
- public function indexAction () {
- switch (Minz_Request::param ('code')) {
+ /**
+ * 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() {
+ $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';
- break;
- case 404:
- $this->view->code = 'Error 404 - Not found';
+ $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');
+ }
+
+ $error_message = trim(implode($error_logs));
+ if ($error_message !== '') {
+ $this->view->errorMessage = $error_message;
}
-
- $this->view->logs = Minz_Request::param ('logs');
-
- Minz_View::prependTitle ($this->view->code . ' · ');
+
+ 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 c718fcd5c..f71f26a4e 100755
--- a/app/Controllers/feedController.php
+++ b/app/Controllers/feedController.php
@@ -1,451 +1,591 @@
<?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 () {
+ public static function addFeed($url, $title = '', $cat_id = 0, $new_cat_name = '', $http_auth = '') {
+ FreshRSS_UserDAO::touch();
@set_time_limit(300);
- if (Minz_Request::isPost ()) {
- $this->catDAO = new FreshRSS_CategoryDAO ();
- $this->catDAO->checkDefault ();
+ $catDAO = new FreshRSS_CategoryDAO();
- $url = Minz_Request::param ('url_rss');
- $cat = Minz_Request::param ('category', false);
- if ($cat === false) {
- $def_cat = $this->catDAO->getDefault ();
- $cat = $def_cat->id ();
+ $cat = null;
+ if ($cat_id > 0) {
+ $cat = $catDAO->searchById($cat_id);
+ }
+ if ($cat == null && $new_cat_name != '') {
+ $cat = $catDAO->addCategory(array('name' => $new_cat_name));
+ }
+ if ($cat == null) {
+ $catDAO->checkDefault();
+ }
+ $cat_id = $cat == null ? FreshRSS_CategoryDAO::defaultCategoryId : $cat->id();
+
+ $feed = new FreshRSS_Feed($url); //Throws FreshRSS_BadUrl_Exception
+ $feed->_httpAuth($http_auth);
+ $feed->load(true); //Throws FreshRSS_Feed_Exception, Minz_FileNotExistException
+ $feed->_category($cat_id);
+
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ if ($feedDAO->searchByUrl($feed->url())) {
+ throw new FreshRSS_AlreadySubscribed_Exception($url, $feed->name());
+ }
+
+ // Call the extension hook
+ $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
+ if ($feed === null) {
+ throw new FreshRSS_FeedNotAdded_Exception($url, $feed->name());
+ }
+
+ $values = array(
+ 'url' => $feed->url(),
+ 'category' => $feed->category(),
+ 'name' => $title != '' ? $title : $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.
+ throw new FreshRSS_FeedNotAdded_Exception($url, $feed->name());
+ }
+ $feed->_id($id);
+
+ // Ok, feed has been added in database. Now we have to refresh entries.
+ self::actualizeFeed($id, $url, false, null, true);
+
+ return $feed;
+ }
+
+ /**
+ * 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' => 'subscription',
+ 'a' => 'index'
+ ), true);
+ }
+
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ $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()) {
+ $cat = Minz_Request::param('category');
+ $new_cat_name = '';
+ if ($cat === 'nc') {
+ // User want to create a new category, new_category parameter
+ // must exist
+ $new_cat = Minz_Request::param('new_category');
+ $new_cat_name = isset($new_cat['name']) ? $new_cat['name'] : '';
}
- $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 = trim(Minz_Request::param('http_user', ''));
+ $pass = Minz_Request::param('http_pass', '');
+ $http_auth = '';
+ if ($user != '' && $pass != '') { //TODO: Sanitize
+ $http_auth = $user . ':' . $pass;
+ }
- $transactionStarted = false;
try {
- $feed = new FreshRSS_Feed ($url);
- $feed->_category ($cat);
-
- $httpAuth = '';
- if ($user != '' || $pass != '') {
- $httpAuth = $user . ':' . $pass;
- }
- $feed->_httpAuth ($httpAuth);
-
- $feed->load(true);
-
- $feedDAO = new FreshRSS_FeedDAO ();
- $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 = new FreshRSS_EntryDAO ();
- $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);
-
- $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);
- }
- $feedDAO->updateLastUpdate ($feed->id ());
- $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 ();
- }
- }
+ $feed = self::addFeed($url, '', $cat, $new_cat_name, $http_auth);
} 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);
+ // Given url was not a valid url!
+ Minz_Log::warning($e->getMessage());
+ Minz_Request::bad(_t('feedback.sub.feed.invalid_url', $url), $url_redirect);
} 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_Session::_param ('notification', $notif);
+ // 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);
} 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_Session::_param ('notification', $notif);
+ // Cache directory doesn't exist!
+ Minz_Log::error($e->getMessage());
+ Minz_Request::bad(_t('feedback.sub.feed.internal_problem', _url('index', 'logs')), $url_redirect);
+ } catch (FreshRSS_AlreadySubscribed_Exception $e) {
+ Minz_Request::bad(_t('feedback.sub.feed.already_subscribed', $e->feedName()), $url_redirect);
+ } catch (FreshRSS_FeedNotAdded_Exception $e) {
+ Minz_Request::bad(_t('feedback.sub.feed.not_added', $e->feedName()), $url_redirect);
}
- if ($transactionStarted) {
- $feedDAO->rollBack ();
+
+ // 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') . ' · ');
+
+ $this->catDAO = new FreshRSS_CategoryDAO();
+ $this->view->categories = $this->catDAO->listCategories(false);
+ $this->view->feed = new FreshRSS_Feed($url);
+ try {
+ // We try to get more information about the feed.
+ $this->view->feed->load(true);
+ $this->view->load_ok = true;
+ } catch (Exception $e) {
+ $this->view->load_ok = false;
}
- Minz_Request::forward (array ('c' => 'configure', 'a' => 'feed', 'params' => $params), true);
+ $feed = $feedDAO->searchByUrl($this->view->feed->url());
+ if ($feed) {
+ // 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 = new FreshRSS_FeedDAO ();
- $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 () {
+ public static function actualizeFeed($feed_id, $feed_url, $force, $simplePiePush = null, $isNewFeed = false) {
@set_time_limit(300);
- $feedDAO = new FreshRSS_FeedDAO ();
- $entryDAO = new FreshRSS_EntryDAO ();
+ $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);
-
- // 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 ();
- if ($id) {
- $feed = $feedDAO->searchById ($id);
+ // Create a list of feeds to actualize.
+ // If feed_id is set and valid, corresponding feed is added to the list but
+ // alone in order to automatize further process.
+ $feeds = array();
+ if ($feed_id > 0 || $feed_url) {
+ $feed = $feed_id > 0 ? $feedDAO->searchById($feed_id) : $feedDAO->searchByUrl($feed_url);
if ($feed) {
- $feeds = array ($feed);
+ $feeds[] = $feed;
}
} else {
- $feeds = $feedDAO->listFeedsOrderUpdate ();
+ $feeds = $feedDAO->listFeedsOrderUpdate(-1);
}
- // 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;
+ // PubSubHubbub support
+ $pubsubhubbubEnabledGeneral = FreshRSS_Context::$system_conf->pubsubhubbub_enabled;
+ $pshbMinAge = time() - (3600 * 24); //TODO: Make a configuration.
+
+ $updated_feeds = 0;
+ $is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0;
foreach ($feeds as $feed) {
+ $url = $feed->url(); //For detection of HTTP 301
+
+ $pubSubHubbubEnabled = $pubsubhubbubEnabledGeneral && $feed->pubSubHubbubEnabled();
+ if ((!$simplePiePush) && (!$feed_id) && $pubSubHubbubEnabled && ($feed->lastUpdate() > $pshbMinAge)) {
+ //$text = 'Skip pull of feed using PubSubHubbub: ' . $url;
+ //Minz_Log::debug($text);
+ //file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND);
+ continue; //When PubSubHubbub is used, do not pull refresh so often
+ }
+
+ $mtime = 0;
+ $ttl = $feed->ttl();
+ if ($ttl == -1) {
+ continue; //Feed refresh is disabled
+ }
+ if ((!$simplePiePush) && (!$feed_id) &&
+ ($feed->lastUpdate() + 10 >= time() - ($ttl == -2 ? FreshRSS_Context::$user_conf->ttl_default : $ttl))) {
+ //Too early to refresh from source, but check whether the feed was updated by another user
+ $mtime = $feed->cacheModifiedTime();
+ if ($feed->lastUpdate() + 10 >= $mtime) {
+ continue; //Nothing newer from other users
+ }
+ //Minz_Log::debug($feed->url() . ' was updated at ' . date('c', $mtime) . ' by another user');
+ //Will take advantage of the newer cache
+ }
+
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();
+ if ($simplePiePush) {
+ $feed->loadEntries($simplePiePush); //Used by PubSubHubbub
+ } else {
+ $feed->load(false, $isNewFeed);
+ }
+ } catch (FreshRSS_Feed_Exception $e) {
+ Minz_Log::warning($e->getMessage());
+ $feedDAO->updateLastUpdate($feed->id(), true);
+ $feed->unlock();
+ continue;
+ }
- $feed->load(false);
- $entries = array_reverse($feed->entries()); //We want chronological order and SimplePie uses reverse order
- $hasTransaction = false;
+ $feed_history = $feed->keepHistory();
+ if ($isNewFeed) {
+ $feed_history = -1; //∞
+ } elseif ($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 (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);
+ // We want chronological order and SimplePie uses reverse order.
+ $entries = array_reverse($feed->entries());
+ if (count($entries) > 0) {
+ $newGuids = array();
+ foreach ($entries as $entry) {
+ $newGuids[] = safe_ascii($entry->guid());
+ }
+ // For this feed, check existing GUIDs already in database.
+ $existingHashForGuids = $entryDAO->listHashForFeedGuids($feed->id(), $newGuids);
+ unset($newGuids);
+
+ $oldGuids = array();
+ // Add entries in database if possible.
+ foreach ($entries as $entry) {
+ $entry_date = $entry->date(true);
+ if (isset($existingHashForGuids[$entry->guid()])) {
+ $existingHash = $existingHashForGuids[$entry->guid()];
+ if (strcasecmp($existingHash, $entry->hash()) === 0 || trim($existingHash, '0') == '') {
+ //This entry already exists and is unchanged. TODO: Remove the test with the zero'ed hash in FreshRSS v1.3
+ $oldGuids[] = $entry->guid();
+ } else { //This entry already exists but has been updated
+ //Minz_Log::debug('Entry with GUID `' . $entry->guid() . '` updated in feed ' . $feed->id() .
+ //', old hash ' . $existingHash . ', new hash ' . $entry->hash());
+ //TODO: Make an updated/is_read policy by feed, in addition to the global one.
+ $entry->_isRead(FreshRSS_Context::$user_conf->mark_updated_article_unread ? false : null); //Change is_read according to policy.
+ if (!$entryDAO->inTransaction()) {
+ $entryDAO->beginTransaction();
+ }
+ $entryDAO->updateEntry($entry->toArray());
+ }
+ } elseif ($feed_history == 0 && $entry_date < $date_min) {
+ // This entry should not be added considering configuration and date.
+ $oldGuids[] = $entry->guid();
+ } else {
+ if ($isNewFeed) {
+ $id = min(time(), $entry_date) . uSecString();
+ } elseif ($entry_date < $date_min) {
+ $id = min(time(), $entry_date) . uSecString();
+ $entry->_isRead(true); //Old article that was not in database. Probably an error, so mark as read
+ } else {
+ $id = uTimeString();
+ $entry->_isRead($is_read);
+ }
+ $entry->_id($id);
- if ($feedHistory == -2) { //default
- $feedHistory = $this->view->conf->keep_history_default;
- }
+ $entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry);
+ if ($entry === null) {
+ // An extension has returned a null value, there is nothing to insert.
+ continue;
+ }
- $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);
+ if ($pubSubHubbubEnabled && !$simplePiePush) { //We use push, but have discovered an article by pull!
+ $text = 'An article was discovered by pull although we use PubSubHubbub!: Feed ' . $url . ' GUID ' . $entry->guid();
+ file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND);
+ Minz_Log::warning($text);
+ $pubSubHubbubEnabled = false;
+ $feed->pubSubHubbubError(true);
}
+
+ if (!$entryDAO->inTransaction()) {
+ $entryDAO->beginTransaction();
+ }
+ $entryDAO->addEntry($entry->toArray());
}
}
+ $entryDAO->updateLastSeen($feed->id(), $oldGuids, $mtime);
+ }
- 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);
- }
+ 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 (!$entryDAO->inTransaction()) {
+ $entryDAO->beginTransaction();
}
- // on indique que le flux vient d'être mis à jour en BDD
- $feedDAO->updateLastUpdate ($feed->id (), 0, $hasTransaction);
- if ($hasTransaction) {
- $feedDAO->commit();
+ $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() . ']');
}
- $flux_update++;
- if ($feed->url() !== $url) { //URL has changed (auto-discovery)
- $feedDAO->updateFeed($feed->id(), array('url' => $feed->url()));
+ }
+
+ $feedDAO->updateLastUpdate($feed->id(), false, $entryDAO->inTransaction(), $mtime);
+ if ($entryDAO->inTransaction()) {
+ $entryDAO->commit();
+ }
+
+ if ($feed->hubUrl() && $feed->selfUrl()) { //selfUrl has priority for PubSubHubbub
+ if ($feed->selfUrl() !== $url) { //https://code.google.com/p/pubsubhubbub/wiki/MovingFeedsOrChangingHubs
+ $selfUrl = checkUrl($feed->selfUrl());
+ if ($selfUrl) {
+ Minz_Log::debug('PubSubHubbub unsubscribe ' . $feed->url());
+ if (!$feed->pubSubHubbubSubscribe(false)) { //Unsubscribe
+ Minz_Log::warning('Error while PubSubHubbub unsubscribing from ' . $feed->url());
+ }
+ $feed->_url($selfUrl, false);
+ Minz_Log::notice('Feed ' . $url . ' canonical address moved to ' . $feed->url());
+ $feedDAO->updateFeed($feed->id(), array('url' => $feed->url()));
+ }
}
- } catch (FreshRSS_Feed_Exception $e) {
- Minz_Log::record ($e->getMessage (), Minz_Log::NOTICE);
- $feedDAO->updateLastUpdate ($feed->id (), 1);
+ }
+ elseif ($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();
+ if ($pubsubhubbubEnabledGeneral && $feed->pubSubHubbubPrepare()) {
+ Minz_Log::notice('PubSubHubbub subscribe ' . $feed->url());
+ if (!$feed->pubSubHubbubSubscribe(true)) { //Subscribe
+ Minz_Log::warning('Error while PubSubHubbub subscribing to ' . $feed->url());
+ }
+ }
$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;
}
}
+ return array($updated_feeds, reset($feeds));
+ }
- $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 (
- 'type' => 'good',
- 'content' => Minz_Translate::t ('n_feeds_actualized', $flux_update)
- );
- } else {
- // aucun flux n'a été mis à jour, oups
- $notif = array (
- 'type' => 'good',
- 'content' => Minz_Translate::t ('no_feed_to_refresh')
- );
- }
+ /**
+ * This action actualizes entries from one or several feeds.
+ *
+ * Parameters are:
+ * - id (default: false): Feed ID
+ * - url (default: false): Feed URL
+ * - force (default: false)
+ * If id and url are 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() {
+ Minz_Session::_param('actualize_feeds', false);
+ $id = Minz_Request::param('id');
+ $url = Minz_Request::param('url');
+ $force = Minz_Request::param('force');
- 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 ());
- }
+ list($updated_feeds, $feed) = self::actualizeFeed($id, $url, $force);
- if (Minz_Request::param ('ajax', 0) === 0) {
- Minz_Session::_param ('notification', $notif);
- Minz_Request::forward ($url, true);
- } 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 (
+ 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 ('feeds_actualized')
+ 'content' => _t('feedback.sub.feed.actualizeds')
);
- Minz_Session::_param ('notification', $notif);
- // et on désactive le layout car ne sert à rien
- $this->view->_useLayout (false);
+ Minz_Session::_param('notification', $notif);
+ // No layout in ajax request.
+ $this->view->_useLayout(false);
+ } else {
+ // Redirect to the main page with correct notification.
+ if ($updated_feeds === 1) {
+ 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 {
+ Minz_Request::good(_t('feedback.sub.feed.no_refresh'), array());
+ }
}
+ return $updated_feeds;
}
- public function massiveImportAction () {
- @set_time_limit(300);
-
- $this->catDAO = new FreshRSS_CategoryDAO ();
- $this->catDAO->checkDefault ();
-
- $entryDAO = new FreshRSS_EntryDAO ();
- $feedDAO = new FreshRSS_FeedDAO ();
+ public static function renameFeed($feed_id, $feed_name) {
+ if ($feed_id <= 0 || $feed_name == '') {
+ return false;
+ }
+ FreshRSS_UserDAO::touch();
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ return $feedDAO->updateFeed($feed_id, array('name' => $feed_name));
+ }
- $categories = Minz_Request::param ('categories', array (), true);
- $feeds = Minz_Request::param ('feeds', array (), true);
+ public static function moveFeed($feed_id, $cat_id, $new_cat_name = '') {
+ if ($feed_id <= 0 || ($cat_id <= 0 && $new_cat_name == '')) {
+ return false;
+ }
+ FreshRSS_UserDAO::touch();
- // on ajoute les catégories en masse dans une fonction à part
- $this->addCategories ($categories);
+ $catDAO = new FreshRSS_CategoryDAO();
+ if ($cat_id > 0) {
+ $cat = $catDAO->searchById($cat_id);
+ $cat_id = $cat == null ? 0 : $cat->id();
+ }
+ if ($cat_id <= 1 && $new_cat_name != '') {
+ $cat_id = $catDAO->addCategory(array('name' => $new_cat_name));
+ }
+ if ($cat_id <= 1) {
+ $catDAO->checkDefault();
+ $cat_id = FreshRSS_CategoryDAO::defaultCategoryId;
+ }
- // 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);
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ return $feedDAO->updateFeed($feed_id, array('category' => $cat_id));
+ }
- // la variable $error permet de savoir si une erreur est survenue
- // Le but est de ne pas arrêter l'import même en cas d'erreur
- // L'utilisateur sera mis au courant s'il y a eu des erreurs, mais
- // ne connaîtra pas les détails. Ceux-ci seront toutefois logguées
- $error = false;
- $i = 0;
- foreach ($feeds as $feed) {
- try {
- $values = array (
- 'id' => $feed->id (),
- 'url' => $feed->url (),
- 'category' => $feed->category (),
- 'name' => $feed->name (),
- 'website' => $feed->website (),
- 'description' => $feed->description (),
- 'lastUpdate' => 0,
- 'httpAuth' => $feed->httpAuth ()
- );
-
- // ajout du flux que s'il n'est pas déjà en BDD
- if (!$feedDAO->searchByUrl ($values['url'])) {
- $id = $feedDAO->addFeed ($values);
- if ($id) {
- $feed->_id ($id);
- $feed->faviconPrepare();
- } else {
- $error = true;
- }
- }
- } catch (FreshRSS_Feed_Exception $e) {
- $error = true;
- Minz_Log::record ($e->getMessage (), Minz_Log::WARNING);
- }
+ /**
+ * 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 ($error) {
- $res = Minz_Translate::t ('feeds_imported_with_errors');
+ $feed_id = Minz_Request::param('f_id');
+ $cat_id = Minz_Request::param('c_id');
+
+ if (self::moveFeed($feed_id, $cat_id)) {
+ // TODO: return something useful
} else {
- $res = Minz_Translate::t ('feeds_imported');
+ Minz_Log::warning('Cannot move feed `' . $feed_id . '` ' .
+ 'in the category `' . $cat_id . '`');
+ Minz_Error::error(404);
}
-
- $notif = array (
- 'type' => 'good',
- 'content' => $res
- );
- Minz_Session::_param ('notification', $notif);
- Minz_Session::_param ('actualize_feeds', true);
-
- // et on redirige vers la page d'accueil
- Minz_Request::forward (array (
- 'c' => 'index',
- 'a' => 'index'
- ), true);
}
- public function deleteAction () {
- if (Minz_Request::isPost ()) {
- $type = Minz_Request::param ('type', 'feed');
- $id = Minz_Request::param ('id');
-
- $feedDAO = new FreshRSS_FeedDAO ();
- if ($type == 'category') {
- if ($feedDAO->deleteFeedByCategory ($id)) {
- $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)) {
- $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')
- );
- }
- }
+ public static function deleteFeed($feed_id) {
+ FreshRSS_UserDAO::touch();
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ if ($feedDAO->deleteFeed($feed_id)) {
+ // TODO: Delete old favicon
- Minz_Session::_param ('notification', $notif);
+ // Remove related queries
+ FreshRSS_Context::$user_conf->queries = remove_query_by_get(
+ 'f_' . $feed_id, FreshRSS_Context::$user_conf->queries);
+ FreshRSS_Context::$user_conf->save();
- if ($type == 'category') {
- Minz_Request::forward (array ('c' => 'configure', 'a' => 'categorize'), true);
- } else {
- Minz_Request::forward (array ('c' => 'configure', 'a' => 'feed'), true);
- }
+ return true;
}
+ return false;
}
- private function addCategories ($categories) {
- foreach ($categories as $cat) {
- if (!$this->catDAO->searchByName ($cat->name ())) {
- $values = array (
- 'id' => $cat->id (),
- 'name' => $cat->name (),
- );
- $catDAO->addCategory ($values);
- }
+ /**
+ * 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 (!Minz_Request::isPost()) {
+ Minz_Request::forward($redirect_url, true);
+ }
+
+ $id = Minz_Request::param('id');
+
+ if (self::deleteFeed($id)) {
+ 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
new file mode 100644
index 000000000..6ae89defb
--- /dev/null
+++ b/app/Controllers/importExportController.php
@@ -0,0 +1,716 @@
+<?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 (!FreshRSS_Auth::hasAccess()) {
+ Minz_Error::error(403);
+ }
+
+ require_once(LIB_PATH . '/lib_opml.php');
+
+ $this->catDAO = new FreshRSS_CategoryDAO();
+ $this->entryDAO = FreshRSS_Factory::createEntryDao();
+ $this->feedDAO = FreshRSS_Factory::createFeedDao();
+ }
+
+ /**
+ * This action displays the main page for import / export system.
+ */
+ public function indexAction() {
+ $this->view->feeds = $this->feedDAO->listFeeds();
+ Minz_View::prependTitle(_t('sub.import_export.title') . ' · ');
+ }
+
+ public function importFile($name, $path, $username = null) {
+ require_once(LIB_PATH . '/lib_opml.php');
+
+ $this->catDAO = new FreshRSS_CategoryDAO($username);
+ $this->entryDAO = FreshRSS_Factory::createEntryDao($username);
+ $this->feedDAO = FreshRSS_Factory::createFeedDao($username);
+
+ $type_file = self::guessFileType($name);
+
+ $list_files = array(
+ 'opml' => array(),
+ 'json_starred' => array(),
+ 'json_feed' => array()
+ );
+
+ // We try to list all files according to their type
+ $list = array();
+ if ($type_file === 'zip' && extension_loaded('zip')) {
+ $zip = zip_open($path);
+ if (!is_resource($zip)) {
+ // zip_open cannot open file: something is wrong
+ throw new FreshRSS_Zip_Exception($zip);
+ }
+ while (($zipfile = zip_read($zip)) !== false) {
+ if (!is_resource($zipfile)) {
+ // zip_entry() can also return an error code!
+ throw new FreshRSS_Zip_Exception($zipfile);
+ } else {
+ $type_zipfile = self::guessFileType(zip_entry_name($zipfile));
+ if ($type_file !== 'unknown') {
+ $list_files[$type_zipfile][] = zip_entry_read(
+ $zipfile,
+ zip_entry_filesize($zipfile)
+ );
+ }
+ }
+ }
+ zip_close($zip);
+ } elseif ($type_file === 'zip') {
+ // ZIP extension is not loaded
+ throw new FreshRSS_ZipMissing_Exception();
+ } elseif ($type_file !== 'unknown') {
+ $list_files[$type_file][] = file_get_contents($path);
+ }
+
+ // Import file contents.
+ // OPML first(so categories and feeds are imported)
+ // Starred articles then so the "favourite" status is already set
+ // And finally all other files.
+ $ok = true;
+ foreach ($list_files['opml'] as $opml_file) {
+ if (!$this->importOpml($opml_file)) {
+ $ok = false;
+ if (FreshRSS_Context::$isCli) {
+ fwrite(STDERR, 'FreshRSS error during OPML import' . "\n");
+ } else {
+ Minz_Log::warning('Error during OPML import');
+ }
+ }
+ }
+ foreach ($list_files['json_starred'] as $article_file) {
+ if (!$this->importJson($article_file, true)) {
+ $ok = false;
+ if (FreshRSS_Context::$isCli) {
+ fwrite(STDERR, 'FreshRSS error during JSON stars import' . "\n");
+ } else {
+ Minz_Log::warning('Error during JSON stars import');
+ }
+ }
+ }
+ foreach ($list_files['json_feed'] as $article_file) {
+ if (!$this->importJson($article_file)) {
+ $ok = false;
+ if (FreshRSS_Context::$isCli) {
+ fwrite(STDERR, 'FreshRSS error during JSON feeds import' . "\n");
+ } else {
+ Minz_Log::warning('Error during JSON feeds import');
+ }
+ }
+ }
+
+ return $ok;
+ }
+
+ /**
+ * 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);
+ }
+
+ $file = $_FILES['file'];
+ $status_file = $file['error'];
+
+ if ($status_file !== 0) {
+ Minz_Log::warning('File cannot be uploaded. Error code: ' . $status_file);
+ Minz_Request::bad(_t('feedback.import_export.file_cannot_be_uploaded'),
+ array('c' => 'importExport', 'a' => 'index'));
+ }
+
+ @set_time_limit(300);
+
+ $error = false;
+ try {
+ $error = !$this->importFile($file['name'], $file['tmp_name']);
+ } catch (FreshRSS_ZipMissing_Exception $zme) {
+ Minz_Request::bad(_t('feedback.import_export.no_zip_extension'),
+ array('c' => 'importExport', 'a' => 'index'));
+ } catch (FreshRSS_Zip_Exception $ze) {
+ Minz_Log::warning('ZIP archive cannot be imported. Error code: ' . $ze->zipErrorCode());
+ Minz_Request::bad(_t('feedback.import_export.zip_error'),
+ array('c' => 'importExport', 'a' => 'index'));
+ }
+
+ // And finally, we get import status and redirect to the home page
+ Minz_Session::_param('actualize_feeds', true);
+ $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 static function guessFileType($filename) {
+ if (substr_compare($filename, '.zip', -4) === 0) {
+ return 'zip';
+ } elseif (substr_compare($filename, '.opml', -5) === 0 ||
+ substr_compare($filename, '.xml', -4) === 0) {
+ return 'opml';
+ } elseif (substr_compare($filename, '.json', -5) === 0 &&
+ strpos($filename, 'starred') !== false) {
+ return 'json_starred';
+ } elseif (substr_compare($filename, '.json', -5) === 0) {
+ return 'json_feed';
+ } else {
+ return 'unknown';
+ }
+ }
+
+ /**
+ * This method parses and imports an OPML file.
+ *
+ * @param string $opml_file the OPML file content.
+ * @return boolean false if an error occured, true otherwise.
+ */
+ private function importOpml($opml_file) {
+ $opml_array = array();
+ try {
+ $opml_array = libopml_parse_string($opml_file, false);
+ } catch (LibOPML_Exception $e) {
+ if (FreshRSS_Context::$isCli) {
+ fwrite(STDERR, 'FreshRSS error during OPML parsing: ' . $e->getMessage() . "\n");
+ } else {
+ Minz_Log::warning($e->getMessage());
+ }
+ return false;
+ }
+
+ $this->catDAO->checkDefault();
+
+ 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 false if an error occured, true otherwise.
+ */
+ private function addOpmlElements($opml_elements, $parent_cat = null) {
+ $ok = true;
+
+ $nb_feeds = count($this->feedDAO->listFeeds());
+ $nb_cats = count($this->catDAO->listCategories(false));
+ $limits = FreshRSS_Context::$system_conf->limits;
+
+ foreach ($opml_elements as $elt) {
+ if (isset($elt['xmlUrl'])) {
+ // If xmlUrl exists, it means it is a feed
+ if (FreshRSS_Context::$isCli && $nb_feeds >= $limits['max_feeds']) {
+ Minz_Log::warning(_t('feedback.sub.feed.over_max',
+ $limits['max_feeds']));
+ $ok = false;
+ continue;
+ }
+
+ if ($this->addFeedOpml($elt, $parent_cat)) {
+ $nb_feeds++;
+ } else {
+ $ok = false;
+ }
+ } else {
+ // No xmlUrl? It should be a category!
+ $limit_reached = ($nb_cats >= $limits['max_categories']);
+ if (!FreshRSS_Context::$isCli && $limit_reached) {
+ Minz_Log::warning(_t('feedback.sub.category.over_max',
+ $limits['max_categories']));
+ $ok = false;
+ continue;
+ }
+
+ if ($this->addCategoryOpml($elt, $parent_cat, $limit_reached)) {
+ $nb_cats++;
+ } else {
+ $ok = false;
+ }
+ }
+ }
+
+ return $ok;
+ }
+
+ /**
+ * 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 false if an error occured, true otherwise.
+ */
+ private function addFeedOpml($feed_elt, $parent_cat) {
+ if ($parent_cat == null) {
+ // This feed has no parent category so we get the default one
+ $this->catDAO->checkDefault();
+ $default_cat = $this->catDAO->getDefault();
+ $parent_cat = $default_cat->name();
+ }
+
+ $cat = $this->catDAO->searchByName($parent_cat);
+ if ($cat == null) {
+ // If there is not $cat, it means parent category does not exist in
+ // database.
+ // If it happens, take the default category.
+ $this->catDAO->checkDefault();
+ $cat = $this->catDAO->getDefault();
+ }
+
+ // We get different useful information
+ $url = Minz_Helper::htmlspecialchars_utf8($feed_elt['xmlUrl']);
+ $name = Minz_Helper::htmlspecialchars_utf8($feed_elt['text']);
+ $website = '';
+ if (isset($feed_elt['htmlUrl'])) {
+ $website = Minz_Helper::htmlspecialchars_utf8($feed_elt['htmlUrl']);
+ }
+ $description = '';
+ if (isset($feed_elt['description'])) {
+ $description = Minz_Helper::htmlspecialchars_utf8($feed_elt['description']);
+ }
+
+ $error = false;
+ try {
+ // Create a Feed object and add it in DB
+ $feed = new FreshRSS_Feed($url);
+ $feed->_category($cat->id());
+ $feed->_name($name);
+ $feed->_website($website);
+ $feed->_description($description);
+
+ // Call the extension hook
+ $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
+ if ($feed != null) {
+ // 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) {
+ if (FreshRSS_Context::$isCli) {
+ fwrite(STDERR, 'FreshRSS error during OPML feed import: ' . $e->getMessage() . "\n");
+ } else {
+ Minz_Log::warning($e->getMessage());
+ }
+ $error = true;
+ }
+
+ if ($error) {
+ if (FreshRSS_Context::$isCli) {
+ fwrite(STDERR, 'FreshRSS error during OPML feed import from URL: ' . $url . ' in category ' . $cat->id() . "\n");
+ } else {
+ Minz_Log::warning('Error during OPML feed import from URL: ' . $url . ' in category ' . $cat->id());
+ }
+ }
+
+ return !$error;
+ }
+
+ /**
+ * 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 false if an error occured, true otherwise.
+ */
+ private function addCategoryOpml($cat_elt, $parent_cat, $cat_limit_reached) {
+ // Create a new Category object
+ $catName = Minz_Helper::htmlspecialchars_utf8($cat_elt['text']);
+ $cat = new FreshRSS_Category($catName);
+
+ $error = true;
+ if (FreshRSS_Context::$isCli || !$cat_limit_reached) {
+ $id = $this->catDAO->addCategoryObject($cat);
+ $error = ($id === false);
+ }
+ if ($error) {
+ if (FreshRSS_Context::$isCli) {
+ fwrite(STDERR, 'FreshRSS error during OPML category import from URL: ' . $catName . "\n");
+ } else {
+ Minz_Log::warning('Error during OPML category import from URL: ' . $catName);
+ }
+ }
+
+ if (isset($cat_elt['@outlines'])) {
+ // Our cat_elt contains more categories or more feeds, so we
+ // add them recursively.
+ // Note: FreshRSS does not support yet category arborescence
+ $error &= !$this->addOpmlElements($cat_elt['@outlines'], $catName);
+ }
+
+ return !$error;
+ }
+
+ /**
+ * 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 false if an error occured, true otherwise.
+ */
+ private function importJson($article_file, $starred = false) {
+ $article_object = json_decode($article_file, true);
+ if ($article_object == null) {
+ if (FreshRSS_Context::$isCli) {
+ fwrite(STDERR, 'FreshRSS error trying to import a non-JSON file' . "\n");
+ } else {
+ Minz_Log::warning('Try to import a non-JSON file');
+ }
+ return false;
+ }
+
+ $is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0;
+
+ $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) {
+ $key = $google_compliant ? 'htmlUrl' : 'feedUrl';
+ $feed = new FreshRSS_Feed($item['origin'][$key]);
+ $feed = $this->feedDAO->searchByUrl($feed->url());
+
+ if ($feed == null) {
+ // Feed does not exist in DB,we should to try to add it.
+ if ((!FreshRSS_Context::$isCli) && ($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 ($feed == null) {
+ // Still null? It means something went wrong.
+ $error = true;
+ } else {
+ $nb_feeds++;
+ }
+ }
+
+ if ($feed != null) {
+ $article_to_feed[$item['id']] = $feed->id();
+ }
+ }
+
+ $newGuids = array();
+ foreach ($article_object['items'] as $item) {
+ $newGuids[] = safe_ascii($item['id']);
+ }
+ // For this feed, check existing GUIDs already in database.
+ $existingHashForGuids = $this->entryDAO->listHashForFeedGuids($feed->id(), $newGuids);
+ unset($newGuids);
+
+ // Then, articles are imported.
+ $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;
+ }
+
+ $feed_id = $article_to_feed[$item['id']];
+ $author = isset($item['author']) ? $item['author'] : '';
+ $key_content = ($google_compliant && !isset($item['content'])) ?
+ '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;
+ });
+ }
+
+ $entry = new FreshRSS_Entry(
+ $feed_id, $item['id'], $item['title'], $author,
+ $item[$key_content]['content'], $item['alternate'][0]['href'],
+ $item['published'], $is_read, $starred
+ );
+ $entry->_id(min(time(), $entry->date(true)) . uSecString());
+ $entry->_tags($tags);
+
+ $entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry);
+ if ($entry == null) {
+ // An extension has returned a null value, there is nothing to insert.
+ continue;
+ }
+
+ $values = $entry->toArray();
+ if (isset($existingHashForGuids[$entry->guid()])) {
+ $id = $this->entryDAO->updateEntry($values);
+ } else {
+ $id = $this->entryDAO->addEntry($values);
+ }
+
+ if (!$error && ($id === false)) {
+ $error = true;
+ }
+ }
+ $this->entryDAO->commit();
+
+ return !$error;
+ }
+
+ /**
+ * 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) {
+ $return = null;
+ $key = $google_compliant ? 'htmlUrl' : 'feedUrl';
+ $url = $origin[$key];
+ $name = $origin['title'];
+ $website = $origin['htmlUrl'];
+
+ try {
+ // Create a Feed object and add it in database.
+ $feed = new FreshRSS_Feed($url);
+ $feed->_category(FreshRSS_CategoryDAO::defaultCategoryId);
+ $feed->_name($name);
+ $feed->_website($website);
+
+ // Call the extension hook
+ $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
+ if ($feed != null) {
+ // 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;
+ }
+ }
+ } catch (FreshRSS_Feed_Exception $e) {
+ if (FreshRSS_Context::$isCli) {
+ fwrite(STDERR, 'FreshRSS error during JSON feed import: ' . $e->getMessage() . "\n");
+ } else {
+ Minz_Log::warning($e->getMessage());
+ }
+ }
+
+ return $return;
+ }
+
+ public function exportFile($export_opml = true, $export_starred = false, $export_feeds = array(), $maxFeedEntries = 50, $username = null) {
+ require_once(LIB_PATH . '/lib_opml.php');
+
+ $this->catDAO = new FreshRSS_CategoryDAO($username);
+ $this->entryDAO = FreshRSS_Factory::createEntryDao($username);
+ $this->feedDAO = FreshRSS_Factory::createFeedDao($username);
+
+ $this->entryDAO->disableBuffering();
+
+ if ($export_feeds === true) {
+ //All feeds
+ $export_feeds = $this->feedDAO->listFeedsIds();
+ }
+ if (!is_array($export_feeds)) {
+ $export_feeds = array();
+ }
+
+ $day = date('Y-m-d');
+
+ $export_files = array();
+ if ($export_opml) {
+ $export_files["feeds_${day}.opml.xml"] = $this->generateOpml();
+ }
+
+ if ($export_starred) {
+ $export_files["starred_${day}.json"] = $this->generateEntries('starred');
+ }
+
+ foreach ($export_feeds as $feed_id) {
+ $feed = $this->feedDAO->searchById($feed_id);
+ if ($feed) {
+ $filename = "feed_${day}_" . $feed->category() . '_'
+ . $feed->id() . '.json';
+ $export_files[$filename] = $this->generateEntries('feed', $feed, $maxFeedEntries);
+ }
+ }
+
+ $nb_files = count($export_files);
+ if ($nb_files > 1) {
+ // If there are more than 1 file to export, we need a ZIP archive.
+ try {
+ $this->sendZip($export_files);
+ } catch (Exception $e) {
+ throw new FreshRSS_ZipMissing_Exception($e);
+ }
+ } elseif ($nb_files === 1) {
+ // Only one file? Guess its type and export it.
+ $filename = key($export_files);
+ $type = self::guessFileType($filename);
+ $this->sendFile('freshrss_' . $filename, $export_files[$filename], $type);
+ }
+ return $nb_files;
+ }
+
+ /**
+ * 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);
+ }
+ $this->view->_useLayout(false);
+
+ $nb_files = 0;
+ try {
+ $nb_files = $this->exportFile(
+ Minz_Request::param('export_opml', false),
+ Minz_Request::param('export_starred', false),
+ Minz_Request::param('export_feeds', array())
+ );
+ } catch (FreshRSS_ZipMissing_Exception $zme) {
+ # Oops, there is no ZIP extension!
+ Minz_Request::bad(_t('feedback.import_export.export_no_zip_extension'),
+ array('c' => 'importExport', 'a' => 'index'));
+ }
+
+ if ($nb_files < 1) {
+ // 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) {
+ $list[$key]['name'] = $cat->name();
+ $list[$key]['feeds'] = $this->feedDAO->listByCategory($cat->id());
+ }
+
+ $this->view->categories = $list;
+ return $this->view->helperToString('export/opml');
+ }
+
+ /**
+ * 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, $maxFeedEntries = 50) {
+ $this->view->categories = $this->catDAO->listCategories();
+
+ if ($type == 'starred') {
+ $this->view->list_title = _t('sub.import_export.starred_list');
+ $this->view->type = 'starred';
+ $unread_fav = $this->entryDAO->countUnreadReadFavorites();
+ $this->view->entriesRaw = $this->entryDAO->listWhereRaw(
+ 's', '', FreshRSS_Entry::STATE_ALL, 'ASC', $unread_fav['all']
+ );
+ } elseif ($type === 'feed' && $feed != null) {
+ $this->view->list_title = _t('sub.import_export.feed_list', $feed->name());
+ $this->view->type = 'feed/' . $feed->id();
+ $this->view->entriesRaw = $this->entryDAO->listWhereRaw(
+ 'f', $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC',
+ $maxFeedEntries
+ );
+ $this->view->feed = $feed;
+ }
+
+ 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 sendZip($files) {
+ if (!extension_loaded('zip')) {
+ throw new Exception();
+ }
+
+ // From https://stackoverflow.com/questions/1061710/php-zip-files-on-the-fly
+ $zip_file = tempnam('tmp', 'zip');
+ $zip = new ZipArchive();
+ $zip->open($zip_file, ZipArchive::OVERWRITE);
+
+ foreach ($files as $filename => $content) {
+ $zip->addFromString($filename, $content);
+ }
+
+ // Close and send to user
+ $zip->close();
+ header('Content-Type: application/zip');
+ header('Content-Length: ' . filesize($zip_file));
+ $day = date('Y-m-d');
+ header('Content-Disposition: attachment; filename="freshrss_' . $day . '_export.zip"');
+ readfile($zip_file);
+ 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 sendFile($filename, $content, $type) {
+ if ($type === 'unknown') {
+ return;
+ }
+
+ $content_type = '';
+ if ($type === 'opml') {
+ $content_type = 'application/xml';
+ } elseif ($type === 'json_feed' || $type === 'json_starred') {
+ $content_type = 'application/json';
+ }
+
+ header('Content-Type: ' . $content_type . '; charset=utf-8');
+ header('Content-disposition: attachment; filename=' . $filename);
+ print($content);
+ }
+}
diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php
index 38f4c0e7c..5ca147ff3 100755
--- a/app/Controllers/indexController.php
+++ b/app/Controllers/indexController.php
@@ -1,371 +1,272 @@
<?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;
- }
- }
- // construction of RSS url of this feed
- $params = Minz_Request::params ();
- $params['output'] = 'rss';
- if (isset ($params['search'])) {
- $params['search'] = urlencode ($params['search']);
- }
- if (!Minz_Configuration::allowAnonymous()) {
- $params['token'] = $token;
- }
- $this->view->rss_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')));
- }
+ 'a' => $prefered_output
+ ));
+ }
- $catDAO = new FreshRSS_CategoryDAO();
- $entryDAO = new FreshRSS_EntryDAO();
-
- $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();
- if ($this->view->nb_not_read > 0) {
- Minz_View::appendTitle (' (' . formatNumber($this->view->nb_not_read) . ')');
+ try {
+ $this->updateContext();
+ } catch (FreshRSS_Context_Exception $e) {
+ Minz_Error::error(404);
}
- Minz_View::prependTitle (
- $this->view->currentName .
- ($this->nb_not_read_cat > 0 ? ' (' . formatNumber($this->nb_not_read_cat) . ')' : '') .
- ' · '
- );
- // On récupère les différents éléments de filtrage
- $this->view->state = $state = Minz_Request::param ('state', $this->view->conf->default_view);
- $filter = Minz_Request::param ('search', '');
- if (!empty($filter)) {
- $state = 'all'; //Search always in read and unread articles
- }
- $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', '');
-
- if ($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':
- $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;
+ $this->view->callbackBeforeContent = function($view) {
+ try {
+ FreshRSS_Context::$number++; //+1 for pagination
+ $entries = FreshRSS_index_Controller::listEntriesByContext();
+ FreshRSS_Context::$number--;
+
+ $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();
+ }
+
+ $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;
+ }
+ }
+
+ $view->entries = $entries;
+ } catch (FreshRSS_EntriesGetter_Exception $e) {
+ Minz_Log::notice($e->getMessage());
+ Minz_Error::error(404);
}
- if (!$hasUnread) {
- $this->view->state = $state = 'all';
+
+ $view->categories = FreshRSS_Context::$categories;
+
+ $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 . ' · ');
+ };
+ }
- $today = @strtotime('today');
- $this->view->today = $today;
+ /**
+ * This action displays the reader view of FreshRSS.
+ *
+ * @todo: change this view into specific CSS rules?
+ */
+ public function readerAction() {
+ $this->normalAction();
+ }
+
+ /**
+ * 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;
+ }
- // 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;
+ Minz_View::appendScript(Minz_Url::display('/scripts/global_view.js?' . @filemtime(PUBLIC_PATH . '/scripts/global_view.js')));
try {
- $entries = $entryDAO->listWhere($getType, $getId, $state, $order, $nb + 1, $first, $filter, $date_min, $keepHistoryDefault);
-
- // Si on a récupéré aucun article "non lus"
- // on essaye de récupérer tous les articles
- if ($state === 'not_read' && empty($entries)) {
- Minz_Log::record ('Conflicting information about nbNotRead!', Minz_Log::DEBUG);
- $this->view->state = 'all';
- $entries = $entryDAO->listWhere($getType, $getId, 'all', $order, $nb, $first, $filter, $date_min, $keepHistoryDefault);
- }
+ $this->updateContext();
+ } catch (FreshRSS_Context_Exception $e) {
+ Minz_Error::error(404);
+ }
- if (count($entries) <= $nb) {
- $this->view->nextId = '';
- } else { //We have more elements for pagination
- $lastEntry = array_pop($entries);
- $this->view->nextId = $lastEntry->id();
- }
+ $this->view->categories = FreshRSS_Context::$categories;
- $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')))
- );
+ $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 . ' · ');
}
- /*
- * 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
+ /**
+ * This action displays the RSS feed of FreshRSS.
*/
- 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 = new FreshRSS_FeedDAO();
- $feed = $feedDAO->searchById($getId);
- }
- if ($feed) {
- $this->view->currentName = $feed->name ();
- $this->nb_not_read_cat = $feed->nbNotRead ();
- $this->view->get_f = $getId;
- $this->view->get_c = $feed->category ();
- return true;
- } else {
- return false;
- }
- default:
- return false;
+ public function 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);
}
- }
-
- public function statsAction () {
- if (!$this->view->loginOk) {
- Minz_Error::error (
- 403,
- array ('error' => array (Minz_Translate::t ('access_denied')))
- );
+
+ try {
+ $this->updateContext();
+ } catch (FreshRSS_Context_Exception $e) {
+ Minz_Error::error(404);
}
- Minz_View::prependTitle (Minz_Translate::t ('stats') . ' · ');
+ try {
+ $this->view->entries = FreshRSS_index_Controller::listEntriesByContext();
+ } catch (FreshRSS_EntriesGetter_Exception $e) {
+ Minz_Log::notice($e->getMessage());
+ Minz_Error::error(404);
+ }
- $statsDAO = new FreshRSS_StatsDAO ();
- Minz_View::appendScript (Minz_Url::display ('/scripts/flotr2.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/flotr2.min.js')));
- $this->view->repartition = $statsDAO->calculateEntryRepartition();
- $this->view->count = ($statsDAO->calculateEntryCount());
- $this->view->feedByCategory = $statsDAO->calculateFeedByCategory();
- $this->view->entryByCategory = $statsDAO->calculateEntryByCategory();
- $this->view->topFeed = $statsDAO->calculateTopFeed();
+ // No layout for RSS output.
+ $this->view->url = empty($_SERVER['QUERY_STRING']) ? '' : '?' . $_SERVER['QUERY_STRING'];
+ $this->view->rss_title = FreshRSS_Context::$name . ' | ' . Minz_View::title();
+ $this->view->_useLayout(false);
+ header('Content-Type: application/rss+xml; charset=utf-8');
}
- public function aboutAction () {
- Minz_View::prependTitle (Minz_Translate::t ('about') . ' · ');
- }
+ /**
+ * This action updates the Context object by using request parameters.
+ *
+ * Parameters are:
+ * - state (default: conf->default_view)
+ * - search (default: empty string)
+ * - order (default: conf->sort_order)
+ * - nb (default: conf->posts_per_page)
+ * - next (default: empty string)
+ * - hours (default: 0)
+ */
+ private function updateContext() {
+ if (empty(FreshRSS_Context::$categories)) {
+ $catDAO = new FreshRSS_CategoryDAO();
+ FreshRSS_Context::$categories = $catDAO->listCategories();
+ }
- public function logsAction () {
- if (!$this->view->loginOk) {
- Minz_Error::error (
- 403,
- array ('error' => array (Minz_Translate::t ('access_denied')))
- );
+ // 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
+ );
+
+ 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;
}
- Minz_View::prependTitle (Minz_Translate::t ('logs') . ' · ');
+ FreshRSS_Context::$search = new FreshRSS_Search(Minz_Request::param('search', ''));
+ FreshRSS_Context::$order = Minz_Request::param(
+ 'order', FreshRSS_Context::$user_conf->sort_order
+ );
+ FreshRSS_Context::$number = intval(Minz_Request::param('nb', FreshRSS_Context::$user_conf->posts_per_page));
+ if (FreshRSS_Context::$number > FreshRSS_Context::$user_conf->max_posts_per_rss) {
+ FreshRSS_Context::$number = max(
+ FreshRSS_Context::$user_conf->max_posts_per_rss,
+ FreshRSS_Context::$user_conf->posts_per_page);
+ }
+ FreshRSS_Context::$first_id = Minz_Request::param('next', '');
+ FreshRSS_Context::$sinceHours = intval(Minz_Request::param('hours', 0));
+ }
- if (Minz_Request::isPost ()) {
- FreshRSS_LogDAO::truncate();
+ /**
+ * This method returns a list of entries based on the Context object.
+ */
+ public static function listEntriesByContext() {
+ $entryDAO = FreshRSS_Factory::createEntryDao();
+
+ $get = FreshRSS_Context::currentGet(true);
+ if (count($get) > 1) {
+ $type = $get[0];
+ $id = $get[1];
+ } else {
+ $type = $get;
+ $id = '';
}
- $logs = FreshRSS_LogDAO::lines(); //TODO: ask only the necessary lines
+ $limit = FreshRSS_Context::$number;
- //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);
- }
+ $date_min = 0;
+ if (FreshRSS_Context::$sinceHours) {
+ $date_min = time() - (FreshRSS_Context::$sinceHours * 3600);
+ $limit = FreshRSS_Context::$user_conf->max_posts_per_rss;
+ }
- 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
+ $entries = $entryDAO->listWhere(
+ $type, $id, FreshRSS_Context::$state, FreshRSS_Context::$order,
+ $limit, FreshRSS_Context::$first_id,
+ FreshRSS_Context::$search, $date_min
);
- curl_setopt_array ($ch, $options);
- $result = curl_exec ($ch);
- curl_close ($ch);
-
- $res = json_decode ($result, true);
-
- $loginOk = false;
- $reason = '';
- if ($res['status'] === 'okay') {
- $email = filter_var($res['email'], FILTER_VALIDATE_EMAIL);
- if ($email != '') {
- $personaFile = DATA_PATH . '/persona/' . $email . '.txt';
- if (($currentUser = @file_get_contents($personaFile)) !== false) {
- $currentUser = trim($currentUser);
- if (ctype_alnum($currentUser)) {
- try {
- $this->conf = new FreshRSS_Configuration($currentUser);
- $loginOk = strcasecmp($email, $this->conf->mail_login) === 0;
- } catch (Minz_Exception $e) {
- $reason = 'Invalid configuration for user [' . $currentUser . ']! ' . $e->getMessage(); //Permission denied or conf file does not exist
- }
- } else {
- $reason = 'Invalid username format [' . $currentUser . ']!';
- }
- }
- } else {
- $reason = 'Invalid email format [' . $res['email'] . ']!';
- }
- }
- if ($loginOk) {
- Minz_Session::_param('currentUser', $currentUser);
- Minz_Session::_param ('mail', $email);
- $this->view->loginOk = true;
- invalidateHttpCache();
- } else {
- $res = array ();
- $res['status'] = 'failure';
- $res['reason'] = $reason == '' ? Minz_Translate::t ('invalid_login') : $reason;
- Minz_Log::record ('Persona: ' . $res['reason'], Minz_Log::WARNING);
+
+ if (FreshRSS_Context::$sinceHours && (count($entries) < FreshRSS_Context::$user_conf->min_posts_per_rss)) {
+ $date_min = 0;
+ $limit = FreshRSS_Context::$user_conf->min_posts_per_rss;
+ $entries = $entryDAO->listWhere(
+ $type, $id, FreshRSS_Context::$state, FreshRSS_Context::$order,
+ $limit, FreshRSS_Context::$first_id,
+ FreshRSS_Context::$search, $date_min
+ );
}
- header('Content-Type: application/json; charset=UTF-8');
- $this->view->res = json_encode ($res);
+ return $entries;
}
- public function logoutAction () {
- $this->view->_useLayout(false);
- invalidateHttpCache();
- Minz_Session::_param('currentUser');
- Minz_Session::_param('mail');
- Minz_Session::_param('passwordHash');
+ /**
+ * This action displays the about page of FreshRSS.
+ */
+ public function aboutAction() {
+ Minz_View::prependTitle(_t('index.about.title') . ' · ');
}
- public function formLoginAction () {
+ /**
+ * This action displays logs of FreshRSS for the current user.
+ */
+ public function logsAction() {
+ if (!FreshRSS_Auth::hasAccess()) {
+ Minz_Error::error(403);
+ }
+
+ Minz_View::prependTitle(_t('index.log.title') . ' · ');
+
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);
- } 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::canLogIn()) {
- Minz_Error::error (
- 403,
- array ('error' => array (Minz_Translate::t ('access_denied')))
- );
+ FreshRSS_LogDAO::truncate();
}
- invalidateHttpCache();
- }
- public function formLogoutAction () {
- $this->view->_useLayout(false);
- invalidateHttpCache();
- Minz_Session::_param('currentUser');
- Minz_Session::_param('mail');
- Minz_Session::_param('passwordHash');
- Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
+ $logs = FreshRSS_LogDAO::lines(); //TODO: ask only the necessary lines
+
+ //gestion pagination
+ $page = Minz_Request::param('page', 1);
+ $this->view->logsPaginator = new Minz_Paginator($logs);
+ $this->view->logsPaginator->_nbItemsPerPage(50);
+ $this->view->logsPaginator->_currentPage($page);
}
}
diff --git a/app/Controllers/javascriptController.php b/app/Controllers/javascriptController.php
index 3d741e298..00a7b5c38 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 () {
- header('Content-Type: text/javascript; charset=UTF-8');
- $feedDAO = new FreshRSS_FeedDAO ();
- $this->view->feeds = $feedDAO->listFeedsOrderUpdate();
+ public function actualizeAction() {
+ header('Content-Type: application/json; charset=UTF-8');
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ $this->view->feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::$user_conf->ttl_default);
}
public function nbUnreadsPerFeedAction() {
@@ -28,19 +28,27 @@ 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!');
+ }
+ //Failure: Return random data.
+ $this->view->salt1 = sprintf('$2a$%02d$', FreshRSS_user_Controller::BCRYPT_COST);
+ $alphabet = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ for ($i = 22; $i > 0; $i--) {
+ $this->view->salt1 .= $alphabet[rand(0, 63)];
}
- $this->view->nonce = ''; //Failure
- $this->view->salt1 = '';
+ $this->view->nonce = sha1(rand());
}
}
diff --git a/app/Controllers/statsController.php b/app/Controllers/statsController.php
new file mode 100644
index 000000000..5d1dee72c
--- /dev/null
+++ b/app/Controllers/statsController.php
@@ -0,0 +1,150 @@
+<?php
+
+/**
+ * Controller to handle application statistics.
+ */
+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') . ' · ');
+ }
+
+ private function convertToSerie($data) {
+ $serie = array();
+
+ foreach ($data as $key => $value) {
+ $serie[] = array($key, $value);
+ }
+
+ return $serie;
+ }
+
+ private function convertToPieSerie($data) {
+ $serie = array();
+
+ foreach ($data as $value) {
+ $value['data'] = array(array(0, (int) $value['data']));
+ $serie[] = $value;
+ }
+
+ return $serie;
+ }
+
+ /**
+ * This action handles the statistic main page.
+ *
+ * It displays the statistic main page.
+ * The values computed to display the page are:
+ * - repartition of read/unread/favorite/not favorite
+ * - number of article per day
+ * - number of feed by category
+ * - number of article by category
+ * - list of most prolific feed
+ */
+ public function indexAction() {
+ $statsDAO = FreshRSS_Factory::createStatsDAO();
+ Minz_View::appendScript(Minz_Url::display('/scripts/flotr2.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/flotr2.min.js')));
+ $this->view->repartition = $statsDAO->calculateEntryRepartition();
+ $entryCount = $statsDAO->calculateEntryCount();
+ $this->view->count = $this->convertToSerie($entryCount);
+ $this->view->average = round(array_sum(array_values($entryCount)) / count($entryCount), 2);
+ $this->view->feedByCategory = $this->convertToPieSerie($statsDAO->calculateFeedByCategory());
+ $this->view->entryByCategory = $this->convertToPieSerie($statsDAO->calculateEntryByCategory());
+ $this->view->topFeed = $statsDAO->calculateTopFeed();
+ }
+
+ /**
+ * This action handles the idle feed statistic page.
+ *
+ * It displays the list of idle feed for different period. The supported
+ * periods are:
+ * - last year
+ * - last 6 months
+ * - last 3 months
+ * - last month
+ * - last week
+ */
+ public function idleAction() {
+ $statsDAO = FreshRSS_Factory::createStatsDAO();
+ $feeds = $statsDAO->calculateFeedLastDate();
+ $idleFeeds = array(
+ 'last_year' => array(),
+ 'last_6_month' => array(),
+ 'last_3_month' => array(),
+ 'last_month' => array(),
+ 'last_week' => array(),
+ );
+ $now = new \DateTime();
+ $feedDate = clone $now;
+ $lastWeek = clone $now;
+ $lastWeek->modify('-1 week');
+ $lastMonth = clone $now;
+ $lastMonth->modify('-1 month');
+ $last3Month = clone $now;
+ $last3Month->modify('-3 month');
+ $last6Month = clone $now;
+ $last6Month->modify('-6 month');
+ $lastYear = clone $now;
+ $lastYear->modify('-1 year');
+
+ foreach ($feeds as $feed) {
+ $feedDate->setTimestamp($feed['last_date']);
+ if ($feedDate >= $lastWeek) {
+ continue;
+ }
+ if ($feedDate < $lastYear) {
+ $idleFeeds['last_year'][] = $feed;
+ } elseif ($feedDate < $last6Month) {
+ $idleFeeds['last_6_month'][] = $feed;
+ } elseif ($feedDate < $last3Month) {
+ $idleFeeds['last_3_month'][] = $feed;
+ } elseif ($feedDate < $lastMonth) {
+ $idleFeeds['last_month'][] = $feed;
+ } elseif ($feedDate < $lastWeek) {
+ $idleFeeds['last_week'][] = $feed;
+ }
+ }
+
+ $this->view->idleFeeds = $idleFeeds;
+ }
+
+ /**
+ * This action handles the article repartition statistic page.
+ *
+ * It displays the number of article and the average of article for the
+ * following periods:
+ * - hour of the day
+ * - day of the week
+ * - month
+ *
+ * @todo verify that the metrics used here make some sense. Especially
+ * for the average.
+ */
+ public function repartitionAction() {
+ $statsDAO = FreshRSS_Factory::createStatsDAO();
+ $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);
+ $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 = $this->convertToSerie($statsDAO->calculateEntryRepartitionPerFeedPerHour($id));
+ $this->view->averageHour = $statsDAO->calculateEntryAveragePerFeedPerHour($id);
+ $this->view->repartitionDayOfWeek = $this->convertToSerie($statsDAO->calculateEntryRepartitionPerFeedPerDayOfWeek($id));
+ $this->view->averageDayOfWeek = $statsDAO->calculateEntryAveragePerFeedPerDayOfWeek($id);
+ $this->view->repartitionMonth = $this->convertToSerie($statsDAO->calculateEntryRepartitionPerFeedPerMonth($id));
+ $this->view->averageMonth = $statsDAO->calculateEntryAveragePerFeedPerMonth($id);
+ }
+}
diff --git a/app/Controllers/subscriptionController.php b/app/Controllers/subscriptionController.php
new file mode 100644
index 000000000..03d3ee15e
--- /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 = trim(Minz_Request::param('http_user_feed' . $id, ''));
+ $pass = Minz_Request::param('http_pass_feed' . $id, '');
+
+ $httpAuth = '';
+ if ($user != '' && $pass != '') { //TODO: Sanitize
+ $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
new file mode 100644
index 000000000..8f939dbdb
--- /dev/null
+++ b/app/Controllers/updateController.php
@@ -0,0 +1,226 @@
+<?php
+
+class FreshRSS_update_Controller extends Minz_ActionController {
+
+ public static function isGit() {
+ return is_dir(FRESHRSS_PATH . '/.git/');
+ }
+
+ public static function hasGitUpdate() {
+ $cwd = getcwd();
+ chdir(FRESHRSS_PATH);
+ $output = array();
+ try {
+ exec('git fetch', $output, $return);
+ if ($return == 0) {
+ exec('git status -sb --porcelain remote', $output, $return);
+ } else {
+ $line = is_array($output) ? implode('; ', $output) : '' . $output;
+ Minz_Log::warning('git fetch warning:' . $line);
+ }
+ } catch (Exception $e) {
+ Minz_Log::warning('git fetch error:' . $e->getMessage());
+ }
+ chdir($cwd);
+ $line = is_array($output) ? implode('; ', $output) : '' . $output;
+ return strpos($line, '[behind') !== false;
+ }
+
+ public static function gitPull() {
+ $cwd = getcwd();
+ chdir(FRESHRSS_PATH);
+ $output = array();
+ $return = 1;
+ try {
+ exec('git pull --ff-only', $output, $return);
+ } catch (Exception $e) {
+ Minz_Log::warning('git pull error:' . $e->getMessage());
+ }
+ chdir($cwd);
+ $line = is_array($output) ? implode('; ', $output) : '' . $output;
+ return $return == 0 ? true : 'Git error: ' . $line;
+ }
+
+ public function firstAction() {
+ if (!FreshRSS_Auth::hasAccess('admin')) {
+ Minz_Error::error(403);
+ }
+
+ invalidateHttpCache();
+
+ $this->view->update_to_apply = false;
+ $this->view->last_update_time = 'unknown';
+ $timestamp = @filemtime(join_path(DATA_PATH, 'last_update.txt'));
+ if ($timestamp !== false) {
+ $this->view->last_update_time = timestamptodate($timestamp);
+ }
+ }
+
+ public function indexAction() {
+ Minz_View::prependTitle(_t('admin.update.title') . ' · ');
+
+ if (!is_writable(FRESHRSS_PATH)) {
+ $this->view->message = array(
+ 'status' => 'bad',
+ '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('gen.short.ok'),
+ 'body' => _t('feedback.update.can_apply', $version)
+ );
+ }
+ }
+
+ public function checkAction() {
+ $this->view->change_view('update', 'index');
+
+ 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'), true);
+
+ return;
+ }
+
+ $script = '';
+ $version = '';
+
+ if (self::isGit()) {
+ if (self::hasGitUpdate()) {
+ $version = 'git';
+ } else {
+ $this->view->message = array(
+ 'status' => 'bad',
+ 'title' => _t('gen.short.damn'),
+ 'body' => _t('feedback.update.none')
+ );
+ @touch(join_path(DATA_PATH, 'last_update.txt'));
+ return;
+ }
+ } else {
+ $auto_update_url = FreshRSS_Context::$system_conf->auto_update_url . '?v=' . FRESHRSS_VERSION;
+ Minz_Log::debug('HTTP GET ' . $auto_update_url);
+ $c = curl_init($auto_update_url);
+ curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($c, CURLOPT_SSL_VERIFYPEER, true);
+ curl_setopt($c, CURLOPT_SSL_VERIFYHOST, 2);
+ $result = curl_exec($c);
+ $c_status = curl_getinfo($c, CURLINFO_HTTP_CODE);
+ $c_error = curl_error($c);
+ curl_close($c);
+
+ if ($c_status !== 200) {
+ Minz_Log::warning(
+ 'Error during update (HTTP code ' . $c_status . '): ' . $c_error
+ );
+
+ $this->view->message = array(
+ 'status' => 'bad',
+ 'title' => _t('gen.short.damn'),
+ 'body' => _t('feedback.update.server_not_found', $auto_update_url)
+ );
+ return;
+ }
+
+ $res_array = explode("\n", $result, 2);
+ $status = $res_array[0];
+ if (strpos($status, 'UPDATE') !== 0) {
+ $this->view->message = array(
+ 'status' => 'bad',
+ 'title' => _t('gen.short.damn'),
+ 'body' => _t('feedback.update.none')
+ );
+ @touch(join_path(DATA_PATH, 'last_update.txt'));
+ return;
+ }
+
+ $script = $res_array[1];
+ $version = explode(' ', $status, 2);
+ $version = $version[1];
+ }
+
+ if (file_put_contents(UPDATE_FILENAME, $script) !== false) {
+ @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('gen.short.damn'),
+ 'body' => _t('feedback.update.error', 'Cannot save the update script')
+ );
+ }
+ }
+
+ public function applyAction() {
+ if (!file_exists(UPDATE_FILENAME) || !is_writable(FRESHRSS_PATH)) {
+ Minz_Request::forward(array('c' => 'update'), true);
+ }
+
+ if (Minz_Request::param('post_conf', false)) {
+ if (self::isGit()) {
+ $res = !self::hasGitUpdate();
+ } else {
+ require(UPDATE_FILENAME);
+ $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'));
+ }
+ } else {
+ $res = false;
+
+ if (self::isGit()) {
+ $res = self::gitPull();
+ } else {
+ if (Minz_Request::isPost()) {
+ save_info_update();
+ }
+ if (!need_info_update()) {
+ $res = apply_update();
+ } else {
+ return;
+ }
+ }
+
+ if ($res === true) {
+ Minz_Request::forward(array(
+ 'c' => 'update',
+ 'a' => 'apply',
+ 'params' => array('post_conf' => true)
+ ), true);
+ } else {
+ 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/userController.php b/app/Controllers/userController.php
new file mode 100644
index 000000000..9d6ae18e6
--- /dev/null
+++ b/app/Controllers/userController.php
@@ -0,0 +1,257 @@
+<?php
+
+/**
+ * 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.
+ *
+ * @todo clean up the access condition.
+ */
+ public function firstAction() {
+ if (!FreshRSS_Auth::hasAccess() && !(
+ Minz_Request::actionName() === 'create' &&
+ !max_registrations_reached()
+ )) {
+ Minz_Error::error(403);
+ }
+ }
+
+ public static function hashPassword($passwordPlain) {
+ if (!function_exists('password_hash')) {
+ include_once(LIB_PATH . '/password_compat.php');
+ }
+ $passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST));
+ $passwordPlain = '';
+ $passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js
+ return $passwordHash == '' ? '' : $passwordHash;
+ }
+
+ /**
+ * This action displays the user profile page.
+ */
+ public function profileAction() {
+ Minz_View::prependTitle(_t('conf.profile.title') . ' · ');
+
+ Minz_View::appendScript(Minz_Url::display(
+ '/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js')
+ ));
+
+ if (Minz_Request::isPost()) {
+ $ok = true;
+
+ $passwordPlain = Minz_Request::param('newPasswordPlain', '', true);
+ if ($passwordPlain != '') {
+ Minz_Request::_param('newPasswordPlain'); //Discard plain-text password ASAP
+ $_POST['newPasswordPlain'] = '';
+ $passwordHash = self::hashPassword($passwordPlain);
+ $ok &= ($passwordHash != '');
+ FreshRSS_Context::$user_conf->passwordHash = $passwordHash;
+ }
+ Minz_Session::_param('passwordHash', FreshRSS_Context::$user_conf->passwordHash);
+
+ $passwordPlain = Minz_Request::param('apiPasswordPlain', '', true);
+ if ($passwordPlain != '') {
+ $passwordHash = self::hashPassword($passwordPlain);
+ $ok &= ($passwordHash != '');
+ FreshRSS_Context::$user_conf->apiPasswordHash = $passwordHash;
+ }
+
+ $ok &= FreshRSS_Context::$user_conf->save();
+
+ 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'));
+ }
+ }
+ }
+
+ /**
+ * This action displays the user management page.
+ */
+ public function manageAction() {
+ if (!FreshRSS_Auth::hasAccess('admin')) {
+ Minz_Error::error(403);
+ }
+
+ 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');
+ }
+ $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 static function createUser($new_user_name, $passwordPlain, $apiPasswordPlain, $userConfig = array(), $insertDefaultFeeds = true) {
+ if (!is_array($userConfig)) {
+ $userConfig = array();
+ }
+
+ $ok = ($new_user_name != '') && ctype_alnum($new_user_name);
+
+ if ($ok) {
+ $languages = Minz_Translate::availableLanguages();
+ if (empty($userConfig['language']) || !in_array($userConfig['language'], $languages)) {
+ $userConfig['language'] = 'en';
+ }
+
+ $ok &= !in_array(strtoupper($new_user_name), array_map('strtoupper', listUsers())); //Not an existing user, case-insensitive
+
+ $configPath = join_path(DATA_PATH, 'users', $new_user_name, 'config.php');
+ $ok &= !file_exists($configPath);
+ }
+ if ($ok) {
+ $passwordHash = '';
+ if ($passwordPlain != '') {
+ $passwordHash = self::hashPassword($passwordPlain);
+ $ok &= ($passwordHash != '');
+ }
+
+ $apiPasswordHash = '';
+ if ($apiPasswordPlain != '') {
+ $apiPasswordHash = self::hashPassword($apiPasswordPlain);
+ $ok &= ($apiPasswordHash != '');
+ }
+ }
+ if ($ok) {
+ mkdir(join_path(DATA_PATH, 'users', $new_user_name));
+ $userConfig['passwordHash'] = $passwordHash;
+ $userConfig['apiPasswordHash'] = $apiPasswordHash;
+ $ok &= (file_put_contents($configPath, "<?php\n return " . var_export($userConfig, true) . ';') !== false);
+ }
+ if ($ok) {
+ $userDAO = new FreshRSS_UserDAO();
+ $ok &= $userDAO->createUser($new_user_name, $userConfig['language'], $insertDefaultFeeds);
+ }
+ return $ok;
+ }
+
+ /**
+ * This action creates a new user.
+ *
+ * Request parameters are:
+ * - new_user_language
+ * - new_user_name
+ * - new_user_passwordPlain
+ * - r (i.e. a redirection url, optional)
+ *
+ * @todo clean up this method. Idea: write a method to init a user with basic information.
+ * @todo handle r redirection in Minz_Request::forward directly?
+ */
+ public function createAction() {
+ if (Minz_Request::isPost() && (
+ FreshRSS_Auth::hasAccess('admin') ||
+ !max_registrations_reached()
+ )) {
+ $new_user_name = Minz_Request::param('new_user_name');
+ $passwordPlain = Minz_Request::param('new_user_passwordPlain', '', true);
+ $new_user_language = Minz_Request::param('new_user_language', FreshRSS_Context::$user_conf->language);
+
+ $ok = self::createUser($new_user_name, $passwordPlain, '', array('language' => $new_user_language));
+ Minz_Request::_param('new_user_passwordPlain'); //Discard plain-text password ASAP
+ $_POST['new_user_passwordPlain'] = '';
+ invalidateHttpCache();
+
+ $notif = array(
+ 'type' => $ok ? 'good' : 'bad',
+ 'content' => _t('feedback.user.created' . (!$ok ? '.error' : ''), $new_user_name)
+ );
+ Minz_Session::_param('notification', $notif);
+ }
+
+ $redirect_url = urldecode(Minz_Request::param('r', false, true));
+ if (!$redirect_url) {
+ $redirect_url = array('c' => 'user', 'a' => 'manage');
+ }
+ Minz_Request::forward($redirect_url, true);
+ }
+
+ public static function deleteUser($username) {
+ $db = FreshRSS_Context::$system_conf->db;
+ require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
+
+ $ok = ctype_alnum($username);
+ if ($ok) {
+ $default_user = FreshRSS_Context::$system_conf->default_user;
+ $ok &= (strcasecmp($username, $default_user) !== 0); //It is forbidden to delete the default user
+ }
+ $user_data = join_path(DATA_PATH, 'users', $username);
+ if ($ok) {
+ $ok &= is_dir($user_data);
+ }
+ if ($ok) {
+ $userDAO = new FreshRSS_UserDAO();
+ $ok &= $userDAO->deleteUser($username);
+ $ok &= recursive_unlink($user_data);
+ }
+ return $ok;
+ }
+
+ /**
+ * This action delete an existing user.
+ *
+ * Request parameter is:
+ * - username
+ *
+ * @todo clean up this method. Idea: create a User->clean() method.
+ */
+ public function deleteAction() {
+ $username = Minz_Request::param('username');
+ $redirect_url = urldecode(Minz_Request::param('r', false, true));
+ if (!$redirect_url) {
+ $redirect_url = array('c' => 'user', 'a' => 'manage');
+ }
+
+ $self_deletion = Minz_Session::param('currentUser', '_') === $username;
+
+ if (Minz_Request::isPost() && (
+ FreshRSS_Auth::hasAccess('admin') ||
+ $self_deletion
+ )) {
+ $ok = true;
+ if ($ok && $self_deletion) {
+ // We check the password if it's a self-destruction
+ $nonce = Minz_Session::param('nonce');
+ $challenge = Minz_Request::param('challenge', '');
+
+ $ok &= FreshRSS_FormAuth::checkCredentials(
+ $username, FreshRSS_Context::$user_conf->passwordHash,
+ $nonce, $challenge
+ );
+ }
+ if ($ok) {
+ $ok &= self::deleteUser($username);
+ }
+ if ($ok && $self_deletion) {
+ FreshRSS_Auth::removeAccess();
+ $redirect_url = array('c' => 'index', 'a' => 'index');
+ }
+ invalidateHttpCache();
+
+ $notif = array(
+ 'type' => $ok ? 'good' : 'bad',
+ 'content' => _t('feedback.user.deleted' . (!$ok ? '.error' : ''), $username)
+ );
+ Minz_Session::_param('notification', $notif);
+ }
+
+ Minz_Request::forward($redirect_url, true);
+ }
+}
diff --git a/app/Controllers/usersController.php b/app/Controllers/usersController.php
deleted file mode 100644
index bb4f34c5e..000000000
--- a/app/Controllers/usersController.php
+++ /dev/null
@@ -1,183 +0,0 @@
-<?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
-
- public function firstAction() {
- if (!$this->view->loginOk) {
- Minz_Error::error(
- 403,
- array('error' => array(Minz_Translate::t('access_denied')))
- );
- }
- }
-
- public function authAction() {
- if (Minz_Request::isPost()) {
- $ok = true;
-
- $passwordPlain = Minz_Request::param('passwordPlain', false);
- if ($passwordPlain != '') {
- Minz_Request::_param('passwordPlain'); //Discard plain-text password ASAP
- $_POST['passwordPlain'] = '';
- if (!function_exists('password_hash')) {
- include_once(LIB_PATH . '/password_compat.php');
- }
- $passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST));
- $passwordPlain = '';
- $passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js
- $ok &= ($passwordHash != '');
- $this->view->conf->_passwordHash($passwordHash);
- }
- Minz_Session::_param('passwordHash', $this->view->conf->passwordHash);
-
- if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
- $this->view->conf->_mail_login(Minz_Request::param('mail_login', false));
- }
- $email = $this->view->conf->mail_login;
- Minz_Session::_param('mail', $email);
-
- $ok &= $this->view->conf->save();
-
- if ($email != '') {
- $personaFile = DATA_PATH . '/persona/' . $email . '.txt';
- @unlink($personaFile);
- $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');
- if ($anon != Minz_Configuration::allowAnonymous() ||
- $auth_type != Minz_Configuration::authType() ||
- $anon_refresh != Minz_Configuration::allowAnonymousRefresh()) {
-
- Minz_Configuration::_authType($auth_type);
- Minz_Configuration::_allowAnonymous($anon);
- Minz_Configuration::_allowAnonymousRefresh($anon_refresh);
- $ok &= Minz_Configuration::writeFile();
- }
- }
-
- invalidateHttpCache();
-
- $notif = array(
- 'type' => $ok ? 'good' : 'bad',
- 'content' => Minz_Translate::t($ok ? 'configuration_updated' : 'error_occurred')
- );
- Minz_Session::_param('notification', $notif);
- }
- Minz_Request::forward(array('c' => 'configure', 'a' => 'users'), true);
- }
-
- public function createAction() {
- if (Minz_Request::isPost() && Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
- require_once(APP_PATH . '/sql.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_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
-
- $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';
- $ok &= !file_exists($configPath);
- }
- if ($ok) {
-
- $passwordPlain = Minz_Request::param('new_user_passwordPlain', false);
- $passwordHash = '';
- if ($passwordPlain != '') {
- Minz_Request::_param('new_user_passwordPlain'); //Discard plain-text password ASAP
- $_POST['new_user_passwordPlain'] = '';
- if (!function_exists('password_hash')) {
- include_once(LIB_PATH . '/password_compat.php');
- }
- $passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST));
- $passwordPlain = '';
- $passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js
- $ok &= ($passwordHash != '');
- }
- if (empty($passwordHash)) {
- $passwordHash = '';
- }
-
- $new_user_email = filter_var($_POST['new_user_email'], FILTER_VALIDATE_EMAIL);
- if (empty($new_user_email)) {
- $new_user_email = '';
- } else {
- $personaFile = DATA_PATH . '/persona/' . $new_user_email . '.txt';
- @unlink($personaFile);
- $ok &= (file_put_contents($personaFile, $new_user_name) !== false);
- }
- }
- if ($ok) {
- $config_array = array(
- 'language' => $new_user_language,
- 'passwordHash' => $passwordHash,
- 'mail_login' => $new_user_email,
- );
- $ok &= (file_put_contents($configPath, "<?php\n return " . var_export($config_array, true) . ';') !== false);
- }
- if ($ok) {
- $userDAO = new FreshRSS_UserDAO();
- $ok &= $userDAO->createUser($new_user_name);
- }
- invalidateHttpCache();
-
- $notif = array(
- 'type' => $ok ? 'good' : 'bad',
- 'content' => Minz_Translate::t($ok ? 'user_created' : 'error_occurred', $new_user_name)
- );
- Minz_Session::_param('notification', $notif);
- }
- Minz_Request::forward(array('c' => 'configure', 'a' => 'users'), true);
- }
-
- public function deleteAction() {
- if (Minz_Request::isPost() && Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
- require_once(APP_PATH . '/sql.php');
-
- $username = Minz_Request::param('username');
- $ok = ctype_alnum($username);
-
- if ($ok) {
- $ok &= (strcasecmp($username, Minz_Configuration::defaultUser()) !== 0); //It is forbidden to delete the default user
- }
- if ($ok) {
- $configPath = DATA_PATH . '/' . $username . '_user.php';
- $ok &= file_exists($configPath);
- }
- if ($ok) {
- $userDAO = new FreshRSS_UserDAO();
- $ok &= $userDAO->deleteUser($username);
- $ok &= unlink($configPath);
- //TODO: delete Persona file
- }
- invalidateHttpCache();
-
- $notif = array(
- 'type' => $ok ? 'good' : 'bad',
- 'content' => Minz_Translate::t($ok ? 'user_deleted' : 'error_occurred', $username)
- );
- Minz_Session::_param('notification', $notif);
- }
- Minz_Request::forward(array('c' => 'configure', 'a' => 'users'), true);
- }
-}