diff options
| author | 2017-02-15 14:12:25 +0100 | |
|---|---|---|
| committer | 2017-02-15 14:12:25 +0100 | |
| commit | 2d097bc855dbd1ad06c7c306c05e78a198209084 (patch) | |
| tree | 67028e45792c575c25c92616633f64cc7a4a13eb /app/Controllers | |
| parent | fe293900061263a1917fc1cf18ca369c8e07cb99 (diff) | |
| parent | 5f637bd816b7323885bfe1751a1724ee59a822f6 (diff) | |
Merge remote-tracking branch 'FreshRSS/master' into dev
Diffstat (limited to 'app/Controllers')
| -rw-r--r-- | app/Controllers/authController.php | 228 | ||||
| -rw-r--r-- | app/Controllers/categoryController.php | 60 | ||||
| -rwxr-xr-x | app/Controllers/configureController.php | 254 | ||||
| -rwxr-xr-x | app/Controllers/entryController.php | 51 | ||||
| -rw-r--r-- | app/Controllers/errorController.php | 40 | ||||
| -rw-r--r-- | app/Controllers/extensionController.php | 215 | ||||
| -rwxr-xr-x | app/Controllers/feedController.php | 496 | ||||
| -rw-r--r-- | app/Controllers/importExportController.php | 472 | ||||
| -rwxr-xr-x | app/Controllers/indexController.php | 403 | ||||
| -rwxr-xr-x | app/Controllers/javascriptController.php | 20 | ||||
| -rw-r--r-- | app/Controllers/statsController.php | 66 | ||||
| -rw-r--r-- | app/Controllers/subscriptionController.php | 27 | ||||
| -rw-r--r-- | app/Controllers/updateController.php | 211 | ||||
| -rw-r--r-- | app/Controllers/userController.php | 285 |
14 files changed, 1681 insertions, 1147 deletions
diff --git a/app/Controllers/authController.php b/app/Controllers/authController.php index 4af39cb71..1398e4e49 100644 --- a/app/Controllers/authController.php +++ b/app/Controllers/authController.php @@ -19,17 +19,18 @@ class FreshRSS_auth_Controller extends Minz_ActionController { */ public function indexAction() { if (!FreshRSS_Auth::hasAccess('admin')) { - Minz_Error::error(403, - array('error' => array(_t('access_denied')))); + Minz_Error::error(403); } + Minz_View::prependTitle(_t('admin.auth.title') . ' · '); + if (Minz_Request::isPost()) { $ok = true; - $current_token = FreshRSS_Context::$conf->token; + $current_token = FreshRSS_Context::$user_conf->token; $token = Minz_Request::param('token', $current_token); - FreshRSS_Context::$conf->_token($token); - $ok &= FreshRSS_Context::$conf->save(); + FreshRSS_Context::$user_conf->token = $token; + $ok &= FreshRSS_Context::$user_conf->save(); $anon = Minz_Request::param('anon_access', false); $anon = ((bool)$anon) && ($anon !== 'no'); @@ -38,27 +39,29 @@ class FreshRSS_auth_Controller extends Minz_ActionController { $auth_type = Minz_Request::param('auth_type', 'none'); $unsafe_autologin = Minz_Request::param('unsafe_autologin', false); $api_enabled = Minz_Request::param('api_enabled', false); - if ($anon != Minz_Configuration::allowAnonymous() || - $auth_type != Minz_Configuration::authType() || - $anon_refresh != Minz_Configuration::allowAnonymousRefresh() || - $unsafe_autologin != Minz_Configuration::unsafeAutologinEnabled() || - $api_enabled != Minz_Configuration::apiEnabled()) { - - Minz_Configuration::_authType($auth_type); - Minz_Configuration::_allowAnonymous($anon); - Minz_Configuration::_allowAnonymousRefresh($anon_refresh); - Minz_Configuration::_enableAutologin($unsafe_autologin); - Minz_Configuration::_enableApi($api_enabled); - $ok &= Minz_Configuration::writeFile(); + if ($anon != FreshRSS_Context::$system_conf->allow_anonymous || + $auth_type != FreshRSS_Context::$system_conf->auth_type || + $anon_refresh != FreshRSS_Context::$system_conf->allow_anonymous_refresh || + $unsafe_autologin != FreshRSS_Context::$system_conf->unsafe_autologin_enabled || + $api_enabled != FreshRSS_Context::$system_conf->api_enabled) { + + // TODO: test values from form + FreshRSS_Context::$system_conf->auth_type = $auth_type; + FreshRSS_Context::$system_conf->allow_anonymous = $anon; + FreshRSS_Context::$system_conf->allow_anonymous_refresh = $anon_refresh; + FreshRSS_Context::$system_conf->unsafe_autologin_enabled = $unsafe_autologin; + FreshRSS_Context::$system_conf->api_enabled = $api_enabled; + + $ok &= FreshRSS_Context::$system_conf->save(); } invalidateHttpCache(); if ($ok) { - Minz_Request::good('configuration_updated', + Minz_Request::good(_t('feedback.conf.updated'), array('c' => 'auth', 'a' => 'index')); } else { - Minz_Request::bad('error_occurred', + Minz_Request::bad(_t('feedback.conf.error'), array('c' => 'auth', 'a' => 'index')); } } @@ -67,7 +70,7 @@ class FreshRSS_auth_Controller extends Minz_ActionController { /** * This action handles the login page. * - * It forwards to the correct login page (form or Persona) or main page if + * It forwards to the correct login page (form) or main page if * the user is already connected. */ public function loginAction() { @@ -75,14 +78,11 @@ class FreshRSS_auth_Controller extends Minz_ActionController { Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true); } - $auth_type = Minz_Configuration::authType(); + $auth_type = FreshRSS_Context::$system_conf->auth_type; switch ($auth_type) { case 'form': Minz_Request::forward(array('c' => 'auth', 'a' => 'formLogin')); break; - case 'persona': - Minz_Request::forward(array('c' => 'auth', 'a' => 'personaLogin')); - break; case 'http_auth': case 'none': // It should not happened! @@ -113,17 +113,19 @@ class FreshRSS_auth_Controller extends Minz_ActionController { $file_mtime = @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js'); Minz_View::appendScript(Minz_Url::display('/scripts/bcrypt.min.js?' . $file_mtime)); + $conf = Minz_Configuration::get('system'); + $limits = $conf->limits; + $this->view->cookie_days = round($limits['cookie_duration'] / 86400, 1); + if (Minz_Request::isPost()) { $nonce = Minz_Session::param('nonce'); $username = Minz_Request::param('username', ''); $challenge = Minz_Request::param('challenge', ''); - try { - $conf = new FreshRSS_Configuration($username); - } catch(Minz_Exception $e) { - // $username is not a valid user, nor the configuration file! - Minz_Log::warning('Login failure: ' . $e->getMessage()); - Minz_Request::bad(_t('invalid_login'), - array('c' => 'auth', 'a' => 'login')); + + $conf = get_user_configuration($username); + if (is_null($conf)) { + Minz_Error::error(403, array(_t('feedback.auth.login.invalid')), false); + return; } $ok = FreshRSS_FormAuth::checkCredentials( @@ -143,17 +145,16 @@ class FreshRSS_auth_Controller extends Minz_ActionController { } // All is good, go back to the index. - Minz_Request::good(_t('login'), + Minz_Request::good(_t('feedback.auth.login.success'), array('c' => 'index', 'a' => 'index')); } else { Minz_Log::warning('Password mismatch for' . ' user=' . $username . ', nonce=' . $nonce . ', c=' . $challenge); - Minz_Request::bad(_t('invalid_login'), - array('c' => 'auth', 'a' => 'login')); + Minz_Error::error(403, array(_t('feedback.auth.login.invalid')), false); } - } elseif (Minz_Configuration::unsafeAutologinEnabled()) { + } elseif (FreshRSS_Context::$system_conf->unsafe_autologin_enabled) { $username = Minz_Request::param('u', ''); $password = Minz_Request::param('p', ''); Minz_Request::_param('p'); @@ -162,11 +163,8 @@ class FreshRSS_auth_Controller extends Minz_ActionController { return; } - try { - $conf = new FreshRSS_Configuration($username); - } catch(Minz_Exception $e) { - // $username is not a valid user, nor the configuration file! - Minz_Log::warning('Login failure: ' . $e->getMessage()); + $conf = get_user_configuration($username); + if (is_null($conf)) { return; } @@ -182,89 +180,12 @@ class FreshRSS_auth_Controller extends Minz_ActionController { Minz_Session::_param('passwordHash', $s); FreshRSS_Auth::giveAccess(); - Minz_Request::good(_t('login'), + Minz_Request::good(_t('feedback.auth.login.success'), array('c' => 'index', 'a' => 'index')); } else { Minz_Log::warning('Unsafe password mismatch for user ' . $username); - Minz_Request::bad(_t('invalid_login'), - array('c' => 'auth', 'a' => 'login')); - } - } - } - - /** - * This action handles Persona login page. - * - * If this action is reached through a POST request, assertion from Persona - * is verificated and user connected if all is ok. - * - * Parameter is: - * - assertion (default: false) - * - * @todo: Persona system should be moved to a plugin - */ - public function personaLoginAction() { - $this->view->res = false; - - if (Minz_Request::isPost()) { - $this->view->_useLayout(false); - - $assert = Minz_Request::param('assertion'); - $url = 'https://verifier.login.persona.org/verify'; - $params = 'assertion=' . $assert . '&audience=' . - urlencode(Minz_Url::display(null, 'php', true)); - $ch = curl_init(); - $options = array( - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => TRUE, - CURLOPT_POST => 2, - CURLOPT_POSTFIELDS => $params - ); - curl_setopt_array($ch, $options); - $result = curl_exec($ch); - curl_close($ch); - - $res = json_decode($result, true); - - $login_ok = false; - $reason = ''; - if ($res['status'] === 'okay') { - $email = filter_var($res['email'], FILTER_VALIDATE_EMAIL); - if ($email != '') { - $persona_file = DATA_PATH . '/persona/' . $email . '.txt'; - if (($current_user = @file_get_contents($persona_file)) !== false) { - $current_user = trim($current_user); - try { - $conf = new FreshRSS_Configuration($current_user); - $login_ok = strcasecmp($email, $conf->mail_login) === 0; - } catch (Minz_Exception $e) { - //Permission denied or conf file does not exist - $reason = 'Invalid configuration for user ' . - '[' . $current_user . '] ' . $e->getMessage(); - } - } - } else { - $reason = 'Invalid email format [' . $res['email'] . ']'; - } - } else { - $reason = $res['reason']; + Minz_Error::error(403, array(_t('feedback.auth.login.invalid')), false); } - - if ($login_ok) { - Minz_Session::_param('currentUser', $current_user); - Minz_Session::_param('mail', $email); - FreshRSS_Auth::giveAccess(); - invalidateHttpCache(); - } else { - Minz_Log::error($reason); - - $res = array(); - $res['status'] = 'failure'; - $res['reason'] = _t('invalid_login'); - } - - header('Content-Type: application/json; charset=UTF-8'); - $this->view->res = $res; } } @@ -274,75 +195,18 @@ class FreshRSS_auth_Controller extends Minz_ActionController { public function logoutAction() { invalidateHttpCache(); FreshRSS_Auth::removeAccess(); - Minz_Request::good(_t('disconnected'), + Minz_Request::good(_t('feedback.auth.logout.success'), array('c' => 'index', 'a' => 'index')); } /** - * This action resets the authentication system. - * - * After reseting, form auth is set by default. + * This action gives possibility to a user to create an account. */ - public function resetAction() { - 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; + public function registerAction() { + if (max_registrations_reached()) { + Minz_Error::error(403); } - $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', ''); - $challenge = Minz_Request::param('challenge', ''); - - $ok = FreshRSS_FormAuth::checkCredentials( - $username, $conf->passwordHash, $nonce, $challenge - ); - - 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' => 'auth', 'a' => 'reset')); - } - } else { - Minz_Log::warning('Password mismatch for' . - ' user=' . $username . - ', nonce=' . $nonce . - ', c=' . $challenge); - Minz_Request::bad(_t('invalid_login'), - array('c' => 'auth', 'a' => 'reset')); - } - } + Minz_View::prependTitle(_t('gen.auth.registration.title') . ' · '); } } diff --git a/app/Controllers/categoryController.php b/app/Controllers/categoryController.php index 977ce51be..922f92844 100644 --- a/app/Controllers/categoryController.php +++ b/app/Controllers/categoryController.php @@ -13,10 +13,7 @@ class FreshRSS_category_Controller extends Minz_ActionController { */ public function firstAction() { if (!FreshRSS_Auth::hasAccess()) { - Minz_Error::error( - 403, - array('error' => array(_t('access_denied'))) - ); + Minz_Error::error(403); } $catDAO = new FreshRSS_CategoryDAO(); @@ -33,18 +30,26 @@ class FreshRSS_category_Controller extends Minz_ActionController { $catDAO = new FreshRSS_CategoryDAO(); $url_redirect = array('c' => 'subscription', 'a' => 'index'); + $limits = FreshRSS_Context::$system_conf->limits; + $this->view->categories = $catDAO->listCategories(false); + + if (count($this->view->categories) >= $limits['max_categories']) { + Minz_Request::bad(_t('feedback.sub.category.over_max', $limits['max_categories']), + $url_redirect); + } + if (Minz_Request::isPost()) { invalidateHttpCache(); $cat_name = Minz_Request::param('new-category'); if (!$cat_name) { - Minz_Request::bad(_t('category_no_name'), $url_redirect); + Minz_Request::bad(_t('feedback.sub.category.no_name'), $url_redirect); } $cat = new FreshRSS_Category($cat_name); if ($catDAO->searchByName($cat->name()) != null) { - Minz_Request::bad(_t('category_name_exists'), $url_redirect); + Minz_Request::bad(_t('feedback.sub.category.name_exists'), $url_redirect); } $values = array( @@ -53,9 +58,9 @@ class FreshRSS_category_Controller extends Minz_ActionController { ); if ($catDAO->addCategory($values)) { - Minz_Request::good(_t('category_created', $cat->name()), $url_redirect); + Minz_Request::good(_t('feedback.sub.category.created', $cat->name()), $url_redirect); } else { - Minz_Request::bad(_t('error_occurred'), $url_redirect); + Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect); } } @@ -79,11 +84,11 @@ class FreshRSS_category_Controller extends Minz_ActionController { $id = Minz_Request::param('id'); $name = Minz_Request::param('name', ''); if (strlen($name) <= 0) { - Minz_Request::bad(_t('category_no_name'), $url_redirect); + Minz_Request::bad(_t('feedback.sub.category.no_name'), $url_redirect); } if ($catDAO->searchById($id) == null) { - Minz_Request::bad(_t('category_not_exist'), $url_redirect); + Minz_Request::bad(_t('feedback.sub.category.not_exist'), $url_redirect); } $cat = new FreshRSS_Category($name); @@ -92,9 +97,9 @@ class FreshRSS_category_Controller extends Minz_ActionController { ); if ($catDAO->updateCategory($id, $values)) { - Minz_Request::good(_t('category_updated'), $url_redirect); + Minz_Request::good(_t('feedback.sub.category.updated'), $url_redirect); } else { - Minz_Request::bad(_t('error_occurred'), $url_redirect); + Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect); } } @@ -112,7 +117,6 @@ class FreshRSS_category_Controller extends Minz_ActionController { public function deleteAction() { $feedDAO = FreshRSS_Factory::createFeedDao(); $catDAO = new FreshRSS_CategoryDAO(); - $default_category = $catDAO->getDefault(); $url_redirect = array('c' => 'subscription', 'a' => 'index'); if (Minz_Request::isPost()) { @@ -120,26 +124,27 @@ class FreshRSS_category_Controller extends Minz_ActionController { $id = Minz_Request::param('id'); if (!$id) { - Minz_Request::bad(_t('category_no_id'), $url_redirect); + Minz_Request::bad(_t('feedback.sub.category.no_id'), $url_redirect); } - if ($id === $default_category->id()) { - Minz_Request::bad(_t('category_not_delete_default'), $url_redirect); + if ($id === FreshRSS_CategoryDAO::defaultCategoryId) { + Minz_Request::bad(_t('feedback.sub.category.not_delete_default'), $url_redirect); } - if ($feedDAO->changeCategory($id, $default_category->id()) === false) { - Minz_Request::bad(_t('error_occurred'), $url_redirect); + if ($feedDAO->changeCategory($id, FreshRSS_CategoryDAO::defaultCategoryId) === false) { + Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect); } if ($catDAO->deleteCategory($id) === false) { - Minz_Request::bad(_t('error_occurred'), $url_redirect); + Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect); } // Remove related queries. - FreshRSS_Context::$conf->remove_query_by_get('c_' . $id); - FreshRSS_Context::$conf->save(); + FreshRSS_Context::$user_conf->queries = remove_query_by_get( + 'c_' . $id, FreshRSS_Context::$user_conf->queries); + FreshRSS_Context::$user_conf->save(); - Minz_Request::good(_t('category_deleted'), $url_redirect); + Minz_Request::good(_t('feedback.sub.category.deleted'), $url_redirect); } Minz_Request::forward($url_redirect, true); @@ -161,7 +166,7 @@ class FreshRSS_category_Controller extends Minz_ActionController { $id = Minz_Request::param('id'); if (!$id) { - Minz_Request::bad(_t('category_no_id'), $url_redirect); + Minz_Request::bad(_t('feedback.sub.category.no_id'), $url_redirect); } // List feeds to remove then related user queries. @@ -172,13 +177,14 @@ class FreshRSS_category_Controller extends Minz_ActionController { // Remove related queries foreach ($feeds as $feed) { - FreshRSS_Context::$conf->remove_query_by_get('f_' . $feed->id()); + FreshRSS_Context::$user_conf->queries = remove_query_by_get( + 'f_' . $feed->id(), FreshRSS_Context::$user_conf->queries); } - FreshRSS_Context::$conf->save(); + FreshRSS_Context::$user_conf->save(); - Minz_Request::good(_t('category_emptied'), $url_redirect); + Minz_Request::good(_t('feedback.sub.category.emptied'), $url_redirect); } else { - Minz_Request::bad(_t('error_occurred'), $url_redirect); + Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect); } } diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php index 8a9dcdc62..e73f106a6 100755 --- a/app/Controllers/configureController.php +++ b/app/Controllers/configureController.php @@ -11,10 +11,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController { */ public function firstAction() { if (!FreshRSS_Auth::hasAccess()) { - Minz_Error::error( - 403, - array('error' => array(_t('access_denied'))) - ); + Minz_Error::error(403); } } @@ -44,33 +41,33 @@ class FreshRSS_configure_Controller extends Minz_ActionController { */ public function displayAction() { if (Minz_Request::isPost()) { - FreshRSS_Context::$conf->_language(Minz_Request::param('language', 'en')); - FreshRSS_Context::$conf->_theme(Minz_Request::param('theme', FreshRSS_Themes::$defaultTheme)); - FreshRSS_Context::$conf->_content_width(Minz_Request::param('content_width', 'thin')); - FreshRSS_Context::$conf->_topline_read(Minz_Request::param('topline_read', false)); - FreshRSS_Context::$conf->_topline_favorite(Minz_Request::param('topline_favorite', false)); - FreshRSS_Context::$conf->_topline_date(Minz_Request::param('topline_date', false)); - FreshRSS_Context::$conf->_topline_link(Minz_Request::param('topline_link', false)); - FreshRSS_Context::$conf->_bottomline_read(Minz_Request::param('bottomline_read', false)); - FreshRSS_Context::$conf->_bottomline_favorite(Minz_Request::param('bottomline_favorite', false)); - FreshRSS_Context::$conf->_bottomline_sharing(Minz_Request::param('bottomline_sharing', false)); - FreshRSS_Context::$conf->_bottomline_tags(Minz_Request::param('bottomline_tags', false)); - FreshRSS_Context::$conf->_bottomline_date(Minz_Request::param('bottomline_date', false)); - FreshRSS_Context::$conf->_bottomline_link(Minz_Request::param('bottomline_link', false)); - FreshRSS_Context::$conf->_html5_notif_timeout(Minz_Request::param('html5_notif_timeout', 0)); - FreshRSS_Context::$conf->save(); - - Minz_Session::_param('language', FreshRSS_Context::$conf->language); - Minz_Translate::reset(); + FreshRSS_Context::$user_conf->language = Minz_Request::param('language', 'en'); + FreshRSS_Context::$user_conf->theme = Minz_Request::param('theme', FreshRSS_Themes::$defaultTheme); + FreshRSS_Context::$user_conf->content_width = Minz_Request::param('content_width', 'thin'); + FreshRSS_Context::$user_conf->topline_read = Minz_Request::param('topline_read', false); + FreshRSS_Context::$user_conf->topline_favorite = Minz_Request::param('topline_favorite', false); + FreshRSS_Context::$user_conf->topline_date = Minz_Request::param('topline_date', false); + FreshRSS_Context::$user_conf->topline_link = Minz_Request::param('topline_link', false); + FreshRSS_Context::$user_conf->bottomline_read = Minz_Request::param('bottomline_read', false); + FreshRSS_Context::$user_conf->bottomline_favorite = Minz_Request::param('bottomline_favorite', false); + FreshRSS_Context::$user_conf->bottomline_sharing = Minz_Request::param('bottomline_sharing', false); + FreshRSS_Context::$user_conf->bottomline_tags = Minz_Request::param('bottomline_tags', false); + FreshRSS_Context::$user_conf->bottomline_date = Minz_Request::param('bottomline_date', false); + FreshRSS_Context::$user_conf->bottomline_link = Minz_Request::param('bottomline_link', false); + FreshRSS_Context::$user_conf->html5_notif_timeout = Minz_Request::param('html5_notif_timeout', 0); + FreshRSS_Context::$user_conf->save(); + + Minz_Session::_param('language', FreshRSS_Context::$user_conf->language); + Minz_Translate::reset(FreshRSS_Context::$user_conf->language); invalidateHttpCache(); - Minz_Request::good(_t('configuration_updated'), + Minz_Request::good(_t('feedback.conf.updated'), array('c' => 'configure', 'a' => 'display')); } $this->view->themes = FreshRSS_Themes::get(); - Minz_View::prependTitle(_t('display_configuration') . ' · '); + Minz_View::prependTitle(_t('conf.display.title') . ' · '); } /** @@ -92,6 +89,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController { * - image lazy loading * - stick open articles to the top * - display a confirmation when reading all articles + * - auto remove article after reading * - article order (default: DESC) * - mark articles as read when: * - displayed @@ -102,35 +100,34 @@ class FreshRSS_configure_Controller extends Minz_ActionController { */ public function readingAction() { if (Minz_Request::isPost()) { - FreshRSS_Context::$conf->_posts_per_page(Minz_Request::param('posts_per_page', 10)); - FreshRSS_Context::$conf->_view_mode(Minz_Request::param('view_mode', 'normal')); - FreshRSS_Context::$conf->_default_view((int)Minz_Request::param('default_view', FreshRSS_Entry::STATE_ALL)); - FreshRSS_Context::$conf->_auto_load_more(Minz_Request::param('auto_load_more', false)); - FreshRSS_Context::$conf->_display_posts(Minz_Request::param('display_posts', false)); - FreshRSS_Context::$conf->_display_categories(Minz_Request::param('display_categories', false)); - FreshRSS_Context::$conf->_hide_read_feeds(Minz_Request::param('hide_read_feeds', false)); - FreshRSS_Context::$conf->_onread_jump_next(Minz_Request::param('onread_jump_next', false)); - FreshRSS_Context::$conf->_lazyload(Minz_Request::param('lazyload', false)); - FreshRSS_Context::$conf->_sticky_post(Minz_Request::param('sticky_post', false)); - FreshRSS_Context::$conf->_reading_confirm(Minz_Request::param('reading_confirm', false)); - FreshRSS_Context::$conf->_sort_order(Minz_Request::param('sort_order', 'DESC')); - FreshRSS_Context::$conf->_mark_when(array( + FreshRSS_Context::$user_conf->posts_per_page = Minz_Request::param('posts_per_page', 10); + FreshRSS_Context::$user_conf->view_mode = Minz_Request::param('view_mode', 'normal'); + FreshRSS_Context::$user_conf->default_view = Minz_Request::param('default_view', 'adaptive'); + FreshRSS_Context::$user_conf->auto_load_more = Minz_Request::param('auto_load_more', false); + FreshRSS_Context::$user_conf->display_posts = Minz_Request::param('display_posts', false); + FreshRSS_Context::$user_conf->display_categories = Minz_Request::param('display_categories', false); + FreshRSS_Context::$user_conf->hide_read_feeds = Minz_Request::param('hide_read_feeds', false); + FreshRSS_Context::$user_conf->onread_jump_next = Minz_Request::param('onread_jump_next', false); + FreshRSS_Context::$user_conf->lazyload = Minz_Request::param('lazyload', false); + FreshRSS_Context::$user_conf->sticky_post = Minz_Request::param('sticky_post', false); + FreshRSS_Context::$user_conf->reading_confirm = Minz_Request::param('reading_confirm', false); + FreshRSS_Context::$user_conf->auto_remove_article = Minz_Request::param('auto_remove_article', false); + FreshRSS_Context::$user_conf->mark_updated_article_unread = Minz_Request::param('mark_updated_article_unread', false); + FreshRSS_Context::$user_conf->sort_order = Minz_Request::param('sort_order', 'DESC'); + FreshRSS_Context::$user_conf->mark_when = array( 'article' => Minz_Request::param('mark_open_article', false), 'site' => Minz_Request::param('mark_open_site', false), 'scroll' => Minz_Request::param('mark_scroll', false), 'reception' => Minz_Request::param('mark_upon_reception', false), - )); - FreshRSS_Context::$conf->save(); - - Minz_Session::_param('language', FreshRSS_Context::$conf->language); - Minz_Translate::reset(); + ); + FreshRSS_Context::$user_conf->save(); invalidateHttpCache(); - Minz_Request::good(_t('configuration_updated'), + Minz_Request::good(_t('feedback.conf.updated'), array('c' => 'configure', 'a' => 'reading')); } - Minz_View::prependTitle(_t('reading_configuration') . ' · '); + Minz_View::prependTitle(_t('conf.reading.title') . ' · '); } /** @@ -142,16 +139,16 @@ class FreshRSS_configure_Controller extends Minz_ActionController { */ public function sharingAction() { if (Minz_Request::isPost()) { - $params = Minz_Request::params(); - FreshRSS_Context::$conf->_sharing($params['share']); - FreshRSS_Context::$conf->save(); + $params = Minz_Request::fetchPOST(); + FreshRSS_Context::$user_conf->sharing = $params['share']; + FreshRSS_Context::$user_conf->save(); invalidateHttpCache(); - Minz_Request::good(_t('configuration_updated'), + Minz_Request::good(_t('feedback.conf.updated'), array('c' => 'configure', 'a' => 'sharing')); } - Minz_View::prependTitle(_t('sharing') . ' · '); + Minz_View::prependTitle(_t('conf.sharing.title') . ' · '); } /** @@ -185,15 +182,15 @@ class FreshRSS_configure_Controller extends Minz_ActionController { } } - FreshRSS_Context::$conf->_shortcuts($shortcuts_ok); - FreshRSS_Context::$conf->save(); + FreshRSS_Context::$user_conf->shortcuts = $shortcuts_ok; + FreshRSS_Context::$user_conf->save(); invalidateHttpCache(); - Minz_Request::good(_t('shortcuts_updated'), + Minz_Request::good(_t('feedback.conf.shortcuts_updated'), array('c' => 'configure', 'a' => 'shortcut')); } - Minz_View::prependTitle(_t('shortcuts') . ' · '); + Minz_View::prependTitle(_t('conf.shortcut.title') . ' · '); } /** @@ -213,17 +210,17 @@ class FreshRSS_configure_Controller extends Minz_ActionController { */ public function archivingAction() { if (Minz_Request::isPost()) { - FreshRSS_Context::$conf->_old_entries(Minz_Request::param('old_entries', 3)); - FreshRSS_Context::$conf->_keep_history_default(Minz_Request::param('keep_history_default', 0)); - FreshRSS_Context::$conf->_ttl_default(Minz_Request::param('ttl_default', -2)); - FreshRSS_Context::$conf->save(); + FreshRSS_Context::$user_conf->old_entries = Minz_Request::param('old_entries', 3); + FreshRSS_Context::$user_conf->keep_history_default = Minz_Request::param('keep_history_default', 0); + FreshRSS_Context::$user_conf->ttl_default = Minz_Request::param('ttl_default', -2); + FreshRSS_Context::$user_conf->save(); invalidateHttpCache(); - Minz_Request::good(_t('configuration_updated'), + Minz_Request::good(_t('feedback.conf.updated'), array('c' => 'configure', 'a' => 'archiving')); } - Minz_View::prependTitle(_t('archiving_configuration') . ' · '); + Minz_View::prependTitle(_t('conf.archiving.title') . ' · '); $entryDAO = FreshRSS_Factory::createEntryDao(); $this->view->nb_total = $entryDAO->count(); @@ -245,80 +242,30 @@ class FreshRSS_configure_Controller extends Minz_ActionController { * checking if categories and feeds are still in use. */ public function queriesAction() { + $category_dao = new FreshRSS_CategoryDAO(); + $feed_dao = FreshRSS_Factory::createFeedDao(); if (Minz_Request::isPost()) { - $queries = Minz_Request::param('queries', array()); + $params = Minz_Request::param('queries', array()); - foreach ($queries as $key => $query) { + foreach ($params as $key => $query) { if (!$query['name']) { - $query['name'] = _t('query_number', $key + 1); + $query['name'] = _t('conf.query.number', $key + 1); } + $queries[] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao); } - FreshRSS_Context::$conf->_queries($queries); - FreshRSS_Context::$conf->save(); + FreshRSS_Context::$user_conf->queries = $queries; + FreshRSS_Context::$user_conf->save(); - Minz_Request::good(_t('configuration_updated'), + Minz_Request::good(_t('feedback.conf.updated'), array('c' => 'configure', 'a' => 'queries')); } else { - $this->view->query_get = array(); - $cat_dao = new FreshRSS_CategoryDAO(); - $feed_dao = FreshRSS_Factory::createFeedDao(); - foreach (FreshRSS_Context::$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; - } + $this->view->queries = array(); + foreach (FreshRSS_Context::$user_conf->queries as $key => $query) { + $this->view->queries[$key] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao); } } - Minz_View::prependTitle(_t('queries') . ' · '); + Minz_View::prependTitle(_t('conf.query.title') . ' · '); } /** @@ -329,23 +276,56 @@ class FreshRSS_configure_Controller extends Minz_ActionController { * lean data. */ public function addQueryAction() { - $whitelist = array('get', 'order', 'name', 'search', 'state'); - $queries = FreshRSS_Context::$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; + $category_dao = new FreshRSS_CategoryDAO(); + $feed_dao = FreshRSS_Factory::createFeedDao(); + $queries = array(); + foreach (FreshRSS_Context::$user_conf->queries as $key => $query) { + $queries[$key] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao); } - $queries[] = $query; - FreshRSS_Context::$conf->_queries($queries); - FreshRSS_Context::$conf->save(); + $params = Minz_Request::fetchGET(); + $params['url'] = Minz_Url::display(array('params' => $params)); + $params['name'] = _t('conf.query.number', count($queries) + 1); + $queries[] = new FreshRSS_UserQuery($params, $feed_dao, $category_dao); + + FreshRSS_Context::$user_conf->queries = $queries; + FreshRSS_Context::$user_conf->save(); - Minz_Request::good(_t('query_created', $query['name']), + Minz_Request::good(_t('feedback.conf.query_created', $query['name']), array('c' => 'configure', 'a' => 'queries')); } + + /** + * This action handles the system configuration page. + * + * It displays the system configuration page. + * If this action is reach through a POST request, it stores all new + * configuration values then sends a notification to the user. + * + * The options available on the page are: + * - user limit (default: 1) + * - user category limit (default: 16384) + * - user feed limit (default: 16384) + */ + public function systemAction() { + if (!FreshRSS_Auth::hasAccess('admin')) { + Minz_Error::error(403); + } + if (Minz_Request::isPost()) { + $limits = FreshRSS_Context::$system_conf->limits; + $limits['max_registrations'] = Minz_Request::param('max-registrations', 1); + $limits['max_feeds'] = Minz_Request::param('max-feeds', 16384); + $limits['max_categories'] = Minz_Request::param('max-categories', 16384); + FreshRSS_Context::$system_conf->limits = $limits; + FreshRSS_Context::$system_conf->title = Minz_Request::param('instance-name', 'FreshRSS'); + FreshRSS_Context::$system_conf->auto_update_url = Minz_Request::param('auto-update-url', false); + FreshRSS_Context::$system_conf->save(); + + invalidateHttpCache(); + + Minz_Session::_param('notification', array( + 'type' => 'good', + 'content' => _t('feedback.conf.updated') + )); + } + } } diff --git a/app/Controllers/entryController.php b/app/Controllers/entryController.php index 449029648..c40588105 100755 --- a/app/Controllers/entryController.php +++ b/app/Controllers/entryController.php @@ -11,18 +11,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController { */ public function firstAction() { if (!FreshRSS_Auth::hasAccess()) { - Minz_Error::error( - 403, - array('error' => array(_t('access_denied'))) - ); - } - - // Keep parameter information (output) to do a correct redirection at - // the end. - $this->params = array(); - $output = Minz_Request::param('output', ''); - if ($output != '' && FreshRSS_Context::$conf->view_mode !== $output) { - $this->params['output'] = $output; + Minz_Error::error(403); } // If ajax request, we do not print layout @@ -45,19 +34,30 @@ class FreshRSS_entry_Controller extends Minz_ActionController { * - nextGet (default: $get) * - idMax (default: 0) * - is_read (default: true) - * - * @todo nextGet system should not be present here... or should be? */ public function readAction() { $id = Minz_Request::param('id'); $get = Minz_Request::param('get'); $next_get = Minz_Request::param('nextGet', $get); $id_max = Minz_Request::param('idMax', 0); + FreshRSS_Context::$search = new FreshRSS_Search(Minz_Request::param('search', '')); + + FreshRSS_Context::$state = Minz_Request::param('state', 0); + if (FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_FAVORITE)) { + FreshRSS_Context::$state = FreshRSS_Entry::STATE_FAVORITE; + } elseif (FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_NOT_FAVORITE)) { + FreshRSS_Context::$state = FreshRSS_Entry::STATE_NOT_FAVORITE; + } else { + FreshRSS_Context::$state = 0; + } + + $params = array(); $entryDAO = FreshRSS_Factory::createEntryDao(); if ($id === false) { // id is false? It MUST be a POST request! if (!Minz_Request::isPost()) { + Minz_Request::bad(_t('feedback.access.not_found'), array('c' => 'index', 'a' => 'index')); return; } @@ -69,16 +69,16 @@ class FreshRSS_entry_Controller extends Minz_ActionController { $get = substr($get, 2); switch($type_get) { case 'c': - $entryDAO->markReadCat($get, $id_max); + $entryDAO->markReadCat($get, $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state); break; case 'f': - $entryDAO->markReadFeed($get, $id_max); + $entryDAO->markReadFeed($get, $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state); break; case 's': - $entryDAO->markReadEntries($id_max, true); + $entryDAO->markReadEntries($id_max, true, 0, FreshRSS_Context::$search); break; case 'a': - $entryDAO->markReadEntries($id_max); + $entryDAO->markReadEntries($id_max, false, 0, FreshRSS_Context::$search, FreshRSS_Context::$state); break; } @@ -86,7 +86,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController { // Redirect to the correct page (category, feed or starred) // Not "a" because it is the default value if nothing is // given. - $this->params['get'] = $next_get; + $params['get'] = $next_get; } } } else { @@ -95,10 +95,10 @@ class FreshRSS_entry_Controller extends Minz_ActionController { } if (!$this->ajax) { - Minz_Request::good(_t('feeds_marked_read'), array( + Minz_Request::good(_t('feedback.sub.feed.marked_read'), array( 'c' => 'index', 'a' => 'index', - 'params' => $this->params, + 'params' => $params, ), true); } } @@ -123,7 +123,6 @@ class FreshRSS_entry_Controller extends Minz_ActionController { Minz_Request::forward(array( 'c' => 'index', 'a' => 'index', - 'params' => $this->params, ), true); } } @@ -155,7 +154,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController { $feedDAO->updateCachedValues(); invalidateHttpCache(); - Minz_Request::good(_t('optimization_complete'), $url_redirect); + Minz_Request::good(_t('feedback.admin.optimization_complete'), $url_redirect); } /** @@ -167,7 +166,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController { public function purgeAction() { @set_time_limit(300); - $nb_month_old = max(FreshRSS_Context::$conf->old_entries, 1); + $nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1); $date_min = time() - (3600 * 24 * 30 * $nb_month_old); $feedDAO = FreshRSS_Factory::createFeedDao(); @@ -181,7 +180,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController { if ($feed_history == -2) { // TODO: -2 must be a constant! // -2 means we take the default value from configuration - $feed_history = FreshRSS_Context::$conf->keep_history_default; + $feed_history = FreshRSS_Context::$user_conf->keep_history_default; } if ($feed_history >= 0) { @@ -196,7 +195,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController { $feedDAO->updateCachedValues(); invalidateHttpCache(); - Minz_Request::good(_t('purge_completed', $nb_total), array( + Minz_Request::good(_t('feedback.sub.purge_completed', $nb_total), array( 'c' => 'configure', 'a' => 'archiving' )); diff --git a/app/Controllers/errorController.php b/app/Controllers/errorController.php index 76ab930e0..b0bafda72 100644 --- a/app/Controllers/errorController.php +++ b/app/Controllers/errorController.php @@ -9,41 +9,43 @@ class FreshRSS_error_Controller extends Minz_ActionController { * * It is called by Minz_Error::error() method. * - * Parameters are: - * - code (default: 404) - * - logs (default: array()) + * Parameters are passed by Minz_Session to have a proper url: + * - error_code (default: 404) + * - error_logs (default: array()) */ public function indexAction() { - $code_int = Minz_Request::param('code', 404); + $code_int = Minz_Session::param('error_code', 404); + $error_logs = Minz_Session::param('error_logs', array()); + Minz_Session::_param('error_code'); + Minz_Session::_param('error_logs'); + switch ($code_int) { + case 200 : + header('HTTP/1.1 200 OK'); + break; case 403: + header('HTTP/1.1 403 Forbidden'); $this->view->code = 'Error 403 - Forbidden'; - break; - case 404: - $this->view->code = 'Error 404 - Not found'; + $this->view->errorMessage = _t('feedback.access.denied'); break; case 500: + header('HTTP/1.1 500 Internal Server Error'); $this->view->code = 'Error 500 - Internal Server Error'; break; case 503: + header('HTTP/1.1 503 Service Unavailable'); $this->view->code = 'Error 503 - Service Unavailable'; break; + case 404: default: + header('HTTP/1.1 404 Not Found'); $this->view->code = 'Error 404 - Not found'; + $this->view->errorMessage = _t('feedback.access.not_found'); } - $errors = Minz_Request::param('logs', array()); - $this->view->errorMessage = trim(implode($errors)); - if ($this->view->errorMessage == '') { - switch($code_int) { - case 403: - $this->view->errorMessage = _t('forbidden_access'); - break; - case 404: - default: - $this->view->errorMessage = _t('page_not_found'); - break; - } + $error_message = trim(implode($error_logs)); + if ($error_message !== '') { + $this->view->errorMessage = $error_message; } Minz_View::prependTitle($this->view->code . ' · '); diff --git a/app/Controllers/extensionController.php b/app/Controllers/extensionController.php new file mode 100644 index 000000000..b6d2d3fe4 --- /dev/null +++ b/app/Controllers/extensionController.php @@ -0,0 +1,215 @@ +<?php + +/** + * The controller to manage extensions. + */ +class FreshRSS_extension_Controller extends Minz_ActionController { + /** + * This action is called before every other action in that class. It is + * the common boiler plate for every action. It is triggered by the + * underlying framework. + */ + public function firstAction() { + if (!FreshRSS_Auth::hasAccess()) { + Minz_Error::error(403); + } + } + + /** + * This action lists all the extensions available to the current user. + */ + public function indexAction() { + Minz_View::prependTitle(_t('admin.extensions.title') . ' · '); + $this->view->extension_list = array( + 'system' => array(), + 'user' => array(), + ); + + $extensions = Minz_ExtensionManager::listExtensions(); + foreach ($extensions as $ext) { + $this->view->extension_list[$ext->getType()][] = $ext; + } + } + + /** + * This action handles configuration of a given extension. + * + * Only administrator can configure a system extension. + * + * Parameters are: + * - e: the extension name (urlencoded) + * - additional parameters which should be handle by the extension + * handleConfigureAction() method (POST request). + */ + public function configureAction() { + if (Minz_Request::param('ajax')) { + $this->view->_useLayout(false); + } else { + $this->indexAction(); + $this->view->change_view('extension', 'index'); + } + + $ext_name = urldecode(Minz_Request::param('e')); + $ext = Minz_ExtensionManager::findExtension($ext_name); + + if (is_null($ext)) { + Minz_Error::error(404); + } + if ($ext->getType() === 'system' && !FreshRSS_Auth::hasAccess('admin')) { + Minz_Error::error(403); + } + + $this->view->extension = $ext; + $this->view->extension->handleConfigureAction(); + } + + /** + * This action enables a disabled extension for the current user. + * + * System extensions can only be enabled by an administrator. + * This action must be reached by a POST request. + * + * Parameter is: + * - e: the extension name (urlencoded). + */ + public function enableAction() { + $url_redirect = array('c' => 'extension', 'a' => 'index'); + + if (Minz_Request::isPost()) { + $ext_name = urldecode(Minz_Request::param('e')); + $ext = Minz_ExtensionManager::findExtension($ext_name); + + if (is_null($ext)) { + Minz_Request::bad(_t('feedback.extensions.not_found', $ext_name), + $url_redirect); + } + + if ($ext->isEnabled()) { + Minz_Request::bad(_t('feedback.extensions.already_enabled', $ext_name), + $url_redirect); + } + + $conf = null; + if ($ext->getType() === 'system' && FreshRSS_Auth::hasAccess('admin')) { + $conf = FreshRSS_Context::$system_conf; + } elseif ($ext->getType() === 'user') { + $conf = FreshRSS_Context::$user_conf; + } else { + Minz_Request::bad(_t('feedback.extensions.no_access', $ext_name), + $url_redirect); + } + + $res = $ext->install(); + + if ($res === true) { + $ext_list = $conf->extensions_enabled; + array_push_unique($ext_list, $ext_name); + $conf->extensions_enabled = $ext_list; + $conf->save(); + + Minz_Request::good(_t('feedback.extensions.enable.ok', $ext_name), + $url_redirect); + } else { + Minz_Log::warning('Can not enable extension ' . $ext_name . ': ' . $res); + Minz_Request::bad(_t('feedback.extensions.enable.ko', $ext_name, _url('index', 'logs')), + $url_redirect); + } + } + + Minz_Request::forward($url_redirect, true); + } + + /** + * This action disables an enabled extension for the current user. + * + * System extensions can only be disabled by an administrator. + * This action must be reached by a POST request. + * + * Parameter is: + * - e: the extension name (urlencoded). + */ + public function disableAction() { + $url_redirect = array('c' => 'extension', 'a' => 'index'); + + if (Minz_Request::isPost()) { + $ext_name = urldecode(Minz_Request::param('e')); + $ext = Minz_ExtensionManager::findExtension($ext_name); + + if (is_null($ext)) { + Minz_Request::bad(_t('feedback.extensions.not_found', $ext_name), + $url_redirect); + } + + if (!$ext->isEnabled()) { + Minz_Request::bad(_t('feedback.extensions.not_enabled', $ext_name), + $url_redirect); + } + + $conf = null; + if ($ext->getType() === 'system' && FreshRSS_Auth::hasAccess('admin')) { + $conf = FreshRSS_Context::$system_conf; + } elseif ($ext->getType() === 'user') { + $conf = FreshRSS_Context::$user_conf; + } else { + Minz_Request::bad(_t('feedback.extensions.no_access', $ext_name), + $url_redirect); + } + + $res = $ext->uninstall(); + + if ($res === true) { + $ext_list = $conf->extensions_enabled; + array_remove($ext_list, $ext_name); + $conf->extensions_enabled = $ext_list; + $conf->save(); + + Minz_Request::good(_t('feedback.extensions.disable.ok', $ext_name), + $url_redirect); + } else { + Minz_Log::warning('Can not unable extension ' . $ext_name . ': ' . $res); + Minz_Request::bad(_t('feedback.extensions.disable.ko', $ext_name, _url('index', 'logs')), + $url_redirect); + } + } + + Minz_Request::forward($url_redirect, true); + } + + /** + * This action handles deletion of an extension. + * + * Only administrator can remove an extension. + * This action must be reached by a POST request. + * + * Parameter is: + * -e: extension name (urlencoded) + */ + public function removeAction() { + if (!FreshRSS_Auth::hasAccess('admin')) { + Minz_Error::error(403); + } + + $url_redirect = array('c' => 'extension', 'a' => 'index'); + + if (Minz_Request::isPost()) { + $ext_name = urldecode(Minz_Request::param('e')); + $ext = Minz_ExtensionManager::findExtension($ext_name); + + if (is_null($ext)) { + Minz_Request::bad(_t('feedback.extensions.not_found', $ext_name), + $url_redirect); + } + + $res = recursive_unlink($ext->getPath()); + if ($res) { + Minz_Request::good(_t('feedback.extensions.removed', $ext_name), + $url_redirect); + } else { + Minz_Request::bad(_t('feedback.extensions.cannot_delete', $ext_name), + $url_redirect); + } + } + + Minz_Request::forward($url_redirect, true); + } +} diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index c2859edf4..f71f26a4e 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -14,20 +14,75 @@ class FreshRSS_feed_Controller extends Minz_ActionController { // 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 = FreshRSS_Context::$conf->token; + $token = FreshRSS_Context::$user_conf->token; $token_param = Minz_Request::param('token', ''); $token_is_ok = ($token != '' && $token == $token_param); $action = Minz_Request::actionName(); + $allow_anonymous_refresh = FreshRSS_Context::$system_conf->allow_anonymous_refresh; if ($action !== 'actualize' || - !(Minz_Configuration::allowAnonymousRefresh() || $token_is_ok)) { - Minz_Error::error( - 403, - array('error' => array(_t('access_denied'))) - ); + !($allow_anonymous_refresh || $token_is_ok)) { + Minz_Error::error(403); } } } + public static function addFeed($url, $title = '', $cat_id = 0, $new_cat_name = '', $http_auth = '') { + FreshRSS_UserDAO::touch(); + @set_time_limit(300); + + $catDAO = new FreshRSS_CategoryDAO(); + + $cat = null; + if ($cat_id > 0) { + $cat = $catDAO->searchById($cat_id); + } + if ($cat == null && $new_cat_name != '') { + $cat = $catDAO->addCategory(array('name' => $new_cat_name)); + } + if ($cat == null) { + $catDAO->checkDefault(); + } + $cat_id = $cat == null ? FreshRSS_CategoryDAO::defaultCategoryId : $cat->id(); + + $feed = new FreshRSS_Feed($url); //Throws FreshRSS_BadUrl_Exception + $feed->_httpAuth($http_auth); + $feed->load(true); //Throws FreshRSS_Feed_Exception, Minz_FileNotExistException + $feed->_category($cat_id); + + $feedDAO = FreshRSS_Factory::createFeedDao(); + if ($feedDAO->searchByUrl($feed->url())) { + throw new FreshRSS_AlreadySubscribed_Exception($url, $feed->name()); + } + + // Call the extension hook + $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed); + if ($feed === null) { + throw new FreshRSS_FeedNotAdded_Exception($url, $feed->name()); + } + + $values = array( + 'url' => $feed->url(), + 'category' => $feed->category(), + 'name' => $title != '' ? $title : $feed->name(), + 'website' => $feed->website(), + 'description' => $feed->description(), + 'lastUpdate' => time(), + 'httpAuth' => $feed->httpAuth(), + ); + + $id = $feedDAO->addFeed($values); + if (!$id) { + // There was an error in database... we cannot say what here. + throw new FreshRSS_FeedNotAdded_Exception($url, $feed->name()); + } + $feed->_id($id); + + // Ok, feed has been added in database. Now we have to refresh entries. + self::actualizeFeed($id, $url, false, null, true); + + return $feed; + } + /** * This action subscribes to a feed. * @@ -61,131 +116,66 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } $feedDAO = FreshRSS_Factory::createFeedDao(); - $this->catDAO = new FreshRSS_CategoryDAO(); $url_redirect = array( 'c' => 'subscription', 'a' => 'index', 'params' => array(), ); - if (Minz_Request::isPost()) { - @set_time_limit(300); + $limits = FreshRSS_Context::$system_conf->limits; + $this->view->feeds = $feedDAO->listFeeds(); + if (count($this->view->feeds) >= $limits['max_feeds']) { + Minz_Request::bad(_t('feedback.sub.feed.over_max', $limits['max_feeds']), + $url_redirect); + } + if (Minz_Request::isPost()) { $cat = Minz_Request::param('category'); + $new_cat_name = ''; if ($cat === 'nc') { // User want to create a new category, new_category parameter // must exist $new_cat = Minz_Request::param('new_category'); - if (empty($new_cat['name'])) { - $cat = false; - } else { - $cat = $this->catDAO->addCategory($new_cat); - } - } - - if ($cat === false) { - // If category was not given or if creating new category failed, - // get the default category - $this->catDAO->checkDefault(); - $def_cat = $this->catDAO->getDefault(); - $cat = $def_cat->id(); + $new_cat_name = isset($new_cat['name']) ? $new_cat['name'] : ''; } // HTTP information are useful if feed is protected behind a // HTTP authentication - $user = Minz_Request::param('http_user'); - $pass = Minz_Request::param('http_pass'); + $user = trim(Minz_Request::param('http_user', '')); + $pass = Minz_Request::param('http_pass', ''); $http_auth = ''; - if ($user != '' || $pass != '') { + if ($user != '' && $pass != '') { //TODO: Sanitize $http_auth = $user . ':' . $pass; } - $transaction_started = false; try { - $feed = new FreshRSS_Feed($url); + $feed = self::addFeed($url, '', $cat, $new_cat_name, $http_auth); } catch (FreshRSS_BadUrl_Exception $e) { // Given url was not a valid url! Minz_Log::warning($e->getMessage()); - Minz_Request::bad(_t('invalid_url', $url), $url_redirect); - } - - try { - $feed->load(true); + Minz_Request::bad(_t('feedback.sub.feed.invalid_url', $url), $url_redirect); } catch (FreshRSS_Feed_Exception $e) { // Something went bad (timeout, server not found, etc.) Minz_Log::warning($e->getMessage()); - Minz_Request::bad( - _t('internal_problem_feed', _url('index', 'logs')), - $url_redirect - ); + Minz_Request::bad(_t('feedback.sub.feed.internal_problem', _url('index', 'logs')), $url_redirect); } catch (Minz_FileNotExistException $e) { // Cache directory doesn't exist! Minz_Log::error($e->getMessage()); - Minz_Request::bad( - _t('internal_problem_feed', _url('index', 'logs')), - $url_redirect - ); + Minz_Request::bad(_t('feedback.sub.feed.internal_problem', _url('index', 'logs')), $url_redirect); + } catch (FreshRSS_AlreadySubscribed_Exception $e) { + Minz_Request::bad(_t('feedback.sub.feed.already_subscribed', $e->feedName()), $url_redirect); + } catch (FreshRSS_FeedNotAdded_Exception $e) { + Minz_Request::bad(_t('feedback.sub.feed.not_added', $e->feedName()), $url_redirect); } - if ($feedDAO->searchByUrl($feed->url())) { - Minz_Request::bad(_t('already_subscribed', $feed->name()), $url_redirect); - } - - $feed->_category($cat); - $feed->_httpAuth($http_auth); - - $values = array( - 'url' => $feed->url(), - 'category' => $feed->category(), - 'name' => $feed->name(), - 'website' => $feed->website(), - 'description' => $feed->description(), - 'lastUpdate' => time(), - 'httpAuth' => $feed->httpAuth(), - ); - - $id = $feedDAO->addFeed($values); - if (!$id) { - // There was an error in database... we cannot say what here. - Minz_Request::bad(_t('feed_not_added', $feed->name()), $url_redirect); - } - - // Ok, feed has been added in database. Now we have to refresh entries. - $feed->_id($id); - $feed->faviconPrepare(); - - $is_read = FreshRSS_Context::$conf->mark_when['reception'] ? 1 : 0; - - $entryDAO = FreshRSS_Factory::createEntryDao(); - // We want chronological order and SimplePie uses reverse order. - $entries = array_reverse($feed->entries()); - - // Calculate date of oldest entries we accept in DB. - $nb_month_old = FreshRSS_Context::$conf->old_entries; - $date_min = time() - (3600 * 24 * 30 * $nb_month_old); - - // Use a shared statement and a transaction to improve a LOT the - // performances. - $prepared_statement = $entryDAO->addEntryPrepare(); - $feedDAO->beginTransaction(); - foreach ($entries as $entry) { - // Entries are added without any verification. - $values = $entry->toArray(); - $values['id_feed'] = $feed->id(); - $values['id'] = min(time(), $entry->date(true)) . uSecString(); - $values['is_read'] = $is_read; - $entryDAO->addEntry($values, $prepared_statement); - } - $feedDAO->updateLastUpdate($feed->id()); - $feedDAO->commit(); - // Entries are in DB, we redirect to feed configuration page. $url_redirect['params']['id'] = $feed->id(); - Minz_Request::good(_t('feed_added', $feed->name()), $url_redirect); + Minz_Request::good(_t('feedback.sub.feed.added', $feed->name()), $url_redirect); } else { // GET request: we must ask confirmation to user before adding feed. - Minz_View::prependTitle(_t('add_rss_feed') . ' · '); + Minz_View::prependTitle(_t('sub.feed.title_add') . ' · '); + $this->catDAO = new FreshRSS_CategoryDAO(); $this->view->categories = $this->catDAO->listCategories(false); $this->view->feed = new FreshRSS_Feed($url); try { @@ -200,7 +190,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController { if ($feed) { // Already subscribe so we redirect to the feed configuration page. $url_redirect['params']['id'] = $feed->id(); - Minz_Request::good(_t('already_subscribed', $feed->name()), $url_redirect); + Minz_Request::good(_t('feedback.sub.feed.already_subscribed', $feed->name()), $url_redirect); } } } @@ -230,112 +220,168 @@ class FreshRSS_feed_Controller extends Minz_ActionController { invalidateHttpCache(); if ($n === false) { - Minz_Request::bad(_t('error_occurred'), $url_redirect); + Minz_Request::bad(_t('feedback.sub.feed.error'), $url_redirect); } else { - Minz_Request::good(_t('n_entries_deleted', $n), $url_redirect); + Minz_Request::good(_t('feedback.sub.feed.n_entries_deleted', $n), $url_redirect); } } - /** - * This action actualizes entries from one or several feeds. - * - * Parameters are: - * - id (default: false) - * - force (default: false) - * If id is not specified, all the feeds are actualized. But if force is - * false, process stops at 10 feeds to avoid time execution problem. - */ - public function actualizeAction() { + public static function actualizeFeed($feed_id, $feed_url, $force, $simplePiePush = null, $isNewFeed = false) { @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'); - // Create a list of feeds to actualize. - // If id is set and valid, corresponding feed is added to the list but + // If feed_id is set and valid, corresponding feed is added to the list but // alone in order to automatize further process. $feeds = array(); - if ($id) { - $feed = $feedDAO->searchById($id); + if ($feed_id > 0 || $feed_url) { + $feed = $feed_id > 0 ? $feedDAO->searchById($feed_id) : $feedDAO->searchByUrl($feed_url); if ($feed) { $feeds[] = $feed; } } else { - $feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::$conf->ttl_default); + $feeds = $feedDAO->listFeedsOrderUpdate(-1); } // Calculate date of oldest entries we accept in DB. - $nb_month_old = max(FreshRSS_Context::$conf->old_entries, 1); + $nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1); $date_min = time() - (3600 * 24 * 30 * $nb_month_old); + // PubSubHubbub support + $pubsubhubbubEnabledGeneral = FreshRSS_Context::$system_conf->pubsubhubbub_enabled; + $pshbMinAge = time() - (3600 * 24); //TODO: Make a configuration. + $updated_feeds = 0; - $is_read = FreshRSS_Context::$conf->mark_when['reception'] ? 1 : 0; + $is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0; foreach ($feeds as $feed) { + $url = $feed->url(); //For detection of HTTP 301 + + $pubSubHubbubEnabled = $pubsubhubbubEnabledGeneral && $feed->pubSubHubbubEnabled(); + if ((!$simplePiePush) && (!$feed_id) && $pubSubHubbubEnabled && ($feed->lastUpdate() > $pshbMinAge)) { + //$text = 'Skip pull of feed using PubSubHubbub: ' . $url; + //Minz_Log::debug($text); + //file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND); + continue; //When PubSubHubbub is used, do not pull refresh so often + } + + $mtime = 0; + $ttl = $feed->ttl(); + if ($ttl == -1) { + continue; //Feed refresh is disabled + } + if ((!$simplePiePush) && (!$feed_id) && + ($feed->lastUpdate() + 10 >= time() - ($ttl == -2 ? FreshRSS_Context::$user_conf->ttl_default : $ttl))) { + //Too early to refresh from source, but check whether the feed was updated by another user + $mtime = $feed->cacheModifiedTime(); + if ($feed->lastUpdate() + 10 >= $mtime) { + continue; //Nothing newer from other users + } + //Minz_Log::debug($feed->url() . ' was updated at ' . date('c', $mtime) . ' by another user'); + //Will take advantage of the newer cache + } + if (!$feed->lock()) { Minz_Log::notice('Feed already being actualized: ' . $feed->url()); continue; } try { - // Load entries - $feed->load(false); + if ($simplePiePush) { + $feed->loadEntries($simplePiePush); //Used by PubSubHubbub + } else { + $feed->load(false, $isNewFeed); + } } catch (FreshRSS_Feed_Exception $e) { - Minz_Log::notice($e->getMessage()); - $feedDAO->updateLastUpdate($feed->id(), 1); + Minz_Log::warning($e->getMessage()); + $feedDAO->updateLastUpdate($feed->id(), true); + $feed->unlock(); continue; } - $url = $feed->url(); $feed_history = $feed->keepHistory(); - if ($feed_history == -2) { + if ($isNewFeed) { + $feed_history = -1; //∞ + } elseif ($feed_history == -2) { // TODO: -2 must be a constant! // -2 means we take the default value from configuration - $feed_history = FreshRSS_Context::$conf->keep_history_default; + $feed_history = FreshRSS_Context::$user_conf->keep_history_default; } // We want chronological order and SimplePie uses reverse order. $entries = array_reverse($feed->entries()); if (count($entries) > 0) { - // For this feed, check last n entry GUIDs already in database. - $existing_guids = array_fill_keys($entryDAO->listLastGuidsByFeed( - $feed->id(), count($entries) + 10 - ), 1); - $use_declared_date = empty($existing_guids); + $newGuids = array(); + foreach ($entries as $entry) { + $newGuids[] = safe_ascii($entry->guid()); + } + // For this feed, check existing GUIDs already in database. + $existingHashForGuids = $entryDAO->listHashForFeedGuids($feed->id(), $newGuids); + unset($newGuids); + $oldGuids = array(); // Add entries in database if possible. - $prepared_statement = $entryDAO->addEntryPrepare(); - $feedDAO->beginTransaction(); foreach ($entries as $entry) { $entry_date = $entry->date(true); - if (isset($existing_guids[$entry->guid()]) || - ($feed_history == 0 && $entry_date < $date_min)) { - // This entry already exists in DB or should not be added - // considering configuration and date. - continue; - } - - $id = uTimeString(); - if ($use_declared_date || $entry_date < $date_min) { - // Use declared date at first import. - $id = min(time(), $entry_date) . uSecString(); + if (isset($existingHashForGuids[$entry->guid()])) { + $existingHash = $existingHashForGuids[$entry->guid()]; + if (strcasecmp($existingHash, $entry->hash()) === 0 || trim($existingHash, '0') == '') { + //This entry already exists and is unchanged. TODO: Remove the test with the zero'ed hash in FreshRSS v1.3 + $oldGuids[] = $entry->guid(); + } else { //This entry already exists but has been updated + //Minz_Log::debug('Entry with GUID `' . $entry->guid() . '` updated in feed ' . $feed->id() . + //', old hash ' . $existingHash . ', new hash ' . $entry->hash()); + //TODO: Make an updated/is_read policy by feed, in addition to the global one. + $entry->_isRead(FreshRSS_Context::$user_conf->mark_updated_article_unread ? false : null); //Change is_read according to policy. + if (!$entryDAO->inTransaction()) { + $entryDAO->beginTransaction(); + } + $entryDAO->updateEntry($entry->toArray()); + } + } elseif ($feed_history == 0 && $entry_date < $date_min) { + // This entry should not be added considering configuration and date. + $oldGuids[] = $entry->guid(); + } else { + if ($isNewFeed) { + $id = min(time(), $entry_date) . uSecString(); + } elseif ($entry_date < $date_min) { + $id = min(time(), $entry_date) . uSecString(); + $entry->_isRead(true); //Old article that was not in database. Probably an error, so mark as read + } else { + $id = uTimeString(); + $entry->_isRead($is_read); + } + $entry->_id($id); + + $entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry); + if ($entry === null) { + // An extension has returned a null value, there is nothing to insert. + continue; + } + + if ($pubSubHubbubEnabled && !$simplePiePush) { //We use push, but have discovered an article by pull! + $text = 'An article was discovered by pull although we use PubSubHubbub!: Feed ' . $url . ' GUID ' . $entry->guid(); + file_put_contents(USERS_PATH . '/_/log_pshb.txt', date('c') . "\t" . $text . "\n", FILE_APPEND); + Minz_Log::warning($text); + $pubSubHubbubEnabled = false; + $feed->pubSubHubbubError(true); + } + + if (!$entryDAO->inTransaction()) { + $entryDAO->beginTransaction(); + } + $entryDAO->addEntry($entry->toArray()); } - - $values = $entry->toArray(); - $values['id'] = $id; - $values['is_read'] = $is_read; - $entryDAO->addEntry($values, $prepared_statement); } + $entryDAO->updateLastSeen($feed->id(), $oldGuids, $mtime); } if ($feed_history >= 0 && rand(0, 30) === 1) { // TODO: move this function in web cron when available (see entry::purge) // Remove old entries once in 30. - if (!$feedDAO->hasTransaction()) { - $feedDAO->beginTransaction(); + if (!$entryDAO->inTransaction()) { + $entryDAO->beginTransaction(); } $nb = $feedDAO->cleanOldEntries($feed->id(), @@ -347,18 +393,37 @@ class FreshRSS_feed_Controller extends Minz_ActionController { } } - $feedDAO->updateLastUpdate($feed->id(), 0, $feedDAO->hasTransaction()); - if ($feedDAO->hasTransaction()) { - $feedDAO->commit(); + $feedDAO->updateLastUpdate($feed->id(), false, $entryDAO->inTransaction(), $mtime); + if ($entryDAO->inTransaction()) { + $entryDAO->commit(); } - if ($feed->url() !== $url) { - // HTTP 301 Moved Permanently + if ($feed->hubUrl() && $feed->selfUrl()) { //selfUrl has priority for PubSubHubbub + if ($feed->selfUrl() !== $url) { //https://code.google.com/p/pubsubhubbub/wiki/MovingFeedsOrChangingHubs + $selfUrl = checkUrl($feed->selfUrl()); + if ($selfUrl) { + Minz_Log::debug('PubSubHubbub unsubscribe ' . $feed->url()); + if (!$feed->pubSubHubbubSubscribe(false)) { //Unsubscribe + Minz_Log::warning('Error while PubSubHubbub unsubscribing from ' . $feed->url()); + } + $feed->_url($selfUrl, false); + Minz_Log::notice('Feed ' . $url . ' canonical address moved to ' . $feed->url()); + $feedDAO->updateFeed($feed->id(), array('url' => $feed->url())); + } + } + } + elseif ($feed->url() !== $url) { // HTTP 301 Moved Permanently Minz_Log::notice('Feed ' . $url . ' moved permanently to ' . $feed->url()); $feedDAO->updateFeed($feed->id(), array('url' => $feed->url())); } $feed->faviconPrepare(); + if ($pubsubhubbubEnabledGeneral && $feed->pubSubHubbubPrepare()) { + Minz_Log::notice('PubSubHubbub subscribe ' . $feed->url()); + if (!$feed->pubSubHubbubSubscribe(true)) { //Subscribe + Minz_Log::warning('Error while PubSubHubbub subscribing to ' . $feed->url()); + } + } $feed->unlock(); $updated_feeds++; unset($feed); @@ -369,6 +434,26 @@ class FreshRSS_feed_Controller extends Minz_ActionController { break; } } + return array($updated_feeds, reset($feeds)); + } + + /** + * This action actualizes entries from one or several feeds. + * + * Parameters are: + * - id (default: false): Feed ID + * - url (default: false): Feed URL + * - force (default: false) + * If id and url are not specified, all the feeds are actualized. But if force is + * false, process stops at 10 feeds to avoid time execution problem. + */ + public function actualizeAction() { + Minz_Session::_param('actualize_feeds', false); + $id = Minz_Request::param('id'); + $url = Minz_Request::param('url'); + $force = Minz_Request::param('force'); + + list($updated_feeds, $feed) = self::actualizeFeed($id, $url, $force); if (Minz_Request::param('ajax')) { // Most of the time, ajax request is for only one feed. But since @@ -376,24 +461,56 @@ class FreshRSS_feed_Controller extends Minz_ActionController { // are several updated feeds. $notif = array( 'type' => 'good', - 'content' => _t('feeds_actualized') + 'content' => _t('feedback.sub.feed.actualizeds') ); Minz_Session::_param('notification', $notif); // No layout in ajax request. $this->view->_useLayout(false); - return; + } else { + // Redirect to the main page with correct notification. + if ($updated_feeds === 1) { + Minz_Request::good(_t('feedback.sub.feed.actualized', $feed->name()), array( + 'params' => array('get' => 'f_' . $feed->id()) + )); + } elseif ($updated_feeds > 1) { + Minz_Request::good(_t('feedback.sub.feed.n_actualized', $updated_feeds), array()); + } else { + Minz_Request::good(_t('feedback.sub.feed.no_refresh'), array()); + } } + return $updated_feeds; + } - // Redirect to the main page with correct notification. - if ($updated_feeds === 1) { - $feed = reset($feeds); - Minz_Request::good(_t('feed_actualized', $feed->name()), - array('get' => 'f_' . $feed->id())); - } elseif ($updated_feeds > 1) { - Minz_Request::good(_t('n_feeds_actualized', $updated_feeds), array()); - } else { - Minz_Request::good(_t('no_feed_to_refresh'), array()); + public static function renameFeed($feed_id, $feed_name) { + if ($feed_id <= 0 || $feed_name == '') { + return false; + } + FreshRSS_UserDAO::touch(); + $feedDAO = FreshRSS_Factory::createFeedDao(); + return $feedDAO->updateFeed($feed_id, array('name' => $feed_name)); + } + + public static function moveFeed($feed_id, $cat_id, $new_cat_name = '') { + if ($feed_id <= 0 || ($cat_id <= 0 && $new_cat_name == '')) { + return false; + } + FreshRSS_UserDAO::touch(); + + $catDAO = new FreshRSS_CategoryDAO(); + if ($cat_id > 0) { + $cat = $catDAO->searchById($cat_id); + $cat_id = $cat == null ? 0 : $cat->id(); + } + if ($cat_id <= 1 && $new_cat_name != '') { + $cat_id = $catDAO->addCategory(array('name' => $new_cat_name)); } + if ($cat_id <= 1) { + $catDAO->checkDefault(); + $cat_id = FreshRSS_CategoryDAO::defaultCategoryId; + } + + $feedDAO = FreshRSS_Factory::createFeedDao(); + return $feedDAO->updateFeed($feed_id, array('category' => $cat_id)); } /** @@ -416,31 +533,31 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $feed_id = Minz_Request::param('f_id'); $cat_id = Minz_Request::param('c_id'); - if ($cat_id === false) { - // If category was not given get the default one. - $catDAO = new FreshRSS_CategoryDAO(); - $catDAO->checkDefault(); - $def_cat = $catDAO->getDefault(); - $cat_id = $def_cat->id(); - } - - $feedDAO = FreshRSS_Factory::createFeedDao(); - $values = array('category' => $cat_id); - - $feed = $feedDAO->searchById($feed_id); - if ($feed && ($feed->category() == $cat_id || - $feedDAO->updateFeed($feed_id, $values))) { + if (self::moveFeed($feed_id, $cat_id)) { // TODO: return something useful } else { Minz_Log::warning('Cannot move feed `' . $feed_id . '` ' . 'in the category `' . $cat_id . '`'); - Minz_Error::error( - 404, - array('error' => array(_t('error_occurred'))) - ); + Minz_Error::error(404); } } + public static function deleteFeed($feed_id) { + FreshRSS_UserDAO::touch(); + $feedDAO = FreshRSS_Factory::createFeedDao(); + if ($feedDAO->deleteFeed($feed_id)) { + // TODO: Delete old favicon + + // Remove related queries + FreshRSS_Context::$user_conf->queries = remove_query_by_get( + 'f_' . $feed_id, FreshRSS_Context::$user_conf->queries); + FreshRSS_Context::$user_conf->save(); + + return true; + } + return false; + } + /** * This action deletes a feed. * @@ -459,23 +576,16 @@ class FreshRSS_feed_Controller extends Minz_ActionController { if (!$redirect_url) { $redirect_url = array('c' => 'subscription', 'a' => 'index'); } - if (!Minz_Request::isPost()) { Minz_Request::forward($redirect_url, true); } $id = Minz_Request::param('id'); - $feedDAO = FreshRSS_Factory::createFeedDao(); - if ($feedDAO->deleteFeed($id)) { - // TODO: Delete old favicon - - // Remove related queries - FreshRSS_Context::$conf->remove_query_by_get('f_' . $id); - FreshRSS_Context::$conf->save(); - Minz_Request::good(_t('feed_deleted'), $redirect_url); + if (self::deleteFeed($id)) { + Minz_Request::good(_t('feedback.sub.feed.deleted'), $redirect_url); } else { - Minz_Request::bad(_t('error_occurred'), $redirect_url); + Minz_Request::bad(_t('feedback.sub.feed.error'), $redirect_url); } } } diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php index ab277e688..6ae89defb 100644 --- a/app/Controllers/importExportController.php +++ b/app/Controllers/importExportController.php @@ -11,10 +11,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { */ public function firstAction() { if (!FreshRSS_Auth::hasAccess()) { - Minz_Error::error( - 403, - array('error' => array(_t('access_denied'))) - ); + Minz_Error::error(403); } require_once(LIB_PATH . '/lib_opml.php'); @@ -29,35 +26,17 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { */ public function indexAction() { $this->view->feeds = $this->feedDAO->listFeeds(); - Minz_View::prependTitle(_t('import_export') . ' · '); + Minz_View::prependTitle(_t('sub.import_export.title') . ' · '); } - /** - * This action handles import action. - * - * It must be reached by a POST request. - * - * Parameter is: - * - file (default: nothing!) - * Available file types are: zip, json or xml. - */ - public function importAction() { - if (!Minz_Request::isPost()) { - Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true); - } - - $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')); - } + public function importFile($name, $path, $username = null) { + require_once(LIB_PATH . '/lib_opml.php'); - @set_time_limit(300); + $this->catDAO = new FreshRSS_CategoryDAO($username); + $this->entryDAO = FreshRSS_Factory::createEntryDao($username); + $this->feedDAO = FreshRSS_Factory::createFeedDao($username); - $type_file = $this->guessFileType($file['name']); + $type_file = self::guessFileType($name); $list_files = array( 'opml' => array(), @@ -68,21 +47,17 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { // 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']); - + $zip = zip_open($path); 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')); + throw new FreshRSS_Zip_Exception($zip); } - 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); + throw new FreshRSS_Zip_Exception($zipfile); } else { - $type_zipfile = $this->guessFileType(zip_entry_name($zipfile)); + $type_zipfile = self::guessFileType(zip_entry_name($zipfile)); if ($type_file !== 'unknown') { $list_files[$type_zipfile][] = zip_entry_read( $zipfile, @@ -91,35 +66,94 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { } } } - zip_close($zip); } elseif ($type_file === 'zip') { - // Zip extension is not loaded - Minz_Request::bad(_t('no_zip_extension'), - array('c' => 'importExport', 'a' => 'index')); + // ZIP extension is not loaded + throw new FreshRSS_ZipMissing_Exception(); } elseif ($type_file !== 'unknown') { - $list_files[$type_file][] = file_get_contents($file['tmp_name']); + $list_files[$type_file][] = file_get_contents($path); } // Import file contents. // OPML first(so categories and feeds are imported) // Starred articles then so the "favourite" status is already set // And finally all other files. - $error = false; + $ok = true; foreach ($list_files['opml'] as $opml_file) { - $error = $this->importOpml($opml_file); + if (!$this->importOpml($opml_file)) { + $ok = false; + if (FreshRSS_Context::$isCli) { + fwrite(STDERR, 'FreshRSS error during OPML import' . "\n"); + } else { + Minz_Log::warning('Error during OPML import'); + } + } } foreach ($list_files['json_starred'] as $article_file) { - $error = $this->importJson($article_file, true); + if (!$this->importJson($article_file, true)) { + $ok = false; + if (FreshRSS_Context::$isCli) { + fwrite(STDERR, 'FreshRSS error during JSON stars import' . "\n"); + } else { + Minz_Log::warning('Error during JSON stars import'); + } + } } foreach ($list_files['json_feed'] as $article_file) { - $error = $this->importJson($article_file); + if (!$this->importJson($article_file)) { + $ok = false; + if (FreshRSS_Context::$isCli) { + fwrite(STDERR, 'FreshRSS error during JSON feeds import' . "\n"); + } else { + Minz_Log::warning('Error during JSON feeds import'); + } + } + } + + return $ok; + } + + /** + * This action handles import action. + * + * It must be reached by a POST request. + * + * Parameter is: + * - file (default: nothing!) + * Available file types are: zip, json or xml. + */ + public function importAction() { + if (!Minz_Request::isPost()) { + Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true); + } + + $file = $_FILES['file']; + $status_file = $file['error']; + + if ($status_file !== 0) { + Minz_Log::warning('File cannot be uploaded. Error code: ' . $status_file); + Minz_Request::bad(_t('feedback.import_export.file_cannot_be_uploaded'), + array('c' => 'importExport', 'a' => 'index')); + } + + @set_time_limit(300); + + $error = false; + try { + $error = !$this->importFile($file['name'], $file['tmp_name']); + } catch (FreshRSS_ZipMissing_Exception $zme) { + Minz_Request::bad(_t('feedback.import_export.no_zip_extension'), + array('c' => 'importExport', 'a' => 'index')); + } catch (FreshRSS_Zip_Exception $ze) { + Minz_Log::warning('ZIP archive cannot be imported. Error code: ' . $ze->zipErrorCode()); + Minz_Request::bad(_t('feedback.import_export.zip_error'), + array('c' => 'importExport', 'a' => 'index')); } // And finally, we get import status and redirect to the home page Minz_Session::_param('actualize_feeds', true); - $content_notif = $error === true ? _t('feeds_imported_with_errors') : - _t('feeds_imported'); + $content_notif = $error === true ? _t('feedback.import_export.feeds_imported_with_errors') : + _t('feedback.import_export.feeds_imported'); Minz_Request::good($content_notif); } @@ -128,10 +162,8 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { * * Itis a *very* basic guess file type function. Only based on filename. * That's could be improved but should be enough for what we have to do. - * - * @todo move into lib_rss.php */ - private function guessFileType($filename) { + private static function guessFileType($filename) { if (substr_compare($filename, '.zip', -4) === 0) { return 'zip'; } elseif (substr_compare($filename, '.opml', -5) === 0 || @@ -151,15 +183,19 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { * This method parses and imports an OPML file. * * @param string $opml_file the OPML file content. - * @return boolean true if an error occured, false else. + * @return boolean false if an error occured, true otherwise. */ private function importOpml($opml_file) { $opml_array = array(); try { - $opml_array = libopml_parse_string($opml_file); + $opml_array = libopml_parse_string($opml_file, false); } catch (LibOPML_Exception $e) { - Minz_Log::warning($e->getMessage()); - return true; + if (FreshRSS_Context::$isCli) { + fwrite(STDERR, 'FreshRSS error during OPML parsing: ' . $e->getMessage() . "\n"); + } else { + Minz_Log::warning($e->getMessage()); + } + return false; } $this->catDAO->checkDefault(); @@ -172,27 +208,49 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { * * @param array $opml_elements an OPML element (body or outline). * @param string $parent_cat the name of the parent category. - * @return boolean true if an error occured, false else. + * @return boolean false if an error occured, true otherwise. */ private function addOpmlElements($opml_elements, $parent_cat = null) { - $error = false; + $ok = true; + + $nb_feeds = count($this->feedDAO->listFeeds()); + $nb_cats = count($this->catDAO->listCategories(false)); + $limits = FreshRSS_Context::$system_conf->limits; + foreach ($opml_elements as $elt) { - $res = false; if (isset($elt['xmlUrl'])) { // If xmlUrl exists, it means it is a feed - $res = $this->addFeedOpml($elt, $parent_cat); + if (FreshRSS_Context::$isCli && $nb_feeds >= $limits['max_feeds']) { + Minz_Log::warning(_t('feedback.sub.feed.over_max', + $limits['max_feeds'])); + $ok = false; + continue; + } + + if ($this->addFeedOpml($elt, $parent_cat)) { + $nb_feeds++; + } else { + $ok = false; + } } else { // No xmlUrl? It should be a category! - $res = $this->addCategoryOpml($elt, $parent_cat); - } + $limit_reached = ($nb_cats >= $limits['max_categories']); + if (!FreshRSS_Context::$isCli && $limit_reached) { + Minz_Log::warning(_t('feedback.sub.category.over_max', + $limits['max_categories'])); + $ok = false; + continue; + } - if (!$error && $res) { - // oops: there is at least one error! - $error = $res; + if ($this->addCategoryOpml($elt, $parent_cat, $limit_reached)) { + $nb_cats++; + } else { + $ok = false; + } } } - return $error; + return $ok; } /** @@ -200,19 +258,23 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { * * @param array $feed_elt an OPML element (must be a feed element). * @param string $parent_cat the name of the parent category. - * @return boolean true if an error occured, false else. + * @return boolean false if an error occured, true otherwise. */ private function addFeedOpml($feed_elt, $parent_cat) { - if (is_null($parent_cat)) { + if ($parent_cat == null) { // This feed has no parent category so we get the default one - $parent_cat = $this->catDAO->getDefault()->name(); + $this->catDAO->checkDefault(); + $default_cat = $this->catDAO->getDefault(); + $parent_cat = $default_cat->name(); } $cat = $this->catDAO->searchByName($parent_cat); - if (!$cat) { + if ($cat == null) { // If there is not $cat, it means parent category does not exist in - // database. It should not happened! - return true; + // database. + // If it happens, take the default category. + $this->catDAO->checkDefault(); + $cat = $this->catDAO->getDefault(); } // We get different useful information @@ -236,16 +298,34 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $feed->_website($website); $feed->_description($description); - // addFeedObject checks if feed is already in DB so nothing else to - // check here - $id = $this->feedDAO->addFeedObject($feed); - $error = ($id === false); + // Call the extension hook + $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed); + if ($feed != null) { + // addFeedObject checks if feed is already in DB so nothing else to + // check here + $id = $this->feedDAO->addFeedObject($feed); + $error = ($id === false); + } else { + $error = true; + } } catch (FreshRSS_Feed_Exception $e) { - Minz_Log::warning($e->getMessage()); + if (FreshRSS_Context::$isCli) { + fwrite(STDERR, 'FreshRSS error during OPML feed import: ' . $e->getMessage() . "\n"); + } else { + Minz_Log::warning($e->getMessage()); + } $error = true; } - return $error; + if ($error) { + if (FreshRSS_Context::$isCli) { + fwrite(STDERR, 'FreshRSS error during OPML feed import from URL: ' . $url . ' in category ' . $cat->id() . "\n"); + } else { + Minz_Log::warning('Error during OPML feed import from URL: ' . $url . ' in category ' . $cat->id()); + } + } + + return !$error; } /** @@ -253,26 +333,36 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { * * @param array $cat_elt an OPML element (must be a category element). * @param string $parent_cat the name of the parent category. - * @return boolean true if an error occured, false else. + * @param boolean $cat_limit_reached indicates if category limit has been reached. + * if yes, category is not added (but we try for feeds!) + * @return boolean false if an error occured, true otherwise. */ - private function addCategoryOpml($cat_elt, $parent_cat) { + private function addCategoryOpml($cat_elt, $parent_cat, $cat_limit_reached) { // Create a new Category object - $cat = new FreshRSS_Category(Minz_Helper::htmlspecialchars_utf8($cat_elt['text'])); + $catName = Minz_Helper::htmlspecialchars_utf8($cat_elt['text']); + $cat = new FreshRSS_Category($catName); - $id = $this->catDAO->addCategoryObject($cat); - $error = ($id === false); + $error = true; + if (FreshRSS_Context::$isCli || !$cat_limit_reached) { + $id = $this->catDAO->addCategoryObject($cat); + $error = ($id === false); + } + if ($error) { + if (FreshRSS_Context::$isCli) { + fwrite(STDERR, 'FreshRSS error during OPML category import from URL: ' . $catName . "\n"); + } else { + Minz_Log::warning('Error during OPML category import from URL: ' . $catName); + } + } if (isset($cat_elt['@outlines'])) { // Our cat_elt contains more categories or more feeds, so we // add them recursively. // Note: FreshRSS does not support yet category arborescence - $res = $this->addOpmlElements($cat_elt['@outlines'], $cat->name()); - if (!$error && $res) { - $error = true; - } + $error &= !$this->addOpmlElements($cat_elt['@outlines'], $catName); } - return $error; + return !$error; } /** @@ -280,34 +370,66 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { * * @param string $article_file the JSON file content. * @param boolean $starred true if articles from the file must be starred. - * @return boolean true if an error occured, false else. + * @return boolean false if an error occured, true otherwise. */ private function importJson($article_file, $starred = false) { $article_object = json_decode($article_file, true); - if (is_null($article_object)) { - Minz_Log::warning('Try to import a non-JSON file'); - return true; + if ($article_object == null) { + if (FreshRSS_Context::$isCli) { + fwrite(STDERR, 'FreshRSS error trying to import a non-JSON file' . "\n"); + } else { + Minz_Log::warning('Try to import a non-JSON file'); + } + return false; } - $is_read = FreshRSS_Context::$conf->mark_when['reception'] ? 1 : 0; + $is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0; $google_compliant = strpos($article_object['id'], 'com.google') !== false; $error = false; $article_to_feed = array(); + $nb_feeds = count($this->feedDAO->listFeeds()); + $limits = FreshRSS_Context::$system_conf->limits; + // First, we check feeds of articles are in DB (and add them if needed). foreach ($article_object['items'] as $item) { - $feed = $this->addFeedJson($item['origin'], $google_compliant); - if (is_null($feed)) { - $error = true; - } else { + $key = $google_compliant ? 'htmlUrl' : 'feedUrl'; + $feed = new FreshRSS_Feed($item['origin'][$key]); + $feed = $this->feedDAO->searchByUrl($feed->url()); + + if ($feed == null) { + // Feed does not exist in DB,we should to try to add it. + if ((!FreshRSS_Context::$isCli) && ($nb_feeds >= $limits['max_feeds'])) { + // Oops, no more place! + Minz_Log::warning(_t('feedback.sub.feed.over_max', $limits['max_feeds'])); + } else { + $feed = $this->addFeedJson($item['origin'], $google_compliant); + } + + if ($feed == null) { + // Still null? It means something went wrong. + $error = true; + } else { + $nb_feeds++; + } + } + + if ($feed != null) { $article_to_feed[$item['id']] = $feed->id(); } } + $newGuids = array(); + foreach ($article_object['items'] as $item) { + $newGuids[] = safe_ascii($item['id']); + } + // For this feed, check existing GUIDs already in database. + $existingHashForGuids = $this->entryDAO->listHashForFeedGuids($feed->id(), $newGuids); + unset($newGuids); + // Then, articles are imported. - $prepared_statement = $this->entryDAO->addEntryPrepare(); $this->entryDAO->beginTransaction(); foreach ($article_object['items'] as $item) { if (!isset($article_to_feed[$item['id']])) { @@ -323,7 +445,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { if ($google_compliant) { // Remove tags containing "/state/com.google" which are useless. $tags = array_filter($tags, function($var) { - return strpos($var, '/state/com.google') === false; + return strpos($var, '/state/com.google') !== false; }); } @@ -335,8 +457,18 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $entry->_id(min(time(), $entry->date(true)) . uSecString()); $entry->_tags($tags); + $entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry); + if ($entry == null) { + // An extension has returned a null value, there is nothing to insert. + continue; + } + $values = $entry->toArray(); - $id = $this->entryDAO->addEntry($values, $prepared_statement); + if (isset($existingHashForGuids[$entry->guid()])) { + $id = $this->entryDAO->updateEntry($values); + } else { + $id = $this->entryDAO->addEntry($values); + } if (!$error && ($id === false)) { $error = true; @@ -344,7 +476,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { } $this->entryDAO->commit(); - return $error; + return !$error; } /** @@ -356,8 +488,6 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { * else null. */ private function addFeedJson($origin, $google_compliant) { - $default_cat = $this->catDAO->getDefault(); - $return = null; $key = $google_compliant ? 'htmlUrl' : 'feedUrl'; $url = $origin[$key]; @@ -367,80 +497,117 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { try { // Create a Feed object and add it in database. $feed = new FreshRSS_Feed($url); - $feed->_category($default_cat->id()); + $feed->_category(FreshRSS_CategoryDAO::defaultCategoryId); $feed->_name($name); $feed->_website($website); - // addFeedObject checks if feed is already in DB so nothing else to - // check here. - $id = $this->feedDAO->addFeedObject($feed); + // Call the extension hook + $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed); + if ($feed != null) { + // addFeedObject checks if feed is already in DB so nothing else to + // check here. + $id = $this->feedDAO->addFeedObject($feed); - if ($id !== false) { - $feed->_id($id); - $return = $feed; + if ($id !== false) { + $feed->_id($id); + $return = $feed; + } } } catch (FreshRSS_Feed_Exception $e) { - Minz_Log::warning($e->getMessage()); + if (FreshRSS_Context::$isCli) { + fwrite(STDERR, 'FreshRSS error during JSON feed import: ' . $e->getMessage() . "\n"); + } else { + Minz_Log::warning($e->getMessage()); + } } return $return; } - /** - * This action handles export action. - * - * This action must be reached by a POST request. - * - * Parameters are: - * - export_opml (default: false) - * - export_starred (default: false) - * - export_feeds (default: array()) a list of feed ids - */ - public function exportAction() { - if (!Minz_Request::isPost()) { - Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true); - } + public function exportFile($export_opml = true, $export_starred = false, $export_feeds = array(), $maxFeedEntries = 50, $username = null) { + require_once(LIB_PATH . '/lib_opml.php'); - $this->view->_useLayout(false); + $this->catDAO = new FreshRSS_CategoryDAO($username); + $this->entryDAO = FreshRSS_Factory::createEntryDao($username); + $this->feedDAO = FreshRSS_Factory::createFeedDao($username); + + $this->entryDAO->disableBuffering(); + + if ($export_feeds === true) { + //All feeds + $export_feeds = $this->feedDAO->listFeedsIds(); + } + if (!is_array($export_feeds)) { + $export_feeds = array(); + } - $export_opml = Minz_Request::param('export_opml', false); - $export_starred = Minz_Request::param('export_starred', false); - $export_feeds = Minz_Request::param('export_feeds', array()); + $day = date('Y-m-d'); $export_files = array(); if ($export_opml) { - $export_files['feeds.opml'] = $this->generateOpml(); + $export_files["feeds_${day}.opml.xml"] = $this->generateOpml(); } if ($export_starred) { - $export_files['starred.json'] = $this->generateEntries('starred'); + $export_files["starred_${day}.json"] = $this->generateEntries('starred'); } foreach ($export_feeds as $feed_id) { $feed = $this->feedDAO->searchById($feed_id); if ($feed) { - $filename = 'feed_' . $feed->category() . '_' + $filename = "feed_${day}_" . $feed->category() . '_' . $feed->id() . '.json'; - $export_files[$filename] = $this->generateEntries('feed', $feed); + $export_files[$filename] = $this->generateEntries('feed', $feed, $maxFeedEntries); } } $nb_files = count($export_files); if ($nb_files > 1) { - // If there are more than 1 file to export, we need a zip archive. + // If there are more than 1 file to export, we need a ZIP archive. try { - $this->exportZip($export_files); + $this->sendZip($export_files); } catch (Exception $e) { - # Oops, there is no Zip extension! - Minz_Request::bad(_t('export_no_zip_extension'), - array('c' => 'importExport', 'a' => 'index')); + throw new FreshRSS_ZipMissing_Exception($e); } } 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 { + $type = self::guessFileType($filename); + $this->sendFile('freshrss_' . $filename, $export_files[$filename], $type); + } + return $nb_files; + } + + /** + * This action handles export action. + * + * This action must be reached by a POST request. + * + * Parameters are: + * - export_opml (default: false) + * - export_starred (default: false) + * - export_feeds (default: array()) a list of feed ids + */ + public function exportAction() { + if (!Minz_Request::isPost()) { + Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true); + } + $this->view->_useLayout(false); + + $nb_files = 0; + try { + $nb_files = $this->exportFile( + Minz_Request::param('export_opml', false), + Minz_Request::param('export_starred', false), + Minz_Request::param('export_feeds', array()) + ); + } catch (FreshRSS_ZipMissing_Exception $zme) { + # Oops, there is no ZIP extension! + Minz_Request::bad(_t('feedback.import_export.export_no_zip_extension'), + array('c' => 'importExport', 'a' => 'index')); + } + + if ($nb_files < 1) { // Nothing to do... Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true); } @@ -469,22 +636,22 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { * @param FreshRSS_Feed $feed feed of which we want to get entries. * @return string the JSON file content. */ - private function generateEntries($type, $feed = NULL) { + private function generateEntries($type, $feed = NULL, $maxFeedEntries = 50) { $this->view->categories = $this->catDAO->listCategories(); if ($type == 'starred') { - $this->view->list_title = _t('starred_list'); + $this->view->list_title = _t('sub.import_export.starred_list'); $this->view->type = 'starred'; $unread_fav = $this->entryDAO->countUnreadReadFavorites(); - $this->view->entries = $this->entryDAO->listWhere( + $this->view->entriesRaw = $this->entryDAO->listWhereRaw( 's', '', FreshRSS_Entry::STATE_ALL, 'ASC', $unread_fav['all'] ); - } elseif ($type == 'feed' && !is_null($feed)) { - $this->view->list_title = _t('feed_list', $feed->name()); + } elseif ($type === 'feed' && $feed != null) { + $this->view->list_title = _t('sub.import_export.feed_list', $feed->name()); $this->view->type = 'feed/' . $feed->id(); - $this->view->entries = $this->entryDAO->listWhere( + $this->view->entriesRaw = $this->entryDAO->listWhereRaw( 'f', $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC', - FreshRSS_Context::$conf->posts_per_page + $maxFeedEntries ); $this->view->feed = $feed; } @@ -498,7 +665,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { * @param array $files list of files where key is filename and value the content. * @throws Exception if Zip extension is not loaded. */ - private function exportZip($files) { + private function sendZip($files) { if (!extension_loaded('zip')) { throw new Exception(); } @@ -516,7 +683,8 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $zip->close(); header('Content-Type: application/zip'); header('Content-Length: ' . filesize($zip_file)); - header('Content-Disposition: attachment; filename="freshrss_export.zip"'); + $day = date('Y-m-d'); + header('Content-Disposition: attachment; filename="freshrss_' . $day . '_export.zip"'); readfile($zip_file); unlink($zip_file); } @@ -529,16 +697,16 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { * @param string $type the file type (opml, json_feed or json_starred). * If equals to unknown, nothing happens. */ - private function exportFile($filename, $content, $type) { + private function sendFile($filename, $content, $type) { if ($type === 'unknown') { return; } $content_type = ''; if ($type === 'opml') { - $content_type = "text/opml"; + $content_type = 'application/xml'; } elseif ($type === 'json_feed' || $type === 'json_starred') { - $content_type = "text/json"; + $content_type = 'application/json'; } header('Content-Type: ' . $content_type . '; charset=utf-8'); diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php index f994e257c..5ca147ff3 100755 --- a/app/Controllers/indexController.php +++ b/app/Controllers/indexController.php @@ -1,220 +1,261 @@ <?php +/** + * This class handles main actions of FreshRSS. + */ class FreshRSS_index_Controller extends Minz_ActionController { - private $nb_not_read_cat = 0; + /** + * This action only redirect on the default view mode (normal or global) + */ public function indexAction() { - $output = Minz_Request::param('output'); - $token = FreshRSS_Context::$conf->token; - - // check if user is logged in - if (!FreshRSS_Auth::hasAccess() && !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(_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' => 'auth', 'a' => 'login')); - return; - } - } + $prefered_output = FreshRSS_Context::$user_conf->view_mode; + Minz_Request::forward(array( + 'c' => 'index', + 'a' => $prefered_output + )); + } - $params = Minz_Request::params(); - if (isset($params['search'])) { - $params['search'] = urlencode($params['search']); + /** + * This action displays the normal view of FreshRSS. + */ + public function normalAction() { + $allow_anonymous = FreshRSS_Context::$system_conf->allow_anonymous; + if (!FreshRSS_Auth::hasAccess() && !$allow_anonymous) { + Minz_Request::forward(array('c' => 'auth', 'a' => 'login')); + return; } - $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'))); + try { + $this->updateContext(); + } catch (FreshRSS_Context_Exception $e) { + Minz_Error::error(404); } - $catDAO = new FreshRSS_CategoryDAO(); - $entryDAO = FreshRSS_Factory::createEntryDao(); + $this->view->callbackBeforeContent = function($view) { + try { + FreshRSS_Context::$number++; //+1 for pagination + $entries = FreshRSS_index_Controller::listEntriesByContext(); + FreshRSS_Context::$number--; - $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::debug('Not found [' . $getType . '][' . $getId . ']'); - Minz_Error::error( - 404, - array('error' => array(_t('page_not_found'))) - ); - return; - } + $nb_entries = count($entries); + if ($nb_entries > FreshRSS_Context::$number) { + // We have more elements for pagination + $last_entry = array_pop($entries); + FreshRSS_Context::$next_id = $last_entry->id(); + } - // 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 . - ' · ' - ); + $first_entry = $nb_entries > 0 ? $entries[0] : null; + FreshRSS_Context::$id_max = $first_entry === null ? + (time() - 1) . '000000' : + $first_entry->id(); + if (FreshRSS_Context::$order === 'ASC') { + // In this case we do not know but we guess id_max + $id_max = (time() - 1) . '000000'; + if (strcmp($id_max, FreshRSS_Context::$id_max) > 0) { + FreshRSS_Context::$id_max = $id_max; + } + } - // On récupère les différents éléments de filtrage - $this->view->state = Minz_Request::param('state', FreshRSS_Context::$conf->default_view); - $state_param = Minz_Request::param('state', null); - $filter = Minz_Request::param('search', ''); - $this->view->order = $order = Minz_Request::param('order', FreshRSS_Context::$conf->sort_order); - $nb = Minz_Request::param('nb', FreshRSS_Context::$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; + $view->entries = $entries; + } catch (FreshRSS_EntriesGetter_Exception $e) { + Minz_Log::notice($e->getMessage()); + Minz_Error::error(404); } - if (!$hasUnread && ($state_param === null)) { - $this->view->state = FreshRSS_Entry::STATE_ALL; + + $view->categories = FreshRSS_Context::$categories; + + $view->rss_title = FreshRSS_Context::$name . ' | ' . Minz_View::title(); + $title = FreshRSS_Context::$name; + if (FreshRSS_Context::$get_unread > 0) { + $title = '(' . FreshRSS_Context::$get_unread . ') ' . $title; } + Minz_View::prependTitle($title . ' · '); + }; + } + + /** + * This action displays the reader view of FreshRSS. + * + * @todo: change this view into specific CSS rules? + */ + public function readerAction() { + $this->normalAction(); + } + + /** + * This action displays the global view of FreshRSS. + */ + public function globalAction() { + $allow_anonymous = FreshRSS_Context::$system_conf->allow_anonymous; + if (!FreshRSS_Auth::hasAccess() && !$allow_anonymous) { + Minz_Request::forward(array('c' => 'auth', 'a' => 'login')); + return; } - $this->view->today = @strtotime('today'); + Minz_View::appendScript(Minz_Url::display('/scripts/global_view.js?' . @filemtime(PUBLIC_PATH . '/scripts/global_view.js'))); try { - $entries = $entryDAO->listWhere($getType, $getId, $this->view->state, $order, $nb + 1, $first, $filter); - - // 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::debug('Conflicting information about nbNotRead!'); - $feedDAO = FreshRSS_Factory::createFeedDao(); - try { - $feedDAO->updateCachedValues(); - } catch (Exception $ex) { - Minz_Log::notice('Failed to automatically correct nbNotRead! ' + $ex->getMessage()); - } - $this->view->state = FreshRSS_Entry::STATE_ALL; - $entries = $entryDAO->listWhere($getType, $getId, $this->view->state, $order, $nb, $first, $filter); - } - Minz_Request::_param('state', $this->view->state); + $this->updateContext(); + } catch (FreshRSS_Context_Exception $e) { + Minz_Error::error(404); + } - if (count($entries) <= $nb) { - $this->view->nextId = ''; - } else { //We have more elements for pagination - $lastEntry = array_pop($entries); - $this->view->nextId = $lastEntry->id(); - } + $this->view->categories = FreshRSS_Context::$categories; - $this->view->entries = $entries; + $this->view->rss_title = FreshRSS_Context::$name . ' | ' . Minz_View::title(); + $title = _t('index.feed.title_global'); + if (FreshRSS_Context::$get_unread > 0) { + $title = '(' . FreshRSS_Context::$get_unread . ') ' . $title; + } + Minz_View::prependTitle($title . ' · '); + } + + /** + * This action displays the RSS feed of FreshRSS. + */ + public function rssAction() { + $allow_anonymous = FreshRSS_Context::$system_conf->allow_anonymous; + $token = FreshRSS_Context::$user_conf->token; + $token_param = Minz_Request::param('token', ''); + $token_is_ok = ($token != '' && $token === $token_param); + + // Check if user has access. + if (!FreshRSS_Auth::hasAccess() && + !$allow_anonymous && + !$token_is_ok) { + Minz_Error::error(403); + } + + try { + $this->updateContext(); + } catch (FreshRSS_Context_Exception $e) { + Minz_Error::error(404); + } + + try { + $this->view->entries = FreshRSS_index_Controller::listEntriesByContext(); } catch (FreshRSS_EntriesGetter_Exception $e) { Minz_Log::notice($e->getMessage()); - Minz_Error::error( - 404, - array('error' => array(_t('page_not_found'))) - ); + Minz_Error::error(404); } + + // No layout for RSS output. + $this->view->url = empty($_SERVER['QUERY_STRING']) ? '' : '?' . $_SERVER['QUERY_STRING']; + $this->view->rss_title = FreshRSS_Context::$name . ' | ' . Minz_View::title(); + $this->view->_useLayout(false); + header('Content-Type: application/rss+xml; charset=utf-8'); } - /* - * Vérifie que la catégorie / flux sélectionné existe - * + Initialise correctement les variables de vue get_c et get_f - * + Met à jour la variable $this->nb_not_read_cat + /** + * This action updates the Context object by using request parameters. + * + * Parameters are: + * - state (default: conf->default_view) + * - search (default: empty string) + * - order (default: conf->sort_order) + * - nb (default: conf->posts_per_page) + * - next (default: empty string) + * - hours (default: 0) */ - private function checkAndProcessType($getType, $getId) { - switch($getType) { - case 'a': - $this->view->currentName = _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 = _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; + private function updateContext() { + if (empty(FreshRSS_Context::$categories)) { + $catDAO = new FreshRSS_CategoryDAO(); + FreshRSS_Context::$categories = $catDAO->listCategories(); + } + + // Update number of read / unread variables. + $entryDAO = FreshRSS_Factory::createEntryDao(); + FreshRSS_Context::$total_starred = $entryDAO->countUnreadReadFavorites(); + FreshRSS_Context::$total_unread = FreshRSS_CategoryDAO::CountUnreads( + FreshRSS_Context::$categories, 1 + ); + + FreshRSS_Context::_get(Minz_Request::param('get', 'a')); + + FreshRSS_Context::$state = Minz_Request::param( + 'state', FreshRSS_Context::$user_conf->default_state + ); + $state_forced_by_user = Minz_Request::param('state', false) !== false; + if (FreshRSS_Context::$user_conf->default_view === 'adaptive' && + FreshRSS_Context::$get_unread <= 0 && + !FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_READ) && + !$state_forced_by_user) { + FreshRSS_Context::$state |= FreshRSS_Entry::STATE_READ; + } + + FreshRSS_Context::$search = new FreshRSS_Search(Minz_Request::param('search', '')); + FreshRSS_Context::$order = Minz_Request::param( + 'order', FreshRSS_Context::$user_conf->sort_order + ); + FreshRSS_Context::$number = intval(Minz_Request::param('nb', FreshRSS_Context::$user_conf->posts_per_page)); + if (FreshRSS_Context::$number > FreshRSS_Context::$user_conf->max_posts_per_rss) { + FreshRSS_Context::$number = max( + FreshRSS_Context::$user_conf->max_posts_per_rss, + FreshRSS_Context::$user_conf->posts_per_page); } + FreshRSS_Context::$first_id = Minz_Request::param('next', ''); + FreshRSS_Context::$sinceHours = intval(Minz_Request::param('hours', 0)); } - + + /** + * This method returns a list of entries based on the Context object. + */ + public static function listEntriesByContext() { + $entryDAO = FreshRSS_Factory::createEntryDao(); + + $get = FreshRSS_Context::currentGet(true); + if (count($get) > 1) { + $type = $get[0]; + $id = $get[1]; + } else { + $type = $get; + $id = ''; + } + + $limit = FreshRSS_Context::$number; + + $date_min = 0; + if (FreshRSS_Context::$sinceHours) { + $date_min = time() - (FreshRSS_Context::$sinceHours * 3600); + $limit = FreshRSS_Context::$user_conf->max_posts_per_rss; + } + + $entries = $entryDAO->listWhere( + $type, $id, FreshRSS_Context::$state, FreshRSS_Context::$order, + $limit, FreshRSS_Context::$first_id, + FreshRSS_Context::$search, $date_min + ); + + if (FreshRSS_Context::$sinceHours && (count($entries) < FreshRSS_Context::$user_conf->min_posts_per_rss)) { + $date_min = 0; + $limit = FreshRSS_Context::$user_conf->min_posts_per_rss; + $entries = $entryDAO->listWhere( + $type, $id, FreshRSS_Context::$state, FreshRSS_Context::$order, + $limit, FreshRSS_Context::$first_id, + FreshRSS_Context::$search, $date_min + ); + } + + return $entries; + } + + /** + * This action displays the about page of FreshRSS. + */ public function aboutAction() { - Minz_View::prependTitle(_t('about') . ' · '); + Minz_View::prependTitle(_t('index.about.title') . ' · '); } + /** + * This action displays logs of FreshRSS for the current user. + */ public function logsAction() { if (!FreshRSS_Auth::hasAccess()) { - Minz_Error::error( - 403, - array('error' => array(_t('access_denied'))) - ); + Minz_Error::error(403); } - Minz_View::prependTitle(_t('logs') . ' · '); + Minz_View::prependTitle(_t('index.log.title') . ' · '); if (Minz_Request::isPost()) { FreshRSS_LogDAO::truncate(); diff --git a/app/Controllers/javascriptController.php b/app/Controllers/javascriptController.php index 62f413989..00a7b5c38 100755 --- a/app/Controllers/javascriptController.php +++ b/app/Controllers/javascriptController.php @@ -6,9 +6,9 @@ class FreshRSS_javascript_Controller extends Minz_ActionController { } public function actualizeAction() { - header('Content-Type: text/javascript; charset=UTF-8'); + header('Content-Type: application/json; charset=UTF-8'); $feedDAO = FreshRSS_Factory::createFeedDao(); - $this->view->feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::$conf->ttl_default); + $this->view->feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::$user_conf->ttl_default); } public function nbUnreadsPerFeedAction() { @@ -28,19 +28,27 @@ class FreshRSS_javascript_Controller extends Minz_ActionController { $user = isset($_GET['user']) ? $_GET['user'] : ''; if (ctype_alnum($user)) { try { - $conf = new FreshRSS_Configuration($user); + $salt = FreshRSS_Context::$system_conf->salt; + $conf = get_user_configuration($user); $s = $conf->passwordHash; if (strlen($s) >= 60) { $this->view->salt1 = substr($s, 0, 29); //CRYPT_BLOWFISH Salt: "$2a$", a two digit cost parameter, "$", and 22 characters from the alphabet "./0-9A-Za-z". - $this->view->nonce = sha1(Minz_Configuration::salt() . uniqid(mt_rand(), true)); + $this->view->nonce = sha1($salt . uniqid(mt_rand(), true)); Minz_Session::_param('nonce', $this->view->nonce); return; //Success } } catch (Minz_Exception $me) { Minz_Log::warning('Nonce failure: ' . $me->getMessage()); } + } else { + Minz_Log::notice('Nonce failure due to invalid username!'); + } + //Failure: Return random data. + $this->view->salt1 = sprintf('$2a$%02d$', FreshRSS_user_Controller::BCRYPT_COST); + $alphabet = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for ($i = 22; $i > 0; $i--) { + $this->view->salt1 .= $alphabet[rand(0, 63)]; } - $this->view->nonce = ''; //Failure - $this->view->salt1 = ''; + $this->view->nonce = sha1(rand()); } } diff --git a/app/Controllers/statsController.php b/app/Controllers/statsController.php index 0e3430fcc..5d1dee72c 100644 --- a/app/Controllers/statsController.php +++ b/app/Controllers/statsController.php @@ -6,6 +6,40 @@ class FreshRSS_stats_Controller extends Minz_ActionController { /** + * This action is called before every other action in that class. It is + * the common boiler plate for every action. It is triggered by the + * underlying framework. + */ + public function firstAction() { + if (!FreshRSS_Auth::hasAccess()) { + Minz_Error::error(403); + } + + Minz_View::prependTitle(_t('admin.stats.title') . ' · '); + } + + private function convertToSerie($data) { + $serie = array(); + + foreach ($data as $key => $value) { + $serie[] = array($key, $value); + } + + return $serie; + } + + private function convertToPieSerie($data) { + $serie = array(); + + foreach ($data as $value) { + $value['data'] = array(array(0, (int) $value['data'])); + $serie[] = $value; + } + + return $serie; + } + + /** * This action handles the statistic main page. * * It displays the statistic main page. @@ -20,10 +54,11 @@ class FreshRSS_stats_Controller extends Minz_ActionController { $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->average = $statsDAO->calculateEntryAverage(); - $this->view->feedByCategory = $statsDAO->calculateFeedByCategory(); - $this->view->entryByCategory = $statsDAO->calculateEntryByCategory(); + $entryCount = $statsDAO->calculateEntryCount(); + $this->view->count = $this->convertToSerie($entryCount); + $this->view->average = round(array_sum(array_values($entryCount)) / count($entryCount), 2); + $this->view->feedByCategory = $this->convertToPieSerie($statsDAO->calculateFeedByCategory()); + $this->view->entryByCategory = $this->convertToPieSerie($statsDAO->calculateEntryByCategory()); $this->view->topFeed = $statsDAO->calculateTopFeed(); } @@ -104,27 +139,12 @@ class FreshRSS_stats_Controller extends Minz_ActionController { $this->view->feed = $feedDAO->searchById($id); $this->view->days = $statsDAO->getDays(); $this->view->months = $statsDAO->getMonths(); - $this->view->repartitionHour = $statsDAO->calculateEntryRepartitionPerFeedPerHour($id); + $this->view->repartition = $statsDAO->calculateEntryRepartitionPerFeed($id); + $this->view->repartitionHour = $this->convertToSerie($statsDAO->calculateEntryRepartitionPerFeedPerHour($id)); $this->view->averageHour = $statsDAO->calculateEntryAveragePerFeedPerHour($id); - $this->view->repartitionDayOfWeek = $statsDAO->calculateEntryRepartitionPerFeedPerDayOfWeek($id); + $this->view->repartitionDayOfWeek = $this->convertToSerie($statsDAO->calculateEntryRepartitionPerFeedPerDayOfWeek($id)); $this->view->averageDayOfWeek = $statsDAO->calculateEntryAveragePerFeedPerDayOfWeek($id); - $this->view->repartitionMonth = $statsDAO->calculateEntryRepartitionPerFeedPerMonth($id); + $this->view->repartitionMonth = $this->convertToSerie($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 (!FreshRSS_Auth::hasAccess()) { - Minz_Error::error( - 403, array('error' => array(_t('access_denied'))) - ); - } - - Minz_View::prependTitle(_t('stats') . ' · '); - } - } diff --git a/app/Controllers/subscriptionController.php b/app/Controllers/subscriptionController.php index a89168eb3..03d3ee15e 100644 --- a/app/Controllers/subscriptionController.php +++ b/app/Controllers/subscriptionController.php @@ -11,10 +11,7 @@ class FreshRSS_subscription_Controller extends Minz_ActionController { */ public function firstAction() { if (!FreshRSS_Auth::hasAccess()) { - Minz_Error::error( - 403, - array('error' => array(_t('access_denied'))) - ); + Minz_Error::error(403); } $catDAO = new FreshRSS_CategoryDAO(); @@ -32,7 +29,7 @@ class FreshRSS_subscription_Controller extends Minz_ActionController { public function indexAction() { Minz_View::appendScript(Minz_Url::display('/scripts/category.js?' . @filemtime(PUBLIC_PATH . '/scripts/category.js'))); - Minz_View::prependTitle(_t('subscription_management') . ' · '); + Minz_View::prependTitle(_t('sub.title') . ' · '); $id = Minz_Request::param('id'); if ($id !== false) { @@ -71,23 +68,20 @@ class FreshRSS_subscription_Controller extends Minz_ActionController { $id = Minz_Request::param('id'); if ($id === false || !isset($this->view->feeds[$id])) { - Minz_Error::error( - 404, - array('error' => array(_t('page_not_found'))) - ); + Minz_Error::error(404); return; } $this->view->feed = $this->view->feeds[$id]; - Minz_View::prependTitle(_t('rss_feed_management') . ' · ' . $this->view->feed->name() . ' · '); + Minz_View::prependTitle(_t('sub.title.feed_management') . ' · ' . $this->view->feed->name() . ' · '); if (Minz_Request::isPost()) { - $user = Minz_Request::param('http_user', ''); - $pass = Minz_Request::param('http_pass', ''); + $user = trim(Minz_Request::param('http_user_feed' . $id, '')); + $pass = Minz_Request::param('http_pass_feed' . $id, ''); $httpAuth = ''; - if ($user != '' || $pass != '') { + if ($user != '' && $pass != '') { //TODO: Sanitize $httpAuth = $user . ':' . $pass; } @@ -108,13 +102,14 @@ class FreshRSS_subscription_Controller extends Minz_ActionController { invalidateHttpCache(); - if ($feedDAO->updateFeed($id, $values)) { + $url_redirect = array('c' => 'subscription', 'params' => array('id' => $id)); + if ($feedDAO->updateFeed($id, $values) !== false) { $this->view->feed->_category($cat); $this->view->feed->faviconPrepare(); - Minz_Request::good(_t('feed_updated'), array('c' => 'subscription', 'params' => array('id' => $id))); + Minz_Request::good(_t('feedback.sub.feed.updated'), $url_redirect); } else { - Minz_Request::bad(_t('error_occurred_update'), array('c' => 'subscription')); + Minz_Request::bad(_t('feedback.sub.feed.error'), $url_redirect); } } } diff --git a/app/Controllers/updateController.php b/app/Controllers/updateController.php index 4ebb11f51..8f939dbdb 100644 --- a/app/Controllers/updateController.php +++ b/app/Controllers/updateController.php @@ -1,43 +1,81 @@ <?php class FreshRSS_update_Controller extends Minz_ActionController { + + public static function isGit() { + return is_dir(FRESHRSS_PATH . '/.git/'); + } + + public static function hasGitUpdate() { + $cwd = getcwd(); + chdir(FRESHRSS_PATH); + $output = array(); + try { + exec('git fetch', $output, $return); + if ($return == 0) { + exec('git status -sb --porcelain remote', $output, $return); + } else { + $line = is_array($output) ? implode('; ', $output) : '' . $output; + Minz_Log::warning('git fetch warning:' . $line); + } + } catch (Exception $e) { + Minz_Log::warning('git fetch error:' . $e->getMessage()); + } + chdir($cwd); + $line = is_array($output) ? implode('; ', $output) : '' . $output; + return strpos($line, '[behind') !== false; + } + + public static function gitPull() { + $cwd = getcwd(); + chdir(FRESHRSS_PATH); + $output = array(); + $return = 1; + try { + exec('git pull --ff-only', $output, $return); + } catch (Exception $e) { + Minz_Log::warning('git pull error:' . $e->getMessage()); + } + chdir($cwd); + $line = is_array($output) ? implode('; ', $output) : '' . $output; + return $return == 0 ? true : 'Git error: ' . $line; + } + public function firstAction() { - $current_user = Minz_Session::param('currentUser', ''); if (!FreshRSS_Auth::hasAccess('admin')) { - Minz_Error::error( - 403, - array('error' => array(_t('access_denied'))) - ); + Minz_Error::error(403); } invalidateHttpCache(); $this->view->update_to_apply = false; $this->view->last_update_time = 'unknown'; - $this->view->check_last_hour = false; - $timestamp = (int)@file_get_contents(DATA_PATH . '/last_update.txt'); - if (is_numeric($timestamp) && $timestamp > 0) { + $timestamp = @filemtime(join_path(DATA_PATH, 'last_update.txt')); + if ($timestamp !== false) { $this->view->last_update_time = timestamptodate($timestamp); - $this->view->check_last_hour = (time() - 3600) <= $timestamp; } } public function indexAction() { - Minz_View::prependTitle(_t('update_system') . ' · '); + Minz_View::prependTitle(_t('admin.update.title') . ' · '); - if (file_exists(UPDATE_FILENAME) && !is_writable(FRESHRSS_PATH)) { + if (!is_writable(FRESHRSS_PATH)) { $this->view->message = array( 'status' => 'bad', - 'title' => _t('damn'), - 'body' => _t('file_is_nok', FRESHRSS_PATH) + 'title' => _t('gen.short.damn'), + 'body' => _t('feedback.update.file_is_nok', FRESHRSS_PATH) ); } elseif (file_exists(UPDATE_FILENAME)) { // There is an update file to apply! + $version = @file_get_contents(join_path(DATA_PATH, 'last_update.txt')); + if (empty($version)) { + $version = 'unknown'; + } $this->view->update_to_apply = true; $this->view->message = array( 'status' => 'good', - 'title' => _t('ok'), - 'body' => _t('update_can_apply') + 'title' => _t('gen.short.ok'), + 'body' => _t('feedback.update.can_apply', $version) ); } } @@ -45,59 +83,80 @@ class FreshRSS_update_Controller extends Minz_ActionController { public function checkAction() { $this->view->change_view('update', 'index'); - if (file_exists(UPDATE_FILENAME) || $this->view->check_last_hour) { + if (file_exists(UPDATE_FILENAME)) { // There is already an update file to apply: we don't need to check // the webserver! // Or if already check during the last hour, do nothing. - Minz_Request::forward(array('c' => 'update')); + Minz_Request::forward(array('c' => 'update'), true); return; } - $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 - ); + $script = ''; + $version = ''; - $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') - ); + if (self::isGit()) { + if (self::hasGitUpdate()) { + $version = 'git'; + } else { + $this->view->message = array( + 'status' => 'bad', + 'title' => _t('gen.short.damn'), + 'body' => _t('feedback.update.none') + ); + @touch(join_path(DATA_PATH, 'last_update.txt')); + return; + } + } else { + $auto_update_url = FreshRSS_Context::$system_conf->auto_update_url . '?v=' . FRESHRSS_VERSION; + Minz_Log::debug('HTTP GET ' . $auto_update_url); + $c = curl_init($auto_update_url); + curl_setopt($c, CURLOPT_RETURNTRANSFER, true); + curl_setopt($c, CURLOPT_SSL_VERIFYPEER, true); + curl_setopt($c, CURLOPT_SSL_VERIFYHOST, 2); + $result = curl_exec($c); + $c_status = curl_getinfo($c, CURLINFO_HTTP_CODE); + $c_error = curl_error($c); + curl_close($c); + + if ($c_status !== 200) { + Minz_Log::warning( + 'Error during update (HTTP code ' . $c_status . '): ' . $c_error + ); + + $this->view->message = array( + 'status' => 'bad', + 'title' => _t('gen.short.damn'), + 'body' => _t('feedback.update.server_not_found', $auto_update_url) + ); + return; + } - @file_put_contents(DATA_PATH . '/last_update.txt', time()); + $res_array = explode("\n", $result, 2); + $status = $res_array[0]; + if (strpos($status, 'UPDATE') !== 0) { + $this->view->message = array( + 'status' => 'bad', + 'title' => _t('gen.short.damn'), + 'body' => _t('feedback.update.none') + ); + @touch(join_path(DATA_PATH, 'last_update.txt')); + return; + } - return; + $script = $res_array[1]; + $version = explode(' ', $status, 2); + $version = $version[1]; } - $script = $res_array[1]; if (file_put_contents(UPDATE_FILENAME, $script) !== false) { - Minz_Request::forward(array('c' => 'update')); + @file_put_contents(join_path(DATA_PATH, 'last_update.txt'), $version); + Minz_Request::forward(array('c' => 'update'), true); } else { $this->view->message = array( 'status' => 'bad', - 'title' => _t('damn'), - 'body' => _t('update_problem', 'Cannot save the update script') + 'title' => _t('gen.short.damn'), + 'body' => _t('feedback.update.error', 'Cannot save the update script') ); } } @@ -107,22 +166,48 @@ class FreshRSS_update_Controller extends Minz_ActionController { Minz_Request::forward(array('c' => 'update'), true); } - require(UPDATE_FILENAME); - - if (Minz_Request::isPost()) { - save_info_update(); - } + if (Minz_Request::param('post_conf', false)) { + if (self::isGit()) { + $res = !self::hasGitUpdate(); + } else { + require(UPDATE_FILENAME); + $res = do_post_update(); + } - if (!need_info_update()) { - $res = apply_update(); + Minz_ExtensionManager::callHook('post_update'); if ($res === true) { @unlink(UPDATE_FILENAME); - @file_put_contents(DATA_PATH . '/last_update.txt', time()); + @file_put_contents(join_path(DATA_PATH, 'last_update.txt'), ''); + Minz_Request::good(_t('feedback.update.finished')); + } else { + Minz_Request::bad(_t('feedback.update.error', $res), + array('c' => 'update', 'a' => 'index')); + } + } else { + $res = false; - Minz_Request::good(_t('update_finished')); + if (self::isGit()) { + $res = self::gitPull(); + } else { + if (Minz_Request::isPost()) { + save_info_update(); + } + if (!need_info_update()) { + $res = apply_update(); + } else { + return; + } + } + + if ($res === true) { + Minz_Request::forward(array( + 'c' => 'update', + 'a' => 'apply', + 'params' => array('post_conf' => true) + ), true); } else { - Minz_Request::bad(_t('update_problem', $res), + Minz_Request::bad(_t('feedback.update.error', $res), array('c' => 'update', 'a' => 'index')); } } @@ -132,7 +217,7 @@ class FreshRSS_update_Controller extends Minz_ActionController { * This action displays information about installation. */ public function checkInstallAction() { - Minz_View::prependTitle(_t('gen.title.check_install') . ' · '); + Minz_View::prependTitle(_t('admin.check_install.title') . ' · '); $this->view->status_php = check_install_php(); $this->view->status_files = check_install_files(); diff --git a/app/Controllers/userController.php b/app/Controllers/userController.php index 61d33437d..9d6ae18e6 100644 --- a/app/Controllers/userController.php +++ b/app/Controllers/userController.php @@ -12,73 +12,66 @@ class FreshRSS_user_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 clean up the access condition. */ public function firstAction() { - if (!FreshRSS_Auth::hasAccess()) { - Minz_Error::error( - 403, - array('error' => array(_t('access_denied'))) - ); + if (!FreshRSS_Auth::hasAccess() && !( + Minz_Request::actionName() === 'create' && + !max_registrations_reached() + )) { + Minz_Error::error(403); + } + } + + public static function hashPassword($passwordPlain) { + if (!function_exists('password_hash')) { + include_once(LIB_PATH . '/password_compat.php'); } + $passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST)); + $passwordPlain = ''; + $passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js + return $passwordHash == '' ? '' : $passwordHash; } /** - * This action displays the user profil page. + * This action displays the user profile page. */ - public function profilAction() { - Minz_View::prependTitle(_t('users.profil') . ' · '); + public function profileAction() { + Minz_View::prependTitle(_t('conf.profile.title') . ' · '); + + Minz_View::appendScript(Minz_Url::display( + '/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js') + )); if (Minz_Request::isPost()) { $ok = true; - $passwordPlain = Minz_Request::param('passwordPlain', '', true); + $passwordPlain = Minz_Request::param('newPasswordPlain', '', 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 + Minz_Request::_param('newPasswordPlain'); //Discard plain-text password ASAP + $_POST['newPasswordPlain'] = ''; + $passwordHash = self::hashPassword($passwordPlain); $ok &= ($passwordHash != ''); - FreshRSS_Context::$conf->_passwordHash($passwordHash); + FreshRSS_Context::$user_conf->passwordHash = $passwordHash; } - Minz_Session::_param('passwordHash', FreshRSS_Context::$conf->passwordHash); + Minz_Session::_param('passwordHash', FreshRSS_Context::$user_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 + $passwordHash = self::hashPassword($passwordPlain); $ok &= ($passwordHash != ''); - FreshRSS_Context::$conf->_apiPasswordHash($passwordHash); - } - - // TODO: why do we need of hasAccess here? - if (FreshRSS_Auth::hasAccess('admin')) { - FreshRSS_Context::$conf->_mail_login(Minz_Request::param('mail_login', '', true)); + FreshRSS_Context::$user_conf->apiPasswordHash = $passwordHash; } - $email = FreshRSS_Context::$conf->mail_login; - Minz_Session::_param('mail', $email); - $ok &= FreshRSS_Context::$conf->save(); - - if ($email != '') { - $personaFile = DATA_PATH . '/persona/' . $email . '.txt'; - @unlink($personaFile); - $ok &= (file_put_contents($personaFile, Minz_Session::param('currentUser', '_')) !== false); - } + $ok &= FreshRSS_Context::$user_conf->save(); if ($ok) { - Minz_Request::good('users.profil.updated', - array('c' => 'user', 'a' => 'profil')); + Minz_Request::good(_t('feedback.profile.updated'), + array('c' => 'user', 'a' => 'profile')); } else { - Minz_Request::bad('error_occurred', - array('c' => 'user', 'a' => 'profil')); + Minz_Request::bad(_t('feedback.profile.error'), + array('c' => 'user', 'a' => 'profile')); } } } @@ -88,16 +81,14 @@ class FreshRSS_user_Controller extends Minz_ActionController { */ public function manageAction() { if (!FreshRSS_Auth::hasAccess('admin')) { - Minz_Error::error(403, - array('error' => array(_t('access_denied')))); + Minz_Error::error(403); } - Minz_View::prependTitle(_t('users.manage') . ' · '); + Minz_View::prependTitle(_t('admin.user.title') . ' · '); // Get the correct current user. - $userDAO = new FreshRSS_UserDAO(); $username = Minz_Request::param('u', Minz_Session::param('currentUser')); - if (!$userDAO->exist($username)) { + if (!FreshRSS_UserDAO::exist($username)) { $username = Minz_Session::param('currentUser'); } $this->view->current_user = $username; @@ -108,109 +99,159 @@ class FreshRSS_user_Controller extends Minz_ActionController { $this->view->size_user = $entryDAO->size(); } - public function createAction() { - if (Minz_Request::isPost() && FreshRSS_Auth::hasAccess('admin')) { - $db = Minz_Configuration::dataBase(); - require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php'); - - $new_user_language = Minz_Request::param('new_user_language', FreshRSS_Context::$conf->language); - if (!in_array($new_user_language, FreshRSS_Context::$conf->availableLanguages())) { - $new_user_language = FreshRSS_Context::$conf->language; - } + public static function createUser($new_user_name, $passwordPlain, $apiPasswordPlain, $userConfig = array(), $insertDefaultFeeds = true) { + if (!is_array($userConfig)) { + $userConfig = array(); + } - $new_user_name = Minz_Request::param('new_user_name'); - $ok = ($new_user_name != '') && ctype_alnum($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 + if ($ok) { + $languages = Minz_Translate::availableLanguages(); + if (empty($userConfig['language']) || !in_array($userConfig['language'], $languages)) { + $userConfig['language'] = 'en'; + } - $ok &= !in_array(strtoupper($new_user_name), array_map('strtoupper', listUsers())); //Not an existing user, case-insensitive + $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); + $configPath = join_path(DATA_PATH, 'users', $new_user_name, 'config.php'); + $ok &= !file_exists($configPath); + } + if ($ok) { + $passwordHash = ''; + if ($passwordPlain != '') { + $passwordHash = self::hashPassword($passwordPlain); + $ok &= ($passwordHash != ''); } - if ($ok) { - $userDAO = new FreshRSS_UserDAO(); - $ok &= $userDAO->createUser($new_user_name); + + $apiPasswordHash = ''; + if ($apiPasswordPlain != '') { + $apiPasswordHash = self::hashPassword($apiPasswordPlain); + $ok &= ($apiPasswordHash != ''); } + } + if ($ok) { + mkdir(join_path(DATA_PATH, 'users', $new_user_name)); + $userConfig['passwordHash'] = $passwordHash; + $userConfig['apiPasswordHash'] = $apiPasswordHash; + $ok &= (file_put_contents($configPath, "<?php\n return " . var_export($userConfig, true) . ';') !== false); + } + if ($ok) { + $userDAO = new FreshRSS_UserDAO(); + $ok &= $userDAO->createUser($new_user_name, $userConfig['language'], $insertDefaultFeeds); + } + return $ok; + } + + /** + * This action creates a new user. + * + * Request parameters are: + * - new_user_language + * - new_user_name + * - new_user_passwordPlain + * - r (i.e. a redirection url, optional) + * + * @todo clean up this method. Idea: write a method to init a user with basic information. + * @todo handle r redirection in Minz_Request::forward directly? + */ + public function createAction() { + if (Minz_Request::isPost() && ( + FreshRSS_Auth::hasAccess('admin') || + !max_registrations_reached() + )) { + $new_user_name = Minz_Request::param('new_user_name'); + $passwordPlain = Minz_Request::param('new_user_passwordPlain', '', true); + $new_user_language = Minz_Request::param('new_user_language', FreshRSS_Context::$user_conf->language); + + $ok = self::createUser($new_user_name, $passwordPlain, '', array('language' => $new_user_language)); + Minz_Request::_param('new_user_passwordPlain'); //Discard plain-text password ASAP + $_POST['new_user_passwordPlain'] = ''; invalidateHttpCache(); $notif = array( 'type' => $ok ? 'good' : 'bad', - 'content' => _t($ok ? 'user_created' : 'error_occurred', $new_user_name) + 'content' => _t('feedback.user.created' . (!$ok ? '.error' : ''), $new_user_name) ); Minz_Session::_param('notification', $notif); } - Minz_Request::forward(array('c' => 'user', 'a' => 'manage'), true); + $redirect_url = urldecode(Minz_Request::param('r', false, true)); + if (!$redirect_url) { + $redirect_url = array('c' => 'user', 'a' => 'manage'); + } + Minz_Request::forward($redirect_url, true); } + public static function deleteUser($username) { + $db = FreshRSS_Context::$system_conf->db; + require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php'); + + $ok = ctype_alnum($username); + if ($ok) { + $default_user = FreshRSS_Context::$system_conf->default_user; + $ok &= (strcasecmp($username, $default_user) !== 0); //It is forbidden to delete the default user + } + $user_data = join_path(DATA_PATH, 'users', $username); + if ($ok) { + $ok &= is_dir($user_data); + } + if ($ok) { + $userDAO = new FreshRSS_UserDAO(); + $ok &= $userDAO->deleteUser($username); + $ok &= recursive_unlink($user_data); + } + return $ok; + } + + /** + * This action delete an existing user. + * + * Request parameter is: + * - username + * + * @todo clean up this method. Idea: create a User->clean() method. + */ public function deleteAction() { - if (Minz_Request::isPost() && FreshRSS_Auth::hasAccess('admin')) { - $db = Minz_Configuration::dataBase(); - require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php'); + $username = Minz_Request::param('username'); + $redirect_url = urldecode(Minz_Request::param('r', false, true)); + if (!$redirect_url) { + $redirect_url = array('c' => 'user', 'a' => 'manage'); + } - $username = Minz_Request::param('username'); - $ok = ctype_alnum($username); + $self_deletion = Minz_Session::param('currentUser', '_') === $username; - if ($ok) { - $ok &= (strcasecmp($username, Minz_Configuration::defaultUser()) !== 0); //It is forbidden to delete the default user + if (Minz_Request::isPost() && ( + FreshRSS_Auth::hasAccess('admin') || + $self_deletion + )) { + $ok = true; + if ($ok && $self_deletion) { + // We check the password if it's a self-destruction + $nonce = Minz_Session::param('nonce'); + $challenge = Minz_Request::param('challenge', ''); + + $ok &= FreshRSS_FormAuth::checkCredentials( + $username, FreshRSS_Context::$user_conf->passwordHash, + $nonce, $challenge + ); } if ($ok) { - $configPath = DATA_PATH . '/' . $username . '_user.php'; - $ok &= file_exists($configPath); + $ok &= self::deleteUser($username); } - if ($ok) { - $userDAO = new FreshRSS_UserDAO(); - $ok &= $userDAO->deleteUser($username); - $ok &= unlink($configPath); - //TODO: delete Persona file + if ($ok && $self_deletion) { + FreshRSS_Auth::removeAccess(); + $redirect_url = array('c' => 'index', 'a' => 'index'); } invalidateHttpCache(); $notif = array( 'type' => $ok ? 'good' : 'bad', - 'content' => _t($ok ? 'user_deleted' : 'error_occurred', $username) + 'content' => _t('feedback.user.deleted' . (!$ok ? '.error' : ''), $username) ); Minz_Session::_param('notification', $notif); } - Minz_Request::forward(array('c' => 'user', 'a' => 'manage'), true); + Minz_Request::forward($redirect_url, true); } } |
