2 * Copyright 2014, Stephan Aßmus <superstippi@gmx.de>.
3 * All rights reserved. Distributed under the terms of the MIT License.
6 #include "UserLoginWindow.h"
11 #include <mail_encoding.h>
17 #include <LayoutBuilder.h>
18 #include <MenuField.h>
19 #include <PopUpMenu.h>
21 #include <TextControl.h>
22 #include <UnicodeChar.h>
24 #include "BitmapView.h"
26 #include "WebAppInterface.h"
29 #undef B_TRANSLATION_CONTEXT
30 #define B_TRANSLATION_CONTEXT "UserLoginWindow"
35 MSG_TAB_SELECTED
= 'tbsl',
36 MSG_CAPTCHA_OBTAINED
= 'cpob',
37 MSG_VALIDATE_FIELDS
= 'vldt',
38 MSG_LANGUAGE_SELECTED
= 'lngs',
42 class TabView
: public BTabView
{
44 TabView(const BMessenger
& target
, const BMessage
& message
)
46 BTabView("tab view", B_WIDTH_FROM_WIDEST
),
52 virtual void Select(int32 tabIndex
)
54 BTabView::Select(tabIndex
);
56 BMessage
message(fMessage
);
57 message
.AddInt32("tab index", tabIndex
);
58 fTarget
.SendMessage(&message
);
68 add_languages_to_menu(const StringList
& languages
, BMenu
* menu
)
70 for (int i
= 0; i
< languages
.CountItems(); i
++) {
71 const BString
& language
= languages
.ItemAtFast(i
);
72 BMessage
* message
= new BMessage(MSG_LANGUAGE_SELECTED
);
73 message
->AddString("code", language
);
74 BMenuItem
* item
= new BMenuItem(language
, message
);
80 UserLoginWindow::UserLoginWindow(BWindow
* parent
, BRect frame
, Model
& model
)
82 BWindow(frame
, B_TRANSLATE("Log in"),
83 B_FLOATING_WINDOW_LOOK
, B_FLOATING_SUBSET_WINDOW_FEEL
,
84 B_ASYNCHRONOUS_CONTROLS
| B_AUTO_UPDATE_SIZE_LIMITS
85 | B_NOT_RESIZABLE
| B_NOT_ZOOMABLE
),
86 fPreferredLanguage(model
.PreferredLanguage()),
93 fUsernameField
= new BTextControl(B_TRANSLATE("User name:"), "", NULL
);
94 fPasswordField
= new BTextControl(B_TRANSLATE("Pass phrase:"), "", NULL
);
95 fPasswordField
->TextView()->HideTyping(true);
97 fNewUsernameField
= new BTextControl(B_TRANSLATE("User name:"), "",
99 fNewPasswordField
= new BTextControl(B_TRANSLATE("Pass phrase:"), "",
100 new BMessage(MSG_VALIDATE_FIELDS
));
101 fNewPasswordField
->TextView()->HideTyping(true);
102 fRepeatPasswordField
= new BTextControl(B_TRANSLATE("Repeat pass phrase:"),
103 "", new BMessage(MSG_VALIDATE_FIELDS
));
104 fRepeatPasswordField
->TextView()->HideTyping(true);
106 // Construct languages popup
107 BPopUpMenu
* languagesMenu
= new BPopUpMenu(B_TRANSLATE("Language"));
108 fLanguageCodeField
= new BMenuField("language",
109 B_TRANSLATE("Preferred language:"), languagesMenu
);
111 add_languages_to_menu(fModel
.SupportedLanguages(), languagesMenu
);
112 languagesMenu
->SetTargetForItems(this);
114 BMenuItem
* defaultItem
= languagesMenu
->ItemAt(
115 fModel
.SupportedLanguages().IndexOf(fPreferredLanguage
));
116 if (defaultItem
!= NULL
)
117 defaultItem
->SetMarked(true);
120 fEmailField
= new BTextControl(B_TRANSLATE("Email address:"), "", NULL
);
121 fCaptchaView
= new BitmapView("captcha view");
122 fCaptchaResultField
= new BTextControl("", "", NULL
);
124 // Setup modification messages on all text fields to trigger validation
126 fNewUsernameField
->SetModificationMessage(
127 new BMessage(MSG_VALIDATE_FIELDS
));
128 fNewPasswordField
->SetModificationMessage(
129 new BMessage(MSG_VALIDATE_FIELDS
));
130 fRepeatPasswordField
->SetModificationMessage(
131 new BMessage(MSG_VALIDATE_FIELDS
));
132 fEmailField
->SetModificationMessage(
133 new BMessage(MSG_VALIDATE_FIELDS
));
134 fCaptchaResultField
->SetModificationMessage(
135 new BMessage(MSG_VALIDATE_FIELDS
));
137 fTabView
= new TabView(BMessenger(this),
138 BMessage(MSG_TAB_SELECTED
));
140 BGridView
* loginCard
= new BGridView(B_TRANSLATE("Log in"));
141 BLayoutBuilder::Grid
<>(loginCard
)
142 .AddTextControl(fUsernameField
, 0, 0)
143 .AddTextControl(fPasswordField
, 0, 1)
146 .SetInsets(B_USE_DEFAULT_SPACING
)
148 fTabView
->AddTab(loginCard
);
150 BGridView
* createAccountCard
= new BGridView(B_TRANSLATE("Create account"));
151 BLayoutBuilder::Grid
<>(createAccountCard
)
152 .AddTextControl(fNewUsernameField
, 0, 0)
153 .AddTextControl(fNewPasswordField
, 0, 1)
154 .AddTextControl(fRepeatPasswordField
, 0, 2)
155 .AddTextControl(fEmailField
, 0, 3)
156 .AddMenuField(fLanguageCodeField
, 0, 4)
157 .Add(fCaptchaView
, 0, 5)
158 .Add(fCaptchaResultField
, 1, 5)
160 .SetInsets(B_USE_DEFAULT_SPACING
)
162 fTabView
->AddTab(createAccountCard
);
164 fSendButton
= new BButton("send", B_TRANSLATE("Log in"),
165 new BMessage(MSG_SEND
));
166 fCancelButton
= new BButton("cancel", B_TRANSLATE("Cancel"),
167 new BMessage(B_QUIT_REQUESTED
));
170 BLayoutBuilder::Group
<>(this, B_VERTICAL
)
172 .AddGroup(B_HORIZONTAL
)
177 .SetInsets(B_USE_WINDOW_INSETS
)
180 SetDefaultButton(fSendButton
);
184 CenterIn(parent
->Frame());
188 UserLoginWindow::~UserLoginWindow()
190 BAutolock
locker(&fLock
);
192 if (fWorkerThread
>= 0)
193 wait_for_thread(fWorkerThread
, NULL
);
198 UserLoginWindow::MessageReceived(BMessage
* message
)
200 switch (message
->what
) {
201 case MSG_VALIDATE_FIELDS
:
202 _ValidateCreateAccountFields();
218 case MSG_TAB_SELECTED
:
221 if (message
->FindInt32("tab index", &tabIndex
) == B_OK
) {
227 _SetMode(CREATE_ACCOUNT
);
236 case MSG_CAPTCHA_OBTAINED
:
237 if (fCaptchaImage
.Get() != NULL
) {
238 fCaptchaView
->SetBitmap(fCaptchaImage
);
240 fCaptchaView
->UnsetBitmap();
242 fCaptchaResultField
->SetText("");
245 case MSG_LANGUAGE_SELECTED
:
246 message
->FindString("code", &fPreferredLanguage
);
250 BWindow::MessageReceived(message
);
257 UserLoginWindow::SetOnSuccessMessage(
258 const BMessenger
& messenger
, const BMessage
& message
)
260 fOnSuccessTarget
= messenger
;
261 fOnSuccessMessage
= message
;
266 UserLoginWindow::_SetMode(Mode mode
)
275 fTabView
->Select((int32
)0);
276 fSendButton
->SetLabel(B_TRANSLATE("Log in"));
277 fUsernameField
->MakeFocus();
281 fSendButton
->SetLabel(B_TRANSLATE("Create account"));
282 if (fCaptchaToken
.IsEmpty())
284 fNewUsernameField
->MakeFocus();
285 _ValidateCreateAccountFields();
294 count_digits(const BString
& string
)
297 const char* c
= string
.String();
298 for (int32 i
= 0; i
< string
.CountChars(); i
++) {
299 uint32 unicodeChar
= BUnicodeChar::FromUTF8(&c
);
300 if (BUnicodeChar::IsDigit(unicodeChar
))
308 count_upper_case_letters(const BString
& string
)
310 int32 upperCaseLetters
= 0;
311 const char* c
= string
.String();
312 for (int32 i
= 0; i
< string
.CountChars(); i
++) {
313 uint32 unicodeChar
= BUnicodeChar::FromUTF8(&c
);
314 if (BUnicodeChar::IsUpper(unicodeChar
))
317 return upperCaseLetters
;
322 UserLoginWindow::_ValidateCreateAccountFields(bool alertProblems
)
324 BString
nickName(fNewUsernameField
->Text());
325 BString
password1(fNewPasswordField
->Text());
326 BString
password2(fRepeatPasswordField
->Text());
327 BString
email(fEmailField
->Text());
328 BString
captcha(fCaptchaResultField
->Text());
330 // TODO: Use the same validation as the web-serivce
331 bool validUserName
= nickName
.Length() >= 3;
332 fNewUsernameField
->MarkAsInvalid(!validUserName
);
334 bool validPassword
= password1
.Length() >= 8
335 && count_digits(password1
) >= 2
336 && count_upper_case_letters(password1
) >= 2;
337 fNewPasswordField
->MarkAsInvalid(!validPassword
);
338 fRepeatPasswordField
->MarkAsInvalid(password1
!= password2
);
340 bool validCaptcha
= captcha
.Length() > 0;
341 fCaptchaResultField
->MarkAsInvalid(!validCaptcha
);
343 bool valid
= validUserName
&& validPassword
&& password1
== password2
345 if (valid
&& email
.Length() > 0)
350 alert_type alertType
;
351 const char* okLabel
= B_TRANSLATE("OK");
352 const char* cancelLabel
= NULL
;
354 message
= B_TRANSLATE("There are problems in the form:\n\n");
355 alertType
= B_WARNING_ALERT
;
357 alertType
= B_IDEA_ALERT
;
358 okLabel
= B_TRANSLATE("Ignore");
359 cancelLabel
= B_TRANSLATE("Cancel");
362 if (!validUserName
) {
363 message
<< B_TRANSLATE(
364 "The user name needs to be at least "
365 "3 letters long.") << "\n\n";
367 if (!validPassword
) {
368 message
<< B_TRANSLATE(
369 "The password is too weak or invalid. "
370 "Please use at least 8 characters with "
371 "at least 2 numbers and 2 upper-case "
372 "letters.") << "\n\n";
374 if (password1
!= password2
) {
375 message
<< B_TRANSLATE(
376 "The passwords do not match.") << "\n\n";
378 if (email
.Length() == 0) {
379 message
<< B_TRANSLATE(
380 "If you do not provide an email address, "
381 "you will not be able to reset your password "
382 "if you forget it.") << "\n\n";
385 message
<< B_TRANSLATE(
386 "The captcha puzzle needs to be solved.") << "\n\n";
389 BAlert
* alert
= new(std::nothrow
) BAlert(
390 B_TRANSLATE("Input validation"),
392 okLabel
, cancelLabel
, NULL
,
393 B_WIDTH_AS_USUAL
, alertType
);
396 int32 choice
= alert
->Go();
407 UserLoginWindow::_Login()
409 BAutolock
locker(&fLock
);
411 if (fWorkerThread
>= 0)
414 thread_id thread
= spawn_thread(&_AuthenticateThreadEntry
,
415 "Authenticator", B_NORMAL_PRIORITY
, this);
417 _SetWorkerThread(thread
);
422 UserLoginWindow::_CreateAccount()
424 if (!_ValidateCreateAccountFields(true))
427 BAutolock
locker(&fLock
);
429 if (fWorkerThread
>= 0)
432 thread_id thread
= spawn_thread(&_CreateAccountThreadEntry
,
433 "Account creator", B_NORMAL_PRIORITY
, this);
435 _SetWorkerThread(thread
);
440 UserLoginWindow::_RequestCaptcha()
444 fCaptchaView
->UnsetBitmap();
445 fCaptchaImage
.Unset();
449 BAutolock
locker(&fLock
);
451 if (fWorkerThread
>= 0)
454 thread_id thread
= spawn_thread(&_RequestCaptchaThreadEntry
,
455 "Captcha requester", B_NORMAL_PRIORITY
, this);
457 _SetWorkerThread(thread
);
462 UserLoginWindow::_LoginSuccessful(const BString
& message
)
464 // Clone these fields before the window goes away.
465 // (This method is executd from another thread.)
466 BMessenger
onSuccessTarget(fOnSuccessTarget
);
467 BMessage
onSuccessMessage(fOnSuccessMessage
);
469 BMessenger(this).SendMessage(B_QUIT_REQUESTED
);
471 BAlert
* alert
= new(std::nothrow
) BAlert(
472 B_TRANSLATE("Success"),
474 B_TRANSLATE("Close"));
479 // Send the success message after the alert has been closed,
480 // otherwise more windows will popup alongside the alert.
481 if (onSuccessTarget
.IsValid() && onSuccessMessage
.what
!= 0)
482 onSuccessTarget
.SendMessage(&onSuccessMessage
);
487 UserLoginWindow::_SetWorkerThread(thread_id thread
)
492 bool enabled
= thread
< 0;
494 fUsernameField
->SetEnabled(enabled
);
495 fPasswordField
->SetEnabled(enabled
);
496 fNewUsernameField
->SetEnabled(enabled
);
497 fNewPasswordField
->SetEnabled(enabled
);
498 fRepeatPasswordField
->SetEnabled(enabled
);
499 fEmailField
->SetEnabled(enabled
);
500 fLanguageCodeField
->SetEnabled(enabled
);
501 fCaptchaResultField
->SetEnabled(enabled
);
502 fSendButton
->SetEnabled(enabled
);
505 fWorkerThread
= thread
;
506 resume_thread(fWorkerThread
);
516 UserLoginWindow::_AuthenticateThreadEntry(void* data
)
518 UserLoginWindow
* window
= reinterpret_cast<UserLoginWindow
*>(data
);
519 window
->_AuthenticateThread();
525 UserLoginWindow::_AuthenticateThread()
530 BString
nickName(fUsernameField
->Text());
531 BString
passwordClear(fPasswordField
->Text());
535 WebAppInterface interface
;
538 status_t status
= interface
.AuthenticateUser(
539 nickName
, passwordClear
, info
);
541 BString error
= B_TRANSLATE("Authentication failed. "
542 "Connection to the service failed.");
545 if (status
== B_OK
&& info
.FindMessage("result", &result
) == B_OK
) {
547 if (result
.FindString("token", &token
) == B_OK
&& !token
.IsEmpty()) {
548 // We don't care for or store the token for now. The web-service
549 // supports two methods of authorizing requests. One is via
550 // Basic Authentication in the HTTP header, the other is via
551 // Token Bearer. Since the connection is encrypted, it is hopefully
552 // ok to send the password with each request instead of implementing
553 // the Token Bearer. See section 5.1.2 in the haiku-depot-web
556 fModel
.SetAuthorization(nickName
, passwordClear
, true);
558 error
= B_TRANSLATE("Authentication failed. The user does "
559 "not exist or the wrong password was supplied.");
563 if (!error
.IsEmpty()) {
564 BAlert
* alert
= new(std::nothrow
) BAlert(
565 B_TRANSLATE("Authentication failed"),
567 B_TRANSLATE("Close"), NULL
, NULL
,
568 B_WIDTH_AS_USUAL
, B_WARNING_ALERT
);
573 _SetWorkerThread(-1);
575 _SetWorkerThread(-1);
576 _LoginSuccessful(B_TRANSLATE("The authentication was successful."));
582 UserLoginWindow::_RequestCaptchaThreadEntry(void* data
)
584 UserLoginWindow
* window
= reinterpret_cast<UserLoginWindow
*>(data
);
585 window
->_RequestCaptchaThread();
591 UserLoginWindow::_RequestCaptchaThread()
593 WebAppInterface interface
;
596 status_t status
= interface
.RequestCaptcha(info
);
598 BAutolock
locker(&fLock
);
601 if (status
== B_OK
&& info
.FindMessage("result", &result
) == B_OK
) {
602 result
.FindString("token", &fCaptchaToken
);
603 BString imageDataBase64
;
604 if (result
.FindString("pngImageDataBase64", &imageDataBase64
) == B_OK
) {
605 ssize_t encodedSize
= imageDataBase64
.Length();
606 ssize_t decodedSize
= (encodedSize
* 3 + 3) / 4;
607 if (decodedSize
> 0) {
608 char* buffer
= new char[decodedSize
];
609 decodedSize
= decode_base64(buffer
, imageDataBase64
.String(),
611 if (decodedSize
> 0) {
612 BMemoryIO
memoryIO(buffer
, (size_t)decodedSize
);
613 fCaptchaImage
.SetTo(new(std::nothrow
) SharedBitmap(
615 BMessenger(this).SendMessage(MSG_CAPTCHA_OBTAINED
);
617 fprintf(stderr
, "Failed to decode captcha: %s\n",
618 strerror(decodedSize
));
624 fprintf(stderr
, "Failed to obtain captcha: %s\n", strerror(status
));
627 _SetWorkerThread(-1);
632 UserLoginWindow::_CreateAccountThreadEntry(void* data
)
634 UserLoginWindow
* window
= reinterpret_cast<UserLoginWindow
*>(data
);
635 window
->_CreateAccountThread();
641 UserLoginWindow::_CreateAccountThread()
646 BString
nickName(fNewUsernameField
->Text());
647 BString
passwordClear(fNewPasswordField
->Text());
648 BString
email(fEmailField
->Text());
649 BString
captchaToken(fCaptchaToken
);
650 BString
captchaResponse(fCaptchaResultField
->Text());
651 BString
languageCode(fPreferredLanguage
);
655 WebAppInterface interface
;
658 status_t status
= interface
.CreateUser(
659 nickName
, passwordClear
, email
, captchaToken
, captchaResponse
,
662 BAutolock
locker(&fLock
);
664 BString error
= B_TRANSLATE(
665 "There was a puzzling response from the web service.");
668 if (status
== B_OK
) {
669 if (info
.FindMessage("result", &result
) == B_OK
) {
671 } else if (info
.FindMessage("error", &result
) == B_OK
) {
672 result
.PrintToStream();
674 if (result
.FindString("message", &message
) == B_OK
) {
675 if (message
== "captchabadresponse") {
676 error
= B_TRANSLATE("You have not solved the captcha "
677 "puzzle correctly.");
678 } else if (message
== "validationerror") {
679 _CollectValidationFailures(result
, error
);
681 error
<< B_TRANSLATE(" It responded with: ");
688 "It was not possible to contact the web service.");
693 if (!error
.IsEmpty()) {
694 BAlert
* alert
= new(std::nothrow
) BAlert(
695 B_TRANSLATE("Failed to create account"),
697 B_TRANSLATE("Close"), NULL
, NULL
,
698 B_WIDTH_AS_USUAL
, B_WARNING_ALERT
);
704 B_TRANSLATE("Failed to create account: %s\n"), error
.String());
706 _SetWorkerThread(-1);
708 // We need a new captcha, it can be used only once
712 fModel
.SetAuthorization(nickName
, passwordClear
, true);
714 _SetWorkerThread(-1);
715 _LoginSuccessful(B_TRANSLATE("Account created successfully. "
716 "You can now rate packages and do other useful things."));
722 UserLoginWindow::_CollectValidationFailures(const BMessage
& result
,
723 BString
& error
) const
725 error
= B_TRANSLATE("There are problems with the data you entered:\n\n");
731 if (result
.FindMessage("data", &data
) == B_OK
732 && data
.FindMessage("validationfailures", &failures
) == B_OK
) {
738 if (failures
.FindMessage(name
, &failure
) != B_OK
)
743 if (failure
.FindString("property", &property
) == B_OK
744 && failure
.FindString("message", &message
) == B_OK
) {
746 if (property
== "nickname" && message
== "notunique") {
747 error
<< B_TRANSLATE(
748 "The username is already taken. "
749 "Please choose another.");
750 } else if (property
== "passwordClear"
751 && message
== "invalid") {
752 error
<< B_TRANSLATE(
753 "The password is too weak or invalid. "
754 "Please use at least 8 characters with "
755 "at least 2 numbers and 2 upper-case "
757 } else if (property
== "email" && message
== "malformed") {
758 error
<< B_TRANSLATE(
759 "The email address appears to be malformed.");
761 error
<< property
<< ": " << message
;
768 error
<< B_TRANSLATE("But none could be listed here, sorry.");