aboutsummaryrefslogtreecommitdiff
path: root/app/Controllers/userController.php
diff options
context:
space:
mode:
authorGravatar Alexandre Alapetite <alexandre@alapetite.fr> 2019-10-31 18:15:47 +0100
committerGravatar GitHub <noreply@github.com> 2019-10-31 18:15:47 +0100
commit3aa66f317b496ccd9a2df914bbc747c52081a7ad (patch)
tree6a3f3f74899801abdca00546e213dfdc141c53cf /app/Controllers/userController.php
parent82611c9622ed23b0e9fcf5f9f651ddffa1fd7706 (diff)
parentfcae48f313d399050cb15f37a8a73ae52fc67796 (diff)
Merge pull request #2599 from FreshRSS/dev1.15.0
FreshRSS 1.15
Diffstat (limited to 'app/Controllers/userController.php')
-rw-r--r--app/Controllers/userController.php276
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