diff options
Diffstat (limited to 'app/Controllers')
| -rw-r--r-- | app/Controllers/authController.php | 14 | ||||
| -rwxr-xr-x | app/Controllers/configureController.php | 59 | ||||
| -rwxr-xr-x | app/Controllers/entryController.php | 22 | ||||
| -rw-r--r-- | app/Controllers/extensionController.php | 4 | ||||
| -rwxr-xr-x | app/Controllers/feedController.php | 28 | ||||
| -rw-r--r-- | app/Controllers/importExportController.php | 22 | ||||
| -rwxr-xr-x | app/Controllers/indexController.php | 21 | ||||
| -rwxr-xr-x | app/Controllers/javascriptController.php | 2 | ||||
| -rw-r--r-- | app/Controllers/subscriptionController.php | 67 | ||||
| -rw-r--r-- | app/Controllers/tagController.php | 4 | ||||
| -rw-r--r-- | app/Controllers/updateController.php | 2 | ||||
| -rw-r--r-- | app/Controllers/userController.php | 276 |
12 files changed, 408 insertions, 113 deletions
diff --git a/app/Controllers/authController.php b/app/Controllers/authController.php index ca44b1a96..e2e1aaa22 100644 --- a/app/Controllers/authController.php +++ b/app/Controllers/authController.php @@ -169,10 +169,6 @@ class FreshRSS_auth_Controller extends Minz_ActionController { return; } - if (!function_exists('password_verify')) { - include_once(LIB_PATH . '/password_compat.php'); - } - $s = $conf->passwordHash; $ok = password_verify($password, $s); unset($password); @@ -203,12 +199,22 @@ class FreshRSS_auth_Controller extends Minz_ActionController { /** * This action gives possibility to a user to create an account. + * + * The user is redirected to the home if he's connected. + * + * A 403 is sent if max number of registrations is reached. */ public function registerAction() { + if (FreshRSS_Auth::hasAccess()) { + Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true); + } + if (max_registrations_reached()) { Minz_Error::error(403); } + $this->view->show_tos_checkbox = file_exists(join_path(DATA_PATH, 'tos.html')); + $this->view->show_email_field = FreshRSS_Context::$system_conf->force_email_validation; Minz_View::prependTitle(_t('gen.auth.registration.title') . ' · '); } } diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php index 6d3c4dcce..b38d3289a 100755 --- a/app/Controllers/configureController.php +++ b/app/Controllers/configureController.php @@ -48,6 +48,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController { 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->topline_display_authors = Minz_Request::param('topline_display_authors', 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); @@ -166,8 +167,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController { * tab and up. */ public function shortcutAction() { - global $SHORTCUT_KEYS; - $this->view->list_keys = $SHORTCUT_KEYS; + $this->view->list_keys = SHORTCUT_KEYS; if (Minz_Request::isPost()) { FreshRSS_Context::$user_conf->shortcuts = validateShortcutList(Minz_Request::param('shortcuts')); @@ -196,9 +196,31 @@ class FreshRSS_configure_Controller extends Minz_ActionController { */ 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); + if (!Minz_Request::paramBoolean('enable_keep_max')) { + $keepMax = false; + } elseif (!$keepMax = Minz_Request::param('keep_max')) { + $keepMax = FreshRSS_Feed::ARCHIVING_RETENTION_COUNT_LIMIT; + } + if ($enableRetentionPeriod = Minz_Request::paramBoolean('enable_keep_period')) { + $keepPeriod = FreshRSS_Feed::ARCHIVING_RETENTION_PERIOD; + if (is_numeric(Minz_Request::param('keep_period_count')) && preg_match('/^PT?1[YMWDH]$/', Minz_Request::param('keep_period_unit'))) { + $keepPeriod = str_replace('1', Minz_Request::param('keep_period_count'), Minz_Request::param('keep_period_unit')); + } + } else { + $keepPeriod = false; + } + FreshRSS_Context::$user_conf->ttl_default = Minz_Request::param('ttl_default', FreshRSS_Feed::TTL_DEFAULT); + FreshRSS_Context::$user_conf->archiving = [ + 'keep_period' => $keepPeriod, + 'keep_max' => $keepMax, + 'keep_min' => Minz_Request::param('keep_min_default', 0), + 'keep_favourites' => Minz_Request::paramBoolean('keep_favourites'), + 'keep_labels' => Minz_Request::paramBoolean('keep_labels'), + 'keep_unreads' => Minz_Request::paramBoolean('keep_unreads'), + ]; + FreshRSS_Context::$user_conf->keep_history_default = null; //Legacy < FreshRSS 1.15 + FreshRSS_Context::$user_conf->old_entries = null; //Legacy < FreshRSS 1.15 FreshRSS_Context::$user_conf->save(); invalidateHttpCache(); @@ -206,7 +228,20 @@ class FreshRSS_configure_Controller extends Minz_ActionController { array('c' => 'configure', 'a' => 'archiving')); } - Minz_View::prependTitle(_t('conf.archiving.title') . ' · '); + $volatile = [ + 'enable_keep_period' => false, + 'keep_period_count' => '3', + 'keep_period_unit' => 'P1M', + ]; + $keepPeriod = FreshRSS_Context::$user_conf->archiving['keep_period']; + if (preg_match('/^PT?(?P<count>\d+)[YMWDH]$/', $keepPeriod, $matches)) { + $volatile = [ + 'enable_keep_period' => true, + 'keep_period_count' => $matches['count'], + 'keep_period_unit' => str_replace($matches['count'], 1, $keepPeriod), + ]; + } + FreshRSS_Context::$user_conf->volatile = $volatile; $entryDAO = FreshRSS_Factory::createEntryDao(); $this->view->nb_total = $entryDAO->count(); @@ -217,6 +252,8 @@ class FreshRSS_configure_Controller extends Minz_ActionController { if (FreshRSS_Auth::hasAccess('admin')) { $this->view->size_total = $databaseDAO->size(true); } + + Minz_View::prependTitle(_t('conf.archiving.title') . ' · '); } /** @@ -292,15 +329,24 @@ class FreshRSS_configure_Controller extends Minz_ActionController { * configuration values then sends a notification to the user. * * The options available on the page are: + * - instance name (default: FreshRSS) + * - auto update URL (default: false) + * - force emails validation (default: false) * - user limit (default: 1) * - user category limit (default: 16384) * - user feed limit (default: 16384) * - user login duration for form auth (default: 2592000) + * + * The `force-email-validation` is ignored with PHP < 5.5 */ public function systemAction() { if (!FreshRSS_Auth::hasAccess('admin')) { Minz_Error::error(403); } + + $can_enable_email_validation = version_compare(PHP_VERSION, '5.5') >= 0; + $this->view->can_enable_email_validation = $can_enable_email_validation; + if (Minz_Request::isPost()) { $limits = FreshRSS_Context::$system_conf->limits; $limits['max_registrations'] = Minz_Request::param('max-registrations', 1); @@ -310,6 +356,9 @@ class FreshRSS_configure_Controller extends Minz_ActionController { 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); + if ($can_enable_email_validation) { + FreshRSS_Context::$system_conf->force_email_validation = Minz_Request::param('force-email-validation', false); + } FreshRSS_Context::$system_conf->save(); invalidateHttpCache(); diff --git a/app/Controllers/entryController.php b/app/Controllers/entryController.php index 9c5ee2616..7881cb3ec 100755 --- a/app/Controllers/entryController.php +++ b/app/Controllers/entryController.php @@ -17,7 +17,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController { // If ajax request, we do not print layout $this->ajax = Minz_Request::param('ajax'); if ($this->ajax) { - $this->view->_useLayout(false); + $this->view->_layout(false); Minz_Request::_param('ajax'); } } @@ -181,32 +181,20 @@ class FreshRSS_entry_Controller extends Minz_ActionController { public function purgeAction() { @set_time_limit(300); - $nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1); - $date_min = time() - (3600 * 24 * 30 * $nb_month_old); - - $entryDAO = FreshRSS_Factory::createEntryDao(); $feedDAO = FreshRSS_Factory::createFeedDao(); $feeds = $feedDAO->listFeeds(); $nb_total = 0; invalidateHttpCache(); - foreach ($feeds as $feed) { - $feed_history = $feed->keepHistory(); - if (FreshRSS_Feed::KEEP_HISTORY_DEFAULT === $feed_history) { - $feed_history = FreshRSS_Context::$user_conf->keep_history_default; - } + $feedDAO->beginTransaction(); - if ($feed_history >= 0) { - $nb = $entryDAO->cleanOldEntries($feed->id(), $date_min, $feed_history); - if ($nb > 0) { - $nb_total += $nb; - Minz_Log::debug($nb . ' old entries cleaned in feed [' . $feed->url(false) . ']'); - } - } + foreach ($feeds as $feed) { + $nb_total += $feed->cleanOldEntries(); } $feedDAO->updateCachedValues(); + $feedDAO->commit(); $databaseDAO = FreshRSS_Factory::createDatabaseDAO(); $databaseDAO->minorDbMaintenance(); diff --git a/app/Controllers/extensionController.php b/app/Controllers/extensionController.php index 806e5a696..68bd90f4c 100644 --- a/app/Controllers/extensionController.php +++ b/app/Controllers/extensionController.php @@ -80,10 +80,10 @@ class FreshRSS_extension_Controller extends Minz_ActionController { */ public function configureAction() { if (Minz_Request::param('ajax')) { - $this->view->_useLayout(false); + $this->view->_layout(false); } else { $this->indexAction(); - $this->view->change_view('extension', 'index'); + $this->view->_path('extension/index.phtml'); } $ext_name = urldecode(Minz_Request::param('e')); diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 862bb10fb..aabeb80ff 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -267,10 +267,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $maxFeeds = 10; } - // 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); - // WebSub (PubSubHubbub) support $pubsubhubbubEnabledGeneral = FreshRSS_Context::$system_conf->pubsubhubbub_enabled; $pshbMinAge = time() - (3600 * 24); //TODO: Make a configuration. @@ -323,12 +319,6 @@ class FreshRSS_feed_Controller extends Minz_ActionController { continue; } - $feed_history = $feed->keepHistory(); - if ($isNewFeed) { - $feed_history = FreshRSS_Feed::KEEP_HISTORY_INFINITE; - } elseif (FreshRSS_Feed::KEEP_HISTORY_DEFAULT === $feed_history) { - $feed_history = FreshRSS_Context::$user_conf->keep_history_default; - } $needFeedCacheRefresh = false; // We want chronological order and SimplePie uses reverse order. @@ -376,15 +366,9 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } $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 { $id = uTimeString(); $entry->_id($id); - if ($entry_date < $date_min) { - $entry->_isRead(true); //Old article that was not in database. Probably an error, so mark as read - } $entry->applyFilterActions(); @@ -413,23 +397,19 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $entryDAO->updateLastSeen($feed->id(), $oldGuids, $mtime); } - if ($feed_history >= 0 && mt_rand(0, 30) === 1) { - // TODO: move this function in web cron when available (see entry::purge) - // Remove old entries once in 30. + if (mt_rand(0, 30) === 1) { // Remove old entries once in 30. if (!$entryDAO->inTransaction()) { $entryDAO->beginTransaction(); } - - $nb = $entryDAO->cleanOldEntries($feed->id(), $date_min, max($feed_history, count($entries) + 10)); + $nb = $feed->cleanOldEntries(); if ($nb > 0) { $needFeedCacheRefresh = true; - Minz_Log::debug($nb . ' old entries cleaned in feed [' . $feed->url(false) . ']'); } } $feedDAO->updateLastUpdate($feed->id(), false, $mtime); if ($needFeedCacheRefresh) { - $feedDAO->updateCachedValue($feed->id()); + $feedDAO->updateCachedValues($feed->id()); } if ($entryDAO->inTransaction()) { $entryDAO->commit(); @@ -530,7 +510,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { ); Minz_Session::_param('notification', $notif); // No layout in ajax request. - $this->view->_useLayout(false); + $this->view->_layout(false); } else { // Redirect to the main page with correct notification. if ($updated_feeds === 1) { diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php index 1d7176929..f2ae8238e 100644 --- a/app/Controllers/importExportController.php +++ b/app/Controllers/importExportController.php @@ -29,7 +29,25 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { Minz_View::prependTitle(_t('sub.import_export.title') . ' · '); } + private static function megabytes($size_str) { + switch (substr($size_str, -1)) { + case 'M': case 'm': return (int)$size_str; + case 'K': case 'k': return (int)$size_str / 1024; + case 'G': case 'g': return (int)$size_str * 1024; + } + return $size_str; + } + + private static function minimumMemory($mb) { + $mb = (int)$mb; + $ini = self::megabytes(ini_get('memory_limit')); + if ($ini < $mb) { + ini_set('memory_limit', $mb . 'M'); + } + } + public function importFile($name, $path, $username = null) { + self::minimumMemory(256); require_once(LIB_PATH . '/lib_opml.php'); $this->catDAO = new FreshRSS_CategoryDAO($username); @@ -709,8 +727,6 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $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(); @@ -773,7 +789,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { if (!Minz_Request::isPost()) { Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true); } - $this->view->_useLayout(false); + $this->view->_layout(false); $nb_files = 0; try { diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php index f536113dd..967029fd1 100755 --- a/app/Controllers/indexController.php +++ b/app/Controllers/indexController.php @@ -155,7 +155,7 @@ class FreshRSS_index_Controller extends Minz_ActionController { // No layout for RSS output. $this->view->url = PUBLIC_TO_INDEX_PATH . '/' . (empty($_SERVER['QUERY_STRING']) ? '' : '?' . $_SERVER['QUERY_STRING']); $this->view->rss_title = FreshRSS_Context::$name . ' | ' . Minz_View::title(); - $this->view->_useLayout(false); + $this->view->_layout(false); header('Content-Type: application/rss+xml; charset=utf-8'); } @@ -173,7 +173,7 @@ class FreshRSS_index_Controller extends Minz_ActionController { private function updateContext() { if (empty(FreshRSS_Context::$categories)) { $catDAO = FreshRSS_Factory::createCategoryDao(); - FreshRSS_Context::$categories = $catDAO->listCategories(); + FreshRSS_Context::$categories = $catDAO->listSortedCategories(); } // Update number of read / unread variables. @@ -260,6 +260,23 @@ class FreshRSS_index_Controller extends Minz_ActionController { } /** + * This action displays the EULA page of FreshRSS. + * This page is enabled only if admin created a data/tos.html file. + * The content of the page is the content of data/tos.html. + * It returns 404 if there is no EULA. + */ + public function tosAction() { + $terms_of_service = file_get_contents(join_path(DATA_PATH, 'tos.html')); + if (!$terms_of_service) { + Minz_Error::error(404); + } + + $this->view->terms_of_service = $terms_of_service; + $this->view->can_register = !max_registrations_reached(); + Minz_View::prependTitle(_t('index.tos.title') . ' · '); + } + + /** * This action displays logs of FreshRSS for the current user. */ public function logsAction() { diff --git a/app/Controllers/javascriptController.php b/app/Controllers/javascriptController.php index d56da9cbb..c84e5483b 100755 --- a/app/Controllers/javascriptController.php +++ b/app/Controllers/javascriptController.php @@ -2,7 +2,7 @@ class FreshRSS_javascript_Controller extends Minz_ActionController { public function firstAction() { - $this->view->_useLayout(false); + $this->view->_layout(false); } public function actualizeAction() { diff --git a/app/Controllers/subscriptionController.php b/app/Controllers/subscriptionController.php index 79da39751..b4520c8e6 100644 --- a/app/Controllers/subscriptionController.php +++ b/app/Controllers/subscriptionController.php @@ -19,7 +19,7 @@ class FreshRSS_subscription_Controller extends Minz_ActionController { $catDAO->checkDefault(); $feedDAO->updateTTL(); - $this->view->categories = $catDAO->listCategories(false); + $this->view->categories = $catDAO->listSortedCategories(false); $this->view->default_category = $catDAO->getDefault(); } @@ -74,7 +74,7 @@ class FreshRSS_subscription_Controller extends Minz_ActionController { */ public function feedAction() { if (Minz_Request::param('ajax')) { - $this->view->_useLayout(false); + $this->view->_layout(false); } $feedDAO = FreshRSS_Factory::createFeedDao(); @@ -121,6 +121,32 @@ class FreshRSS_subscription_Controller extends Minz_ActionController { $feed->_attributes('timeout', null); } + if (Minz_Request::paramBoolean('use_default_purge_options')) { + $feed->_attributes('archiving', null); + } else { + if (!Minz_Request::paramBoolean('enable_keep_max')) { + $keepMax = false; + } elseif (!$keepMax = Minz_Request::param('keep_max')) { + $keepMax = FreshRSS_Feed::ARCHIVING_RETENTION_COUNT_LIMIT; + } + if ($enableRetentionPeriod = Minz_Request::paramBoolean('enable_keep_period')) { + $keepPeriod = FreshRSS_Feed::ARCHIVING_RETENTION_PERIOD; + if (is_numeric(Minz_Request::param('keep_period_count')) && preg_match('/^PT?1[YMWDH]$/', Minz_Request::param('keep_period_unit'))) { + $keepPeriod = str_replace(1, Minz_Request::param('keep_period_count'), Minz_Request::param('keep_period_unit')); + } + } else { + $keepPeriod = false; + } + $feed->_attributes('archiving', [ + 'keep_period' => $keepPeriod, + 'keep_max' => $keepMax, + 'keep_min' => intval(Minz_Request::param('keep_min', 0)), + 'keep_favourites' => Minz_Request::paramBoolean('keep_favourites'), + 'keep_labels' => Minz_Request::paramBoolean('keep_labels'), + 'keep_unreads' => Minz_Request::paramBoolean('keep_unreads'), + ]); + } + $feed->_filtersAction('read', preg_split('/[\n\r]+/', Minz_Request::param('filteractions_read', ''))); $values = array( @@ -132,7 +158,6 @@ class FreshRSS_subscription_Controller extends Minz_ActionController { 'pathEntries' => Minz_Request::param('path_entries', ''), 'priority' => intval(Minz_Request::param('priority', FreshRSS_Feed::PRIORITY_MAIN_STREAM)), 'httpAuth' => $httpAuth, - 'keep_history' => intval(Minz_Request::param('keep_history', FreshRSS_Feed::KEEP_HISTORY_DEFAULT)), 'ttl' => $ttl * ($mute ? -1 : 1), 'attributes' => $feed->attributes(), ); @@ -152,7 +177,7 @@ class FreshRSS_subscription_Controller extends Minz_ActionController { } public function categoryAction() { - $this->view->_useLayout(false); + $this->view->_layout(false); $categoryDAO = FreshRSS_Factory::createCategoryDao(); @@ -165,9 +190,39 @@ class FreshRSS_subscription_Controller extends Minz_ActionController { $this->view->category = $category; if (Minz_Request::isPost()) { - $values = array( + if (Minz_Request::paramBoolean('use_default_purge_options')) { + $category->_attributes('archiving', null); + } else { + if (!Minz_Request::paramBoolean('enable_keep_max')) { + $keepMax = false; + } elseif (!$keepMax = Minz_Request::param('keep_max')) { + $keepMax = FreshRSS_Feed::ARCHIVING_RETENTION_COUNT_LIMIT; + } + if ($enableRetentionPeriod = Minz_Request::paramBoolean('enable_keep_period')) { + $keepPeriod = FreshRSS_Feed::ARCHIVING_RETENTION_PERIOD; + if (is_numeric(Minz_Request::param('keep_period_count')) && preg_match('/^PT?1[YMWDH]$/', Minz_Request::param('keep_period_unit'))) { + $keepPeriod = str_replace(1, Minz_Request::param('keep_period_count'), Minz_Request::param('keep_period_unit')); + } + } else { + $keepPeriod = false; + } + $category->_attributes('archiving', [ + 'keep_period' => $keepPeriod, + 'keep_max' => $keepMax, + 'keep_min' => intval(Minz_Request::param('keep_min', 0)), + 'keep_favourites' => Minz_Request::paramBoolean('keep_favourites'), + 'keep_labels' => Minz_Request::paramBoolean('keep_labels'), + 'keep_unreads' => Minz_Request::paramBoolean('keep_unreads'), + ]); + } + + $position = Minz_Request::param('position'); + $category->_attributes('position', '' === $position ? null : (int) $position); + + $values = [ 'name' => Minz_Request::param('name', ''), - ); + 'attributes' => $category->attributes(), + ]; invalidateHttpCache(); diff --git a/app/Controllers/tagController.php b/app/Controllers/tagController.php index 106e0afa8..ba9efe2fc 100644 --- a/app/Controllers/tagController.php +++ b/app/Controllers/tagController.php @@ -16,7 +16,7 @@ class FreshRSS_tag_Controller extends Minz_ActionController { // If ajax request, we do not print layout $this->ajax = Minz_Request::param('ajax'); if ($this->ajax) { - $this->view->_useLayout(false); + $this->view->_layout(false); Minz_Request::_param('ajax'); } } @@ -70,7 +70,7 @@ class FreshRSS_tag_Controller extends Minz_ActionController { } public function getTagsForEntryAction() { - $this->view->_useLayout(false); + $this->view->_layout(false); header('Content-Type: application/json; charset=UTF-8'); header('Cache-Control: private, no-cache, no-store, must-revalidate'); $id_entry = Minz_Request::param('id_entry', 0); diff --git a/app/Controllers/updateController.php b/app/Controllers/updateController.php index 2be644c85..ebe5e4cc8 100644 --- a/app/Controllers/updateController.php +++ b/app/Controllers/updateController.php @@ -89,7 +89,7 @@ class FreshRSS_update_Controller extends Minz_ActionController { } public function checkAction() { - $this->view->change_view('update', 'index'); + $this->view->_path('update/index.phtml'); if (file_exists(UPDATE_FILENAME)) { // There is already an update file to apply: we don't need to check diff --git a/app/Controllers/userController.php b/app/Controllers/userController.php index 6d0fced5b..6afc91b4e 100644 --- a/app/Controllers/userController.php +++ b/app/Controllers/userController.php @@ -8,26 +8,7 @@ class FreshRSS_user_Controller extends Minz_ActionController { // 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 @@ -52,12 +33,23 @@ class FreshRSS_user_Controller extends Minz_ActionController { return false; } - public static function updateUser($user, $passwordPlain, $apiPasswordPlain, $userConfigUpdated = array()) { + public static function updateUser($user, $email, $passwordPlain, $apiPasswordPlain, $userConfigUpdated = array()) { $userConfig = get_user_configuration($user); if ($userConfig === null) { return false; } + if ($email !== null && $userConfig->mail_login !== $email) { + $userConfig->mail_login = $email; + + if (FreshRSS_Context::$system_conf->force_email_validation) { + $salt = FreshRSS_Context::$system_conf->salt; + $userConfig->email_validation_token = sha1($salt . uniqid(mt_rand(), true)); + $mailer = new FreshRSS_User_Mailer(); + $mailer->send_email_need_validation($user, $userConfig); + } + } + if ($passwordPlain != '') { $passwordHash = self::hashPassword($passwordPlain); $userConfig->passwordHash = $passwordHash; @@ -103,7 +95,7 @@ class FreshRSS_user_Controller extends Minz_ActionController { $apiPasswordPlain = Minz_Request::param('apiPasswordPlain', '', true); $username = Minz_Request::param('username'); - $ok = self::updateUser($username, $passwordPlain, $apiPasswordPlain, array( + $ok = self::updateUser($username, null, $passwordPlain, $apiPasswordPlain, array( 'token' => Minz_Request::param('token', null), )); @@ -126,25 +118,63 @@ class FreshRSS_user_Controller extends Minz_ActionController { * This action displays the user profile page. */ public function profileAction() { + if (!FreshRSS_Auth::hasAccess()) { + Minz_Error::error(403); + } + + $email_not_verified = FreshRSS_Context::$user_conf->email_validation_token !== ''; + $this->view->disable_aside = false; + if ($email_not_verified) { + $this->view->_layout('simple'); + $this->view->disable_aside = true; + } + 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()) { + $system_conf = FreshRSS_Context::$system_conf; + $user_config = FreshRSS_Context::$user_conf; + $old_email = $user_config->mail_login; + + $email = trim(Minz_Request::param('email', '')); $passwordPlain = Minz_Request::param('newPasswordPlain', '', true); Minz_Request::_param('newPasswordPlain'); //Discard plain-text password ASAP $_POST['newPasswordPlain'] = ''; $apiPasswordPlain = Minz_Request::param('apiPasswordPlain', '', true); - $ok = self::updateUser(Minz_Session::param('currentUser'), $passwordPlain, $apiPasswordPlain, array( + if ($system_conf->force_email_validation && empty($email)) { + Minz_Request::bad( + _t('user.email.feedback.required'), + array('c' => 'user', 'a' => 'profile') + ); + } + + if (!empty($email) && !validateEmailAddress($email)) { + Minz_Request::bad( + _t('user.email.feedback.invalid'), + array('c' => 'user', 'a' => 'profile') + ); + } + + $ok = self::updateUser( + Minz_Session::param('currentUser'), + $email, + $passwordPlain, + $apiPasswordPlain, + array( 'token' => Minz_Request::param('token', null), - )); + ) + ); Minz_Session::_param('passwordHash', FreshRSS_Context::$user_conf->passwordHash); if ($ok) { - if ($passwordPlain == '') { + if ($system_conf->force_email_validation && $email !== $old_email) { + Minz_Request::good(_t('feedback.profile.updated'), array('c' => 'user', 'a' => 'validateEmail')); + } elseif ($passwordPlain == '') { Minz_Request::good(_t('feedback.profile.updated'), array('c' => 'user', 'a' => 'profile')); } else { Minz_Request::good(_t('feedback.profile.updated'), array('c' => 'index', 'a' => 'index')); @@ -166,6 +196,7 @@ class FreshRSS_user_Controller extends Minz_ActionController { Minz_View::prependTitle(_t('admin.user.title') . ' · '); + $this->view->show_email_field = FreshRSS_Context::$system_conf->force_email_validation; $this->view->current_user = Minz_Request::param('u'); $this->view->nb_articles = 0; @@ -180,9 +211,19 @@ class FreshRSS_user_Controller extends Minz_ActionController { } } - public static function createUser($new_user_name, $passwordPlain, $apiPasswordPlain, $userConfig = array(), $insertDefaultFeeds = true) { - if (!is_array($userConfig)) { - $userConfig = array(); + public static function createUser($new_user_name, $email, $passwordPlain, $apiPasswordPlain = '', $userConfigOverride = [], $insertDefaultFeeds = true) { + $userConfig = []; + + $customUserConfigPath = join_path(DATA_PATH, 'config-user.custom.php'); + if (file_exists($customUserConfigPath)) { + $customUserConfig = include($customUserConfigPath); + if (is_array($customUserConfig)) { + $userConfig = $customUserConfig; + } + } + + if (is_array($userConfigOverride)) { + $userConfig = array_merge($userConfig, $userConfigOverride); } $ok = self::checkUsername($new_user_name); @@ -206,9 +247,9 @@ class FreshRSS_user_Controller extends Minz_ActionController { $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); - $ok &= self::updateUser($new_user_name, $passwordPlain, $apiPasswordPlain); + $newUserDAO = FreshRSS_Factory::createUserDao($new_user_name); + $ok &= $newUserDAO->createUser($insertDefaultFeeds); + $ok &= self::updateUser($new_user_name, $email, $passwordPlain, $apiPasswordPlain); } return $ok; } @@ -219,6 +260,7 @@ class FreshRSS_user_Controller extends Minz_ActionController { * Request parameters are: * - new_user_language * - new_user_name + * - new_user_email * - new_user_passwordPlain * - r (i.e. a redirection url, optional) * @@ -226,15 +268,43 @@ class FreshRSS_user_Controller extends Minz_ActionController { * @todo handle r redirection in Minz_Request::forward directly? */ public function createAction() { - if (Minz_Request::isPost() && ( - FreshRSS_Auth::hasAccess('admin') || - !max_registrations_reached() - )) { + if (!FreshRSS_Auth::hasAccess('admin') && max_registrations_reached()) { + Minz_Error::error(403); + } + + if (Minz_Request::isPost()) { + $system_conf = FreshRSS_Context::$system_conf; + $new_user_name = Minz_Request::param('new_user_name'); + $email = Minz_Request::param('new_user_email', ''); $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)); + $tos_enabled = file_exists(join_path(DATA_PATH, 'tos.html')); + $accept_tos = Minz_Request::param('accept_tos', false); + + if ($system_conf->force_email_validation && empty($email)) { + Minz_Request::bad( + _t('user.email.feedback.required'), + array('c' => 'auth', 'a' => 'register') + ); + } + + if (!empty($email) && !validateEmailAddress($email)) { + Minz_Request::bad( + _t('user.email.feedback.invalid'), + array('c' => 'auth', 'a' => 'register') + ); + } + + if ($tos_enabled && !$accept_tos) { + Minz_Request::bad( + _t('user.tos.feedback.invalid'), + array('c' => 'auth', 'a' => 'register') + ); + } + + $ok = self::createUser($new_user_name, $email, $passwordPlain, '', array('language' => $new_user_language)); Minz_Request::_param('new_user_passwordPlain'); //Discard plain-text password ASAP $_POST['new_user_passwordPlain'] = ''; invalidateHttpCache(); @@ -266,9 +336,6 @@ class FreshRSS_user_Controller extends Minz_ActionController { } public static function deleteUser($username) { - $db = FreshRSS_Context::$system_conf->db; - require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php'); - $ok = self::checkUsername($username); if ($ok) { $default_user = FreshRSS_Context::$system_conf->default_user; @@ -278,8 +345,8 @@ class FreshRSS_user_Controller extends Minz_ActionController { $ok &= is_dir($user_data); if ($ok) { self::deleteFeverKey($username); - $userDAO = new FreshRSS_UserDAO(); - $ok &= $userDAO->deleteUser($username); + $oldUserDAO = FreshRSS_Factory::createUserDao($username); + $ok &= $oldUserDAO->deleteUser(); $ok &= recursive_unlink($user_data); array_map('unlink', glob(PSHB_PATH . '/feeds/*/' . $username . '.txt')); } @@ -287,6 +354,122 @@ class FreshRSS_user_Controller extends Minz_ActionController { } /** + * This action validates an email address, based on the token sent by email. + * It also serves the main page when user is blocked. + * + * Request parameters are: + * - username + * - token + * + * This route works with GET requests since the URL is provided by email. + * The security risks (e.g. forged URL by an attacker) are not very high so + * it's ok. + * + * It returns 404 error if `force_email_validation` is disabled or if the + * user doesn't exist. + * + * It returns 403 if user isn't logged in and `username` param isn't passed. + */ + public function validateEmailAction() { + if (!FreshRSS_Context::$system_conf->force_email_validation) { + Minz_Error::error(404); + } + + Minz_View::prependTitle(_t('user.email.validation.title') . ' · '); + $this->view->_layout('simple'); + + $username = Minz_Request::param('username'); + $token = Minz_Request::param('token'); + + if ($username) { + $user_config = get_user_configuration($username); + } elseif (FreshRSS_Auth::hasAccess()) { + $user_config = FreshRSS_Context::$user_conf; + } else { + Minz_Error::error(403); + } + + if (!FreshRSS_UserDAO::exists($username) || $user_config === null) { + Minz_Error::error(404); + } + + if ($user_config->email_validation_token === '') { + Minz_Request::good( + _t('user.email.validation.feedback.unnecessary'), + array('c' => 'index', 'a' => 'index') + ); + } + + if ($token) { + if ($user_config->email_validation_token !== $token) { + Minz_Request::bad( + _t('user.email.validation.feedback.wrong_token'), + array('c' => 'user', 'a' => 'validateEmail') + ); + } + + $user_config->email_validation_token = ''; + if ($user_config->save()) { + Minz_Request::good( + _t('user.email.validation.feedback.ok'), + array('c' => 'index', 'a' => 'index') + ); + } else { + Minz_Request::bad( + _t('user.email.validation.feedback.error'), + array('c' => 'user', 'a' => 'validateEmail') + ); + } + } + } + + /** + * This action resends a validation email to the current user. + * + * It only acts on POST requests but doesn't require any param (except the + * CSRF token). + * + * It returns 403 error if the user is not logged in or 404 if request is + * not POST. Else it redirects silently to the index if user has already + * validated its email, or to the user#validateEmail route. + */ + public function sendValidationEmailAction() { + if (!FreshRSS_Auth::hasAccess()) { + Minz_Error::error(403); + } + + if (!Minz_Request::isPost()) { + Minz_Error::error(404); + } + + $username = Minz_Session::param('currentUser', '_'); + $user_config = FreshRSS_Context::$user_conf; + + if ($user_config->email_validation_token === '') { + Minz_Request::forward(array( + 'c' => 'index', + 'a' => 'index', + ), true); + } + + $mailer = new FreshRSS_User_Mailer(); + $ok = $mailer->send_email_need_validation($username, $user_config); + + $redirect_url = array('c' => 'user', 'a' => 'validateEmail'); + if ($ok) { + Minz_Request::good( + _t('user.email.validation.feedback.email_sent'), + $redirect_url + ); + } else { + Minz_Request::bad( + _t('user.email.validation.feedback.email_failed'), + $redirect_url + ); + } + } + + /** * This action delete an existing user. * * Request parameter is: @@ -296,17 +479,18 @@ class FreshRSS_user_Controller extends Minz_ActionController { */ public function deleteAction() { $username = Minz_Request::param('username'); + $self_deletion = Minz_Session::param('currentUser', '_') === $username; + + if (!FreshRSS_Auth::hasAccess('admin') && !$self_deletion) { + Minz_Error::error(403); + } + $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 - )) { + if (Minz_Request::isPost()) { $ok = true; if ($ok && $self_deletion) { // We check the password if it's a self-destruction |
