summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Alexandre Alapetite <alexandre@alapetite.fr> 2014-01-12 03:10:31 +0100
committerGravatar Alexandre Alapetite <alexandre@alapetite.fr> 2014-01-12 03:10:31 +0100
commitd58886a937cbe425163526fc2ba3d2a118602035 (patch)
tree4769024f513d927c45fe3a6475e8dcdf92f01d0f
parent43f1b227b459f8edade9d551164c18f56cfa1925 (diff)
Implémentation de l'indentification par mot de passe
Implémentation de https://github.com/marienfressinaud/FreshRSS/issues/104
-rw-r--r--CHANGELOG3
-rwxr-xr-xapp/Controllers/indexController.php52
-rwxr-xr-xapp/Controllers/javascriptController.php12
-rw-r--r--app/Controllers/usersController.php26
-rw-r--r--app/FreshRSS.php27
-rw-r--r--app/i18n/en.php7
-rw-r--r--app/i18n/fr.php7
-rw-r--r--app/layout/header.phtml51
-rw-r--r--app/views/configure/users.phtml16
-rw-r--r--app/views/helpers/javascript_vars.phtml4
-rw-r--r--app/views/helpers/view/login.phtml43
-rw-r--r--app/views/index/index.phtml14
-rw-r--r--lib/Minz/Configuration.php3
-rw-r--r--lib/Minz/FrontController.php2
-rw-r--r--p/scripts/main.js57
15 files changed, 265 insertions, 59 deletions
diff --git a/CHANGELOG b/CHANGELOG
index fe856fe4a..5c9b56465 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -5,6 +5,8 @@
* Nouveau mode multi-utilisateur
* L’utilisateur par défaut (administrateur) peut créer et supprimer d’autres utilisateurs
* Nécessite un contrôle d’accès, soit :
+ * par le nouveau mode de connexion par formulaire (nom d’utilisateur + mot de passe)
+ * relativement sûr même sans HTTPS (le mot de passe n’est pas transmis en clair)
* par HTTP (par exemple sous Apache en créant un fichier ./p/i/.htaccess et .htpasswd)
* le nom d’utilisateur HTTP doit correspondre au nom d’utilisateur FreshRSS
* par Mozilla Persona, en renseignant l’adresse courriel des utilisateurs
@@ -68,6 +70,7 @@
* Réorganisation des fichiers et répertoires, en particulier :
* Tous les fichiers utilisateur sont dans “./data/” (y compris “cache”, “favicons”, et “log”)
* Déplacement de “./app/configuration/application.ini” vers “./data/config.php”
+ * Meilleure sécurité et compatibilité
* Déplacement de “./public/data/Configuration.array.php” vers “./data/*_user.php”
* Déplacement de “./public/” vers “./p/”
* Déplacement de “./public/index.php” vers “./p/i/index.php” (voir cookie ci-dessous)
diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php
index 81dfefabb..772a08f30 100755
--- a/app/Controllers/indexController.php
+++ b/app/Controllers/indexController.php
@@ -47,8 +47,6 @@ class FreshRSS_index_Controller extends Minz_ActionController {
$this->view->_useLayout (false);
header('Content-Type: application/rss+xml; charset=utf-8');
} else {
- Minz_View::appendScript (Minz_Url::display ('/scripts/shortcut.js?' . @filemtime(PUBLIC_PATH . '/scripts/shortcut.js')));
-
if ($output === 'global') {
Minz_View::appendScript (Minz_Url::display ('/scripts/global_view.js?' . @filemtime(PUBLIC_PATH . '/scripts/global_view.js')));
}
@@ -290,8 +288,56 @@ class FreshRSS_index_Controller extends Minz_ActionController {
}
public function logoutAction () {
+ $this->view->_useLayout(false);
+ invalidateHttpCache();
+ Minz_Session::_param('currentUser');
+ Minz_Session::_param('mail');
+ Minz_Session::_param('passwordHash');
+ }
+
+ public function formLoginAction () {
$this->view->_useLayout (false);
- Minz_Session::_param ('mail');
+ if (Minz_Request::isPost()) {
+ $ok = false;
+ $nonce = Minz_Session::param('nonce');
+ $username = Minz_Request::param('username', '');
+ $c = Minz_Request::param('challenge', '');
+ if (ctype_alnum($username) && ctype_graph($c) && ctype_alnum($nonce)) {
+ if (!function_exists('password_verify')) {
+ include_once(LIB_PATH . '/password_compat.php');
+ }
+ try {
+ $conf = new FreshRSS_Configuration($username);
+ $s = $conf->passwordHash;
+ $ok = password_verify($nonce . $s, $c);
+ if ($ok) {
+ Minz_Session::_param('currentUser', $username);
+ Minz_Session::_param('passwordHash', $s);
+ } else {
+ Minz_Log::record('Password mismatch for user ' . $username . ', nonce=' . $nonce . ', c=' . $c, Minz_Log::WARNING);
+ }
+ } catch (Minz_Exception $me) {
+ Minz_Log::record('Login failure: ' . $me->getMessage(), Minz_Log::WARNING);
+ }
+ }
+ if (!$ok) {
+ $notif = array(
+ 'type' => 'bad',
+ 'content' => Minz_Translate::t('invalid_login')
+ );
+ Minz_Session::_param('notification', $notif);
+ }
+ }
+ invalidateHttpCache();
+ Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
+ }
+
+ public function formLogoutAction () {
+ $this->view->_useLayout(false);
invalidateHttpCache();
+ Minz_Session::_param('currentUser');
+ Minz_Session::_param('mail');
+ Minz_Session::_param('passwordHash');
+ Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
}
}
diff --git a/app/Controllers/javascriptController.php b/app/Controllers/javascriptController.php
index e29f439d8..02e424437 100755
--- a/app/Controllers/javascriptController.php
+++ b/app/Controllers/javascriptController.php
@@ -17,7 +17,7 @@ class FreshRSS_javascript_Controller extends Minz_ActionController {
$this->view->categories = $catDAO->listCategories(true, false);
}
- // For Web-form login
+ //For Web-form login
public function nonceAction() {
header('Content-Type: application/json; charset=UTF-8');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s \G\M\T'));
@@ -29,15 +29,15 @@ class FreshRSS_javascript_Controller extends Minz_ActionController {
if (ctype_alnum($user)) {
try {
$conf = new FreshRSS_Configuration($user);
- $hash = $conf->passwordHash; //CRYPT_BLOWFISH - Blowfish hashing with a salt as follows: "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters from the alphabet "./0-9A-Za-z".
- if (strlen($hash) >= 60) {
- $this->view->salt1 = substr($hash, 0, 29);
+ $s = $conf->passwordHash;
+ if (strlen($s) >= 60) {
+ $this->view->salt1 = substr($s, 0, 29); //CRYPT_BLOWFISH Salt: "$2a$", a two digit cost parameter, "$", and 22 characters from the alphabet "./0-9A-Za-z".
$this->view->nonce = sha1(Minz_Configuration::salt() . uniqid(mt_rand(), true));
- Minz_Session::_param ('nonce', $this->view->nonce);
+ Minz_Session::_param('nonce', $this->view->nonce);
return; //Success
}
} catch (Minz_Exception $me) {
- Minz_Log::record ('Login failure: ' . $me->getMessage(), Minz_Log::WARNING);
+ Minz_Log::record('Login failure: ' . $me->getMessage(), Minz_Log::WARNING);
}
}
$this->view->nonce = ''; //Failure
diff --git a/app/Controllers/usersController.php b/app/Controllers/usersController.php
index 8954c845d..cb5ebd209 100644
--- a/app/Controllers/usersController.php
+++ b/app/Controllers/usersController.php
@@ -21,18 +21,20 @@ class FreshRSS_users_Controller extends Minz_ActionController {
if (!function_exists('password_hash')) {
include_once(LIB_PATH . '/password_compat.php');
}
- $passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT); //A bit expensive, on purpose
+ $passwordHash = password_hash($passwordPlain, PASSWORD_BCRYPT, array('cost' => 8)); //This will also have to be computed client side on mobile devices, so do not use a too high cost
$passwordPlain = '';
+ $passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash); //Compatibility with bcrypt.js
$this->view->conf->_passwordHash($passwordHash);
}
- $mail = Minz_Request::param('mail_login', false);
- $this->view->conf->_mail_login($mail);
+ $email = Minz_Request::param('mail_login', false);
+ $this->view->conf->_mail_login($email);
$ok &= $this->view->conf->save();
$email = $this->view->conf->mail_login;
Minz_Session::_param('mail', $email);
+ Minz_Session::_param('passwordHash', $this->view->conf->passwordHash);
if ($email != '') {
$personaFile = DATA_PATH . '/persona/' . $email . '.txt';
@@ -89,10 +91,25 @@ class FreshRSS_users_Controller extends Minz_ActionController {
$ok &= !file_exists($configPath);
}
if ($ok) {
+
+ $passwordPlain = Minz_Request::param('new_user_passwordPlain', false);
+ $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' => 8));
+ $passwordPlain = '';
+ }
+ if (empty($passwordHash)) {
+ $passwordHash = '';
+ }
+
$new_user_email = filter_var($_POST['new_user_email'], FILTER_VALIDATE_EMAIL);
if (empty($new_user_email)) {
$new_user_email = '';
- $ok &= Minz_Configuration::authType() !== 'persona';
} else {
$personaFile = DATA_PATH . '/persona/' . $new_user_email . '.txt';
@unlink($personaFile);
@@ -102,6 +119,7 @@ class FreshRSS_users_Controller extends Minz_ActionController {
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);
diff --git a/app/FreshRSS.php b/app/FreshRSS.php
index f9857a4cb..4c462c835 100644
--- a/app/FreshRSS.php
+++ b/app/FreshRSS.php
@@ -4,15 +4,20 @@ class FreshRSS extends Minz_FrontController {
if (!isset($_SESSION)) {
Minz_Session::init('FreshRSS');
}
- $this->accessControl(Minz_Session::param('currentUser', ''));
+ $loginOk = $this->accessControl(Minz_Session::param('currentUser', ''));
$this->loadParamsView();
- $this->loadStylesAndScripts(); //TODO: Do not load that when not needed, e.g. some Ajax requests
+ $this->loadStylesAndScripts($loginOk); //TODO: Do not load that when not needed, e.g. some Ajax requests
$this->loadNotifications();
}
private function accessControl($currentUser) {
if ($currentUser == '') {
switch (Minz_Configuration::authType()) {
+ case 'form':
+ $currentUser = Minz_Configuration::defaultUser();
+ Minz_Session::_param('passwordHash');
+ $loginOk = false;
+ break;
case 'http_auth':
$currentUser = httpAuthUser();
$loginOk = $currentUser != '';
@@ -73,6 +78,9 @@ class FreshRSS extends Minz_FrontController {
if ($loginOk) {
switch (Minz_Configuration::authType()) {
+ case 'form':
+ $loginOk = Minz_Session::param('passwordHash') === $this->conf->passwordHash;
+ break;
case 'http_auth':
$loginOk = strcasecmp($currentUser, httpAuthUser()) === 0;
break;
@@ -92,6 +100,7 @@ class FreshRSS extends Minz_FrontController {
}
}
Minz_View::_param ('loginOk', $loginOk);
+ return $loginOk;
}
private function loadParamsView () {
@@ -104,7 +113,7 @@ class FreshRSS extends Minz_FrontController {
}
}
- private function loadStylesAndScripts () {
+ private function loadStylesAndScripts ($loginOk) {
$theme = FreshRSS_Themes::get_infos($this->conf->theme);
if ($theme) {
foreach($theme['files'] as $file) {
@@ -112,14 +121,22 @@ class FreshRSS extends Minz_FrontController {
}
}
- if (Minz_Configuration::authType() === 'persona') {
- Minz_View::appendScript ('https://login.persona.org/include.js');
+ switch (Minz_Configuration::authType()) {
+ case 'form':
+ if (!$loginOk) {
+ Minz_View::appendScript(Minz_Url::display ('/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js')));
+ }
+ break;
+ case 'persona':
+ Minz_View::appendScript('https://login.persona.org/include.js');
+ break;
}
$includeLazyLoad = $this->conf->lazyload && ($this->conf->display_posts || Minz_Request::param ('output') === 'reader');
Minz_View::appendScript (Minz_Url::display ('/scripts/jquery.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/jquery.min.js')), false, !$includeLazyLoad, !$includeLazyLoad);
if ($includeLazyLoad) {
Minz_View::appendScript (Minz_Url::display ('/scripts/jquery.lazyload.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/jquery.lazyload.min.js')));
}
+ Minz_View::appendScript (Minz_Url::display ('/scripts/shortcut.js?' . @filemtime(PUBLIC_PATH . '/scripts/shortcut.js')));
Minz_View::appendScript (Minz_Url::display ('/scripts/main.js?' . @filemtime(PUBLIC_PATH . '/scripts/main.js')));
}
diff --git a/app/i18n/en.php b/app/i18n/en.php
index 3b9936e8e..71ca9538f 100644
--- a/app/i18n/en.php
+++ b/app/i18n/en.php
@@ -170,6 +170,9 @@ return array (
'is_admin' => 'is administrator',
'auth_type' => 'Authentication method',
'auth_none' => 'None (dangerous)',
+ 'auth_form' => 'Web form (traditional, requires JavaScript)',
+ 'http_auth' => 'HTTP (for advanced users with HTTPS)',
+ 'auth_persona' => 'Mozilla Persona (modern, requires JavaScript)',
'users_list' => 'List of users',
'create_user' => 'Create new user',
'username' => 'Username',
@@ -258,8 +261,8 @@ return array (
'logs_empty' => 'Log file is empty',
'clear_logs' => 'Clear the logs',
- 'forbidden_access' => 'Forbidden access',
- 'forbidden_access_description' => 'Access is password protected, please <a class="signin" href="#">sign in</a> to read your feeds.',
+ 'forbidden_access' => 'Access forbidden! (%s)',
+ 'login_required' => 'Login required:',
'confirm_action' => 'Are you sure you want to perform this action? It cannot be cancelled!',
diff --git a/app/i18n/fr.php b/app/i18n/fr.php
index 7e71cbb6d..8ffc5ec88 100644
--- a/app/i18n/fr.php
+++ b/app/i18n/fr.php
@@ -170,6 +170,9 @@ return array (
'is_admin' => 'est administrateur',
'auth_type' => 'Méthode d’authentification',
'auth_none' => 'Aucune (dangereux)',
+ 'auth_form' => 'Formulaire (traditionnel, requiert JavaScript)',
+ 'http_auth' => 'HTTP (pour utilisateurs avancés avec HTTPS)',
+ 'auth_persona' => 'Mozilla Persona (moderne, requiert JavaScript)',
'users_list' => 'Liste des utilisateurs',
'create_user' => 'Créer un nouvel utilisateur',
'username' => 'Nom d’utilisateur',
@@ -258,8 +261,8 @@ return array (
'logs_empty' => 'Les logs sont vides',
'clear_logs' => 'Effacer les logs',
- 'forbidden_access' => 'Accès interdit',
- 'forbidden_access_description' => 'L’accès est protégé par un mot de passe, veuillez <a class="signin" href="#">vous connecter</a> pour accéder aux flux.',
+ 'forbidden_access' => 'Accès interdit ! (%s)',
+ 'login_required' => 'Accès protégé par mot de passe :',
'confirm_action' => 'Êtes-vous sûr(e) de vouloir continuer ? Cette action ne peut être annulée !',
diff --git a/app/layout/header.phtml b/app/layout/header.phtml
index 0f2c524c4..e90da6196 100644
--- a/app/layout/header.phtml
+++ b/app/layout/header.phtml
@@ -1,12 +1,25 @@
-<?php if (Minz_Configuration::canLogIn()) { ?>
-<ul class="nav nav-head nav-login">
- <?php if ($this->loginOk) { ?>
- <li class="item"><?php echo FreshRSS_Themes::icon('logout'); ?> <a class="signout" href="#"><?php echo Minz_Translate::t ('logout'); ?></a></li>
- <?php } else { ?>
- <li class="item"><?php echo FreshRSS_Themes::icon('login'); ?> <a class="signin" href="#"><?php echo Minz_Translate::t ('login'); ?></a></li>
- <?php } ?>
-</ul>
-<?php } ?>
+<?php
+if (Minz_Configuration::canLogIn()) {
+ ?><ul class="nav nav-head nav-login"><?php
+ switch (Minz_Configuration::authType()) {
+ case 'form':
+ if ($this->loginOk) {
+ ?><li class="item"><?php echo FreshRSS_Themes::icon('logout'); ?> <a class="signout" href="<?php echo _url ('index', 'formLogout'); ?>"><?php echo Minz_Translate::t ('logout'); ?></a></li><?php
+ } else {
+ ?><li class="item"><?php echo FreshRSS_Themes::icon('login'); ?> <a class="signin" href="<?php echo _url ('index', 'formLogin'); ?>"><?php echo Minz_Translate::t ('login'); ?></a></li><?php
+ }
+ break;
+ case 'persona':
+ if ($this->loginOk) {
+ ?><li class="item"><?php echo FreshRSS_Themes::icon('logout'); ?> <a class="signout" href="#"><?php echo Minz_Translate::t ('logout'); ?></a></li><?php
+ } else {
+ ?><li class="item"><?php echo FreshRSS_Themes::icon('login'); ?> <a class="signin" href="#"><?php echo Minz_Translate::t ('login'); ?></a></li><?php
+ }
+ break;
+ }
+ ?></ul><?php
+}
+?>
<div class="header">
<div class="item title">
@@ -62,16 +75,24 @@
<li class="separator"></li>
<li class="item"><a href="<?php echo _url ('index', 'about'); ?>"><?php echo Minz_Translate::t ('about'); ?></a></li>
<li class="item"><a href="<?php echo _url ('index', 'logs'); ?>"><?php echo Minz_Translate::t ('logs'); ?></a></li>
- <?php if (Minz_Configuration::canLogIn()) { ?>
- <li class="separator"></li>
- <li class="item"><a class="signout" href="#"><?php echo FreshRSS_Themes::icon('logout'); ?> <?php echo Minz_Translate::t ('logout'); ?></a></li>
- <?php } ?>
+ <?php
+ if (Minz_Configuration::canLogIn()) {
+ ?><li class="separator"></li><?php
+ switch (Minz_Configuration::authType()) {
+ case 'form':
+ ?><li class="item"><a class="signout" href="<?php echo _url ('index', 'formLogout'); ?>"><?php echo FreshRSS_Themes::icon('logout'), ' ', Minz_Translate::t ('logout'); ?></a></li><?php
+ break;
+ case 'persona':
+ ?><li class="item"><a class="signout" href="#"><?php echo FreshRSS_Themes::icon('logout'), ' ', Minz_Translate::t ('logout'); ?></a></li><?php
+ break;
+ }
+ } ?>
</ul>
</div>
</div>
- <?php } elseif (Minz_Configuration::canLogIn()) { ?>
+ <?php }/* elseif (Minz_Configuration::authType() === 'persona') { ?>
<div class="item configure">
<?php echo FreshRSS_Themes::icon('login'); ?> <a class="signin" href="#"><?php echo Minz_Translate::t ('login'); ?></a>
</div>
- <?php } ?>
+ <?php }*/ ?>
</div>
diff --git a/app/views/configure/users.phtml b/app/views/configure/users.phtml
index 68111bdbe..1597004e1 100644
--- a/app/views/configure/users.phtml
+++ b/app/views/configure/users.phtml
@@ -20,7 +20,7 @@
<div class="form-group">
<label class="group-name" for="passwordPlain"><?php echo Minz_Translate::t('password'); ?></label>
<div class="group-controls">
- <input type="password" id="passwordPlain" name="passwordPlain" />
+ <input type="password" id="passwordPlain" name="passwordPlain" pattern=".{7,}" />
<noscript><b><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></b></noscript>
</div>
</div>
@@ -52,11 +52,11 @@
<div class="group-controls">
<select id="auth_type" name="auth_type" required="required">
<option value=""></option>
+ <option value="form"<?php echo Minz_Configuration::authType() === 'form' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t('auth_form'); ?></option>
+ <option value="persona"<?php echo Minz_Configuration::authType() === 'persona' ? ' selected="selected"' : '', $this->conf->mail_login == '' ? ' disabled="disabled"' : ''; ?>><?php echo Minz_Translate::t('auth_persona'); ?></option>
+ <option value="http_auth"<?php echo Minz_Configuration::authType() === 'http_auth' ? ' selected="selected"' : '', httpAuthUser() == '' ? ' disabled="disabled"' : ''; ?>><?php echo Minz_Translate::t('http_auth'); ?> (REMOTE_USER = '<?php echo httpAuthUser(); ?>')</option>
<option value="none"<?php echo Minz_Configuration::authType() === 'none' ? ' selected="selected"' : ''; ?>><?php echo Minz_Translate::t('auth_none'); ?></option>
- <option value="http_auth"<?php echo Minz_Configuration::authType() === 'http_auth' ? ' selected="selected"' : '', httpAuthUser() == '' ? ' disabled="disabled"' : ''; ?>>HTTP Auth</option>
- <option value="persona"<?php echo Minz_Configuration::authType() === 'persona' ? ' selected="selected"' : '', $this->conf->mail_login == '' ? ' disabled="disabled"' : ''; ?>>Mozilla Persona</option>
</select>
- <code>$_SERVER['REMOTE_USER'] = `<?php echo httpAuthUser(); ?>`</code>
</div>
</div>
@@ -142,6 +142,14 @@
</div>
<div class="form-group">
+ <label class="group-name" for="new_user_passwordPlain"><?php echo Minz_Translate::t('password'); ?></label>
+ <div class="group-controls">
+ <input type="password" id="new_user_passwordPlain" name="new_user_passwordPlain" pattern=".{7,}" />
+ <noscript><b><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></b></noscript>
+ </div>
+ </div>
+
+ <div class="form-group">
<label class="group-name" for="new_user_email"><?php echo Minz_Translate::t('persona_connection_email'); ?></label>
<?php $mail = $this->conf->mail_login; ?>
<div class="group-controls">
diff --git a/app/views/helpers/javascript_vars.phtml b/app/views/helpers/javascript_vars.phtml
index 92c068f7e..935294e60 100644
--- a/app/views/helpers/javascript_vars.phtml
+++ b/app/views/helpers/javascript_vars.phtml
@@ -30,8 +30,8 @@
if ($mail != 'null') {
$mail = '"' . $mail . '"';
}
- echo 'use_persona=', Minz_Configuration::authType() === 'persona' ? 'true' : 'false',
- ',url_freshrss="', _url ('index', 'index'), '",',
+ echo 'authType="', Minz_Configuration::authType(), '",',
+ 'url_freshrss="', _url ('index', 'index'), '",',
'url_login="', _url ('index', 'login'), '",',
'url_logout="', _url ('index', 'logout'), '",',
'current_user_mail=', $mail, ",\n";
diff --git a/app/views/helpers/view/login.phtml b/app/views/helpers/view/login.phtml
new file mode 100644
index 000000000..e4a24f9ed
--- /dev/null
+++ b/app/views/helpers/view/login.phtml
@@ -0,0 +1,43 @@
+<div class="post content">
+
+<?php
+if (Minz_Configuration::canLogIn()) {
+ ?><h1><?php echo Minz_Translate::t('login'); ?></h1><?php
+ switch (Minz_Configuration::authType()) {
+
+ case 'form':
+ ?><form id="loginForm" method="post" action="<?php echo _url('index', 'formLogin'); ?>">
+ <div class="form-group">
+ <label class="group-name" for="username"><?php echo Minz_Translate::t('username'); ?></label>
+ <div class="group-controls">
+ <input type="text" id="username" name="username" size="16" required="required" maxlength="16" pattern="[0-9a-zA-Z]{1,16}" />
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="group-name" for="passwordPlain"><?php echo Minz_Translate::t('password'); ?></label>
+ <div class="group-controls">
+ <input type="password" id="passwordPlain" required="required" />
+ <input type="hidden" id="challenge" name="challenge" />
+ <noscript><strong><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></strong></noscript>
+ </div>
+ </div>
+ <div class="form-group form-actions">
+ <div class="group-controls">
+ <button id="loginButton" type="submit" class="btn btn-important"><?php echo Minz_Translate::t('login'); ?></button>
+ </div>
+ </div>
+ </form><?php
+ break;
+
+ case 'persona':
+ ?><p><?php echo FreshRSS_Themes::icon('login'); ?> <a class="signin" href="#"><?php echo Minz_Translate::t('login'); ?></a></p><?php
+ break;
+ }
+} else {
+ ?><h1>FreshRSS</h1>
+ <p><?php echo Minz_Translate::t('forbidden_access', Minz_Configuration::authType()); ?></p><?php
+}
+?>
+
+ <p><a href="<?php echo _url('index', 'about'); ?>"><?php echo Minz_Translate::t('about_freshrss'); ?></a></p>
+</div>
diff --git a/app/views/index/index.phtml b/app/views/index/index.phtml
index 549d0b61e..9b69233e9 100644
--- a/app/views/index/index.phtml
+++ b/app/views/index/index.phtml
@@ -1,15 +1,5 @@
<?php
-function showForbidden() {
-?><div class="post content">
- <h1><?php echo Minz_Translate::t ('forbidden_access'); ?></h1>
- <p><?php echo Minz_Configuration::canLogIn() ?
- Minz_Translate::t ('forbidden_access_description') :
- Minz_Translate::t ('forbidden_access') . ' (' . Minz_Configuration::authType() . ')'; ?></p>
- <p><a href="<?php echo _url ('index', 'about'); ?>"><?php echo Minz_Translate::t ('about_freshrss'); ?></a></p>
-</div><?php
-}
-
$output = Minz_Request::param ('output', 'normal');
if ($this->loginOk || Minz_Configuration::allowAnonymous()) {
@@ -31,8 +21,8 @@ if ($this->loginOk || Minz_Configuration::allowAnonymous()) {
if ($token_is_ok) {
$this->renderHelper ('view/rss_view');
} else {
- showForbidden();
+ $this->renderHelper ('view/login');
}
} else {
- showForbidden();
+ $this->renderHelper ('view/login');
}
diff --git a/lib/Minz/Configuration.php b/lib/Minz/Configuration.php
index 2c30661ed..433992e0d 100644
--- a/lib/Minz/Configuration.php
+++ b/lib/Minz/Configuration.php
@@ -109,7 +109,7 @@ class Minz_Configuration {
return self::$auth_type !== 'none';
}
public static function canLogIn() {
- return self::$auth_type === 'persona';
+ return self::$auth_type === 'form' || self::$auth_type === 'persona';
}
public static function _allowAnonymous($allow = false) {
@@ -118,6 +118,7 @@ class Minz_Configuration {
public static function _authType($value) {
$value = strtolower($value);
switch ($value) {
+ case 'form':
case 'http_auth':
case 'persona':
case 'none':
diff --git a/lib/Minz/FrontController.php b/lib/Minz/FrontController.php
index 7b8526bc8..80eda8877 100644
--- a/lib/Minz/FrontController.php
+++ b/lib/Minz/FrontController.php
@@ -34,7 +34,7 @@ class Minz_FrontController {
*/
public function __construct () {
if (LOG_PATH === false) {
- $this->killApp ('Path doesn’t exist : LOG_PATH');
+ $this->killApp ('Path not found: LOG_PATH');
}
try {
diff --git a/p/scripts/main.js b/p/scripts/main.js
index 24af1b210..0c4c3f1b2 100644
--- a/p/scripts/main.js
+++ b/p/scripts/main.js
@@ -587,6 +587,54 @@ function init_load_more(box) {
}
//</endless_mode>
+//<Web login form>
+function poormanSalt() { //If crypto.getRandomValues is not available
+ var text = '$2a$04$',
+ base = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ.0123456789/abcdefghijklmnopqrstuvwxyz';
+ for (var i = 22; i > 0; i--) {
+ text += base.charAt(Math.floor(Math.random() * 64));
+ }
+ return text;
+}
+
+function init_loginForm() {
+ var $loginForm = $('#loginForm');
+ if ($loginForm.length === 0) {
+ return;
+ }
+ if (!(window.dcodeIO)) {
+ if (window.console) {
+ console.log('FreshRSS waiting for bcrypt.js…');
+ }
+ window.setTimeout(init_loginForm, 100);
+ return;
+ }
+ $loginForm.on('submit', function() {
+ $('#loginButton').attr('disabled', '');
+ var success = false;
+ $.ajax({
+ url: './?c=javascript&a=nonce&user=' + $('#username').val(),
+ dataType: 'json',
+ async: false
+ }).done(function (data) {
+ if (data.salt1 == '' || data.nonce == '') {
+ alert('Invalid user!');
+ } else {
+ var strong = window.Uint32Array && window.crypto && (typeof window.crypto.getRandomValues === 'function'),
+ s = dcodeIO.bcrypt.hashSync($('#passwordPlain').val(), data.salt1),
+ c = dcodeIO.bcrypt.hashSync(data.nonce + s, strong ? 4 : poormanSalt());
+ $('#challenge').val(c);
+ success = true;
+ }
+ }).fail(function() {
+ alert('Communication error!');
+ });
+ $('#loginButton').removeAttr('disabled');
+ return success;
+ });
+}
+//</Web login form>
+
//<persona>
function init_persona() {
if (!(navigator.id)) {
@@ -696,8 +744,13 @@ function init_all() {
init_notifications();
init_actualize();
init_load_more($stream);
- if (use_persona) {
- init_persona();
+ switch (authType) {
+ case 'form':
+ init_loginForm();
+ break;
+ case 'persona':
+ init_persona();
+ break;
}
init_confirm_action();
init_print_action();