/*
 * Copyright 2014, Stephan Aßmus <superstippi@gmx.de>.
 * Copyright 2019, Andrew Lindesay <apl@lindesay.co.nz>.
 * All rights reserved. Distributed under the terms of the MIT License.
 */
 
#include "UserLoginWindow.h"
 
#include <algorithm>
#include <stdio.h>
 
#include <mail_encoding.h>
 
#include <Alert.h>
#include <Autolock.h>
#include <AutoLocker.h>
#include <Catalog.h>
#include <Button.h>
#include <LayoutBuilder.h>
#include <MenuField.h>
#include <PopUpMenu.h>
#include <TextControl.h>
#include <UnicodeChar.h>
 
#include "BitmapView.h"
#include "HaikuDepotConstants.h"
#include "LanguageMenuUtils.h"
#include "Model.h"
#include "TabView.h"
#include "WebAppInterface.h"
 
 
#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "UserLoginWindow"
 
 
enum {
	MSG_SEND					= 'send',
	MSG_TAB_SELECTED			= 'tbsl',
	MSG_CAPTCHA_OBTAINED		= 'cpob',
	MSG_VALIDATE_FIELDS			= 'vldt'
};
 
 
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),
	fPreferredLanguageCode(LANGUAGE_DEFAULT_CODE),
	fModel(model),
	fMode(NONE),
	fWorkerThread(-1)
{
	AddToSubset(parent);
 
	fUsernameField = new BTextControl(B_TRANSLATE("User name:"), "", NULL);
	fPasswordField = new BTextControl(B_TRANSLATE("Pass phrase:"), "", NULL);
	fPasswordField->TextView()->HideTyping(true);
 
	fNewUsernameField = new BTextControl(B_TRANSLATE("User name:"), "",
		NULL);
	fNewPasswordField = new BTextControl(B_TRANSLATE("Pass phrase:"), "",
		new BMessage(MSG_VALIDATE_FIELDS));
	fNewPasswordField->TextView()->HideTyping(true);
	fRepeatPasswordField = new BTextControl(B_TRANSLATE("Repeat pass phrase:"),
		"", new BMessage(MSG_VALIDATE_FIELDS));
	fRepeatPasswordField->TextView()->HideTyping(true);
 
	{
		AutoLocker<BLocker> locker(fModel.Lock());
		fPreferredLanguageCode = fModel.Language().PreferredLanguage().Code();
		// Construct languages popup
		BPopUpMenu* languagesMenu = new BPopUpMenu(B_TRANSLATE("Language"));
		fLanguageCodeField = new BMenuField("language",
			B_TRANSLATE("Preferred language:"), languagesMenu);
 
		LanguageMenuUtils::AddLanguagesToMenu(
			fModel.Language().SupportedLanguages(),
			languagesMenu);
		languagesMenu->SetTargetForItems(this);
 
		printf("using preferred language code [%s]\n",
			fPreferredLanguageCode.String());
		LanguageMenuUtils::MarkLanguageInMenu(fPreferredLanguageCode,
			languagesMenu);
	}
 
	fEmailField = new BTextControl(B_TRANSLATE("Email address:"), "", NULL);
	fCaptchaView = new BitmapView("captcha view");
	fCaptchaResultField = new BTextControl("", "", NULL);
 
	// Setup modification messages on all text fields to trigger validation
	// of input
	fNewUsernameField->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(fUsernameField, 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(fNewUsernameField, 0, 0)
		.AddTextControl(fNewPasswordField, 0, 1)
		.AddTextControl(fRepeatPasswordField, 0, 2)
		.AddTextControl(fEmailField, 0, 3)
		.AddMenuField(fLanguageCodeField, 0, 4)
		.Add(fCaptchaView, 0, 5)
		.Add(fCaptchaResultField, 1, 5)
 
		.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));
 
	// Build layout
	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:
			_ValidateCreateAccountFields();
			break;
 
		case MSG_SEND:
			switch (fMode) {
				case LOGIN:
					_Login();
					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 0:
						_SetMode(LOGIN);
						break;
					case 1:
						_SetMode(CREATE_ACCOUNT);
						break;
					default:
						break;
				}
			}
			break;
		}
 
		case MSG_CAPTCHA_OBTAINED:
			if (fCaptchaImage.Get() != NULL) {
				fCaptchaView->SetBitmap(fCaptchaImage);
			} else {
				fCaptchaView->UnsetBitmap();
			}
			fCaptchaResultField->SetText("");
			break;
 
		case MSG_LANGUAGE_SELECTED:
			message->FindString("code", &fPreferredLanguageCode);
			break;
 
		default:
			BWindow::MessageReceived(message);
			break;
	}
}
 
 
void
UserLoginWindow::SetOnSuccessMessage(
	const BMessenger& messenger, const BMessage& message)
{
	fOnSuccessTarget = messenger;
	fOnSuccessMessage = message;
}
 
 
void
UserLoginWindow::_SetMode(Mode mode)
{
	if (fMode == mode)
		return;
 
	fMode = mode;
 
	switch (fMode) {
		case LOGIN:
			fTabView->Select((int32)0);
			fSendButton->SetLabel(B_TRANSLATE("Log in"));
			fUsernameField->MakeFocus();
			break;
		case CREATE_ACCOUNT:
			fTabView->Select(1);
			fSendButton->SetLabel(B_TRANSLATE("Create account"));
			if (fCaptchaToken.IsEmpty())
				_RequestCaptcha();
			fNewUsernameField->MakeFocus();
			_ValidateCreateAccountFields();
			break;
		default:
			break;
	}
}
 
 
static int32
count_digits(const BString& string)
{
	int32 digits = 0;
	const char* c = string.String();
	for (int32 i = 0; i < string.CountChars(); i++) {
		uint32 unicodeChar = BUnicodeChar::FromUTF8(&c);
		if (BUnicodeChar::IsDigit(unicodeChar))
			digits++;
	}
	return digits;
}
 
 
static int32
count_upper_case_letters(const BString& string)
{
	int32 upperCaseLetters = 0;
	const char* c = string.String();
	for (int32 i = 0; i < string.CountChars(); i++) {
		uint32 unicodeChar = BUnicodeChar::FromUTF8(&c);
		if (BUnicodeChar::IsUpper(unicodeChar))
			upperCaseLetters++;
	}
	return upperCaseLetters;
}
 
 
bool
UserLoginWindow::_ValidateCreateAccountFields(bool alertProblems)
{
	BString nickName(fNewUsernameField->Text());
	BString password1(fNewPasswordField->Text());
	BString password2(fRepeatPasswordField->Text());
	BString email(fEmailField->Text());
	BString captcha(fCaptchaResultField->Text());
 
	// TODO: Use the same validation as the web-serivce
	bool validUserName = nickName.Length() >= 3;
	fNewUsernameField->MarkAsInvalid(!validUserName);
 
	bool validPassword = password1.Length() >= 8
		&& count_digits(password1) >= 2
		&& count_upper_case_letters(password1) >= 2;
	fNewPasswordField->MarkAsInvalid(!validPassword);
	fRepeatPasswordField->MarkAsInvalid(password1 != password2);
 
	bool validCaptcha = captcha.Length() > 0;
	fCaptchaResultField->MarkAsInvalid(!validCaptcha);
 
	bool valid = validUserName && validPassword && password1 == password2
		&& validCaptcha;
	if (valid && email.Length() > 0)
		return true;
 
	if (alertProblems) {
		BString message;
		alert_type alertType;
		const char* okLabel = B_TRANSLATE("OK");
		const char* cancelLabel = NULL;
		if (!valid) {
			message = B_TRANSLATE("There are problems in the form:\n\n");
			alertType = B_WARNING_ALERT;
		} else {
			alertType = B_IDEA_ALERT;
			okLabel = B_TRANSLATE("Ignore");
			cancelLabel = B_TRANSLATE("Cancel");
		}
 
		if (!validUserName) {
			message << B_TRANSLATE(
				"The user name needs to be at least "
				"3 letters long.") << "\n\n";
		}
		if (!validPassword) {
			message << B_TRANSLATE(
				"The password is too weak or invalid. "
				"Please use at least 8 characters with "
				"at least 2 numbers and 2 upper-case "
				"letters.") << "\n\n";
		}
		if (password1 != password2) {
			message << B_TRANSLATE(
				"The passwords do not match.") << "\n\n";
		}
		if (email.Length() == 0) {
			message << B_TRANSLATE(
				"If you do not provide an email address, "
				"you will not be able to reset your password "
				"if you forget it.") << "\n\n";
		}
		if (!validCaptcha) {
			message << B_TRANSLATE(
				"The captcha puzzle needs to be solved.") << "\n\n";
		}
 
		BAlert* alert = new(std::nothrow) BAlert(
			B_TRANSLATE("Input validation"),
			message,
			okLabel, cancelLabel, NULL,
			B_WIDTH_AS_USUAL, alertType);
 
		if (alert != NULL) {
			int32 choice = alert->Go();
			if (choice == 1)
				return false;
		}
	}
 
	return valid;
}
 
 
void
UserLoginWindow::_Login()
{
	BAutolock locker(&fLock);
 
	if (fWorkerThread >= 0)
		return;
 
	thread_id thread = spawn_thread(&_AuthenticateThreadEntry,
		"Authenticator", B_NORMAL_PRIORITY, this);
	if (thread >= 0)
		_SetWorkerThread(thread);
}
 
 
void
UserLoginWindow::_CreateAccount()
{
	if (!_ValidateCreateAccountFields(true))
		return;
 
	BAutolock locker(&fLock);
 
	if (fWorkerThread >= 0)
		return;
 
	thread_id thread = spawn_thread(&_CreateAccountThreadEntry,
		"Account creator", B_NORMAL_PRIORITY, this);
	if (thread >= 0)
		_SetWorkerThread(thread);
}
 
 
void
UserLoginWindow::_RequestCaptcha()
{
	if (Lock()) {
		fCaptchaToken = "";
		fCaptchaView->UnsetBitmap();
		fCaptchaImage.Unset();
		Unlock();
	}
 
	BAutolock locker(&fLock);
 
	if (fWorkerThread >= 0)
		return;
 
	thread_id thread = spawn_thread(&_RequestCaptchaThreadEntry,
		"Captcha requester", B_NORMAL_PRIORITY, this);
	if (thread >= 0)
		_SetWorkerThread(thread);
}
 
 
void
UserLoginWindow::_LoginSuccessful(const BString& message)
{
	// Clone these fields before the window goes away.
	// (This method is executd from another thread.)
	BMessenger onSuccessTarget(fOnSuccessTarget);
	BMessage onSuccessMessage(fOnSuccessMessage);
 
	BMessenger(this).SendMessage(B_QUIT_REQUESTED);
 
	BAlert* alert = new(std::nothrow) BAlert(
		B_TRANSLATE("Success"),
		message,
		B_TRANSLATE("Close"));
 
	if (alert != NULL)
		alert->Go();
 
	// Send the success message after the alert has been closed,
	// otherwise more windows will popup alongside the alert.
	if (onSuccessTarget.IsValid() && onSuccessMessage.what != 0)
		onSuccessTarget.SendMessage(&onSuccessMessage);
}
 
 
void
UserLoginWindow::_SetWorkerThread(thread_id thread)
{
	if (!Lock())
		return;
 
	bool enabled = thread < 0;
 
	fUsernameField->SetEnabled(enabled);
	fPasswordField->SetEnabled(enabled);
	fNewUsernameField->SetEnabled(enabled);
	fNewPasswordField->SetEnabled(enabled);
	fRepeatPasswordField->SetEnabled(enabled);
	fEmailField->SetEnabled(enabled);
	fLanguageCodeField->SetEnabled(enabled);
	fCaptchaResultField->SetEnabled(enabled);
	fSendButton->SetEnabled(enabled);
 
	if (thread >= 0) {
		fWorkerThread = thread;
		resume_thread(fWorkerThread);
	} else {
		fWorkerThread = -1;
	}
 
	Unlock();
}
 
 
int32
UserLoginWindow::_AuthenticateThreadEntry(void* data)
{
	UserLoginWindow* window = reinterpret_cast<UserLoginWindow*>(data);
	window->_AuthenticateThread();
	return 0;
}
 
 
void
UserLoginWindow::_AuthenticateThread()
{
	if (!Lock())
		return;
 
	BString nickName(fUsernameField->Text());
	BString passwordClear(fPasswordField->Text());
 
	Unlock();
 
	WebAppInterface interface;
	BMessage info;
 
	status_t status = interface.AuthenticateUser(
		nickName, passwordClear, info);
 
	BString error = B_TRANSLATE("Authentication failed. "
		"Connection to the service failed.");
 
	BMessage result;
	if (status == B_OK && info.FindMessage("result", &result) == B_OK) {
		BString token;
		if (result.FindString("token", &token) == B_OK && !token.IsEmpty()) {
			// We don't care for or store the token for now. The web-service
			// supports two methods of authorizing requests. One is via
			// Basic Authentication in the HTTP header, the other is via
			// Token Bearer. Since the connection is encrypted, it is hopefully
			// ok to send the password with each request instead of implementing
			// the Token Bearer. See section 5.1.2 in the haiku-depot-web
			// documentation.
			error = "";
			fModel.SetAuthorization(nickName, passwordClear, true);
		} else {
			error = B_TRANSLATE("Authentication failed. The user does "
				"not exist or the wrong password was supplied.");
		}
	}
 
	if (!error.IsEmpty()) {
		BAlert* alert = new(std::nothrow) BAlert(
			B_TRANSLATE("Authentication failed"),
			error,
			B_TRANSLATE("Close"), NULL, NULL,
			B_WIDTH_AS_USUAL, B_WARNING_ALERT);
 
		if (alert != NULL)
			alert->Go();
 
		_SetWorkerThread(-1);
	} else {
		_SetWorkerThread(-1);
		_LoginSuccessful(B_TRANSLATE("The authentication was successful."));
	}
}
 
 
int32
UserLoginWindow::_RequestCaptchaThreadEntry(void* data)
{
	UserLoginWindow* window = reinterpret_cast<UserLoginWindow*>(data);
	window->_RequestCaptchaThread();
	return 0;
}
 
 
void
UserLoginWindow::_RequestCaptchaThread()
{
	WebAppInterface interface;
	BMessage info;
 
	status_t status = interface.RequestCaptcha(info);
 
	BAutolock locker(&fLock);
 
	BMessage result;
	if (status == B_OK && info.FindMessage("result", &result) == B_OK) {
		result.FindString("token", &fCaptchaToken);
		BString imageDataBase64;
		if (result.FindString("pngImageDataBase64", &imageDataBase64) == B_OK) {
			ssize_t encodedSize = imageDataBase64.Length();
			ssize_t decodedSize = (encodedSize * 3 + 3) / 4;
			if (decodedSize > 0) {
				char* buffer = new char[decodedSize];
				decodedSize = decode_base64(buffer, imageDataBase64.String(),
					encodedSize);
				if (decodedSize > 0) {
					BMemoryIO memoryIO(buffer, (size_t)decodedSize);
					fCaptchaImage.SetTo(new(std::nothrow) SharedBitmap(
						memoryIO), true);
					BMessenger(this).SendMessage(MSG_CAPTCHA_OBTAINED);
				} else {
					fprintf(stderr, "Failed to decode captcha: %s\n",
						strerror(decodedSize));
				}
				delete[] buffer;
			}
		}
	} else {
		fprintf(stderr, "Failed to obtain captcha: %s\n", strerror(status));
	}
 
	_SetWorkerThread(-1);
}
 
 
int32
UserLoginWindow::_CreateAccountThreadEntry(void* data)
{
	UserLoginWindow* window = reinterpret_cast<UserLoginWindow*>(data);
	window->_CreateAccountThread();
	return 0;
}
 
 
void
UserLoginWindow::_CreateAccountThread()
{
	if (!Lock())
		return;
 
	BString nickName(fNewUsernameField->Text());
	BString passwordClear(fNewPasswordField->Text());
	BString email(fEmailField->Text());
	BString captchaToken(fCaptchaToken);
	BString captchaResponse(fCaptchaResultField->Text());
	BString languageCode(fPreferredLanguageCode);
 
	Unlock();
 
	WebAppInterface interface;
	BMessage info;
 
	status_t status = interface.CreateUser(
		nickName, passwordClear, email, captchaToken, captchaResponse,
		languageCode, info);
 
	BAutolock locker(&fLock);
 
	BString error = B_TRANSLATE(
		"There was a puzzling response from the web service.");
 
	BMessage result;
	if (status == B_OK) {
		if (info.FindMessage("result", &result) == B_OK) {
			error = "";
		} else if (info.FindMessage("error", &result) == B_OK) {
			result.PrintToStream();
			BString message;
			if (result.FindString("message", &message) == B_OK) {
				if (message == "captchabadresponse") {
					error = B_TRANSLATE("You have not solved the captcha "
						"puzzle correctly.");
				} else if (message == "validationerror") {
					_CollectValidationFailures(result, error);
				} else {
					BString response = B_TRANSLATE("It responded with: %message%");
					response.ReplaceFirst("%message%", message);
					error << " " << response;
				}
			}
		}
	} else {
		error = B_TRANSLATE(
			"It was not possible to contact the web service.");
	}
 
	locker.Unlock();
 
	if (!error.IsEmpty()) {
		BAlert* alert = new(std::nothrow) BAlert(
			B_TRANSLATE("Failed to create account"),
			error,
			B_TRANSLATE("Close"), NULL, NULL,
			B_WIDTH_AS_USUAL, B_WARNING_ALERT);
 
		if (alert != NULL)
			alert->Go();
 
		fprintf(stderr,
			B_TRANSLATE("Failed to create account: %s\n"), error.String());
 
		_SetWorkerThread(-1);
 
		// We need a new captcha, it can be used only once
		fCaptchaToken = "";
		_RequestCaptcha();
	} else {
		fModel.SetAuthorization(nickName, passwordClear, true);
 
		_SetWorkerThread(-1);
		_LoginSuccessful(B_TRANSLATE("Account created successfully. "
			"You can now rate packages and do other useful things."));
	}
}
 
 
void
UserLoginWindow::_CollectValidationFailures(const BMessage& result,
	BString& error) const
{
	error = B_TRANSLATE("There are problems with the data you entered:\n\n");
 
	bool found = false;
 
	BMessage data;
	BMessage failures;
	if (result.FindMessage("data", &data) == B_OK
		&& data.FindMessage("validationfailures", &failures) == B_OK) {
		int32 index = 0;
		while (true) {
			BString name;
			name << index++;
			BMessage failure;
			if (failures.FindMessage(name, &failure) != B_OK)
				break;
 
			BString property;
			BString message;
			if (failure.FindString("property", &property) == B_OK
				&& failure.FindString("message", &message) == B_OK) {
				found = true;
				if (property == "nickname" && message == "notunique") {
					error << B_TRANSLATE(
						"The username is already taken. "
						"Please choose another.");
				} else if (property == "passwordClear"
					&& message == "invalid") {
					error << B_TRANSLATE(
						"The password is too weak or invalid. "
						"Please use at least 8 characters with "
						"at least 2 numbers and 2 upper-case "
						"letters.");
				} else if (property == "email" && message == "malformed") {
					error << B_TRANSLATE(
						"The email address appears to be malformed.");
				} else {
					error << property << ": " << message;
				}
			}
		}
	}
 
	if (!found) {
		error << B_TRANSLATE("But none could be listed here, sorry.");
	}
}

V773 Visibility scope of the 'alert' pointer was exited without releasing the memory. A memory leak is possible.

V773 Visibility scope of the 'alert' pointer was exited without releasing the memory. A memory leak is possible.

V773 Visibility scope of the 'alert' pointer was exited without releasing the memory. A memory leak is possible.