* Copyright 2014, Stephan Aßmus <superstippi@gmx.de>.
* Copyright 2019-2025, Andrew Lindesay <apl@lindesay.co.nz>.
* All rights reserved. Distributed under the terms of the MIT License.
*/
#include "UserLoginWindow.h"
#include <algorithm>
#include <ctype.h>
#include <mail_encoding.h>
#include <Alert.h>
#include <AutoLocker.h>
#include <Autolock.h>
#include <Button.h>
#include <Catalog.h>
#include <CheckBox.h>
#include <LayoutBuilder.h>
#include <MenuField.h>
#include <PopUpMenu.h>
#include <TextControl.h>
#include <TranslationUtils.h>
#include "AppUtils.h"
#include "BitmapHolder.h"
#include "BitmapView.h"
#include "Captcha.h"
#include "HaikuDepotConstants.h"
#include "IdentityAndAccessUtils.h"
#include "LanguageMenuUtils.h"
#include "LinkView.h"
#include "LocaleUtils.h"
#include "Logger.h"
#include "Model.h"
#include "ServerHelper.h"
#include "StringUtils.h"
#include "TabView.h"
#include "UserUsageConditions.h"
#include "UserUsageConditionsWindow.h"
#include "ValidationUtils.h"
#include "WebAppInterface.h"
#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "UserLoginWindow"
#define PLACEHOLDER_TEXT B_UTF8_ELLIPSIS
#define KEY_USER_CREDENTIALS "userCredentials"
#define KEY_CAPTCHA_IMAGE "captchaImage"
#define KEY_USER_USAGE_CONDITIONS "userUsageConditions"
#define KEY_PASSWORD_REQUIREMENTS "passwordRequirements"
#define KEY_VALIDATION_FAILURES "validationFailures"
enum ActionTabs {
TAB_LOGIN = 0,
TAB_CREATE_ACCOUNT = 1
};
enum {
MSG_SEND = 'send',
MSG_TAB_SELECTED = 'tbsl',
MSG_CREATE_ACCOUNT_SETUP_SUCCESS = 'cass',
MSG_CREATE_ACCOUNT_SETUP_ERROR = 'case',
MSG_VALIDATE_FIELDS = 'vldt',
MSG_LOGIN_SUCCESS = 'lsuc',
MSG_LOGIN_FAILED = 'lfai',
MSG_LOGIN_ERROR = 'lter',
MSG_CREATE_ACCOUNT_SUCCESS = 'csuc',
MSG_CREATE_ACCOUNT_FAILED = 'cfai',
MSG_CREATE_ACCOUNT_ERROR = 'cfae',
MSG_VIEW_PASSWORD_REQUIREMENTS = 'vpar'
};
loaded in or may later need to be refreshed. This enum controls what
elements of the setup should be performed.
*/
enum CreateAccountSetupMask {
CREATE_CAPTCHA = 1 << 1,
FETCH_USER_USAGE_CONDITIONS = 1 << 2,
FETCH_PASSWORD_REQUIREMENTS = 1 << 3
};
with a pointer to the window structure are provided to the background thread
using this struct.
*/
struct CreateAccountThreadData {
UserLoginWindow* window;
CreateUserDetail* detail;
};
a new user. This structure is passed to the background thread.
*/
struct CreateAccountSetupThreadData {
UserLoginWindow* window;
uint32 mask;
};
system. This structure provides the thread with the necessary data to
perform this work.
*/
struct AuthenticateSetupThreadData {
UserLoginWindow* window;
UserCredentials* credentials;
};
UserLoginWindow::UserLoginWindow(BWindow* parent, BRect frame, Model& model)
:
BWindow(frame, B_TRANSLATE("Log in"), B_FLOATING_WINDOW_LOOK, B_FLOATING_SUBSET_WINDOW_FEEL,
B_ASYNCHRONOUS_CONTROLS | B_AUTO_UPDATE_SIZE_LIMITS | B_NOT_RESIZABLE | B_NOT_ZOOMABLE
| B_CLOSE_ON_ESCAPE),
fPasswordRequirements(NULL),
fUserUsageConditions(NULL),
fCaptcha(NULL),
fPreferredLanguageId(LANGUAGE_DEFAULT_ID),
fModel(model),
fMode(NONE),
fWorkerThread(-1),
fQuitRequestedDuringWorkerThread(false)
{
AddToSubset(parent);
fNicknameField = new BTextControl(B_TRANSLATE("Nickname:"), "", NULL);
fPasswordField = new BTextControl(B_TRANSLATE("Password:"), "", NULL);
fPasswordField->TextView()->HideTyping(true);
for (uint32 i = 0; i <= ' '; i++)
fNicknameField->TextView()->DisallowChar(i);
fNewNicknameField = new BTextControl(B_TRANSLATE("Nickname:"), "", NULL);
fNewPasswordField
= new BTextControl(B_TRANSLATE("Password:"), "", new BMessage(MSG_VALIDATE_FIELDS));
fNewPasswordField->TextView()->HideTyping(true);
fRepeatPasswordField
= new BTextControl(B_TRANSLATE("Repeat password:"), "", new BMessage(MSG_VALIDATE_FIELDS));
fRepeatPasswordField->TextView()->HideTyping(true);
fPreferredLanguageId = fModel.PreferredLanguage()->ID();
BPopUpMenu* languagesMenu = new BPopUpMenu(B_TRANSLATE("Language"));
fLanguageIdField
= new BMenuField("language", B_TRANSLATE("Preferred language:"), languagesMenu);
LanguageMenuUtils::AddLanguagesToMenu(fModel.Languages(), languagesMenu);
languagesMenu->SetTargetForItems(this);
HDINFO("using preferred language code [%s]", fPreferredLanguageId.String());
LanguageMenuUtils::MarkLanguageInMenu(fPreferredLanguageId, languagesMenu);
fEmailField = new BTextControl(B_TRANSLATE("Email address:"), "", NULL);
fCaptchaView = new BitmapView("captcha view");
fCaptchaResultField = new BTextControl("", "", NULL);
fConfirmMinimumAgeCheckBox = new BCheckBox("confirm minimum age", PLACEHOLDER_TEXT,
NULL);
fConfirmMinimumAgeCheckBox->SetEnabled(false);
fConfirmUserUsageConditionsCheckBox = new BCheckBox("confirm usage conditions",
B_TRANSLATE("I agree to the usage conditions"), NULL);
fUserUsageConditionsLink
= new LinkView("usage conditions view", B_TRANSLATE("View the usage conditions"),
new BMessage(MSG_VIEW_LATEST_USER_USAGE_CONDITIONS));
fUserUsageConditionsLink->SetTarget(this);
fPasswordRequirementsLink
= new LinkView("password requirements view", B_TRANSLATE("View the password requirements"),
new BMessage(MSG_VIEW_PASSWORD_REQUIREMENTS));
fPasswordRequirementsLink->SetTarget(this);
fNewNicknameField->SetModificationMessage(new BMessage(MSG_VALIDATE_FIELDS));
fNewPasswordField->SetModificationMessage(new BMessage(MSG_VALIDATE_FIELDS));
fRepeatPasswordField->SetModificationMessage(new BMessage(MSG_VALIDATE_FIELDS));
fEmailField->SetModificationMessage(new BMessage(MSG_VALIDATE_FIELDS));
fCaptchaResultField->SetModificationMessage(new BMessage(MSG_VALIDATE_FIELDS));
fTabView = new TabView(BMessenger(this), BMessage(MSG_TAB_SELECTED));
BGridView* loginCard = new BGridView(B_TRANSLATE("Log in"));
BLayoutBuilder::Grid<>(loginCard)
.AddTextControl(fNicknameField, 0, 0)
.AddTextControl(fPasswordField, 0, 1)
.AddGlue(0, 2)
.SetInsets(B_USE_DEFAULT_SPACING);
fTabView->AddTab(loginCard);
BGridView* createAccountCard = new BGridView(B_TRANSLATE("Create account"));
BLayoutBuilder::Grid<>(createAccountCard)
.AddTextControl(fNewNicknameField, 0, 0)
.AddTextControl(fNewPasswordField, 0, 1)
.Add(fPasswordRequirementsLink, 1, 2)
.AddTextControl(fRepeatPasswordField, 0, 3)
.AddTextControl(fEmailField, 0, 4)
.AddMenuField(fLanguageIdField, 0, 5)
.Add(fCaptchaView, 0, 6)
.Add(fCaptchaResultField, 1, 6)
.Add(fConfirmMinimumAgeCheckBox, 1, 7)
.Add(fConfirmUserUsageConditionsCheckBox, 1, 8)
.Add(fUserUsageConditionsLink, 1, 9)
.SetInsets(B_USE_DEFAULT_SPACING);
fTabView->AddTab(createAccountCard);
fSendButton = new BButton("send", B_TRANSLATE("Log in"), new BMessage(MSG_SEND));
fCancelButton = new BButton("cancel", B_TRANSLATE("Cancel"), new BMessage(B_QUIT_REQUESTED));
BLayoutBuilder::Group<>(this, B_VERTICAL)
.Add(fTabView)
.AddGroup(B_HORIZONTAL)
.AddGlue()
.Add(fCancelButton)
.Add(fSendButton)
.End()
.SetInsets(B_USE_WINDOW_INSETS)
;
SetDefaultButton(fSendButton);
_SetMode(LOGIN);
CenterIn(parent->Frame());
}
UserLoginWindow::~UserLoginWindow()
{
BAutolock locker(&fLock);
if (fWorkerThread >= 0)
wait_for_thread(fWorkerThread, NULL);
}
void
UserLoginWindow::MessageReceived(BMessage* message)
{
switch (message->what) {
case MSG_VALIDATE_FIELDS:
_MarkCreateUserInvalidFields();
break;
case MSG_VIEW_LATEST_USER_USAGE_CONDITIONS:
_ViewUserUsageConditions();
break;
case MSG_VIEW_PASSWORD_REQUIREMENTS:
_ViewPasswordRequirements();
break;
case MSG_SEND:
switch (fMode) {
case LOGIN:
_Authenticate();
break;
case CREATE_ACCOUNT:
_CreateAccount();
break;
default:
break;
}
break;
case MSG_TAB_SELECTED:
{
int32 tabIndex;
if (message->FindInt32("tab index", &tabIndex) == B_OK) {
switch (tabIndex) {
case TAB_LOGIN:
_SetMode(LOGIN);
break;
case TAB_CREATE_ACCOUNT:
_SetMode(CREATE_ACCOUNT);
break;
default:
break;
}
}
break;
}
case MSG_CREATE_ACCOUNT_SETUP_ERROR:
HDERROR("failed to setup for account setup - window must quit");
BMessenger(this).SendMessage(B_QUIT_REQUESTED);
break;
case MSG_CREATE_ACCOUNT_SETUP_SUCCESS:
_HandleCreateAccountSetupSuccess(message);
break;
case MSG_LANGUAGE_SELECTED:
message->FindString("id", &fPreferredLanguageId);
break;
case MSG_LOGIN_ERROR:
_HandleAuthenticationError();
break;
case MSG_LOGIN_FAILED:
_HandleAuthenticationFailed();
break;
case MSG_LOGIN_SUCCESS:
{
BMessage credentialsMessage;
if (message->FindMessage(KEY_USER_CREDENTIALS, &credentialsMessage) != B_OK)
debugger("expected key in internal message not found");
_HandleAuthenticationSuccess(UserCredentials(&credentialsMessage));
break;
}
case MSG_CREATE_ACCOUNT_SUCCESS:
{
BMessage credentialsMessage;
if (message->FindMessage(KEY_USER_CREDENTIALS, &credentialsMessage) != B_OK)
debugger("expected key in internal message not found");
_HandleCreateAccountSuccess(UserCredentials(&credentialsMessage));
break;
}
case MSG_CREATE_ACCOUNT_FAILED:
{
BMessage validationFailuresMessage;
if (message->FindMessage(KEY_VALIDATION_FAILURES, &validationFailuresMessage) != B_OK)
debugger("expected key in internal message not found");
ValidationFailures validationFailures(&validationFailuresMessage);
_HandleCreateAccountFailure(validationFailures);
break;
}
case MSG_CREATE_ACCOUNT_ERROR:
_HandleCreateAccountError();
break;
default:
BWindow::MessageReceived(message);
break;
}
}
bool
UserLoginWindow::QuitRequested()
{
BAutolock locker(&fLock);
if (fWorkerThread >= 0) {
HDDEBUG("quit requested while worker thread is operating -- will "
"try again once the worker thread has completed");
fQuitRequestedDuringWorkerThread = true;
return false;
}
return true;
}
void
UserLoginWindow::SetOnSuccessMessage(const BMessenger& messenger, const BMessage& message)
{
fOnSuccessTarget = messenger;
fOnSuccessMessage = message;
}
void
UserLoginWindow::_EnableMutableControls(bool enabled)
{
fNicknameField->SetEnabled(enabled);
fPasswordField->SetEnabled(enabled);
fNewNicknameField->SetEnabled(enabled);
fNewPasswordField->SetEnabled(enabled);
fRepeatPasswordField->SetEnabled(enabled);
fEmailField->SetEnabled(enabled);
fLanguageIdField->SetEnabled(enabled);
fCaptchaResultField->SetEnabled(enabled);
fConfirmMinimumAgeCheckBox->SetEnabled(enabled);
fConfirmUserUsageConditionsCheckBox->SetEnabled(enabled);
fUserUsageConditionsLink->SetEnabled(enabled);
fPasswordRequirementsLink->SetEnabled(enabled);
fSendButton->SetEnabled(enabled);
}
void
UserLoginWindow::_SetMode(Mode mode)
{
if (fMode == mode)
return;
fMode = mode;
switch (fMode) {
case LOGIN:
fTabView->Select(TAB_LOGIN);
fSendButton->SetLabel(B_TRANSLATE("Log in"));
fNicknameField->MakeFocus();
break;
case CREATE_ACCOUNT:
fTabView->Select(TAB_CREATE_ACCOUNT);
fSendButton->SetLabel(B_TRANSLATE("Create account"));
_CreateAccountSetupIfNecessary();
fNewNicknameField->MakeFocus();
_MarkCreateUserInvalidFields();
break;
default:
break;
}
}
void
UserLoginWindow::_SetWorkerThreadLocked(thread_id thread)
{
BAutolock locker(&fLock);
_SetWorkerThread(thread);
}
void
UserLoginWindow::_SetWorkerThread(thread_id thread)
{
if (thread >= 0) {
fWorkerThread = thread;
resume_thread(fWorkerThread);
} else {
fWorkerThread = -1;
if (fQuitRequestedDuringWorkerThread)
BMessenger(this).SendMessage(B_QUIT_REQUESTED);
fQuitRequestedDuringWorkerThread = false;
}
}
void
UserLoginWindow::_Authenticate()
{
BString username = fNicknameField->Text();
StringUtils::InSituStripSpaceAndControl(username);
_Authenticate(UserCredentials(username, fPasswordField->Text()));
}
void
UserLoginWindow::_Authenticate(const UserCredentials& credentials)
{
BAutolock locker(&fLock);
if (fWorkerThread >= 0)
return;
_EnableMutableControls(false);
AuthenticateSetupThreadData* threadData = new AuthenticateSetupThreadData();
threadData->window = this;
threadData->credentials = new UserCredentials(credentials);
thread_id thread
= spawn_thread(&_AuthenticateThreadEntry, "Authentication", B_NORMAL_PRIORITY, threadData);
if (thread >= 0)
_SetWorkerThread(thread);
}
int32
UserLoginWindow::_AuthenticateThreadEntry(void* data)
{
AuthenticateSetupThreadData* threadData = static_cast<AuthenticateSetupThreadData*>(data);
threadData->window->_AuthenticateThread(*(threadData->credentials));
threadData->window->_SetWorkerThreadLocked(-1);
delete threadData->credentials;
delete threadData;
return 0;
}
void
UserLoginWindow::_AuthenticateThread(UserCredentials& userCredentials)
{
BMessage responsePayload;
WebAppInterfaceRef interface = fModel.WebApp();
status_t status = interface->AuthenticateUser(userCredentials.Nickname(),
userCredentials.PasswordClear(), responsePayload);
BString token;
if (status == B_OK) {
int32 errorCode = WebAppInterface::ErrorCodeFromResponse(responsePayload);
if (errorCode == ERROR_CODE_NONE) {
_UnpackAuthenticationToken(responsePayload, token);
} else {
ServerHelper::NotifyServerJsonRpcError(responsePayload);
BMessenger(this).SendMessage(MSG_LOGIN_ERROR);
return;
}
}
if (status == B_OK) {
userCredentials.SetIsSuccessful(!token.IsEmpty());
if (Logger::IsDebugEnabled()) {
if (token.IsEmpty())
HDINFO("authentication failed");
else
HDINFO("authentication successful");
}
BMessenger messenger(this);
if (userCredentials.IsSuccessful()) {
BMessage message(MSG_LOGIN_SUCCESS);
BMessage credentialsMessage;
status = userCredentials.Archive(&credentialsMessage);
if (status == B_OK)
status = message.AddMessage(KEY_USER_CREDENTIALS, &credentialsMessage);
if (status == B_OK)
messenger.SendMessage(&message);
} else {
BMessage message(MSG_LOGIN_FAILED);
messenger.SendMessage(&message);
}
} else {
ServerHelper::NotifyTransportError(status);
BMessenger(this).SendMessage(MSG_LOGIN_ERROR);
}
}
void
UserLoginWindow::_UnpackAuthenticationToken(BMessage& responsePayload, BString& token)
{
BMessage resultPayload;
if (responsePayload.FindMessage("result", &resultPayload) == B_OK) {
resultPayload.FindString("token", &token);
}
}
like a network error. Because of the large number of possible errors, the
reporting of the error is handled separately from this method. This method
only needs to take responsibility for returning the GUI and state of the
window to a situation where the user can try again.
*/
void
UserLoginWindow::_HandleAuthenticationError()
{
_EnableMutableControls(true);
}
void
UserLoginWindow::_HandleAuthenticationFailed()
{
AppUtils::NotifySimpleError(B_TRANSLATE("Authentication failed"),
B_TRANSLATE("The user does not exist or the wrong password was"
" supplied. Check your credentials and try again."));
fPasswordField->SetText("");
_EnableMutableControls(true);
}
HaikuDepotServer system; this handles the take-up of the data and closing
the window etc...
*/
void
UserLoginWindow::_HandleAuthenticationSuccess(const UserCredentials& credentials)
{
BString message = B_TRANSLATE("You have successfully authenticated as user "
"%Nickname%.");
message.ReplaceAll("%Nickname%", credentials.Nickname());
BAlert* alert = new(std::nothrow) BAlert(B_TRANSLATE("Success"), message, B_TRANSLATE("Close"));
if (alert != NULL)
alert->Go();
_TakeUpCredentialsAndQuit(credentials);
}
authentication details (credentials) into the system so that further API
calls etc... will be from this user and will quit the window.
*/
void
UserLoginWindow::_TakeUpCredentialsAndQuit(const UserCredentials& credentials)
{
status_t result = IdentityAndAccessUtils::StoreCredentials(credentials);
if (result != B_OK)
HDERROR("unable to store the credentials, but they can be used in-memory.");
fModel.SetCredentials(credentials);
BMessenger onSuccessTarget(fOnSuccessTarget);
BMessage onSuccessMessage(fOnSuccessMessage);
BMessenger(this).SendMessage(B_QUIT_REQUESTED);
if (onSuccessTarget.IsValid() && onSuccessMessage.what != 0)
onSuccessTarget.SendMessage(&onSuccessMessage);
}
that is necessary for setting up an account. It will only gather that data
that it does not already have to avoid extra work.
*/
void
UserLoginWindow::_CreateAccountSetupIfNecessary()
{
uint32 setupMask = 0;
if (fCaptcha == NULL)
setupMask |= CREATE_CAPTCHA;
if (fUserUsageConditions == NULL)
setupMask |= FETCH_USER_USAGE_CONDITIONS;
if (fPasswordRequirements == NULL)
setupMask |= FETCH_PASSWORD_REQUIREMENTS;
_CreateAccountSetup(setupMask);
}
\param mask describes what data is required to be fetched.
*/
void
UserLoginWindow::_CreateAccountSetup(uint32 mask)
{
if (mask == 0)
return;
BAutolock locker(&fLock);
if (fWorkerThread >= 0)
return;
if (!Lock())
debugger("unable to lock the user login window");
_EnableMutableControls(false);
if ((mask & CREATE_CAPTCHA) != 0)
_SetCaptcha(NULL);
if ((mask & FETCH_USER_USAGE_CONDITIONS) != 0)
_SetUserUsageConditions(NULL);
if ((mask & FETCH_PASSWORD_REQUIREMENTS) != 0)
_SetPasswordRequirements(NULL);
Unlock();
CreateAccountSetupThreadData* threadData = new CreateAccountSetupThreadData;
threadData->window = this;
threadData->mask = mask;
thread_id thread = spawn_thread(&_CreateAccountSetupThreadEntry, "Create account setup",
B_NORMAL_PRIORITY, threadData);
if (thread >= 0) {
_SetWorkerThreadLocked(thread);
} else {
debugger("unable to start a thread to gather data for creating an "
"account");
}
}
int32
UserLoginWindow::_CreateAccountSetupThreadEntry(void* data)
{
CreateAccountSetupThreadData* threadData = static_cast<CreateAccountSetupThreadData*>(data);
BMessenger messenger(threadData->window);
status_t result = B_OK;
Captcha captcha;
UserUsageConditions userUsageConditions;
PasswordRequirements passwordRequirements;
bool shouldCreateCaptcha = (threadData->mask & CREATE_CAPTCHA) != 0;
bool shouldFetchUserUsageConditions = (threadData->mask & FETCH_USER_USAGE_CONDITIONS) != 0;
bool shouldFetchPasswordRequirements = (threadData->mask & FETCH_PASSWORD_REQUIREMENTS) != 0;
if (result == B_OK && shouldCreateCaptcha)
result = threadData->window->_CreateAccountCaptchaSetupThread(captcha);
if (result == B_OK && shouldFetchUserUsageConditions) {
result
= threadData->window->_CreateAccountUserUsageConditionsSetupThread(userUsageConditions);
}
if (result == B_OK && shouldFetchPasswordRequirements) {
result = threadData->window->_CreateAccountPasswordRequirementsSetupThread(
passwordRequirements);
HDINFO("password requirements fetched; len %" B_PRId32 ", caps %" B_PRId32
", digits %" B_PRId32,
passwordRequirements.MinPasswordLength(),
passwordRequirements.MinPasswordUppercaseChar(),
passwordRequirements.MinPasswordUppercaseChar());
}
if (result == B_OK) {
BMessage message(MSG_CREATE_ACCOUNT_SETUP_SUCCESS);
if (result == B_OK && shouldCreateCaptcha) {
BMessage captchaMessage;
result = captcha.Archive(&captchaMessage);
if (result == B_OK)
result = message.AddMessage(KEY_CAPTCHA_IMAGE, &captchaMessage);
}
if (result == B_OK && shouldFetchUserUsageConditions) {
BMessage userUsageConditionsMessage;
result = userUsageConditions.Archive(&userUsageConditionsMessage);
if (result == B_OK)
result = message.AddMessage(KEY_USER_USAGE_CONDITIONS, &userUsageConditionsMessage);
}
if (result == B_OK && shouldFetchPasswordRequirements) {
BMessage passwordRequirementsMessage;
result = passwordRequirements.Archive(&passwordRequirementsMessage);
if (result == B_OK) {
result
= message.AddMessage(KEY_PASSWORD_REQUIREMENTS, &passwordRequirementsMessage);
}
}
if (result == B_OK) {
HDDEBUG("successfully completed collection of create account "
"data from the server in background thread");
messenger.SendMessage(&message);
} else {
debugger("unable to configure the "
"'MSG_CREATE_ACCOUNT_SETUP_SUCCESS' message.");
}
}
if (result != B_OK) {
messenger.SendMessage(MSG_CREATE_ACCOUNT_SETUP_ERROR);
}
threadData->window->_SetWorkerThreadLocked(-1);
delete threadData;
return 0;
}
status_t
UserLoginWindow::_CreateAccountUserUsageConditionsSetupThread(
UserUsageConditions& userUsageConditions)
{
WebAppInterfaceRef interface = fModel.WebApp();
status_t result = interface->RetrieveUserUsageConditions(NULL, userUsageConditions);
if (result != B_OK) {
AppUtils::NotifySimpleError(B_TRANSLATE("Usage conditions download problem"),
B_TRANSLATE("An error has arisen downloading the usage "
"conditions required to create a new user. Check the log for "
"details and try again. " ALERT_MSG_LOGS_USER_GUIDE));
}
return result;
}
status_t
UserLoginWindow::_CreateAccountPasswordRequirementsSetupThread(
PasswordRequirements& passwordRequirements)
{
WebAppInterfaceRef interface = fModel.WebApp();
status_t result = interface->RetrievePasswordRequirements(passwordRequirements);
if (result != B_OK) {
AppUtils::NotifySimpleError(B_TRANSLATE("Password requirements download problem"),
B_TRANSLATE("An error has arisen downloading the password "
"requirements required to create a new user. Check the log for "
"details and try again. " ALERT_MSG_LOGS_USER_GUIDE));
}
return result;
}
status_t
UserLoginWindow::_CreateAccountCaptchaSetupThread(Captcha& captcha)
{
WebAppInterfaceRef interface = fModel.WebApp();
BMessage responsePayload;
status_t status = interface->RequestCaptcha(responsePayload);
if (status != B_OK) {
AppUtils::NotifySimpleError(B_TRANSLATE("Captcha error"),
B_TRANSLATE("It was not possible to communicate with the server to "
"obtain a captcha image required to create a new user."));
}
if (status == B_OK) {
if (WebAppInterface::ErrorCodeFromResponse(responsePayload) != ERROR_CODE_NONE) {
ServerHelper::AlertTransportError(&responsePayload);
status = B_ERROR;
}
}
if (status == B_OK) {
status = _UnpackCaptcha(responsePayload, captcha);
if (status != B_OK) {
AppUtils::NotifySimpleError(B_TRANSLATE("Captcha error"),
B_TRANSLATE("It was not possible to extract necessary captcha "
"information from the data sent back from the server."));
}
}
return status;
}
server and extracts from it the captcha image.
*/
status_t
UserLoginWindow::_UnpackCaptcha(BMessage& responsePayload, Captcha& captcha)
{
status_t result = B_OK;
BMessage resultMessage;
if (result == B_OK)
result = responsePayload.FindMessage("result", &resultMessage);
BString token;
if (result == B_OK)
result = resultMessage.FindString("token", &token);
BString pngImageDataBase64;
if (result == B_OK)
result = resultMessage.FindString("pngImageDataBase64", &pngImageDataBase64);
ssize_t encodedSize = 0;
ssize_t decodedSize = 0;
if (result == B_OK) {
encodedSize = pngImageDataBase64.Length();
decodedSize = (encodedSize * 3 + 3) / 4;
if (decodedSize <= 0)
result = B_ERROR;
} else {
HDERROR("obtained a captcha with no image data");
}
char* buffer = NULL;
if (result == B_OK) {
buffer = new char[decodedSize];
decodedSize = decode_base64(buffer, pngImageDataBase64.String(), encodedSize);
if (decodedSize <= 0)
result = B_ERROR;
if (result == B_OK) {
captcha.SetToken(token);
captcha.SetPngImageData(buffer, decodedSize);
}
delete[] buffer;
HDDEBUG("did obtain a captcha image of size %" B_PRIuSIZE " bytes", decodedSize);
}
return result;
}
void
UserLoginWindow::_HandleCreateAccountSetupSuccess(BMessage* message)
{
HDDEBUG("handling account setup success");
BMessage captchaMessage;
BMessage userUsageConditionsMessage;
BMessage passwordRequirementsMessage;
if (message->FindMessage(KEY_CAPTCHA_IMAGE, &captchaMessage) == B_OK)
_SetCaptcha(new Captcha(&captchaMessage));
if (message->FindMessage(KEY_USER_USAGE_CONDITIONS, &userUsageConditionsMessage) == B_OK)
_SetUserUsageConditions(new UserUsageConditions(&userUsageConditionsMessage));
if (message->FindMessage(KEY_PASSWORD_REQUIREMENTS, &passwordRequirementsMessage) == B_OK)
_SetPasswordRequirements(new PasswordRequirements(&passwordRequirementsMessage));
_EnableMutableControls(true);
}
void
UserLoginWindow::_SetCaptcha(Captcha* captcha)
{
HDDEBUG("setting captcha");
if (fCaptcha != NULL)
delete fCaptcha;
fCaptcha = captcha;
if (fCaptcha == NULL) {
fCaptchaView->UnsetBitmap();
} else {
BBitmap* bitmap = BTranslationUtils::GetBitmap(fCaptcha->PngImageData());
if (bitmap == NULL) {
HDERROR("unable to read the captcha bitmap as an image");
fCaptchaView->UnsetBitmap();
} else {
BitmapHolderRef bitmapHolderRef
= BitmapHolderRef(new(std::nothrow) BitmapHolder(bitmap), true);
fCaptchaView->SetBitmap(bitmapHolderRef);
}
}
fCaptchaResultField->SetText("");
}
server. At this point some of the UI elements may need to be updated.
*/
void
UserLoginWindow::_SetUserUsageConditions(UserUsageConditions* userUsageConditions)
{
HDDEBUG("setting user usage conditions");
if (fUserUsageConditions != NULL)
delete fUserUsageConditions;
fUserUsageConditions = userUsageConditions;
if (fUserUsageConditions != NULL) {
fConfirmMinimumAgeCheckBox->SetLabel(
LocaleUtils::CreateTranslatedIAmMinimumAgeSlug(fUserUsageConditions->MinimumAge()));
} else {
fConfirmMinimumAgeCheckBox->SetLabel(PLACEHOLDER_TEXT);
fConfirmMinimumAgeCheckBox->SetValue(0);
fConfirmUserUsageConditionsCheckBox->SetValue(0);
}
}
void
UserLoginWindow::_SetPasswordRequirements(PasswordRequirements* passwordRequirements)
{
HDDEBUG("setting password requirements");
if (fPasswordRequirements != NULL)
delete fPasswordRequirements;
fPasswordRequirements = passwordRequirements;
if (fPasswordRequirements != NULL) {
HDDEBUG("password requirements set to; len %" B_PRId32 ", caps %" B_PRId32
", digits %" B_PRId32,
fPasswordRequirements->MinPasswordLength(),
fPasswordRequirements->MinPasswordUppercaseChar(),
fPasswordRequirements->MinPasswordUppercaseChar());
}
}
void
UserLoginWindow::_CreateAccount()
{
BAutolock locker(&fLock);
if (fCaptcha == NULL)
debugger("missing captcha when assembling create user details");
if (fUserUsageConditions == NULL)
debugger("missing user usage conditions when assembling create user details");
if (fWorkerThread >= 0)
return;
CreateUserDetail* detail = new CreateUserDetail();
ValidationFailures validationFailures;
_AssembleCreateUserDetail(*detail);
_ValidateCreateUserDetail(*detail, validationFailures);
_MarkCreateUserInvalidFields(validationFailures);
_AlertCreateUserValidationFailure(validationFailures);
if (validationFailures.IsEmpty()) {
CreateAccountThreadData* data = new CreateAccountThreadData();
data->window = this;
data->detail = detail;
thread_id thread
= spawn_thread(&_CreateAccountThreadEntry, "Account creator", B_NORMAL_PRIORITY, data);
if (thread >= 0)
_SetWorkerThread(thread);
}
}
used as the input for the validation and communication with the backend
application server (HDS).
*/
void
UserLoginWindow::_AssembleCreateUserDetail(CreateUserDetail& detail)
{
detail.SetNickname(fNewNicknameField->Text());
detail.SetPasswordClear(fNewPasswordField->Text());
detail.SetIsPasswordRepeated(strlen(fRepeatPasswordField->Text()) > 0
&& strcmp(fNewPasswordField->Text(), fRepeatPasswordField->Text()) == 0);
detail.SetEmail(fEmailField->Text());
if (fCaptcha != NULL)
detail.SetCaptchaToken(fCaptcha->Token());
detail.SetCaptchaResponse(fCaptchaResultField->Text());
detail.SetLanguageId(fPreferredLanguageId);
if (fUserUsageConditions != NULL && fConfirmMinimumAgeCheckBox->Value() == 1
&& fConfirmUserUsageConditionsCheckBox->Value() == 1) {
detail.SetAgreedToUserUsageConditionsCode(fUserUsageConditions->Code());
}
}
validation or data problems into the supplied ValidationFailures object.
*/
void
UserLoginWindow::_ValidateCreateUserDetail(CreateUserDetail& detail, ValidationFailures& failures)
{
if (!ValidationUtils::IsValidEmail(detail.Email()))
failures.AddFailure("email", "malformed");
if (detail.Nickname().IsEmpty()) {
failures.AddFailure("nickname", "required");
} else {
if (!ValidationUtils::IsValidNickname(detail.Nickname()))
failures.AddFailure("nickname", "malformed");
}
if (detail.PasswordClear().IsEmpty()) {
failures.AddFailure("passwordClear", "required");
} else {
if (!ValidationUtils::IsValidPasswordClear(detail.PasswordClear()))
failures.AddFailure("passwordClear", "invalid");
}
if (!detail.IsPasswordRepeated())
failures.AddFailure("repeatPasswordClear", "repeat");
if (detail.AgreedToUserUsageConditionsCode().IsEmpty())
failures.AddFailure("agreedToUserUsageConditionsCode", "required");
if (detail.CaptchaResponse().IsEmpty())
failures.AddFailure("captchaResponse", "required");
}
void
UserLoginWindow::_MarkCreateUserInvalidFields()
{
CreateUserDetail detail;
ValidationFailures failures;
_AssembleCreateUserDetail(detail);
_ValidateCreateUserDetail(detail, failures);
_MarkCreateUserInvalidFields(failures);
}
void
UserLoginWindow::_MarkCreateUserInvalidFields(const ValidationFailures& failures)
{
fNewNicknameField->MarkAsInvalid(failures.Contains("nickname"));
fNewPasswordField->MarkAsInvalid(failures.Contains("passwordClear"));
fRepeatPasswordField->MarkAsInvalid(failures.Contains("repeatPasswordClear"));
fEmailField->MarkAsInvalid(failures.Contains("email"));
fCaptchaResultField->MarkAsInvalid(failures.Contains("captchaResponse"));
}
void
UserLoginWindow::_AlertCreateUserValidationFailure(const ValidationFailures& failures)
{
if (!failures.IsEmpty()) {
BString alertMessage = B_TRANSLATE("There are problems in the supplied "
"data:");
alertMessage << "\n\n";
for (int32 i = 0; i < failures.CountFailures(); i++) {
ValidationFailure* failure = failures.FailureAtIndex(i);
BStringList messages = failure->Messages();
for (int32 j = 0; j < messages.CountStrings(); j++) {
alertMessage << _CreateAlertTextFromValidationFailure(failure->Property(),
messages.StringAt(j));
alertMessage << '\n';
}
}
BAlert* alert = new(std::nothrow) BAlert(B_TRANSLATE("Input validation"), alertMessage,
B_TRANSLATE("OK"), NULL, NULL, B_WIDTH_AS_USUAL, B_WARNING_ALERT);
if (alert != NULL)
alert->Go();
}
}
*/
void
UserLoginWindow::_ValidationFailuresToString(const ValidationFailures& failures, BString& output)
{
for (int32 i = 0; i < failures.CountFailures(); i++) {
ValidationFailure* failure = failures.FailureAtIndex(i);
BStringList messages = failure->Messages();
for (int32 j = 0; j < messages.CountStrings(); j++) {
if (0 != j || 0 != i)
output << ", ";
output << failure->Property();
output << ":";
output << messages.StringAt(j);
}
}
}
BString
UserLoginWindow::_CreateAlertTextFromValidationFailure(const BString& property,
const BString& message)
{
if (property == "email" && message == "malformed")
return B_TRANSLATE("The email is malformed.");
if (property == "nickname" && message == "notunique") {
return B_TRANSLATE("The nickname must be unique, but the supplied "
"nickname is already taken. Choose a different nickname.");
}
if (property == "nickname" && message == "required")
return B_TRANSLATE("The nickname is required.");
if (property == "nickname" && message == "malformed") {
return B_TRANSLATE("The nickname is malformed. The nickname may only "
"contain digits and lower case latin characters. The nickname "
"must be between four and sixteen characters in length.");
}
if (property == "passwordClear" && message == "required")
return B_TRANSLATE("A password is required.");
if (property == "passwordClear" && message == "invalid") {
return B_TRANSLATE("The password must be at least eight characters "
"long, consist of at least two digits and one upper case "
"character.");
}
if (property == "passwordClearRepeated" && message == "required") {
return B_TRANSLATE("The password must be repeated in order to reduce "
"the chance of entering the password incorrectly.");
}
if (property == "passwordClearRepeated" && message == "repeat")
return B_TRANSLATE("The password has been incorrectly repeated.");
if (property == "agreedToUserUsageConditionsCode" && message == "required") {
return B_TRANSLATE("The usage agreement must be agreed to and a "
"confirmation should be made that the person creating the user "
"meets the minimum age requirement.");
}
if (property == "captchaResponse" && message == "required") {
return B_TRANSLATE("A response to the captcha question must be "
"provided.");
}
if (property == "captchaResponse" && message == "captchabadresponse") {
return B_TRANSLATE("The supplied response to the captcha is "
"incorrect. A new captcha will be generated; try again.");
}
BString result = B_TRANSLATE("An unexpected error '%Message%' has arisen "
"with property '%Property%'");
result.ReplaceAll("%Message%", message);
result.ReplaceAll("%Property%", property);
return result;
}
the new account.
*/
int32
UserLoginWindow::_CreateAccountThreadEntry(void* data)
{
CreateAccountThreadData* threadData = static_cast<CreateAccountThreadData*>(data);
threadData->window->_CreateAccountThread(threadData->detail);
threadData->window->_SetWorkerThreadLocked(-1);
if (NULL != threadData->detail)
delete threadData->detail;
return 0;
}
to the application server to actually create the user.
*/
void
UserLoginWindow::_CreateAccountThread(CreateUserDetail* detail)
{
WebAppInterfaceRef interface = fModel.WebApp();
BMessage responsePayload;
BMessenger messenger(this);
status_t status = interface->CreateUser(detail->Nickname(), detail->PasswordClear(),
detail->Email(), detail->CaptchaToken(), detail->CaptchaResponse(), detail->LanguageId(),
detail->AgreedToUserUsageConditionsCode(), responsePayload);
BString error = B_TRANSLATE("There was a puzzling response from the web service.");
if (status == B_OK) {
int32 errorCode = WebAppInterface::ErrorCodeFromResponse(responsePayload);
switch (errorCode) {
case ERROR_CODE_NONE:
{
BMessage userCredentialsMessage;
UserCredentials userCredentials(detail->Nickname(), detail->PasswordClear());
userCredentials.Archive(&userCredentialsMessage);
BMessage message(MSG_CREATE_ACCOUNT_SUCCESS);
message.AddMessage(KEY_USER_CREDENTIALS, &userCredentialsMessage);
messenger.SendMessage(&message);
break;
}
case ERROR_CODE_CAPTCHABADRESPONSE:
{
ValidationFailures validationFailures;
validationFailures.AddFailure("captchaResponse", "captchabadresponse");
BMessage validationFailuresMessage;
validationFailures.Archive(&validationFailuresMessage);
BMessage message(MSG_CREATE_ACCOUNT_FAILED);
message.AddMessage(KEY_VALIDATION_FAILURES, &validationFailuresMessage);
messenger.SendMessage(&message);
break;
}
case ERROR_CODE_VALIDATION:
{
ValidationFailures validationFailures;
ServerHelper::GetFailuresFromJsonRpcError(validationFailures, responsePayload);
if (Logger::IsDebugEnabled()) {
BString debugString;
_ValidationFailuresToString(validationFailures, debugString);
HDDEBUG("create account validation issues; %s", debugString.String());
}
BMessage validationFailuresMessage;
validationFailures.Archive(&validationFailuresMessage);
BMessage message(MSG_CREATE_ACCOUNT_FAILED);
message.AddMessage(KEY_VALIDATION_FAILURES, &validationFailuresMessage);
messenger.SendMessage(&message);
break;
}
default:
ServerHelper::NotifyServerJsonRpcError(responsePayload);
messenger.SendMessage(MSG_CREATE_ACCOUNT_ERROR);
break;
}
} else {
AppUtils::NotifySimpleError(B_TRANSLATE("User creation error"),
B_TRANSLATE("It was not possible to create the new user."));
messenger.SendMessage(MSG_CREATE_ACCOUNT_ERROR);
}
}
void
UserLoginWindow::_HandleCreateAccountSuccess(const UserCredentials& credentials)
{
BString message
= B_TRANSLATE("The user %Nickname% has been successfully "
"created in the HaikuDepotServer system. You can administer your user "
"details by using the web interface. You are now logged-in as this "
"new user.");
message.ReplaceAll("%Nickname%", credentials.Nickname());
BAlert* alert
= new(std::nothrow) BAlert(B_TRANSLATE("User Created"), message, B_TRANSLATE("Close"));
if (alert != NULL)
alert->Go();
_TakeUpCredentialsAndQuit(credentials);
}
void
UserLoginWindow::_HandleCreateAccountFailure(const ValidationFailures& failures)
{
_MarkCreateUserInvalidFields(failures);
_AlertCreateUserValidationFailure(failures);
_EnableMutableControls(true);
_CreateAccountSetup(CREATE_CAPTCHA);
}
unexpected error when creating the account. Note that any error messages
presented to the user are expected to be prepared and initiated from the
background thread creating the account.
*/
void
UserLoginWindow::_HandleCreateAccountError()
{
_EnableMutableControls(true);
}
*/
void
UserLoginWindow::_ViewUserUsageConditions()
{
if (fUserUsageConditions == NULL)
debugger("the usage conditions should be set");
UserUsageConditionsWindow* window
= new UserUsageConditionsWindow(fModel, *fUserUsageConditions);
window->Show();
}
void
UserLoginWindow::_ViewPasswordRequirements()
{
if (fPasswordRequirements == NULL)
HDFATAL("the password requirements must have been setup");
BString msg = B_TRANSLATE("The password must be a minimum of "
"%MinPasswordLength% characters. "
"%MinPasswordUppercaseChar% characters must be upper-case and "
"%MinPasswordDigitsChar% characters must be digits.");
msg.ReplaceAll("%MinPasswordLength%", BString() << fPasswordRequirements->MinPasswordLength());
msg.ReplaceAll("%MinPasswordUppercaseChar%",
BString() << fPasswordRequirements->MinPasswordUppercaseChar());
msg.ReplaceAll("%MinPasswordDigitsChar%",
BString() << fPasswordRequirements->MinPasswordDigitsChar());
BAlert* alert
= new(std::nothrow) BAlert(B_TRANSLATE("Password requirements"), msg, B_TRANSLATE("OK"));
if (alert != NULL)
alert->Go();
}