Преглед изворни кода

Implémentation de l'indentification par mot de passe

Implémentation de
https://github.com/marienfressinaud/FreshRSS/issues/104
Alexandre Alapetite пре 12 година
родитељ
комит
d58886a937

+ 3 - 0
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)

+ 49 - 3
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);
 	}
 }

+ 6 - 6
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

+ 22 - 4
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);

+ 22 - 5
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')));
 	}
 

+ 5 - 2
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!',
 

+ 5 - 2
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 !',
 

+ 36 - 15
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>

+ 12 - 4
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>
 
@@ -141,6 +141,14 @@
 			</div>
 		</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; ?>

+ 2 - 2
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";

+ 43 - 0
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>

+ 2 - 12
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');
 }

+ 2 - 1
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':

+ 1 - 1
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 {

+ 55 - 2
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();