aboutsummaryrefslogtreecommitdiff
path: root/app/Controllers
diff options
context:
space:
mode:
authorGravatar Marien Fressinaud <dev@marienfressinaud.fr> 2014-10-09 15:53:10 +0200
committerGravatar Marien Fressinaud <dev@marienfressinaud.fr> 2014-10-09 15:53:10 +0200
commitf97d4b3b6cca4a55636bbd50158f3c57666b0f08 (patch)
tree3ca9dd42155228292f0842d65b9b6d90e9140639 /app/Controllers
parente51ceb6812e3736aa9b9ce1f2d5181f5b4b6aaa3 (diff)
parent444b1552364b39761c3278c7da5152fd3998f216 (diff)
Merge branch 'master' into hotfixes
Diffstat (limited to 'app/Controllers')
-rwxr-xr-xapp/Controllers/configureController.php522
-rwxr-xr-xapp/Controllers/entryController.php167
-rw-r--r--app/Controllers/errorController.php38
-rwxr-xr-xapp/Controllers/feedController.php438
-rw-r--r--app/Controllers/importExportController.php447
-rwxr-xr-xapp/Controllers/indexController.php498
-rwxr-xr-xapp/Controllers/javascriptController.php46
-rw-r--r--app/Controllers/statsController.php129
-rw-r--r--app/Controllers/updateController.php129
-rw-r--r--app/Controllers/usersController.php203
10 files changed, 2617 insertions, 0 deletions
diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php
new file mode 100755
index 000000000..231865bd7
--- /dev/null
+++ b/app/Controllers/configureController.php
@@ -0,0 +1,522 @@
+<?php
+
+/**
+ * Controller to handle every configuration options.
+ */
+class FreshRSS_configure_Controller extends Minz_ActionController {
+ /**
+ * This action is called before every other action in that class. It is
+ * the common boiler plate for every action. It is triggered by the
+ * underlying framework.
+ *
+ * @todo see if the category default configuration is needed here or if
+ * we can move it to the categorize action
+ */
+ public function firstAction() {
+ if (!$this->view->loginOk) {
+ Minz_Error::error(
+ 403,
+ array('error' => array(_t('access_denied')))
+ );
+ }
+
+ $catDAO = new FreshRSS_CategoryDAO();
+ $catDAO->checkDefault();
+ }
+
+ /**
+ * This action handles the category configuration page
+ *
+ * It displays the category configuration page.
+ * If this action is reached through a POST request, it loops through
+ * every category to check for modification then add a new category if
+ * needed then sends a notification to the user.
+ * If a category name is emptied, the category is deleted and all
+ * related feeds are moved to the default category. Related user queries
+ * are deleted too.
+ * If a category name is changed, it is updated.
+ */
+ public function categorizeAction() {
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ $catDAO = new FreshRSS_CategoryDAO();
+ $defaultCategory = $catDAO->getDefault();
+ $defaultId = $defaultCategory->id();
+
+ if (Minz_Request::isPost()) {
+ $cats = Minz_Request::param('categories', array());
+ $ids = Minz_Request::param('ids', array());
+ $newCat = trim(Minz_Request::param('new_category', ''));
+
+ foreach ($cats as $key => $name) {
+ if (strlen($name) > 0) {
+ $cat = new FreshRSS_Category($name);
+ $values = array(
+ 'name' => $cat->name(),
+ );
+ $catDAO->updateCategory($ids[$key], $values);
+ } elseif ($ids[$key] != $defaultId) {
+ $feedDAO->changeCategory($ids[$key], $defaultId);
+ $catDAO->deleteCategory($ids[$key]);
+
+ // Remove related queries.
+ $this->view->conf->remove_query_by_get('c_' . $ids[$key]);
+ $this->view->conf->save();
+ }
+ }
+
+ if ($newCat != '') {
+ $cat = new FreshRSS_Category($newCat);
+ $values = array(
+ 'id' => $cat->id(),
+ 'name' => $cat->name(),
+ );
+
+ if ($catDAO->searchByName($newCat) == null) {
+ $catDAO->addCategory($values);
+ }
+ }
+ invalidateHttpCache();
+
+ Minz_Request::good(_t('categories_updated'),
+ array('c' => 'configure', 'a' => 'categorize'));
+ }
+
+ $this->view->categories = $catDAO->listCategories(false);
+ $this->view->defaultCategory = $catDAO->getDefault();
+ $this->view->feeds = $feedDAO->listFeeds();
+
+ Minz_View::prependTitle(_t('categories_management') . ' · ');
+ }
+
+ /**
+ * This action handles the feed configuration page.
+ *
+ * It displays the feed configuration page.
+ * If this action is reached through a POST request, it stores all new
+ * configuraiton values then sends a notification to the user.
+ *
+ * The options available on the page are:
+ * - name
+ * - description
+ * - website URL
+ * - feed URL
+ * - category id (default: default category id)
+ * - CSS path to article on website
+ * - display in main stream (default: 0)
+ * - HTTP authentication
+ * - number of article to retain (default: -2)
+ * - refresh frequency (default: -2)
+ * Default values are empty strings unless specified.
+ */
+ public function feedAction() {
+ $catDAO = new FreshRSS_CategoryDAO();
+ $this->view->categories = $catDAO->listCategories(false);
+
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ $this->view->feeds = $feedDAO->listFeeds();
+
+ $id = Minz_Request::param('id');
+ if ($id == false && !empty($this->view->feeds)) {
+ $id = current($this->view->feeds)->id();
+ }
+
+ $this->view->flux = false;
+ if ($id != false) {
+ $this->view->flux = $this->view->feeds[$id];
+
+ if (!$this->view->flux) {
+ Minz_Error::error(
+ 404,
+ array('error' => array(_t('page_not_found')))
+ );
+ } else {
+ if (Minz_Request::isPost() && $this->view->flux) {
+ $user = Minz_Request::param('http_user', '');
+ $pass = Minz_Request::param('http_pass', '');
+
+ $httpAuth = '';
+ if ($user != '' || $pass != '') {
+ $httpAuth = $user . ':' . $pass;
+ }
+
+ $cat = intval(Minz_Request::param('category', 0));
+
+ $values = array(
+ 'name' => Minz_Request::param('name', ''),
+ 'description' => sanitizeHTML(Minz_Request::param('description', '', true)),
+ 'website' => Minz_Request::param('website', ''),
+ 'url' => Minz_Request::param('url', ''),
+ 'category' => $cat,
+ 'pathEntries' => Minz_Request::param('path_entries', ''),
+ 'priority' => intval(Minz_Request::param('priority', 0)),
+ 'httpAuth' => $httpAuth,
+ 'keep_history' => intval(Minz_Request::param('keep_history', -2)),
+ 'ttl' => intval(Minz_Request::param('ttl', -2)),
+ );
+
+ if ($feedDAO->updateFeed($id, $values)) {
+ $this->view->flux->_category($cat);
+ $this->view->flux->faviconPrepare();
+ $notif = array(
+ 'type' => 'good',
+ 'content' => _t('feed_updated')
+ );
+ } else {
+ $notif = array(
+ 'type' => 'bad',
+ 'content' => _t('error_occurred_update')
+ );
+ }
+ invalidateHttpCache();
+
+ Minz_Session::_param('notification', $notif);
+ Minz_Request::forward(array('c' => 'configure', 'a' => 'feed', 'params' => array('id' => $id)), true);
+ }
+
+ Minz_View::prependTitle(_t('rss_feed_management') . ' — ' . $this->view->flux->name() . ' · ');
+ }
+ } else {
+ Minz_View::prependTitle(_t('rss_feed_management') . ' · ');
+ }
+ }
+
+ /**
+ * 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()) {
+ $this->view->conf->_language(Minz_Request::param('language', 'en'));
+ $this->view->conf->_theme(Minz_Request::param('theme', FreshRSS_Themes::$defaultTheme));
+ $this->view->conf->_content_width(Minz_Request::param('content_width', 'thin'));
+ $this->view->conf->_topline_read(Minz_Request::param('topline_read', false));
+ $this->view->conf->_topline_favorite(Minz_Request::param('topline_favorite', false));
+ $this->view->conf->_topline_date(Minz_Request::param('topline_date', false));
+ $this->view->conf->_topline_link(Minz_Request::param('topline_link', false));
+ $this->view->conf->_bottomline_read(Minz_Request::param('bottomline_read', false));
+ $this->view->conf->_bottomline_favorite(Minz_Request::param('bottomline_favorite', false));
+ $this->view->conf->_bottomline_sharing(Minz_Request::param('bottomline_sharing', false));
+ $this->view->conf->_bottomline_tags(Minz_Request::param('bottomline_tags', false));
+ $this->view->conf->_bottomline_date(Minz_Request::param('bottomline_date', false));
+ $this->view->conf->_bottomline_link(Minz_Request::param('bottomline_link', false));
+ $this->view->conf->_html5_notif_timeout(Minz_Request::param('html5_notif_timeout', 0));
+ $this->view->conf->save();
+
+ Minz_Session::_param('language', $this->view->conf->language);
+ Minz_Translate::reset();
+ invalidateHttpCache();
+
+ Minz_Request::good(_t('configuration_updated'),
+ array('c' => 'configure', 'a' => 'display'));
+ }
+
+ $this->view->themes = FreshRSS_Themes::get();
+
+ Minz_View::prependTitle(_t('display_configuration') . ' · ');
+ }
+
+ /**
+ * 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
+ * - 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->_posts_per_page(Minz_Request::param('posts_per_page', 10));
+ $this->view->conf->_view_mode(Minz_Request::param('view_mode', 'normal'));
+ $this->view->conf->_default_view((int)Minz_Request::param('default_view', FreshRSS_Entry::STATE_ALL));
+ $this->view->conf->_auto_load_more(Minz_Request::param('auto_load_more', false));
+ $this->view->conf->_display_posts(Minz_Request::param('display_posts', false));
+ $this->view->conf->_display_categories(Minz_Request::param('display_categories', false));
+ $this->view->conf->_hide_read_feeds(Minz_Request::param('hide_read_feeds', false));
+ $this->view->conf->_onread_jump_next(Minz_Request::param('onread_jump_next', false));
+ $this->view->conf->_lazyload(Minz_Request::param('lazyload', false));
+ $this->view->conf->_sticky_post(Minz_Request::param('sticky_post', false));
+ $this->view->conf->_reading_confirm(Minz_Request::param('reading_confirm', false));
+ $this->view->conf->_sort_order(Minz_Request::param('sort_order', 'DESC'));
+ $this->view->conf->_mark_when(array(
+ 'article' => Minz_Request::param('mark_open_article', false),
+ 'site' => Minz_Request::param('mark_open_site', false),
+ 'scroll' => Minz_Request::param('mark_scroll', false),
+ 'reception' => Minz_Request::param('mark_upon_reception', false),
+ ));
+ $this->view->conf->save();
+
+ Minz_Session::_param('language', $this->view->conf->language);
+ Minz_Translate::reset();
+ invalidateHttpCache();
+
+ Minz_Request::good(_t('configuration_updated'),
+ array('c' => 'configure', 'a' => 'reading'));
+ }
+
+ Minz_View::prependTitle(_t('reading_configuration') . ' · ');
+ }
+
+ /**
+ * 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::params();
+ $this->view->conf->_sharing($params['share']);
+ $this->view->conf->save();
+ invalidateHttpCache();
+
+ Minz_Request::good(_t('configuration_updated'),
+ array('c' => 'configure', 'a' => 'sharing'));
+ }
+
+ Minz_View::prependTitle(_t('sharing') . ' · ');
+ }
+
+ /**
+ * 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;
+
+ 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;
+ }
+ }
+
+ $this->view->conf->_shortcuts($shortcuts_ok);
+ $this->view->conf->save();
+ invalidateHttpCache();
+
+ Minz_Request::good(_t('shortcuts_updated'),
+ array('c' => 'configure', 'a' => 'shortcut'));
+ }
+
+ Minz_View::prependTitle(_t('shortcuts') . ' · ');
+ }
+
+ /**
+ * This action display the user configuration page
+ *
+ * @todo move that action in the user controller
+ */
+ public function usersAction() {
+ Minz_View::prependTitle(_t('users') . ' · ');
+ }
+
+ /**
+ * 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()) {
+ $this->view->conf->_old_entries(Minz_Request::param('old_entries', 3));
+ $this->view->conf->_keep_history_default(Minz_Request::param('keep_history_default', 0));
+ $this->view->conf->_ttl_default(Minz_Request::param('ttl_default', -2));
+ $this->view->conf->save();
+ invalidateHttpCache();
+
+ Minz_Request::good(_t('configuration_updated'),
+ array('c' => 'configure', 'a' => 'archiving'));
+ }
+
+ Minz_View::prependTitle(_t('archiving_configuration') . ' · ');
+
+ $entryDAO = FreshRSS_Factory::createEntryDao();
+ $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);
+ }
+ }
+
+ /**
+ * 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() {
+ if (Minz_Request::isPost()) {
+ $queries = Minz_Request::param('queries', array());
+
+ foreach ($queries as $key => $query) {
+ if (!$query['name']) {
+ $query['name'] = _t('query_number', $key + 1);
+ }
+ }
+ $this->view->conf->_queries($queries);
+ $this->view->conf->save();
+
+ Minz_Request::good(_t('configuration_updated'),
+ array('c' => 'configure', 'a' => 'queries'));
+ } else {
+ $this->view->query_get = array();
+ $cat_dao = new FreshRSS_CategoryDAO();
+ $feed_dao = FreshRSS_Factory::createFeedDao();
+ foreach ($this->view->conf->queries as $key => $query) {
+ if (!isset($query['get'])) {
+ continue;
+ }
+
+ switch ($query['get'][0]) {
+ case 'c':
+ $category = $cat_dao->searchById(substr($query['get'], 2));
+
+ $deprecated = true;
+ $cat_name = '';
+ if ($category) {
+ $cat_name = $category->name();
+ $deprecated = false;
+ }
+
+ $this->view->query_get[$key] = array(
+ 'type' => 'category',
+ 'name' => $cat_name,
+ 'deprecated' => $deprecated,
+ );
+ break;
+ case 'f':
+ $feed = $feed_dao->searchById(substr($query['get'], 2));
+
+ $deprecated = true;
+ $feed_name = '';
+ if ($feed) {
+ $feed_name = $feed->name();
+ $deprecated = false;
+ }
+
+ $this->view->query_get[$key] = array(
+ 'type' => 'feed',
+ 'name' => $feed_name,
+ 'deprecated' => $deprecated,
+ );
+ break;
+ case 's':
+ $this->view->query_get[$key] = array(
+ 'type' => 'favorite',
+ 'name' => 'favorite',
+ 'deprecated' => false,
+ );
+ break;
+ case 'a':
+ $this->view->query_get[$key] = array(
+ 'type' => 'all',
+ 'name' => 'all',
+ 'deprecated' => false,
+ );
+ break;
+ }
+ }
+ }
+
+ Minz_View::prependTitle(_t('queries') . ' · ');
+ }
+
+ /**
+ * 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() {
+ $whitelist = array('get', 'order', 'name', 'search', 'state');
+ $queries = $this->view->conf->queries;
+ $query = Minz_Request::params();
+ $query['name'] = _t('query_number', count($queries) + 1);
+ foreach ($query as $key => $value) {
+ if (!in_array($key, $whitelist)) {
+ unset($query[$key]);
+ }
+ }
+ if (!empty($query['state']) && $query['state'] & FreshRSS_Entry::STATE_STRICT) {
+ $query['state'] -= FreshRSS_Entry::STATE_STRICT;
+ }
+ $queries[] = $query;
+ $this->view->conf->_queries($queries);
+ $this->view->conf->save();
+
+ Minz_Request::good(_t('query_created', $query['name']),
+ array('c' => 'configure', 'a' => 'queries'));
+ }
+}
diff --git a/app/Controllers/entryController.php b/app/Controllers/entryController.php
new file mode 100755
index 000000000..ab66d9198
--- /dev/null
+++ b/app/Controllers/entryController.php
@@ -0,0 +1,167 @@
+<?php
+
+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->params = array ();
+ $output = Minz_Request::param('output', '');
+ if (($output != '') && ($this->view->conf->view_mode !== $output)) {
+ $this->params['output'] = $output;
+ }
+
+ $this->redirect = false;
+ $ajax = Minz_Request::param ('ajax');
+ if ($ajax) {
+ $this->view->_useLayout (false);
+ }
+ }
+
+ public function lastAction () {
+ $ajax = Minz_Request::param ('ajax');
+ if (!$ajax && $this->redirect) {
+ Minz_Request::forward (array (
+ 'c' => 'index',
+ 'a' => 'index',
+ 'params' => $this->params
+ ), true);
+ } else {
+ Minz_Request::_param ('ajax');
+ }
+ }
+
+ public function readAction () {
+ $this->redirect = true;
+
+ $id = Minz_Request::param ('id');
+ $get = Minz_Request::param ('get');
+ $nextGet = Minz_Request::param ('nextGet', $get);
+ $idMax = Minz_Request::param ('idMax', 0);
+
+ $entryDAO = FreshRSS_Factory::createEntryDao();
+ if ($id == false) {
+ if (!Minz_Request::isPost()) {
+ return;
+ }
+
+ if (!$get) {
+ $entryDAO->markReadEntries ($idMax);
+ } 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;
+ }
+ if ($nextGet !== 'a') {
+ $this->params['get'] = $nextGet;
+ }
+ }
+
+ $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);
+ }
+ }
+
+ public function bookmarkAction () {
+ $this->redirect = true;
+
+ $id = Minz_Request::param ('id');
+ if ($id) {
+ $entryDAO = FreshRSS_Factory::createEntryDao();
+ $entryDAO->markFavorite ($id, (bool)(Minz_Request::param ('is_favorite', true)));
+ }
+ }
+
+ 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 = FreshRSS_Factory::createEntryDao();
+ $entryDAO->optimizeTable();
+
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ $feedDAO->updateCachedValues();
+
+ invalidateHttpCache();
+
+ $notif = array (
+ 'type' => 'good',
+ 'content' => Minz_Translate::t ('optimization_complete')
+ );
+ Minz_Session::_param ('notification', $notif);
+ }
+
+ Minz_Request::forward(array(
+ 'c' => 'configure',
+ 'a' => 'archiving'
+ ), true);
+ }
+
+ public function purgeAction() {
+ @set_time_limit(300);
+
+ $nb_month_old = max($this->view->conf->old_entries, 1);
+ $date_min = time() - (3600 * 24 * 30 * $nb_month_old);
+
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ $feeds = $feedDAO->listFeeds();
+ $nbTotal = 0;
+
+ invalidateHttpCache();
+
+ foreach ($feeds as $feed) {
+ $feedHistory = $feed->keepHistory();
+ if ($feedHistory == -2) { //default
+ $feedHistory = $this->view->conf->keep_history_default;
+ }
+ if ($feedHistory >= 0) {
+ $nb = $feedDAO->cleanOldEntries($feed->id(), $date_min, $feedHistory);
+ if ($nb > 0) {
+ $nbTotal += $nb;
+ Minz_Log::record($nb . ' old entries cleaned in feed [' . $feed->url() . ']', Minz_Log::DEBUG);
+ //$feedDAO->updateLastUpdate($feed->id());
+ }
+ }
+ }
+
+ $feedDAO->updateCachedValues();
+
+ invalidateHttpCache();
+
+ $notif = array(
+ 'type' => 'good',
+ 'content' => Minz_Translate::t('purge_completed', $nbTotal)
+ );
+ Minz_Session::_param('notification', $notif);
+
+ Minz_Request::forward(array(
+ 'c' => 'configure',
+ 'a' => 'archiving'
+ ), true);
+ }
+}
diff --git a/app/Controllers/errorController.php b/app/Controllers/errorController.php
new file mode 100644
index 000000000..922650b3d
--- /dev/null
+++ b/app/Controllers/errorController.php
@@ -0,0 +1,38 @@
+<?php
+
+class FreshRSS_error_Controller extends Minz_ActionController {
+ public function indexAction() {
+ switch (Minz_Request::param('code')) {
+ case 403:
+ $this->view->code = 'Error 403 - Forbidden';
+ break;
+ case 404:
+ $this->view->code = 'Error 404 - Not found';
+ break;
+ case 500:
+ $this->view->code = 'Error 500 - Internal Server Error';
+ break;
+ case 503:
+ $this->view->code = 'Error 503 - Service Unavailable';
+ break;
+ default:
+ $this->view->code = 'Error 404 - Not found';
+ }
+
+ $errors = Minz_Request::param('logs', array());
+ $this->view->errorMessage = trim(implode($errors));
+ if ($this->view->errorMessage == '') {
+ switch(Minz_Request::param('code')) {
+ case 403:
+ $this->view->errorMessage = Minz_Translate::t('forbidden_access');
+ break;
+ case 404:
+ default:
+ $this->view->errorMessage = Minz_Translate::t('page_not_found');
+ break;
+ }
+ }
+
+ Minz_View::prependTitle($this->view->code . ' · ');
+ }
+}
diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php
new file mode 100755
index 000000000..c7cc25fbb
--- /dev/null
+++ b/app/Controllers/feedController.php
@@ -0,0 +1,438 @@
+<?php
+
+class FreshRSS_feed_Controller extends Minz_ActionController {
+ public function firstAction () {
+ if (!$this->view->loginOk) {
+ // 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_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')))
+ );
+ }
+ }
+ }
+
+ public function addAction () {
+ $url = Minz_Request::param('url_rss', false);
+
+ if ($url === false) {
+ Minz_Request::forward(array(
+ 'c' => 'configure',
+ 'a' => 'feed'
+ ), true);
+ }
+
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ $this->catDAO = new FreshRSS_CategoryDAO ();
+ $this->catDAO->checkDefault ();
+
+ if (Minz_Request::isPost()) {
+ @set_time_limit(300);
+
+
+ $cat = Minz_Request::param ('category', false);
+ if ($cat === 'nc') {
+ $new_cat = Minz_Request::param ('new_category');
+ if (empty($new_cat['name'])) {
+ $cat = false;
+ } else {
+ $cat = $this->catDAO->addCategory($new_cat);
+ }
+ }
+ if ($cat === false) {
+ $def_cat = $this->catDAO->getDefault ();
+ $cat = $def_cat->id ();
+ }
+
+ $user = Minz_Request::param ('http_user');
+ $pass = Minz_Request::param ('http_pass');
+ $params = array ();
+
+ $transactionStarted = false;
+ try {
+ $feed = new FreshRSS_Feed ($url);
+ $feed->_category ($cat);
+
+ $httpAuth = '';
+ if ($user != '' || $pass != '') {
+ $httpAuth = $user . ':' . $pass;
+ }
+ $feed->_httpAuth ($httpAuth);
+
+ $feed->load(true);
+
+ $values = array (
+ 'url' => $feed->url (),
+ 'category' => $feed->category (),
+ 'name' => $feed->name (),
+ 'website' => $feed->website (),
+ 'description' => $feed->description (),
+ 'lastUpdate' => time (),
+ 'httpAuth' => $feed->httpAuth (),
+ );
+
+ if ($feedDAO->searchByUrl ($values['url'])) {
+ // on est déjà abonné à ce flux
+ $notif = array (
+ 'type' => 'bad',
+ 'content' => Minz_Translate::t ('already_subscribed', $feed->name ())
+ );
+ Minz_Session::_param ('notification', $notif);
+ } else {
+ $id = $feedDAO->addFeed ($values);
+ if (!$id) {
+ // problème au niveau de la base de données
+ $notif = array (
+ 'type' => 'bad',
+ 'content' => Minz_Translate::t ('feed_not_added', $feed->name ())
+ );
+ Minz_Session::_param ('notification', $notif);
+ } else {
+ $feed->_id ($id);
+ $feed->faviconPrepare();
+
+ $is_read = $this->view->conf->mark_when['reception'] ? 1 : 0;
+
+ $entryDAO = FreshRSS_Factory::createEntryDao();
+ $entries = array_reverse($feed->entries()); //We want chronological order and SimplePie uses reverse order
+
+ // on calcule la date des articles les plus anciens qu'on accepte
+ $nb_month_old = $this->view->conf->old_entries;
+ $date_min = time () - (3600 * 24 * 30 * $nb_month_old);
+
+ //MySQL: http://docs.oracle.com/cd/E17952_01/refman-5.5-en/optimizing-innodb-transaction-management.html
+ //SQLite: http://stackoverflow.com/questions/1711631/how-do-i-improve-the-performance-of-sqlite
+ $preparedStatement = $entryDAO->addEntryPrepare();
+ $transactionStarted = true;
+ $feedDAO->beginTransaction();
+ // on ajoute les articles en masse sans vérification
+ foreach ($entries as $entry) {
+ $values = $entry->toArray();
+ $values['id_feed'] = $feed->id();
+ $values['id'] = min(time(), $entry->date(true)) . uSecString();
+ $values['is_read'] = $is_read;
+ $entryDAO->addEntry($values, $preparedStatement);
+ }
+ $feedDAO->updateLastUpdate($feed->id());
+ if ($transactionStarted) {
+ $feedDAO->commit();
+ }
+ $transactionStarted = false;
+
+ // ok, ajout terminé
+ $notif = array (
+ 'type' => 'good',
+ 'content' => Minz_Translate::t ('feed_added', $feed->name ())
+ );
+ Minz_Session::_param ('notification', $notif);
+
+ // permet de rediriger vers la page de conf du flux
+ $params['id'] = $feed->id ();
+ }
+ }
+ } catch (FreshRSS_BadUrl_Exception $e) {
+ Minz_Log::record ($e->getMessage (), Minz_Log::WARNING);
+ $notif = array (
+ 'type' => 'bad',
+ 'content' => Minz_Translate::t ('invalid_url', $url)
+ );
+ Minz_Session::_param ('notification', $notif);
+ } catch (FreshRSS_Feed_Exception $e) {
+ Minz_Log::record ($e->getMessage (), Minz_Log::WARNING);
+ $notif = array (
+ 'type' => 'bad',
+ 'content' => Minz_Translate::t ('internal_problem_feed', Minz_Url::display(array('a' => 'logs')))
+ );
+ Minz_Session::_param ('notification', $notif);
+ } catch (Minz_FileNotExistException $e) {
+ // Répertoire de cache n'existe pas
+ Minz_Log::record ($e->getMessage (), Minz_Log::ERROR);
+ $notif = array (
+ 'type' => 'bad',
+ 'content' => Minz_Translate::t ('internal_problem_feed', Minz_Url::display(array('a' => 'logs')))
+ );
+ Minz_Session::_param ('notification', $notif);
+ }
+ if ($transactionStarted) {
+ $feedDAO->rollBack ();
+ }
+
+ Minz_Request::forward (array ('c' => 'configure', 'a' => 'feed', 'params' => $params), true);
+ } else {
+
+ // GET request so we must ask confirmation to user
+ Minz_View::prependTitle(Minz_Translate::t('add_rss_feed') . ' · ');
+ $this->view->categories = $this->catDAO->listCategories();
+ $this->view->feed = new FreshRSS_Feed($url);
+ try {
+ // We try to get some more information about the feed
+ $this->view->feed->load(true);
+ $this->view->load_ok = true;
+ } catch (Exception $e) {
+ $this->view->load_ok = false;
+ }
+
+ $feed = $feedDAO->searchByUrl($this->view->feed->url());
+ if ($feed) {
+ // Already subscribe so we redirect to the feed configuration page
+ $notif = array(
+ 'type' => 'bad',
+ 'content' => Minz_Translate::t(
+ 'already_subscribed', $feed->name()
+ )
+ );
+ Minz_Session::_param('notification', $notif);
+
+ Minz_Request::forward(array(
+ 'c' => 'configure',
+ 'a' => 'feed',
+ 'params' => array(
+ 'id' => $feed->id()
+ )
+ ), true);
+ }
+ }
+ }
+
+ public function truncateAction () {
+ if (Minz_Request::isPost ()) {
+ $id = Minz_Request::param ('id');
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ $n = $feedDAO->truncate($id);
+ $notif = array(
+ 'type' => $n === false ? 'bad' : 'good',
+ 'content' => Minz_Translate::t ('n_entries_deleted', $n)
+ );
+ Minz_Session::_param ('notification', $notif);
+ invalidateHttpCache();
+ Minz_Request::forward (array ('c' => 'configure', 'a' => 'feed', 'params' => array('id' => $id)), true);
+ }
+ }
+
+ public function actualizeAction () {
+ @set_time_limit(300);
+
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ $entryDAO = FreshRSS_Factory::createEntryDao();
+
+ Minz_Session::_param('actualize_feeds', false);
+ $id = Minz_Request::param ('id');
+ $force = Minz_Request::param ('force', false);
+
+ // 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);
+ if ($feed) {
+ $feeds = array ($feed);
+ }
+ } else {
+ $feeds = $feedDAO->listFeedsOrderUpdate($this->view->conf->ttl_default);
+ }
+
+ // on calcule la date des articles les plus anciens qu'on accepte
+ $nb_month_old = max($this->view->conf->old_entries, 1);
+ $date_min = time () - (3600 * 24 * 30 * $nb_month_old);
+
+ $i = 0;
+ $flux_update = 0;
+ $is_read = $this->view->conf->mark_when['reception'] ? 1 : 0;
+ foreach ($feeds as $feed) {
+ if (!$feed->lock()) {
+ Minz_Log::record('Feed already being actualized: ' . $feed->url(), Minz_Log::NOTICE);
+ continue;
+ }
+ try {
+ $url = $feed->url();
+ $feedHistory = $feed->keepHistory();
+
+ $feed->load(false);
+ $entries = array_reverse($feed->entries()); //We want chronological order and SimplePie uses reverse order
+ $hasTransaction = false;
+
+ 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);
+
+ if ($feedHistory == -2) { //default
+ $feedHistory = $this->view->conf->keep_history_default;
+ }
+
+ $preparedStatement = $entryDAO->addEntryPrepare();
+ $hasTransaction = true;
+ $feedDAO->beginTransaction();
+
+ // On ne vérifie pas strictement que l'article n'est pas déjà en BDD
+ // La BDD refusera l'ajout car (id_feed, guid) doit être unique
+ foreach ($entries as $entry) {
+ $eDate = $entry->date(true);
+ if ((!isset($existingGuids[$entry->guid()])) &&
+ (($feedHistory != 0) || ($eDate >= $date_min))) {
+ $values = $entry->toArray();
+ //Use declared date at first import, otherwise use discovery date
+ $values['id'] = ($useDeclaredDate || $eDate < $date_min) ?
+ min(time(), $eDate) . uSecString() :
+ uTimeString();
+ $values['is_read'] = $is_read;
+ $entryDAO->addEntry($values, $preparedStatement);
+ }
+ }
+ }
+
+ 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);
+ }
+ }
+
+ // on indique que le flux vient d'être mis à jour en BDD
+ $feedDAO->updateLastUpdate ($feed->id (), 0, $hasTransaction);
+ if ($hasTransaction) {
+ $feedDAO->commit();
+ }
+ $flux_update++;
+ if (($feed->url() !== $url)) { //HTTP 301 Moved Permanently
+ Minz_Log::record('Feed ' . $url . ' moved permanently to ' . $feed->url(), Minz_Log::NOTICE);
+ $feedDAO->updateFeed($feed->id(), array('url' => $feed->url()));
+ }
+ } catch (FreshRSS_Feed_Exception $e) {
+ Minz_Log::record ($e->getMessage (), Minz_Log::NOTICE);
+ $feedDAO->updateLastUpdate ($feed->id (), 1);
+ }
+
+ $feed->faviconPrepare();
+ $feed->unlock();
+ 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) {
+ break;
+ }
+ }
+
+ $url = array ();
+ if ($flux_update === 1) {
+ // on a mis un seul flux à jour
+ $feed = reset ($feeds);
+ $notif = array (
+ 'type' => 'good',
+ 'content' => Minz_Translate::t ('feed_actualized', $feed->name ())
+ );
+ } elseif ($flux_update > 1) {
+ // plusieurs flux on été mis à jour
+ $notif = array (
+ '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')
+ );
+ }
+
+ 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 ());
+ }
+
+ 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 (
+ 'type' => 'good',
+ 'content' => Minz_Translate::t ('feeds_actualized')
+ );
+ Minz_Session::_param ('notification', $notif);
+ // et on désactive le layout car ne sert à rien
+ $this->view->_useLayout (false);
+ }
+ }
+
+ public function deleteAction () {
+ if (Minz_Request::isPost ()) {
+ $type = Minz_Request::param ('type', 'feed');
+ $id = Minz_Request::param ('id');
+
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ if ($type == 'category') {
+ // List feeds to remove then related user queries.
+ $feeds = $feedDAO->listByCategory($id);
+
+ if ($feedDAO->deleteFeedByCategory ($id)) {
+ // Remove related queries
+ foreach ($feeds as $feed) {
+ $this->view->conf->remove_query_by_get('f_' . $feed->id());
+ }
+ $this->view->conf->save();
+
+ $notif = array (
+ 'type' => 'good',
+ 'content' => Minz_Translate::t ('category_emptied')
+ );
+ //TODO: Delete old favicons
+ } else {
+ $notif = array (
+ 'type' => 'bad',
+ 'content' => Minz_Translate::t ('error_occured')
+ );
+ }
+ } else {
+ if ($feedDAO->deleteFeed ($id)) {
+ // Remove related queries
+ $this->view->conf->remove_query_by_get('f_' . $id);
+ $this->view->conf->save();
+
+ $notif = array (
+ 'type' => 'good',
+ 'content' => Minz_Translate::t ('feed_deleted')
+ );
+ //TODO: Delete old favicon
+ } else {
+ $notif = array (
+ 'type' => 'bad',
+ 'content' => Minz_Translate::t ('error_occured')
+ );
+ }
+ }
+
+ Minz_Session::_param ('notification', $notif);
+
+ $redirect_url = Minz_Request::param('r', false, true);
+ if ($redirect_url) {
+ Minz_Request::forward($redirect_url);
+ } elseif ($type == 'category') {
+ Minz_Request::forward(array ('c' => 'configure', 'a' => 'categorize'), true);
+ } else {
+ Minz_Request::forward(array ('c' => 'configure', 'a' => 'feed'), true);
+ }
+ }
+ }
+}
diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php
new file mode 100644
index 000000000..f329766b8
--- /dev/null
+++ b/app/Controllers/importExportController.php
@@ -0,0 +1,447 @@
+<?php
+
+class FreshRSS_importExport_Controller extends Minz_ActionController {
+ public function firstAction() {
+ if (!$this->view->loginOk) {
+ Minz_Error::error(
+ 403,
+ array('error' => array(_t('access_denied')))
+ );
+ }
+
+ require_once(LIB_PATH . '/lib_opml.php');
+
+ $this->catDAO = new FreshRSS_CategoryDAO();
+ $this->entryDAO = FreshRSS_Factory::createEntryDao();
+ $this->feedDAO = FreshRSS_Factory::createFeedDao();
+ }
+
+ public function indexAction() {
+ $this->view->categories = $this->catDAO->listCategories();
+ $this->view->feeds = $this->feedDAO->listFeeds();
+
+ Minz_View::prependTitle(_t('import_export') . ' · ');
+ }
+
+ 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::error('File cannot be uploaded. Error code: ' . $status_file);
+ Minz_Request::bad(_t('file_cannot_be_uploaded'),
+ array('c' => 'importExport', 'a' => 'index'));
+ }
+
+ @set_time_limit(300);
+
+ $type_file = $this->guessFileType($file['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($file['tmp_name']);
+
+ if (!is_resource($zip)) {
+ // zip_open cannot open file: something is wrong
+ Minz_Log::error('Zip archive cannot be imported. Error code: ' . $zip);
+ Minz_Request::bad(_t('zip_error'),
+ array('c' => 'importExport', 'a' => 'index'));
+ }
+
+ while (($zipfile = zip_read($zip)) !== false) {
+ if (!is_resource($zipfile)) {
+ // zip_entry() can also return an error code!
+ Minz_Log::error('Zip file cannot be imported. Error code: ' . $zipfile);
+ } else {
+ $type_zipfile = $this->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
+ Minz_Request::bad(_t('no_zip_extension'),
+ array('c' => 'importExport', 'a' => 'index'));
+ } elseif ($type_file !== 'unknown') {
+ $list_files[$type_file][] = file_get_contents($file['tmp_name']);
+ }
+
+ // 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.
+ $error = false;
+ foreach ($list_files['opml'] as $opml_file) {
+ $error = $this->importOpml($opml_file);
+ }
+ foreach ($list_files['json_starred'] as $article_file) {
+ $error = $this->importArticles($article_file, true);
+ }
+ foreach ($list_files['json_feed'] as $article_file) {
+ $error = $this->importArticles($article_file);
+ }
+
+ // And finally, we get import status and redirect to the home page
+ Minz_Session::_param('actualize_feeds', true);
+ $content_notif = $error === true ? _t('feeds_imported_with_errors') :
+ _t('feeds_imported');
+ Minz_Request::good($content_notif);
+ }
+
+ private function guessFileType($filename) {
+ // A *very* basic guess file type function. Only based on filename
+ // That's could be improved but should be enough, at least for a first
+ // implementation.
+
+ if (substr_compare($filename, '.zip', -4) === 0) {
+ return 'zip';
+ } elseif (substr_compare($filename, '.opml', -5) === 0 ||
+ 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';
+ }
+ }
+
+ private function importOpml($opml_file) {
+ $opml_array = array();
+ try {
+ $opml_array = libopml_parse_string($opml_file);
+ } catch (LibOPML_Exception $e) {
+ Minz_Log::warning($e->getMessage());
+ return true;
+ }
+
+ $this->catDAO->checkDefault();
+
+ return $this->addOpmlElements($opml_array['body']);
+ }
+
+ private function addOpmlElements($opml_elements, $parent_cat = null) {
+ $error = false;
+ foreach ($opml_elements as $elt) {
+ $res = false;
+ if (isset($elt['xmlUrl'])) {
+ $res = $this->addFeedOpml($elt, $parent_cat);
+ } else {
+ $res = $this->addCategoryOpml($elt, $parent_cat);
+ }
+
+ if (!$error && $res) {
+ // oops: there is at least one error!
+ $error = $res;
+ }
+ }
+
+ return $error;
+ }
+
+ private function addFeedOpml($feed_elt, $parent_cat) {
+ if (is_null($parent_cat)) {
+ // This feed has no parent category so we get the default one
+ $parent_cat = $this->catDAO->getDefault()->name();
+ }
+
+ $cat = $this->catDAO->searchByName($parent_cat);
+
+ if (!$cat) {
+ return true;
+ }
+
+ // 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);
+
+ // addFeedObject checks if feed is already in DB so nothing else to
+ // check here
+ $id = $this->feedDAO->addFeedObject($feed);
+ $error = ($id === false);
+ } catch (FreshRSS_Feed_Exception $e) {
+ Minz_Log::warning($e->getMessage());
+ $error = true;
+ }
+
+ return $error;
+ }
+
+ private function addCategoryOpml($cat_elt, $parent_cat) {
+ // Create a new Category object
+ $cat = new FreshRSS_Category(Minz_Helper::htmlspecialchars_utf8($cat_elt['text']));
+
+ $id = $this->catDAO->addCategoryObject($cat);
+ $error = ($id === false);
+
+ 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
+ $res = $this->addOpmlElements($cat_elt['@outlines'], $cat->name());
+ if (!$error && $res) {
+ $error = true;
+ }
+ }
+
+ return $error;
+ }
+
+ private function importArticles($article_file, $starred = false) {
+ $article_object = json_decode($article_file, true);
+ if (is_null($article_object)) {
+ Minz_Log::warning('Try to import a non-JSON file');
+ return true;
+ }
+
+ $is_read = $this->view->conf->mark_when['reception'] ? 1 : 0;
+
+ $google_compliant = (
+ strpos($article_object['id'], 'com.google') !== false
+ );
+
+ $error = false;
+ $article_to_feed = array();
+
+ // First, we check feeds of articles are in DB (and add them if needed).
+ foreach ($article_object['items'] as $item) {
+ $feed = $this->addFeedArticles($item['origin'], $google_compliant);
+ if (is_null($feed)) {
+ $error = true;
+ } else {
+ $article_to_feed[$item['id']] = $feed->id();
+ }
+ }
+
+ // Then, articles are imported.
+ $prepared_statement = $this->entryDAO->addEntryPrepare();
+ $this->entryDAO->beginTransaction();
+ foreach ($article_object['items'] as $item) {
+ if (!isset($article_to_feed[$item['id']])) {
+ 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) {
+ $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);
+
+ $values = $entry->toArray();
+ $id = $this->entryDAO->addEntry($values, $prepared_statement);
+
+ if (!$error && ($id === false)) {
+ $error = true;
+ }
+ }
+ $this->entryDAO->commit();
+
+ return $error;
+ }
+
+ private function addFeedArticles($origin, $google_compliant) {
+ $default_cat = $this->catDAO->getDefault();
+
+ $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 DB
+ $feed = new FreshRSS_Feed($url);
+ $feed->_category($default_cat->id());
+ $feed->_name($name);
+ $feed->_website($website);
+
+ // addFeedObject checks if feed is already in DB so nothing else to
+ // check here
+ $id = $this->feedDAO->addFeedObject($feed);
+
+ if ($id !== false) {
+ $feed->_id($id);
+ $return = $feed;
+ }
+ } catch (FreshRSS_Feed_Exception $e) {
+ Minz_Log::warning($e->getMessage());
+ }
+
+ return $return;
+ }
+
+ public function exportAction() {
+ if (!Minz_Request::isPost()) {
+ Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
+ }
+
+ $this->view->_useLayout(false);
+
+ $export_opml = Minz_Request::param('export_opml', false);
+ $export_starred = Minz_Request::param('export_starred', false);
+ $export_feeds = Minz_Request::param('export_feeds', array());
+
+ $export_files = array();
+ if ($export_opml) {
+ $export_files['feeds.opml'] = $this->generateOpml();
+ }
+
+ if ($export_starred) {
+ $export_files['starred.json'] = $this->generateArticles('starred');
+ }
+
+ foreach ($export_feeds as $feed_id) {
+ $feed = $this->feedDAO->searchById($feed_id);
+ if ($feed) {
+ $filename = 'feed_' . $feed->category() . '_'
+ . $feed->id() . '.json';
+ $export_files[$filename] = $this->generateArticles(
+ 'feed', $feed
+ );
+ }
+ }
+
+ $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->exportZip($export_files);
+ } catch (Exception $e) {
+ # Oops, there is no Zip extension!
+ Minz_Request::bad(_t('export_no_zip_extension'),
+ array('c' => 'importExport', 'a' => 'index'));
+ }
+ } elseif ($nb_files === 1) {
+ // Only one file? Guess its type and export it.
+ $filename = key($export_files);
+ $type = $this->guessFileType($filename);
+ $this->exportFile('freshrss_' . $filename, $export_files[$filename], $type);
+ } else {
+ Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
+ }
+ }
+
+ 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');
+ }
+
+ private function generateArticles($type, $feed = NULL) {
+ $this->view->categories = $this->catDAO->listCategories();
+
+ if ($type == 'starred') {
+ $this->view->list_title = _t('starred_list');
+ $this->view->type = 'starred';
+ $unread_fav = $this->entryDAO->countUnreadReadFavorites();
+ $this->view->entries = $this->entryDAO->listWhere(
+ 's', '', FreshRSS_Entry::STATE_ALL, 'ASC',
+ $unread_fav['all']
+ );
+ } elseif ($type == 'feed' && !is_null($feed)) {
+ $this->view->list_title = _t('feed_list', $feed->name());
+ $this->view->type = 'feed/' . $feed->id();
+ $this->view->entries = $this->entryDAO->listWhere(
+ 'f', $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC',
+ $this->view->conf->posts_per_page
+ );
+ $this->view->feed = $feed;
+ }
+
+ return $this->view->helperToString('export/articles');
+ }
+
+ private function exportZip($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));
+ header('Content-Disposition: attachment; filename="freshrss_export.zip"');
+ readfile($zip_file);
+ unlink($zip_file);
+ }
+
+ private function exportFile($filename, $content, $type) {
+ if ($type === 'unknown') {
+ return;
+ }
+
+ $content_type = '';
+ if ($type === 'opml') {
+ $content_type = "text/opml";
+ } elseif ($type === 'json_feed' || $type === 'json_starred') {
+ $content_type = "text/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
new file mode 100755
index 000000000..e8e26b142
--- /dev/null
+++ b/app/Controllers/indexController.php
@@ -0,0 +1,498 @@
+<?php
+
+class FreshRSS_index_Controller extends Minz_ActionController {
+ private $nb_not_read_cat = 0;
+
+ public function indexAction () {
+ $output = Minz_Request::param ('output');
+ $token = $this->view->conf->token;
+
+ // check if user is logged in
+ if (!$this->view->loginOk && !Minz_Configuration::allowAnonymous()) {
+ $token_param = Minz_Request::param ('token', '');
+ $token_is_ok = ($token != '' && $token === $token_param);
+ if ($output === 'rss' && !$token_is_ok) {
+ Minz_Error::error (
+ 403,
+ array ('error' => array (Minz_Translate::t ('access_denied')))
+ );
+ return;
+ } elseif ($output !== 'rss') {
+ // "hard" redirection is not required, just ask dispatcher to
+ // forward to the login form without 302 redirection
+ Minz_Request::forward(array('c' => 'index', 'a' => 'formLogin'));
+ return;
+ }
+ }
+
+ $params = Minz_Request::params ();
+ if (isset ($params['search'])) {
+ $params['search'] = urlencode ($params['search']);
+ }
+
+ $this->view->url = array (
+ 'c' => 'index',
+ 'a' => 'index',
+ 'params' => $params
+ );
+
+ if ($output === 'rss') {
+ // no layout for RSS output
+ $this->view->_useLayout (false);
+ header('Content-Type: application/rss+xml; charset=utf-8');
+ } elseif ($output === 'global') {
+ Minz_View::appendScript (Minz_Url::display ('/scripts/global_view.js?' . @filemtime(PUBLIC_PATH . '/scripts/global_view.js')));
+ }
+
+ $catDAO = new FreshRSS_CategoryDAO();
+ $entryDAO = FreshRSS_Factory::createEntryDao();
+
+ $this->view->cat_aside = $catDAO->listCategories ();
+ $this->view->nb_favorites = $entryDAO->countUnreadReadFavorites ();
+ $this->view->nb_not_read = FreshRSS_CategoryDAO::CountUnreads($this->view->cat_aside, 1);
+ $this->view->currentName = '';
+
+ $this->view->get_c = '';
+ $this->view->get_f = '';
+
+ $get = Minz_Request::param ('get', 'a');
+ $getType = $get[0];
+ $getId = substr ($get, 2);
+ if (!$this->checkAndProcessType ($getType, $getId)) {
+ Minz_Log::record ('Not found [' . $getType . '][' . $getId . ']', Minz_Log::DEBUG);
+ Minz_Error::error (
+ 404,
+ array ('error' => array (Minz_Translate::t ('page_not_found')))
+ );
+ return;
+ }
+
+ // mise à jour des titres
+ $this->view->rss_title = $this->view->currentName . ' | ' . Minz_View::title();
+ Minz_View::prependTitle(
+ ($this->nb_not_read_cat > 0 ? '(' . formatNumber($this->nb_not_read_cat) . ') ' : '') .
+ $this->view->currentName .
+ ' · '
+ );
+
+ // On récupère les différents éléments de filtrage
+ $this->view->state = Minz_Request::param('state', $this->view->conf->default_view);
+ $state_param = Minz_Request::param ('state', null);
+ $filter = Minz_Request::param ('search', '');
+ $this->view->order = $order = Minz_Request::param ('order', $this->view->conf->sort_order);
+ $nb = Minz_Request::param ('nb', $this->view->conf->posts_per_page);
+ $first = Minz_Request::param ('next', '');
+
+ $ajax_request = Minz_Request::param('ajax', false);
+ if ($output === 'reader') {
+ $nb = max(1, round($nb / 2));
+ }
+
+ if ($this->view->state === FreshRSS_Entry::STATE_NOT_READ) { //Any unread article in this category at all?
+ switch ($getType) {
+ case 'a':
+ $hasUnread = $this->view->nb_not_read > 0;
+ break;
+ case 's':
+ // This is deprecated. The favorite button does not exist anymore
+ $hasUnread = $this->view->nb_favorites['unread'] > 0;
+ break;
+ case 'c':
+ $hasUnread = (!isset($this->view->cat_aside[$getId])) || ($this->view->cat_aside[$getId]->nbNotRead() > 0);
+ break;
+ case 'f':
+ $myFeed = FreshRSS_CategoryDAO::findFeed($this->view->cat_aside, $getId);
+ $hasUnread = ($myFeed === null) || ($myFeed->nbNotRead() > 0);
+ break;
+ default:
+ $hasUnread = true;
+ break;
+ }
+ if (!$hasUnread && ($state_param === null)) {
+ $this->view->state = FreshRSS_Entry::STATE_ALL;
+ }
+ }
+
+ $today = @strtotime('today');
+ $this->view->today = $today;
+
+ // on calcule la date des articles les plus anciens qu'on affiche
+ $nb_month_old = $this->view->conf->old_entries;
+ $date_min = $today - (3600 * 24 * 30 * $nb_month_old); //Do not use a fast changing value such as time() to allow SQL caching
+ $keepHistoryDefault = $this->view->conf->keep_history_default;
+
+ try {
+ $entries = $entryDAO->listWhere($getType, $getId, $this->view->state, $order, $nb + 1, $first, $filter, $date_min, true, $keepHistoryDefault);
+
+ // Si on a récupéré aucun article "non lus"
+ // on essaye de récupérer tous les articles
+ if ($this->view->state === FreshRSS_Entry::STATE_NOT_READ && empty($entries) && ($state_param === null) && ($filter == '')) {
+ Minz_Log::record('Conflicting information about nbNotRead!', Minz_Log::DEBUG);
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ try {
+ $feedDAO->updateCachedValues();
+ } catch (Exception $ex) {
+ Minz_Log::record('Failed to automatically correct nbNotRead! ' + $ex->getMessage(), Minz_Log::NOTICE);
+ }
+ $this->view->state = FreshRSS_Entry::STATE_ALL;
+ $entries = $entryDAO->listWhere($getType, $getId, $this->view->state, $order, $nb, $first, $filter, $date_min, true, $keepHistoryDefault);
+ }
+ Minz_Request::_param('state', $this->view->state);
+
+ if (count($entries) <= $nb) {
+ $this->view->nextId = '';
+ } else { //We have more elements for pagination
+ $lastEntry = array_pop($entries);
+ $this->view->nextId = $lastEntry->id();
+ }
+
+ $this->view->entries = $entries;
+ } catch (FreshRSS_EntriesGetter_Exception $e) {
+ Minz_Log::record ($e->getMessage (), Minz_Log::NOTICE);
+ Minz_Error::error (
+ 404,
+ array ('error' => array (Minz_Translate::t ('page_not_found')))
+ );
+ }
+ }
+
+ /*
+ * Vérifie que la catégorie / flux sélectionné existe
+ * + Initialise correctement les variables de vue get_c et get_f
+ * + Met à jour la variable $this->nb_not_read_cat
+ */
+ private function checkAndProcessType ($getType, $getId) {
+ switch ($getType) {
+ case 'a':
+ $this->view->currentName = Minz_Translate::t ('your_rss_feeds');
+ $this->nb_not_read_cat = $this->view->nb_not_read;
+ $this->view->get_c = $getType;
+ return true;
+ case 's':
+ $this->view->currentName = Minz_Translate::t ('your_favorites');
+ $this->nb_not_read_cat = $this->view->nb_favorites['unread'];
+ $this->view->get_c = $getType;
+ return true;
+ case 'c':
+ $cat = isset($this->view->cat_aside[$getId]) ? $this->view->cat_aside[$getId] : null;
+ if ($cat === null) {
+ $catDAO = new FreshRSS_CategoryDAO();
+ $cat = $catDAO->searchById($getId);
+ }
+ if ($cat) {
+ $this->view->currentName = $cat->name ();
+ $this->nb_not_read_cat = $cat->nbNotRead ();
+ $this->view->get_c = $getId;
+ return true;
+ } else {
+ return false;
+ }
+ case 'f':
+ $feed = FreshRSS_CategoryDAO::findFeed($this->view->cat_aside, $getId);
+ if (empty($feed)) {
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ $feed = $feedDAO->searchById($getId);
+ }
+ if ($feed) {
+ $this->view->currentName = $feed->name ();
+ $this->nb_not_read_cat = $feed->nbNotRead ();
+ $this->view->get_f = $getId;
+ $this->view->get_c = $feed->category ();
+ return true;
+ } else {
+ return false;
+ }
+ default:
+ return false;
+ }
+ }
+
+ public function aboutAction () {
+ Minz_View::prependTitle (Minz_Translate::t ('about') . ' · ');
+ }
+
+ public function logsAction () {
+ if (!$this->view->loginOk) {
+ Minz_Error::error (
+ 403,
+ array ('error' => array (Minz_Translate::t ('access_denied')))
+ );
+ }
+
+ Minz_View::prependTitle (Minz_Translate::t ('logs') . ' · ');
+
+ if (Minz_Request::isPost ()) {
+ FreshRSS_LogDAO::truncate();
+ }
+
+ $logs = FreshRSS_LogDAO::lines(); //TODO: ask only the necessary lines
+
+ //gestion pagination
+ $page = Minz_Request::param ('page', 1);
+ $this->view->logsPaginator = new Minz_Paginator ($logs);
+ $this->view->logsPaginator->_nbItemsPerPage (50);
+ $this->view->logsPaginator->_currentPage ($page);
+ }
+
+ public function loginAction () {
+ $this->view->_useLayout (false);
+
+ $url = 'https://verifier.login.persona.org/verify';
+ $assert = Minz_Request::param ('assertion');
+ $params = 'assertion=' . $assert . '&audience=' .
+ urlencode (Minz_Url::display (null, 'php', true));
+ $ch = curl_init ();
+ $options = array (
+ CURLOPT_URL => $url,
+ CURLOPT_RETURNTRANSFER => TRUE,
+ CURLOPT_POST => 2,
+ CURLOPT_POSTFIELDS => $params
+ );
+ curl_setopt_array ($ch, $options);
+ $result = curl_exec ($ch);
+ curl_close ($ch);
+
+ $res = json_decode ($result, true);
+
+ $loginOk = false;
+ $reason = '';
+ if ($res['status'] === 'okay') {
+ $email = filter_var($res['email'], FILTER_VALIDATE_EMAIL);
+ if ($email != '') {
+ $personaFile = DATA_PATH . '/persona/' . $email . '.txt';
+ if (($currentUser = @file_get_contents($personaFile)) !== false) {
+ $currentUser = trim($currentUser);
+ if (ctype_alnum($currentUser)) {
+ try {
+ $this->conf = new FreshRSS_Configuration($currentUser);
+ $loginOk = strcasecmp($email, $this->conf->mail_login) === 0;
+ } catch (Minz_Exception $e) {
+ $reason = 'Invalid configuration for user [' . $currentUser . ']! ' . $e->getMessage(); //Permission denied or conf file does not exist
+ }
+ } else {
+ $reason = 'Invalid username format [' . $currentUser . ']!';
+ }
+ }
+ } else {
+ $reason = 'Invalid email format [' . $res['email'] . ']!';
+ }
+ }
+ if ($loginOk) {
+ Minz_Session::_param('currentUser', $currentUser);
+ Minz_Session::_param ('mail', $email);
+ $this->view->loginOk = true;
+ invalidateHttpCache();
+ } else {
+ $res = array ();
+ $res['status'] = 'failure';
+ $res['reason'] = $reason == '' ? Minz_Translate::t ('invalid_login') : $reason;
+ Minz_Log::record ('Persona: ' . $res['reason'], Minz_Log::WARNING);
+ }
+
+ header('Content-Type: application/json; charset=UTF-8');
+ $this->view->res = json_encode ($res);
+ }
+
+ public function logoutAction () {
+ $this->view->_useLayout(false);
+ invalidateHttpCache();
+ Minz_Session::_param('currentUser');
+ Minz_Session::_param('mail');
+ Minz_Session::_param('passwordHash');
+ }
+
+ private static function makeLongTermCookie($username, $passwordHash) {
+ do {
+ $token = sha1(Minz_Configuration::salt() . $username . uniqid(mt_rand(), true));
+ $tokenFile = DATA_PATH . '/tokens/' . $token . '.txt';
+ } while (file_exists($tokenFile));
+ if (@file_put_contents($tokenFile, $username . "\t" . $passwordHash) === false) {
+ return false;
+ }
+ $expire = time() + 2629744; //1 month //TODO: Use a configuration instead
+ Minz_Session::setLongTermCookie('FreshRSS_login', $token, $expire);
+ Minz_Session::_param('token', $token);
+ return $token;
+ }
+
+ private static function deleteLongTermCookie() {
+ Minz_Session::deleteLongTermCookie('FreshRSS_login');
+ $token = Minz_Session::param('token', null);
+ if (ctype_alnum($token)) {
+ @unlink(DATA_PATH . '/tokens/' . $token . '.txt');
+ }
+ Minz_Session::_param('token');
+ if (rand(0, 10) === 1) {
+ self::purgeTokens();
+ }
+ }
+
+ private static function purgeTokens() {
+ $oldest = time() - 2629744; //1 month //TODO: Use a configuration instead
+ foreach (new DirectoryIterator(DATA_PATH . '/tokens/') as $fileInfo) {
+ if ($fileInfo->getExtension() === 'txt' && $fileInfo->getMTime() < $oldest) {
+ @unlink($fileInfo->getPathname());
+ }
+ }
+ }
+
+ public function formLoginAction () {
+ if ($this->view->loginOk) {
+ Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
+ }
+
+ if (Minz_Request::isPost()) {
+ $ok = false;
+ $nonce = Minz_Session::param('nonce');
+ $username = Minz_Request::param('username', '');
+ $c = Minz_Request::param('challenge', '');
+ if (ctype_alnum($username) && ctype_graph($c) && ctype_alnum($nonce)) {
+ if (!function_exists('password_verify')) {
+ include_once(LIB_PATH . '/password_compat.php');
+ }
+ try {
+ $conf = new FreshRSS_Configuration($username);
+ $s = $conf->passwordHash;
+ $ok = password_verify($nonce . $s, $c);
+ if ($ok) {
+ Minz_Session::_param('currentUser', $username);
+ Minz_Session::_param('passwordHash', $s);
+ if (Minz_Request::param('keep_logged_in', false)) {
+ self::makeLongTermCookie($username, $s);
+ } else {
+ self::deleteLongTermCookie();
+ }
+ } else {
+ Minz_Log::record('Password mismatch for user ' . $username . ', nonce=' . $nonce . ', c=' . $c, Minz_Log::WARNING);
+ }
+ } catch (Minz_Exception $me) {
+ Minz_Log::record('Login failure: ' . $me->getMessage(), Minz_Log::WARNING);
+ }
+ } else {
+ Minz_Log::record('Invalid credential parameters: user=' . $username . ' challenge=' . $c . ' nonce=' . $nonce, Minz_Log::DEBUG);
+ }
+ if (!$ok) {
+ $notif = array(
+ 'type' => 'bad',
+ 'content' => Minz_Translate::t('invalid_login')
+ );
+ Minz_Session::_param('notification', $notif);
+ }
+ $this->view->_useLayout(false);
+ Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
+ } elseif (Minz_Configuration::unsafeAutologinEnabled() && isset($_GET['u']) && isset($_GET['p'])) {
+ Minz_Session::_param('currentUser');
+ Minz_Session::_param('mail');
+ Minz_Session::_param('passwordHash');
+ $username = ctype_alnum($_GET['u']) ? $_GET['u'] : '';
+ $passwordPlain = $_GET['p'];
+ Minz_Request::_param('p'); //Discard plain-text password ASAP
+ $_GET['p'] = '';
+ if (!function_exists('password_verify')) {
+ include_once(LIB_PATH . '/password_compat.php');
+ }
+ try {
+ $conf = new FreshRSS_Configuration($username);
+ $s = $conf->passwordHash;
+ $ok = password_verify($passwordPlain, $s);
+ unset($passwordPlain);
+ if ($ok) {
+ Minz_Session::_param('currentUser', $username);
+ Minz_Session::_param('passwordHash', $s);
+ } else {
+ Minz_Log::record('Unsafe password mismatch for user ' . $username, Minz_Log::WARNING);
+ }
+ } catch (Minz_Exception $me) {
+ Minz_Log::record('Unsafe login failure: ' . $me->getMessage(), Minz_Log::WARNING);
+ }
+ Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
+ } elseif (!Minz_Configuration::canLogIn()) {
+ Minz_Error::error (
+ 403,
+ array ('error' => array (Minz_Translate::t ('access_denied')))
+ );
+ }
+ invalidateHttpCache();
+ }
+
+ public function formLogoutAction () {
+ $this->view->_useLayout(false);
+ invalidateHttpCache();
+ Minz_Session::_param('currentUser');
+ Minz_Session::_param('mail');
+ Minz_Session::_param('passwordHash');
+ self::deleteLongTermCookie();
+ Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
+ }
+
+ public function resetAuthAction() {
+ Minz_View::prependTitle(_t('auth_reset') . ' · ');
+ Minz_View::appendScript(Minz_Url::display(
+ '/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js')
+ ));
+
+ $this->view->no_form = false;
+ // Enable changement of auth only if Persona!
+ if (Minz_Configuration::authType() != 'persona') {
+ $this->view->message = array(
+ 'status' => 'bad',
+ 'title' => _t('damn'),
+ 'body' => _t('auth_not_persona')
+ );
+ $this->view->no_form = true;
+ return;
+ }
+
+ $conf = new FreshRSS_Configuration(Minz_Configuration::defaultUser());
+ // Admin user must have set its master password.
+ if (!$conf->passwordHash) {
+ $this->view->message = array(
+ 'status' => 'bad',
+ 'title' => _t('damn'),
+ 'body' => _t('auth_no_password_set')
+ );
+ $this->view->no_form = true;
+ return;
+ }
+
+ invalidateHttpCache();
+
+ if (Minz_Request::isPost()) {
+ $nonce = Minz_Session::param('nonce');
+ $username = Minz_Request::param('username', '');
+ $c = Minz_Request::param('challenge', '');
+ if (!(ctype_alnum($username) && ctype_graph($c) && ctype_alnum($nonce))) {
+ Minz_Log::debug('Invalid credential parameters:' .
+ ' user=' . $username .
+ ' challenge=' . $c .
+ ' nonce=' . $nonce);
+ Minz_Request::bad(_t('invalid_login'),
+ array('c' => 'index', 'a' => 'resetAuth'));
+ }
+
+ if (!function_exists('password_verify')) {
+ include_once(LIB_PATH . '/password_compat.php');
+ }
+
+ $s = $conf->passwordHash;
+ $ok = password_verify($nonce . $s, $c);
+ if ($ok) {
+ Minz_Configuration::_authType('form');
+ $ok = Minz_Configuration::writeFile();
+
+ if ($ok) {
+ Minz_Request::good(_t('auth_form_set'));
+ } else {
+ Minz_Request::bad(_t('auth_form_not_set'),
+ array('c' => 'index', 'a' => 'resetAuth'));
+ }
+ } else {
+ Minz_Log::debug('Password mismatch for user ' . $username .
+ ', nonce=' . $nonce . ', c=' . $c);
+
+ Minz_Request::bad(_t('invalid_login'),
+ array('c' => 'index', 'a' => 'resetAuth'));
+ }
+ }
+ }
+}
diff --git a/app/Controllers/javascriptController.php b/app/Controllers/javascriptController.php
new file mode 100755
index 000000000..67148350f
--- /dev/null
+++ b/app/Controllers/javascriptController.php
@@ -0,0 +1,46 @@
+<?php
+
+class FreshRSS_javascript_Controller extends Minz_ActionController {
+ public function firstAction () {
+ $this->view->_useLayout (false);
+ }
+
+ public function actualizeAction () {
+ header('Content-Type: text/javascript; charset=UTF-8');
+ $feedDAO = FreshRSS_Factory::createFeedDao();
+ $this->view->feeds = $feedDAO->listFeedsOrderUpdate($this->view->conf->ttl_default);
+ }
+
+ public function nbUnreadsPerFeedAction() {
+ header('Content-Type: application/json; charset=UTF-8');
+ $catDAO = new FreshRSS_CategoryDAO();
+ $this->view->categories = $catDAO->listCategories(true, false);
+ }
+
+ //For Web-form login
+ public function nonceAction() {
+ header('Content-Type: application/json; charset=UTF-8');
+ header('Last-Modified: ' . gmdate('D, d M Y H:i:s \G\M\T'));
+ header('Expires: 0');
+ header('Cache-Control: private, no-cache, no-store, must-revalidate');
+ header('Pragma: no-cache');
+
+ $user = isset($_GET['user']) ? $_GET['user'] : '';
+ if (ctype_alnum($user)) {
+ try {
+ $conf = new FreshRSS_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));
+ Minz_Session::_param('nonce', $this->view->nonce);
+ return; //Success
+ }
+ } catch (Minz_Exception $me) {
+ Minz_Log::record('Nonce failure: ' . $me->getMessage(), Minz_Log::WARNING);
+ }
+ }
+ $this->view->nonce = ''; //Failure
+ $this->view->salt1 = '';
+ }
+}
diff --git a/app/Controllers/statsController.php b/app/Controllers/statsController.php
new file mode 100644
index 000000000..256543f37
--- /dev/null
+++ b/app/Controllers/statsController.php
@@ -0,0 +1,129 @@
+<?php
+
+/**
+ * Controller to handle application statistics.
+ */
+class FreshRSS_stats_Controller extends Minz_ActionController {
+
+ /**
+ * 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();
+ $this->view->count = $statsDAO->calculateEntryCount();
+ $this->view->feedByCategory = $statsDAO->calculateFeedByCategory();
+ $this->view->entryByCategory = $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->repartitionHour = $statsDAO->calculateEntryRepartitionPerFeedPerHour($id);
+ $this->view->averageHour = $statsDAO->calculateEntryAveragePerFeedPerHour($id);
+ $this->view->repartitionDayOfWeek = $statsDAO->calculateEntryRepartitionPerFeedPerDayOfWeek($id);
+ $this->view->averageDayOfWeek = $statsDAO->calculateEntryAveragePerFeedPerDayOfWeek($id);
+ $this->view->repartitionMonth = $statsDAO->calculateEntryRepartitionPerFeedPerMonth($id);
+ $this->view->averageMonth = $statsDAO->calculateEntryAveragePerFeedPerMonth($id);
+ }
+
+ /**
+ * This action is called before every other action in that class. It is
+ * the common boiler plate for every action. It is triggered by the
+ * underlying framework.
+ */
+ public function firstAction() {
+ if (!$this->view->loginOk) {
+ Minz_Error::error(
+ 403, array('error' => array(Minz_Translate::t('access_denied')))
+ );
+ }
+
+ Minz_View::prependTitle(Minz_Translate::t('stats') . ' · ');
+ }
+
+}
diff --git a/app/Controllers/updateController.php b/app/Controllers/updateController.php
new file mode 100644
index 000000000..da5bddc65
--- /dev/null
+++ b/app/Controllers/updateController.php
@@ -0,0 +1,129 @@
+<?php
+
+class FreshRSS_update_Controller extends Minz_ActionController {
+ public function firstAction() {
+ $current_user = Minz_Session::param('currentUser', '');
+ if (!$this->view->loginOk && Minz_Configuration::isAdmin($current_user)) {
+ Minz_Error::error(
+ 403,
+ array('error' => array(_t('access_denied')))
+ );
+ }
+
+ invalidateHttpCache();
+
+ Minz_View::prependTitle(_t('update_system') . ' · ');
+ $this->view->update_to_apply = false;
+ $this->view->last_update_time = 'unknown';
+ $this->view->check_last_hour = false;
+ $timestamp = (int)@file_get_contents(DATA_PATH . '/last_update.txt');
+ if (is_numeric($timestamp) && $timestamp > 0) {
+ $this->view->last_update_time = timestamptodate($timestamp);
+ $this->view->check_last_hour = (time() - 3600) <= $timestamp;
+ }
+ }
+
+ public function indexAction() {
+ if (file_exists(UPDATE_FILENAME) && !is_writable(FRESHRSS_PATH)) {
+ $this->view->message = array(
+ 'status' => 'bad',
+ 'title' => _t('damn'),
+ 'body' => _t('file_is_nok', FRESHRSS_PATH)
+ );
+ } elseif (file_exists(UPDATE_FILENAME)) {
+ // There is an update file to apply!
+ $this->view->update_to_apply = true;
+ $this->view->message = array(
+ 'status' => 'good',
+ 'title' => _t('ok'),
+ 'body' => _t('update_can_apply')
+ );
+ }
+ }
+
+ public function checkAction() {
+ $this->view->change_view('update', 'index');
+
+ if (file_exists(UPDATE_FILENAME) || $this->view->check_last_hour) {
+ // 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'));
+
+ return;
+ }
+
+ $c = curl_init(FRESHRSS_UPDATE_WEBSITE);
+ 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::error(
+ 'Error during update (HTTP code ' . $c_status . '): ' . $c_error
+ );
+
+ $this->view->message = array(
+ 'status' => 'bad',
+ 'title' => _t('damn'),
+ 'body' => _t('update_server_not_found', FRESHRSS_UPDATE_WEBSITE)
+ );
+ return;
+ }
+
+ $res_array = explode("\n", $result, 2);
+ $status = $res_array[0];
+ if (strpos($status, 'UPDATE') !== 0) {
+ $this->view->message = array(
+ 'status' => 'bad',
+ 'title' => _t('damn'),
+ 'body' => _t('no_update')
+ );
+
+ @file_put_contents(DATA_PATH . '/last_update.txt', time());
+
+ return;
+ }
+
+ $script = $res_array[1];
+ if (file_put_contents(UPDATE_FILENAME, $script) !== false) {
+ Minz_Request::forward(array('c' => 'update'));
+ } else {
+ $this->view->message = array(
+ 'status' => 'bad',
+ 'title' => _t('damn'),
+ 'body' => _t('update_problem', 'Cannot save the update script')
+ );
+ }
+ }
+
+ public function applyAction() {
+ if (!file_exists(UPDATE_FILENAME) || !is_writable(FRESHRSS_PATH)) {
+ Minz_Request::forward(array('c' => 'update'), true);
+ }
+
+ require(UPDATE_FILENAME);
+
+ if (Minz_Request::isPost()) {
+ save_info_update();
+ }
+
+ if (!need_info_update()) {
+ $res = apply_update();
+
+ if ($res === true) {
+ @unlink(UPDATE_FILENAME);
+ @file_put_contents(DATA_PATH . '/last_update.txt', time());
+
+ Minz_Request::good(_t('update_finished'));
+ } else {
+ Minz_Request::bad(_t('update_problem', $res),
+ array('c' => 'update', 'a' => 'index'));
+ }
+ }
+ }
+}
diff --git a/app/Controllers/usersController.php b/app/Controllers/usersController.php
new file mode 100644
index 000000000..a9e6c32bc
--- /dev/null
+++ b/app/Controllers/usersController.php
@@ -0,0 +1,203 @@
+<?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', '', true);
+ 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);
+
+ $passwordPlain = Minz_Request::param('apiPasswordPlain', '', true);
+ if ($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->_apiPasswordHash($passwordHash);
+ }
+
+ if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
+ $this->view->conf->_mail_login(Minz_Request::param('mail_login', '', true));
+ }
+ $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');
+ $unsafe_autologin = Minz_Request::param('unsafe_autologin', false);
+ $api_enabled = Minz_Request::param('api_enabled', false);
+ if ($anon != Minz_Configuration::allowAnonymous() ||
+ $auth_type != Minz_Configuration::authType() ||
+ $anon_refresh != Minz_Configuration::allowAnonymousRefresh() ||
+ $unsafe_autologin != Minz_Configuration::unsafeAutologinEnabled() ||
+ $api_enabled != Minz_Configuration::apiEnabled()) {
+
+ Minz_Configuration::_authType($auth_type);
+ Minz_Configuration::_allowAnonymous($anon);
+ Minz_Configuration::_allowAnonymousRefresh($anon_refresh);
+ Minz_Configuration::_enableAutologin($unsafe_autologin);
+ Minz_Configuration::_enableApi($api_enabled);
+ $ok &= Minz_Configuration::writeFile();
+ }
+ }
+
+ 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', '_'))) {
+ $db = Minz_Configuration::dataBase();
+ require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
+
+ $new_user_language = Minz_Request::param('new_user_language', $this->view->conf->language);
+ if (!in_array($new_user_language, $this->view->conf->availableLanguages())) {
+ $new_user_language = $this->view->conf->language;
+ }
+
+ $new_user_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', '', true);
+ $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', '_'))) {
+ $db = Minz_Configuration::dataBase();
+ require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.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);
+ }
+}