repository_infos: Enable automatic updates on the main Haiku repostiory.
[haiku.git] / src / apps / haikudepot / ui / UserLoginWindow.cpp
blob89cfd85ee7a24275db3cd1d61d2bcb593780f184
1 /*
2 * Copyright 2014, Stephan Aßmus <superstippi@gmx.de>.
3 * All rights reserved. Distributed under the terms of the MIT License.
4 */
6 #include "UserLoginWindow.h"
8 #include <algorithm>
9 #include <stdio.h>
11 #include <mail_encoding.h>
13 #include <Alert.h>
14 #include <Autolock.h>
15 #include <Catalog.h>
16 #include <Button.h>
17 #include <LayoutBuilder.h>
18 #include <MenuField.h>
19 #include <PopUpMenu.h>
20 #include <TabView.h>
21 #include <TextControl.h>
22 #include <UnicodeChar.h>
24 #include "BitmapView.h"
25 #include "Model.h"
26 #include "WebAppInterface.h"
29 #undef B_TRANSLATION_CONTEXT
30 #define B_TRANSLATION_CONTEXT "UserLoginWindow"
33 enum {
34 MSG_SEND = 'send',
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 {
43 public:
44 TabView(const BMessenger& target, const BMessage& message)
46 BTabView("tab view", B_WIDTH_FROM_WIDEST),
47 fTarget(target),
48 fMessage(message)
52 virtual void Select(int32 tabIndex)
54 BTabView::Select(tabIndex);
56 BMessage message(fMessage);
57 message.AddInt32("tab index", tabIndex);
58 fTarget.SendMessage(&message);
61 private:
62 BMessenger fTarget;
63 BMessage fMessage;
67 static void
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);
75 menu->AddItem(item);
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()),
87 fModel(model),
88 fMode(NONE),
89 fWorkerThread(-1)
91 AddToSubset(parent);
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:"), "",
98 NULL);
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
125 // of input
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)
144 .AddGlue(0, 2)
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));
169 // Build layout
170 BLayoutBuilder::Group<>(this, B_VERTICAL)
171 .Add(fTabView)
172 .AddGroup(B_HORIZONTAL)
173 .AddGlue()
174 .Add(fCancelButton)
175 .Add(fSendButton)
176 .End()
177 .SetInsets(B_USE_WINDOW_INSETS)
180 SetDefaultButton(fSendButton);
182 _SetMode(LOGIN);
184 CenterIn(parent->Frame());
188 UserLoginWindow::~UserLoginWindow()
190 BAutolock locker(&fLock);
192 if (fWorkerThread >= 0)
193 wait_for_thread(fWorkerThread, NULL);
197 void
198 UserLoginWindow::MessageReceived(BMessage* message)
200 switch (message->what) {
201 case MSG_VALIDATE_FIELDS:
202 _ValidateCreateAccountFields();
203 break;
205 case MSG_SEND:
206 switch (fMode) {
207 case LOGIN:
208 _Login();
209 break;
210 case CREATE_ACCOUNT:
211 _CreateAccount();
212 break;
213 default:
214 break;
216 break;
218 case MSG_TAB_SELECTED:
220 int32 tabIndex;
221 if (message->FindInt32("tab index", &tabIndex) == B_OK) {
222 switch (tabIndex) {
223 case 0:
224 _SetMode(LOGIN);
225 break;
226 case 1:
227 _SetMode(CREATE_ACCOUNT);
228 break;
229 default:
230 break;
233 break;
236 case MSG_CAPTCHA_OBTAINED:
237 if (fCaptchaImage.Get() != NULL) {
238 fCaptchaView->SetBitmap(fCaptchaImage);
239 } else {
240 fCaptchaView->UnsetBitmap();
242 fCaptchaResultField->SetText("");
243 break;
245 case MSG_LANGUAGE_SELECTED:
246 message->FindString("code", &fPreferredLanguage);
247 break;
249 default:
250 BWindow::MessageReceived(message);
251 break;
256 void
257 UserLoginWindow::SetOnSuccessMessage(
258 const BMessenger& messenger, const BMessage& message)
260 fOnSuccessTarget = messenger;
261 fOnSuccessMessage = message;
265 void
266 UserLoginWindow::_SetMode(Mode mode)
268 if (fMode == mode)
269 return;
271 fMode = mode;
273 switch (fMode) {
274 case LOGIN:
275 fTabView->Select((int32)0);
276 fSendButton->SetLabel(B_TRANSLATE("Log in"));
277 fUsernameField->MakeFocus();
278 break;
279 case CREATE_ACCOUNT:
280 fTabView->Select(1);
281 fSendButton->SetLabel(B_TRANSLATE("Create account"));
282 if (fCaptchaToken.IsEmpty())
283 _RequestCaptcha();
284 fNewUsernameField->MakeFocus();
285 _ValidateCreateAccountFields();
286 break;
287 default:
288 break;
293 static int32
294 count_digits(const BString& string)
296 int32 digits = 0;
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))
301 digits++;
303 return digits;
307 static int32
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))
315 upperCaseLetters++;
317 return upperCaseLetters;
321 bool
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
344 && validCaptcha;
345 if (valid && email.Length() > 0)
346 return true;
348 if (alertProblems) {
349 BString message;
350 alert_type alertType;
351 const char* okLabel = B_TRANSLATE("OK");
352 const char* cancelLabel = NULL;
353 if (!valid) {
354 message = B_TRANSLATE("There are problems in the form:\n\n");
355 alertType = B_WARNING_ALERT;
356 } else {
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";
384 if (!validCaptcha) {
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"),
391 message,
392 okLabel, cancelLabel, NULL,
393 B_WIDTH_AS_USUAL, alertType);
395 if (alert != NULL) {
396 int32 choice = alert->Go();
397 if (choice == 1)
398 return false;
402 return valid;
406 void
407 UserLoginWindow::_Login()
409 BAutolock locker(&fLock);
411 if (fWorkerThread >= 0)
412 return;
414 thread_id thread = spawn_thread(&_AuthenticateThreadEntry,
415 "Authenticator", B_NORMAL_PRIORITY, this);
416 if (thread >= 0)
417 _SetWorkerThread(thread);
421 void
422 UserLoginWindow::_CreateAccount()
424 if (!_ValidateCreateAccountFields(true))
425 return;
427 BAutolock locker(&fLock);
429 if (fWorkerThread >= 0)
430 return;
432 thread_id thread = spawn_thread(&_CreateAccountThreadEntry,
433 "Account creator", B_NORMAL_PRIORITY, this);
434 if (thread >= 0)
435 _SetWorkerThread(thread);
439 void
440 UserLoginWindow::_RequestCaptcha()
442 if (Lock()) {
443 fCaptchaToken = "";
444 fCaptchaView->UnsetBitmap();
445 fCaptchaImage.Unset();
446 Unlock();
449 BAutolock locker(&fLock);
451 if (fWorkerThread >= 0)
452 return;
454 thread_id thread = spawn_thread(&_RequestCaptchaThreadEntry,
455 "Captcha requester", B_NORMAL_PRIORITY, this);
456 if (thread >= 0)
457 _SetWorkerThread(thread);
461 void
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"),
473 message,
474 B_TRANSLATE("Close"));
476 if (alert != NULL)
477 alert->Go();
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);
486 void
487 UserLoginWindow::_SetWorkerThread(thread_id thread)
489 if (!Lock())
490 return;
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);
504 if (thread >= 0) {
505 fWorkerThread = thread;
506 resume_thread(fWorkerThread);
507 } else {
508 fWorkerThread = -1;
511 Unlock();
515 int32
516 UserLoginWindow::_AuthenticateThreadEntry(void* data)
518 UserLoginWindow* window = reinterpret_cast<UserLoginWindow*>(data);
519 window->_AuthenticateThread();
520 return 0;
524 void
525 UserLoginWindow::_AuthenticateThread()
527 if (!Lock())
528 return;
530 BString nickName(fUsernameField->Text());
531 BString passwordClear(fPasswordField->Text());
533 Unlock();
535 WebAppInterface interface;
536 BMessage info;
538 status_t status = interface.AuthenticateUser(
539 nickName, passwordClear, info);
541 BString error = B_TRANSLATE("Authentication failed. "
542 "Connection to the service failed.");
544 BMessage result;
545 if (status == B_OK && info.FindMessage("result", &result) == B_OK) {
546 BString token;
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
554 // documentation.
555 error = "";
556 fModel.SetAuthorization(nickName, passwordClear, true);
557 } else {
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"),
566 error,
567 B_TRANSLATE("Close"), NULL, NULL,
568 B_WIDTH_AS_USUAL, B_WARNING_ALERT);
570 if (alert != NULL)
571 alert->Go();
573 _SetWorkerThread(-1);
574 } else {
575 _SetWorkerThread(-1);
576 _LoginSuccessful(B_TRANSLATE("The authentication was successful."));
581 int32
582 UserLoginWindow::_RequestCaptchaThreadEntry(void* data)
584 UserLoginWindow* window = reinterpret_cast<UserLoginWindow*>(data);
585 window->_RequestCaptchaThread();
586 return 0;
590 void
591 UserLoginWindow::_RequestCaptchaThread()
593 WebAppInterface interface;
594 BMessage info;
596 status_t status = interface.RequestCaptcha(info);
598 BAutolock locker(&fLock);
600 BMessage result;
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(),
610 encodedSize);
611 if (decodedSize > 0) {
612 BMemoryIO memoryIO(buffer, (size_t)decodedSize);
613 fCaptchaImage.SetTo(new(std::nothrow) SharedBitmap(
614 memoryIO), true);
615 BMessenger(this).SendMessage(MSG_CAPTCHA_OBTAINED);
616 } else {
617 fprintf(stderr, "Failed to decode captcha: %s\n",
618 strerror(decodedSize));
620 delete[] buffer;
623 } else {
624 fprintf(stderr, "Failed to obtain captcha: %s\n", strerror(status));
627 _SetWorkerThread(-1);
631 int32
632 UserLoginWindow::_CreateAccountThreadEntry(void* data)
634 UserLoginWindow* window = reinterpret_cast<UserLoginWindow*>(data);
635 window->_CreateAccountThread();
636 return 0;
640 void
641 UserLoginWindow::_CreateAccountThread()
643 if (!Lock())
644 return;
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);
653 Unlock();
655 WebAppInterface interface;
656 BMessage info;
658 status_t status = interface.CreateUser(
659 nickName, passwordClear, email, captchaToken, captchaResponse,
660 languageCode, info);
662 BAutolock locker(&fLock);
664 BString error = B_TRANSLATE(
665 "There was a puzzling response from the web service.");
667 BMessage result;
668 if (status == B_OK) {
669 if (info.FindMessage("result", &result) == B_OK) {
670 error = "";
671 } else if (info.FindMessage("error", &result) == B_OK) {
672 result.PrintToStream();
673 BString message;
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);
680 } else {
681 error << B_TRANSLATE(" It responded with: ");
682 error << message;
686 } else {
687 error = B_TRANSLATE(
688 "It was not possible to contact the web service.");
691 locker.Unlock();
693 if (!error.IsEmpty()) {
694 BAlert* alert = new(std::nothrow) BAlert(
695 B_TRANSLATE("Failed to create account"),
696 error,
697 B_TRANSLATE("Close"), NULL, NULL,
698 B_WIDTH_AS_USUAL, B_WARNING_ALERT);
700 if (alert != NULL)
701 alert->Go();
703 fprintf(stderr,
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
709 fCaptchaToken = "";
710 _RequestCaptcha();
711 } else {
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."));
721 void
722 UserLoginWindow::_CollectValidationFailures(const BMessage& result,
723 BString& error) const
725 error = B_TRANSLATE("There are problems with the data you entered:\n\n");
727 bool found = false;
729 BMessage data;
730 BMessage failures;
731 if (result.FindMessage("data", &data) == B_OK
732 && data.FindMessage("validationfailures", &failures) == B_OK) {
733 int32 index = 0;
734 while (true) {
735 BString name;
736 name << index++;
737 BMessage failure;
738 if (failures.FindMessage(name, &failure) != B_OK)
739 break;
741 BString property;
742 BString message;
743 if (failure.FindString("property", &property) == B_OK
744 && failure.FindString("message", &message) == B_OK) {
745 found = true;
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 "
756 "letters.");
757 } else if (property == "email" && message == "malformed") {
758 error << B_TRANSLATE(
759 "The email address appears to be malformed.");
760 } else {
761 error << property << ": " << message;
767 if (!found) {
768 error << B_TRANSLATE("But none could be listed here, sorry.");