djmaze 2021-02-24 22:03:14 +01:00
parent 65b508364a
commit 1a82dde49b
30 changed files with 401 additions and 31 deletions

View file

@ -48,14 +48,6 @@ export class AbstractApp {
return true;
}
/**
* @param {string} token
*/
setClientSideToken(token) {
rl.hash.set();
Settings.set('AuthAccountHash', token);
}
logoutReload(close = false) {
const url = logoutLink();

View file

@ -98,10 +98,10 @@ export const Notification = {
AccountTwoFactorAuthRequired: 120,
AccountTwoFactorAuthError: 121,
// CurrentPasswordIncorrect: 131,
// NewPasswordShort: 132,
// NewPasswordWeak: 133,
// NewPasswordForbidden: 134,
CouldNotSaveNewPassword: 130,
CurrentPasswordIncorrect: 131,
NewPasswordShort: 132,
NewPasswordWeak: 133,
ContactsSyncError: 140,

View file

@ -594,6 +594,18 @@ class RemoteUserFetch extends AbstractFetchRemote {
};
}
/**
* @param {?Function} fCallback
* @param {string} prevPassword
* @param {string} newPassword
*/
changePassword(fCallback, prevPassword, newPassword) {
this.defaultRequest(fCallback, 'ChangePassword', {
'PrevPassword': prevPassword,
'NewPassword': newPassword
});
}
/**
* @param {?Function} fCallback
* @param {string} sFolderFullNameRaw

View file

@ -18,6 +18,7 @@ import { TemplatesUserSettings } from 'Settings/User/Templates';
import { FoldersUserSettings } from 'Settings/User/Folders';
import { ThemesUserSettings } from 'Settings/User/Themes';
import { OpenPgpUserSettings } from 'Settings/User/OpenPgp';
import { ChangePasswordUserSettings } from 'Settings/User/ChangePassword';
import { SystemDropDownSettingsUserView } from 'View/User/Settings/SystemDropDown';
import { MenuSettingsUserView } from 'View/User/Settings/Menu';
@ -77,6 +78,15 @@ export class SettingsUserScreen extends AbstractSettingsScreen {
);
}
if (Settings.get('ChangePasswordIsAllowed')) {
settingsAddViewModel(
ChangePasswordUserSettings,
'SettingsChangePassword',
'GLOBAL/PASSWORD',
'change-password'
);
}
if (Settings.capa(Capa.Folders)) {
settingsAddViewModel(FoldersUserSettings, 'SettingsFolders', 'SETTINGS_LABELS/LABEL_FOLDERS_NAME', 'folders');
}

View file

@ -0,0 +1,90 @@
import ko from 'ko';
import { StorageResultType, Notification } from 'Common/Enums';
import { getNotificationFromResponse, i18n } from 'Common/Translator';
import { Settings } from 'Common/Globals';
import Remote from 'Remote/User/Fetch';
import { decorateKoCommands } from 'Knoin/Knoin';
export class ChangePasswordUserSettings {
constructor() {
ko.addObservablesTo(this, {
changeProcess: false,
errorDescription: '',
passwordMismatch: false,
passwordUpdateError: false,
passwordUpdateSuccess: false,
currentPassword: '',
currentPasswordError: false,
newPassword: '',
newPassword2: '',
});
this.currentPassword.subscribe(() => this.resetUpdate(true));
this.newPassword.subscribe(() => this.resetUpdate());
this.newPassword2.subscribe(() => this.resetUpdate());
decorateKoCommands(this, {
saveNewPasswordCommand: self => !self.changeProcess()
&& '' !== self.currentPassword()
&& '' !== self.newPassword()
&& '' !== self.newPassword2()
});
}
saveNewPasswordCommand() {
if (this.newPassword() !== this.newPassword2()) {
this.passwordMismatch(true);
this.errorDescription(i18n('SETTINGS_CHANGE_PASSWORD/ERROR_PASSWORD_MISMATCH'));
} else {
this.reset(true);
Remote.changePassword(this.onChangePasswordResponse.bind(this), this.currentPassword(), this.newPassword());
}
}
reset(change) {
this.changeProcess(change);
this.resetUpdate();
this.currentPasswordError(false);
this.errorDescription('');
}
resetUpdate(current) {
this.passwordUpdateError(false);
this.passwordUpdateSuccess(false);
current ? this.currentPasswordError(false) : this.passwordMismatch(false);
}
onHide() {
this.reset(false);
this.currentPassword('');
this.newPassword('');
this.newPassword2('');
}
onChangePasswordResponse(result, data) {
this.reset(false);
if (StorageResultType.Success === result && data && data.Result) {
this.currentPassword('');
this.newPassword('');
this.newPassword2('');
this.passwordUpdateSuccess(true);
rl.hash.set();
Settings.set('AuthAccountHash', data.Result);
} else {
if (data && Notification.CurrentPasswordIncorrect === data.ErrorCode) {
this.currentPasswordError(true);
}
this.passwordUpdateError(true);
this.errorDescription(getNotificationFromResponse(data, Notification.CouldNotSaveNewPassword));
}
}
}

View file

@ -0,0 +1,36 @@
<?php
class ChangePasswordExampleDriver implements \RainLoop\Providers\ChangePassword\ChangePasswordInterface
{
/**
* @var string
*/
private $sAllowedEmails = '';
/**
* @param string $sAllowedEmails
*
* @return \ChangePasswordExampleDriver
*/
public function SetAllowedEmails($sAllowedEmails)
{
$this->sAllowedEmails = $sAllowedEmails;
return $this;
}
public function isPossible(\RainLoop\Model\Account $oAccount) : bool
{
$sFoundedValue = '';
return $oAccount && $oAccount->Email() &&
\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->sAllowedEmails, $sFoundedValue);
}
public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool
{
$bResult = false;
// TODO
return $bResult;
}
}

View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013 RainLoop Team
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,49 @@
<?php
class ChangePasswordExamplePlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Change Password Example',
VERSION = '2.0',
RELEASE = '2021-03-01',
REQUIRED = '2.3.4',
CATEGORY = 'Security',
DESCRIPTION = 'Plugin that adds functionality to change the email account password';
public function Init() : void
{
$this->addHook('main.fabrica', 'MainFabrica');
}
/**
* @param string $sName
* @param mixed $oProvider
*/
public function MainFabrica($sName, &$oProvider)
{
switch ($sName)
{
case 'change-password':
include_once __DIR__.'/ChangePasswordExampleDriver.php';
$oProvider = new ChangePasswordExampleDriver();
$oProvider->SetAllowedEmails(\strtolower(\trim($this->Config()->Get('plugin', 'allowed_emails', ''))));
break;
}
}
/**
* @return array
*/
public function configMapping() : array
{
return array(
\RainLoop\Plugins\Property::NewInstance('allowed_emails')->SetLabel('Allowed emails')
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT)
->SetDescription('Allowed emails, space as delimiter, wildcard supported. Example: user1@domain1.net user2@domain1.net *@domain2.net')
->SetDefaultValue('*')
);
}
}

View file

@ -12,6 +12,7 @@ class Actions
use Actions\User;
use Actions\Raw;
use Actions\Response;
use Actions\ChangePassword;
const AUTH_TFA_SIGN_ME_TOKEN_KEY = 'rltfasmauth';
const AUTH_SIGN_ME_TOKEN_KEY = 'rlsmauth';
@ -1064,6 +1065,7 @@ class Actions
'StartupUrl' => \trim(\ltrim(\trim($oConfig->Get('labs', 'startup_url', '')), '#/')),
'SieveAllowFileintoInbox' => (bool)$oConfig->Get('labs', 'sieve_allow_fileinto_inbox', false),
'ContactsIsAllowed' => false,
'ChangePasswordIsAllowed' => false,
'RequireTwoFactor' => false,
'Admin' => array(),
'Capa' => array(),
@ -1115,6 +1117,7 @@ class Actions
$aResult['OutLogin'] = $oAccount->OutLogin();
$aResult['AccountHash'] = $oAccount->Hash();
$aResult['AccountSignMe'] = $oAccount->SignMe();
$aResult['ChangePasswordIsAllowed'] = $this->ChangePasswordProvider()->isPossible($oAccount);
$aResult['ContactsIsAllowed'] = $oAddressBookProvider->IsActive();
$aResult['ContactsSyncIsAllowed'] = (bool)$oConfig->Get('contacts', 'allow_sync', false);
$aResult['ContactsSyncInterval'] = (int)$oConfig->Get('contacts', 'sync_interval', 20);

View file

@ -0,0 +1,48 @@
<?php
namespace RainLoop\Actions;
trait ChangePassword
{
/**
* @var \RainLoop\Providers\ChangePassword
*/
private $oChangePasswordProvider;
public function ChangePasswordProvider() : \RainLoop\Providers\ChangePassword
{
if (!$this->oChangePasswordProvider) {
$this->oChangePasswordProvider = new \RainLoop\Providers\ChangePassword(
$this, $this->fabrica('change-password'), !!$this->Config()->Get('labs', 'check_new_password_strength')
);
}
return $this->oChangePasswordProvider;
}
public function DoChangePassword() : array
{
$mResult = false;
if ($oAccount = $this->getAccountFromToken()) {
try
{
$mResult = $this->ChangePasswordProvider()->ChangePassword(
$oAccount,
$this->GetActionParam('PrevPassword', ''),
$this->GetActionParam('NewPassword', '')
);
}
catch (\Throwable $oException)
{
$this->loginErrorDelay();
$this->Logger()->Write('Error: Can\'t change password for '.$oAccount->Email().' account.', \MailSo\Log\Enumerations\Type::NOTICE);
throw $oException;
}
}
return $this->DefaultResponse(__FUNCTION__, $mResult);
}
}

View file

@ -13,10 +13,10 @@ class Notifications
const AccountTwoFactorAuthRequired = 120;
const AccountTwoFactorAuthError = 121;
// const CurrentPasswordIncorrect = 131;
// const NewPasswordShort = 132;
// const NewPasswordWeak = 133;
// const NewPasswordForbidden = 134;
const CouldNotSaveNewPassword = 130;
const CurrentPasswordIncorrect = 131;
const NewPasswordShort = 132;
const NewPasswordWeak = 133;
const ContactsSyncError = 140;

View file

@ -0,0 +1,71 @@
<?php
namespace RainLoop\Providers;
use \RainLoop\Model\Account;
use \RainLoop\Exceptions\ClientException;
use \RainLoop\Notifications;
class ChangePassword extends AbstractProvider
{
/**
* @var \RainLoop\Actions
*/
private $oActions;
/**
* @var \RainLoop\Providers\ChangePassword\ChangePasswordInterface
*/
private $oDriver;
/**
* @var bool
*/
private $bCheckWeak;
public function __construct(\RainLoop\Actions $oActions, ?ChangePassword\ChangePasswordInterface $oDriver = null, bool $bCheckWeak = true)
{
$this->oActions = $oActions;
$this->oDriver = $oDriver;
$this->bCheckWeak = $bCheckWeak;
}
public function isPossible(Account $oAccount) : bool
{
return $this->IsActive() && $this->oDriver->isPossible($oAccount);
}
public function ChangePassword(Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool
{
if (!$this->isPossible($oAccount)) {
throw new ClientException(Notifications::CouldNotSaveNewPassword);
}
if ($sPrevPassword !== $oAccount->Password()) {
throw new ClientException(Notifications::CurrentPasswordIncorrect);
}
$sPasswordForCheck = \trim($sNewPassword);
if (10 > \strlen($sPasswordForCheck)) {
throw new ClientException(Notifications::NewPasswordShort);
}
if ($this->bCheckWeak && !\MailSo\Base\Utils::PasswordWeaknessCheck($sPasswordForCheck)) {
throw new ClientException(Notifications::NewPasswordWeak);
}
if (!$this->oDriver->ChangePassword($oAccount, $sPrevPassword, $sNewPassword)) {
throw new ClientException(Notifications::CouldNotSaveNewPassword);
}
$oAccount->SetPassword($sNewPassword);
$this->oActions->SetAuthToken($oAccount);
return $this->oActions->GetSpecAuthToken();
}
public function IsActive() : bool
{
return $this->oDriver instanceof ChangePassword\ChangePasswordInterface;
}
}

View file

@ -0,0 +1,10 @@
<?php
namespace RainLoop\Providers\ChangePassword;
interface ChangePasswordInterface
{
public function isPossible(\RainLoop\Model\Account $oAccount) : bool;
public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool;
}

View file

@ -118,6 +118,10 @@ class ServiceActions
case 'DoAccountAdd':
$this->Logger()->AddSecret($this->oActions->GetActionParam('Password', ''));
break;
case 'DoChangePassword':
$this->Logger()->AddSecret($this->oActions->GetActionParam('PrevPassword', ''));
$this->Logger()->AddSecret($this->oActions->GetActionParam('NewPassword', ''));
break;
}
$this->Logger()->Write(\MailSo\Base\Utils::Php2js($aPost, $this->Logger()),

View file

@ -174,7 +174,6 @@ en:
CURRENT_PASSWORD_INCORRECT: "Current password incorrect"
NEW_PASSWORD_SHORT: "Password is too short"
NEW_PASSWORD_WEAK: "Password is too easy"
NEW_PASSWORD_FORBIDDEN: "Password contains forbidden characters"
CONTACTS_SYNC_ERROR: "Contacts synchronization error"
CANT_GET_MESSAGE_LIST: "Can't get message list"
CANT_GET_MESSAGE: "Can't get message"

View file

@ -170,7 +170,6 @@ de_DE:
CURRENT_PASSWORD_INCORRECT: "Aktuelles Passwort falsch"
NEW_PASSWORD_SHORT: "Passwort ist zu kurz"
NEW_PASSWORD_WEAK: "Passwort ist zu einfach"
NEW_PASSWORD_FORBIDDEN: "Passwort enthält unzulässige Zeichen"
CONTACTS_SYNC_ERROR: "Fehler bei Kontakte-Synchronisierung"
CANT_GET_MESSAGE_LIST: "Die Nachrichtenliste ist nicht verfügbar"
CANT_GET_MESSAGE: "Diese Nachricht ist nicht verfügbar"

View file

@ -171,7 +171,6 @@ en_US:
CURRENT_PASSWORD_INCORRECT: "Current password incorrect"
NEW_PASSWORD_SHORT: "Password is too short"
NEW_PASSWORD_WEAK: "Password is too easy"
NEW_PASSWORD_FORBIDDEN: "Password contains forbidden characters"
CONTACTS_SYNC_ERROR: "Contacts synchronization error"
CANT_GET_MESSAGE_LIST: "Can't get message list"
CANT_GET_MESSAGE: "Can't get message"

View file

@ -171,7 +171,6 @@ es_ES:
CURRENT_PASSWORD_INCORRECT: "La contraseña actual es incorrecta"
NEW_PASSWORD_SHORT: "La contraseña es muy corta"
NEW_PASSWORD_WEAK: "La contraseña es muy fácil"
NEW_PASSWORD_FORBIDDEN: "La contraseña contiene caracteres prohibidos"
CONTACTS_SYNC_ERROR: "Error de sincronización de Contactos"
CANT_GET_MESSAGE_LIST: "No se puede obtener la lista de mensajes"
CANT_GET_MESSAGE: "No se puede obtener el mensaje"

View file

@ -171,7 +171,6 @@ fr_FR:
CURRENT_PASSWORD_INCORRECT: "Le mot de passe actuel est incorrect"
NEW_PASSWORD_SHORT: "Le mot de passe est trop court"
NEW_PASSWORD_WEAK: "Le mot de passe n'est pas assez fort"
NEW_PASSWORD_FORBIDDEN: "Le mot de passe contient des caractères invalides"
CONTACTS_SYNC_ERROR: "Erreur de synchronisation des contacts"
CANT_GET_MESSAGE_LIST: "Impossible d'obtenir la liste des messages"
CANT_GET_MESSAGE: "Impossible d'obtenir le message"

View file

@ -170,7 +170,6 @@ nl_NL:
CURRENT_PASSWORD_INCORRECT: "Huidig wachtwoord is onjuist"
NEW_PASSWORD_SHORT: "Wachtwoord is te kort"
NEW_PASSWORD_WEAK: "Wachtwoord is te gemakkelijk"
NEW_PASSWORD_FORBIDDEN: "Wachtwoord bevat verboden karakters"
CONTACTS_SYNC_ERROR: "Contactpersonen synchronisatie fout"
CANT_GET_MESSAGE_LIST: "Kan berichtenlijst niet ophalen"
CANT_GET_MESSAGE: "Kan bericht niet ophalen"

View file

@ -171,7 +171,6 @@ zh_CN:
CURRENT_PASSWORD_INCORRECT: "当前密码不正确"
NEW_PASSWORD_SHORT: "密码太短"
NEW_PASSWORD_WEAK: "密码过于简单"
NEW_PASSWORD_FORBIDDEN: "密码包含禁止使用的字符"
CONTACTS_SYNC_ERROR: "联系人同步错误"
CANT_GET_MESSAGE_LIST: "无法获取邮件列表"
CANT_GET_MESSAGE: "无法获取邮件"

View file

@ -515,7 +515,6 @@ en:
CURRENT_PASSWORD_INCORRECT: "Current password incorrect"
NEW_PASSWORD_SHORT: "Password is too short"
NEW_PASSWORD_WEAK: "Password is too easy"
NEW_PASSWORD_FORBIDDEN: "Password contains forbidden characters"
CONTACTS_SYNC_ERROR: "Contacts synchronization error"
CANT_GET_MESSAGE_LIST: "Can't get message list"
CANT_GET_MESSAGE: "Can't get message"

View file

@ -516,7 +516,6 @@ de_DE:
CURRENT_PASSWORD_INCORRECT: "Aktuelles Passwort falsch"
NEW_PASSWORD_SHORT: "Passwort ist zu kurz"
NEW_PASSWORD_WEAK: "Passwort ist zu einfach"
NEW_PASSWORD_FORBIDDEN: "Passwort enthält unzulässige Zeichen"
CONTACTS_SYNC_ERROR: "Fehler bei Kontakte-Synchronisierung"
CANT_GET_MESSAGE_LIST: "Die Nachrichtenliste ist nicht verfügbar"
CANT_GET_MESSAGE: "Diese Nachricht ist nicht verfügbar"

View file

@ -515,7 +515,6 @@ en_GB:
CURRENT_PASSWORD_INCORRECT: "Current password incorrect"
NEW_PASSWORD_SHORT: "Password is too short"
NEW_PASSWORD_WEAK: "Password is too easy"
NEW_PASSWORD_FORBIDDEN: "Password contains forbidden characters"
CONTACTS_SYNC_ERROR: "Contacts synchronization error"
CANT_GET_MESSAGE_LIST: "Can't get message list"
CANT_GET_MESSAGE: "Can't get message"

View file

@ -515,7 +515,6 @@ en_US:
CURRENT_PASSWORD_INCORRECT: "Current password incorrect"
NEW_PASSWORD_SHORT: "Password is too short"
NEW_PASSWORD_WEAK: "Password is too easy"
NEW_PASSWORD_FORBIDDEN: "Password contains forbidden characters"
CONTACTS_SYNC_ERROR: "Contacts synchronization error"
CANT_GET_MESSAGE_LIST: "Can't get message list"
CANT_GET_MESSAGE: "Can't get message"

View file

@ -517,7 +517,6 @@ es_ES:
CURRENT_PASSWORD_INCORRECT: "La contraseña actual es incorrecta"
NEW_PASSWORD_SHORT: "La contraseña es muy corta"
NEW_PASSWORD_WEAK: "La contraseña es muy fácil"
NEW_PASSWORD_FORBIDDEN: "La contraseña contiene caracteres prohibidos"
CONTACTS_SYNC_ERROR: "Error de sincronización de Contactos"
CANT_GET_MESSAGE_LIST: "No se puede obtener la lista de mensajes"
CANT_GET_MESSAGE: "No se puede obtener el mensaje"

View file

@ -516,7 +516,6 @@ fr_FR:
CURRENT_PASSWORD_INCORRECT: "Le mot de passe actuel est incorrect"
NEW_PASSWORD_SHORT: "Le mot de passe est trop court"
NEW_PASSWORD_WEAK: "Le mot de passe n'est pas assez fort"
NEW_PASSWORD_FORBIDDEN: "Le mot de passe contient des caractères invalides"
CONTACTS_SYNC_ERROR: "Erreur de synchronisation des contacts"
CANT_GET_MESSAGE_LIST: "Impossible d'obtenir la liste des messages"
CANT_GET_MESSAGE: "Impossible d'obtenir le message"

View file

@ -515,7 +515,6 @@ nl_NL:
CURRENT_PASSWORD_INCORRECT: "Huidig wachtwoord onjuist"
NEW_PASSWORD_SHORT: "Wachtwoord is te kort"
NEW_PASSWORD_WEAK: "Wachtwoord is te makkelijk"
NEW_PASSWORD_FORBIDDEN: "Wachtwoord bevat verboden tekens"
CONTACTS_SYNC_ERROR: "Contactpersonen synchronisatie fout"
CANT_GET_MESSAGE_LIST: "Berichtenlijst kan niet worden opgehaald"
CANT_GET_MESSAGE: "Bericht kan niet worden opgehaald"

View file

@ -513,7 +513,6 @@ zh_CN:
CURRENT_PASSWORD_INCORRECT: "当前密码不正确"
NEW_PASSWORD_SHORT: "密码太短"
NEW_PASSWORD_WEAK: "密码过于简单"
NEW_PASSWORD_FORBIDDEN: "密码包含禁止使用的字符"
CONTACTS_SYNC_ERROR: "联系人同步错误"
CANT_GET_MESSAGE_LIST: "无法获取邮件列表"
CANT_GET_MESSAGE: "无法获取邮件"

View file

@ -0,0 +1,40 @@
<div class="b-settings-general g-ui-user-select-none">
<div class="form-horizontal long-label">
<div class="legend" data-i18n="SETTINGS_CHANGE_PASSWORD/LEGEND_CHANGE_PASSWORD"></div>
<div class="row">
<div class="span6">
<div class="control-group" data-bind="css: {'error': currentPasswordError}">
<label class="control-label" data-i18n="SETTINGS_CHANGE_PASSWORD/LABEL_CURRENT_PASSWORD"></label>
<div class="controls">
<input type="password" autocomplete="current-password" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="textInput: currentPassword" />
</div>
</div>
<div class="control-group" data-bind="css: {'error': passwordMismatch}">
<label class="control-label" data-i18n="SETTINGS_CHANGE_PASSWORD/LABEL_NEW_PASSWORD"></label>
<div class="controls">
<input type="password" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false"
minlength="10"
data-bind="textInput: newPassword" />
</div>
</div>
<div class="control-group" data-bind="css: {'error': passwordMismatch}">
<label class="control-label" data-i18n="SETTINGS_CHANGE_PASSWORD/LABEL_REPEAT_PASSWORD"></label>
<div class="controls">
<input type="password" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="textInput: newPassword2" />
</div>
</div>
<div class="control-group">
<div class="controls">
<a class="btn" data-bind="command: saveNewPasswordCommand, css: { 'btn-success': passwordUpdateSuccess, 'btn-danger': passwordUpdateError }">
<i class="fontastic" data-bind="css: {'icon-spinner': changeProcess()}">🔑</i>
<span class="i18n" data-i18n="SETTINGS_CHANGE_PASSWORD/BUTTON_UPDATE_PASSWORD"></span>
</a>
</div>
</div>
</div>
<div class="span4 alert alert-error alert-null-left-margin" data-bind="visible: '' !== errorDescription(), text: errorDescription"></div>
</div>
</div>
</div>