diff options
Diffstat (limited to 'app/Controllers/userController.php')
| -rw-r--r-- | app/Controllers/userController.php | 276 |
1 files changed, 230 insertions, 46 deletions
diff --git a/app/Controllers/userController.php b/app/Controllers/userController.php index 6d0fced5b..6afc91b4e 100644 --- a/app/Controllers/userController.php +++ b/app/Controllers/userController.php @@ -8,26 +8,7 @@ class FreshRSS_user_Controller extends Minz_ActionController { // so do not use a too high cost const BCRYPT_COST = 9; - /** - * This action is called before every other action in that class. It is - * the common boiler plate for every action. It is triggered by the - * underlying framework. - * - * @todo clean up the access condition. - */ - public function firstAction() { - if (!FreshRSS_Auth::hasAccess() && !( - Minz_Request::actionName() === 'create' && - !max_registrations_reached() - )) { - Minz_Error::error(403); - } - } - public static function hashPassword($passwordPlain) { - if (!function_exists('password_hash')) { - include_once(LIB_PATH . '/password_compat.php'); - } $passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => self::BCRYPT_COST)); $passwordPlain = ''; $passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js @@ -52,12 +33,23 @@ class FreshRSS_user_Controller extends Minz_ActionController { return false; } - public static function updateUser($user, $passwordPlain, $apiPasswordPlain, $userConfigUpdated = array()) { + public static function updateUser($user, $email, $passwordPlain, $apiPasswordPlain, $userConfigUpdated = array()) { $userConfig = get_user_configuration($user); if ($userConfig === null) { return false; } + if ($email !== null && $userConfig->mail_login !== $email) { + $userConfig->mail_login = $email; + + if (FreshRSS_Context::$system_conf->force_email_validation) { + $salt = FreshRSS_Context::$system_conf->salt; + $userConfig->email_validation_token = sha1($salt . uniqid(mt_rand(), true)); + $mailer = new FreshRSS_User_Mailer(); + $mailer->send_email_need_validation($user, $userConfig); + } + } + if ($passwordPlain != '') { $passwordHash = self::hashPassword($passwordPlain); $userConfig->passwordHash = $passwordHash; @@ -103,7 +95,7 @@ class FreshRSS_user_Controller extends Minz_ActionController { $apiPasswordPlain = Minz_Request::param('apiPasswordPlain', '', true); $username = Minz_Request::param('username'); - $ok = self::updateUser($username, $passwordPlain, $apiPasswordPlain, array( + $ok = self::updateUser($username, null, $passwordPlain, $apiPasswordPlain, array( 'token' => Minz_Request::param('token', null), )); @@ -126,25 +118,63 @@ class FreshRSS_user_Controller extends Minz_ActionController { * This action displays the user profile page. */ public function profileAction() { + if (!FreshRSS_Auth::hasAccess()) { + Minz_Error::error(403); + } + + $email_not_verified = FreshRSS_Context::$user_conf->email_validation_token !== ''; + $this->view->disable_aside = false; + if ($email_not_verified) { + $this->view->_layout('simple'); + $this->view->disable_aside = true; + } + Minz_View::prependTitle(_t('conf.profile.title') . ' · '); Minz_View::appendScript(Minz_Url::display('/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js'))); if (Minz_Request::isPost()) { + $system_conf = FreshRSS_Context::$system_conf; + $user_config = FreshRSS_Context::$user_conf; + $old_email = $user_config->mail_login; + + $email = trim(Minz_Request::param('email', '')); $passwordPlain = Minz_Request::param('newPasswordPlain', '', true); Minz_Request::_param('newPasswordPlain'); //Discard plain-text password ASAP $_POST['newPasswordPlain'] = ''; $apiPasswordPlain = Minz_Request::param('apiPasswordPlain', '', true); - $ok = self::updateUser(Minz_Session::param('currentUser'), $passwordPlain, $apiPasswordPlain, array( + if ($system_conf->force_email_validation && empty($email)) { + Minz_Request::bad( + _t('user.email.feedback.required'), + array('c' => 'user', 'a' => 'profile') + ); + } + + if (!empty($email) && !validateEmailAddress($email)) { + Minz_Request::bad( + _t('user.email.feedback.invalid'), + array('c' => 'user', 'a' => 'profile') + ); + } + + $ok = self::updateUser( + Minz_Session::param('currentUser'), + $email, + $passwordPlain, + $apiPasswordPlain, + array( 'token' => Minz_Request::param('token', null), - )); + ) + ); Minz_Session::_param('passwordHash', FreshRSS_Context::$user_conf->passwordHash); if ($ok) { - if ($passwordPlain == '') { + if ($system_conf->force_email_validation && $email !== $old_email) { + Minz_Request::good(_t('feedback.profile.updated'), array('c' => 'user', 'a' => 'validateEmail')); + } elseif ($passwordPlain == '') { Minz_Request::good(_t('feedback.profile.updated'), array('c' => 'user', 'a' => 'profile')); } else { Minz_Request::good(_t('feedback.profile.updated'), array('c' => 'index', 'a' => 'index')); @@ -166,6 +196,7 @@ class FreshRSS_user_Controller extends Minz_ActionController { Minz_View::prependTitle(_t('admin.user.title') . ' · '); + $this->view->show_email_field = FreshRSS_Context::$system_conf->force_email_validation; $this->view->current_user = Minz_Request::param('u'); $this->view->nb_articles = 0; @@ -180,9 +211,19 @@ class FreshRSS_user_Controller extends Minz_ActionController { } } - public static function createUser($new_user_name, $passwordPlain, $apiPasswordPlain, $userConfig = array(), $insertDefaultFeeds = true) { - if (!is_array($userConfig)) { - $userConfig = array(); + public static function createUser($new_user_name, $email, $passwordPlain, $apiPasswordPlain = '', $userConfigOverride = [], $insertDefaultFeeds = true) { + $userConfig = []; + + $customUserConfigPath = join_path(DATA_PATH, 'config-user.custom.php'); + if (file_exists($customUserConfigPath)) { + $customUserConfig = include($customUserConfigPath); + if (is_array($customUserConfig)) { + $userConfig = $customUserConfig; + } + } + + if (is_array($userConfigOverride)) { + $userConfig = array_merge($userConfig, $userConfigOverride); } $ok = self::checkUsername($new_user_name); @@ -206,9 +247,9 @@ class FreshRSS_user_Controller extends Minz_ActionController { $ok &= (file_put_contents($configPath, "<?php\n return " . var_export($userConfig, true) . ';') !== false); } if ($ok) { - $userDAO = new FreshRSS_UserDAO(); - $ok &= $userDAO->createUser($new_user_name, $userConfig['language'], $insertDefaultFeeds); - $ok &= self::updateUser($new_user_name, $passwordPlain, $apiPasswordPlain); + $newUserDAO = FreshRSS_Factory::createUserDao($new_user_name); + $ok &= $newUserDAO->createUser($insertDefaultFeeds); + $ok &= self::updateUser($new_user_name, $email, $passwordPlain, $apiPasswordPlain); } return $ok; } @@ -219,6 +260,7 @@ class FreshRSS_user_Controller extends Minz_ActionController { * Request parameters are: * - new_user_language * - new_user_name + * - new_user_email * - new_user_passwordPlain * - r (i.e. a redirection url, optional) * @@ -226,15 +268,43 @@ class FreshRSS_user_Controller extends Minz_ActionController { * @todo handle r redirection in Minz_Request::forward directly? */ public function createAction() { - if (Minz_Request::isPost() && ( - FreshRSS_Auth::hasAccess('admin') || - !max_registrations_reached() - )) { + if (!FreshRSS_Auth::hasAccess('admin') && max_registrations_reached()) { + Minz_Error::error(403); + } + + if (Minz_Request::isPost()) { + $system_conf = FreshRSS_Context::$system_conf; + $new_user_name = Minz_Request::param('new_user_name'); + $email = Minz_Request::param('new_user_email', ''); $passwordPlain = Minz_Request::param('new_user_passwordPlain', '', true); $new_user_language = Minz_Request::param('new_user_language', FreshRSS_Context::$user_conf->language); - $ok = self::createUser($new_user_name, $passwordPlain, '', array('language' => $new_user_language)); + $tos_enabled = file_exists(join_path(DATA_PATH, 'tos.html')); + $accept_tos = Minz_Request::param('accept_tos', false); + + if ($system_conf->force_email_validation && empty($email)) { + Minz_Request::bad( + _t('user.email.feedback.required'), + array('c' => 'auth', 'a' => 'register') + ); + } + + if (!empty($email) && !validateEmailAddress($email)) { + Minz_Request::bad( + _t('user.email.feedback.invalid'), + array('c' => 'auth', 'a' => 'register') + ); + } + + if ($tos_enabled && !$accept_tos) { + Minz_Request::bad( + _t('user.tos.feedback.invalid'), + array('c' => 'auth', 'a' => 'register') + ); + } + + $ok = self::createUser($new_user_name, $email, $passwordPlain, '', array('language' => $new_user_language)); Minz_Request::_param('new_user_passwordPlain'); //Discard plain-text password ASAP $_POST['new_user_passwordPlain'] = ''; invalidateHttpCache(); @@ -266,9 +336,6 @@ class FreshRSS_user_Controller extends Minz_ActionController { } public static function deleteUser($username) { - $db = FreshRSS_Context::$system_conf->db; - require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php'); - $ok = self::checkUsername($username); if ($ok) { $default_user = FreshRSS_Context::$system_conf->default_user; @@ -278,8 +345,8 @@ class FreshRSS_user_Controller extends Minz_ActionController { $ok &= is_dir($user_data); if ($ok) { self::deleteFeverKey($username); - $userDAO = new FreshRSS_UserDAO(); - $ok &= $userDAO->deleteUser($username); + $oldUserDAO = FreshRSS_Factory::createUserDao($username); + $ok &= $oldUserDAO->deleteUser(); $ok &= recursive_unlink($user_data); array_map('unlink', glob(PSHB_PATH . '/feeds/*/' . $username . '.txt')); } @@ -287,6 +354,122 @@ class FreshRSS_user_Controller extends Minz_ActionController { } /** + * This action validates an email address, based on the token sent by email. + * It also serves the main page when user is blocked. + * + * Request parameters are: + * - username + * - token + * + * This route works with GET requests since the URL is provided by email. + * The security risks (e.g. forged URL by an attacker) are not very high so + * it's ok. + * + * It returns 404 error if `force_email_validation` is disabled or if the + * user doesn't exist. + * + * It returns 403 if user isn't logged in and `username` param isn't passed. + */ + public function validateEmailAction() { + if (!FreshRSS_Context::$system_conf->force_email_validation) { + Minz_Error::error(404); + } + + Minz_View::prependTitle(_t('user.email.validation.title') . ' · '); + $this->view->_layout('simple'); + + $username = Minz_Request::param('username'); + $token = Minz_Request::param('token'); + + if ($username) { + $user_config = get_user_configuration($username); + } elseif (FreshRSS_Auth::hasAccess()) { + $user_config = FreshRSS_Context::$user_conf; + } else { + Minz_Error::error(403); + } + + if (!FreshRSS_UserDAO::exists($username) || $user_config === null) { + Minz_Error::error(404); + } + + if ($user_config->email_validation_token === '') { + Minz_Request::good( + _t('user.email.validation.feedback.unnecessary'), + array('c' => 'index', 'a' => 'index') + ); + } + + if ($token) { + if ($user_config->email_validation_token !== $token) { + Minz_Request::bad( + _t('user.email.validation.feedback.wrong_token'), + array('c' => 'user', 'a' => 'validateEmail') + ); + } + + $user_config->email_validation_token = ''; + if ($user_config->save()) { + Minz_Request::good( + _t('user.email.validation.feedback.ok'), + array('c' => 'index', 'a' => 'index') + ); + } else { + Minz_Request::bad( + _t('user.email.validation.feedback.error'), + array('c' => 'user', 'a' => 'validateEmail') + ); + } + } + } + + /** + * This action resends a validation email to the current user. + * + * It only acts on POST requests but doesn't require any param (except the + * CSRF token). + * + * It returns 403 error if the user is not logged in or 404 if request is + * not POST. Else it redirects silently to the index if user has already + * validated its email, or to the user#validateEmail route. + */ + public function sendValidationEmailAction() { + if (!FreshRSS_Auth::hasAccess()) { + Minz_Error::error(403); + } + + if (!Minz_Request::isPost()) { + Minz_Error::error(404); + } + + $username = Minz_Session::param('currentUser', '_'); + $user_config = FreshRSS_Context::$user_conf; + + if ($user_config->email_validation_token === '') { + Minz_Request::forward(array( + 'c' => 'index', + 'a' => 'index', + ), true); + } + + $mailer = new FreshRSS_User_Mailer(); + $ok = $mailer->send_email_need_validation($username, $user_config); + + $redirect_url = array('c' => 'user', 'a' => 'validateEmail'); + if ($ok) { + Minz_Request::good( + _t('user.email.validation.feedback.email_sent'), + $redirect_url + ); + } else { + Minz_Request::bad( + _t('user.email.validation.feedback.email_failed'), + $redirect_url + ); + } + } + + /** * This action delete an existing user. * * Request parameter is: @@ -296,17 +479,18 @@ class FreshRSS_user_Controller extends Minz_ActionController { */ public function deleteAction() { $username = Minz_Request::param('username'); + $self_deletion = Minz_Session::param('currentUser', '_') === $username; + + if (!FreshRSS_Auth::hasAccess('admin') && !$self_deletion) { + Minz_Error::error(403); + } + $redirect_url = urldecode(Minz_Request::param('r', false, true)); if (!$redirect_url) { $redirect_url = array('c' => 'user', 'a' => 'manage'); } - $self_deletion = Minz_Session::param('currentUser', '_') === $username; - - if (Minz_Request::isPost() && ( - FreshRSS_Auth::hasAccess('admin') || - $self_deletion - )) { + if (Minz_Request::isPost()) { $ok = true; if ($ok && $self_deletion) { // We check the password if it's a self-destruction |
