diff --git a/src/apps/haikudepot/Jamfile b/src/apps/haikudepot/Jamfile index 74ec3d990a..27a1432e28 100644 --- a/src/apps/haikudepot/Jamfile +++ b/src/apps/haikudepot/Jamfile @@ -116,6 +116,8 @@ local applicationSources = App.cpp BarberPole.cpp BitmapView.cpp + Captcha.cpp + CreateUserDetail.cpp DecisionProvider.cpp FeaturedPackagesView.cpp FilterView.cpp @@ -149,6 +151,8 @@ local applicationSources = UserLoginWindow.cpp UserUsageConditions.cpp UserUsageConditionsWindow.cpp + ValidationFailure.cpp + ValidationUtils.cpp WorkStatusView.cpp # network + server / local processes diff --git a/src/apps/haikudepot/model/Captcha.cpp b/src/apps/haikudepot/model/Captcha.cpp new file mode 100644 index 0000000000..b4b9df85c2 --- /dev/null +++ b/src/apps/haikudepot/model/Captcha.cpp @@ -0,0 +1,100 @@ +/* + * Copyright 2019, Andrew Lindesay . + * + * All rights reserved. Distributed under the terms of the MIT License. + */ +#include "Captcha.h" + +#include + +#include + +// These are keys that are used to store this object's data into a BMessage +// instance. + +#define KEY_TOKEN "token" +#define KEY_PNG_IMAGE_DATA "pngImageData" + + +Captcha::Captcha(BMessage* from) + : + fToken(""), + fPngImageData(NULL) +{ + if (from->FindString(KEY_TOKEN, &fToken) != B_OK) + printf("expected key [%s] in the message data when creating a " + "Captcha\n", KEY_TOKEN); + + const void* data; + ssize_t len; + + if (from->FindData(KEY_PNG_IMAGE_DATA, B_ANY_TYPE, &data, &len) != B_OK) + printf("expected key [%s] in the message data\n", KEY_PNG_IMAGE_DATA); + else + SetPngImageData(data, len); +} + + +Captcha::Captcha() + : + fToken(""), + fPngImageData(NULL) +{ +} + + +Captcha::~Captcha() +{ + if (fPngImageData != NULL) + delete fPngImageData; +} + + +const BString& +Captcha::Token() const +{ + return fToken; +} + + +BPositionIO* +Captcha::PngImageData() const +{ + return fPngImageData; +} + + +void +Captcha::SetToken(const BString& value) +{ + fToken = value; +} + + +void +Captcha::SetPngImageData(const void* data, size_t len) +{ + if (fPngImageData != NULL) + delete fPngImageData; + fPngImageData = NULL; + if (data != NULL) { + fPngImageData = new BMallocIO(); + fPngImageData->Write(data, len); + } +} + + +status_t +Captcha::Archive(BMessage* into, bool deep) const +{ + status_t result = B_OK; + if (result == B_OK && into == NULL) + result = B_ERROR; + if (result == B_OK) + result = into->AddString(KEY_TOKEN, fToken); + if (result == B_OK && fPngImageData != NULL) { + result = into->AddData(KEY_PNG_IMAGE_DATA, B_ANY_TYPE, + fPngImageData->Buffer(), fPngImageData->BufferLength()); + } + return result; +} \ No newline at end of file diff --git a/src/apps/haikudepot/model/Captcha.h b/src/apps/haikudepot/model/Captcha.h new file mode 100644 index 0000000000..9b619d1020 --- /dev/null +++ b/src/apps/haikudepot/model/Captcha.h @@ -0,0 +1,41 @@ +/* + * Copyright 2019, Andrew Lindesay . + * + * All rights reserved. Distributed under the terms of the MIT License. + */ +#ifndef CAPTCHA_H +#define CAPTCHA_H + + +#include +#include + +class BPositionIO; + +/*! When a user has to perform some sensitive operation, it is necessary to make + sure that it is not a 'robot' or software system that is acting as if it + were a person. It is necessary to know that a real person is acting. In + this case a graphical puzzle is presented to the user that presumably only + a human operator could solve. This is called a Captcha. +*/ + +class Captcha : public BArchivable { +public: + Captcha(BMessage* from); + Captcha(); + virtual ~Captcha(); + + const BString& Token() const; + BPositionIO* PngImageData() const; + + void SetToken(const BString& value); + void SetPngImageData(const void* data, size_t len); + + status_t Archive(BMessage* into, bool deep = true) const; +private: + BString fToken; + BMallocIO* fPngImageData; +}; + + +#endif // CAPTCHA_H diff --git a/src/apps/haikudepot/model/CreateUserDetail.cpp b/src/apps/haikudepot/model/CreateUserDetail.cpp new file mode 100644 index 0000000000..f81a16f5a5 --- /dev/null +++ b/src/apps/haikudepot/model/CreateUserDetail.cpp @@ -0,0 +1,182 @@ +/* + * Copyright 2019, Andrew Lindesay . + * + * All rights reserved. Distributed under the terms of the MIT License. + */ + #include "CreateUserDetail.h" + + // These are keys that are used to store this object's data into a BMessage + // instance. + +#define KEY_NICKNAME "nickname" +#define KEY_PASSWORD_CLEAR "passwordClear" +#define KEY_IS_PASSWORD_REPEATED "isPasswordRepeated" +#define KEY_EMAIL "email" +#define KEY_CAPTCHA_TOKEN "captchaToken" +#define KEY_CAPTCHA_RESPONSE "captchaResponse" +#define KEY_LANGUAGE_CODE "languageCode" +#define KEY_AGREED_USER_USAGE_CONDITIONS_CODE "agreedUserUsageConditionsCode" + + +CreateUserDetail::CreateUserDetail(BMessage* from) +{ + from->FindString(KEY_NICKNAME, &fNickname); + from->FindString(KEY_PASSWORD_CLEAR, &fPasswordClear); + from->FindBool(KEY_IS_PASSWORD_REPEATED, &fIsPasswordRepeated); + from->FindString(KEY_EMAIL, &fEmail); + from->FindString(KEY_CAPTCHA_TOKEN, &fCaptchaToken); + from->FindString(KEY_CAPTCHA_RESPONSE, &fCaptchaResponse); + from->FindString(KEY_LANGUAGE_CODE, &fLanguageCode); + from->FindString(KEY_AGREED_USER_USAGE_CONDITIONS_CODE, + &fAgreedUserUsageConditionsCode); +} + + +CreateUserDetail::CreateUserDetail() + : + fIsPasswordRepeated(false) +{ +} + + +CreateUserDetail::~CreateUserDetail() +{ +} + + +const BString& +CreateUserDetail::Nickname() const +{ + return fNickname; +} + + +const BString& +CreateUserDetail::PasswordClear() const +{ + return fPasswordClear; +} + + +bool +CreateUserDetail::IsPasswordRepeated() const +{ + return fIsPasswordRepeated; +} + + +const BString& +CreateUserDetail::Email() const +{ + return fEmail; +} + + +const BString& +CreateUserDetail::CaptchaToken() const +{ + return fCaptchaToken; +} + + +const BString& +CreateUserDetail::CaptchaResponse() const +{ + return fCaptchaResponse; +} + + +const BString& +CreateUserDetail::LanguageCode() const +{ + return fLanguageCode; +} + + +const BString& +CreateUserDetail::AgreedToUserUsageConditionsCode() const +{ + return fAgreedUserUsageConditionsCode; +} + + +void +CreateUserDetail::SetNickname(const BString& value) +{ + fNickname = value; +} + + +void +CreateUserDetail::SetPasswordClear(const BString& value) +{ + fPasswordClear = value; +} + + +void +CreateUserDetail::SetIsPasswordRepeated(bool value) +{ + fIsPasswordRepeated = value; +} + + +void +CreateUserDetail::SetEmail(const BString& value) +{ + fEmail = value; +} + + +void +CreateUserDetail::SetCaptchaToken(const BString& value) +{ + fCaptchaToken = value; +} + + +void +CreateUserDetail::SetCaptchaResponse(const BString& value) +{ + fCaptchaResponse = value; +} + + +void +CreateUserDetail::SetLanguageCode(const BString& value) +{ + fLanguageCode = value; +} + + +void +CreateUserDetail::SetAgreedToUserUsageConditionsCode(const BString& value) +{ + fAgreedUserUsageConditionsCode = value; +} + + +status_t +CreateUserDetail::Archive(BMessage* into, bool deep) const +{ + status_t result = B_OK; + if (result == B_OK) + result = into->AddString(KEY_NICKNAME, fNickname); + if (result == B_OK) + result = into->AddString(KEY_PASSWORD_CLEAR, fPasswordClear); + if (result == B_OK) + result = into->AddBool(KEY_IS_PASSWORD_REPEATED, fIsPasswordRepeated); + if (result == B_OK) + result = into->AddString(KEY_EMAIL, fEmail); + if (result == B_OK) + result = into->AddString(KEY_CAPTCHA_TOKEN, fCaptchaToken); + if (result == B_OK) + result = into->AddString(KEY_CAPTCHA_RESPONSE, fCaptchaResponse); + if (result == B_OK) + result = into->AddString(KEY_LANGUAGE_CODE, fLanguageCode); + if (result == B_OK) { + result = into->AddString(KEY_AGREED_USER_USAGE_CONDITIONS_CODE, + fAgreedUserUsageConditionsCode); + } + return result; +} \ No newline at end of file diff --git a/src/apps/haikudepot/model/CreateUserDetail.h b/src/apps/haikudepot/model/CreateUserDetail.h new file mode 100644 index 0000000000..917c0c5f44 --- /dev/null +++ b/src/apps/haikudepot/model/CreateUserDetail.h @@ -0,0 +1,59 @@ +/* + * Copyright 2019, Andrew Lindesay . + * + * All rights reserved. Distributed under the terms of the MIT License. + */ +#ifndef CREATE_USER_DETAIL_H +#define CREATE_USER_DETAIL_H + + +#include +#include + +/*! The operator may choose to create a User. In this case, he or she is + required to supply some details such as the nickname, password, email... + and those details are then collated and provided to the application server + (HDS) in order that the User is created in the application server. This + model carries all of those details in an object so that they might be + easily conveyed between methods. +*/ + +class CreateUserDetail : public BArchivable { +public: + CreateUserDetail(BMessage* from); + CreateUserDetail(); + virtual ~CreateUserDetail(); + + const BString& Nickname() const; + const BString& PasswordClear() const; + bool IsPasswordRepeated() const; + const BString& Email() const; + const BString& CaptchaToken() const; + const BString& CaptchaResponse() const; + const BString& LanguageCode() const; + const BString& AgreedToUserUsageConditionsCode() const; + + void SetNickname(const BString& value); + void SetPasswordClear(const BString& value); + void SetIsPasswordRepeated(bool value); + void SetEmail(const BString& value); + void SetCaptchaToken(const BString& value); + void SetCaptchaResponse(const BString& value); + void SetLanguageCode(const BString& value); + void SetAgreedToUserUsageConditionsCode( + const BString& value); + + status_t Archive(BMessage* into, bool deep = true) const; +private: + BString fNickname; + BString fPasswordClear; + bool fIsPasswordRepeated; + BString fEmail; + BString fCaptchaToken; + BString fCaptchaResponse; + BString fLanguageCode; + BString fAgreedUserUsageConditionsCode; +}; + + +#endif // CREATE_USER_DETAIL_H diff --git a/src/apps/haikudepot/model/UserCredentials.cpp b/src/apps/haikudepot/model/UserCredentials.cpp index d62d3ca0fa..a2def1725a 100644 --- a/src/apps/haikudepot/model/UserCredentials.cpp +++ b/src/apps/haikudepot/model/UserCredentials.cpp @@ -32,6 +32,15 @@ UserCredentials::UserCredentials(const BString& nickname, } +UserCredentials::UserCredentials(const UserCredentials& other) + : + fNickname(other.Nickname()), + fPasswordClear(other.PasswordClear()), + fIsSuccessful(false) +{ +} + + UserCredentials::UserCredentials() : fNickname(), diff --git a/src/apps/haikudepot/model/UserCredentials.h b/src/apps/haikudepot/model/UserCredentials.h index 01a5cc902f..c76c0e3429 100644 --- a/src/apps/haikudepot/model/UserCredentials.h +++ b/src/apps/haikudepot/model/UserCredentials.h @@ -11,9 +11,9 @@ #include -/*! This object represents the tuple of the user's nickname (username) and - password. It also carries a boolean that indicates if an authentication - with these credentials was successful or failed. +/*! This object represents the tuple of the user's nickname (username) and + password. It also carries a boolean that indicates if an authentication + with these credentials was successful or failed. */ class UserCredentials : public BArchivable { @@ -21,6 +21,7 @@ public: UserCredentials(BMessage* from); UserCredentials(const BString& nickname, const BString& passwordClear); + UserCredentials(const UserCredentials& other); UserCredentials(); virtual ~UserCredentials(); @@ -42,4 +43,5 @@ private: bool fIsSuccessful; }; + #endif // USER_CREDENTIALS_H diff --git a/src/apps/haikudepot/model/UserUsageConditions.h b/src/apps/haikudepot/model/UserUsageConditions.h index 3d48c443c9..ad2d6cb27a 100644 --- a/src/apps/haikudepot/model/UserUsageConditions.h +++ b/src/apps/haikudepot/model/UserUsageConditions.h @@ -11,11 +11,11 @@ #include -/*! A user in the HDS system should have agreed to user usage conditions when - they created their user on the server. This object represents the user - usage conditions that either they have agreed to or that they could agree - to. Each set of user usage conditions has a code that uniquely identifies - a given set of conditions. +/*! A user in the HDS system should have agreed to user usage conditions when + they created their user on the server. This object represents the user + usage conditions that either they have agreed to or that they could agree + to. Each set of user usage conditions has a code that uniquely identifies + a given set of conditions. */ class UserUsageConditions : public BArchivable { diff --git a/src/apps/haikudepot/model/ValidationFailure.cpp b/src/apps/haikudepot/model/ValidationFailure.cpp new file mode 100644 index 0000000000..e94824cc9c --- /dev/null +++ b/src/apps/haikudepot/model/ValidationFailure.cpp @@ -0,0 +1,230 @@ +/* + * Copyright 2019, Andrew Lindesay . + * + * All rights reserved. Distributed under the terms of the MIT License. + */ +#include "ValidationFailure.h" + +// These are keys that are used to store this object's data into a BMessage +// instance. + +#define KEY_PROPERTY "property" +#define KEY_PREFIX_MESSAGE "message_" +#define KEY_PREFIX_ITEM "item_" + + +// #pragma mark - Single Validation Failure + + +ValidationFailure::ValidationFailure(BMessage* from) +{ + from->FindString(KEY_PROPERTY, &fProperty); + + if (fProperty.IsEmpty()) + debugger("illegal state; missing property in message"); + + status_t result = B_OK; + BString name; + BString message; + + for (int32 i = 0; result == B_OK; i++) { + name.SetToFormat("%s%" B_PRId32, KEY_PREFIX_MESSAGE, i); + result = from->FindString(name, &message); + + if (result == B_OK) + AddMessage(message); + } +} + + +ValidationFailure::ValidationFailure(const BString& property) +{ + fProperty = property; +} + + +ValidationFailure::~ValidationFailure() +{ +} + + +const BString& +ValidationFailure::Property() const +{ + return fProperty; +} + + +const BStringList& +ValidationFailure::Messages() const +{ + return fMessages; +} + + +bool +ValidationFailure::IsEmpty() const +{ + return fMessages.IsEmpty(); +} + + +bool +ValidationFailure::Contains(const BString& message) const +{ + return fMessages.HasString(message); +} + + +void +ValidationFailure::AddMessage(const BString& value) +{ + fMessages.Add(value); +} + + +status_t +ValidationFailure::Archive(BMessage* into, bool deep) const +{ + status_t result = B_OK; + BString key; + if (result == B_OK) + result = into->AddString(KEY_PROPERTY, fProperty); + for (int32 i = 0; result == B_OK && i < fMessages.CountStrings(); i++) { + key.SetToFormat("%s%" B_PRId32, KEY_PREFIX_MESSAGE, i); + result = into->AddString(key, fMessages.StringAt(i)); + } + return result; +} + + +// #pragma mark - Collections of Validation Failures + + +ValidationFailures::ValidationFailures(BMessage* from) + : + fItems(20, true) +{ + _AddFromMessage(from); +} + + +ValidationFailures::ValidationFailures() + : + fItems(20, true) +{ +} + + +ValidationFailures::~ValidationFailures() +{ +} + + +void +ValidationFailures::AddFailure(const BString& property, const BString& message) +{ + _GetOrCreateFailure(property)->AddMessage(message); +} + + +int32 +ValidationFailures::CountFailures() const +{ + return fItems.CountItems(); +} + + +bool +ValidationFailures::IsEmpty() const +{ + return fItems.IsEmpty(); +} + + +bool +ValidationFailures::Contains(const BString& property) const +{ + ValidationFailure* failure = _GetFailure(property); + return failure != NULL && !failure->IsEmpty(); +} + + +bool +ValidationFailures::Contains(const BString& property, + const BString& message) const +{ + ValidationFailure* failure = _GetFailure(property); + return failure != NULL && failure->Contains(message); +} + + +ValidationFailure* +ValidationFailures::FailureAtIndex(int32 index) const +{ + return fItems.ItemAt(index); +} + + +status_t +ValidationFailures::Archive(BMessage* into, bool deep) const +{ + status_t result = B_OK; + BString key; + + for (int32 i = 0; i < fItems.CountItems() && result == B_OK; i++) { + ValidationFailure* item = fItems.ItemAt(i); + BMessage itemMessage; + result = item->Archive(&itemMessage); + if (result == B_OK) { + key.SetToFormat("%s%" B_PRId32, KEY_PREFIX_ITEM, i); + result = into->AddMessage(key, &itemMessage); + } + } + + return result; +} + + +ValidationFailure* +ValidationFailures::_GetFailure(const BString& property) const +{ + for (int32 i = 0; i < fItems.CountItems(); i++) { + ValidationFailure* item = fItems.ItemAt(i); + if (item->Property() == property) + return item; + } + + return NULL; +} + + +ValidationFailure* +ValidationFailures::_GetOrCreateFailure(const BString& property) +{ + ValidationFailure* item = _GetFailure(property); + + if (item == NULL) { + item = new ValidationFailure(property); + fItems.AddItem(item); + } + + return item; +} + + +void +ValidationFailures::_AddFromMessage(const BMessage* from) +{ + int32 i = 0; + BString key; + + while (true) { + BMessage itemMessage; + key.SetToFormat("%s%" B_PRId32, KEY_PREFIX_ITEM, i); + if (from->FindMessage(key, &itemMessage) != B_OK) + return; + fItems.AddItem(new ValidationFailure(&itemMessage)); + i++; + } +} diff --git a/src/apps/haikudepot/model/ValidationFailure.h b/src/apps/haikudepot/model/ValidationFailure.h new file mode 100644 index 0000000000..93d93380b0 --- /dev/null +++ b/src/apps/haikudepot/model/ValidationFailure.h @@ -0,0 +1,69 @@ +/* + * Copyright 2019, Andrew Lindesay . + * + * All rights reserved. Distributed under the terms of the MIT License. + */ +#ifndef VALIDATION_FAILURE_H +#define VALIDATION_FAILURE_H + + +#include +#include +#include +#include + + +/*! This objects marries together a 'key' into some data together with an + error message. The error message is not intended to be human readable, but + is a key into a message. +*/ + +class ValidationFailure : public BArchivable { +public: + ValidationFailure(BMessage* from); + ValidationFailure(const BString& property); + virtual ~ValidationFailure(); + + const BString& Property() const; + const BStringList& Messages() const; + bool IsEmpty() const; + bool Contains(const BString& message) const; + + void AddMessage(const BString& value); + + status_t Archive(BMessage* into, bool deep = true) const; +private: + BString fProperty; + BStringList fMessages; +}; + + +class ValidationFailures : public BArchivable { +public: + ValidationFailures(BMessage* from); + ValidationFailures(); + virtual ~ValidationFailures(); + + void AddFailure(const BString& property, + const BString& message); + int32 CountFailures() const; + ValidationFailure* FailureAtIndex(int32 index) const; + bool IsEmpty() const; + bool Contains(const BString& property) const; + bool Contains(const BString& property, + const BString& message) const; + + status_t Archive(BMessage* into, bool deep = true) const; + +private: + void _AddFromMessage(const BMessage* from); + ValidationFailure* _GetFailure(const BString& property) const; + ValidationFailure* _GetOrCreateFailure(const BString& property); + +private: + BObjectList + fItems; +}; + + +#endif // VALIDATION_FAILURE_H diff --git a/src/apps/haikudepot/server/ServerHelper.cpp b/src/apps/haikudepot/server/ServerHelper.cpp index c3834ed7e8..8fcf0e5efe 100644 --- a/src/apps/haikudepot/server/ServerHelper.cpp +++ b/src/apps/haikudepot/server/ServerHelper.cpp @@ -27,29 +27,30 @@ #define KEY_HEADER_MINIMUM_VERSION "X-Desktop-Application-Minimum-Version" -/*! This method will cause an alert to be shown to the user regarding a +/*! \brief This method will cause an alert to be shown to the user regarding a JSON-RPC error that has been sent from the application server. It will send a message to the application looper which will then relay the message to the looper and then onto the user to see. + \param responsePayload The top level payload returned from the server. */ /*static*/ void -ServerHelper::NotifyServerJsonRpcError(BMessage& error) +ServerHelper::NotifyServerJsonRpcError(BMessage& responsePayload) { BMessage message(MSG_SERVER_ERROR); - message.AddMessage("error", &error); + message.AddMessage("error", &responsePayload); be_app->PostMessage(&message); } /*static*/ void -ServerHelper::AlertServerJsonRpcError(BMessage* message) +ServerHelper::AlertServerJsonRpcError(BMessage* responseEnvelopeMessage) { - BMessage error; + BMessage errorMessage; int32 errorCode = 0; - if (message->FindMessage("error", &error) == B_OK) - errorCode = WebAppInterface::ErrorCodeFromResponse(error); + if (responseEnvelopeMessage->FindMessage("error", &errorMessage) == B_OK) + errorCode = WebAppInterface::ErrorCodeFromResponse(errorMessage); BString alertText; @@ -210,4 +211,78 @@ ServerHelper::IsPlatformNetworkAvailable() } return false; +} + + +/*! If the response is an error and the error is a validation failure then + * various validation errors may be carried in the error data. These are + * copied into the supplied failures. An abridged example input JSON structure + * would be; + * + * \code + * { + * ... + * "error": { + * "code": -32800, + * "data": { + "validationfailures": [ + { "property": "nickname", "message": "required" }, + ... + ] + }, + ... + * } + * } + * \endcode + * + * \param failures is the object into which the validation failures are to be + * written. + * \param responseEnvelopeMessage is a representation of the entire JSON-RPC + * response sent back from the server when the error occurred. + * + */ + +/*static*/ void +ServerHelper::GetFailuresFromJsonRpcError( + ValidationFailures& failures, BMessage& responseEnvelopeMessage) +{ + BMessage errorMessage; + int32 errorCode = WebAppInterface::ErrorCodeFromResponse( + responseEnvelopeMessage); + + if (responseEnvelopeMessage.FindMessage("error", &errorMessage) == B_OK) { + BMessage dataMessage; + + if (errorMessage.FindMessage("data", &dataMessage) == B_OK) { + BMessage validationFailuresMessage; + + if (dataMessage.FindMessage("validationfailures", + &validationFailuresMessage) == B_OK) { + _GetFailuresFromJsonRpcFailures(failures, + validationFailuresMessage); + } + } + } +} + + +/*static*/ void +ServerHelper::_GetFailuresFromJsonRpcFailures( + ValidationFailures& failures, BMessage& jsonRpcFailures) +{ + int32 index = 0; + while (true) { + BString name; + name << index++; + BMessage failure; + if (jsonRpcFailures.FindMessage(name, &failure) != B_OK) + break; + + BString property; + BString message; + if (failure.FindString("property", &property) == B_OK + && failure.FindString("message", &message) == B_OK) { + failures.AddFailure(property, message); + } + } } \ No newline at end of file diff --git a/src/apps/haikudepot/server/ServerHelper.h b/src/apps/haikudepot/server/ServerHelper.h index f7554ac01a..a8eb4f9115 100644 --- a/src/apps/haikudepot/server/ServerHelper.h +++ b/src/apps/haikudepot/server/ServerHelper.h @@ -1,5 +1,5 @@ /* - * Copyright 2017-2018, Andrew Lindesay . + * Copyright 2017-2019, Andrew Lindesay . * All rights reserved. Distributed under the terms of the MIT License. */ #ifndef SERVER_HELPER_H @@ -7,6 +7,8 @@ #include +#include "ValidationFailure.h" + class BMessage; @@ -27,7 +29,15 @@ public: static void NotifyServerJsonRpcError( BMessage& error); static void AlertServerJsonRpcError( - BMessage* message); + BMessage* responseEnvelopeMessage); + static void GetFailuresFromJsonRpcError( + ValidationFailures& failures, + BMessage& responseEnvelopeMessage); + +private: + static void _GetFailuresFromJsonRpcFailures( + ValidationFailures& failures, + BMessage& jsonRpcFailures); }; #endif // SERVER_HELPER_H diff --git a/src/apps/haikudepot/server/WebAppInterface.cpp b/src/apps/haikudepot/server/WebAppInterface.cpp index c88ce6813a..3a57823b60 100644 --- a/src/apps/haikudepot/server/WebAppInterface.cpp +++ b/src/apps/haikudepot/server/WebAppInterface.cpp @@ -8,29 +8,22 @@ #include -#include #include -#include -#include -#include #include #include #include #include #include #include -#include #include #include #include #include -#include "AutoLocker.h" #include "DataIOUtils.h" #include "HaikuDepotConstants.h" #include "List.h" #include "Logger.h" -#include "PackageInfo.h" #include "ServerSettings.h" #include "ServerHelper.h" @@ -40,159 +33,6 @@ #define LOG_PAYLOAD_LIMIT 8192 -class JsonBuilder { -public: - JsonBuilder() - : - fString("{"), - fInList(false) - { - } - - JsonBuilder& AddObject() - { - fString << '{'; - fInList = false; - return *this; - } - - JsonBuilder& AddObject(const char* name) - { - _StartName(name); - fString << '{'; - fInList = false; - return *this; - } - - JsonBuilder& EndObject() - { - fString << '}'; - fInList = true; - return *this; - } - - JsonBuilder& AddArray(const char* name) - { - _StartName(name); - fString << '['; - fInList = false; - return *this; - } - - JsonBuilder& EndArray() - { - fString << ']'; - fInList = true; - return *this; - } - - JsonBuilder& AddStrings(const StringList& strings) - { - for (int i = 0; i < strings.CountItems(); i++) - AddItem(strings.ItemAtFast(i)); - return *this; - } - - JsonBuilder& AddItem(const char* item) - { - return AddItem(item, false); - } - - JsonBuilder& AddItem(const char* item, bool nullIfEmpty) - { - if (item == NULL || (nullIfEmpty && strlen(item) == 0)) { - if (fInList) - fString << ",null"; - else - fString << "null"; - } else { - if (fInList) - fString << ",\""; - else - fString << '"'; - fString << _EscapeString(item); - fString << '"'; - } - fInList = true; - return *this; - } - - JsonBuilder& AddValue(const char* name, const char* value) - { - return AddValue(name, value, false); - } - - JsonBuilder& AddValue(const char* name, const char* value, - bool nullIfEmpty) - { - _StartName(name); - if (value == NULL || (nullIfEmpty && strlen(value) == 0)) { - fString << "null"; - } else { - fString << '"'; - fString << _EscapeString(value); - fString << '"'; - } - fInList = true; - return *this; - } - - JsonBuilder& AddValue(const char* name, int value) - { - _StartName(name); - fString << value; - fInList = true; - return *this; - } - - JsonBuilder& AddValue(const char* name, bool value) - { - _StartName(name); - if (value) - fString << "true"; - else - fString << "false"; - fInList = true; - return *this; - } - - const BString& End() - { - fString << "}\n"; - return fString; - } - -private: - void _StartName(const char* name) - { - if (fInList) - fString << ",\""; - else - fString << '"'; - fString << _EscapeString(name); - fString << "\":"; - } - - BString _EscapeString(const char* original) const - { - BString string(original); - string.ReplaceAll("\\", "\\\\"); - string.ReplaceAll("\"", "\\\""); - string.ReplaceAll("/", "\\/"); - string.ReplaceAll("\b", "\\b"); - string.ReplaceAll("\f", "\\f"); - string.ReplaceAll("\n", "\\n"); - string.ReplaceAll("\r", "\\r"); - string.ReplaceAll("\t", "\\t"); - return string; - } - -private: - BString fString; - bool fInList; -}; - - class ProtocolListener : public BUrlProtocolListener { public: ProtocolListener(bool traceLogging) @@ -314,18 +154,27 @@ WebAppInterface::Nickname() const status_t WebAppInterface::GetChangelog(const BString& packageName, BMessage& message) { - BString jsonString = JsonBuilder() - .AddValue("jsonrpc", "2.0") - .AddValue("id", ++fRequestIndex) - .AddValue("method", "getPkgChangelog") - .AddArray("params") - .AddObject() - .AddValue("pkgName", packageName) - .EndObject() - .EndArray() - .End(); + BMallocIO* requestEnvelopeData = new BMallocIO(); + // BHttpRequest later takes ownership of this. + BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); - return _SendJsonRequest("pkg", jsonString, 0, message); + requestEnvelopeWriter.WriteObjectStart(); + _WriteStandardJsonRpcEnvelopeValues(requestEnvelopeWriter, + "getPkgChangelog"); + requestEnvelopeWriter.WriteObjectName("params"); + requestEnvelopeWriter.WriteArrayStart(); + requestEnvelopeWriter.WriteObjectStart(); + + requestEnvelopeWriter.WriteObjectName("pkgName"); + requestEnvelopeWriter.WriteString(packageName.String()); + + requestEnvelopeWriter.WriteObjectEnd(); + requestEnvelopeWriter.WriteArrayEnd(); + requestEnvelopeWriter.WriteObjectEnd(); + + return _SendJsonRequest("pkg", requestEnvelopeData, + _LengthAndSeekToZero(requestEnvelopeData), 0, + message); } @@ -429,10 +278,10 @@ WebAppInterface::RetreiveUserRatingForPackageAndVersionByUser( } -/*! This method will fill out the supplied UserDetail object with information - about the user that is supplied in the credentials. Importantly it will - also authenticate the request with the details of the credentials and will - not use the credentials that are configured in 'fCredentials'. +/*! This method will fill out the supplied UserDetail object with information + about the user that is supplied in the credentials. Importantly it will + also authenticate the request with the details of the credentials and will + not use the credentials that are configured in 'fCredentials'. */ status_t @@ -468,8 +317,8 @@ WebAppInterface::RetrieveUserDetailForCredentials( } -/*! This method will return the credentials for the currently authenticated - user. +/*! This method will return the credentials for the currently authenticated + user. */ status_t @@ -479,9 +328,9 @@ WebAppInterface::RetrieveCurrentUserDetail(BMessage& message) } -/*! When the user requests user detail, the server sends back an envelope of - response data. This method will unpack the data into a model object. - \return Not B_OK if something went wrong. +/*! When the user requests user detail, the server sends back an envelope of + response data. This method will unpack the data into a model object. + \return Not B_OK if something went wrong. */ /*static*/ status_t @@ -533,14 +382,14 @@ WebAppInterface::UnpackUserDetail(BMessage& responseEnvelopeMessage, } -/*! \brief Returns data relating to the user usage conditions +/*! \brief Returns data relating to the user usage conditions \param code defines the version of the data to return or if empty then the latest is returned. - This method will go to the server and get details relating to the user usage - conditions. It does this in two API calls; first gets the details (the - minimum age) and in the second call, the text of the conditions is returned. + This method will go to the server and get details relating to the user usage + conditions. It does this in two API calls; first gets the details (the + minimum age) and in the second call, the text of the conditions is returned. */ status_t @@ -782,17 +631,23 @@ WebAppInterface::RetrieveScreenshot(const BString& code, status_t WebAppInterface::RequestCaptcha(BMessage& message) { - BString jsonString = JsonBuilder() - .AddValue("jsonrpc", "2.0") - .AddValue("id", ++fRequestIndex) - .AddValue("method", "generateCaptcha") - .AddArray("params") - .AddObject() - .EndObject() - .EndArray() - .End(); + BMallocIO* requestEnvelopeData = new BMallocIO(); + // BHttpRequest later takes ownership of this. + BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); - return _SendJsonRequest("captcha", jsonString, 0, message); + requestEnvelopeWriter.WriteObjectStart(); + _WriteStandardJsonRpcEnvelopeValues(requestEnvelopeWriter, + "generateCaptcha"); + requestEnvelopeWriter.WriteObjectName("params"); + requestEnvelopeWriter.WriteArrayStart(); + requestEnvelopeWriter.WriteObjectStart(); + requestEnvelopeWriter.WriteObjectEnd(); + requestEnvelopeWriter.WriteArrayEnd(); + requestEnvelopeWriter.WriteObjectEnd(); + + return _SendJsonRequest("captcha", requestEnvelopeData, + _LengthAndSeekToZero(requestEnvelopeData), 0, + message); } @@ -845,28 +700,38 @@ status_t WebAppInterface::AuthenticateUser(const BString& nickName, const BString& passwordClear, BMessage& message) { - BString jsonString = JsonBuilder() - .AddValue("jsonrpc", "2.0") - .AddValue("id", ++fRequestIndex) - .AddValue("method", "authenticateUser") - .AddArray("params") - .AddObject() - .AddValue("nickname", nickName) - .AddValue("passwordClear", passwordClear) - .EndObject() - .EndArray() - .End(); + BMallocIO* requestEnvelopeData = new BMallocIO(); + // BHttpRequest later takes ownership of this. + BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); - return _SendJsonRequest("user", jsonString, 0, message); + requestEnvelopeWriter.WriteObjectStart(); + _WriteStandardJsonRpcEnvelopeValues(requestEnvelopeWriter, + "authenticateUser"); + requestEnvelopeWriter.WriteObjectName("params"); + requestEnvelopeWriter.WriteArrayStart(); + requestEnvelopeWriter.WriteObjectStart(); + + requestEnvelopeWriter.WriteObjectName("nickname"); + requestEnvelopeWriter.WriteString(nickName.String()); + requestEnvelopeWriter.WriteObjectName("passwordClear"); + requestEnvelopeWriter.WriteString(passwordClear.String()); + + requestEnvelopeWriter.WriteObjectEnd(); + requestEnvelopeWriter.WriteArrayEnd(); + requestEnvelopeWriter.WriteObjectEnd(); + + return _SendJsonRequest("user", requestEnvelopeData, + _LengthAndSeekToZero(requestEnvelopeData), 0, + message); } -/*! JSON-RPC invocations return a response. The response may be either - a result or it may be an error depending on the response structure. - If it is an error then there may be additional detail that is the - error code and message. This method will extract the error code - from the response. This method will return 0 if the payload does - not look like an error. +/*! JSON-RPC invocations return a response. The response may be either + a result or it may be an error depending on the response structure. + If it is an error then there may be additional detail that is the + error code and message. This method will extract the error code + from the response. This method will return 0 if the payload does + not look like an error. */ int32 @@ -1107,9 +972,9 @@ WebAppInterface::_LogPayload(BPositionIO* requestData, size_t size) } -/*! This will get the position of the data to get the length an then sets the - offset to zero so that it can be re-read for reading the payload in to log - or send. +/*! This will get the position of the data to get the length an then sets the + offset to zero so that it can be re-read for reading the payload in to log + or send. */ off_t diff --git a/src/apps/haikudepot/ui/UserLoginWindow.cpp b/src/apps/haikudepot/ui/UserLoginWindow.cpp index a0dba6e66f..643d40fcf4 100644 --- a/src/apps/haikudepot/ui/UserLoginWindow.cpp +++ b/src/apps/haikudepot/ui/UserLoginWindow.cpp @@ -22,17 +22,20 @@ #include #include #include -#include #include "AppUtils.h" #include "BitmapView.h" +#include "Captcha.h" #include "HaikuDepotConstants.h" #include "LanguageMenuUtils.h" #include "LinkView.h" +#include "Logger.h" #include "Model.h" +#include "ServerHelper.h" #include "TabView.h" #include "UserUsageConditions.h" #include "UserUsageConditionsWindow.h" +#include "ValidationUtils.h" #include "WebAppInterface.h" @@ -41,63 +44,103 @@ #define PLACEHOLDER_TEXT B_UTF8_ELLIPSIS +#define KEY_USER_CREDENTIALS "userCredentials" +#define KEY_CAPTCHA_IMAGE "captchaImage" +#define KEY_USER_USAGE_CONDITIONS "userUsageConditions" +#define KEY_VALIDATION_FAILURES "validationFailures" + + enum ActionTabs { - TAB_LOGIN = 0, - TAB_CREATE_ACCOUNT = 1 + TAB_LOGIN = 0, + TAB_CREATE_ACCOUNT = 1 }; + enum { - MSG_SEND = 'send', - MSG_TAB_SELECTED = 'tbsl', - MSG_CAPTCHA_OBTAINED = 'cpob', - MSG_VALIDATE_FIELDS = 'vldt' + 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' }; -/*! The creation of an account requires that some prerequisite data is first - loaded in or may later need to be refreshed. This enum controls what - elements of the setup should be performed. + +/*! The creation of an account requires that some prerequisite data is first + 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 + CREATE_CAPTCHA = 1 << 1, + FETCH_USER_USAGE_CONDITIONS = 1 << 2 }; -/*! A background thread runs to gather data to use in the interface for creating - a new user. This structure is passed to the background thread. + +/*! To create a user, some details need to be provided. Those details together + with a pointer to the window structure are provided to the background thread + using this struct. +*/ + +struct CreateAccountThreadData { + UserLoginWindow* window; + CreateUserDetail* detail; +}; + + +/*! A background thread runs to gather data to use in the interface for creating + a new user. This structure is passed to the background thread. */ struct CreateAccountSetupThreadData { - UserLoginWindow* window; - uint32 mask; + UserLoginWindow* window; + uint32 mask; // defines what setup steps are required }; +/*! A background thread runs to authenticate the user with the remote server + 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_NOT_RESIZABLE | B_NOT_ZOOMABLE | B_CLOSE_ON_ESCAPE), + fUserUsageConditions(NULL), + fCaptcha(NULL), fPreferredLanguageCode(LANGUAGE_DEFAULT_CODE), fModel(model), fMode(NONE), - fUserUsageConditions(NULL), - fWorkerThread(-1) + fWorkerThread(-1), + fQuitRequestedDuringWorkerThread(false) { AddToSubset(parent); - fUsernameField = new BTextControl(B_TRANSLATE("User name:"), "", NULL); - fPasswordField = new BTextControl(B_TRANSLATE("Pass phrase:"), "", NULL); + fNicknameField = new BTextControl(B_TRANSLATE("Nickname:"), "", NULL); + fPasswordField = new BTextControl(B_TRANSLATE("Password:"), "", NULL); fPasswordField->TextView()->HideTyping(true); - fNewUsernameField = new BTextControl(B_TRANSLATE("User name:"), "", + fNewNicknameField = new BTextControl(B_TRANSLATE("Nickname:"), "", NULL); - fNewPasswordField = new BTextControl(B_TRANSLATE("Pass phrase:"), "", + fNewPasswordField = new BTextControl(B_TRANSLATE("Password:"), "", new BMessage(MSG_VALIDATE_FIELDS)); fNewPasswordField->TextView()->HideTyping(true); - fRepeatPasswordField = new BTextControl(B_TRANSLATE("Repeat pass phrase:"), + fRepeatPasswordField = new BTextControl(B_TRANSLATE("Repeat password:"), "", new BMessage(MSG_VALIDATE_FIELDS)); fRepeatPasswordField->TextView()->HideTyping(true); @@ -139,7 +182,7 @@ UserLoginWindow::UserLoginWindow(BWindow* parent, BRect frame, Model& model) // Setup modification messages on all text fields to trigger validation // of input - fNewUsernameField->SetModificationMessage( + fNewNicknameField->SetModificationMessage( new BMessage(MSG_VALIDATE_FIELDS)); fNewPasswordField->SetModificationMessage( new BMessage(MSG_VALIDATE_FIELDS)); @@ -154,7 +197,7 @@ UserLoginWindow::UserLoginWindow(BWindow* parent, BRect frame, Model& model) BGridView* loginCard = new BGridView(B_TRANSLATE("Log in")); BLayoutBuilder::Grid<>(loginCard) - .AddTextControl(fUsernameField, 0, 0) + .AddTextControl(fNicknameField, 0, 0) .AddTextControl(fPasswordField, 0, 1) .AddGlue(0, 2) @@ -164,7 +207,7 @@ UserLoginWindow::UserLoginWindow(BWindow* parent, BRect frame, Model& model) BGridView* createAccountCard = new BGridView(B_TRANSLATE("Create account")); BLayoutBuilder::Grid<>(createAccountCard) - .AddTextControl(fNewUsernameField, 0, 0) + .AddTextControl(fNewNicknameField, 0, 0) .AddTextControl(fNewPasswordField, 0, 1) .AddTextControl(fRepeatPasswordField, 0, 2) .AddTextControl(fEmailField, 0, 3) @@ -216,7 +259,7 @@ UserLoginWindow::MessageReceived(BMessage* message) { switch (message->what) { case MSG_VALIDATE_FIELDS: - _ValidateCreateAccountFields(); + _MarkCreateUserInvalidFields(); break; case MSG_VIEW_LATEST_USER_USAGE_CONDITIONS: @@ -226,7 +269,7 @@ UserLoginWindow::MessageReceived(BMessage* message) case MSG_SEND: switch (fMode) { case LOGIN: - _Login(); + _Authenticate(); break; case CREATE_ACCOUNT: _CreateAccount(); @@ -254,23 +297,65 @@ UserLoginWindow::MessageReceived(BMessage* message) break; } - case MSG_CAPTCHA_OBTAINED: - if (fCaptchaImage.Get() != NULL) { - fCaptchaView->SetBitmap(fCaptchaImage); - } else { - fCaptchaView->UnsetBitmap(); - } - fCaptchaResultField->SetText(""); + case MSG_CREATE_ACCOUNT_SETUP_ERROR: + printf("failed to setup for account setup - window must quit\n"); + BMessenger(this).SendMessage(B_QUIT_REQUESTED); break; - case MSG_USER_USAGE_CONDITIONS_DATA: - _SetUserUsageConditions(new UserUsageConditions(message)); + case MSG_CREATE_ACCOUNT_SETUP_SUCCESS: + _HandleCreateAccountSetupSuccess(message); break; case MSG_LANGUAGE_SELECTED: message->FindString("code", &fPreferredLanguageCode); 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; @@ -278,6 +363,23 @@ UserLoginWindow::MessageReceived(BMessage* message) } +bool +UserLoginWindow::QuitRequested() +{ + BAutolock locker(&fLock); + + if (fWorkerThread >= 0) { + if (Logger::IsDebugEnabled()) + printf("quit requested while worker thread is operating -- will " + "try again once the worker thread has completed\n"); + fQuitRequestedDuringWorkerThread = true; + return false; + } + + return true; +} + + void UserLoginWindow::SetOnSuccessMessage( const BMessenger& messenger, const BMessage& message) @@ -287,6 +389,24 @@ UserLoginWindow::SetOnSuccessMessage( } +void +UserLoginWindow::_EnableMutableControls(bool enabled) +{ + fNicknameField->SetEnabled(enabled); + fPasswordField->SetEnabled(enabled); + fNewNicknameField->SetEnabled(enabled); + fNewPasswordField->SetEnabled(enabled); + fRepeatPasswordField->SetEnabled(enabled); + fEmailField->SetEnabled(enabled); + fLanguageCodeField->SetEnabled(enabled); + fCaptchaResultField->SetEnabled(enabled); + fConfirmMinimumAgeCheckBox->SetEnabled(enabled); + fConfirmUserUsageConditionsCheckBox->SetEnabled(enabled); + fUserUsageConditionsLink->SetEnabled(enabled); + fSendButton->SetEnabled(enabled); +} + + void UserLoginWindow::_SetMode(Mode mode) { @@ -299,14 +419,14 @@ UserLoginWindow::_SetMode(Mode mode) case LOGIN: fTabView->Select(TAB_LOGIN); fSendButton->SetLabel(B_TRANSLATE("Log in")); - fUsernameField->MakeFocus(); + fNicknameField->MakeFocus(); break; case CREATE_ACCOUNT: fTabView->Select(TAB_CREATE_ACCOUNT); fSendButton->SetLabel(B_TRANSLATE("Create account")); _CreateAccountSetupIfNecessary(); - fNewUsernameField->MakeFocus(); - _ValidateCreateAccountFields(); + fNewNicknameField->MakeFocus(); + _MarkCreateUserInvalidFields(); break; default: break; @@ -314,179 +434,236 @@ UserLoginWindow::_SetMode(Mode mode) } -static int32 -count_digits(const BString& string) +void +UserLoginWindow::_SetWorkerThreadLocked(thread_id thread) { - 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; -} - - -static bool -contains_any_whitespace(const BString& string) -{ - const char* c = string.String(); - for (int32 i = 0; i < string.CountChars(); i++) { - if (isspace(c[i])) - return true; - } - - return false; -} - - -/*! This method will check that the inputs in the user interface are, as far as - the desktop application can be sure, correct. - \return true if the data in the form was valid. -*/ - -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()); - bool minimumAgeConfirmed = fConfirmMinimumAgeCheckBox->Value() - == B_CONTROL_ON; - bool userUsageConditionsConfirmed = - fConfirmUserUsageConditionsCheckBox->Value() == B_CONTROL_ON; - - // TODO: Use the same validation as the web-serivce - - bool validEmail = email.IsEmpty() || - (B_ERROR != email.FindFirst("@") && !contains_any_whitespace(email)); - fEmailField->MarkAsInvalid(!validEmail); - - 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 && minimumAgeConfirmed && userUsageConditionsConfirmed - && validEmail; - - if (!valid && alertProblems) { - BString message = B_TRANSLATE("There are problems in the form:\n\n"); - - 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"; - } - - if (!minimumAgeConfirmed) { - message << B_TRANSLATE( - "The minimum age requirements must be met.") << "\n\n"; - } - - if (!userUsageConditionsConfirmed) { - message << B_TRANSLATE( - "The usage conditions must be agreed to.") << "\n\n"; - } - - BAlert* alert = new(std::nothrow) BAlert( - B_TRANSLATE("Input validation"), - message, - B_TRANSLATE("OK"), NULL, NULL, - B_WIDTH_AS_USUAL, B_WARNING_ALERT); - - if (alert != NULL) - alert->Go(); - } - - return valid; + BAutolock locker(&fLock); + _SetWorkerThread(thread); } void -UserLoginWindow::_Login() +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; + } +} + + +// #pragma mark - Authentication + + +void +UserLoginWindow::_Authenticate() +{ + _Authenticate(UserCredentials( + fNicknameField->Text(), fPasswordField->Text())); +} + + +void +UserLoginWindow::_Authenticate(const UserCredentials& credentials) { BAutolock locker(&fLock); if (fWorkerThread >= 0) return; + _EnableMutableControls(false); + AuthenticateSetupThreadData* threadData = new AuthenticateSetupThreadData(); + // this will be owned and deleted by the thread + threadData->window = this; + threadData->credentials = new UserCredentials(credentials); + thread_id thread = spawn_thread(&_AuthenticateThreadEntry, - "Authenticator", B_NORMAL_PRIORITY, this); + "Authentication", B_NORMAL_PRIORITY, threadData); if (thread >= 0) _SetWorkerThread(thread); } +/*static*/ int32 +UserLoginWindow::_AuthenticateThreadEntry(void* data) +{ + AuthenticateSetupThreadData* threadData + = static_cast(data); + threadData->window->_AuthenticateThread(*(threadData->credentials)); + threadData->window->_SetWorkerThreadLocked(-1); + delete threadData->credentials; + delete threadData; + return 0; +} + + void -UserLoginWindow::_CreateAccount() +UserLoginWindow::_AuthenticateThread(UserCredentials& userCredentials) { - if (!_ValidateCreateAccountFields(true)) - return; + BMessage responsePayload; + WebAppInterface interface = fModel.GetWebAppInterface(); + status_t status = interface.AuthenticateUser( + userCredentials.Nickname(), userCredentials.PasswordClear(), + responsePayload); + BString token; - BAutolock locker(&fLock); + if (status == B_OK) { + int32 errorCode = interface.ErrorCodeFromResponse(responsePayload); - if (fWorkerThread >= 0) - return; + if (errorCode == ERROR_CODE_NONE) + _UnpackAuthenticationToken(responsePayload, token); + else { + ServerHelper::NotifyServerJsonRpcError(responsePayload); + BMessenger(this).SendMessage(MSG_LOGIN_ERROR); + return; + // early exit + } + } - thread_id thread = spawn_thread(&_CreateAccountThreadEntry, - "Account creator", B_NORMAL_PRIORITY, this); - if (thread >= 0) - _SetWorkerThread(thread); + if (status == B_OK) { + userCredentials.SetIsSuccessful(!token.IsEmpty()); + + if (Logger::IsDebugEnabled()) { + if (token.IsEmpty()) + printf("authentication failed\n"); + else + printf("authentication successful\n"); + } + + 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); + // 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. + } +} + + +/*! This method gets hit when an error occurs while authenticating; something + 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 was" + " supplied. Check your credentials and try again.") + ); + fPasswordField->SetText(""); + _EnableMutableControls(true); +} + + +/*! This is called when the user has successfully authenticated with the remote + 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); +} + + +/*! This method will fire any configured target + message, will set the + 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) +{ + { + AutoLocker locker(fModel.Lock()); + fModel.SetAuthorization(credentials.Nickname(), + credentials.PasswordClear(), true); + } + + // Clone these fields before the window goes away. + BMessenger onSuccessTarget(fOnSuccessTarget); + BMessage onSuccessMessage(fOnSuccessMessage); + + BMessenger(this).SendMessage(B_QUIT_REQUESTED); + + // 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); +} + + +// #pragma mark - Create Account Setup + + +/*! This method will trigger the process of gathering the data from the server + 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 (fCaptchaToken.IsEmpty()) + if (fCaptcha == NULL) setupMask |= CREATE_CAPTCHA; if (fUserUsageConditions == NULL) setupMask |= FETCH_USER_USAGE_CONDITIONS; @@ -494,6 +671,10 @@ UserLoginWindow::_CreateAccountSetupIfNecessary() } +/*! Fetches the data required for creating an account. + \param mask describes what data is required to be fetched. +*/ + void UserLoginWindow::_CreateAccountSetup(uint32 mask) { @@ -508,17 +689,12 @@ UserLoginWindow::_CreateAccountSetup(uint32 mask) if (!Lock()) debugger("unable to lock the user login window"); - if ((mask & CREATE_CAPTCHA) != 0) { - fCaptchaToken = ""; - fCaptchaView->UnsetBitmap(); - fCaptchaImage.Unset(); - } + _EnableMutableControls(false); - if ((mask & FETCH_USER_USAGE_CONDITIONS) != 0) { - fConfirmMinimumAgeCheckBox->SetLabel(PLACEHOLDER_TEXT); - fConfirmMinimumAgeCheckBox->SetValue(0); - fConfirmUserUsageConditionsCheckBox->SetValue(0); - } + if ((mask & CREATE_CAPTCHA) != 0) + _SetCaptcha(NULL); + if ((mask & FETCH_USER_USAGE_CONDITIONS) != 0) + _SetUserUsageConditions(NULL); Unlock(); @@ -529,7 +705,7 @@ UserLoginWindow::_CreateAccountSetup(uint32 mask) thread_id thread = spawn_thread(&_CreateAccountSetupThreadEntry, "Create account setup", B_NORMAL_PRIORITY, threadData); if (thread >= 0) - _SetWorkerThread(thread); + _SetWorkerThreadLocked(thread); else { debugger("unable to start a thread to gather data for creating an " "account"); @@ -537,376 +713,642 @@ UserLoginWindow::_CreateAccountSetup(uint32 mask) } -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); - fConfirmMinimumAgeCheckBox->SetEnabled(enabled); - fConfirmUserUsageConditionsCheckBox->SetEnabled(enabled); - fUserUsageConditionsLink->SetEnabled(enabled); - fSendButton->SetEnabled(enabled); - - if (thread >= 0) { - fWorkerThread = thread; - resume_thread(fWorkerThread); - } else { - fWorkerThread = -1; - } - - Unlock(); -} - - -void -UserLoginWindow::_CreateAccountUserUsageConditionsSetupThread() -{ - UserUsageConditions conditions; - WebAppInterface interface = fModel.GetWebAppInterface(); - - if (interface.RetrieveUserUsageConditions(NULL, conditions) == B_OK) { - BMessage dataMessage(MSG_USER_USAGE_CONDITIONS_DATA); - conditions.Archive(&dataMessage, true); - BMessenger(this).SendMessage(&dataMessage); - } else { - AppUtils::NotifySimpleError( - B_TRANSLATE("Usage conditions download problem"), - B_TRANSLATE("An error has arisen downloading the usage " - "conditions. Check the log for details and try again.")); - BMessenger(this).SendMessage(B_QUIT_REQUESTED); - } -} - - -/*! This method is hit when the user usage conditions data arrives back from the - server. At this point some of the UI elements may need to be updated. -*/ - -void -UserLoginWindow::_SetUserUsageConditions( - UserUsageConditions* userUsageConditions) -{ - fUserUsageConditions = userUsageConditions; - BString minimumAgeString; - minimumAgeString.SetToFormat("%" B_PRId8, - fUserUsageConditions->MinimumAge()); - BString label = B_TRANSLATE( - "I am %MinimumAgeYears% years of age or older"); - label.ReplaceAll("%MinimumAgeYears%", minimumAgeString); - fConfirmMinimumAgeCheckBox->SetLabel(label); -} - - -int32 -UserLoginWindow::_AuthenticateThreadEntry(void* data) -{ - UserLoginWindow* window = reinterpret_cast(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::_CreateAccountSetupThreadEntry(void* data) { CreateAccountSetupThreadData* threadData = - reinterpret_cast(data); - if ((threadData->mask & CREATE_CAPTCHA) != 0) - threadData->window->_CreateAccountCaptchaSetupThread(); - if ((threadData->mask & FETCH_USER_USAGE_CONDITIONS) != 0) - threadData->window->_CreateAccountUserUsageConditionsSetupThread(); - threadData->window->_SetWorkerThread(-1); + static_cast(data); + BMessenger messenger(threadData->window); + status_t result = B_OK; + Captcha captcha; + UserUsageConditions userUsageConditions; + bool shouldCreateCaptcha = (threadData->mask & CREATE_CAPTCHA) != 0; + bool shouldFetchUserUsageConditions + = (threadData->mask & FETCH_USER_USAGE_CONDITIONS) != 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) { + 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) { + if (Logger::IsDebugEnabled()) + printf("successfully completed collection of create account " + "data from the server in background thread\n"); + messenger.SendMessage(&message); + } else { + debugger("unable to configure the " + "'MSG_CREATE_ACCOUNT_SETUP_SUCCESS' message."); + } + } + + if (result != B_OK) { + // any error messages / alerts should have already been handled by this + // point. + messenger.SendMessage(MSG_CREATE_ACCOUNT_SETUP_ERROR); + } + + threadData->window->_SetWorkerThreadLocked(-1); delete threadData; return 0; } -void -UserLoginWindow::_CreateAccountCaptchaSetupThread() +status_t +UserLoginWindow::_CreateAccountUserUsageConditionsSetupThread( + UserUsageConditions& userUsageConditions) { - WebAppInterface interface; - BMessage info; + WebAppInterface interface = fModel.GetWebAppInterface(); + status_t result = interface.RetrieveUserUsageConditions( + NULL, userUsageConditions); - status_t status = interface.RequestCaptcha(info); + 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.")); + } - BAutolock locker(&fLock); + return result; +} - 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; - } + +status_t +UserLoginWindow::_CreateAccountCaptchaSetupThread(Captcha& captcha) +{ + WebAppInterface interface = fModel.GetWebAppInterface(); + BMessage responsePayload; + + status_t status = interface.RequestCaptcha(responsePayload); + +// check for transport related errors. + + 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.")); + } + +// check for server-generated errors. + + if (status == B_OK) { + if (interface.ErrorCodeFromResponse(responsePayload) + != ERROR_CODE_NONE) { + ServerHelper::AlertTransportError(&responsePayload); + status = B_ERROR; } + } + +// now parse the response from the server and extract the captcha data. + + 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; +} + + +/*! Takes the data returned to the client after it was requested from the + 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; + ssize_t decodedSize; + if (result == B_OK) { + encodedSize = pngImageDataBase64.Length(); + decodedSize = (encodedSize * 3 + 3) / 4; + if (decodedSize <= 0) + result = B_ERROR; + } + + 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; + } + + return result; +} + + +void +UserLoginWindow::_HandleCreateAccountSetupSuccess(BMessage* message) +{ + if (Logger::IsDebugEnabled()) + printf("handling account setup success\n"); + + BMessage captchaMessage; + BMessage userUsageConditionsMessage; + + 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)); + } + + _EnableMutableControls(true); +} + + +void +UserLoginWindow::_SetCaptcha(Captcha* captcha) +{ + if (Logger::IsDebugEnabled()) + printf("setting captcha\n"); + if (fCaptcha != NULL) + delete fCaptcha; + fCaptcha = captcha; + + if (fCaptcha == NULL) + fCaptchaView->UnsetBitmap(); + else { + off_t size; + fCaptcha->PngImageData()->GetSize(&size); + SharedBitmap* captchaImage + = new SharedBitmap(*(fCaptcha->PngImageData())); + fCaptchaView->SetBitmap(captchaImage); + } + fCaptchaResultField->SetText(""); +} + + +/*! This method is hit when the user usage conditions data arrives back from the + server. At this point some of the UI elements may need to be updated. +*/ + +void +UserLoginWindow::_SetUserUsageConditions( + UserUsageConditions* userUsageConditions) +{ + if (Logger::IsDebugEnabled()) + printf("setting user usage conditions\n"); + if (fUserUsageConditions != NULL) + delete fUserUsageConditions; + fUserUsageConditions = userUsageConditions; + + if (fUserUsageConditions != NULL) { + BString minimumAgeString; + minimumAgeString.SetToFormat("%" B_PRId8, + fUserUsageConditions->MinimumAge()); + BString label = B_TRANSLATE( + "I am %MinimumAgeYears% years of age or older"); + label.ReplaceAll("%MinimumAgeYears%", minimumAgeString); + fConfirmMinimumAgeCheckBox->SetLabel(label); } else { - fprintf(stderr, "Failed to obtain captcha: %s\n", strerror(status)); + fConfirmMinimumAgeCheckBox->SetLabel(PLACEHOLDER_TEXT); + fConfirmMinimumAgeCheckBox->SetValue(0); + fConfirmUserUsageConditionsCheckBox->SetValue(0); } } -int32 -UserLoginWindow::_CreateAccountThreadEntry(void* data) -{ - UserLoginWindow* window = reinterpret_cast(data); - window->_CreateAccountThread(); - return 0; -} +// #pragma mark - Create Account void -UserLoginWindow::_CreateAccountThread() +UserLoginWindow::_CreateAccount() { - if (!Lock()) + 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; - if (fUserUsageConditions == NULL) - debugger("missing usage conditions when creating an account"); + CreateUserDetail* detail = new CreateUserDetail(); + ValidationFailures validationFailures; - if (fConfirmMinimumAgeCheckBox->Value() == 0 - || fConfirmUserUsageConditionsCheckBox->Value() == 0) { - debugger("expected that the minimum age and usage conditions are " - "agreed to at this point"); + _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); + } +} + + +/*! Take the data from the user interface and put it into a model object to be + 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.SetLanguageCode(fPreferredLanguageCode); + + if ( fUserUsageConditions != NULL + && fConfirmMinimumAgeCheckBox->Value() == 1 + && fConfirmUserUsageConditionsCheckBox->Value() == 1) { + detail.SetAgreedToUserUsageConditionsCode(fUserUsageConditions->Code()); + } +} + + +/*! This method will check the data supplied in the detail and will relay any + 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"); } - BString nickName(fNewUsernameField->Text()); - BString passwordClear(fNewPasswordField->Text()); - BString email(fEmailField->Text()); - BString captchaToken(fCaptchaToken); - BString captchaResponse(fCaptchaResultField->Text()); - BString languageCode(fPreferredLanguageCode); - BString userUsageConditionsCode(fUserUsageConditions->Code()); + if (detail.PasswordClear().IsEmpty()) + failures.AddFailure("passwordClear", "required"); + else { + if (!ValidationUtils::IsValidPasswordClear(detail.PasswordClear())) + failures.AddFailure("passwordClear", "invalid"); + } - Unlock(); + if (!detail.IsPasswordRepeated()) + failures.AddFailure("repeatPasswordClear", "repeat"); - WebAppInterface interface; - BMessage info; + if (detail.AgreedToUserUsageConditionsCode().IsEmpty()) + failures.AddFailure("agreedToUserUsageConditionsCode", "required"); - status_t status = interface.CreateUser( - nickName, passwordClear, email, captchaToken, captchaResponse, - languageCode, userUsageConditionsCode, info); + if (detail.CaptchaResponse().IsEmpty()) + failures.AddFailure("captchaResponse", "required"); +} - BAutolock locker(&fLock); - BString error = B_TRANSLATE( - "There was a puzzling response from the web service."); +void +UserLoginWindow::_MarkCreateUserInvalidFields() +{ + CreateUserDetail detail; + ValidationFailures failures; + _AssembleCreateUserDetail(detail); + _ValidateCreateUserDetail(detail, failures); + _MarkCreateUserInvalidFields(failures); +} - 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; - } + +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'; } } - } 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_TRANSLATE("Input validation"), + alertMessage, + B_TRANSLATE("OK"), 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); +/*! This method produces a debug string for a set of validation failures. + */ - // We need a new captcha, it can be used only once - fCaptchaToken = ""; - _CreateAccountSetup(CREATE_CAPTCHA); +/*static*/ 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); + } + } +} + + +/*static*/ 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; +} + + +/*! This is the entry-point for the thread that will process the data to create + the new account. +*/ + +int32 +UserLoginWindow::_CreateAccountThreadEntry(void* data) +{ + CreateAccountThreadData* threadData = + static_cast(data); + threadData->window->_CreateAccountThread(threadData->detail); + threadData->window->_SetWorkerThreadLocked(-1); + if (NULL != threadData->detail) + delete threadData->detail; + return 0; +} + + +/*! This method runs in a background thread run and makes the necessary calls + to the application server to actually create the user. +*/ + +void +UserLoginWindow::_CreateAccountThread(CreateUserDetail* detail) +{ + WebAppInterface interface = fModel.GetWebAppInterface(); + BMessage responsePayload; + BMessenger messenger(this); + + status_t status = interface.CreateUser( + detail->Nickname(), + detail->PasswordClear(), + detail->Email(), + detail->CaptchaToken(), + detail->CaptchaResponse(), + detail->LanguageCode(), + detail->AgreedToUserUsageConditionsCode(), + responsePayload); + + BString error = B_TRANSLATE( + "There was a puzzling response from the web service."); + + if (status == B_OK) { + int32 errorCode = interface.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); + printf("create account validation issues; %s\n", + 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 { - fModel.SetAuthorization(nickName, passwordClear, true); - - _SetWorkerThread(-1); - _LoginSuccessful(B_TRANSLATE("Account created successfully. " - "You can now rate packages and do other useful things.")); + 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::_CollectValidationFailures(const BMessage& result, - BString& error) const +UserLoginWindow::_HandleCreateAccountSuccess( + const UserCredentials& credentials) { - error = B_TRANSLATE("There are problems with the data you entered:\n\n"); + 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()); - bool found = false; + BAlert* alert = new(std::nothrow) BAlert( + B_TRANSLATE("User Created"), message, B_TRANSLATE("Close")); - 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; + if (alert != NULL) + alert->Go(); - 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."); - } + _TakeUpCredentialsAndQuit(credentials); } -/*! Opens a new window that shows the already downloaded user usage conditions. +void +UserLoginWindow::_HandleCreateAccountFailure(const ValidationFailures& failures) +{ + _MarkCreateUserInvalidFields(failures); + _AlertCreateUserValidationFailure(failures); + _EnableMutableControls(true); + + // if an attempt was made to the server then the captcha would have been + // used up and a new captcha is required. + _CreateAccountSetup(CREATE_CAPTCHA); +} + + +/*! Handles the main UI-thread processing for the situation where there was an + 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); +} + + +/*! Opens a new window that shows the already downloaded user usage conditions. */ void @@ -917,4 +1359,4 @@ UserLoginWindow::_ViewUserUsageConditions() UserUsageConditionsWindow* window = new UserUsageConditionsWindow( fModel, *fUserUsageConditions); window->Show(); -} \ No newline at end of file +} diff --git a/src/apps/haikudepot/ui/UserLoginWindow.h b/src/apps/haikudepot/ui/UserLoginWindow.h index 7cc1ee0147..2b50a81bf3 100644 --- a/src/apps/haikudepot/ui/UserLoginWindow.h +++ b/src/apps/haikudepot/ui/UserLoginWindow.h @@ -10,7 +10,10 @@ #include #include +#include "CreateUserDetail.h" #include "PackageInfo.h" +#include "UserCredentials.h" +#include "ValidationFailure.h" class BButton; @@ -19,6 +22,7 @@ class BMenuField; class BTabView; class BTextControl; class BitmapView; +class Captcha; class LinkView; class Model; class UserUsageConditions; @@ -30,6 +34,7 @@ public: Model& model); virtual ~UserLoginWindow(); + virtual bool QuitRequested(); virtual void MessageReceived(BMessage* message); void SetOnSuccessMessage( @@ -45,45 +50,85 @@ private: }; void _SetMode(Mode mode); - bool _ValidateCreateAccountFields( - bool alertProblems = false); - void _Login(); + void _SetWorkerThread(thread_id thread); + void _SetWorkerThreadLocked(thread_id thread); + + void _Authenticate(); + void _Authenticate( + const UserCredentials& credentials); + static int32 _AuthenticateThreadEntry(void* data); + void _AuthenticateThread( + UserCredentials& credentials); + void _UnpackAuthenticationToken( + BMessage& responsePayload, BString& token); + void _HandleAuthenticationFailed(); + void _HandleAuthenticationSuccess( + const UserCredentials & credentials); + void _HandleAuthenticationError(); + void _CreateAccount(); + void _AssembleCreateUserDetail( + CreateUserDetail& detail); + void _ValidateCreateUserDetail( + CreateUserDetail& detail, + ValidationFailures& failures); + void _AlertCreateUserValidationFailure( + const ValidationFailures& failures); + static BString _CreateAlertTextFromValidationFailure( + const BString& property, + const BString& message); + void _MarkCreateUserInvalidFields(); + void _MarkCreateUserInvalidFields( + const ValidationFailures& failures); + static int32 _CreateAccountThreadEntry(void* data); + void _CreateAccountThread(CreateUserDetail* detail); + void _HandleCreateAccountSuccess( + const UserCredentials& credentials); + void _HandleCreateAccountFailure( + const ValidationFailures& failures); + void _HandleCreateAccountError(); + void _CreateAccountSetup(uint32 mask); void _CreateAccountSetupIfNecessary(); - void _LoginSuccessful(const BString& message); - - void _SetWorkerThread(thread_id thread); - - static int32 _AuthenticateThreadEntry(void* data); - void _AuthenticateThread(); - static int32 _CreateAccountSetupThreadEntry(void* data); - void _CreateAccountCaptchaSetupThread(); - void _CreateAccountUserUsageConditionsSetupThread(); + status_t _CreateAccountCaptchaSetupThread( + Captcha& captcha); + status_t _CreateAccountUserUsageConditionsSetupThread( + UserUsageConditions& userUsageConditions); + status_t _UnpackCaptcha(BMessage& responsePayload, + Captcha& captcha); + void _HandleCreateAccountSetupSuccess( + BMessage* message); + void _SetCaptcha(Captcha* captcha); void _SetUserUsageConditions( UserUsageConditions* userUsageConditions); - static int32 _CreateAccountThreadEntry(void* data); - void _CreateAccountThread(); - void _CollectValidationFailures( const BMessage& result, BString& error) const; void _ViewUserUsageConditions(); + void _TakeUpCredentialsAndQuit( + const UserCredentials& credentials); + + void _EnableMutableControls(bool enabled); + + static void _ValidationFailuresToString( + const ValidationFailures& failures, + BString& output); + private: BMessenger fOnSuccessTarget; BMessage fOnSuccessMessage; BTabView* fTabView; - BTextControl* fUsernameField; + BTextControl* fNicknameField; BTextControl* fPasswordField; - BTextControl* fNewUsernameField; + BTextControl* fNewNicknameField; BTextControl* fNewPasswordField; BTextControl* fRepeatPasswordField; BTextControl* fEmailField; @@ -97,19 +142,18 @@ private: BButton* fSendButton; BButton* fCancelButton; - BString fCaptchaToken; - BitmapRef fCaptchaImage; + UserUsageConditions* + fUserUsageConditions; + Captcha* fCaptcha; BString fPreferredLanguageCode; Model& fModel; Mode fMode; - UserUsageConditions* - fUserUsageConditions; - BLocker fLock; thread_id fWorkerThread; + bool fQuitRequestedDuringWorkerThread; }; diff --git a/src/apps/haikudepot/util/ValidationUtils.cpp b/src/apps/haikudepot/util/ValidationUtils.cpp new file mode 100644 index 0000000000..ace3911530 --- /dev/null +++ b/src/apps/haikudepot/util/ValidationUtils.cpp @@ -0,0 +1,130 @@ +/* + * Copyright 2019, Andrew Lindesay . + * All rights reserved. Distributed under the terms of the MIT License. + */ + +#include "ValidationUtils.h" + +#include + +#include + + +#define MIN_LENGTH_NICKNAME 4 +#define MAX_LENGTH_NICKNAME 16 + +#define MIN_LENGTH_PASSWORD_CLEAR 8 +#define MIN_UPPER_PASSWORD_CLEAR 2 +#define MIN_DIGITS_PASSWORD_CLEAR 2 + +/*! 1 if the character would be suitable for use in an email address mailbox + or domain part. +*/ + +static int +hd_is_email_domain_or_mailbox_part(int c) +{ + if (0 == isspace(c) && c != 0x40) + return 1; + return 0; +} + + +/*! Returns true if the entire string is lower case alpha numeric. + */ + +static int +hd_is_lower_alnum(int c) +{ + if ((c >= 0x30 && c <= 0x39) || (c >= 0x60 && c <= 0x7a)) + return 1; + return 0; +} + + +static bool +hd_str_all_matches_fn(const BString& string, int (*hd_match_c)(int c)) +{ + const char* c = string.String(); + for (int32 i = 0; i < string.CountChars(); i++) { + if (0 == hd_match_c(c[i])) + return false; + } + + return true; +} + + +static int32 +hd_str_count_upper_case(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; +} + + +static int32 +hd_str_count_digit(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*/ bool +ValidationUtils::IsValidNickname(const BString& value) +{ + return hd_str_all_matches_fn(value, &hd_is_lower_alnum) + && value.CountChars() >= MIN_LENGTH_NICKNAME + && value.CountChars() <= MAX_LENGTH_NICKNAME; +} + + +/*! Email addresses are quite difficult to validate 100% correctly so go fairly + light on the enforcement here; it should be a string with an '@' symbol, + something either side of the '@' and there should be no whitespace. +*/ + +/*static*/ bool +ValidationUtils::IsValidEmail(const BString& value) +{ + const char* c = value.String(); + size_t len = strlen(c); + bool foundAt = false; + + for (size_t i = 0; i < len; i++) { + if (c[i] == 0x40 && !foundAt) { + if (i == 0 || i == len - 1) + return false; + foundAt = true; + } + else { + if (0 == hd_is_email_domain_or_mailbox_part(c[i])) + return false; + } + } + + return foundAt; +} + + +/*static*/ bool +ValidationUtils::IsValidPasswordClear(const BString& value) +{ + return value.Length() >= MIN_LENGTH_PASSWORD_CLEAR + && hd_str_count_digit(value) >= MIN_DIGITS_PASSWORD_CLEAR + && hd_str_count_upper_case(value) >= MIN_UPPER_PASSWORD_CLEAR; +} + diff --git a/src/apps/haikudepot/util/ValidationUtils.h b/src/apps/haikudepot/util/ValidationUtils.h new file mode 100644 index 0000000000..2a69452508 --- /dev/null +++ b/src/apps/haikudepot/util/ValidationUtils.h @@ -0,0 +1,20 @@ +/* + * Copyright 2019, Andrew Lindesay . + * All rights reserved. Distributed under the terms of the MIT License. + */ +#ifndef VALIDATION_UTILS_H +#define VALIDATION_UTILS_H + + +#include + + +class ValidationUtils { + +public: + static bool IsValidEmail(const BString& value); + static bool IsValidNickname(const BString& value); + static bool IsValidPasswordClear(const BString& value); +}; + +#endif // VALIDATION_UTILS_H diff --git a/src/tests/apps/haikudepot/HaikuDepotTestAddon.cpp b/src/tests/apps/haikudepot/HaikuDepotTestAddon.cpp index 7dd58757c6..d9b4d41cb2 100644 --- a/src/tests/apps/haikudepot/HaikuDepotTestAddon.cpp +++ b/src/tests/apps/haikudepot/HaikuDepotTestAddon.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2017, Andrew Lindesay, apl@lindesay.co.nz + * Copyright 2017-2019, Andrew Lindesay, apl@lindesay.co.nz * Distributed under the terms of the MIT License. */ @@ -8,6 +8,8 @@ #include "StandardMetaDataJsonEventListenerTest.h" #include "DumpExportRepositoryJsonListenerTest.h" +#include "ValidationFailureTest.h" +#include "ValidationUtilsTest.h" #include "ListTest.h" BTestSuite* @@ -17,6 +19,8 @@ getTestSuite() StandardMetaDataJsonEventListenerTest::AddTests(*suite); DumpExportRepositoryJsonListenerTest::AddTests(*suite); + ValidationFailureTest::AddTests(*suite); + ValidationUtilsTest::AddTests(*suite); ListTest::AddTests(*suite); return suite; diff --git a/src/tests/apps/haikudepot/Jamfile b/src/tests/apps/haikudepot/Jamfile index 0606651748..d786a428c3 100644 --- a/src/tests/apps/haikudepot/Jamfile +++ b/src/tests/apps/haikudepot/Jamfile @@ -3,14 +3,18 @@ SubDir HAIKU_TOP src tests apps haikudepot ; AddSubDirSupportedPlatforms libbe_test ; SubDirHdrs [ FDirName $(HAIKU_TOP) src apps haikudepot ] ; +SubDirHdrs [ FDirName $(HAIKU_TOP) src apps haikudepot model ] ; SubDirHdrs [ FDirName $(HAIKU_TOP) src apps haikudepot server ] ; SubDirHdrs [ FDirName $(HAIKU_TOP) src apps haikudepot server dumpexportrepository ] ; +SubDirHdrs [ FDirName $(HAIKU_TOP) src apps haikudepot util ] ; UsePrivateHeaders shared ; local sourceDirs = + model server server/dumpexportrepository + util ; local sourceDir ; @@ -37,5 +41,11 @@ UnitTestLib haikudepottest.so : StandardMetaDataJsonEventListener.cpp StandardMetaDataJsonEventListenerTest.cpp + ValidationFailure.cpp + ValidationFailureTest.cpp + + ValidationUtils.cpp + ValidationUtilsTest.cpp + : be shared bnetapi package [ TargetLibstdc++ ] [ TargetLibsupc++ ] ; \ No newline at end of file diff --git a/src/tests/apps/haikudepot/ValidationFailureTest.cpp b/src/tests/apps/haikudepot/ValidationFailureTest.cpp new file mode 100644 index 0000000000..f5d7db86cb --- /dev/null +++ b/src/tests/apps/haikudepot/ValidationFailureTest.cpp @@ -0,0 +1,165 @@ +/* + * Copyright 2019, Andrew Lindesay . + * All rights reserved. Distributed under the terms of the MIT License. + */ + +#include "ValidationFailureTest.h" + +#include +#include + +#include "ValidationFailure.h" + + +ValidationFailureTest::ValidationFailureTest() +{ +} + + +ValidationFailureTest::~ValidationFailureTest() +{ +} + + +void +ValidationFailureTest::TestDearchive() +{ + BMessage nicknameMessage; + nicknameMessage.AddString("property", "nickname"); + nicknameMessage.AddString("message_0", "malformed"); + nicknameMessage.AddString("message_1", "required"); + + BMessage passwordClearMessage; + passwordClearMessage.AddString("property", "passwordClear"); + passwordClearMessage.AddString("message_0", "required"); + + BMessage validationFailuresMessage; + validationFailuresMessage.AddMessage("item_0", &nicknameMessage); + validationFailuresMessage.AddMessage("item_1", &passwordClearMessage); + +// ---------------------- + ValidationFailures validationFailures(&validationFailuresMessage); +// ---------------------- + + CPPUNIT_ASSERT_EQUAL_MESSAGE("!Count failures", (int32) 2, + validationFailures.CountFailures()); + CPPUNIT_ASSERT_EQUAL_MESSAGE("!Contains 'nickname'", true, + validationFailures.Contains("nickname")); + CPPUNIT_ASSERT_EQUAL_MESSAGE("!Contains 'nickname:required'", true, + validationFailures.Contains("nickname", "required")); + CPPUNIT_ASSERT_EQUAL_MESSAGE("!Contains 'nickname:malformed'", true, + validationFailures.Contains("nickname", "malformed")); + CPPUNIT_ASSERT_EQUAL_MESSAGE("!Contains 'passwordClear:required'", true, + validationFailures.Contains("passwordClear", "required")); +} + + +void +ValidationFailureTest::TestArchive() +{ + ValidationFailures failures; + failures.AddFailure("nickname", "malformed"); + failures.AddFailure("nickname", "required"); + failures.AddFailure("passwordClear", "required"); + BMessage validationFailuresMessage; + +// ---------------------- + status_t archiveResult = failures.Archive(&validationFailuresMessage); +// ---------------------- + + CPPUNIT_ASSERT_EQUAL_MESSAGE("!Archive failure", B_OK, archiveResult); + BMessage validationFailureNicknameMessage; + BMessage validationFailurePasswordClearMessage; + + CPPUNIT_ASSERT_EQUAL_MESSAGE("!Unable to find 'nickname'", B_OK, + FindMessageWithProperty("nickname", validationFailuresMessage, + validationFailureNicknameMessage)); + CPPUNIT_ASSERT_EQUAL_MESSAGE("!Unable to find 'passwordClear'", B_OK, + FindMessageWithProperty("passwordClear", validationFailuresMessage, + validationFailurePasswordClearMessage)); + + BStringList validationFailureMessagesNickname; + BStringList validationFailureMessagesPasswordClear; + FindValidationMessages(validationFailureNicknameMessage, + validationFailureMessagesNickname); + FindValidationMessages(validationFailurePasswordClearMessage, + validationFailureMessagesPasswordClear); + + CPPUNIT_ASSERT_EQUAL_MESSAGE("!Unable to find 'nickname:malformed'", + true, validationFailureMessagesNickname.HasString("malformed")); + CPPUNIT_ASSERT_EQUAL_MESSAGE("!Unable to find 'nickname:required'", + true, validationFailureMessagesNickname.HasString("required")); + CPPUNIT_ASSERT_EQUAL_MESSAGE("!Unexpected validation messages 'nickname'", + (int32) 2, validationFailureMessagesNickname.CountStrings()); + + CPPUNIT_ASSERT_EQUAL_MESSAGE("!Unable to find 'passwordClear:required'", + true, validationFailureMessagesPasswordClear.HasString("required")); + CPPUNIT_ASSERT_EQUAL_MESSAGE("!Unexpected validation messages 'nickname'", + (int32) 1, validationFailureMessagesPasswordClear.CountStrings()); +} + + +/*static*/ void +ValidationFailureTest::AddTests(BTestSuite& parent) +{ + CppUnit::TestSuite& suite = *new CppUnit::TestSuite( + "ValidationFailureTest"); + + suite.addTest( + new CppUnit::TestCaller( + "ValidationFailureTest::TestArchive", + &ValidationFailureTest::TestArchive)); + suite.addTest( + new CppUnit::TestCaller( + "ValidationFailureTest::TestDearchive", + &ValidationFailureTest::TestDearchive)); + + parent.addTest("ValidationFailureTest", &suite); +} + + +/*static*/ status_t +ValidationFailureTest::FindMessageWithProperty( + const char* property, BMessage& validationFailuresMessage, + BMessage& validationFailureMessage) +{ + status_t result = B_OK; + + for (int32 i = 0; result == B_OK; i++) { + BString name = "item_"; + name << i; + result = validationFailuresMessage.FindMessage(name, + &validationFailureMessage); + + if (result == B_OK) { + BString messageProperty; + result = validationFailureMessage.FindString("property", + &messageProperty); + + if (result == B_OK && messageProperty == property) + return result; + } + } + + return result; +} + + +/*static*/ void +ValidationFailureTest::FindValidationMessages( + BMessage& validationFailureMessage, BStringList& validationMessages) +{ + status_t result = B_OK; + + for (int32 i = 0; result == B_OK; i++) { + BString validationMessage; + BString name = "message_"; + name << i; + result = validationFailureMessage.FindString(name, + &validationMessage); + + if (result == B_OK) { + validationMessages.Add(validationMessage); + } + } +} \ No newline at end of file diff --git a/src/tests/apps/haikudepot/ValidationFailureTest.h b/src/tests/apps/haikudepot/ValidationFailureTest.h new file mode 100644 index 0000000000..fd77a95f77 --- /dev/null +++ b/src/tests/apps/haikudepot/ValidationFailureTest.h @@ -0,0 +1,35 @@ +/* + * Copyright 2019, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ +#ifndef VALIDATION_FAILURE_TEST_H +#define VALIDATION_FAILURE_TEST_H + +#include "Message.h" + +#include +#include + + +class ValidationFailureTest : public CppUnit::TestCase { +public: + ValidationFailureTest(); + virtual ~ValidationFailureTest(); + + void TestArchive(); + void TestDearchive(); + + static void AddTests(BTestSuite& suite); + +private: + static status_t FindMessageWithProperty( + const char* property, + BMessage& validationFailuresMessage, + BMessage& validationFailureMessage); + static void FindValidationMessages( + BMessage& validationFailureMessage, + BStringList& validationMessages); +}; + + +#endif // VALIDATION_FAILURE_TEST_H diff --git a/src/tests/apps/haikudepot/ValidationUtilsTest.cpp b/src/tests/apps/haikudepot/ValidationUtilsTest.cpp new file mode 100644 index 0000000000..8d2fd2f856 --- /dev/null +++ b/src/tests/apps/haikudepot/ValidationUtilsTest.cpp @@ -0,0 +1,184 @@ +/* + * Copyright 2019, Andrew Lindesay . + * All rights reserved. Distributed under the terms of the MIT License. + */ + +#include "ValidationUtilsTest.h" + +#include +#include + +#include "ValidationUtils.h" + + +ValidationUtilsTest::ValidationUtilsTest() +{ +} + + +ValidationUtilsTest::~ValidationUtilsTest() +{ +} + + +void +ValidationUtilsTest::TestEmailValid() +{ + BString email("weta@example.com"); + +// ---------------------- + bool result = ValidationUtils::IsValidEmail(email); +// ---------------------- + + CPPUNIT_ASSERT_EQUAL_MESSAGE("!Email valid", true, result); +} + + + void TestEmailInvalidNoAt(); + void TestEmailInvalidNoMailbox(); + void TestEmailInvalidNoDomain(); + void TestEmailInvalidTwoAts(); + +void +ValidationUtilsTest::TestEmailInvalidNoAt() +{ + BString email("wetaexample.com"); + +// ---------------------- + bool result = ValidationUtils::IsValidEmail(email); +// ---------------------- + + CPPUNIT_ASSERT_EQUAL_MESSAGE("!Email invalid - no @", false, result); +} + + +void +ValidationUtilsTest::TestEmailInvalidNoMailbox() +{ + BString email("@example.com"); + +// ---------------------- + bool result = ValidationUtils::IsValidEmail(email); +// ---------------------- + + CPPUNIT_ASSERT_EQUAL_MESSAGE("!Email invalid - no mailbox", false, result); +} + + +void +ValidationUtilsTest::TestEmailInvalidNoDomain() +{ + BString email("fredric@"); + +// ---------------------- + bool result = ValidationUtils::IsValidEmail(email); +// ---------------------- + + CPPUNIT_ASSERT_EQUAL_MESSAGE("!Email invalid - no domain", false, result); +} + + +void +ValidationUtilsTest::TestNicknameValid() +{ + BString nickname("erik55"); + +// ---------------------- + bool result = ValidationUtils::IsValidNickname(nickname); +// ---------------------- + + CPPUNIT_ASSERT_EQUAL_MESSAGE("!Nickname valid", true, result); +} + + +void +ValidationUtilsTest::TestNicknameInvalid() +{ + BString nickname("not a Nickname!"); + +// ---------------------- + bool result = ValidationUtils::IsValidNickname(nickname); +// ---------------------- + + CPPUNIT_ASSERT_EQUAL_MESSAGE("!Nickname invalid", false, result); +} + + +void +ValidationUtilsTest::TestNicknameInvalidBadChars() +{ + BString nickname("erik!!10"); + +// ---------------------- + bool result = ValidationUtils::IsValidNickname(nickname); +// ---------------------- + + CPPUNIT_ASSERT_EQUAL_MESSAGE("!Nickname invalid (bad chars)", + false, result); +} + + +void +ValidationUtilsTest::TestPasswordClearValid() +{ + BString passwordClear("P4NhelQoad4"); + +// ---------------------- + bool result = ValidationUtils::IsValidPasswordClear(passwordClear); +// ---------------------- + + CPPUNIT_ASSERT_EQUAL_MESSAGE("!Password clear valid", true, result); +} + + +void +ValidationUtilsTest::TestPasswordClearInvalid() +{ + BString passwordClear("only has lower case letters"); + // needs some numbers / upper case characters in there too + +// ---------------------- + bool result = ValidationUtils::IsValidPasswordClear(passwordClear); +// ---------------------- + + CPPUNIT_ASSERT_EQUAL_MESSAGE("!Password clear invalid", false, result); +} + + +/*static*/ void +ValidationUtilsTest::AddTests(BTestSuite& parent) +{ + CppUnit::TestSuite& suite = *new CppUnit::TestSuite( + "ValidationUtilsTest"); + + suite.addTest( + new CppUnit::TestCaller( + "ValidationUtilsTest::TestEmailInvalid", + &ValidationUtilsTest::TestEmailInvalid)); + suite.addTest( + new CppUnit::TestCaller( + "ValidationUtilsTest::TestEmailValid", + &ValidationUtilsTest::TestEmailValid)); + suite.addTest( + new CppUnit::TestCaller( + "ValidationUtilsTest::TestNicknameInvalid", + &ValidationUtilsTest::TestNicknameInvalid)); + suite.addTest( + new CppUnit::TestCaller( + "ValidationUtilsTest::TestNicknameValid", + &ValidationUtilsTest::TestNicknameValid)); + suite.addTest( + new CppUnit::TestCaller( + "ValidationUtilsTest::TestNicknameInvalidBadChars", + &ValidationUtilsTest::TestNicknameInvalidBadChars)); + suite.addTest( + new CppUnit::TestCaller( + "ValidationUtilsTest::TestPasswordClearInvalid", + &ValidationUtilsTest::TestPasswordClearInvalid)); + suite.addTest( + new CppUnit::TestCaller( + "ValidationUtilsTest::TestPasswordClearValid", + &ValidationUtilsTest::TestPasswordClearValid)); + + parent.addTest("ValidationUtilsTest", &suite); +} \ No newline at end of file diff --git a/src/tests/apps/haikudepot/ValidationUtilsTest.h b/src/tests/apps/haikudepot/ValidationUtilsTest.h new file mode 100644 index 0000000000..352dde70cf --- /dev/null +++ b/src/tests/apps/haikudepot/ValidationUtilsTest.h @@ -0,0 +1,37 @@ +/* + * Copyright 2019, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ +#ifndef VALIDATION_UTILS_TEST_H +#define VALIDATION_UTILS_TEST_H + +#include "Message.h" + +#include +#include + + +class ValidationUtilsTest : public CppUnit::TestCase { +public: + ValidationUtilsTest(); + virtual ~ValidationUtilsTest(); + + void TestEmailValid(); + void TestEmailInvalidNoAt(); + void TestEmailInvalidNoMailbox(); + void TestEmailInvalidNoDomain(); + void TestEmailInvalidTwoAts(); + + void TestNicknameValid(); + void TestNicknameInvalid(); + void TestNicknameInvalidBadChars(); + + void TestPasswordClearValid(); + void TestPasswordClearInvalid(); + + static void AddTests(BTestSuite& suite); + +}; + + +#endif // VALIDATION_UTILS_TEST_H