Bladeren bron

Merge branch 'refactor-authentication' into dev

Marien Fressinaud 11 jaren geleden
bovenliggende
commit
99cdd2a0ad

+ 250 - 0
app/Controllers/authController.php

@@ -0,0 +1,250 @@
+<?php
+
+/**
+ * This controller handles action about authentication.
+ */
+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
+	 * the user is already connected.
+	 */
+	public function loginAction() {
+		if (FreshRSS_Auth::hasAccess()) {
+			Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
+		}
+
+		$auth_type = Minz_Configuration::authType();
+		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!
+			Minz_Error::error(404);
+		default:
+			// TODO load plugin instead
+			Minz_Error::error(404);
+		}
+	}
+
+	/**
+	 * This action handles form login page.
+	 *
+	 * If this action is reached through a POST request, username and password
+	 * are compared to login the current user.
+	 *
+	 * Parameters are:
+	 *   - nonce (default: false)
+	 *   - username (default: '')
+	 *   - challenge (default: '')
+	 *   - keep_logged_in (default: false)
+	 */
+	public function formLoginAction() {
+		invalidateHttpCache();
+
+		$file_mtime = @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js');
+		Minz_View::appendScript(Minz_Url::display('/scripts/bcrypt.min.js?' . $file_mtime));
+
+		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'));
+			}
+
+			$ok = FreshRSS_FormAuth::checkCredentials(
+				$username, $conf->passwordHash, $nonce, $challenge
+			);
+			if ($ok) {
+				// Set session parameter to give access to the user.
+				Minz_Session::_param('currentUser', $username);
+				Minz_Session::_param('passwordHash', $conf->passwordHash);
+				FreshRSS_Auth::giveAccess();
+
+				// Set cookie parameter if nedded.
+				if (Minz_Request::param('keep_logged_in')) {
+					FreshRSS_FormAuth::makeCookie($username, $conf->passwordHash);
+				} else {
+					FreshRSS_FormAuth::deleteCookie();
+				}
+
+				// All is good, go back to the index.
+				Minz_Request::good(_t('login'),
+				                   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'));
+			}
+		}
+	}
+
+	/**
+	 * 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'];
+			}
+
+			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;
+		}
+	}
+
+	/**
+	 * This action removes all accesses of the current user.
+	 */
+	public function logoutAction() {
+		invalidateHttpCache();
+		FreshRSS_Auth::removeAccess();
+		Minz_Request::good(_t('disconnected'),
+		                   array('c' => 'index', 'a' => 'index'));
+	}
+
+	/**
+	 * This action resets the authentication system.
+	 *
+	 * After reseting, form auth is set by default.
+	 */
+	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;
+		}
+
+		$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'));
+			}
+		}
+	}
+}

+ 1 - 1
app/Controllers/categoryController.php

@@ -12,7 +12,7 @@ class FreshRSS_category_Controller extends Minz_ActionController {
 	 *
 	 */
 	public function firstAction() {
-		if (!$this->view->loginOk) {
+		if (!FreshRSS_Auth::hasAccess()) {
 			Minz_Error::error(
 				403,
 				array('error' => array(_t('access_denied')))

+ 2 - 2
app/Controllers/configureController.php

@@ -10,7 +10,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 	 * underlying framework.
 	 */
 	public function firstAction() {
-		if (!$this->view->loginOk) {
+		if (!FreshRSS_Auth::hasAccess()) {
 			Minz_Error::error(
 				403,
 				array('error' => array(_t('access_denied')))
@@ -229,7 +229,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 		$this->view->nb_total = $entryDAO->count();
 		$this->view->size_user = $entryDAO->size();
 
-		if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
+		if (FreshRSS_Auth::hasAccess('admin')) {
 			$this->view->size_total = $entryDAO->size(true);
 		}
 	}

+ 1 - 1
app/Controllers/entryController.php

@@ -10,7 +10,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 	 * underlying framework.
 	 */
 	public function firstAction() {
-		if (!$this->view->loginOk) {
+		if (!FreshRSS_Auth::hasAccess()) {
 			Minz_Error::error(
 				403,
 				array('error' => array(_t('access_denied')))

+ 1 - 1
app/Controllers/feedController.php

@@ -10,7 +10,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 	 * underlying framework.
 	 */
 	public function firstAction() {
-		if (!$this->view->loginOk) {
+		if (!FreshRSS_Auth::hasAccess()) {
 			// 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

+ 1 - 1
app/Controllers/importExportController.php

@@ -10,7 +10,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 	 * underlying framework.
 	 */
 	public function firstAction() {
-		if (!$this->view->loginOk) {
+		if (!FreshRSS_Auth::hasAccess()) {
 			Minz_Error::error(
 				403,
 				array('error' => array(_t('access_denied')))

+ 3 - 265
app/Controllers/indexController.php

@@ -8,7 +8,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 		$token = $this->view->conf->token;
 
 		// check if user is logged in
-		if (!$this->view->loginOk && !Minz_Configuration::allowAnonymous()) {
+		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) {
@@ -20,7 +20,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 			} 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' => 'index', 'a' => 'formLogin'));
+				Minz_Request::forward(array('c' => 'auth', 'a' => 'login'));
 				return;
 			}
 		}
@@ -207,7 +207,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 	}
 
 	public function logsAction() {
-		if (!$this->view->loginOk) {
+		if (!FreshRSS_Auth::hasAccess()) {
 			Minz_Error::error(
 				403,
 				array('error' => array(_t('access_denied')))
@@ -228,266 +228,4 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 		$this->view->logsPaginator->_nbItemsPerPage(50);
 		$this->view->logsPaginator->_currentPage($page);
 	}
-
-	public function loginAction() {
-		$this->view->_useLayout(false);
-
-		$url = 'https://verifier.login.persona.org/verify';
-		$assert = Minz_Request::param('assertion');
-		$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);
-
-		$loginOk = false;
-		$reason = '';
-		if ($res['status'] === 'okay') {
-			$email = filter_var($res['email'], FILTER_VALIDATE_EMAIL);
-			if ($email != '') {
-				$personaFile = DATA_PATH . '/persona/' . $email . '.txt';
-				if (($currentUser = @file_get_contents($personaFile)) !== false) {
-					$currentUser = trim($currentUser);
-					if (ctype_alnum($currentUser)) {
-						try {
-							$this->conf = new FreshRSS_Configuration($currentUser);
-							$loginOk = strcasecmp($email, $this->conf->mail_login) === 0;
-						} catch (Minz_Exception $e) {
-							$reason = 'Invalid configuration for user [' . $currentUser . ']! ' . $e->getMessage();	//Permission denied or conf file does not exist
-						}
-					} else {
-						$reason = 'Invalid username format [' . $currentUser . ']!';
-					}
-				}
-			} else {
-				$reason = 'Invalid email format [' . $res['email'] . ']!';
-			}
-		}
-		if ($loginOk) {
-			Minz_Session::_param('currentUser', $currentUser);
-			Minz_Session::_param('mail', $email);
-			$this->view->loginOk = true;
-			invalidateHttpCache();
-		} else {
-			$res = array();
-			$res['status'] = 'failure';
-			$res['reason'] = $reason == '' ? _t('invalid_login') : $reason;
-			Minz_Log::warning('Persona: ' . $res['reason']);
-		}
-
-		header('Content-Type: application/json; charset=UTF-8');
-		$this->view->res = json_encode($res);
-	}
-
-	public function logoutAction() {
-		$this->view->_useLayout(false);
-		invalidateHttpCache();
-		Minz_Session::_param('currentUser');
-		Minz_Session::_param('mail');
-		Minz_Session::_param('passwordHash');
-	}
-
-	private static function makeLongTermCookie($username, $passwordHash) {
-		do {
-			$token = sha1(Minz_Configuration::salt() . $username . uniqid(mt_rand(), true));
-			$tokenFile = DATA_PATH . '/tokens/' . $token . '.txt';
-		} while (file_exists($tokenFile));
-		if (@file_put_contents($tokenFile, $username . "\t" . $passwordHash) === false) {
-			return false;
-		}
-		$expire = time() + 2629744;	//1 month	//TODO: Use a configuration instead
-		Minz_Session::setLongTermCookie('FreshRSS_login', $token, $expire);
-		Minz_Session::_param('token', $token);
-		return $token;
-	}
-
-	private static function deleteLongTermCookie() {
-		Minz_Session::deleteLongTermCookie('FreshRSS_login');
-		$token = Minz_Session::param('token', null);
-		if (ctype_alnum($token)) {
-			@unlink(DATA_PATH . '/tokens/' . $token . '.txt');
-		}
-		Minz_Session::_param('token');
-		if (rand(0, 10) === 1) {
-			self::purgeTokens();
-		}
-	}
-
-	private static function purgeTokens() {
-		$oldest = time() - 2629744;	//1 month	//TODO: Use a configuration instead
-		foreach (new DirectoryIterator(DATA_PATH . '/tokens/') as $fileInfo) {
-			if ($fileInfo->getExtension() === 'txt' && $fileInfo->getMTime() < $oldest) {
-				@unlink($fileInfo->getPathname());
-			}
-		}
-	}
-
-	public function formLoginAction() {
-		if ($this->view->loginOk) {
-			Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
-		}
-
-		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);
-						if (Minz_Request::param('keep_logged_in', false)) {
-							self::makeLongTermCookie($username, $s);
-						} else {
-							self::deleteLongTermCookie();
-						}
-					} else {
-						Minz_Log::warning('Password mismatch for user ' . $username . ', nonce=' . $nonce . ', c=' . $c);
-					}
-				} catch (Minz_Exception $me) {
-					Minz_Log::warning('Login failure: ' . $me->getMessage());
-				}
-			} else {
-				Minz_Log::debug('Invalid credential parameters: user=' . $username . ' challenge=' . $c . ' nonce=' . $nonce);
-			}
-			if (!$ok) {
-				$notif = array(
-					'type' => 'bad',
-					'content' => _t('invalid_login')
-				);
-				Minz_Session::_param('notification', $notif);
-			}
-			$this->view->_useLayout(false);
-			Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
-		} elseif (Minz_Configuration::unsafeAutologinEnabled() && isset($_GET['u']) && isset($_GET['p'])) {
-			Minz_Session::_param('currentUser');
-			Minz_Session::_param('mail');
-			Minz_Session::_param('passwordHash');
-			$username = ctype_alnum($_GET['u']) ? $_GET['u'] : '';
-			$passwordPlain = $_GET['p'];
-			Minz_Request::_param('p');	//Discard plain-text password ASAP
-			$_GET['p'] = '';
-			if (!function_exists('password_verify')) {
-				include_once(LIB_PATH . '/password_compat.php');
-			}
-			try {
-				$conf = new FreshRSS_Configuration($username);
-				$s = $conf->passwordHash;
-				$ok = password_verify($passwordPlain, $s);
-				unset($passwordPlain);
-				if ($ok) {
-					Minz_Session::_param('currentUser', $username);
-					Minz_Session::_param('passwordHash', $s);
-				} else {
-					Minz_Log::warning('Unsafe password mismatch for user ' . $username);
-				}
-			} catch (Minz_Exception $me) {
-				Minz_Log::warning('Unsafe login failure: ' . $me->getMessage());
-			}
-			Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
-		} elseif (!Minz_Configuration::canLogIn()) {
-			Minz_Error::error(
-				403,
-				array('error' => array(_t('access_denied')))
-			);
-		}
-		invalidateHttpCache();
-	}
-
-	public function formLogoutAction() {
-		$this->view->_useLayout(false);
-		invalidateHttpCache();
-		Minz_Session::_param('currentUser');
-		Minz_Session::_param('mail');
-		Minz_Session::_param('passwordHash');
-		self::deleteLongTermCookie();
-		Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
-	}
-
-	public function resetAuthAction() {
-		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;
-		}
-
-		$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', '');
-			$c = Minz_Request::param('challenge', '');
-			if (!(ctype_alnum($username) && ctype_graph($c) && ctype_alnum($nonce))) {
-				Minz_Log::debug('Invalid credential parameters:' .
-				                ' user=' . $username .
-				                ' challenge=' . $c .
-				                ' nonce=' . $nonce);
-				Minz_Request::bad(_t('invalid_login'),
-				                  array('c' => 'index', 'a' => 'resetAuth'));
-			}
-
-			if (!function_exists('password_verify')) {
-				include_once(LIB_PATH . '/password_compat.php');
-			}
-
-			$s = $conf->passwordHash;
-			$ok = password_verify($nonce . $s, $c);
-			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' => 'index', 'a' => 'resetAuth'));
-				}
-			} else {
-				Minz_Log::debug('Password mismatch for user ' . $username .
-				                ', nonce=' . $nonce . ', c=' . $c);
-
-				Minz_Request::bad(_t('invalid_login'),
-				                  array('c' => 'index', 'a' => 'resetAuth'));
-			}
-		}
-	}
 }

+ 1 - 1
app/Controllers/statsController.php

@@ -118,7 +118,7 @@ class FreshRSS_stats_Controller extends Minz_ActionController {
 	 * underlying framework.
 	 */
 	public function firstAction() {
-		if (!$this->view->loginOk) {
+		if (!FreshRSS_Auth::hasAccess()) {
 			Minz_Error::error(
 			    403, array('error' => array(_t('access_denied')))
 			);

+ 1 - 1
app/Controllers/subscriptionController.php

@@ -10,7 +10,7 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
 	 * underlying framework.
 	 */
 	public function firstAction() {
-		if (!$this->view->loginOk) {
+		if (!FreshRSS_Auth::hasAccess()) {
 			Minz_Error::error(
 				403,
 				array('error' => array(_t('access_denied')))

+ 1 - 1
app/Controllers/updateController.php

@@ -3,7 +3,7 @@
 class FreshRSS_update_Controller extends Minz_ActionController {
 	public function firstAction() {
 		$current_user = Minz_Session::param('currentUser', '');
-		if (!$this->view->loginOk && Minz_Configuration::isAdmin($current_user)) {
+		if (!FreshRSS_Auth::hasAccess('admin')) {
 			Minz_Error::error(
 				403,
 				array('error' => array(_t('access_denied')))

+ 5 - 5
app/Controllers/usersController.php

@@ -5,7 +5,7 @@ class FreshRSS_users_Controller extends Minz_ActionController {
 	const BCRYPT_COST = 9;	//Will also have to be computed client side on mobile devices, so do not use a too high cost
 
 	public function firstAction() {
-		if (!$this->view->loginOk) {
+		if (!FreshRSS_Auth::hasAccess()) {
 			Minz_Error::error(
 				403,
 				array('error' => array(_t('access_denied')))
@@ -51,7 +51,7 @@ class FreshRSS_users_Controller extends Minz_ActionController {
 				$this->view->conf->_apiPasswordHash($passwordHash);
 			}
 
-			if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
+			if (FreshRSS_Auth::hasAccess('admin')) {
 				$this->view->conf->_mail_login(Minz_Request::param('mail_login', '', true));
 			}
 			$email = $this->view->conf->mail_login;
@@ -65,7 +65,7 @@ class FreshRSS_users_Controller extends Minz_ActionController {
 				$ok &= (file_put_contents($personaFile, Minz_Session::param('currentUser', '_')) !== false);
 			}
 
-			if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
+			if (FreshRSS_Auth::hasAccess('admin')) {
 				$current_token = $this->view->conf->token;
 				$token = Minz_Request::param('token', $current_token);
 				$this->view->conf->_token($token);
@@ -105,7 +105,7 @@ class FreshRSS_users_Controller extends Minz_ActionController {
 	}
 
 	public function createAction() {
-		if (Minz_Request::isPost() && Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
+		if (Minz_Request::isPost() && FreshRSS_Auth::hasAccess('admin')) {
 			$db = Minz_Configuration::dataBase();
 			require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
 
@@ -177,7 +177,7 @@ class FreshRSS_users_Controller extends Minz_ActionController {
 	}
 
 	public function deleteAction() {
-		if (Minz_Request::isPost() && Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) {
+		if (Minz_Request::isPost() && FreshRSS_Auth::hasAccess('admin')) {
 			$db = Minz_Configuration::dataBase();
 			require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
 

+ 22 - 121
app/FreshRSS.php

@@ -4,130 +4,33 @@ class FreshRSS extends Minz_FrontController {
 		if (!isset($_SESSION)) {
 			Minz_Session::init('FreshRSS');
 		}
-		$loginOk = $this->accessControl(Minz_Session::param('currentUser', ''));
+
+		FreshRSS_Auth::init();
+		$this->loadConfiguration();
 		$this->loadParamsView();
 		if (Minz_Request::isPost() && !is_referer_from_same_domain()) {
-			$loginOk = false;	//Basic protection against XSRF attacks
+			//Basic protection against XSRF attacks
+			FreshRSS_Auth::removeAccess();
 			Minz_Error::error(
 				403,
 				array('error' => array(_t('access_denied') . ' [HTTP_REFERER=' .
-					htmlspecialchars(empty($_SERVER['HTTP_REFERER']) ? '' : $_SERVER['HTTP_REFERER']) . ']'))
+				      htmlspecialchars(empty($_SERVER['HTTP_REFERER']) ? '' : $_SERVER['HTTP_REFERER']) . ']'))
 			);
 		}
-		Minz_View::_param('loginOk', $loginOk);
-		$this->loadStylesAndScripts($loginOk);	//TODO: Do not load that when not needed, e.g. some Ajax requests
+		$this->loadStylesAndScripts();
 		$this->loadNotifications();
 		$this->loadExtensions();
 	}
 
-	private static function getCredentialsFromLongTermCookie() {
-		$token = Minz_Session::getLongTermCookie('FreshRSS_login');
-		if (!ctype_alnum($token)) {
-			return array();
-		}
-		$tokenFile = DATA_PATH . '/tokens/' . $token . '.txt';
-		$mtime = @filemtime($tokenFile);
-		if ($mtime + 2629744 < time()) {	//1 month	//TODO: Use a configuration instead
-			@unlink($tokenFile);
-			return array(); 	//Expired or token does not exist
-		}
-		$credentials = @file_get_contents($tokenFile);
-		return $credentials === false ? array() : explode("\t", $credentials, 2);
-	}
-
-	private function accessControl($currentUser) {
-		if ($currentUser == '') {
-			switch (Minz_Configuration::authType()) {
-				case 'form':
-					$credentials = self::getCredentialsFromLongTermCookie();
-					if (isset($credentials[1])) {
-						$currentUser = trim($credentials[0]);
-						Minz_Session::_param('passwordHash', trim($credentials[1]));
-					}
-					$loginOk = $currentUser != '';
-					if (!$loginOk) {
-						$currentUser = Minz_Configuration::defaultUser();
-						Minz_Session::_param('passwordHash');
-					}
-					break;
-				case 'http_auth':
-					$currentUser = httpAuthUser();
-					$loginOk = $currentUser != '';
-					break;
-				case 'persona':
-					$loginOk = false;
-					$email = filter_var(Minz_Session::param('mail'), FILTER_VALIDATE_EMAIL);
-					if ($email != '') {	//TODO: Remove redundancy with indexController
-						$personaFile = DATA_PATH . '/persona/' . $email . '.txt';
-						if (($currentUser = @file_get_contents($personaFile)) !== false) {
-							$currentUser = trim($currentUser);
-							$loginOk = true;
-						}
-					}
-					if (!$loginOk) {
-						$currentUser = Minz_Configuration::defaultUser();
-					}
-					break;
-				case 'none':
-					$currentUser = Minz_Configuration::defaultUser();
-					$loginOk = true;
-					break;
-				default:
-					$currentUser = Minz_Configuration::defaultUser();
-					$loginOk = false;
-					break;
-			}
-		} else {
-			$loginOk = true;
-		}
-
-		if (!ctype_alnum($currentUser)) {
-			Minz_Session::_param('currentUser', '');
-			die('Invalid username [' . $currentUser . ']!');
-		}
-
+	private function loadConfiguration() {
+		$current_user = Minz_Session::param('currentUser');
 		try {
-			$this->conf = new FreshRSS_Configuration($currentUser);
+			$this->conf = new FreshRSS_Configuration($current_user);
 			Minz_View::_param('conf', $this->conf);
-			Minz_Session::_param('currentUser', $currentUser);
-		} catch (Minz_Exception $me) {
-			$loginOk = false;
-			try {
-				$this->conf = new FreshRSS_Configuration(Minz_Configuration::defaultUser());
-				Minz_Session::_param('currentUser', Minz_Configuration::defaultUser());
-				Minz_View::_param('conf', $this->conf);
-				$notif = array(
-					'type' => 'bad',
-					'content' => 'Invalid configuration for user [' . $currentUser . ']!',
-				);
-				Minz_Session::_param('notification', $notif);
-				Minz_Log::warning($notif['content'] . ' ' . $me->getMessage());
-				Minz_Session::_param('currentUser', '');
-			} catch (Exception $e) {
-				die($e->getMessage());
-			}
-		}
-
-		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;
-				case 'persona':
-					$loginOk = strcasecmp(Minz_Session::param('mail'), $this->conf->mail_login) === 0;
-					break;
-				case 'none':
-					$loginOk = true;
-					break;
-				default:
-					$loginOk = false;
-					break;
-			}
+		} catch(Minz_Exception $e) {
+			Minz_Log::error('Cannot load configuration file of user `' . $current_user . '`');
+			die($e->getMessage());
 		}
-		return $loginOk;
 	}
 
 	private function loadParamsView() {
@@ -140,7 +43,7 @@ class FreshRSS extends Minz_FrontController {
 		}
 	}
 
-	private function loadStylesAndScripts($loginOk) {
+	private function loadStylesAndScripts() {
 		$theme = FreshRSS_Themes::load($this->conf->theme);
 		if ($theme) {
 			foreach($theme['files'] as $file) {
@@ -158,19 +61,17 @@ class FreshRSS extends Minz_FrontController {
 			}
 		}
 
-		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;
-		}
 		Minz_View::appendScript(Minz_Url::display('/scripts/jquery.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/jquery.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')));
+
+		if (Minz_Configuration::authType() === 'persona') {
+			// TODO move it in a plugin
+			// Needed for login AND logout with Persona.
+			Minz_View::appendScript('https://login.persona.org/include.js');
+			$file_mtime = @filemtime(PUBLIC_PATH . '/scripts/persona.js');
+			Minz_View::appendScript(Minz_Url::display('/scripts/persona.js?' . $file_mtime));
+		}
 	}
 
 	private function loadNotifications() {

+ 235 - 0
app/Models/Auth.php

@@ -0,0 +1,235 @@
+<?php
+
+/**
+ * This class handles all authentication process.
+ */
+class FreshRSS_Auth {
+	/**
+	 * Determines if user is connected.
+	 */
+	private static $login_ok = false;
+
+	/**
+	 * This method initializes authentication system.
+	 */
+	public static function init() {
+		self::$login_ok = Minz_Session::param('loginOk', false);
+		$current_user = Minz_Session::param('currentUser', '');
+		if ($current_user === '') {
+			$current_user = Minz_Configuration::defaultUser();
+			Minz_Session::_param('currentUser', $current_user);
+		}
+
+		$access_ok = self::accessControl();
+
+		if ($access_ok) {
+			self::giveAccess();
+		} else {
+			// Be sure all accesses are removed!
+			self::removeAccess();
+		}
+	}
+
+	/**
+	 * This method checks if user is allowed to connect.
+	 *
+	 * Required session parameters are also set in this method (such as
+	 * currentUser).
+	 *
+	 * @return boolean true if user can be connected, false else.
+	 */
+	public static function accessControl() {
+		if (self::$login_ok) {
+			return true;
+		}
+
+		switch (Minz_Configuration::authType()) {
+		case 'form':
+			$credentials = FreshRSS_FormAuth::getCredentialsFromCookie();
+			$current_user = '';
+			if (isset($credentials[1])) {
+				$current_user = trim($credentials[0]);
+				Minz_Session::_param('currentUser', $current_user);
+				Minz_Session::_param('passwordHash', trim($credentials[1]));
+			}
+			return $current_user != '';
+		case 'http_auth':
+			$current_user = httpAuthUser();
+			$login_ok = $current_user != '';
+			if ($login_ok) {
+				Minz_Session::_param('currentUser', $current_user);
+			}
+			return $login_ok;
+		case 'persona':
+			$email = filter_var(Minz_Session::param('mail'), FILTER_VALIDATE_EMAIL);
+			$persona_file = DATA_PATH . '/persona/' . $email . '.txt';
+			if (($current_user = @file_get_contents($persona_file)) !== false) {
+				$current_user = trim($current_user);
+				Minz_Session::_param('currentUser', $current_user);
+				Minz_Session::_param('mail', $email);
+				return true;
+			}
+			return false;
+		case 'none':
+			return true;
+		default:
+			// TODO load extension
+			return false;
+		}
+	}
+
+	/**
+	 * Gives access to the current user.
+	 */
+	public static function giveAccess() {
+		$current_user = Minz_Session::param('currentUser');
+		try {
+			$conf = new FreshRSS_Configuration($current_user);
+		} catch(Minz_Exception $e) {
+			die($e->getMessage());
+		}
+
+		switch (Minz_Configuration::authType()) {
+		case 'form':
+			self::$login_ok = Minz_Session::param('passwordHash') === $conf->passwordHash;
+			break;
+		case 'http_auth':
+			self::$login_ok = strcasecmp($current_user, httpAuthUser()) === 0;
+			break;
+		case 'persona':
+			self::$login_ok = strcasecmp(Minz_Session::param('mail'), $conf->mail_login) === 0;
+			break;
+		case 'none':
+			self::$login_ok = true;
+			break;
+		default:
+			// TODO: extensions
+			self::$login_ok = false;
+		}
+
+		Minz_Session::_param('loginOk', self::$login_ok);
+	}
+
+	/**
+	 * Returns if current user has access to the given scope.
+	 *
+	 * @param string $scope general (default) or admin
+	 * @return boolean true if user has corresponding access, false else.
+	 */
+	public static function hasAccess($scope = 'general') {
+		$ok = self::$login_ok;
+		switch ($scope) {
+		case 'general':
+			break;
+		case 'admin':
+			$ok &= Minz_Session::param('currentUser') === Minz_Configuration::defaultUser();
+			break;
+		default:
+			$ok = false;
+		}
+		return $ok;
+	}
+
+	/**
+	 * Removes all accesses for the current user.
+	 */
+	public static function removeAccess() {
+		Minz_Session::_param('loginOk');
+		self::$login_ok = false;
+		Minz_Session::_param('currentUser', Minz_Configuration::defaultUser());
+
+		switch (Minz_Configuration::authType()) {
+		case 'form':
+			Minz_Session::_param('passwordHash');
+			FreshRSS_FormAuth::deleteCookie();
+			break;
+		case 'persona':
+			Minz_Session::_param('mail');
+			break;
+		case 'http_auth':
+		case 'none':
+			// Nothing to do...
+			break;
+		default:
+			// TODO: extensions
+		}
+	}
+}
+
+
+class FreshRSS_FormAuth {
+	public static function checkCredentials($username, $hash, $nonce, $challenge) {
+		if (!ctype_alnum($username) ||
+				!ctype_graph($challenge) ||
+				!ctype_alnum($nonce)) {
+			Minz_Log::debug('Invalid credential parameters:' .
+			                ' user=' . $username .
+			                ' challenge=' . $challenge .
+			                ' nonce=' . $nonce);
+			return false;
+		}
+
+		if (!function_exists('password_verify')) {
+			include_once(LIB_PATH . '/password_compat.php');
+		}
+
+		return password_verify($nonce . $hash, $challenge);
+	}
+
+	public static function getCredentialsFromCookie() {
+		$token = Minz_Session::getLongTermCookie('FreshRSS_login');
+		if (!ctype_alnum($token)) {
+			return array();
+		}
+
+		$token_file = DATA_PATH . '/tokens/' . $token . '.txt';
+		$mtime = @filemtime($token_file);
+		if ($mtime + 2629744 < time()) {
+			// Token has expired (> 1 month) or does not exist.
+			// TODO: 1 month -> use a configuration instead
+			@unlink($token_file);
+			return array(); 	
+		}
+
+		$credentials = @file_get_contents($token_file);
+		return $credentials === false ? array() : explode("\t", $credentials, 2);
+	}
+
+	public static function makeCookie($username, $password_hash) {
+		do {
+			$token = sha1(Minz_Configuration::salt() . $username . uniqid(mt_rand(), true));
+			$token_file = DATA_PATH . '/tokens/' . $token . '.txt';
+		} while (file_exists($token_file));
+
+		if (@file_put_contents($token_file, $username . "\t" . $password_hash) === false) {
+			return false;
+		}
+
+		$expire = time() + 2629744;	//1 month	//TODO: Use a configuration instead
+		Minz_Session::setLongTermCookie('FreshRSS_login', $token, $expire);
+		return $token;
+	}
+
+	public static function deleteCookie() {
+		$token = Minz_Session::getLongTermCookie('FreshRSS_login');
+		Minz_Session::deleteLongTermCookie('FreshRSS_login');
+		if (ctype_alnum($token)) {
+			@unlink(DATA_PATH . '/tokens/' . $token . '.txt');
+		}
+
+		if (rand(0, 10) === 1) {
+			self::purgeTokens();
+		}
+	}
+
+	public static function purgeTokens() {
+		$oldest = time() - 2629744;	// 1 month	// TODO: Use a configuration instead
+		foreach (new DirectoryIterator(DATA_PATH . '/tokens/') as $file_info) {
+			// $extension = $file_info->getExtension(); doesn't work in PHP < 5.3.7
+			$extension = pathinfo($file_info->getFilename(), PATHINFO_EXTENSION);
+			if ($extension === 'txt' && $file_info->getMTime() < $oldest) {
+				@unlink($file_info->getPathname());
+			}
+		}
+	}
+}

+ 1 - 4
app/layout/aside_configure.phtml

@@ -22,10 +22,7 @@
 	<li class="item<?php echo Minz_Request::controllerName() === 'users' ? ' active' : ''; ?>">
 		<a href="<?php echo _url('users', 'index'); ?>"><?php echo _t('users'); ?></a>
 	</li>
-	<?php
-		$current_user = Minz_Session::param('currentUser', '');
-		if (Minz_Configuration::isAdmin($current_user)) {
-	?>
+	<?php if (FreshRSS_Auth::hasAccess('admin')) { ?>
 	<li class="item<?php echo Minz_Request::controllerName() === 'update' ? ' active' : ''; ?>">
 		<a href="<?php echo _url('update', 'index'); ?>"><?php echo _t('update'); ?></a>
 	</li>

+ 3 - 3
app/layout/aside_flux.phtml

@@ -2,7 +2,7 @@
 	<a class="toggle_aside" href="#close"><?php echo _i('close'); ?></a>
 
 	<ul class="categories">
-		<?php if ($this->loginOk) { ?>
+		<?php if (FreshRSS_Auth::hasAccess()) { ?>
 		<form id="mark-read-aside" method="post" style="display: none"></form>
 
 		<li>
@@ -83,11 +83,11 @@
 	<ul class="dropdown-menu">
 		<li class="dropdown-close"><a href="#close">❌</a></li>
 		<li class="item"><a href="<?php echo _url('index', 'index', 'get', 'f_!!!!!!'); ?>"><?php echo _t('filter'); ?></a></li>
-		<?php if ($this->loginOk) { ?>
+		<?php if (FreshRSS_Auth::hasAccess()) { ?>
 		<li class="item"><a href="<?php echo _url('stats', 'repartition', 'id', '!!!!!!'); ?>"><?php echo _t('stats'); ?></a></li>
 		<?php } ?>
 		<li class="item"><a target="_blank" href="http://example.net/"><?php echo _t('see_website'); ?></a></li>
-		<?php if ($this->loginOk) { ?>
+		<?php if (FreshRSS_Auth::hasAccess()) { ?>
 		<li class="separator"></li>
 		<li class="item"><a href="<?php echo _url('subscription', 'index', 'id', '!!!!!!'); ?>"><?php echo _t('administration'); ?></a></li>
 		<li class="item"><a href="<?php echo _url('feed', 'actualize', 'id', '!!!!!!'); ?>"><?php echo _t('actualize'); ?></a></li>

+ 13 - 41
app/layout/header.phtml

@@ -1,22 +1,11 @@
 <?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 _i('logout'); ?> <a class="signout" href="<?php echo _url('index', 'formLogout'); ?>"><?php echo _t('logout'); ?></a></li><?php
+		if (FreshRSS_Auth::hasAccess()) {
+			?><li class="item"><?php echo _i('logout'); ?> <a class="signout" href="<?php echo _url('auth', 'logout'); ?>"><?php echo _t('logout'); ?></a></li><?php
 		} else {
-			?><li class="item"><?php echo _i('login'); ?> <a class="signin" href="<?php echo _url('index', 'formLogin'); ?>"><?php echo _t('login'); ?></a></li><?php
+			?><li class="item"><?php echo _i('login'); ?> <a class="signin" href="<?php echo _url('auth', 'login'); ?>"><?php echo _t('login'); ?></a></li><?php
 		}
-		break;
-	case 'persona':
-		if ($this->loginOk) {
-			?><li class="item"><?php echo _i('logout'); ?> <a class="signout" href="#"><?php echo _t('logout'); ?></a></li><?php
-		} else {
-			?><li class="item"><?php echo _i('login'); ?> <a class="signin" href="#"><?php echo _t('login'); ?></a></li><?php
-		}
-		break;
-	}
 	?></ul><?php
 }
 ?>
@@ -32,7 +21,7 @@ if (Minz_Configuration::canLogIn()) {
 	</div>
 
 	<div class="item search">
-		<?php if ($this->loginOk || Minz_Configuration::allowAnonymous()) { ?>
+		<?php if (FreshRSS_Auth::hasAccess() || Minz_Configuration::allowAnonymous()) { ?>
 		<form action="<?php echo _url('index', 'index'); ?>" method="get">
 			<div class="stick">
 				<?php $search = Minz_Request::param('search', ''); ?>
@@ -59,7 +48,7 @@ if (Minz_Configuration::canLogIn()) {
 		<?php } ?>
 	</div>
 
-	<?php if ($this->loginOk) { ?>
+	<?php if (FreshRSS_Auth::hasAccess()) { ?>
 	<div class="item configure">
 		<div class="dropdown">
 			<div id="dropdown-configure" class="dropdown-target"></div>
@@ -75,10 +64,7 @@ if (Minz_Configuration::canLogIn()) {
 				<li class="item"><a href="<?php echo _url('configure', 'queries'); ?>"><?php echo _t('queries'); ?></a></li>
 				<li class="separator"></li>
 				<li class="item"><a href="<?php echo _url('users', 'index'); ?>"><?php echo _t('users'); ?></a></li>
-				<?php
-					$current_user = Minz_Session::param('currentUser', '');
-					if (Minz_Configuration::isAdmin($current_user)) {
-				?>
+				<?php if (FreshRSS_Auth::hasAccess('admin')) { ?>
 				<li class="item"><a href="<?php echo _url('update', 'index'); ?>"><?php echo _t('update'); ?></a></li>
 				<?php } ?>
 				<li class="separator"></li>
@@ -87,29 +73,15 @@ if (Minz_Configuration::canLogIn()) {
 				<li class="item"><a href="<?php echo _url('index', 'about'); ?>"><?php echo _t('about'); ?></a></li>
 				<?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 _i('logout'), ' ', _t('logout'); ?></a></li><?php
-						break;
-					case 'persona':
-						?><li class="item"><a class="signout" href="#"><?php echo _i('logout'), ' ', _t('logout'); ?></a></li><?php
-						break;
-					}
+					?><li class="separator"></li>
+				<li class="item"><a class="signout" href="<?php echo _url('auth', 'logout'); ?>"><?php echo _i('logout'), ' ', _t('logout'); ?></a></li><?php
 				} ?>
 			</ul>
 		</div>
 	</div>
-	<?php } elseif (Minz_Configuration::canLogIn()) {
-		?><div class="item configure"><?php
-		switch (Minz_Configuration::authType()) {
-		case 'form':
-			echo _i('login'); ?><a class="signin" href="<?php echo _url('index', 'formLogin'); ?>"><?php echo _t('login'); ?></a></li><?php
-			break;
-		case 'persona':
-			echo _i('login'); ?><a class="signin" href="#"><?php echo _t('login'); ?></a></li><?php
-			break;
-		}
-		?></div><?php
-	} ?>
+	<?php } elseif (Minz_Configuration::canLogIn()) { ?>
+	<div class="item configure">
+		<?php echo _i('login'); ?><a class="signin" href="<?php echo _url('auth', 'login'); ?>"><?php echo _t('login'); ?></a>
+	</div>
+	<?php } ?>
 </div>

+ 2 - 2
app/layout/nav_menu.phtml

@@ -6,7 +6,7 @@
 	<a class="btn toggle_aside" href="#aside_flux"><?php echo _i('category'); ?></a>
 	<?php } ?>
 
-	<?php if ($this->loginOk) { ?>
+	<?php if (FreshRSS_Auth::hasAccess()) { ?>
 	<div id="nav_menu_actions" class="stick">
 		<?php
 			$url_state = $this->url;
@@ -300,7 +300,7 @@
 		<?php echo _i($icon); ?>
 	</a>
 	
-	<?php if ($this->loginOk || Minz_Configuration::allowAnonymousRefresh()) { ?>
+	<?php if (FreshRSS_Auth::hasAccess() || Minz_Configuration::allowAnonymousRefresh()) { ?>
 	<a id="actualize" class="btn" href="<?php echo _url('feed', 'actualize'); ?>"><?php echo _i('refresh'); ?></a>
 	<?php } ?>
 </div>

+ 3 - 21
app/views/index/formLogin.phtml → app/views/auth/formLogin.phtml

@@ -1,9 +1,7 @@
 <div class="prompt">
-	<h1><?php echo _t('login'); ?></h1><?php
+	<h1><?php echo _t('login'); ?></h1>
 
-	switch (Minz_Configuration::authType()) {
-	case 'form':
-	?><form id="crypto-form" method="post" action="<?php echo _url('index', 'formLogin'); ?>">
+	<form id="crypto-form" method="post" action="<?php echo _url('auth', 'login'); ?>">
 		<div>
 			<label for="username"><?php echo _t('username'); ?></label>
 			<input type="text" id="username" name="username" size="16" required="required" maxlength="16" pattern="[0-9a-zA-Z]{1,16}" autofocus="autofocus" />
@@ -24,23 +22,7 @@
 		<div>
 			<button id="loginButton" type="submit" class="btn btn-important"><?php echo _t('login'); ?></button>
 		</div>
-	</form><?php
-		break;
-
-	case 'persona':
-		?><p>
-			<a class="signin btn btn-important" href="#">
-				<?php echo _i('login'); ?>
-				<?php echo _t('login_with_persona'); ?>
-			</a><br /><br />
-
-			<?php echo _i('help'); ?>
-			<small>
-				<a href="<?php echo _url('index', 'resetAuth'); ?>"><?php echo _t('login_persona_problem'); ?></a>
-			</small>
-		</p><?php
-		break;
-	} ?>
+	</form>
 
 	<p><a href="<?php echo _url('index', 'about'); ?>"><?php echo _t('about_freshrss'); ?></a></p>
 </div>

+ 0 - 0
app/views/auth/logout.phtml


+ 24 - 0
app/views/auth/personaLogin.phtml

@@ -0,0 +1,24 @@
+<?php if ($this->res === false) { ?>
+<div class="prompt">
+	<h1><?php echo _t('login'); ?></h1>
+
+	<p>
+		<a class="signin btn btn-important" href="<?php echo _url('auth', 'login'); ?>">
+			<?php echo _i('login'); ?> <?php echo _t('login_with_persona'); ?>
+		</a>
+
+		<br /><br />
+
+		<?php echo _i('help'); ?>
+		<small>
+			<a href="<?php echo _url('auth', 'reset'); ?>"><?php echo _t('login_persona_problem'); ?></a>
+		</small>
+	</p>
+
+	<p><a href="<?php echo _url('index', 'about'); ?>"><?php echo _t('about_freshrss'); ?></a></p>
+</div>
+<?php
+} else {
+	echo json_encode($this->res);
+}
+?>

+ 1 - 1
app/views/index/resetAuth.phtml → app/views/auth/reset.phtml

@@ -9,7 +9,7 @@
 	<?php } ?>
 
 	<?php if (!$this->no_form) { ?>
-	<form id="crypto-form" method="post" action="<?php echo _url('index', 'resetAuth'); ?>">
+	<form id="crypto-form" method="post" action="<?php echo _url('auth', 'reset'); ?>">
 		<p class="alert alert-warn">
 			<span class="alert-head"><?php echo _t('attention'); ?></span><br />
 			<?php echo _t('auth_will_reset'); ?>

+ 1 - 1
app/views/configure/archiving.phtml

@@ -67,7 +67,7 @@
 			</div>
 		</div>
 
-		<?php if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) { ?>
+		<?php if (FreshRSS_Auth::hasAccess('admin')) { ?>
 		<div class="form-group">
 			<p class="group-name"><?php echo _t('users'); ?></p>
 			<div class="group-controls">

+ 11 - 2
app/views/helpers/javascript_vars.phtml

@@ -8,6 +8,15 @@ $hide_posts = ($this->conf->display_posts ||
                Minz_Request::param('output') === 'reader');
 $s = $this->conf->shortcuts;
 
+$url_login = Minz_Url::display(array(
+	'c' => 'auth',
+	'a' => 'login'
+), 'php');
+$url_logout = Minz_Url::display(array(
+	'c' => 'auth',
+	'a' => 'logout'
+), 'php');
+
 echo 'var context={',
 	'hide_posts:', $hide_posts ? 'false' : 'true', ',',
 	'display_order:"', Minz_Request::param('order', $this->conf->sort_order), '",',
@@ -43,8 +52,8 @@ echo 'shortcuts={',
 
 echo 'url={',
 	'index:"', _url('index', 'index'), '",',
-	'login:"', _url('index', 'login'), '",',
-	'logout:"', _url('index', 'logout'), '",',
+	'login:"', $url_login, '",',
+	'logout:"', $url_logout, '",',
 	'help:"', FRESHRSS_WIKI, '"',
 "},\n";
 

+ 3 - 3
app/views/helpers/view/normal_view.phtml

@@ -7,7 +7,7 @@ if (!empty($this->entries)) {
 	$display_today = true;
 	$display_yesterday = true;
 	$display_others = true;
-	if ($this->loginOk) {
+	if (FreshRSS_Auth::hasAccess()) {
 		$sharing = $this->conf->sharing;
 	} else {
 		$sharing = array();
@@ -58,7 +58,7 @@ if (!empty($this->entries)) {
 		}
 	?><div class="flux<?php echo !$item->isRead() ? ' not_read' : ''; ?><?php echo $item->isFavorite() ? ' favorite' : ''; ?>" id="flux_<?php echo $item->id(); ?>">
 		<ul class="horizontal-list flux_header"><?php
-			if ($this->loginOk) {
+			if (FreshRSS_Auth::hasAccess()) {
 				if ($topline_read) {
 					?><li class="item manage"><?php
 						$arUrl = array('c' => 'entry', 'a' => 'read', 'params' => array('id' => $item->id()));
@@ -103,7 +103,7 @@ if (!empty($this->entries)) {
 				?>
 			</div>
 			<ul class="horizontal-list bottom"><?php
-				if ($this->loginOk) {
+				if (FreshRSS_Auth::hasAccess()) {
 					if ($bottomline_read) {
 						?><li class="item manage"><?php
 							$arUrl = array('c' => 'entry', 'a' => 'read', 'params' => array('id' => $item->id()));

+ 1 - 1
app/views/index/index.phtml

@@ -2,7 +2,7 @@
 
 $output = Minz_Request::param('output', 'normal');
 
-if ($this->loginOk || Minz_Configuration::allowAnonymous()) {
+if (FreshRSS_Auth::hasAccess() || Minz_Configuration::allowAnonymous()) {
 	if ($output === 'normal') {
 		$this->renderHelper('view/normal_view');
 	} elseif ($output === 'reader') {

+ 0 - 1
app/views/index/login.phtml

@@ -1 +0,0 @@
-<?php print_r($this->res); ?>

+ 0 - 1
app/views/index/logout.phtml

@@ -1 +0,0 @@
-OK

+ 3 - 3
app/views/users/index.phtml

@@ -11,7 +11,7 @@
 			<div class="group-controls">
 				<input id="current_user" type="text" disabled="disabled" value="<?php echo Minz_Session::param('currentUser', '_'); ?>" />
 				<label class="checkbox" for="is_admin">
-					<input type="checkbox" id="is_admin" disabled="disabled" <?php echo Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_')) ? 'checked="checked" ' : ''; ?>/>
+					<input type="checkbox" id="is_admin" disabled="disabled" <?php echo FreshRSS_Auth::hasAccess('admin') ? 'checked="checked" ' : ''; ?>/>
 					<?php echo _t('is_admin'); ?>
 				</label>
 			</div>
@@ -44,7 +44,7 @@
 			<label class="group-name" for="mail_login"><?php echo _t('persona_connection_email'); ?></label>
 			<?php $mail = $this->conf->mail_login; ?>
 			<div class="group-controls">
-				<input type="email" id="mail_login" name="mail_login" class="extend" autocomplete="off" value="<?php echo $mail; ?>" <?php echo Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_')) ? '' : 'disabled="disabled"'; ?> placeholder="alice@example.net" />
+				<input type="email" id="mail_login" name="mail_login" class="extend" autocomplete="off" value="<?php echo $mail; ?>" <?php echo FreshRSS_Auth::hasAccess('admin') ? '' : 'disabled="disabled"'; ?> placeholder="alice@example.net" />
 				<noscript><b><?php echo _t('javascript_should_be_activated'); ?></b></noscript>
 			</div>
 		</div>
@@ -56,7 +56,7 @@
 			</div>
 		</div>
 
-	<?php if (Minz_Configuration::isAdmin(Minz_Session::param('currentUser', '_'))) { ?>
+	<?php if (FreshRSS_Auth::hasAccess('admin')) { ?>
 
 		<legend><?php echo _t('auth_type'); ?></legend>
 

+ 0 - 3
lib/Minz/Configuration.php

@@ -100,9 +100,6 @@ class Minz_Configuration {
 	public static function defaultUser () {
 		return self::$default_user;
 	}
-	public static function isAdmin($currentUser) {
-		return $currentUser === self::$default_user;
-	}
 	public static function allowAnonymous() {
 		return self::$allow_anonymous;
 	}

+ 0 - 65
p/scripts/main.js

@@ -1034,67 +1034,7 @@ function init_crypto_form() {
 }
 //</crypto form (Web login)>
 
-//<persona>
-function init_persona() {
-	if (!(navigator.id)) {
-		if (window.console) {
-			console.log('FreshRSS waiting for Persona…');
-		}
-		window.setTimeout(init_persona, 100);
-		return;
-	}
-	$('a.signin').click(function() {
-		navigator.id.request();
-		return false;
-	});
-
-	$('a.signout').click(function() {
-		navigator.id.logout();
-		return false;
-	});
 
-	navigator.id.watch({
-		loggedInUser: context['current_user_mail'],
-
-		onlogin: function(assertion) {
-			// A user has logged in! Here you need to:
-			// 1. Send the assertion to your backend for verification and to create a session.
-			// 2. Update your UI.
-			$.ajax ({
-				type: 'POST',
-				url: url['login'],
-				data: {assertion: assertion},
-				success: function(res, status, xhr) {
-					/*if (res.status === 'failure') {
-						alert (res_obj.reason);
-					} else*/ if (res.status === 'okay') {
-						location.href = url['index'];
-					}
-				},
-				error: function(res, status, xhr) {
-					alert("Login failure: " + res);
-				}
-			});
-		},
-		onlogout: function() {
-			// A user has logged out! Here you need to:
-			// Tear down the user's session by redirecting the user or making a call to your backend.
-			// Also, make sure loggedInUser will get set to null on the next page load.
-			// (That's a literal JavaScript null. Not false, 0, or undefined. null.)
-			$.ajax ({
-				type: 'POST',
-				url: url['logout'],
-				success: function(res, status, xhr) {
-					location.href = url['index'];
-				},
-				error: function(res, status, xhr) {
-					//alert("logout failure" + res);
-				}
-			});
-		}
-	});
-}
-//</persona>
 
 function init_confirm_action() {
 	$('body').on('click', '.confirm', function () {
@@ -1274,11 +1214,6 @@ function init_all() {
 		return;
 	}
 	init_notifications();
-	switch (context['auth_type']) {
-		case 'persona':
-			init_persona();
-			break;
-	}
 	init_confirm_action();
 	$stream = $('#stream');
 	if ($stream.length > 0) {

+ 76 - 0
p/scripts/persona.js

@@ -0,0 +1,76 @@
+"use strict";
+
+function init_persona() {
+	if (!(navigator.id && window.$)) {
+		if (window.console) {
+			console.log('FreshRSS (Persona) waiting for JS…');
+		}
+		window.setTimeout(init_persona, 100);
+		return;
+	}
+
+	$('a.signin').click(function() {
+		navigator.id.request();
+		return false;
+	});
+
+	$('a.signout').click(function() {
+		navigator.id.logout();
+		return false;
+	});
+
+	navigator.id.watch({
+		loggedInUser: context['current_user_mail'],
+
+		onlogin: function(assertion) {
+			// A user has logged in! Here you need to:
+			// 1. Send the assertion to your backend for verification and to create a session.
+			// 2. Update your UI.
+			$.ajax ({
+				type: 'POST',
+				url: url['login'],
+				data: {assertion: assertion},
+				success: function(res, status, xhr) {
+					if (res.status === 'failure') {
+						openNotification(res.reason, 'bad');
+					} else if (res.status === 'okay') {
+						location.href = url['index'];
+					}
+				},
+				error: function(res, status, xhr) {
+					// alert(res);
+				}
+			});
+		},
+		onlogout: function() {
+			// A user has logged out! Here you need to:
+			// Tear down the user's session by redirecting the user or making a call to your backend.
+			// Also, make sure loggedInUser will get set to null on the next page load.
+			// (That's a literal JavaScript null. Not false, 0, or undefined. null.)
+			$.ajax ({
+				type: 'POST',
+				url: url['logout'],
+				success: function(res, status, xhr) {
+					location.href = url['index'];
+				},
+				error: function(res, status, xhr) {
+					// alert(res);
+				}
+			});
+		}
+	});
+}
+
+if (document.readyState && document.readyState !== 'loading') {
+	if (window.console) {
+		console.log('FreshRSS (Persona) immediate init…');
+	}
+	init_persona();
+} else if (document.addEventListener) {
+	document.addEventListener('DOMContentLoaded', function () {
+		if (window.console) {
+			console.log('FreshRSS (Persona) waiting for DOMContentLoaded…');
+		}
+		init_persona();
+	}, false);
+}