Explorar o código

Merge pull request #914 from marienfressinaud/679-create-accounts

Add account creation feature. To be tested for 1.1.2-beta
Alexandre Alapetite %!s(int64=10) %!d(string=hai) anos
pai
achega
28dd652228

+ 11 - 0
app/Controllers/authController.php

@@ -346,4 +346,15 @@ class FreshRSS_auth_Controller extends Minz_ActionController {
 			}
 		}
 	}
+
+	/**
+	 * This action gives possibility to a user to create an account.
+	 */
+	public function registerAction() {
+		if (max_registrations_reached()) {
+			Minz_Error::error(403);
+		}
+
+		Minz_View::prependTitle(_t('gen.auth.registration.title') . ' · ');
+	}
 }

+ 93 - 8
app/Controllers/userController.php

@@ -12,9 +12,14 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 	 * This action is called before every other action in that class. It is
 	 * the common boiler plate for every action. It is triggered by the
 	 * underlying framework.
+	 *
+	 * @todo clean up the access condition.
 	 */
 	public function firstAction() {
-		if (!FreshRSS_Auth::hasAccess()) {
+		if (!FreshRSS_Auth::hasAccess() && !(
+				Minz_Request::actionName() === 'create' &&
+				!max_registrations_reached()
+		)) {
 			Minz_Error::error(403);
 		}
 	}
@@ -25,13 +30,17 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 	public function profileAction() {
 		Minz_View::prependTitle(_t('conf.profile.title') . ' · ');
 
+		Minz_View::appendScript(Minz_Url::display(
+			'/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js')
+		));
+
 		if (Minz_Request::isPost()) {
 			$ok = true;
 
-			$passwordPlain = Minz_Request::param('passwordPlain', '', true);
+			$passwordPlain = Minz_Request::param('newPasswordPlain', '', true);
 			if ($passwordPlain != '') {
-				Minz_Request::_param('passwordPlain');	//Discard plain-text password ASAP
-				$_POST['passwordPlain'] = '';
+				Minz_Request::_param('newPasswordPlain');	//Discard plain-text password ASAP
+				$_POST['newPasswordPlain'] = '';
 				if (!function_exists('password_hash')) {
 					include_once(LIB_PATH . '/password_compat.php');
 				}
@@ -103,8 +112,24 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 		$this->view->size_user = $entryDAO->size();
 	}
 
+	/**
+	 * This action creates a new user.
+	 *
+	 * Request parameters are:
+	 *   - new_user_language
+	 *   - new_user_name
+	 *   - new_user_passwordPlain
+	 *   - new_user_email
+	 *   - r (i.e. a redirection url, optional)
+	 *
+	 * @todo clean up this method. Idea: write a method to init a user with basic information.
+	 * @todo handle r redirection in Minz_Request::forward directly?
+	 */
 	public function createAction() {
-		if (Minz_Request::isPost() && FreshRSS_Auth::hasAccess('admin')) {
+		if (Minz_Request::isPost() && (
+				FreshRSS_Auth::hasAccess('admin') ||
+				!max_registrations_reached()
+		)) {
 			$db = FreshRSS_Context::$system_conf->db;
 			require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
 
@@ -175,15 +200,37 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 			Minz_Session::_param('notification', $notif);
 		}
 
-		Minz_Request::forward(array('c' => 'user', 'a' => 'manage'), true);
+		$redirect_url = urldecode(Minz_Request::param('r', false, true));
+		if (!$redirect_url) {
+			$redirect_url = array('c' => 'user', 'a' => 'manage');
+		}
+		Minz_Request::forward($redirect_url, true);
 	}
 
+	/**
+	 * This action delete an existing user.
+	 *
+	 * Request parameter is:
+	 *   - username
+	 *
+	 * @todo clean up this method. Idea: create a User->clean() method.
+	 */
 	public function deleteAction() {
-		if (Minz_Request::isPost() && FreshRSS_Auth::hasAccess('admin')) {
+		$username = Minz_Request::param('username');
+		$redirect_url = urldecode(Minz_Request::param('r', false, true));
+		if (!$redirect_url) {
+			$redirect_url = array('c' => 'user', 'a' => 'manage');
+		}
+
+		$self_deletion = Minz_Session::param('currentUser', '_') === $username;
+
+		if (Minz_Request::isPost() && (
+				FreshRSS_Auth::hasAccess('admin') ||
+				$self_deletion
+		)) {
 			$db = FreshRSS_Context::$system_conf->db;
 			require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
 
-			$username = Minz_Request::param('username');
 			$ok = ctype_alnum($username);
 			$user_data = join_path(DATA_PATH, 'users', $username);
 
@@ -191,6 +238,16 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 				$default_user = FreshRSS_Context::$system_conf->default_user;
 				$ok &= (strcasecmp($username, $default_user) !== 0);	//It is forbidden to delete the default user
 			}
+			if ($ok && $self_deletion) {
+				// We check the password if it's a self-destruction
+				$nonce = Minz_Session::param('nonce');
+				$challenge = Minz_Request::param('challenge', '');
+
+				$ok &= FreshRSS_FormAuth::checkCredentials(
+					$username, FreshRSS_Context::$user_conf->passwordHash,
+					$nonce, $challenge
+				);
+			}
 			if ($ok) {
 				$ok &= is_dir($user_data);
 			}
@@ -200,6 +257,10 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 				$ok &= recursive_unlink($user_data);
 				//TODO: delete Persona file
 			}
+			if ($ok && $self_deletion) {
+				FreshRSS_Auth::removeAccess();
+				$redirect_url = array('c' => 'index', 'a' => 'index');
+			}
 			invalidateHttpCache();
 
 			$notif = array(
@@ -209,6 +270,30 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 			Minz_Session::_param('notification', $notif);
 		}
 
+		Minz_Request::forward($redirect_url, true);
+	}
+
+	/**
+	 * This action updates the max number of registrations.
+	 *
+	 * Request parameter is:
+	 *   - max-registrations (int >= 0)
+	 */
+	public function setRegistrationAction() {
+		if (Minz_Request::isPost() && FreshRSS_Auth::hasAccess('admin')) {
+			$limits = FreshRSS_Context::$system_conf->limits;
+			$limits['max_registrations'] = Minz_Request::param('max-registrations', 1);
+			FreshRSS_Context::$system_conf->limits = $limits;
+			FreshRSS_Context::$system_conf->save();
+
+			invalidateHttpCache();
+
+			Minz_Session::_param('notification', array(
+				'type' => 'good',
+				'content' => _t('feedback.user.set_registration')
+			));
+		}
+
 		Minz_Request::forward(array('c' => 'user', 'a' => 'manage'), true);
 	}
 }

+ 5 - 2
app/Models/ConfigurationSetter.php

@@ -352,6 +352,9 @@ class FreshRSS_ConfigurationSetter {
 				'min' => 0,
 				'max' => $max_small_int,
 			),
+			'max_registrations' => array(
+				'min' => 0,
+			),
 		);
 
 		foreach ($values as $key => $value) {
@@ -361,8 +364,8 @@ class FreshRSS_ConfigurationSetter {
 
 			$limits = $limits_keys[$key];
 			if (
-				(!isset($limits['min']) || $value > $limits['min']) &&
-				(!isset($limits['max']) || $value < $limits['max'])
+				(!isset($limits['min']) || $value >= $limits['min']) &&
+				(!isset($limits['max']) || $value <= $limits['max'])
 			) {
 				$data['limits'][$key] = $value;
 			}

+ 7 - 0
app/i18n/cz/admin.php

@@ -160,8 +160,15 @@ return array(
 		'create' => 'Vytvořit nového uživatele',
 		'email_persona' => 'Email pro přihlášení<br /><small>(pro <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'language' => 'Jazyk',
+		'number' => 'There is %d account created yet',  // TODO: translate
+		'numbers' => 'There are %d accounts created yet',  // TODO: translate
 		'password_form' => 'Heslo<br /><small>(pro přihlášení webovým formulářem)</small>',
 		'password_format' => 'Alespoň 7 znaků',
+		'registration' => array(
+			'allow' => 'Allow account creation',  // TODO: translate
+			'help' => '0 means that there is no account limit',  // TODO: translate
+			'number' => 'Max number of accounts',  // TODO: translate
+		),
 		'title' => 'Správa uživatelů',
 		'user_list' => 'Seznam uživatelů',
 		'username' => 'Přihlašovací jméno',

+ 4 - 0
app/i18n/cz/conf.php

@@ -72,6 +72,10 @@ return array(
 	),
 	'profile' => array(
 		'_' => 'Správa profilu',
+		'delete' => array(
+			'_' => 'Account deletion',  // TODO: translate
+			'warn' => 'Your account and all the related data will be deleted.',  // TODO: translate
+		),
 		'email_persona' => 'Email pro přihlášení<br /><small>(pro <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'password_api' => 'Password API<br /><small>(tzn. pro mobilní aplikace)</small>',
 		'password_form' => 'Heslo<br /><small>(pro přihlášení webovým formulářem)</small>',

+ 1 - 0
app/i18n/cz/feedback.php

@@ -102,6 +102,7 @@ return array(
 			'_' => 'Uživatel %s byl smazán',
 			'error' => 'Uživatele %s nelze smazat',
 		),
+		'set_registration' => 'The maximum amount of accounts has been updated.',  // TODO: translate
 	),
 	'profile' => array(
 		'error' => 'Váš profil nelze změnit',

+ 15 - 3
app/i18n/cz/gen.php

@@ -21,15 +21,27 @@ return array(
 		'truncate' => 'Smazat všechny články',
 	),
 	'auth' => array(
+		'email' => 'Email',
 		'keep_logged_in' => 'Zapamatovat přihlášení <small>(1 měsíc)</small>',
 		'login' => 'Login',
 		'login_persona' => 'Přihlášení pomocí Persona',
 		'login_persona_problem' => 'Problém s připojením k Persona?',
 		'logout' => 'Odhlášení',
-		'password' => 'Heslo',
+		'password' =>  array(
+			'_' => 'Heslo',
+			'format' => '<small>Alespoň 7 znaků</small>',
+		),
+		'registration' => array(
+			'_' => 'New account',  // TODO: translate
+			'ask' => 'Create an account?',  // TODO: translate
+			'title' => 'Account creation',  // TODO: translate
+		),
 		'reset' => 'Reset přihlášení',
-		'username' => 'Uživatel',
-		'username_admin' => 'Název administrátorského účtu',
+		'username' => array(
+			'_' => 'Uživatel',
+			'admin' => 'Název administrátorského účtu',
+			'format' => '<small>maximálně 16 alfanumerických znaků</small>',
+		),
 		'will_reset' => 'Přihlašovací systém bude vyresetován: místo sytému Persona bude použito přihlášení formulářem.',
 	),
 	'date' => array(

+ 7 - 0
app/i18n/de/admin.php

@@ -160,8 +160,15 @@ return array(
 		'create' => 'Neuen Benutzer erstellen',
 		'email_persona' => 'Anmelde-E-Mail-Adresse<br /><small>(für <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'language' => 'Sprache',
+		'number' => 'There is %d account created yet',  // TODO: translate
+		'numbers' => 'There are %d accounts created yet',  // TODO: translate
 		'password_form' => 'Passwort<br /><small>(für die Anmeldemethode per Webformular)</small>',
 		'password_format' => 'mindestens 7 Zeichen',
+		'registration' => array(
+			'allow' => 'Allow account creation',  // TODO: translate
+			'help' => '0 means that there is no account limit',  // TODO: translate
+			'number' => 'Max number of accounts',  // TODO: translate
+		),
 		'title' => 'Benutzer verwalten',
 		'user_list' => 'Liste der Benutzer',
 		'username' => 'Nutzername',

+ 4 - 0
app/i18n/de/conf.php

@@ -72,6 +72,10 @@ return array(
 	),
 	'profile' => array(
 		'_' => 'Profil-Verwaltung',
+		'delete' => array(
+			'_' => 'Account deletion',  // TODO: translate
+			'warn' => 'Your account and all the related data will be deleted.',  // TODO: translate
+		),
 		'email_persona' => 'Anmelde-E-Mail-Adresse<br /><small>(für <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'password_api' => 'Passwort-API<br /><small>(z. B. für mobile Anwendungen)</small>',
 		'password_form' => 'Passwort<br /><small>(für die Anmeldemethode per Webformular)</small>',

+ 1 - 0
app/i18n/de/feedback.php

@@ -102,6 +102,7 @@ return array(
 			'_' => 'Der Benutzer %s ist gelöscht worden',
 			'error' => 'Der Benutzer %s kann nicht gelöscht werden',
 		),
+		'set_registration' => 'The maximum amount of accounts has been updated.',  // TODO: translate
 	),
 	'profile' => array(
 		'error' => 'Ihr Profil kann nicht geändert werden',

+ 15 - 3
app/i18n/de/gen.php

@@ -21,15 +21,27 @@ return array(
 		'truncate' => 'Alle Artikel löschen',
 	),
 	'auth' => array(
+		'email' => 'E-Mail-Adresse',
 		'keep_logged_in' => 'Eingeloggt bleiben <small>(1 Monat)</small>',
 		'login' => 'Anmelden',
 		'login_persona' => 'Anmelden mit Persona',
 		'login_persona_problem' => 'Verbindungsproblem mit Persona?',
 		'logout' => 'Abmelden',
-		'password' => 'Passwort',
+		'password' => array(
+			'_' => 'Passwort',
+			'format' => '<small>mindestens 7 Zeichen</small>',
+		),
+		'registration' => array(
+			'_' => 'New account',  // TODO: translate
+			'ask' => 'Create an account?',  // TODO: translate
+			'title' => 'Account creation',  // TODO: translate
+		),
 		'reset' => 'Zurücksetzen der Authentifizierung',
-		'username' => 'Nutzername',
-		'username_admin' => 'Administrator-Nutzername',
+		'username' => array(
+			'_' => 'Nutzername',
+			'admin' => 'Administrator-Nutzername',
+			'format' => '<small>maximal 16 alphanumerische Zeichen</small>',
+		),
 		'will_reset' => 'Authentifikationssystem wird zurückgesetzt: ein Formular wird anstelle von Persona benutzt.',
 	),
 	'date' => array(

+ 7 - 0
app/i18n/en/admin.php

@@ -160,8 +160,15 @@ return array(
 		'create' => 'Create new user',
 		'email_persona' => 'Login mail address<br /><small>(for <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'language' => 'Language',
+		'number' => 'There is %d account created yet',
+		'numbers' => 'There are %d accounts created yet',
 		'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',
 		'password_format' => 'At least 7 characters',
+		'registration' => array(
+			'allow' => 'Allow account creation',
+			'help' => '0 means that there is no account limit',
+			'number' => 'Max number of accounts',
+		),
 		'title' => 'Manage users',
 		'user_list' => 'List of users',
 		'username' => 'Username',

+ 4 - 0
app/i18n/en/conf.php

@@ -72,6 +72,10 @@ return array(
 	),
 	'profile' => array(
 		'_' => 'Profile management',
+		'delete' => array(
+			'_' => 'Account deletion',
+			'warn' => 'Your account and all the related data will be deleted.',
+		),
 		'email_persona' => 'Login email address<br /><small>(for <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'password_api' => 'Password API<br /><small>(e.g., for mobile apps)</small>',
 		'password_form' => 'Password<br /><small>(for the Web-form login method)</small>',

+ 1 - 0
app/i18n/en/feedback.php

@@ -102,6 +102,7 @@ return array(
 			'_' => 'User %s has been deleted',
 			'error' => 'User %s cannot be deleted',
 		),
+		'set_registration' => 'The maximum amount of accounts has been updated.',
 	),
 	'profile' => array(
 		'error' => 'Your profile cannot be modified',

+ 15 - 3
app/i18n/en/gen.php

@@ -21,15 +21,27 @@ return array(
 		'truncate' => 'Delete all articles',
 	),
 	'auth' => array(
+		'email' => 'Email address',
 		'keep_logged_in' => 'Keep me logged in <small>(1 month)</small>',
 		'login' => 'Login',
 		'login_persona' => 'Login with Persona',
 		'login_persona_problem' => 'Connection problem with Persona?',
 		'logout' => 'Logout',
-		'password' => 'Password',
+		'password' => array(
+			'_' => 'Password',
+			'format' => '<small>At least 7 characters</small>',
+		),
+		'registration' => array(
+			'_' => 'New account',
+			'ask' => 'Create an account?',
+			'title' => 'Account creation',
+		),
 		'reset' => 'Authentication reset',
-		'username' => 'Username',
-		'username_admin' => 'Administrator username',
+		'username' => array(
+			'_' => 'Username',
+			'admin' => 'Administrator username',
+			'format' => '<small>maximum 16 alphanumeric characters</small>',
+		),
 		'will_reset' => 'Authentication system will be reset: a form will be used instead of Persona.',
 	),
 	'date' => array(

+ 7 - 0
app/i18n/fr/admin.php

@@ -160,8 +160,15 @@ return array(
 		'create' => 'Créer un nouvel utilisateur',
 		'email_persona' => 'Adresse courriel de connexion<br /><small>(pour <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'language' => 'Langue',
+		'number' => '%d compte a déjà été créé',
+		'numbers' => '%d comptes ont déjà été créés',
 		'password_form' => 'Mot de passe<br /><small>(pour connexion par formulaire)</small>',
 		'password_format' => '7 caractères minimum',
+		'registration' => array(
+			'allow' => 'Autoriser la création de comptes',
+			'help' => 'Un chiffre de 0 signifie que l’on peut créer un nombre infini de comptes',
+			'number' => 'Nombre max de comptes',
+		),
 		'title' => 'Gestion des utilisateurs',
 		'user_list' => 'Liste des utilisateurs',
 		'username' => 'Nom d’utilisateur',

+ 4 - 0
app/i18n/fr/conf.php

@@ -72,6 +72,10 @@ return array(
 	),
 	'profile' => array(
 		'_' => 'Gestion du profil',
+		'delete' => array(
+			'_' => 'Suppression du compte',
+			'warn' => 'Le compte et toutes les données associées vont être supprimées.',
+		),
 		'email_persona' => 'Adresse courriel de connexion<br /><small>(pour <a href="https://persona.org/" rel="external">Mozilla Persona</a>)</small>',
 		'password_api' => 'Mot de passe API<br /><small>(ex. : pour applis mobiles)</small>',
 		'password_form' => 'Mot de passe<br /><small>(pour connexion par formulaire)</small>',

+ 1 - 0
app/i18n/fr/feedback.php

@@ -102,6 +102,7 @@ return array(
 			'_' => 'L’utilisateur %s a été supprimé.',
 			'error' => 'L’utilisateur %s ne peut pas être supprimé.',
 		),
+		'set_registration' => 'Le nombre maximal de comptes a été mis à jour.',
 	),
 	'profile' => array(
 		'error' => 'Votre profil n’a pas pu être mis à jour',

+ 15 - 3
app/i18n/fr/gen.php

@@ -21,15 +21,27 @@ return array(
 		'truncate' => 'Supprimer tous les articles',
 	),
 	'auth' => array(
+		'email' => 'Adresse courriel',
 		'keep_logged_in' => 'Rester connecté <small>(1 mois)</small>',
 		'login' => 'Connexion',
 		'login_persona' => 'Connexion avec Persona',
 		'login_persona_problem' => 'Problème de connexion à Persona ?',
 		'logout' => 'Déconnexion',
-		'password' => 'Mot de passe',
+		'password' => array(
+			'_' => 'Mot de passe',
+			'format' => '<small>7 caractères minimum</small>',
+		),
+		'registration' => array(
+			'_' => 'Nouveau compte',
+			'ask' => 'Créer un compte ?',
+			'title' => 'Création de compte',
+		),
 		'reset' => 'Réinitialisation de l’authentification',
-		'username' => 'Nom d’utilisateur',
-		'username_admin' => 'Nom d’utilisateur administrateur',
+		'username' => array(
+			'_' => 'Nom d’utilisateur',
+			'admin' => 'Nom d’utilisateur administrateur',
+			'format' => '<small>16 caractères alphanumériques maximum</small>',
+		),
 		'will_reset' => 'Le système d’authentification va être réinitialisé : un formulaire sera utilisé à la place de Persona.',
 	),
 	'date' => array(

+ 4 - 0
app/views/auth/formLogin.phtml

@@ -1,6 +1,10 @@
 <div class="prompt">
 	<h1><?php echo _t('gen.auth.login'); ?></h1>
 
+	<?php if (!max_registrations_reached()) { ?>
+		<a href="<?php echo _url('auth', 'register'); ?>"><?php echo _t('gen.auth.registration.ask'); ?></a>
+	<?php } ?>
+
 	<form id="crypto-form" method="post" action="<?php echo _url('auth', 'login'); ?>">
 		<div>
 			<label for="username"><?php echo _t('gen.auth.username'); ?></label>

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

@@ -2,6 +2,10 @@
 <div class="prompt">
 	<h1><?php echo _t('gen.auth.login'); ?></h1>
 
+	<?php if (!max_registrations_reached()) { ?>
+		<a href="<?php echo _url('auth', 'register'); ?>"><?php echo _t('gen.auth.registration.ask'); ?></a>
+	<?php } ?>
+
 	<p>
 		<a class="signin btn btn-important" href="<?php echo _url('auth', 'login'); ?>">
 			<?php echo _i('login'); ?> <?php echo _t('gen.auth.login_persona'); ?>

+ 38 - 0
app/views/auth/register.phtml

@@ -0,0 +1,38 @@
+<div class="prompt">
+    <h1><?php echo _t('gen.auth.registration'); ?></h1>
+
+    <form method="post" action="<?php echo _url('user', 'create'); ?>">
+        <div>
+            <label class="group-name" for="new_user_name"><?php echo _t('gen.auth.username'), '<br />', _i('help'), ' ', _t('gen.auth.username.format'); ?></label>
+            <input id="new_user_name" name="new_user_name" type="text" size="16" required="required" maxlength="16" autocomplete="off" pattern="[0-9a-zA-Z]{1,16}" />
+        </div>
+
+        <div>
+            <label class="group-name" for="new_user_passwordPlain"><?php echo _t('gen.auth.password'), '<br />', _i('help'), ' ', _t('gen.auth.password.format'); ?></label>
+            <div class="stick">
+                <input type="password" id="new_user_passwordPlain" name="new_user_passwordPlain" required="required" autocomplete="off" pattern=".{7,}" />
+                <a class="btn toggle-password" data-toggle="new_user_passwordPlain"><?php echo _i('key'); ?></a>
+            </div>
+            <noscript><b><?php echo _t('gen.js.should_be_activated'); ?></b></noscript>
+        </div>
+
+        <div>
+            <label class="group-name" for="new_user_email"><?php echo _t('gen.auth.email'); ?></label>
+            <input type="email" id="new_user_email" name="new_user_email" class="extend" required="required" autocomplete="off" />
+        </div>
+
+        <div>
+            <?php
+                $redirect_url = urlencode(Minz_Url::display(
+                    array('c' => 'index', 'a' => 'index'),
+                    'php', true
+                ));
+            ?>
+            <input type="hidden" name="r" value="<?php echo $redirect_url; ?>" />
+            <button type="submit" class="btn btn-important"><?php echo _t('gen.action.create'); ?></button>
+            <a class="btn" href="<?php echo _url('index', 'index'); ?>"><?php echo _t('gen.action.cancel'); ?></a>
+        </div>
+    </form>
+
+    <p><a href="<?php echo _url('index', 'about'); ?>"><?php echo _t('gen.freshrss.about'); ?></a></p>
+</div>

+ 1 - 1
app/views/auth/reset.phtml

@@ -16,7 +16,7 @@
 		</p>
 
 		<div>
-			<label for="username"><?php echo _t('gen.auth.username_admin'); ?></label>
+			<label for="username"><?php echo _t('gen.auth.username.admin'); ?></label>
 			<input type="text" id="username" name="username" size="16" required="required" maxlength="16" pattern="[0-9a-zA-Z]{1,16}" autofocus="autofocus" />
 		</div>
 		<div>

+ 28 - 0
app/views/user/manage.phtml

@@ -3,6 +3,34 @@
 <div class="post">
 	<a href="<?php echo _url('index', 'index'); ?>"><?php echo _t('gen.action.back_to_rss_feeds'); ?></a>
 
+	<form method="post" action="<?php echo _url('user', 'setRegistration'); ?>">
+		<legend><?php echo _t('admin.user.registration.allow'); ?></legend>
+
+		<div class="form-group">
+			<label class="group-name" for="max-registrations"><?php echo _t('admin.user.registration.number'); ?></label>
+			<div class="group-controls">
+				<input type="number" id="max-registrations" name="max-registrations" value="<?php echo FreshRSS_Context::$system_conf->limits['max_registrations']; ?>" min="0" data-leave-validation="<?php echo FreshRSS_Context::$system_conf->limits['max_registrations']; ?>"/>
+				<?php echo _i('help'); ?> <?php echo _t('admin.user.registration.help'); ?>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<div class="group-controls">
+				<?php
+					$number = count(listUsers());
+					echo _t($number > 1 ? 'admin.user.numbers' : 'admin.user.number', $number);
+				?>
+			</div>
+		</div>
+
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button type="submit" class="btn btn-important"><?php echo _t('gen.action.submit'); ?></button>
+				<button type="reset" class="btn"><?php echo _t('gen.action.cancel'); ?></button>
+			</div>
+		</div>
+	</form>
+
 	<form method="post" action="<?php echo _url('user', 'create'); ?>">
 		<legend><?php echo _t('admin.user.create'); ?></legend>
 

+ 34 - 3
app/views/user/profile.phtml

@@ -18,11 +18,11 @@
 		</div>
 
 		<div class="form-group">
-			<label class="group-name" for="passwordPlain"><?php echo _t('conf.profile.password_form'); ?></label>
+			<label class="group-name" for="newPasswordPlain"><?php echo _t('conf.profile.password_form'); ?></label>
 			<div class="group-controls">
 				<div class="stick">
-					<input type="password" id="passwordPlain" name="passwordPlain" autocomplete="off" pattern=".{7,}" <?php echo cryptAvailable() ? '' : 'disabled="disabled" '; ?>/>
-					<a class="btn toggle-password" data-toggle="passwordPlain"><?php echo _i('key'); ?></a>
+					<input type="password" id="newPasswordPlain" name="newPasswordPlain" autocomplete="off" pattern=".{7,}" <?php echo cryptAvailable() ? '' : 'disabled="disabled" '; ?>/>
+					<a class="btn toggle-password" data-toggle="newPasswordPlain"><?php echo _i('key'); ?></a>
 				</div>
 				<?php echo _i('help'); ?> <?php echo _t('conf.profile.password_format'); ?>
 				<noscript><b><?php echo _t('gen.js.should_be_activated'); ?></b></noscript>
@@ -57,4 +57,35 @@
 			</div>
 		</div>
 	</form>
+
+	<?php if (!FreshRSS_Auth::hasAccess('admin')) { ?>
+	<form id="crypto-form" method="post" action="<?php echo _url('user', 'delete'); ?>">
+		<legend><?php echo _t('conf.profile.delete'); ?></legend>
+
+		<p class="alert alert-warn"><span class="alert-head"><?php echo _t('gen.short.attention'); ?></span> <?php echo _t('conf.profile.delete.warn'); ?></p>
+
+		<div class="form-group">
+			<label class="group-name" for="passwordPlain"><?php echo _t('gen.auth.password'); ?></label>
+			<div class="group-controls">
+					<input type="password" id="passwordPlain" required="required" />
+					<input type="hidden" id="challenge" name="challenge" /><br />
+					<noscript><strong><?php echo _t('gen.js.should_be_activated'); ?></strong></noscript>
+			</div>
+		</div>
+
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<?php
+					$redirect_url = urlencode(Minz_Url::display(
+						array('c' => 'user', 'a' => 'profile'),
+						'php', true
+					));
+				?>
+				<input type="hidden" name="r" value="<?php echo $redirect_url; ?>" />
+				<input type="hidden" name="username" id="username" value="<?php echo Minz_Session::param('currentUser', '_'); ?>" />
+				<button type="submit" class="btn btn-attention confirm"><?php echo _t('gen.action.remove'); ?></button>
+			</div>
+		</div>
+	</form>
+	<?php } ?>
 </div>

+ 4 - 0
data/config.default.php

@@ -77,6 +77,10 @@ return array(
 		# Max number of categories for a user.
 		'max_categories' => 16384,
 
+		# Max number of accounts that anonymous users can create
+		#   0 for an unlimited number of accounts
+		#   1 is to not allow user registrations (1 is corresponding to the admin account)
+		'max_registrations' => 1,
 	),
 
 	# Options used by cURL when making HTTP requests, e.g. when the SimplePie library retrieves feeds.

+ 16 - 0
lib/lib_rss.php

@@ -266,6 +266,22 @@ function listUsers() {
 }
 
 
+/**
+ * Return if the maximum number of registrations has been reached.
+ *
+ * Note a max_regstrations of 0 means there is no limit.
+ *
+ * @return true if number of users >= max registrations, false else.
+ */
+function max_registrations_reached() {
+	$system_conf = Minz_Configuration::get('system');
+	$limit_registrations = $system_conf->limits['max_registrations'];
+	$number_accounts = count(listUsers());
+
+	return $limit_registrations > 0 && $number_accounts >= $limit_registrations;
+}
+
+
 /**
  * Register and return the configuration for a given user.
  *