diff options
Diffstat (limited to 'src/leap/gui')
31 files changed, 4780 insertions, 517 deletions
diff --git a/src/leap/gui/__init__.py b/src/leap/gui/__init__.py index e69de29b..804bfbc1 100644 --- a/src/leap/gui/__init__.py +++ b/src/leap/gui/__init__.py @@ -0,0 +1,11 @@ +try: + import sip + sip.setapi('QString', 2) + sip.setapi('QVariant', 2) +except ValueError: + pass + +import firstrun +import firstrun.wizard + +__all__ = ['firstrun', 'firstrun.wizard'] diff --git a/src/leap/gui/constants.py b/src/leap/gui/constants.py new file mode 100644 index 00000000..277f3540 --- /dev/null +++ b/src/leap/gui/constants.py @@ -0,0 +1,13 @@ +import time + +APP_LOGO = ':/images/leap-color-small.png' + +# bare is the username portion of a JID +# full includes the "at" and some extra chars +# that can be allowed for fqdn + +BARE_USERNAME_REGEX = r"^[A-Za-z\d_]+$" +FULL_USERNAME_REGEX = r"^[A-Za-z\d_@.-]+$" + +GUI_PAUSE_FOR_USER_SECONDS = 1 +pause_for_user = lambda: time.sleep(GUI_PAUSE_FOR_USER_SECONDS) diff --git a/src/leap/gui/firstrun/__init__.py b/src/leap/gui/firstrun/__init__.py new file mode 100644 index 00000000..2a523d6a --- /dev/null +++ b/src/leap/gui/firstrun/__init__.py @@ -0,0 +1,28 @@ +try: + import sip + sip.setapi('QString', 2) + sip.setapi('QVariant', 2) +except ValueError: + pass + +import intro +import connect +import last +import login +import mixins +import providerinfo +import providerselect +import providersetup +import register + +__all__ = [ + 'intro', + 'connect', + 'last', + 'login', + 'mixins', + 'providerinfo', + 'providerselect', + 'providersetup', + 'register', + ] # ,'wizard'] diff --git a/src/leap/gui/firstrun/connect.py b/src/leap/gui/firstrun/connect.py new file mode 100644 index 00000000..ad7bb13a --- /dev/null +++ b/src/leap/gui/firstrun/connect.py @@ -0,0 +1,214 @@ +""" +Provider Setup Validation Page, +used in First Run Wizard +""" +import logging + +from PyQt4 import QtGui + +#import requests + +from leap.gui.progress import ValidationPage +from leap.util.web import get_https_domain_and_port + +from leap.base import auth +from leap.gui.constants import APP_LOGO + +logger = logging.getLogger(__name__) + + +class ConnectionPage(ValidationPage): + + def __init__(self, parent=None): + super(ConnectionPage, self).__init__(parent) + self.current_page = "connect" + + title = self.tr("Connecting...") + subtitle = self.tr("Setting up a encrypted " + "connection with the provider") + + self.setTitle(title) + self.setSubTitle(subtitle) + + self.setPixmap( + QtGui.QWizard.LogoPixmap, + QtGui.QPixmap(APP_LOGO)) + + def _do_checks(self, update_signal=None): + """ + executes actual checks in a separate thread + + we initialize the srp protocol register + and try to register user. + """ + wizard = self.wizard() + full_domain = self.field('provider_domain') + domain, port = get_https_domain_and_port(full_domain) + + pconfig = wizard.eipconfigchecker(domain=domain) + # this should be persisted... + pconfig.defaultprovider.load() + pconfig.set_api_domain() + + pCertChecker = wizard.providercertchecker( + domain=domain) + pCertChecker.set_api_domain(pconfig.apidomain) + + ########################################### + # Set Credentials. + # username and password are in different fields + # if they were stored in log_in or sign_up pages. + from_login = wizard.from_login + + unamek_base = 'userName' + passwk_base = 'userPassword' + unamek = 'login_%s' % unamek_base if from_login else unamek_base + passwk = 'login_%s' % passwk_base if from_login else passwk_base + + username = self.field(unamek) + password = self.field(passwk) + credentials = username, password + + yield(("head_sentinel", 0), lambda: None) + + ################################################## + # 1) fetching eip service config + ################################################## + def fetcheipconf(): + try: + pconfig.fetch_eip_service_config() + + # XXX get specific exception + except Exception as exc: + return self.fail(exc.message) + + yield((self.tr("Getting EIP configuration files"), 40), + fetcheipconf) + + ################################################## + # 2) getting client certificate + ################################################## + + def fetcheipcert(): + try: + downloaded = pCertChecker.download_new_client_cert( + credentials=credentials) + if not downloaded: + logger.error('Could not download client cert') + return False + + except auth.SRPAuthenticationError as exc: + return self.fail(self.tr( + "Authentication error: %s" % exc.message)) + + except Exception as exc: + return self.fail(exc.message) + else: + return True + + yield((self.tr("Getting EIP certificate"), 80), + fetcheipcert) + + ################ + # end ! + ################ + self.set_done() + yield(("end_sentinel", 100), lambda: None) + + def on_checks_validation_ready(self): + """ + called after _do_checks has finished + (connected to checker thread finished signal) + """ + # here we go! :) + if self.is_done(): + nextbutton = self.wizard().button(QtGui.QWizard.NextButton) + nextbutton.setFocus() + + full_domain = self.field('provider_domain') + domain, port = get_https_domain_and_port(full_domain) + _domain = u"%s:%s" % ( + domain, port) if port != 443 else unicode(domain) + self.run_eip_checks_for_provider_and_connect(_domain) + + def run_eip_checks_for_provider_and_connect(self, domain): + wizard = self.wizard() + conductor = wizard.conductor + start_eip_signal = getattr( + wizard, + 'start_eipconnection_signal', None) + + if conductor: + conductor.set_provider_domain(domain) + # we could run some of the checks to be + # sure everything is in order, but + # I see no point in doing it, we assume + # we've gone thru all checks during the wizard. + #conductor.run_checks() + #self.conductor = conductor + #errors = self.eip_error_check() + #if not errors and start_eip_signal: + if start_eip_signal: + start_eip_signal.emit() + + else: + logger.warning( + "No conductor found. This means that " + "probably the wizard has been launched " + "in an stand-alone way.") + + self.set_done() + + #def eip_error_check(self): + #""" + #a version of the main app error checker, + #but integrated within the connecting page of the wizard. + #consumes the conductor error queue. + #pops errors, and add those to the wizard page + #""" + # TODO handle errors. + # We should redirect them to the log viewer + # with a brief message. + # XXX move to LAST PAGE instead. + #logger.debug('eip error check from connecting page') + #errq = self.conductor.error_queue + + #def _do_validation(self): + #""" + #called after _do_checks has finished + #(connected to checker thread finished signal) + #""" + #from_login = self.wizard().from_login + #prevpage = "login" if from_login else "signup" + + #wizard = self.wizard() + #if self.errors: + #logger.debug('going back with errors') + #logger.error(self.errors) + #name, first_error = self.pop_first_error() + #wizard.set_validation_error( + #prevpage, + #first_error) + #self.go_back() + + def nextId(self): + wizard = self.wizard() + return wizard.get_page_index('lastpage') + + def initializePage(self): + super(ConnectionPage, self).initializePage() + self.set_undone() + cancelbutton = self.wizard().button(QtGui.QWizard.CancelButton) + cancelbutton.hide() + self.completeChanged.emit() + + wizard = self.wizard() + eip_statuschange_signal = wizard.eip_statuschange_signal + if eip_statuschange_signal: + eip_statuschange_signal.connect( + lambda status: self.send_status( + status)) + + def send_status(self, status): + wizard = self.wizard() + wizard.openvpn_status.append(status) diff --git a/src/leap/gui/firstrun/constants.py b/src/leap/gui/firstrun/constants.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/leap/gui/firstrun/constants.py diff --git a/src/leap/gui/firstrun/intro.py b/src/leap/gui/firstrun/intro.py new file mode 100644 index 00000000..b519362f --- /dev/null +++ b/src/leap/gui/firstrun/intro.py @@ -0,0 +1,68 @@ +""" +Intro page used in first run wizard +""" + +from PyQt4 import QtGui + +from leap.gui.constants import APP_LOGO + + +class IntroPage(QtGui.QWizardPage): + def __init__(self, parent=None): + super(IntroPage, self).__init__(parent) + + self.setTitle(self.tr("First run wizard")) + + #self.setPixmap( + #QtGui.QWizard.WatermarkPixmap, + #QtGui.QPixmap(':/images/watermark1.png')) + + self.setPixmap( + QtGui.QWizard.LogoPixmap, + QtGui.QPixmap(APP_LOGO)) + + label = QtGui.QLabel(self.tr( + "Now we will guide you through " + "some configuration that is needed before you " + "can connect for the first time.<br><br>" + "If you ever need to modify these options again, " + "you can find the wizard in the '<i>Settings</i>' menu from the " + "main window.<br><br>" + "Do you want to <b>sign up</b> for a new account, or <b>log " + "in</b> with an already existing username?<br>")) + label.setWordWrap(True) + + radiobuttonGroup = QtGui.QGroupBox() + + self.sign_up = QtGui.QRadioButton( + self.tr("Sign up for a new account")) + self.sign_up.setChecked(True) + self.log_in = QtGui.QRadioButton( + self.tr("Log In with my credentials")) + + radiobLayout = QtGui.QVBoxLayout() + radiobLayout.addWidget(self.sign_up) + radiobLayout.addWidget(self.log_in) + radiobuttonGroup.setLayout(radiobLayout) + + layout = QtGui.QVBoxLayout() + layout.addWidget(label) + layout.addWidget(radiobuttonGroup) + self.setLayout(layout) + + #self.registerField('is_signup', self.sign_up) + + def validatePage(self): + return True + + def nextId(self): + """ + returns next id + in a non-linear wizard + """ + if self.sign_up.isChecked(): + next_ = 'providerselection' + if self.log_in.isChecked(): + next_ = 'login' + wizard = self.wizard() + return wizard.get_page_index(next_) diff --git a/src/leap/gui/firstrun/last.py b/src/leap/gui/firstrun/last.py new file mode 100644 index 00000000..32d98acc --- /dev/null +++ b/src/leap/gui/firstrun/last.py @@ -0,0 +1,109 @@ +""" +Last Page, used in First Run Wizard +""" +import logging + +from PyQt4 import QtGui + +from leap.util.coroutines import coroutine +from leap.gui.constants import APP_LOGO + +logger = logging.getLogger(__name__) + + +class LastPage(QtGui.QWizardPage): + def __init__(self, parent=None): + super(LastPage, self).__init__(parent) + + self.setTitle(self.tr( + "Connecting to Encrypted Internet Proxy service...")) + + self.setPixmap( + QtGui.QWizard.LogoPixmap, + QtGui.QPixmap(APP_LOGO)) + + #self.setPixmap( + #QtGui.QWizard.WatermarkPixmap, + #QtGui.QPixmap(':/images/watermark2.png')) + + self.label = QtGui.QLabel() + self.label.setWordWrap(True) + + # XXX REFACTOR to a Validating Page... + self.status_line_1 = QtGui.QLabel() + self.status_line_2 = QtGui.QLabel() + self.status_line_3 = QtGui.QLabel() + self.status_line_4 = QtGui.QLabel() + self.status_line_5 = QtGui.QLabel() + + layout = QtGui.QVBoxLayout() + layout.addWidget(self.label) + + # make loop + layout.addWidget(self.status_line_1) + layout.addWidget(self.status_line_2) + layout.addWidget(self.status_line_3) + layout.addWidget(self.status_line_4) + layout.addWidget(self.status_line_5) + + self.setLayout(layout) + + def set_status_line(self, line, status): + statusline = getattr(self, 'status_line_%s' % line) + if statusline: + statusline.setText(status) + + def set_finished_status(self): + self.setTitle(self.tr('You are now using an encrypted connection!')) + finishText = self.wizard().buttonText( + QtGui.QWizard.FinishButton) + finishText = finishText.replace('&', '') + self.label.setText(self.tr( + "Click '<i>%s</i>' to end the wizard and " + "save your settings." % finishText)) + # XXX init network checker + # trigger signal + + @coroutine + def eip_status_handler(self): + # XXX this can be changed to use + # signals. See progress.py + logger.debug('logging status in last page') + self.validation_done = False + status_count = 1 + try: + while True: + status = (yield) + status_count += 1 + # XXX add to line... + logger.debug('status --> %s', status) + self.set_status_line(status_count, status) + if status == "connected": + self.set_finished_status() + break + except GeneratorExit: + pass + except StopIteration: + pass + + def initializePage(self): + super(LastPage, self).initializePage() + wizard = self.wizard() + handler = self.eip_status_handler() + + # get statuses done in prev page + for st in wizard.openvpn_status: + self.send_status(handler.send, st) + + # bind signal for events yet to come + eip_statuschange_signal = wizard.eip_statuschange_signal + if eip_statuschange_signal: + eip_statuschange_signal.connect( + lambda status: self.send_status( + handler.send, status)) + + def send_status(self, cb, status): + try: + cb(status) + except StopIteration: + pass diff --git a/src/leap/gui/firstrun/login.py b/src/leap/gui/firstrun/login.py new file mode 100644 index 00000000..3707d3ff --- /dev/null +++ b/src/leap/gui/firstrun/login.py @@ -0,0 +1,332 @@ +""" +LogIn Page, used inf First Run Wizard +""" +from PyQt4 import QtCore +from PyQt4 import QtGui + +import requests + +from leap.base import auth +from leap.gui.firstrun.mixins import UserFormMixIn +from leap.gui.progress import InlineValidationPage +from leap.gui import styles + +from leap.gui.constants import APP_LOGO, FULL_USERNAME_REGEX + + +class LogInPage(InlineValidationPage, UserFormMixIn): # InlineValidationPage + + def __init__(self, parent=None): + + super(LogInPage, self).__init__(parent) + self.current_page = "login" + + self.setTitle(self.tr("Log In")) + self.setSubTitle(self.tr("Log in with your credentials")) + self.current_page = "login" + + self.setPixmap( + QtGui.QWizard.LogoPixmap, + QtGui.QPixmap(APP_LOGO)) + + self.setupSteps() + self.setupUI() + + self.do_confirm_next = False + + def setupUI(self): + userNameLabel = QtGui.QLabel(self.tr("User &name:")) + userNameLineEdit = QtGui.QLineEdit() + userNameLineEdit.cursorPositionChanged.connect( + self.reset_validation_status) + userNameLabel.setBuddy(userNameLineEdit) + + # let's add regex validator + usernameRe = QtCore.QRegExp(FULL_USERNAME_REGEX) + userNameLineEdit.setValidator( + QtGui.QRegExpValidator(usernameRe, self)) + + #userNameLineEdit.setPlaceholderText( + #'username@provider.example.org') + self.userNameLineEdit = userNameLineEdit + + userPasswordLabel = QtGui.QLabel(self.tr("&Password:")) + self.userPasswordLineEdit = QtGui.QLineEdit() + self.userPasswordLineEdit.setEchoMode( + QtGui.QLineEdit.Password) + userPasswordLabel.setBuddy(self.userPasswordLineEdit) + + self.registerField('login_userName*', self.userNameLineEdit) + self.registerField('login_userPassword*', self.userPasswordLineEdit) + + layout = QtGui.QGridLayout() + layout.setColumnMinimumWidth(0, 20) + + validationMsg = QtGui.QLabel("") + validationMsg.setStyleSheet(styles.ErrorLabelStyleSheet) + self.validationMsg = validationMsg + + layout.addWidget(validationMsg, 0, 3) + layout.addWidget(userNameLabel, 1, 0) + layout.addWidget(self.userNameLineEdit, 1, 3) + layout.addWidget(userPasswordLabel, 2, 0) + layout.addWidget(self.userPasswordLineEdit, 2, 3) + + # add validation frame + self.setupValidationFrame() + layout.addWidget(self.valFrame, 4, 2, 4, 2) + self.valFrame.hide() + + self.nextText(self.tr("Log in")) + self.setLayout(layout) + + #self.registerField('is_login_wizard') + + # actual checks + + def _do_checks(self): + + full_username = self.userNameLineEdit.text() + ########################### + # 0) check user@domain form + ########################### + + def checkusername(): + if full_username.count('@') != 1: + return self.fail( + self.tr( + "Username must be in the username@provider form.")) + else: + return True + + yield(("head_sentinel", 0), checkusername) + + username, domain = full_username.split('@') + password = self.userPasswordLineEdit.text() + + # We try a call to an authenticated + # page here as a mean to catch + # srp authentication errors while + wizard = self.wizard() + eipconfigchecker = wizard.eipconfigchecker(domain=domain) + + ######################## + # 1) try name resolution + ######################## + # show the frame before going on... + QtCore.QMetaObject.invokeMethod( + self, "showStepsFrame") + + # Able to contact domain? + # can get definition? + # two-by-one + def resolvedomain(): + try: + eipconfigchecker.fetch_definition(domain=domain) + + # we're using requests here for all + # the possible error cases that it catches. + except requests.exceptions.ConnectionError as exc: + return self.fail(exc.message[1]) + except requests.exceptions.HTTPError as exc: + return self.fail(exc.message) + except Exception as exc: + # XXX get catchall error msg + return self.fail( + exc.message) + else: + return True + + yield((self.tr("Resolving domain name"), 20), resolvedomain) + + wizard.set_providerconfig( + eipconfigchecker.defaultprovider.config) + + ######################## + # 2) do authentication + ######################## + credentials = username, password + pCertChecker = wizard.providercertchecker( + domain=domain) + + def validate_credentials(): + ################# + # FIXME #BUG #638 + verify = False + + try: + pCertChecker.download_new_client_cert( + credentials=credentials, + verify=verify) + + except auth.SRPAuthenticationError as exc: + return self.fail( + self.tr("Authentication error: %s" % exc.message)) + + except Exception as exc: + return self.fail(exc.message) + + else: + return True + + yield(('Validating credentials', 60), validate_credentials) + + self.set_done() + yield(("end_sentinel", 100), lambda: None) + + def green_validation_status(self): + val = self.validationMsg + val.setText(self.tr('Credentials validated.')) + val.setStyleSheet(styles.GreenLineEdit) + + def on_checks_validation_ready(self): + """ + after checks + """ + if self.is_done(): + self.disableFields() + self.cleanup_errormsg() + self.clean_wizard_errors(self.current_page) + # make the user confirm the transition + # to next page. + self.nextText('&Next') + self.nextFocus() + self.green_validation_status() + self.do_confirm_next = True + + # ui update + + def nextText(self, text): + self.setButtonText( + QtGui.QWizard.NextButton, text) + + def nextFocus(self): + self.wizard().button( + QtGui.QWizard.NextButton).setFocus() + + def disableNextButton(self): + self.wizard().button( + QtGui.QWizard.NextButton).setDisabled(True) + + def onUserNamePositionChanged(self, *args): + if self.initial_username_sample: + self.userNameLineEdit.setText('') + # XXX set regular color + self.initial_username_sample = None + + def onUserNameTextChanged(self, *args): + if self.initial_username_sample: + k = args[0][-1] + self.initial_username_sample = None + self.userNameLineEdit.setText(k) + + def disableFields(self): + for field in (self.userNameLineEdit, + self.userPasswordLineEdit): + field.setDisabled(True) + + def populateErrors(self): + # XXX could move this to ValidationMixin + # used in providerselect and register too + + errors = self.wizard().get_validation_error( + self.current_page) + showerr = self.validationMsg.setText + + if errors: + bad_str = getattr(self, 'bad_string', None) + cur_str = self.userNameLineEdit.text() + + if bad_str is None: + # first time we fall here. + # save the current bad_string value + self.bad_string = cur_str + showerr(errors) + else: + # not the first time + if cur_str == bad_str: + showerr(errors) + else: + self.focused_field = False + showerr('') + + def cleanup_errormsg(self): + """ + we reset bad_string to None + should be called before leaving the page + """ + self.bad_string = None + + def paintEvent(self, event): + """ + we hook our populate errors + on paintEvent because we need it to catch + when user enters the page coming from next, + and initializePage does not cover that case. + Maybe there's a better event to hook upon. + """ + super(LogInPage, self).paintEvent(event) + self.populateErrors() + + def set_prevalidation_error(self, error): + self.prevalidation_error = error + + # pagewizard methods + + def nextId(self): + wizard = self.wizard() + if not wizard: + return + if wizard.is_provider_setup is False: + next_ = 'providersetupvalidation' + if wizard.is_provider_setup is True: + # XXX bad name, ok, gonna change that + next_ = 'signupvalidation' + return wizard.get_page_index(next_) + + def initializePage(self): + super(LogInPage, self).initializePage() + username = self.userNameLineEdit + username.setText('username@provider.example.org') + username.cursorPositionChanged.connect( + self.onUserNamePositionChanged) + username.textChanged.connect( + self.onUserNameTextChanged) + self.initial_username_sample = True + self.validationMsg.setText('') + self.valFrame.hide() + + def reset_validation_status(self): + """ + empty the validation msg + and clean the inline validation widget. + """ + self.validationMsg.setText('') + self.steps.removeAllSteps() + self.clearTable() + + def validatePage(self): + """ + if not register done, do checks. + if done, wait for click. + """ + self.disableNextButton() + self.cleanup_errormsg() + self.clean_wizard_errors(self.current_page) + + if self.do_confirm_next: + full_username = self.userNameLineEdit.text() + password = self.userPasswordLineEdit.text() + username, domain = full_username.split('@') + self.setField('provider_domain', domain) + self.setField('login_userName', username) + self.setField('login_userPassword', password) + self.wizard().from_login = True + + return True + + if not self.is_done(): + self.reset_validation_status() + self.do_checks() + + return self.is_done() diff --git a/src/leap/gui/firstrun/mixins.py b/src/leap/gui/firstrun/mixins.py new file mode 100644 index 00000000..c4731893 --- /dev/null +++ b/src/leap/gui/firstrun/mixins.py @@ -0,0 +1,18 @@ +""" +mixins used in First Run Wizard +""" + + +class UserFormMixIn(object): + + def reset_validation_status(self): + """ + empty the validation msg + """ + self.validationMsg.setText('') + + def set_validation_status(self, msg): + """ + set generic validation status + """ + self.validationMsg.setText(msg) diff --git a/src/leap/gui/firstrun/providerinfo.py b/src/leap/gui/firstrun/providerinfo.py new file mode 100644 index 00000000..cff4caca --- /dev/null +++ b/src/leap/gui/firstrun/providerinfo.py @@ -0,0 +1,106 @@ +""" +Provider Info Page, used in First run Wizard +""" +import logging + +from PyQt4 import QtGui + +from leap.gui.constants import APP_LOGO +from leap.util.translations import translate + +logger = logging.getLogger(__name__) + + +class ProviderInfoPage(QtGui.QWizardPage): + + def __init__(self, parent=None): + super(ProviderInfoPage, self).__init__(parent) + + self.setTitle(self.tr("Provider Information")) + self.setSubTitle(self.tr( + "Services offered by this provider")) + + self.setPixmap( + QtGui.QWizard.LogoPixmap, + QtGui.QPixmap(APP_LOGO)) + + self.create_info_panel() + + def create_info_panel(self): + # Use stacked widget instead + # of reparenting the layout. + + infoWidget = QtGui.QStackedWidget() + + info = QtGui.QWidget() + layout = QtGui.QVBoxLayout() + + displayName = QtGui.QLabel("") + description = QtGui.QLabel("") + enrollment_policy = QtGui.QLabel("") + + # XXX set stylesheet... + # prettify a little bit. + # bigger fonts and so on... + + # We could use a QFrame here + + layout.addWidget(displayName) + layout.addWidget(description) + layout.addWidget(enrollment_policy) + layout.addStretch(1) + + info.setLayout(layout) + infoWidget.addWidget(info) + + pageLayout = QtGui.QVBoxLayout() + pageLayout.addWidget(infoWidget) + self.setLayout(pageLayout) + + # add refs to self to allow for + # updates. + # Watch out! Have to get rid of these references! + # this should be better handled with signals !! + self.displayName = displayName + self.description = description + self.description.setWordWrap(True) + self.enrollment_policy = enrollment_policy + + def show_provider_info(self): + + # XXX get multilingual objects + # directly from the config object + + lang = "en" + pconfig = self.wizard().providerconfig + + dn = pconfig.get('name') + display_name = dn[lang] if dn else '' + domain_name = self.field('provider_domain') + + self.displayName.setText( + "<b>%s</b> https://%s" % (display_name, domain_name)) + + desc = pconfig.get('description') + + #description_text = desc[lang] if desc else '' + description_text = translate(desc) if desc else '' + + self.description.setText( + "<i>%s</i>" % description_text) + + # XXX should translate this... + enroll = pconfig.get('enrollment_policy') + if enroll: + self.enrollment_policy.setText( + '<b>%s</b>: <em>%s</em>' % ( + self.tr('enrollment policy'), + enroll)) + + def nextId(self): + wizard = self.wizard() + next_ = "providersetupvalidation" + return wizard.get_page_index(next_) + + def initializePage(self): + self.show_provider_info() diff --git a/src/leap/gui/firstrun/providerselect.py b/src/leap/gui/firstrun/providerselect.py new file mode 100644 index 00000000..917b16fd --- /dev/null +++ b/src/leap/gui/firstrun/providerselect.py @@ -0,0 +1,471 @@ +""" +Select Provider Page, used in First Run Wizard +""" +import logging + +import requests + +from PyQt4 import QtCore +from PyQt4 import QtGui + +from leap.base import exceptions as baseexceptions +#from leap.crypto import certs +from leap.eip import exceptions as eipexceptions +from leap.gui.progress import InlineValidationPage +from leap.gui import styles +from leap.gui.utils import delay +from leap.util.web import get_https_domain_and_port + +from leap.gui.constants import APP_LOGO + +logger = logging.getLogger(__name__) + + +class SelectProviderPage(InlineValidationPage): + + launchChecks = QtCore.pyqtSignal() + + def __init__(self, parent=None, providers=None): + super(SelectProviderPage, self).__init__(parent) + self.current_page = 'providerselection' + + self.setTitle(self.tr("Enter Provider")) + self.setSubTitle(self.tr( + "Please enter the domain of the provider you want " + "to use for your connection") + ) + self.setPixmap( + QtGui.QWizard.LogoPixmap, + QtGui.QPixmap(APP_LOGO)) + + self.did_cert_check = False + + self.done = False + + self.setupSteps() + self.setupUI() + + self.launchChecks.connect( + self.launch_checks) + + self.providerNameEdit.editingFinished.connect( + lambda: self.providerCheckButton.setFocus(True)) + + def setupUI(self): + """ + initializes the UI + """ + providerNameLabel = QtGui.QLabel("h&ttps://") + # note that we expect the bare domain name + # we will add the scheme later + providerNameEdit = QtGui.QLineEdit() + providerNameEdit.cursorPositionChanged.connect( + self.reset_validation_status) + providerNameLabel.setBuddy(providerNameEdit) + + # add regex validator + providerDomainRe = QtCore.QRegExp(r"^[a-z1-9_\-\.]+$") + providerNameEdit.setValidator( + QtGui.QRegExpValidator(providerDomainRe, self)) + self.providerNameEdit = providerNameEdit + + # Eventually we will seed a list of + # well known providers here. + + #providercombo = QtGui.QComboBox() + #if providers: + #for provider in providers: + #providercombo.addItem(provider) + #providerNameSelect = providercombo + + self.registerField("provider_domain*", self.providerNameEdit) + #self.registerField('provider_name_index', providerNameSelect) + + validationMsg = QtGui.QLabel("") + validationMsg.setStyleSheet(styles.ErrorLabelStyleSheet) + self.validationMsg = validationMsg + providerCheckButton = QtGui.QPushButton(self.tr("chec&k!")) + self.providerCheckButton = providerCheckButton + + # cert info + + # this is used in the callback + # for the checkbox changes. + # tricky, since the first time came + # from the exception message. + # should get string from exception too! + self.bad_cert_status = self.tr( + "Server certificate could not be verified.") + + self.certInfo = QtGui.QLabel("") + self.certInfo.setWordWrap(True) + self.certWarning = QtGui.QLabel("") + self.trustProviderCertCheckBox = QtGui.QCheckBox( + self.tr("&Trust this provider certificate.")) + + self.trustProviderCertCheckBox.stateChanged.connect( + self.onTrustCheckChanged) + self.providerNameEdit.textChanged.connect( + self.onProviderChanged) + self.providerCheckButton.clicked.connect( + self.onCheckButtonClicked) + + layout = QtGui.QGridLayout() + layout.addWidget(validationMsg, 0, 2) + layout.addWidget(providerNameLabel, 1, 1) + layout.addWidget(providerNameEdit, 1, 2) + layout.addWidget(providerCheckButton, 1, 3) + + # add certinfo group + # XXX not shown now. should move to validation box. + #layout.addWidget(certinfoGroup, 4, 1, 4, 2) + #self.certinfoGroup = certinfoGroup + #self.certinfoGroup.hide() + + # add validation frame + self.setupValidationFrame() + layout.addWidget(self.valFrame, 4, 2, 4, 2) + self.valFrame.hide() + + self.setLayout(layout) + + # certinfo + + def setupCertInfoGroup(self): # pragma: no cover + # XXX not used now. + certinfoGroup = QtGui.QGroupBox( + self.tr("Certificate validation")) + certinfoLayout = QtGui.QVBoxLayout() + certinfoLayout.addWidget(self.certInfo) + certinfoLayout.addWidget(self.certWarning) + certinfoLayout.addWidget(self.trustProviderCertCheckBox) + certinfoGroup.setLayout(certinfoLayout) + self.certinfoGroup = self.certinfoGroup + + # progress frame + + def setupValidationFrame(self): + qframe = QtGui.QFrame + valFrame = qframe() + valFrame.setFrameStyle(qframe.NoFrame) + valframeLayout = QtGui.QVBoxLayout() + zeros = (0, 0, 0, 0) + valframeLayout.setContentsMargins(*zeros) + + valframeLayout.addWidget(self.stepsTableWidget) + valFrame.setLayout(valframeLayout) + self.valFrame = valFrame + + @QtCore.pyqtSlot() + def onDisableCheckButton(self): + #print 'CHECK BUTTON DISABLED!!!' + self.providerCheckButton.setDisabled(True) + + @QtCore.pyqtSlot() + def launch_checks(self): + self.do_checks() + + def onCheckButtonClicked(self): + QtCore.QMetaObject.invokeMethod( + self, "onDisableCheckButton") + + QtCore.QMetaObject.invokeMethod( + self, "showStepsFrame") + + delay(self, "launch_checks") + + def _do_checks(self): + """ + generator that yields actual checks + that are executed in a separate thread + """ + + wizard = self.wizard() + full_domain = self.providerNameEdit.text() + + # we check if we have a port in the domain string. + domain, port = get_https_domain_and_port(full_domain) + _domain = u"%s:%s" % (domain, port) if port != 443 else unicode(domain) + + netchecker = wizard.netchecker() + providercertchecker = wizard.providercertchecker() + eipconfigchecker = wizard.eipconfigchecker(domain=_domain) + + yield(("head_sentinel", 0), lambda: None) + + ######################## + # 1) try name resolution + ######################## + + def namecheck(): + """ + in which we check if + we are able to name resolve + this domain + """ + try: + #import ipdb;ipdb.set_trace() + netchecker.check_name_resolution( + domain) + + except baseexceptions.LeapException as exc: + logger.error(exc.message) + return self.fail(exc.usermessage) + + except Exception as exc: + return self.fail(exc.message) + + else: + return True + + logger.debug('checking name resolution') + yield((self.tr("Checking if it is a valid provider"), 20), namecheck) + + ######################### + # 2) try https connection + ######################### + + def httpscheck(): + """ + in which we check + if the provider + is offering service over + https + """ + try: + providercertchecker.is_https_working( + "https://%s" % _domain, + verify=True) + + except eipexceptions.HttpsBadCertError as exc: + logger.debug('exception') + return self.fail(exc.usermessage) + # XXX skipping for now... + ############################################## + # We had this validation logic + # in the provider selection page before + ############################################## + #if self.trustProviderCertCheckBox.isChecked(): + #pass + #else: + #fingerprint = certs.get_cert_fingerprint( + #domain=domain, sep=" ") + + # it's ok if we've trusted this fgprt before + #trustedcrts = wizard.trusted_certs + #if trustedcrts and \ + # fingerprint.replace(' ', '') in trustedcrts: + #pass + #else: + # let your user face panick :P + #self.add_cert_info(fingerprint) + #self.did_cert_check = True + #self.completeChanged.emit() + #return False + + except baseexceptions.LeapException as exc: + return self.fail(exc.usermessage) + + except Exception as exc: + return self.fail(exc.message) + + else: + return True + + logger.debug('checking https connection') + yield((self.tr("Checking for a secure connection"), 40), httpscheck) + + ################################## + # 3) try download provider info... + ################################## + + def fetchinfo(): + try: + # XXX we already set _domain in the initialization + # so it should not be needed here. + eipconfigchecker.fetch_definition(domain=_domain) + wizard.set_providerconfig( + eipconfigchecker.defaultprovider.config) + except requests.exceptions.SSLError: + return self.fail(self.tr( + "Could not get info from provider.")) + except requests.exceptions.ConnectionError: + return self.fail(self.tr( + "Could not download provider info " + "(refused conn.).")) + + except Exception as exc: + return self.fail( + self.tr(exc.message)) + else: + return True + + yield((self.tr("Getting info from the provider"), 80), fetchinfo) + + # done! + + self.done = True + yield(("end_sentinel", 100), lambda: None) + + def on_checks_validation_ready(self): + """ + called after _do_checks has finished. + """ + self.domain_checked = True + self.completeChanged.emit() + # let's set focus... + if self.is_done(): + self.wizard().clean_validation_error(self.current_page) + nextbutton = self.wizard().button(QtGui.QWizard.NextButton) + nextbutton.setFocus() + else: + self.providerNameEdit.setFocus() + + # cert trust verification + # (disabled for now) + + def is_insecure_cert_trusted(self): + return self.trustProviderCertCheckBox.isChecked() + + def onTrustCheckChanged(self, state): # pragma: no cover XXX + checked = False + if state == 2: + checked = True + + if checked: + self.reset_validation_status() + else: + self.set_validation_status(self.bad_cert_status) + + # trigger signal to redraw next button + self.completeChanged.emit() + + def add_cert_info(self, certinfo): # pragma: no cover XXX + self.certWarning.setText( + self.tr("Do you want to <b>trust this provider certificate?</b>")) + # XXX Check if this needs to abstracted to remove certinfo + self.certInfo.setText( + self.tr('SHA-256 fingerprint: <i>%s</i><br>' % certinfo)) + self.certInfo.setWordWrap(True) + self.certinfoGroup.show() + + def onProviderChanged(self, text): + self.done = False + provider = self.providerNameEdit.text() + if provider: + self.providerCheckButton.setDisabled(False) + else: + self.providerCheckButton.setDisabled(True) + self.completeChanged.emit() + + def reset_validation_status(self): + """ + empty the validation msg + and clean the inline validation widget. + """ + self.validationMsg.setText('') + self.steps.removeAllSteps() + self.clearTable() + self.domain_checked = False + + # pagewizard methods + + def isComplete(self): + provider = self.providerNameEdit.text() + + if not self.is_done(): + return False + + if not provider: + return False + else: + if self.is_insecure_cert_trusted(): + return True + if not self.did_cert_check: + if self.is_done(): + # XXX sure? + return True + return False + + def populateErrors(self): + # XXX could move this to ValidationMixin + # with some defaults for the validating fields + # (now it only allows one field, manually specified) + + #logger.debug('getting errors') + errors = self.wizard().get_validation_error( + self.current_page) + if errors: + bad_str = getattr(self, 'bad_string', None) + cur_str = self.providerNameEdit.text() + showerr = self.validationMsg.setText + markred = lambda: self.providerNameEdit.setStyleSheet( + styles.ErrorLineEdit) + umarkrd = lambda: self.providerNameEdit.setStyleSheet( + styles.RegularLineEdit) + if bad_str is None: + # first time we fall here. + # save the current bad_string value + self.bad_string = cur_str + showerr(errors) + markred() + else: + # not the first time + # XXX hey, this is getting convoluted. + # roll out this. + # but be careful about all the possibilities + # with going back and forth once you + # enter a domain. + if cur_str == bad_str: + showerr(errors) + markred() + else: + if not getattr(self, 'domain_checked', None): + showerr('') + umarkrd() + else: + self.bad_string = cur_str + showerr(errors) + + def cleanup_errormsg(self): + """ + we reset bad_string to None + should be called before leaving the page + """ + self.bad_string = None + self.domain_checked = False + + def paintEvent(self, event): + """ + we hook our populate errors + on paintEvent because we need it to catch + when user enters the page coming from next, + and initializePage does not cover that case. + Maybe there's a better event to hook upon. + """ + super(SelectProviderPage, self).paintEvent(event) + self.populateErrors() + + def initializePage(self): + self.validationMsg.setText('') + if hasattr(self, 'certinfoGroup'): + # XXX remove ? + self.certinfoGroup.hide() + self.done = False + self.providerCheckButton.setDisabled(True) + self.valFrame.hide() + self.steps.removeAllSteps() + self.clearTable() + + def validatePage(self): + # some cleanup before we leave the page + self.cleanup_errormsg() + + # go + return True + + def nextId(self): + wizard = self.wizard() + if not wizard: + return + return wizard.get_page_index('providerinfo') diff --git a/src/leap/gui/firstrun/providersetup.py b/src/leap/gui/firstrun/providersetup.py new file mode 100644 index 00000000..47060f6e --- /dev/null +++ b/src/leap/gui/firstrun/providersetup.py @@ -0,0 +1,157 @@ +""" +Provider Setup Validation Page, +used if First Run Wizard +""" +import logging + +import requests + +from PyQt4 import QtGui + +from leap.base import exceptions as baseexceptions +from leap.gui.progress import ValidationPage + +from leap.gui.constants import APP_LOGO + +logger = logging.getLogger(__name__) + + +class ProviderSetupValidationPage(ValidationPage): + def __init__(self, parent=None): + super(ProviderSetupValidationPage, self).__init__(parent) + self.current_page = "providersetupvalidation" + + # XXX needed anymore? + #is_signup = self.field("is_signup") + #self.is_signup = is_signup + + self.setTitle(self.tr("Provider setup")) + self.setSubTitle( + self.tr("Gathering configuration options for this provider")) + + self.setPixmap( + QtGui.QWizard.LogoPixmap, + QtGui.QPixmap(APP_LOGO)) + + def _do_checks(self): + """ + generator that yields actual checks + that are executed in a separate thread + """ + + full_domain = self.field('provider_domain') + wizard = self.wizard() + pconfig = wizard.providerconfig + + #pCertChecker = wizard.providercertchecker + #certchecker = pCertChecker(domain=full_domain) + pCertChecker = wizard.providercertchecker( + domain=full_domain) + + yield(("head_sentinel", 0), lambda: None) + + ######################## + # 1) fetch ca cert + ######################## + + def fetchcacert(): + if pconfig: + ca_cert_uri = pconfig.get('ca_cert_uri').geturl() + else: + ca_cert_uri = None + + # XXX check scheme == "https" + # XXX passing verify == False because + # we have trusted right before. + # We should check it's the same domain!!! + # (Check with the trusted fingerprints dict + # or something smart) + try: + pCertChecker.download_ca_cert( + uri=ca_cert_uri, + verify=False) + + except baseexceptions.LeapException as exc: + logger.error(exc.message) + # XXX this should be _ method + return self.fail(self.tr(exc.usermessage)) + + except Exception as exc: + return self.fail(exc.message) + + else: + return True + + yield((self.tr('Fetching CA certificate'), 30), + fetchcacert) + + ######################### + # 2) check CA fingerprint + ######################### + + def checkcafingerprint(): + # XXX get the real thing!!! + pass + #ca_cert_fingerprint = pconfig.get('ca_cert_fingerprint', None) + + # XXX get fingerprint dict (types) + #sha256_fpr = ca_cert_fingerprint.split('=')[1] + + #validate_fpr = pCertChecker.check_ca_cert_fingerprint( + #fingerprint=sha256_fpr) + #if not validate_fpr: + # XXX update validationMsg + # should catch exception + #return False + + yield((self.tr("Checking CA fingerprint"), 60), + checkcafingerprint) + + ######################### + # 2) check CA fingerprint + ######################### + + def validatecacert(): + api_uri = pconfig.get('api_uri', None) + try: + pCertChecker.verify_api_https(api_uri) + except requests.exceptions.SSLError as exc: + return self.fail("Validation Error") + except Exception as exc: + return self.fail(exc.msg) + else: + return True + + yield((self.tr('Validating api certificate'), 90), validatecacert) + + self.set_done() + yield(('end_sentinel', 100), lambda: None) + + def on_checks_validation_ready(self): + """ + called after _do_checks has finished + (connected to checker thread finished signal) + """ + wizard = self.wizard() + prevpage = "login" if wizard.from_login else "providerselection" + + if self.errors: + logger.debug('going back with errors') + name, first_error = self.pop_first_error() + wizard.set_validation_error( + prevpage, + first_error) + + def nextId(self): + wizard = self.wizard() + from_login = wizard.from_login + if from_login: + next_ = 'connect' + else: + next_ = 'signup' + return wizard.get_page_index(next_) + + def initializePage(self): + super(ProviderSetupValidationPage, self).initializePage() + self.set_undone() + self.completeChanged.emit() diff --git a/src/leap/gui/firstrun/register.py b/src/leap/gui/firstrun/register.py new file mode 100644 index 00000000..15278330 --- /dev/null +++ b/src/leap/gui/firstrun/register.py @@ -0,0 +1,387 @@ +""" +Register User Page, used in First Run Wizard +""" +import json +import logging +import socket + +import requests + +from PyQt4 import QtCore +from PyQt4 import QtGui + +from leap.gui.firstrun.mixins import UserFormMixIn + +logger = logging.getLogger(__name__) + +from leap.base import auth +from leap.gui import styles +from leap.gui.constants import APP_LOGO, BARE_USERNAME_REGEX +from leap.gui.progress import InlineValidationPage +from leap.gui.styles import ErrorLabelStyleSheet + + +class RegisterUserPage(InlineValidationPage, UserFormMixIn): + + def __init__(self, parent=None): + + super(RegisterUserPage, self).__init__(parent) + self.current_page = "signup" + + self.setTitle(self.tr("Sign Up")) + # subtitle is set in the initializePage + + self.setPixmap( + QtGui.QWizard.LogoPixmap, + QtGui.QPixmap(APP_LOGO)) + + # commit page means there's no way back after this... + # XXX should change the text on the "commit" button... + self.setCommitPage(True) + + self.setupSteps() + self.setupUI() + self.do_confirm_next = False + self.focused_field = False + + def setupUI(self): + userNameLabel = QtGui.QLabel(self.tr("User &name:")) + userNameLineEdit = QtGui.QLineEdit() + userNameLineEdit.cursorPositionChanged.connect( + self.reset_validation_status) + userNameLabel.setBuddy(userNameLineEdit) + + # let's add regex validator + usernameRe = QtCore.QRegExp(BARE_USERNAME_REGEX) + userNameLineEdit.setValidator( + QtGui.QRegExpValidator(usernameRe, self)) + self.userNameLineEdit = userNameLineEdit + + userPasswordLabel = QtGui.QLabel(self.tr("&Password:")) + self.userPasswordLineEdit = QtGui.QLineEdit() + self.userPasswordLineEdit.setEchoMode( + QtGui.QLineEdit.Password) + userPasswordLabel.setBuddy(self.userPasswordLineEdit) + + userPassword2Label = QtGui.QLabel(self.tr("Password (again):")) + self.userPassword2LineEdit = QtGui.QLineEdit() + self.userPassword2LineEdit.setEchoMode( + QtGui.QLineEdit.Password) + userPassword2Label.setBuddy(self.userPassword2LineEdit) + + rememberPasswordCheckBox = QtGui.QCheckBox( + self.tr("&Remember username and password.")) + rememberPasswordCheckBox.setChecked(True) + + self.registerField('userName*', self.userNameLineEdit) + self.registerField('userPassword*', self.userPasswordLineEdit) + self.registerField('userPassword2*', self.userPassword2LineEdit) + + # XXX missing password confirmation + # XXX validator! + + self.registerField('rememberPassword', rememberPasswordCheckBox) + + layout = QtGui.QGridLayout() + layout.setColumnMinimumWidth(0, 20) + + validationMsg = QtGui.QLabel("") + validationMsg.setStyleSheet(ErrorLabelStyleSheet) + + self.validationMsg = validationMsg + + layout.addWidget(validationMsg, 0, 3) + layout.addWidget(userNameLabel, 1, 0) + layout.addWidget(self.userNameLineEdit, 1, 3) + layout.addWidget(userPasswordLabel, 2, 0) + layout.addWidget(userPassword2Label, 3, 0) + layout.addWidget(self.userPasswordLineEdit, 2, 3) + layout.addWidget(self.userPassword2LineEdit, 3, 3) + layout.addWidget(rememberPasswordCheckBox, 4, 3, 4, 4) + + # add validation frame + self.setupValidationFrame() + layout.addWidget(self.valFrame, 5, 2, 5, 2) + self.valFrame.hide() + + self.setLayout(layout) + self.commitText("Sign up!") + + # commit button + + def commitText(self, text): + # change "commit" button text + self.setButtonText( + QtGui.QWizard.CommitButton, text) + + @property + def commitButton(self): + return self.wizard().button(QtGui.QWizard.CommitButton) + + def commitFocus(self): + self.commitButton.setFocus() + + def disableCommitButton(self): + self.commitButton.setDisabled(True) + + def disableFields(self): + for field in (self.userNameLineEdit, + self.userPasswordLineEdit, + self.userPassword2LineEdit): + field.setDisabled(True) + + # error painting + def paintEvent(self, event): + """ + we hook our populate errors + on paintEvent because we need it to catch + when user enters the page coming from next, + and initializePage does not cover that case. + Maybe there's a better event to hook upon. + """ + super(RegisterUserPage, self).paintEvent(event) + self.populateErrors() + + def markRedAndGetFocus(self, field): + field.setStyleSheet(styles.ErrorLineEdit) + if not self.focused_field: + self.focused_field = True + field.setFocus(QtCore.Qt.OtherFocusReason) + + def markRegular(self, field): + field.setStyleSheet(styles.RegularLineEdit) + + def populateErrors(self): + def showerr(text): + self.validationMsg.setText(text) + err_lower = text.lower() + if "username" in err_lower: + self.markRedAndGetFocus( + self.userNameLineEdit) + if "password" in err_lower: + self.markRedAndGetFocus( + self.userPasswordLineEdit) + + def unmarkred(): + for field in (self.userNameLineEdit, + self.userPasswordLineEdit, + self.userPassword2LineEdit): + self.markRegular(field) + + errors = self.wizard().get_validation_error( + self.current_page) + if errors: + bad_str = getattr(self, 'bad_string', None) + cur_str = self.userNameLineEdit.text() + #prev_er = getattr(self, 'prevalidation_error', None) + + if bad_str is None: + # first time we fall here. + # save the current bad_string value + self.bad_string = cur_str + showerr(errors) + else: + #if prev_er: + #showerr(prev_er) + #return + # not the first time + if cur_str == bad_str: + showerr(errors) + else: + self.focused_field = False + showerr('') + unmarkred() + else: + # no errors + self.focused_field = False + unmarkred() + + def cleanup_errormsg(self): + """ + we reset bad_string to None + should be called before leaving the page + """ + self.bad_string = None + + def green_validation_status(self): + val = self.validationMsg + val.setText(self.tr('Registration succeeded!')) + val.setStyleSheet(styles.GreenLineEdit) + + def reset_validation_status(self): + """ + empty the validation msg + and clean the inline validation widget. + """ + self.validationMsg.setText('') + self.steps.removeAllSteps() + self.clearTable() + + # actual checks + + def _do_checks(self): + """ + generator that yields actual checks + that are executed in a separate thread + """ + wizard = self.wizard() + + provider = self.field('provider_domain') + username = self.userNameLineEdit.text() + password = self.userPasswordLineEdit.text() + password2 = self.userPassword2LineEdit.text() + + pconfig = wizard.eipconfigchecker(domain=provider) + pconfig.defaultprovider.load() + pconfig.set_api_domain() + + def checkpass(): + # we better have here + # some call to a password checker... + # to assess strenght and avoid silly stuff. + + if password != password2: + return self.fail(self.tr('Password does not match..')) + + if len(password) < 6: + #self.set_prevalidation_error('Password too short.') + return self.fail(self.tr('Password too short.')) + + if password == "123456": + # joking, but not too much. + #self.set_prevalidation_error('Password too obvious.') + return self.fail(self.tr('Password too obvious.')) + + # go + return True + + yield(("head_sentinel", 0), checkpass) + + # XXX should emit signal for .show the frame! + # XXX HERE! + + ################################################## + # 1) register user + ################################################## + + # show the frame before going on... + QtCore.QMetaObject.invokeMethod( + self, "showStepsFrame") + + def register(): + + signup = auth.LeapSRPRegister( + schema="https", + provider=pconfig.apidomain, + verify=pconfig.cacert) + try: + ok, req = signup.register_user( + username, password) + + except socket.timeout: + return self.fail( + self.tr("Error connecting to provider (timeout)")) + + except requests.exceptions.ConnectionError as exc: + logger.error(exc.message) + return self.fail( + self.tr('Error Connecting to provider (connerr).')) + except Exception as exc: + return self.fail(exc.message) + + # XXX check for != OK instead??? + + if req.status_code in (404, 500): + return self.fail( + self.tr( + "Error during registration (%s)") % req.status_code) + + try: + validation_msgs = json.loads(req.content) + errors = validation_msgs.get('errors', None) + logger.debug('validation errors: %s' % validation_msgs) + except ValueError: + # probably bad json returned + return self.fail( + self.tr( + "Could not register (bad response)")) + + if errors and errors.get('login', None): + # XXX this sometimes catch the blank username + # but we're not allowing that (soon) + return self.fail( + self.tr('Username not available.')) + + return True + + logger.debug('registering user') + yield(("Registering username", 40), register) + + self.set_done() + yield(("end_sentinel", 100), lambda: None) + + def on_checks_validation_ready(self): + """ + after checks + """ + if self.is_done(): + self.disableFields() + self.cleanup_errormsg() + self.clean_wizard_errors(self.current_page) + # make the user confirm the transition + # to next page. + self.commitText('Connect!') + self.commitFocus() + self.green_validation_status() + self.do_confirm_next = True + + # pagewizard methods + + def validatePage(self): + """ + if not register done, do checks. + if done, wait for click. + """ + self.disableCommitButton() + self.cleanup_errormsg() + self.clean_wizard_errors(self.current_page) + + # After a successful validation + # (ie, success register with server) + # we change the commit button text + # and set this flag to True. + if self.do_confirm_next: + return True + + if not self.is_done(): + # calls checks, which after successful + # execution will call on_checks_validation_ready + self.reset_validation_status() + self.do_checks() + + return self.is_done() + + def initializePage(self): + """ + inits wizard page + """ + provider = unicode(self.field('provider_domain')) + if provider: + # here we should have provider + # but in tests we might not. + + # XXX this error causes a segfault on free() + # that we might want to get fixed ... + #self.setSubTitle( + #self.tr("Register a new user with provider %s.") % + #provider) + self.setSubTitle( + self.tr("Register a new user with provider <em>%s</em>" % + provider)) + self.validationMsg.setText('') + self.userPassword2LineEdit.setText('') + self.valFrame.hide() + + def nextId(self): + wizard = self.wizard() + return wizard.get_page_index('connect') diff --git a/src/leap/gui/firstrun/tests/integration/fake_provider.py b/src/leap/gui/firstrun/tests/integration/fake_provider.py new file mode 100755 index 00000000..668db5d1 --- /dev/null +++ b/src/leap/gui/firstrun/tests/integration/fake_provider.py @@ -0,0 +1,302 @@ +#!/usr/bin/env python +"""A server faking some of the provider resources and apis, +used for testing Leap Client requests + +It needs that you create a subfolder named 'certs', +and that you place the following files: + +[ ] certs/leaptestscert.pem +[ ] certs/leaptestskey.pem +[ ] certs/cacert.pem +[ ] certs/openvpn.pem + +[ ] provider.json +[ ] eip-service.json +""" +# XXX NOTE: intended for manual debug. +# I intend to include this as a regular test after 0.2.0 release +# (so we can add twisted as a dep there) +import binascii +import json +import os +import sys + +# python SRP LIB (! important MUST be >=1.0.1 !) +import srp + +# GnuTLS Example -- is not working as expected +#from gnutls import crypto +#from gnutls.constants import COMP_LZO, COMP_DEFLATE, COMP_NULL +#from gnutls.interfaces.twisted import X509Credentials + +# Going with OpenSSL as a workaround instead +# But we DO NOT want to introduce this dependency. +from OpenSSL import SSL + +from zope.interface import Interface, Attribute, implements + +from twisted.web.server import Site +from twisted.web.static import File +from twisted.web.resource import Resource +from twisted.internet import reactor + +from leap.testing.https_server import where + +# See +# http://twistedmatrix.com/documents/current/web/howto/web-in-60/index.htmln +# for more examples + +""" +Testing the FAKE_API: +##################### + + 1) register an user + >> curl -d "user[login]=me" -d "user[password_salt]=foo" \ + -d "user[password_verifier]=beef" http://localhost:8000/1/users.json + << {"errors": null} + + 2) check that if you try to register again, it will fail: + >> curl -d "user[login]=me" -d "user[password_salt]=foo" \ + -d "user[password_verifier]=beef" http://localhost:8000/1/users.json + << {"errors": {"login": "already taken!"}} + +""" + +# Globals to mock user/sessiondb + +USERDB = {} +SESSIONDB = {} + + +safe_unhexlify = lambda x: binascii.unhexlify(x) \ + if (len(x) % 2 == 0) else binascii.unhexlify('0' + x) + + +class IUser(Interface): + login = Attribute("User login.") + salt = Attribute("Password salt.") + verifier = Attribute("Password verifier.") + session = Attribute("Session.") + svr = Attribute("Server verifier.") + + +class User(object): + implements(IUser) + + def __init__(self, login, salt, verifier): + self.login = login + self.salt = salt + self.verifier = verifier + self.session = None + + def set_server_verifier(self, svr): + self.svr = svr + + def set_session(self, session): + SESSIONDB[session] = self + self.session = session + + +class FakeUsers(Resource): + def __init__(self, name): + self.name = name + + def render_POST(self, request): + args = request.args + + login = args['user[login]'][0] + salt = args['user[password_salt]'][0] + verifier = args['user[password_verifier]'][0] + + if login in USERDB: + return "%s\n" % json.dumps( + {'errors': {'login': 'already taken!'}}) + + print login, verifier, salt + user = User(login, salt, verifier) + USERDB[login] = user + return json.dumps({'errors': None}) + + +def get_user(request): + login = request.args.get('login') + if login: + user = USERDB.get(login[0], None) + if user: + return user + + session = request.getSession() + user = SESSIONDB.get(session, None) + return user + + +class FakeSession(Resource): + def __init__(self, name): + self.name = name + + def render_GET(self, request): + return "%s\n" % json.dumps({'errors': None}) + + def render_POST(self, request): + + user = get_user(request) + + if not user: + # XXX get real error from demo provider + return json.dumps({'errors': 'no such user'}) + + A = request.args['A'][0] + + _A = safe_unhexlify(A) + _salt = safe_unhexlify(user.salt) + _verifier = safe_unhexlify(user.verifier) + + svr = srp.Verifier( + user.login, + _salt, + _verifier, + _A, + hash_alg=srp.SHA256, + ng_type=srp.NG_1024) + + s, B = svr.get_challenge() + + _B = binascii.hexlify(B) + + print 'login = %s' % user.login + print 'salt = %s' % user.salt + print 'len(_salt) = %s' % len(_salt) + print 'vkey = %s' % user.verifier + print 'len(vkey) = %s' % len(_verifier) + print 's = %s' % binascii.hexlify(s) + print 'B = %s' % _B + print 'len(B) = %s' % len(_B) + + session = request.getSession() + user.set_session(session) + user.set_server_verifier(svr) + + # yep, this is tricky. + # some things are *already* unhexlified. + data = { + 'salt': user.salt, + 'B': _B, + 'errors': None} + + return json.dumps(data) + + def render_PUT(self, request): + + # XXX check session??? + user = get_user(request) + + if not user: + print 'NO USER' + return json.dumps({'errors': 'no such user'}) + + data = request.content.read() + auth = data.split("client_auth=") + M = auth[1] if len(auth) > 1 else None + # if not H, return + if not M: + return json.dumps({'errors': 'no M proof passed by client'}) + + svr = user.svr + HAMK = svr.verify_session(binascii.unhexlify(M)) + if HAMK is None: + print 'verification failed!!!' + raise Exception("Authentication failed!") + #import ipdb;ipdb.set_trace() + + assert svr.authenticated() + print "***" + print 'server authenticated user SRP!' + print "***" + + return json.dumps( + {'M2': binascii.hexlify(HAMK), 'errors': None}) + + +class API_Sessions(Resource): + def getChild(self, name, request): + return FakeSession(name) + + +def get_certs_path(): + script_path = os.path.realpath(os.path.dirname(sys.argv[0])) + certs_path = os.path.join(script_path, 'certs') + return certs_path + + +def get_TLS_credentials(): + # XXX this is giving errors + # XXX REview! We want to use gnutls! + + cert = crypto.X509Certificate( + open(where('leaptestscert.pem')).read()) + key = crypto.X509PrivateKey( + open(where('leaptestskey.pem')).read()) + ca = crypto.X509Certificate( + open(where('cacert.pem')).read()) + #crl = crypto.X509CRL(open(certs_path + '/crl.pem').read()) + #cred = crypto.X509Credentials(cert, key, [ca], [crl]) + cred = X509Credentials(cert, key, [ca]) + cred.verify_peer = True + cred.session_params.compressions = (COMP_LZO, COMP_DEFLATE, COMP_NULL) + return cred + + +class OpenSSLServerContextFactory: + # XXX workaround for broken TLS interface + # from gnuTLS. + + def getContext(self): + """Create an SSL context. + This is a sample implementation that loads a certificate from a file + called 'server.pem'.""" + + ctx = SSL.Context(SSL.SSLv23_METHOD) + #certs_path = get_certs_path() + #ctx.use_certificate_file(certs_path + '/leaptestscert.pem') + #ctx.use_privatekey_file(certs_path + '/leaptestskey.pem') + ctx.use_certificate_file(where('leaptestscert.pem')) + ctx.use_privatekey_file(where('leaptestskey.pem')) + return ctx + + +def serve_fake_provider(): + root = Resource() + root.putChild("provider.json", File("./provider.json")) + config = Resource() + config.putChild( + "eip-service.json", + File("./eip-service.json")) + apiv1 = Resource() + apiv1.putChild("config", config) + apiv1.putChild("sessions.json", API_Sessions()) + apiv1.putChild("users.json", FakeUsers(None)) + apiv1.putChild("cert", File(get_certs_path() + '/openvpn.pem')) + root.putChild("1", apiv1) + + cred = get_TLS_credentials() + + factory = Site(root) + + # regular http (for debugging with curl) + reactor.listenTCP(8000, factory) + + # TLS with gnutls --- seems broken :( + #reactor.listenTLS(8003, factory, cred) + + # OpenSSL + reactor.listenSSL(8443, factory, OpenSSLServerContextFactory()) + + reactor.run() + + +if __name__ == "__main__": + + from twisted.python import log + log.startLogging(sys.stdout) + + serve_fake_provider() diff --git a/src/leap/gui/firstrun/wizard.py b/src/leap/gui/firstrun/wizard.py new file mode 100755 index 00000000..f198dca0 --- /dev/null +++ b/src/leap/gui/firstrun/wizard.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python +import logging + +import sip +try: + sip.setapi('QString', 2) + sip.setapi('QVariant', 2) +except ValueError: + pass + +from PyQt4 import QtCore +from PyQt4 import QtGui + +from leap.base import checks as basechecks +from leap.crypto import leapkeyring +from leap.eip import checks as eipchecks + +from leap.gui import firstrun + +from leap.gui import mainwindow_rc + +try: + from collections import OrderedDict +except ImportError: + # We must be in 2.6 + from leap.util.dicts import OrderedDict + +logger = logging.getLogger(__name__) + +""" +~~~~~~~~~~~~~~~~~~~~~~~~~~ +Work in progress! +~~~~~~~~~~~~~~~~~~~~~~~~~~ +This wizard still needs to be refactored out. + +TODO-ish: + +[X] Break file in wizard / pages files (and its own folder). +[ ] Separate presentation from logic. +[ ] Have a "manager" class for connections, that can be + dep-injected for testing. +[ ] Document signals used / expected. +[ ] Separate style from widgets. +[ ] Fix TOFU Widget for provider cert. +[X] Refactor widgets out. +[ ] Follow more MVC style. +[ ] Maybe separate "first run wizard" into different wizards + that share some of the pages? +""" + + +def get_pages_dict(): + return OrderedDict(( + ('intro', firstrun.intro.IntroPage), + ('providerselection', + firstrun.providerselect.SelectProviderPage), + ('login', firstrun.login.LogInPage), + ('providerinfo', firstrun.providerinfo.ProviderInfoPage), + ('providersetupvalidation', + firstrun.providersetup.ProviderSetupValidationPage), + ('signup', firstrun.register.RegisterUserPage), + ('connect', + firstrun.connect.ConnectionPage), + ('lastpage', firstrun.last.LastPage) + )) + + +class FirstRunWizard(QtGui.QWizard): + + def __init__( + self, + conductor_instance, + parent=None, + pages_dict=None, + username=None, + providers=None, + success_cb=None, is_provider_setup=False, + trusted_certs=None, + netchecker=basechecks.LeapNetworkChecker, + providercertchecker=eipchecks.ProviderCertChecker, + eipconfigchecker=eipchecks.EIPConfigChecker, + start_eipconnection_signal=None, + eip_statuschange_signal=None, + debug_server=None, + quitcallback=None): + super(FirstRunWizard, self).__init__( + parent, + QtCore.Qt.WindowStaysOnTopHint) + + # we keep a reference to the conductor + # to be able to launch eip checks and connection + # in the connection page, before the wizard has ended. + self.conductor = conductor_instance + + self.username = username + self.providers = providers + + # success callback + self.success_cb = success_cb + + # is provider setup? + self.is_provider_setup = is_provider_setup + + # a dict with trusted fingerprints + # in the form {'nospacesfingerprint': ['host1', 'host2']} + self.trusted_certs = trusted_certs + + # Checkers + self.netchecker = netchecker + self.providercertchecker = providercertchecker + self.eipconfigchecker = eipconfigchecker + + # debug server + self.debug_server = debug_server + + # Signals + # will be emitted in connecting page + self.start_eipconnection_signal = start_eipconnection_signal + self.eip_statuschange_signal = eip_statuschange_signal + + if quitcallback is not None: + self.button( + QtGui.QWizard.CancelButton).clicked.connect( + quitcallback) + + self.providerconfig = None + # previously registered + # if True, jumps to LogIn page. + # by setting 1st page?? + #self.is_previously_registered = is_previously_registered + # XXX ??? ^v + self.is_previously_registered = bool(self.username) + self.from_login = False + + pages_dict = pages_dict or get_pages_dict() + self.add_pages_from_dict(pages_dict) + + self.validation_errors = {} + self.openvpn_status = [] + + self.setPixmap( + QtGui.QWizard.BannerPixmap, + QtGui.QPixmap(':/images/banner.png')) + self.setPixmap( + QtGui.QWizard.BackgroundPixmap, + QtGui.QPixmap(':/images/background.png')) + + # set options + self.setOption(QtGui.QWizard.IndependentPages, on=False) + self.setOption(QtGui.QWizard.NoBackButtonOnStartPage, on=True) + + self.setWindowTitle("First Run Wizard") + + # TODO: set style for MAC / windows ... + #self.setWizardStyle() + + # + # setup pages in wizard + # + + def add_pages_from_dict(self, pages_dict): + """ + @param pages_dict: the dictionary with pages, where + values are a tuple of InstanceofWizardPage, kwargs. + @type pages_dict: dict + """ + for name, page in pages_dict.items(): + # XXX check for is_previously registered + # and skip adding the signup branch if so + self.addPage(page()) + self.pages_dict = pages_dict + + def get_page_index(self, page_name): + """ + returns the index of the given page + @param page_name: the name of the desired page + @type page_name: str + @rparam: index of page in wizard + @rtype: int + """ + return self.pages_dict.keys().index(page_name) + + # + # validation errors + # + + def set_validation_error(self, pagename, error): + self.validation_errors[pagename] = error + + def clean_validation_error(self, pagename): + vald = self.validation_errors + if pagename in vald: + del vald[pagename] + + def get_validation_error(self, pagename): + return self.validation_errors.get(pagename, None) + + def accept(self): + """ + final step in the wizard. + gather the info, update settings + and call the success callback if any has been passed. + """ + super(FirstRunWizard, self).accept() + + # username and password are in different fields + # if they were stored in log_in or sign_up pages. + from_login = self.from_login + unamek_base = 'userName' + passwk_base = 'userPassword' + unamek = 'login_%s' % unamek_base if from_login else unamek_base + passwk = 'login_%s' % passwk_base if from_login else passwk_base + + username = self.field(unamek) + password = self.field(passwk) + provider = self.field('provider_domain') + remember_pass = self.field('rememberPassword') + + logger.debug('chosen provider: %s', provider) + logger.debug('username: %s', username) + logger.debug('remember password: %s', remember_pass) + + # we are assuming here that we only remember one username + # in the form username@provider.domain + # We probably could extend this to support some form of + # profiles. + + settings = QtCore.QSettings() + + settings.setValue("FirstRunWizardDone", True) + settings.setValue("provider_domain", provider) + full_username = "%s@%s" % (username, provider) + + settings.setValue("remember_user_and_pass", remember_pass) + + if remember_pass: + settings.setValue("username", full_username) + seed = self.get_random_str(10) + settings.setValue("%s_seed" % provider, seed) + + # XXX #744: comment out for 0.2.0 release + # if we need to have a version of python-keyring < 0.9 + leapkeyring.leap_set_password( + full_username, password, seed=seed) + + logger.debug('First Run Wizard Done.') + cb = self.success_cb + if cb and callable(cb): + self.success_cb() + + # misc helpers + + def get_random_str(self, n): + """ + returns a random string + :param n: the length of the desired string + :rvalue: str + """ + from string import (ascii_uppercase, ascii_lowercase, digits) + from random import choice + return ''.join(choice( + ascii_uppercase + + ascii_lowercase + + digits) for x in range(n)) + + def set_providerconfig(self, providerconfig): + """ + sets a providerconfig attribute + used when we fetch and parse a json configuration + """ + self.providerconfig = providerconfig + + def get_provider_by_index(self): # pragma: no cover + """ + returns the value of a provider given its index. + this was used in the select provider page, + in the case where we were preseeding providers in a combobox + """ + # Leaving it here for the moment when we go back at the + # option of preseeding with known provider values. + provider = self.field('provider_index') + return self.providers[provider] + + +if __name__ == '__main__': + # standalone test + # it can be (somehow) run against + # gui/tests/integration/fake_user_signup.py + + import sys + import logging + logging.basicConfig() + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + + app = QtGui.QApplication(sys.argv) + server = sys.argv[1] if len(sys.argv) > 1 else None + + trusted_certs = { + "3DF83F316BFA0186" + "0A11A5C9C7FC24B9" + "18C62B941192CC1A" + "49AE62218B2A4B7C": ['springbok']} + + wizard = FirstRunWizard( + None, trusted_certs=trusted_certs, + debug_server=server) + wizard.show() + sys.exit(app.exec_()) diff --git a/src/leap/gui/firstrunwizard.py b/src/leap/gui/firstrunwizard.py deleted file mode 100755 index a76865fd..00000000 --- a/src/leap/gui/firstrunwizard.py +++ /dev/null @@ -1,507 +0,0 @@ -#!/usr/bin/env python -import logging -import json -import socket - -import sip -sip.setapi('QString', 2) -sip.setapi('QVariant', 2) - -from PyQt4 import QtCore -from PyQt4 import QtGui - -from leap.crypto import leapkeyring -from leap.gui import mainwindow_rc - -logger = logging.getLogger(__name__) - -APP_LOGO = ':/images/leap-color-small.png' - -# registration ###################### -# move to base/ -import binascii - -import requests -import srp - -from leap.base import constants as baseconstants - -SIGNUP_TIMEOUT = getattr(baseconstants, 'SIGNUP_TIMEOUT', 5) - - -class LeapSRPRegister(object): - - def __init__(self, - schema="https", - provider=None, - port=None, - register_path="1/users.json", - method="POST", - fetcher=requests, - srp=srp, - hashfun=srp.SHA256, - ng_constant=srp.NG_1024): - - self.schema = schema - self.provider = provider - self.port = port - self.register_path = register_path - self.method = method - self.fetcher = fetcher - self.srp = srp - self.HASHFUN = hashfun - self.NG = ng_constant - - self.init_session() - - def init_session(self): - self.session = self.fetcher.session() - - def get_registration_uri(self): - # XXX assert is https! - # use urlparse - if self.port: - uri = "%s://%s:%s/%s" % ( - self.schema, - self.provider, - self.port, - self.register_path) - else: - uri = "%s://%s/%s" % ( - self.schema, - self.provider, - self.register_path) - - return uri - - def register_user(self, username, password, keep=False): - """ - @rtype: tuple - @rvalue: (ok, request) - """ - salt, vkey = self.srp.create_salted_verification_key( - username, - password, - self.HASHFUN, - self.NG) - - user_data = { - 'user[login]': username, - 'user[password_verifier]': binascii.hexlify(vkey), - 'user[password_salt]': binascii.hexlify(salt)} - - uri = self.get_registration_uri() - logger.debug('post to uri: %s' % uri) - - # XXX get self.method - req = self.session.post( - uri, data=user_data, - timeout=SIGNUP_TIMEOUT) - logger.debug(req) - logger.debug('user_data: %s', user_data) - #logger.debug('response: %s', req.text) - # we catch it in the form - #req.raise_for_status() - return (req.ok, req) - -###################################### - -ErrorLabelStyleSheet = """ -QLabel { color: red; - font-weight: bold} -""" - - -class FirstRunWizard(QtGui.QWizard): - - def __init__( - self, parent=None, providers=None, - success_cb=None): - super(FirstRunWizard, self).__init__( - parent, - QtCore.Qt.WindowStaysOnTopHint) - - # XXX hardcoded for tests - if not providers: - providers = ('springbok',) - self.providers = providers - - # success callback - self.success_cb = success_cb - - self.addPage(IntroPage()) - self.addPage(SelectProviderPage(providers=providers)) - - self.addPage(RegisterUserPage(wizard=self)) - #self.addPage(GlobalEIPSettings()) - self.addPage(LastPage()) - - self.setPixmap( - QtGui.QWizard.BannerPixmap, - QtGui.QPixmap(':/images/banner.png')) - self.setPixmap( - QtGui.QWizard.BackgroundPixmap, - QtGui.QPixmap(':/images/background.png')) - - self.setWindowTitle("First Run Wizard") - - # TODO: set style for MAC / windows ... - #self.setWizardStyle() - - def setWindowFlags(self, flags): - logger.debug('setting window flags') - QtGui.QWizard.setWindowFlags(self, flags) - - def focusOutEvent(self, event): - # needed ? - self.setFocus(True) - self.activateWindow() - self.raise_() - self.show() - - def accept(self): - """ - final step in the wizard. - gather the info, update settings - and call the success callback. - """ - provider = self.get_provider() - username = self.field('userName') - #password = self.field('userPassword') - remember_pass = self.field('rememberPassword') - - logger.debug('chosen provider: %s', provider) - logger.debug('username: %s', username) - logger.debug('remember password: %s', remember_pass) - super(FirstRunWizard, self).accept() - - settings = QtCore.QSettings() - settings.setValue("FirstRunWizardDone", True) - settings.setValue( - "eip_%s_username" % provider, - username) - settings.setValue("%s_remember_pass" % provider, remember_pass) - - seed = self.get_random_str(10) - settings.setValue("%s_seed" % provider, seed) - - # Commenting out for 0.2.0 release - # since we did not fix #744 on time. - - #leapkeyring.leap_set_password(username, password, seed=seed) - - logger.debug('First Run Wizard Done.') - cb = self.success_cb - if cb and callable(cb): - self.success_cb() - - def get_provider(self): - provider = self.field('provider_index') - return self.providers[provider] - - def get_random_str(self, n): - from string import (ascii_uppercase, ascii_lowercase, digits) - from random import choice - return ''.join(choice( - ascii_uppercase + - ascii_lowercase + - digits) for x in range(n)) - - -class IntroPage(QtGui.QWizardPage): - def __init__(self, parent=None): - super(IntroPage, self).__init__(parent) - - self.setTitle("First run wizard.") - - #self.setPixmap( - #QtGui.QWizard.WatermarkPixmap, - #QtGui.QPixmap(':/images/watermark1.png')) - - label = QtGui.QLabel( - "Now we will guide you through " - "some configuration that is needed before you " - "can connect for the first time.<br><br>" - "If you ever need to modify these options again, " - "you can find the wizard in the '<i>Settings</i>' menu from the " - "main window of the Leap App.") - - label.setWordWrap(True) - - layout = QtGui.QVBoxLayout() - layout.addWidget(label) - self.setLayout(layout) - - -class SelectProviderPage(QtGui.QWizardPage): - def __init__(self, parent=None, providers=None): - super(SelectProviderPage, self).__init__(parent) - - self.setTitle("Select Provider") - self.setSubTitle( - "Please select which provider do you want " - "to use for your connection." - ) - self.setPixmap( - QtGui.QWizard.LogoPixmap, - QtGui.QPixmap(APP_LOGO)) - - providerNameLabel = QtGui.QLabel("&Provider:") - - providercombo = QtGui.QComboBox() - if providers: - for provider in providers: - providercombo.addItem(provider) - providerNameSelect = providercombo - - providerNameLabel.setBuddy(providerNameSelect) - - self.registerField('provider_index', providerNameSelect) - - layout = QtGui.QGridLayout() - layout.addWidget(providerNameLabel, 0, 0) - layout.addWidget(providerNameSelect, 0, 1) - self.setLayout(layout) - - -class RegisterUserPage(QtGui.QWizardPage): - setSigningUpStatus = QtCore.pyqtSignal([]) - - def __init__(self, parent=None, wizard=None): - super(RegisterUserPage, self).__init__(parent) - - # bind wizard page signals - self.setSigningUpStatus.connect( - self.set_status_validating) - - # XXX check for no wizard pased - # getting provider from previous step - provider = wizard.get_provider() - - self.setTitle("User registration") - self.setSubTitle( - "Register a new user with provider %s." % - provider) - self.setPixmap( - QtGui.QWizard.LogoPixmap, - QtGui.QPixmap(APP_LOGO)) - - rememberPasswordCheckBox = QtGui.QCheckBox( - "&Remember password.") - rememberPasswordCheckBox.setChecked(True) - - userNameLabel = QtGui.QLabel("User &name:") - userNameLineEdit = QtGui.QLineEdit() - userNameLineEdit.cursorPositionChanged.connect( - self.reset_validation_status) - userNameLabel.setBuddy(userNameLineEdit) - - # add regex validator - usernameRe = QtCore.QRegExp(r"^[A-Za-z\d_]+$") - userNameLineEdit.setValidator( - QtGui.QRegExpValidator(usernameRe, self)) - self.userNameLineEdit = userNameLineEdit - - userPasswordLabel = QtGui.QLabel("&Password:") - self.userPasswordLineEdit = QtGui.QLineEdit() - self.userPasswordLineEdit.setEchoMode( - QtGui.QLineEdit.Password) - - userPasswordLabel.setBuddy(self.userPasswordLineEdit) - - self.registerField('userName', self.userNameLineEdit) - self.registerField('userPassword', self.userPasswordLineEdit) - self.registerField('rememberPassword', rememberPasswordCheckBox) - - layout = QtGui.QGridLayout() - layout.setColumnMinimumWidth(0, 20) - - validationMsg = QtGui.QLabel("") - validationMsg.setStyleSheet(ErrorLabelStyleSheet) - - self.validationMsg = validationMsg - - layout.addWidget(validationMsg, 0, 3) - - layout.addWidget(userNameLabel, 1, 0) - layout.addWidget(self.userNameLineEdit, 1, 3) - - layout.addWidget(userPasswordLabel, 2, 0) - layout.addWidget(self.userPasswordLineEdit, 2, 3) - - layout.addWidget(rememberPasswordCheckBox, 3, 3, 3, 4) - self.setLayout(layout) - - def reset_validation_status(self): - """ - empty the validation msg - """ - self.validationMsg.setText('') - - def set_status_validating(self): - """ - set validation msg to 'registering...' - """ - # XXX this is NOT WORKING. - # My guess is that, even if we are using - # signals to trigger this, it does - # not show until the validate function - # returns. - # I guess it is because there is no delay... - logger.debug('registering........') - self.validationMsg.setText('registering...') - # need to call update somehow??? - - def set_status_invalid_username(self): - """ - set validation msg to - not available user - """ - self.validationMsg.setText('Username not available.') - - def set_status_server_500(self): - """ - set validation msg to - internal server error - """ - self.validationMsg.setText("Error during registration (500)") - - def set_status_timeout(self): - """ - set validation msg to - timeout - """ - self.validationMsg.setText("Error connecting to provider (timeout)") - - def set_status_connerror(self): - """ - set validation msg to - connection refused - """ - self.validationMsg.setText( - "Error connecting to provider " - "(connection error)") - - def set_status_unknown_error(self): - """ - set validation msg to - unknown error - """ - self.validationMsg.setText("Error during signup") - - # overwritten methods - - def initializePage(self): - """ - inits wizard page - """ - self.validationMsg.setText('') - - def validatePage(self): - """ - validation - we initialize the srp protocol register - and try to register user. if error - returned we write validation error msg - above the form. - """ - # the slot for this signal is not doing - # what's expected. Investigate why, - # right now we're not giving any feedback - # to the user re. what's going on. The only - # thing I can see as a workaround is setting - # a low timeout. - self.setSigningUpStatus.emit() - - username = self.userNameLineEdit.text() - password = self.userPasswordLineEdit.text() - - # XXX TODO -- remove debug info - # XXX get from provider info - # XXX enforce https - # and pass a verify value - - signup = LeapSRPRegister( - schema="http", - provider="springbok", - - # debug ----- - #provider="localhost", - #register_path="timeout", - #port=8000 - ) - try: - ok, req = signup.register_user(username, password) - except socket.timeout: - self.set_status_timeout() - return False - - except requests.exceptions.ConnectionError as exc: - logger.error(exc) - self.set_status_connerror() - return False - - if ok: - return True - - # something went wrong. - # not registered, let's catch what. - # get timeout - # ... - if req.status_code == 500: - self.set_status_server_500() - return False - - validation_msgs = json.loads(req.content) - logger.debug('validation errors: %s' % validation_msgs) - errors = validation_msgs.get('errors', None) - if errors and errors.get('login', None): - self.set_status_invalid_username() - else: - self.set_status_unknown_error() - return False - - -class GlobalEIPSettings(QtGui.QWizardPage): - def __init__(self, parent=None): - super(GlobalEIPSettings, self).__init__(parent) - - -class LastPage(QtGui.QWizardPage): - def __init__(self, parent=None): - super(LastPage, self).__init__(parent) - - self.setTitle("Ready to go!") - - #self.setPixmap( - #QtGui.QWizard.WatermarkPixmap, - #QtGui.QPixmap(':/images/watermark2.png')) - - self.label = QtGui.QLabel() - self.label.setWordWrap(True) - - layout = QtGui.QVBoxLayout() - layout.addWidget(self.label) - self.setLayout(layout) - - def initializePage(self): - finishText = self.wizard().buttonText( - QtGui.QWizard.FinishButton) - finishText = finishText.replace('&', '') - self.label.setText( - "Click '<i>%s</i>' to end the wizard and start " - "encrypting your connection." % finishText) - - -if __name__ == '__main__': - # standalone test - import sys - import logging - logging.basicConfig() - logger = logging.getLogger() - logger.setLevel(logging.DEBUG) - - app = QtGui.QApplication(sys.argv) - wizard = FirstRunWizard() - wizard.show() - sys.exit(app.exec_()) diff --git a/src/leap/gui/locale_rc.py b/src/leap/gui/locale_rc.py new file mode 100644 index 00000000..f165ff8e --- /dev/null +++ b/src/leap/gui/locale_rc.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- + +# Resource object code +# +# Created: vie nov 16 22:33:33 2012 +# by: The Resource Compiler for PyQt (Qt v4.8.2) +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore + +qt_resource_data = "\ +\x00\x00\x05\xaa\ +\x3c\ +\xb8\x64\x18\xca\xef\x9c\x95\xcd\x21\x1c\xbf\x60\xa1\xbd\xdd\x42\ +\x00\x00\x00\x20\x09\xfc\x2c\x8e\x00\x00\x04\xfb\x0a\x74\xb8\x1e\ +\x00\x00\x00\xd6\x0a\xfd\x99\xfe\x00\x00\x00\x51\x0c\x44\x41\xbe\ +\x00\x00\x00\x00\x69\x00\x00\x05\x69\x03\x00\x00\x00\x22\x00\x50\ +\x00\x72\x00\x69\x00\x6d\x00\x65\x00\x72\x00\x61\x00\x20\x00\x63\ +\x00\x6f\x00\x6e\x00\x65\x00\x78\x00\x69\x00\x6f\x00\x6e\x00\x2e\ +\x08\x00\x00\x00\x00\x06\x00\x00\x00\x11\x46\x69\x72\x73\x74\x20\ +\x72\x75\x6e\x20\x77\x69\x7a\x61\x72\x64\x2e\x07\x00\x00\x00\x09\ +\x49\x6e\x74\x72\x6f\x50\x61\x67\x65\x01\x03\x00\x00\x00\x4c\x00\ +\x4c\x00\x6f\x00\x67\x00\x75\x00\x65\x00\x61\x00\x72\x00\x6d\x00\ +\x65\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\x20\x00\x6d\x00\x69\x00\ +\x20\x00\x75\x00\x73\x00\x75\x00\x61\x00\x72\x00\x69\x00\x6f\x00\ +\x20\x00\x79\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\x74\x00\x72\x00\ +\x61\x00\x73\x00\x65\x00\x6e\x00\x61\x00\x2e\x08\x00\x00\x00\x00\ +\x06\x00\x00\x00\x1b\x4c\x6f\x67\x20\x49\x6e\x20\x77\x69\x74\x68\ +\x20\x6d\x79\x20\x63\x72\x65\x64\x65\x6e\x74\x69\x61\x6c\x73\x2e\ +\x07\x00\x00\x00\x09\x49\x6e\x74\x72\x6f\x50\x61\x67\x65\x01\x03\ +\x00\x00\x02\xaa\x00\x56\x00\x61\x00\x6d\x00\x6f\x00\x73\x00\x20\ +\x00\x61\x00\x20\x00\x72\x00\x65\x00\x75\x00\x6e\x00\x69\x00\x72\ +\x00\x20\x00\x6c\x00\x61\x00\x20\x00\x69\x00\x6e\x00\x66\x00\x6f\ +\x00\x72\x00\x6d\x00\x61\x00\x63\x00\x69\x00\x6f\x00\x6e\x00\x20\ +\x00\x71\x00\x75\x00\x65\x00\x20\x00\x6e\x00\x65\x00\x63\x00\x65\ +\x00\x73\x00\x69\x00\x74\x00\x61\x00\x73\x00\x20\x00\x61\x00\x6e\ +\x00\x74\x00\x65\x00\x73\x00\x20\x00\x64\x00\x65\x00\x20\x00\x6c\ +\x00\x61\x00\x20\x00\x70\x00\x72\x00\x69\x00\x6d\x00\x65\x00\x72\ +\x00\x61\x00\x20\x00\x63\x00\x6f\x00\x6e\x00\x65\x00\x78\x00\x69\ +\x00\x6f\x00\x6e\x00\x2e\x00\x3c\x00\x62\x00\x72\x00\x3e\x00\x3c\ +\x00\x62\x00\x72\x00\x3e\x00\x53\x00\x69\x00\x20\x00\x61\x00\x6c\ +\x00\x67\x00\x75\x00\x6e\x00\x61\x00\x20\x00\x76\x00\x65\x00\x7a\ +\x00\x20\x00\x6e\x00\x65\x00\x63\x00\x65\x00\x73\x00\x69\x00\x74\ +\x00\x61\x00\x73\x00\x20\x00\x6d\x00\x6f\x00\x64\x00\x69\x00\x66\ +\x00\x69\x00\x63\x00\x61\x00\x72\x00\x20\x00\x65\x00\x73\x00\x74\ +\x00\x61\x00\x73\x00\x20\x00\x6f\x00\x70\x00\x63\x00\x69\x00\x6f\ +\x00\x6e\x00\x65\x00\x73\x00\x20\x00\x64\x00\x65\x00\x20\x00\x6e\ +\x00\x75\x00\x65\x00\x76\x00\x6f\x00\x2c\x00\x20\x00\x70\x00\x75\ +\x00\x65\x00\x64\x00\x65\x00\x73\x00\x20\x00\x65\x00\x6e\x00\x63\ +\x00\x6f\x00\x6e\x00\x74\x00\x72\x00\x61\x00\x72\x00\x20\x00\x65\ +\x00\x73\x00\x74\x00\x65\x00\x20\x00\x61\x00\x73\x00\x69\x00\x73\ +\x00\x74\x00\x65\x00\x6e\x00\x74\x00\x65\x00\x20\x00\x65\x00\x6e\ +\x00\x20\x00\x65\x00\x6c\x00\x20\x00\x6d\x00\x65\x00\x6e\x00\x75\ +\x00\x20\x00\x3c\x00\x69\x00\x3e\x00\x4f\x00\x70\x00\x63\x00\x69\ +\x00\x6f\x00\x6e\x00\x65\x00\x73\x00\x3c\x00\x2f\x00\x69\x00\x3e\ +\x00\x20\x00\x65\x00\x6e\x00\x20\x00\x6c\x00\x61\x00\x20\x00\x76\ +\x00\x65\x00\x6e\x00\x74\x00\x61\x00\x6e\x00\x61\x00\x20\x00\x70\ +\x00\x72\x00\x69\x00\x6e\x00\x63\x00\x69\x00\x70\x00\x61\x00\x6c\ +\x00\x2e\x00\x3c\x00\x62\x00\x72\x00\x3e\x00\x3c\x00\x62\x00\x72\ +\x00\x3e\x00\x51\x00\x75\x00\x65\x00\x20\x00\x64\x00\x65\x00\x73\ +\x00\x65\x00\x61\x00\x73\x00\x20\x00\x68\x00\x61\x00\x63\x00\x65\ +\x00\x72\x00\x20\x00\x61\x00\x68\x00\x6f\x00\x72\x00\x61\x00\x3f\ +\x00\x20\x00\x50\x00\x75\x00\x65\x00\x64\x00\x65\x00\x73\x00\x20\ +\x00\x3c\x00\x62\x00\x3e\x00\x72\x00\x65\x00\x67\x00\x69\x00\x73\ +\x00\x74\x00\x72\x00\x61\x00\x72\x00\x3c\x00\x2f\x00\x62\x00\x3e\ +\x00\x20\x00\x75\x00\x6e\x00\x61\x00\x20\x00\x6e\x00\x75\x00\x65\ +\x00\x76\x00\x61\x00\x20\x00\x63\x00\x75\x00\x65\x00\x6e\x00\x74\ +\x00\x61\x00\x20\x00\x6f\x00\x20\x00\x3c\x00\x62\x00\x3e\x00\x6c\ +\x00\x6f\x00\x67\x00\x75\x00\x65\x00\x61\x00\x72\x00\x74\x00\x65\ +\x00\x3c\x00\x2f\x00\x62\x00\x3e\x00\x20\x00\x63\x00\x6f\x00\x6e\ +\x00\x20\x00\x75\x00\x6e\x00\x61\x00\x20\x00\x71\x00\x75\x00\x65\ +\x00\x20\x00\x79\x00\x61\x00\x20\x00\x74\x00\x69\x00\x65\x00\x6e\ +\x00\x65\x00\x73\x00\x3f\x00\x3c\x00\x62\x00\x72\x00\x3e\x08\x00\ +\x00\x00\x00\x06\x00\x00\x01\x5d\x4e\x6f\x77\x20\x77\x65\x20\x77\ +\x69\x6c\x6c\x20\x67\x75\x69\x64\x65\x20\x79\x6f\x75\x20\x74\x68\ +\x72\x6f\x75\x67\x68\x20\x73\x6f\x6d\x65\x20\x63\x6f\x6e\x66\x69\ +\x67\x75\x72\x61\x74\x69\x6f\x6e\x20\x74\x68\x61\x74\x20\x69\x73\ +\x20\x6e\x65\x65\x64\x65\x64\x20\x62\x65\x66\x6f\x72\x65\x20\x79\ +\x6f\x75\x20\x63\x61\x6e\x20\x63\x6f\x6e\x6e\x65\x63\x74\x20\x66\ +\x6f\x72\x20\x74\x68\x65\x20\x66\x69\x72\x73\x74\x20\x74\x69\x6d\ +\x65\x2e\x3c\x62\x72\x3e\x3c\x62\x72\x3e\x49\x66\x20\x79\x6f\x75\ +\x20\x65\x76\x65\x72\x20\x6e\x65\x65\x64\x20\x74\x6f\x20\x6d\x6f\ +\x64\x69\x66\x79\x20\x74\x68\x65\x73\x65\x20\x6f\x70\x74\x69\x6f\ +\x6e\x73\x20\x61\x67\x61\x69\x6e\x2c\x20\x79\x6f\x75\x20\x63\x61\ +\x6e\x20\x66\x69\x6e\x64\x20\x74\x68\x65\x20\x77\x69\x7a\x61\x72\ +\x64\x20\x69\x6e\x20\x74\x68\x65\x20\x27\x3c\x69\x3e\x53\x65\x74\ +\x74\x69\x6e\x67\x73\x3c\x2f\x69\x3e\x27\x20\x6d\x65\x6e\x75\x20\ +\x66\x72\x6f\x6d\x20\x74\x68\x65\x20\x6d\x61\x69\x6e\x20\x77\x69\ +\x6e\x64\x6f\x77\x2e\x3c\x62\x72\x3e\x3c\x62\x72\x3e\x44\x6f\x20\ +\x79\x6f\x75\x20\x77\x61\x6e\x74\x20\x74\x6f\x20\x3c\x62\x3e\x73\ +\x69\x67\x6e\x20\x75\x70\x3c\x2f\x62\x3e\x20\x66\x6f\x72\x20\x61\ +\x20\x6e\x65\x77\x20\x61\x63\x63\x6f\x75\x6e\x74\x2c\x20\x6f\x72\ +\x20\x3c\x62\x3e\x6c\x6f\x67\x20\x69\x6e\x3c\x2f\x62\x3e\x20\x77\ +\x69\x74\x68\x20\x61\x6e\x20\x61\x6c\x72\x65\x61\x64\x79\x20\x65\ +\x78\x69\x73\x74\x69\x6e\x67\x20\x75\x73\x65\x72\x6e\x61\x6d\x65\ +\x3f\x3c\x62\x72\x3e\x07\x00\x00\x00\x09\x49\x6e\x74\x72\x6f\x50\ +\x61\x67\x65\x01\x03\x00\x00\x00\x36\x00\x52\x00\x65\x00\x67\x00\ +\x69\x00\x73\x00\x74\x00\x72\x00\x61\x00\x72\x00\x20\x00\x75\x00\ +\x6e\x00\x61\x00\x20\x00\x63\x00\x75\x00\x65\x00\x6e\x00\x74\x00\ +\x61\x00\x20\x00\x6e\x00\x75\x00\x65\x00\x76\x00\x61\x00\x2e\x08\ +\x00\x00\x00\x00\x06\x00\x00\x00\x1a\x53\x69\x67\x6e\x20\x75\x70\ +\x20\x66\x6f\x72\x20\x61\x20\x6e\x65\x77\x20\x61\x63\x63\x6f\x75\ +\x6e\x74\x2e\x07\x00\x00\x00\x09\x49\x6e\x74\x72\x6f\x50\x61\x67\ +\x65\x01\x88\x00\x00\x00\x02\x01\x01\ +" + +qt_resource_name = "\ +\x00\x0c\ +\x0d\xfc\x11\x13\ +\x00\x74\ +\x00\x72\x00\x61\x00\x6e\x00\x73\x00\x6c\x00\x61\x00\x74\x00\x69\x00\x6f\x00\x6e\x00\x73\ +\x00\x14\ +\x08\xa9\x0f\x1d\ +\x00\x6c\ +\x00\x65\x00\x61\x00\x70\x00\x5f\x00\x63\x00\x6c\x00\x69\x00\x65\x00\x6e\x00\x74\x00\x5f\x00\x65\x00\x73\x00\x5f\x00\x45\x00\x53\ +\x00\x2e\x00\x71\x00\x6d\ +" + +qt_resource_struct = "\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ +\x00\x00\x00\x1e\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +" + +def qInitResources(): + QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +def qCleanupResources(): + QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +qInitResources() diff --git a/src/leap/gui/mainwindow_rc.py b/src/leap/gui/mainwindow_rc.py index be575159..5bee35c7 100644 --- a/src/leap/gui/mainwindow_rc.py +++ b/src/leap/gui/mainwindow_rc.py @@ -2,7 +2,7 @@ # Resource object code # -# Created: Thu Sep 13 16:12:58 2012 +# Created: Wed Nov 21 04:25:36 2012 # by: The Resource Compiler for PyQt (Qt v4.8.2) # # WARNING! All changes made in this file will be lost! @@ -236,6 +236,87 @@ qt_resource_data = "\ \x71\xa4\x40\xda\x14\x7a\xd1\x73\x1f\xf4\x7f\xb7\xf9\x1f\xc2\x26\ \x56\xd5\x70\x45\xfc\x8a\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\ \x60\x82\ +\x00\x00\x04\xec\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x18\x00\x00\x00\x18\x08\x06\x00\x00\x00\xe0\x77\x3d\xf8\ +\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ +\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x06\xec\x00\x00\x06\xec\ +\x01\x1e\x75\x38\x35\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\ +\x74\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\ +\x70\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x00\x13\x74\x45\ +\x58\x74\x41\x75\x74\x68\x6f\x72\x00\x52\x6f\x64\x6e\x65\x79\x20\ +\x44\x61\x77\x65\x73\x0e\xd8\x7e\x1d\x00\x00\x04\x4a\x49\x44\x41\ +\x54\x48\x89\x8d\x96\x5d\x6c\x53\x65\x18\xc7\x7f\xef\x39\x6b\xbb\ +\x7e\x9c\x75\x65\xad\x2b\x9b\xfb\xd0\x31\xdd\x14\xb6\x8c\x19\x44\ +\x90\x44\x63\x82\x42\x88\x5e\x90\x98\xcc\x19\x15\x13\xd4\x18\x76\ +\x61\xd4\x18\xe3\x85\x57\xca\x05\xe1\xc2\x0c\xa3\xa8\x51\xd0\x4c\ +\x12\xe3\x85\x31\x80\x26\x6a\xe2\x85\x23\xb0\x38\xb6\xc1\x1c\xce\ +\xb1\x40\x59\xf6\xe5\xca\xda\xae\xed\xfa\x75\x7a\x5e\x2f\x4e\xd7\ +\x59\xd6\x32\xfe\xc9\x7b\xf3\x9e\xe7\xf9\xff\x9f\xe7\xff\x9e\xf3\ +\x9c\x57\x48\x29\x59\x0f\xbd\x7b\x85\x0d\x17\xed\x1e\xbb\xb2\x07\ +\x20\x94\x30\x7e\x22\xc6\x48\xcf\x59\x99\x5a\x2f\x57\x94\x12\xf8\ +\xec\x55\x61\x71\x65\x6d\x47\xfc\xbe\xda\x47\x9d\x5a\xa5\xbf\xda\ +\x69\xaf\xda\xe0\x28\x2f\x07\x58\x5c\x4e\x26\xe7\xe3\x89\x9b\xf1\ +\x68\x78\x6e\x6e\x61\xfa\x8f\x98\x9a\x7a\xfb\x95\xe3\x32\x73\xc7\ +\x02\x9f\x76\x89\x8e\xba\xda\xda\x2f\xb7\x37\xdf\xdf\xe6\x2a\x13\ +\x8a\x94\x06\x82\xc2\x38\x89\x40\x08\x85\x98\x2e\x8d\xf3\x13\xe3\ +\x97\xa6\xa6\xa7\x5f\x7e\xed\x94\x1c\x5a\x57\xa0\xef\xa0\xfd\x70\ +\x5b\xf3\x96\x03\xcd\xde\x8a\x6a\x61\x64\xd7\x73\xc0\x14\x53\x54\ +\x26\x82\x4b\xf3\x97\x26\x2e\x7f\xd5\xfd\x79\xe2\xdd\x92\x02\x27\ +\x5f\x2a\x7b\xe1\x89\xce\x1d\xc7\xbc\x76\x55\x13\xc5\x98\xac\x4e\ +\x10\x0a\xa4\xa2\x6b\x45\x80\x60\x22\x1b\xfd\x6d\xf0\xdc\xa1\x17\ +\x4f\xe8\x5f\xaf\x11\x38\xfa\x9c\xf0\x6e\xdb\xf4\xc0\xf9\x6d\xf5\ +\xfe\x26\x30\xf2\x89\xca\xc6\x76\xd4\x07\xf7\xa3\xd4\x74\x80\xd5\ +\x65\x6e\xa6\xe3\x64\x03\xfd\x64\x2f\x9e\x40\x46\x67\xff\x27\xa3\ +\x30\x70\x63\x6e\x72\xe0\xea\xd8\xf6\x37\xbf\x95\x41\x73\x27\x87\ +\x06\x8f\xa7\x6f\x6b\x7d\x4d\x01\x39\x80\x52\xff\x08\x4a\xe3\xae\ +\x55\xf2\x5c\x27\x6a\xf3\x6e\x2c\x7b\x8f\x9a\x5d\xe5\x61\xb0\xb5\ +\xbe\xa6\xa9\xc1\xe3\xe9\x5b\x95\x04\x7a\xbb\x44\x47\x5b\x53\xcb\ +\x4e\x15\xbd\x98\x31\xc8\x70\x00\xfd\xfc\xc7\x64\xce\xbc\x81\x7e\ +\xe1\x13\xc8\x75\x2d\xb4\x8d\x28\xb5\x0f\x15\xc4\xaa\xe8\xb4\x35\ +\xb5\xec\xec\xed\x12\x1d\x00\x65\x00\xee\x72\x65\x9f\x5f\x73\x38\ +\x05\x6b\x0f\x35\x3b\xf6\x03\xfa\xc0\xf1\x3c\x29\xb3\xc3\xa8\xf7\ +\x3e\x8e\xf0\xb5\x98\x22\xf6\x0d\x05\xf1\x02\xf0\x6b\x0e\xa7\xbb\ +\x5c\xd9\x07\x0c\x29\x00\x9a\xc3\xd5\x69\x55\xd5\xe2\xd5\x47\xe7\ +\x56\xc9\x01\xe1\xbe\x1b\xe1\xb9\x67\xf5\x79\x70\x7c\x4d\x8e\x55\ +\x55\xd1\x1c\xae\xce\xbc\x45\x15\x6e\x5f\x9d\x90\xc5\xed\x29\xa8\ +\xae\xa2\x06\xcb\x53\x47\xa0\xcc\x66\x76\x37\xfa\x3d\xd9\xa9\x81\ +\xb5\x71\x52\xa7\xc2\xed\xab\x83\x9c\x45\x76\xbb\x56\x25\xa5\xa4\ +\xe8\xab\xb9\x02\x9b\x86\x65\xf7\x87\x08\xcd\x6f\x92\x8f\x9f\x21\ +\xf5\xdd\xf3\xa0\xa7\x10\xe5\x6e\x44\x45\x2d\x38\x7d\x08\x21\x90\ +\xd2\xe4\xcc\x0b\x24\x12\xd1\x9b\x42\xbd\xab\x81\x6c\xba\x28\xb7\ +\x94\x06\x65\xcd\x4f\x22\x2a\x1b\x00\x30\xa6\xff\x24\xd5\xb7\x1f\ +\x74\x73\x14\xc9\x64\x04\x99\x8c\x80\xc5\x8e\xe2\xae\x03\xab\x93\ +\x44\x22\x7a\x33\x6f\xd1\x52\x64\x61\x0a\xb5\xbc\x28\xb1\xb1\x34\ +\x83\x91\xb3\xc1\x98\x1d\xc1\x98\x1d\x41\x3f\xd7\x9b\x27\x2f\x40\ +\x26\x81\x11\xfc\x07\x99\x8a\x99\x9c\x2b\x1d\x44\x97\x63\x83\xc9\ +\xe8\xfc\x33\x36\x23\x05\xaa\x05\xd2\xcb\xc8\x74\xcc\xfc\x88\x72\ +\x5d\xa5\x7f\x3c\x74\x3b\x03\x0b\x90\x52\xed\x44\x97\x63\x83\x79\ +\x81\x48\xd2\x38\x3d\x1b\xcf\xbc\x53\x1f\xb9\xe4\x44\x1a\x45\x93\ +\xac\xcf\x7e\x83\xda\xb8\xcb\x2c\xf4\xd7\xf7\xd1\x2f\x9e\x2c\xce\ +\x2e\x14\xe6\xd2\x65\xf1\x48\xd2\x38\x0d\x39\x8b\x7a\x4e\xc9\xa1\ +\xd1\xc0\xb5\xfe\xac\xb7\xb5\x64\x55\xc2\xe5\x47\x54\x36\x98\xe7\ +\x60\xd3\x4a\xc6\x65\xbd\xad\x8c\x06\xae\xf5\xf7\xe4\x26\x6b\x7e\ +\x54\x04\x42\xa1\xee\xe1\x90\x31\x29\x1c\xde\xd2\xbd\xaf\x03\xe1\ +\xf0\x32\x1c\x32\x26\x03\xa1\x50\x77\x7e\xef\xd6\x69\xfa\x58\x7b\ +\xe7\x31\x5f\x78\x54\x23\xb3\x5c\x90\xac\xf8\xdb\x10\x0e\xf3\xab\ +\x35\x82\x13\xc8\xa5\xe9\x42\x76\x8b\x83\x85\xca\xcd\xd1\xdf\x47\ +\x06\x8b\x4f\xd3\x15\xf4\x1d\xb4\x1f\xde\xd2\xd4\x7a\x60\x93\x1a\ +\xaa\x26\x74\xfd\xce\x4a\xf7\x34\x72\x35\xeb\x99\xbf\x3c\x79\xe5\ +\xf6\xff\x83\x15\x7c\xf0\xb4\xd8\xbe\xb9\xa9\xe6\x8b\x1d\x0d\xd5\ +\xad\xae\xd8\x94\x22\x13\x21\x90\xb7\xcc\x29\xa1\x22\xec\x1e\x62\ +\xae\x3a\xa3\xff\xfa\xfc\xdf\xe7\xc6\x66\x5e\x3f\xf2\x0b\xfd\x52\ +\x16\x8e\x84\x02\x01\x21\x84\x0a\x54\x01\x95\x9a\x1d\xdf\x7b\x7b\ +\xac\x6f\xdd\x57\xb7\xb1\x6d\x83\xbb\xd2\x53\xe3\x10\x2e\x9f\xcd\ +\xb0\x00\xfc\x9b\x54\xf4\x99\x84\x8c\x2d\x86\xc3\xe1\x2b\x81\xd9\ +\xbf\x0e\xff\x9c\xfe\x28\x9e\x22\x08\x84\x80\xb0\x94\x32\x5c\xb2\ +\x03\x21\x84\x13\xf0\x00\xee\xdc\xd2\x5c\x56\x3c\x5b\xeb\x69\x79\ +\xb8\x51\x74\x18\x12\xe5\xc2\x75\x39\x3c\x74\x83\xc9\x78\x86\x10\ +\x10\x03\x96\x80\x48\x6e\x2d\x4a\xb9\x7a\x01\x28\x79\xab\xc8\x89\ +\x59\x00\x2b\x60\xcb\x2d\x0b\xa0\x02\x3a\x90\x02\xd2\x40\x12\xc8\ +\x48\x79\xab\x87\x26\xfe\x03\x26\x93\xd5\x41\x51\x76\x98\xdb\x00\ +\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ \x00\x00\x0b\xd7\ \x89\ \x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ @@ -1491,6 +1572,180 @@ qt_resource_data = "\ \xc3\x25\x0d\x25\x35\x01\xd7\x0f\x5b\xb5\x7e\x8e\x93\x83\xff\x0f\ \x92\x04\x28\x92\xfd\x58\xc9\xac\x00\x00\x00\x00\x49\x45\x4e\x44\ \xae\x42\x60\x82\ +\x00\x00\x05\x24\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x18\x00\x00\x00\x18\x08\x06\x00\x00\x00\xe0\x77\x3d\xf8\ +\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ +\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x06\xec\x00\x00\x06\xec\ +\x01\x1e\x75\x38\x35\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\ +\x74\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\ +\x70\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x00\x13\x74\x45\ +\x58\x74\x41\x75\x74\x68\x6f\x72\x00\x52\x6f\x64\x6e\x65\x79\x20\ +\x44\x61\x77\x65\x73\x0e\xd8\x7e\x1d\x00\x00\x04\x82\x49\x44\x41\ +\x54\x48\x89\x8d\x96\x6d\x88\x54\x55\x18\xc7\x7f\xe7\xbe\xcd\xdc\ +\xb9\xb3\xb3\x33\xfb\x1a\xeb\xbe\xe9\x6e\xad\x2f\x69\xea\x4a\x68\ +\x8a\x22\x24\x94\x58\x7e\xaa\x48\x23\x30\x41\xa1\xec\x5b\x51\x41\ +\xf8\x31\xa3\x3e\x44\x94\x44\x92\x09\xa5\x46\x54\x84\x21\x59\x44\ +\x18\x96\x19\xe8\xb2\x5a\x9b\xba\xea\xa4\xfb\x66\xee\xce\xce\xec\ +\xec\xcc\xdc\xbd\xf3\x76\xef\xe9\x43\xb3\x63\xe6\xbe\x3d\xf0\x7c\ +\xfb\x9f\xff\xef\x3c\xcf\x3d\xf7\x39\x47\x48\x29\x99\x2d\x36\xbf\ +\x27\x7c\x6a\x40\x7f\xc0\x6f\xaa\x8f\x02\x64\x1d\xf7\x84\x3b\x51\ +\xb8\xf0\xed\x8b\x32\x37\xdb\x5a\x31\x1d\x60\xf7\x01\xa1\x67\x2a\ +\x42\x6f\x37\xd7\x76\xac\xaf\x0a\xd6\xd5\x07\x2c\x7f\xb5\x69\x05\ +\x7c\x52\xba\x38\xb6\x93\xb3\xed\x6c\x62\xdc\x8e\xdd\xea\x8b\x5d\ +\x39\x15\x4c\xa7\x5e\xfe\x70\x97\x2c\xcc\x19\xb0\xf5\xa0\xb1\xa2\ +\xb9\x7e\xfe\xa1\x65\x1d\xab\x96\x7a\x5a\x56\xc9\xbb\x0e\x92\x3b\ +\x75\x02\x81\xa1\x9a\x50\x50\xbd\x9e\x2b\xe7\xff\xe8\x1f\xbe\xb1\ +\xe3\xd8\xce\x7c\xf7\xac\x80\x6d\x9f\x87\xf7\x2d\x69\x59\xf9\x5c\ +\x5d\x7d\x4d\x9d\xe3\xa6\x67\xeb\x00\x00\x01\x35\x44\x7c\x78\x74\ +\xe4\x42\x5f\xd7\xc7\x47\x9f\x4a\xbe\x36\x2d\xe0\x89\xc3\xe6\xb3\ +\x6b\x96\xae\xdf\x6f\x04\xf5\xa0\x27\xdd\x39\x99\x87\xb4\x7a\xaa\ +\xb4\x05\xa4\x8a\x43\x8c\x24\xaf\x65\xce\xf4\xfc\xfa\xc2\x17\xcf\ +\x38\x9f\xdc\x05\x78\xfc\x80\xa8\x59\xdc\xb6\xfc\xb7\xc6\xd6\x79\ +\x6d\x73\x33\x17\x6c\xa8\xdc\x43\x83\xb1\x0c\x9f\x08\x72\xc9\xf9\ +\x9e\x73\x99\x4f\x19\xb8\xd1\x17\xbd\x18\xed\x59\xfd\xcd\x2e\x39\ +\x0a\xa0\x4c\xca\xab\xab\xea\x8e\x34\x35\x37\xcc\xd1\x1c\x56\x5a\ +\x4f\xd2\xea\x5b\x8d\x5f\x54\x10\x2b\x46\x39\x97\x39\x8c\x2b\x5d\ +\x5a\x9a\x5b\xdb\xaa\xab\x6a\x8f\x4c\xea\x14\xf8\xf7\xa3\xde\xd7\ +\xd2\xb1\xd6\x15\x73\x33\xaf\xd1\xdb\xb9\xd7\xdc\x88\x82\x4a\xda\ +\x1d\xe1\x97\xd4\x07\xb8\xb2\x08\x40\x51\xb8\xb4\x37\xcf\x5f\xbb\ +\xf5\xa0\xb1\xa2\x0c\xf0\xfb\xd4\x2d\x56\xd0\xb4\x40\x20\xc4\x54\ +\xa9\x94\x53\x53\xfc\xac\x0b\xed\x22\xa0\x86\xc9\x4b\x87\x4b\xce\ +\x09\xd2\xee\x2d\x14\xa1\xa2\x08\x0d\x21\x54\x02\x96\xdf\xf2\xfb\ +\xd4\x2d\x00\x1a\x80\x65\x55\x76\xaa\x86\x86\xbc\xdd\xb1\x52\x97\ +\xc5\x5d\xbb\x5f\x57\xb1\x9b\x88\xd6\x02\xc0\xad\x42\x0f\x97\xb3\ +\x3f\xa2\x08\xed\x0e\x8d\x66\xe8\x58\x66\xa8\xb3\x0c\x08\x9b\x91\ +\x26\x4f\x4a\x54\x45\x9d\xd1\x7c\x81\xff\x21\x1a\x7d\xcb\xf1\x3c\ +\x8f\x91\x42\x2f\xc7\xe3\x7b\x31\xd5\x10\x86\x12\xb8\x43\x57\x14\ +\x0a\xe1\x40\xa8\xa9\x0c\x30\x0d\xb3\x5a\x11\x3a\x0a\x0a\x4c\x61\ +\x0c\x10\x50\x23\x2c\x31\x1f\x43\xb8\x3a\x49\xf7\x26\xc7\xe3\x7b\ +\x89\xe5\xa3\x00\x58\x6a\x15\x11\xa3\x11\x53\x0d\x23\x10\xe4\xbd\ +\x1c\xa6\xe1\xab\x2e\x03\x9c\xfc\x44\x5c\x57\x8c\x96\xff\x9a\x77\ +\x98\x9b\x08\x28\x61\xba\xed\x2f\xf1\x64\x91\x4e\x73\x1b\x16\xb5\ +\x14\x65\x8e\x0b\xf6\x57\x0c\xe5\xcf\x97\xb5\xb6\x9b\xc0\x76\x12\ +\xf8\x14\x8b\xb0\xde\x48\xde\x4b\xe1\xe4\xb3\xf1\x32\x20\x69\x8f\ +\x0d\x08\xe9\xad\x14\xc2\x00\x60\x51\xe0\x11\x96\x04\x36\x23\x50\ +\x18\x2b\x0c\x52\x70\x73\xdc\xa3\x2d\x06\xa0\x3f\x77\x96\x53\xa9\ +\xfd\x53\x56\x99\xf3\x6c\x86\x73\xbd\x54\xa9\xb5\x24\x27\xc6\x07\ +\xca\x00\xdb\x49\x75\x25\x9d\xa1\xad\xe8\x3a\xa6\x5a\x89\x81\x85\ +\xf0\x34\x3c\xcf\xa3\xcd\xd8\x80\x2e\x4c\x14\xa1\x13\x2b\x5c\xe5\ +\x58\xe2\x55\x60\xe6\x09\xac\xba\x3a\xb6\x93\xe9\x2a\x03\xb2\x39\ +\xf7\x78\x2e\x53\x7c\x65\x3c\xd0\x67\x49\x3c\xbc\xa2\xa4\xb5\x76\ +\x0d\x9a\xf0\x53\x55\x3a\x31\xb6\x1b\xe7\x64\xea\x1d\x26\xbc\xc4\ +\x8c\xe6\x02\x85\x82\x2d\xed\x6c\xce\x3d\x0e\xa5\xff\xe0\xd8\xce\ +\x7c\x77\x74\xa0\xff\x74\x8d\xd2\x00\xc0\x60\xbe\x9b\x44\xb1\xbf\ +\xbc\xc8\x93\x2e\x97\x9c\xef\xb8\xea\xfc\x34\xa3\x39\x40\x8d\x52\ +\x47\x74\x70\xf0\xf4\xe4\x64\x2d\x1f\xfc\x78\x22\xb6\x3d\x31\xe4\ +\x44\x4d\xc5\xc2\xc3\xe5\x77\xfb\x6b\x3c\x59\xa4\x20\x1d\x06\xf2\ +\xe7\xf8\x21\xf9\xe6\xac\xe6\xa6\x12\x20\x31\xe4\x44\xe3\x89\xf8\ +\xf6\x72\x45\xff\x9f\xa6\x9d\x0b\xef\x7f\x3f\xe9\xff\xbb\xc2\x95\ +\x1e\x0b\xcd\x87\xc9\xca\x34\xd7\xb3\x67\x98\xad\xef\x9a\xd0\x08\ +\x3b\xb5\xe9\xae\xde\xde\x3d\x53\x4e\xd3\xc9\xd8\xf6\x59\x78\x5f\ +\xc7\xbc\x96\x1d\x6a\x4d\xa1\x3e\xe9\xc6\x67\xdd\x35\x40\x58\x8d\ +\xe0\x8e\x6a\xc3\xbd\x43\xfd\x87\x8e\x3e\x9d\x9a\xfe\x3e\x98\x8c\ +\x4d\x6f\x19\xab\xdb\xda\xeb\x3f\x5a\xd0\x5e\xb7\x28\xad\x24\x94\ +\xac\x9c\xfa\x46\xf3\x0b\x3f\x15\x6e\xd8\xfb\xeb\xda\xc8\xe5\x8b\ +\x5d\xb1\xe7\x7f\xde\x57\x3c\x2d\x65\x69\xea\x4d\x05\x10\x42\xa8\ +\x40\x35\x10\x36\x2b\xa8\xdd\xf8\x7a\xf0\xa5\x96\xe6\xea\x65\xa1\ +\x50\x20\x12\x08\x2b\x41\xa3\x42\xd1\x01\x72\x29\xaf\x38\x91\x2c\ +\x66\xd2\xa9\x6c\xf2\xfa\xf5\xd8\x9f\x27\xdf\x98\x78\x37\x67\x33\ +\x0a\x8c\x01\x49\x29\x65\x72\xda\x0a\x84\x10\x16\x10\x01\x2a\x4b\ +\x59\x61\x04\x89\x34\x3c\xa8\x2c\x6c\x5a\xa5\xad\x90\x12\x65\xe0\ +\x6c\xf1\xfc\xcd\xb3\x5e\xb4\x60\x33\x06\x64\x80\x14\x30\x5e\xca\ +\x84\x94\xb7\x1f\x00\xd3\xbe\x2a\x4a\x30\x1d\x30\x00\x5f\x29\x75\ +\x40\x05\x8a\x40\x0e\xc8\x03\x59\xa0\x20\xe5\xd4\x37\xd5\x3f\x13\ +\x05\x02\x8c\xec\xcf\x7e\xae\x00\x00\x00\x00\x49\x45\x4e\x44\xae\ +\x42\x60\x82\ +\x00\x00\x05\x64\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x18\x00\x00\x00\x18\x08\x06\x00\x00\x00\xe0\x77\x3d\xf8\ +\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ +\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0d\xd7\x00\x00\x0d\xd7\ +\x01\x42\x28\x9b\x78\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\ +\x74\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\ +\x70\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x04\xe1\x49\x44\ +\x41\x54\x48\x89\xb5\x95\x4b\x6c\x54\x55\x18\xc7\x7f\xe7\x9c\x3b\ +\xd3\x99\x32\xb5\xf4\x41\x1f\x38\x02\xa5\x8d\x4c\x54\xb0\x25\x48\ +\xa2\x12\x62\x14\x17\x46\x12\x64\x81\x26\xb0\x31\x18\x12\x36\x04\ +\xd2\x45\xe9\xca\x84\x5d\x29\x1a\x43\x80\x6e\x0c\x2b\xdc\x88\x09\ +\xa0\x21\x04\x35\x18\xa2\x26\x3e\xa3\x62\xa8\x4f\x7c\x61\x95\x81\ +\xb6\x4c\xcb\xbc\x67\xee\x39\x9f\x8b\xdb\xde\x4e\x2b\x10\x37\xde\ +\xe4\x4b\xee\xf3\xf7\xff\xfe\xdf\xf9\xee\x77\x94\x88\xf0\x7f\x1e\ +\xde\xdd\x1e\x9e\x53\xaa\x23\x6a\xcc\x33\xf1\x58\xec\xc9\xc6\xae\ +\x15\x2b\xeb\x97\x76\x2e\xb5\xc5\x62\x29\xfb\xd7\x5f\x63\xb9\x6b\ +\xd7\x7f\x2c\x55\x2a\xe7\x2b\xd6\x7e\xb0\x59\xa4\x70\x27\x86\xba\ +\xad\x03\xa5\xd4\x87\xb1\xd8\xcb\xc9\xc7\x1e\xdd\xdd\xd9\xde\xd6\ +\x11\xab\x8b\xa3\x4a\x45\xc8\xe5\xc1\x18\x48\x2c\xc2\x79\x86\x5c\ +\x21\xef\x7e\x1f\xbd\x7c\x65\xe2\xa7\x5f\xf7\x3c\x59\xad\xbe\xf7\ +\x9f\x04\xce\x2b\xb5\xa2\x75\x69\xe7\x1b\xa9\x0d\x8f\xaf\x4f\x54\ +\x6d\x84\x52\x29\x7c\x26\xc0\xec\xfb\x32\x13\x34\x24\xb8\x91\x9f\ +\x9e\xfa\xe5\xe3\x4f\xdf\xb6\xb7\xb2\xbb\x9f\x10\x29\xd5\xf2\xe6\ +\x09\xbc\xa7\x54\x6a\xc5\xda\xde\xf7\x7b\xba\xbb\x93\x3a\x57\x98\ +\x83\x2c\x80\x2e\x14\x11\xcf\xc3\xc6\x23\xf2\xdd\xa7\x9f\x7d\x91\ +\x1f\xbb\xf6\xf8\x13\x22\xfe\x2c\x53\xcf\x9e\x1c\x50\x4a\xb7\x2e\ +\x5f\x76\xa2\x67\xf9\xb2\x24\xd9\x3c\x4e\x24\x0c\x0b\xd8\x9a\x6b\ +\x07\xc1\x3d\xc0\x01\xce\xf7\x21\x5b\x54\x3d\x0f\xaf\x5e\xa7\xeb\ +\xeb\x5f\xab\x75\x10\x0a\x6c\x8a\xc7\x87\xee\x7f\xe8\x81\x3e\x29\ +\x56\x02\x80\x08\x95\xed\xdb\xa9\x6e\xd9\x32\x27\x52\x03\xb5\x9e\ +\x87\x1d\x18\xc0\xef\xed\x0d\xc5\x8d\x55\x3a\xd9\xf7\xe0\x0b\xef\ +\x46\xa3\xeb\x67\xb9\x1e\xc0\x05\xa5\xee\x5f\xf9\xd8\xfa\x17\xeb\ +\x7c\x67\x1c\x0a\x01\xec\x8e\x1d\xf8\xcf\x3d\x17\x64\xa1\x14\xfa\ +\xf4\xe9\xb0\x3c\x2e\x12\x81\xfd\xfb\xa1\xb7\x17\xfa\xfa\x70\x43\ +\x43\xf0\xf5\xd7\x88\x15\x9a\x1b\x9b\x96\x34\x25\xdb\x5f\x47\xa9\ +\x5e\x44\x44\x03\x78\x9e\xf7\x52\xc7\xe2\xa6\x25\x0e\x15\x64\xe9\ +\x79\xd8\x9e\x9e\xd0\xa6\xdb\xbe\x1d\x7f\xeb\xd6\x20\xd3\x68\x14\ +\x06\x07\x51\x7d\x7d\x28\xa5\x50\x91\x08\x74\x77\x63\x95\xc2\x29\ +\x85\xad\x38\x5a\xdb\x5a\x97\x9d\x85\x9e\xd0\xc1\xa2\xd6\x96\x07\ +\x11\x70\x5a\x07\x59\x3a\x07\x43\x43\xa8\xc1\x41\x58\xbd\x3a\x50\ +\xd9\xb1\x03\x17\x89\xa0\x52\x29\xd4\x9a\x35\xa1\xb8\x3d\x79\x12\ +\xff\xcc\x19\xc4\x18\x10\x41\xb4\xa6\x2e\x16\x5b\x1c\xd1\xfa\x29\ +\xe0\x67\x0d\x10\x6b\x6d\xba\xcf\x39\xc1\x69\x3d\x17\xd6\xe2\x86\ +\x87\x91\xcb\x97\xe7\x16\xec\xf9\xe7\x43\xb8\x88\xe0\xbf\xf9\x26\ +\xd5\x53\xa7\x82\xc4\xb4\xc6\x79\x1e\x62\x0c\x26\x12\xa5\x2e\x11\ +\xdf\x00\xa0\xdf\x52\x2a\x51\xb7\x28\xde\xee\xbc\xc8\x7c\x01\xad\ +\x71\xce\xe1\xbf\xf2\x0a\x32\x3a\x3a\x57\x2e\xe7\xf0\x7d\x9f\xe2\ +\xa1\x43\x64\x07\x06\x28\x5f\xb9\x82\x2d\x95\x70\xc6\x04\xdf\x18\ +\x83\xad\x5a\xe2\x4b\x9a\x97\x87\x25\xc2\x98\x60\xe1\x6a\x7b\x7b\ +\x06\x28\xc6\x20\x4a\x81\x13\x9c\xb8\xf0\x1f\x70\x80\xb5\x16\x3f\ +\x9d\x46\xd2\x69\x74\x4b\x0b\x5e\x32\x89\x6e\x6e\xc6\x96\xcb\x88\ +\xb8\xa0\x8b\xb6\x89\xe4\xbe\x79\x68\xd5\x75\xb9\xd7\xb4\x8b\x95\ +\x00\xae\x54\x00\x8f\x44\x88\xec\xdb\x07\xa9\x14\xd6\xd9\xda\xf6\ +\x26\xd6\xdf\x8f\x03\x0a\x87\x0f\x07\x5d\x37\x31\x41\x65\x7c\x1c\ +\xe2\x71\xbc\xce\x26\xf2\x13\x53\x7f\x84\xff\x41\x71\x32\xf3\xa7\ +\x3f\x39\x81\xd3\x26\xb4\xea\x97\xca\xe8\x5d\xbb\x90\x55\xab\x70\ +\x2e\xc8\xa6\xf0\xea\xab\x54\x3f\xfa\x28\x14\xa9\xef\xef\x27\xb6\ +\x77\x2f\x56\x24\x08\xc0\x2f\x14\xa8\x7a\x9a\x72\xae\xf8\x71\x58\ +\xa2\xc2\x44\x66\xb4\x9c\xcd\x3c\xeb\x46\x7f\x40\x25\x12\x38\xdf\ +\x27\x3e\x32\x82\x5e\xbb\x36\x84\xe5\x87\x87\x29\x1c\x3b\x86\x44\ +\xa3\x34\x1e\x3f\x4e\xdd\xc6\x8d\x00\x24\xfa\xfb\x71\xbe\x4f\xf6\ +\xc8\x91\xb0\xb4\x3e\x4c\x39\xe7\x2e\x84\x0e\xf0\xfd\xe3\x19\xe7\ +\xc6\x2d\x8e\xca\xf8\x38\xd5\x4c\x06\xff\xd2\xa5\x10\x9e\x3b\x78\ +\x90\xdc\xd1\xa3\x41\x96\xe5\x32\x93\x3b\x77\x52\xba\x78\x31\x28\ +\x63\xb9\x4c\xe9\xdb\x6f\xf1\x67\x5c\xe8\xf6\x26\xa6\x27\x32\x57\ +\x37\xc3\x15\xa8\x19\x76\x17\xea\xeb\x87\xbb\xfa\x52\xfd\xf6\xf2\ +\x2f\x46\x24\x58\x8b\xc4\xe0\x20\xf6\xd6\x2d\x72\x23\x23\x41\x8f\ +\xd7\x36\x41\x34\x4a\xeb\xc8\x08\xd9\x13\x27\x28\x5c\xbc\x18\x0c\ +\xc4\xa8\x87\xac\xec\x18\xff\xfb\xab\x9f\x36\x6f\x16\xf9\x7c\x9e\ +\xc0\x01\xa5\xf4\xa6\x64\xfb\x67\x1d\x0d\x0d\xeb\x2a\x63\xd7\x91\ +\x05\xc0\xd9\xde\x9f\x27\x52\x73\x0d\x50\x97\xba\xcf\xa5\xbf\xff\ +\x7d\xe4\xe9\x42\x69\xcf\xac\xfb\x79\xe3\xfa\xac\x52\xa9\xb6\x54\ +\xd7\xfb\x8d\xc6\x24\x2b\x7f\xa4\xff\x05\x58\x08\x0d\x47\xb7\x67\ +\x88\x76\x77\xca\x74\xfa\xe6\x17\x92\x9e\x9c\x37\xae\x6f\xbb\xe1\ +\xc4\xda\x9a\xdf\x58\xb2\x62\xe9\x23\xfe\x6f\xd7\xa2\x36\x5f\x9c\ +\x2f\xb0\x40\xc4\x6b\x6b\x44\x9a\x13\x99\xc9\x1f\xc7\xde\x89\x15\ +\x4a\x77\xdf\x70\x00\x94\x52\x0d\x31\x68\x39\x12\x31\x03\xa9\x55\ +\xcb\xb7\x35\x18\xd3\xaa\x7c\x8b\x9d\xce\x53\x9d\xce\x83\xd1\x98\ +\xc5\x09\x74\x43\x0c\xab\x95\x4c\x66\x72\x63\x17\xae\x5e\x3f\x78\ +\x08\xde\x05\xa6\x80\x29\xb9\x93\x03\xa5\x54\x04\x68\x01\x16\x03\ +\x4d\xbd\xd0\xb5\x49\xeb\x8d\x5d\x9e\xe9\x6b\x69\x69\x68\x6b\x6c\ +\x5c\xd4\x58\xf5\xad\xbd\x99\xc9\x4d\xdf\x98\xce\x5f\x1b\xf5\xed\ +\x97\xe7\xe0\x93\x71\x48\xcf\xc2\x81\x8c\x88\x64\xef\xe8\x60\x46\ +\xa8\x1e\xb8\x67\x26\xea\x81\x38\x50\x07\x18\x40\x11\xec\x3b\x15\ +\xa0\x0c\xe4\x81\x1c\x70\x0b\xc8\xca\xec\x8c\x98\x39\xfe\x01\x76\ +\x95\xba\xf1\x06\x3a\xff\x81\x00\x00\x00\x00\x49\x45\x4e\x44\xae\ +\x42\x60\x82\ " qt_resource_name = "\ @@ -1502,6 +1757,11 @@ qt_resource_name = "\ \x05\xcd\xf4\xe7\ \x00\x63\ \x00\x6f\x00\x6e\x00\x6e\x00\x5f\x00\x65\x00\x72\x00\x72\x00\x6f\x00\x72\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x13\ +\x09\xd2\x6c\x67\ +\x00\x45\ +\x00\x6d\x00\x62\x00\x6c\x00\x65\x00\x6d\x00\x2d\x00\x71\x00\x75\x00\x65\x00\x73\x00\x74\x00\x69\x00\x6f\x00\x6e\x00\x2e\x00\x70\ +\x00\x6e\x00\x67\ \x00\x12\ \x04\xe4\x91\x47\ \x00\x63\ @@ -1521,16 +1781,28 @@ qt_resource_name = "\ \x00\x6c\ \x00\x65\x00\x61\x00\x70\x00\x2d\x00\x63\x00\x6f\x00\x6c\x00\x6f\x00\x72\x00\x2d\x00\x73\x00\x6d\x00\x61\x00\x6c\x00\x6c\x00\x2e\ \x00\x70\x00\x6e\x00\x67\ +\x00\x11\ +\x06\x1a\x44\xa7\ +\x00\x44\ +\x00\x69\x00\x61\x00\x6c\x00\x6f\x00\x67\x00\x2d\x00\x61\x00\x63\x00\x63\x00\x65\x00\x70\x00\x74\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\ +\x00\x10\ +\x0f\xc3\x90\x67\ +\x00\x44\ +\x00\x69\x00\x61\x00\x6c\x00\x6f\x00\x67\x00\x2d\x00\x65\x00\x72\x00\x72\x00\x6f\x00\x72\x00\x2e\x00\x70\x00\x6e\x00\x67\ " qt_resource_struct = "\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x02\ -\x00\x00\x00\xa8\x00\x00\x00\x00\x00\x01\x00\x00\x2d\x4e\ -\x00\x00\x00\x34\x00\x00\x00\x00\x00\x01\x00\x00\x0d\xf7\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x08\x00\x00\x00\x02\ +\x00\x00\x00\xd4\x00\x00\x00\x00\x00\x01\x00\x00\x32\x3e\ +\x00\x00\x00\x60\x00\x00\x00\x00\x00\x01\x00\x00\x12\xe7\ \x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x00\x5e\x00\x00\x00\x00\x00\x01\x00\x00\x19\xd2\ -\x00\x00\x00\x7c\x00\x00\x00\x00\x00\x01\x00\x00\x20\xbd\ +\x00\x00\x01\x02\x00\x00\x00\x00\x00\x01\x00\x00\x60\xc7\ +\x00\x00\x00\x8a\x00\x00\x00\x00\x00\x01\x00\x00\x1e\xc2\ +\x00\x00\x00\x34\x00\x00\x00\x00\x00\x01\x00\x00\x0d\xf7\ +\x00\x00\x00\xa8\x00\x00\x00\x00\x00\x01\x00\x00\x25\xad\ +\x00\x00\x01\x2a\x00\x00\x00\x00\x00\x01\x00\x00\x65\xef\ " def qInitResources(): diff --git a/src/leap/gui/progress.py b/src/leap/gui/progress.py new file mode 100644 index 00000000..ca4f6cc3 --- /dev/null +++ b/src/leap/gui/progress.py @@ -0,0 +1,488 @@ +""" +classes used in progress pages +from first run wizard +""" +try: + from collections import OrderedDict +except ImportError: # pragma: no cover + # We must be in 2.6 + from leap.util.dicts import OrderedDict + +import logging + +from PyQt4 import QtCore +from PyQt4 import QtGui + +from leap.gui.threads import FunThread + +from leap.gui import mainwindow_rc + +ICON_CHECKMARK = ":/images/Dialog-accept.png" +ICON_FAILED = ":/images/Dialog-error.png" +ICON_WAITING = ":/images/Emblem-question.png" + +logger = logging.getLogger(__name__) + + +class ImgWidget(QtGui.QWidget): + + # XXX move to widgets + + def __init__(self, parent=None, img=None): + super(ImgWidget, self).__init__(parent) + self.pic = QtGui.QPixmap(img) + + def paintEvent(self, event): + painter = QtGui.QPainter(self) + painter.drawPixmap(0, 0, self.pic) + + +class ProgressStep(object): + """ + Data model for sequential steps + to be used in a progress page in + connection wizard + """ + NAME = 0 + DONE = 1 + + def __init__(self, stepname, done, index=None): + """ + @param step: the name of the step + @type step: str + @param done: whether is completed or not + @type done: bool + """ + self.index = int(index) if index else 0 + self.name = unicode(stepname) + self.done = bool(done) + + @classmethod + def columns(self): + return ('name', 'done') + + +class ProgressStepContainer(object): + """ + a container for ProgressSteps objects + access data in the internal dict + """ + + def __init__(self): + self.dirty = False + self.steps = {} + + def step(self, identity): + return self.steps.get(identity, None) + + def addStep(self, step): + self.steps[step.index] = step + + def removeStep(self, step): + if step and self.steps.get(step.index, None): + del self.steps[step.index] + del step + self.dirty = True + + def removeAllSteps(self): + for item in iter(self): + self.removeStep(item) + + @property + def columns(self): + return ProgressStep.columns() + + def __len__(self): + return len(self.steps) + + def __iter__(self): + for step in self.steps.values(): + yield step + + +class StepsTableWidget(QtGui.QTableWidget): + """ + initializes a TableWidget + suitable for our display purposes, like removing + header info and grid display + """ + + def __init__(self, parent=None): + super(StepsTableWidget, self).__init__(parent=parent) + + # remove headers and all edit/select behavior + self.horizontalHeader().hide() + self.verticalHeader().hide() + self.setEditTriggers( + QtGui.QAbstractItemView.NoEditTriggers) + self.setSelectionMode( + QtGui.QAbstractItemView.NoSelection) + width = self.width() + + # WTF? Here init width is 100... + # but on populating is 456... :( + #logger.debug('init table. width=%s' % width) + + # XXX do we need this initial? + self.horizontalHeader().resizeSection(0, width * 0.7) + + # this disables the table grid. + # we should add alignment to the ImgWidget (it's top-left now) + self.setShowGrid(False) + self.setFocusPolicy(QtCore.Qt.NoFocus) + #self.setStyleSheet("QTableView{outline: 0;}") + + # XXX change image for done to rc + + # Note about the "done" status painting: + # + # XXX currently we are setting the CellWidget + # for the whole table on a per-row basis + # (on add_status_line method on ValidationPage). + # However, a more generic solution might be + # to implement a custom Delegate that overwrites + # the paint method (so it paints a checked tickmark if + # done is True and some other thing if checking or false). + # What we have now is quick and works because + # I'm supposing that on first fail we will + # go back to previous wizard page to signal the failure. + # A more generic solution could be used for + # some failing tests if they are not critical. + + +class WithStepsMixIn(object): + """ + This Class is a mixin that can be inherited + by InlineValidation pages (which will display + a progress steps widget in the same page as the form) + or by Validation Pages (which will only display + the progress steps in the page, below a progress bar widget) + """ + STEPS_TIMER_MS = 100 + + # + # methods related to worker threads + # launched for individual checks + # + + def setupStepsProcessingQueue(self): + """ + should be called from the init method + of the derived classes + """ + self.steps_queue = Queue.Queue() + self.stepscheck_timer = QtCore.QTimer() + self.stepscheck_timer.timeout.connect(self.processStepsQueue) + self.stepscheck_timer.start(self.STEPS_TIMER_MS) + # we need to keep a reference to child threads + self.threads = [] + + def do_checks(self): + """ + main entry point for checks. + it calls _do_checks in derived classes, + and it expects it to be a generator + yielding a tuple in the form (("message", progress_int), checkfunction) + """ + + # yo dawg, I heard you like checks + # so I put a __do_checks in your do_checks + # for calling others' _do_checks + + def __do_checks(fun=None, queue=None): + + for checkcase in fun(): # pragma: no cover + checkmsg, checkfun = checkcase + + queue.put(checkmsg) + if checkfun() is False: + queue.put("failed") + break + + t = FunThread(fun=partial( + __do_checks, + fun=self._do_checks, + queue=self.steps_queue)) + if hasattr(self, 'on_checks_validation_ready'): + t.finished.connect(self.on_checks_validation_ready) + t.begin() + self.threads.append(t) + + def processStepsQueue(self): + """ + consume steps queue + and pass messages + to the ui updater functions + """ + while self.steps_queue.qsize(): + try: + status = self.steps_queue.get(0) + if status == "failed": + self.set_failed_icon() + else: + self.onStepStatusChanged(*status) + except Queue.Empty: # pragma: no cover + pass + + def fail(self, err=None): + """ + return failed state + and send error notification as + a nice side effect. this function is called from + the _do_checks check functions returned in the + generator. + """ + wizard = self.wizard() + senderr = lambda err: wizard.set_validation_error( + self.current_page, err) + self.set_undone() + if err: + senderr(err) + return False + + @QtCore.pyqtSlot() + def launch_checks(self): + self.do_checks() + + # (gui) presentation stuff begins ##################### + + # slot + #@QtCore.pyqtSlot(str, int) + def onStepStatusChanged(self, status, progress=None): + status = unicode(status) + if status not in ("head_sentinel", "end_sentinel"): + self.add_status_line(status) + if status in ("end_sentinel"): + #self.checks_finished = True + self.set_checked_icon() + if progress and hasattr(self, 'progress'): + self.progress.setValue(progress) + self.progress.update() + + def setupSteps(self): + self.steps = ProgressStepContainer() + # steps table widget + if isinstance(self, QtCore.QObject): + parent = self + else: + parent = None + self.stepsTableWidget = StepsTableWidget(parent=parent) + zeros = (0, 0, 0, 0) + self.stepsTableWidget.setContentsMargins(*zeros) + self.errors = OrderedDict() + + def set_error(self, name, error): + self.errors[name] = error + + def pop_first_error(self): + errkey, errval = list(reversed(self.errors.items())).pop() + del self.errors[errkey] + return errkey, errval + + def clean_errors(self): + self.errors = OrderedDict() + + def clean_wizard_errors(self, pagename=None): + if pagename is None: # pragma: no cover + pagename = getattr(self, 'prev_page', None) + if pagename is None: # pragma: no cover + return + #logger.debug('cleaning wizard errors for %s' % pagename) + self.wizard().set_validation_error(pagename, None) + + def populateStepsTable(self): + # from examples, + # but I guess it's not needed to re-populate + # the whole table. + table = self.stepsTableWidget + table.setRowCount(len(self.steps)) + columns = self.steps.columns + table.setColumnCount(len(columns)) + + for row, step in enumerate(self.steps): + item = QtGui.QTableWidgetItem(step.name) + item.setData(QtCore.Qt.UserRole, + long(id(step))) + table.setItem(row, columns.index('name'), item) + table.setItem(row, columns.index('done'), + QtGui.QTableWidgetItem(step.done)) + self.resizeTable() + self.update() + + def clearTable(self): + # ??? -- not sure what's the difference + #self.stepsTableWidget.clear() + self.stepsTableWidget.clearContents() + + def resizeTable(self): + # resize first column to ~80% + table = self.stepsTableWidget + FIRST_COLUMN_PERCENT = 0.70 + width = table.width() + #logger.debug('populate table. width=%s' % width) + table.horizontalHeader().resizeSection(0, width * FIRST_COLUMN_PERCENT) + + def set_item_icon(self, img=ICON_CHECKMARK, current=True): + """ + mark the last item + as done + """ + # setting cell widget. + # see note on StepsTableWidget about plans to + # change this for a better solution. + if not hasattr(self, 'steps'): + return + index = len(self.steps) + table = self.stepsTableWidget + _index = index - 1 if current else index - 2 + table.setCellWidget( + _index, + ProgressStep.DONE, + ImgWidget(img=img)) + table.update() + + def set_failed_icon(self): + self.set_item_icon(img=ICON_FAILED, current=True) + + def set_checking_icon(self): + self.set_item_icon(img=ICON_WAITING, current=True) + + def set_checked_icon(self, current=True): + self.set_item_icon(current=current) + + def add_status_line(self, message): + """ + adds a new status line + and mark the next-to-last item + as done + """ + index = len(self.steps) + step = ProgressStep(message, False, index=index) + self.steps.addStep(step) + self.populateStepsTable() + self.set_checking_icon() + self.set_checked_icon(current=False) + + # Sets/unsets done flag + # for isComplete checks + + def set_done(self): + self.done = True + self.completeChanged.emit() + + def set_undone(self): + self.done = False + self.completeChanged.emit() + + def is_done(self): + return self.done + + # convenience for going back and forth + # in the wizard pages. + + def go_back(self): + self.wizard().back() + + def go_next(self): + self.wizard().next() + + +""" +We will use one base class for the intermediate pages +and another one for the in-page validations, both sharing the creation +of the tablewidgets. +The logic of this split comes from where I was trying to solve +the ui update using signals, but now that it's working well with +queues I could join them again. +""" + +import Queue +from functools import partial + + +class InlineValidationPage(QtGui.QWizardPage, WithStepsMixIn): + + def __init__(self, parent=None): + super(InlineValidationPage, self).__init__(parent) + self.setupStepsProcessingQueue() + self.done = False + + # slot + + @QtCore.pyqtSlot() + def showStepsFrame(self): + self.valFrame.show() + self.update() + + # progress frame + + def setupValidationFrame(self): + qframe = QtGui.QFrame + valFrame = qframe() + valFrame.setFrameStyle(qframe.NoFrame) + valframeLayout = QtGui.QVBoxLayout() + zeros = (0, 0, 0, 0) + valframeLayout.setContentsMargins(*zeros) + + valframeLayout.addWidget(self.stepsTableWidget) + valFrame.setLayout(valframeLayout) + self.valFrame = valFrame + + +class ValidationPage(QtGui.QWizardPage, WithStepsMixIn): + """ + class to be used as an intermediate + between two pages in a wizard. + shows feedback to the user and goes back if errors, + goes forward if ok. + initializePage triggers a one shot timer + that calls do_checks. + Derived classes should implement + _do_checks and + _do_validation + """ + + # signals + stepChanged = QtCore.pyqtSignal([str, int]) + + def __init__(self, parent=None): + super(ValidationPage, self).__init__(parent) + self.setupSteps() + #self.connect_step_status() + + layout = QtGui.QVBoxLayout() + self.progress = QtGui.QProgressBar(self) + layout.addWidget(self.progress) + layout.addWidget(self.stepsTableWidget) + + self.setLayout(layout) + self.layout = layout + + self.timer = QtCore.QTimer() + self.done = False + + self.setupStepsProcessingQueue() + + def isComplete(self): + return self.is_done() + + ######################## + + def show_progress(self): + self.progress.show() + self.stepsTableWidget.show() + + def hide_progress(self): + self.progress.hide() + self.stepsTableWidget.hide() + + # pagewizard methods. + # if overriden, child classes should call super. + + def initializePage(self): + self.clean_errors() + self.clean_wizard_errors() + self.steps.removeAllSteps() + self.clearTable() + self.resizeTable() + self.timer.singleShot(0, self.do_checks) diff --git a/src/leap/gui/styles.py b/src/leap/gui/styles.py new file mode 100644 index 00000000..b482922e --- /dev/null +++ b/src/leap/gui/styles.py @@ -0,0 +1,16 @@ +GreenLineEdit = "QLabel {color: green; font-weight: bold}" +ErrorLabelStyleSheet = """QLabel { color: red; font-weight: bold }""" +ErrorLineEdit = """QLineEdit { border: 1px solid red; }""" + + +# XXX this is bad. +# and you should feel bad for it. +# The original style has a sort of box color +# white/beige left-top/right-bottom or something like +# that. + +RegularLineEdit = """ +QLineEdit { + border: 1px solid black; +} +""" diff --git a/src/leap/gui/tests/__init__.py b/src/leap/gui/tests/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/leap/gui/tests/__init__.py diff --git a/src/leap/gui/tests/integration/fake_user_signup.py b/src/leap/gui/tests/integration/fake_user_signup.py index 12f18966..78873749 100644 --- a/src/leap/gui/tests/integration/fake_user_signup.py +++ b/src/leap/gui/tests/integration/fake_user_signup.py @@ -12,6 +12,7 @@ curl -d login=python_test_user -d password_salt=54321\ from BaseHTTPServer import HTTPServer from BaseHTTPServer import BaseHTTPRequestHandler import cgi +import json import urlparse HOST = "localhost" @@ -19,12 +20,15 @@ PORT = 8000 LOGIN_ERROR = """{"errors":{"login":["has already been taken"]}}""" +from leap.base.tests.test_providers import EXPECTED_DEFAULT_CONFIG + class request_handler(BaseHTTPRequestHandler): responses = { '/': ['ok\n'], '/users.json': ['ok\n'], - '/timeout': ['ok\n'] + '/timeout': ['ok\n'], + '/provider.json': ['%s\n' % json.dumps(EXPECTED_DEFAULT_CONFIG)] } def do_GET(self): diff --git a/src/leap/gui/tests/test_firstrun_login.py b/src/leap/gui/tests/test_firstrun_login.py new file mode 100644 index 00000000..6c45b8ef --- /dev/null +++ b/src/leap/gui/tests/test_firstrun_login.py @@ -0,0 +1,212 @@ +import sys +import unittest + +import mock + +from leap.testing import qunittest +#from leap.testing import pyqt + +from PyQt4 import QtGui +#from PyQt4 import QtCore +#import PyQt4.QtCore # some weirdness with mock module + +from PyQt4.QtTest import QTest +from PyQt4.QtCore import Qt + +from leap.gui import firstrun + +try: + from collections import OrderedDict +except ImportError: + # We must be in 2.6 + from leap.util.dicts import OrderedDict + + +class TestPage(firstrun.login.LogInPage): + pass + + +class LogInPageLogicTestCase(qunittest.TestCase): + + # XXX can spy on signal connections + __name__ = "register user page logic tests" + + def setUp(self): + self.app = QtGui.QApplication(sys.argv) + QtGui.qApp = self.app + self.page = TestPage(None) + self.page.wizard = mock.MagicMock() + + def tearDown(self): + QtGui.qApp = None + self.app = None + self.page = None + + def test__do_checks(self): + eq = self.assertEqual + + self.page.userNameLineEdit.setText('testuser@domain') + self.page.userPasswordLineEdit.setText('testpassword') + + # fake register process + with mock.patch('leap.base.auth.LeapSRPRegister') as mockAuth: + mockSignup = mock.MagicMock() + + reqMockup = mock.Mock() + # XXX should inject bad json to get error + reqMockup.content = '{"errors": null}' + mockSignup.register_user.return_value = (True, reqMockup) + mockAuth.return_value = mockSignup + checks = [x for x in self.page._do_checks()] + + eq(len(checks), 4) + labels = [str(x) for (x, y), z in checks] + eq(labels, ['head_sentinel', + 'Resolving domain name', + 'Validating credentials', + 'end_sentinel']) + progress = [y for (x, y), z in checks] + eq(progress, [0, 20, 60, 100]) + + # normal run, ie, no exceptions + + checkfuns = [z for (x, y), z in checks] + checkusername, resolvedomain, valcreds = checkfuns[:-1] + + self.assertTrue(checkusername()) + #self.mocknetchecker.check_name_resolution.assert_called_with( + #'test_provider1') + + self.assertTrue(resolvedomain()) + #self.mockpcertchecker.is_https_working.assert_called_with( + #"https://test_provider1", verify=True) + + self.assertTrue(valcreds()) + + # XXX missing: inject failing exceptions + # XXX TODO make it break + + +class RegisterUserPageUITestCase(qunittest.TestCase): + + # XXX can spy on signal connections + __name__ = "Register User Page UI tests" + + def setUp(self): + self.app = QtGui.QApplication(sys.argv) + QtGui.qApp = self.app + + self.pagename = "signup" + pages = OrderedDict(( + (self.pagename, TestPage), + ('providersetupvalidation', + firstrun.connect.ConnectionPage))) + self.wizard = firstrun.wizard.FirstRunWizard(None, pages_dict=pages) + self.page = self.wizard.page(self.wizard.get_page_index(self.pagename)) + + self.page.do_checks = mock.Mock() + + # wizard would do this for us + self.page.initializePage() + + def tearDown(self): + QtGui.qApp = None + self.app = None + self.wizard = None + + # XXX refactor out + def fill_field(self, field, text): + """ + fills a field (line edit) that is passed along + :param field: the qLineEdit + :param text: the text to be filled + :type field: QLineEdit widget + :type text: str + """ + keyp = QTest.keyPress + field.setFocus(True) + for c in text: + keyp(field, c) + self.assertEqual(field.text(), text) + + def del_field(self, field): + """ + deletes entried text in + field line edit + :param field: the QLineEdit + :type field: QLineEdit widget + """ + keyp = QTest.keyPress + for c in range(len(field.text())): + keyp(field, Qt.Key_Backspace) + self.assertEqual(field.text(), "") + + def test_buttons_disabled_until_textentry(self): + # it's a commit button this time + nextbutton = self.wizard.button(QtGui.QWizard.CommitButton) + + self.assertFalse(nextbutton.isEnabled()) + + f_username = self.page.userNameLineEdit + f_password = self.page.userPasswordLineEdit + + self.fill_field(f_username, "testuser") + self.fill_field(f_password, "testpassword") + + # commit should be enabled + # XXX Need a workaround here + # because the isComplete is not being evaluated... + # (no event loop running??) + #import ipdb;ipdb.set_trace() + #self.assertTrue(nextbutton.isEnabled()) + self.assertTrue(self.page.isComplete()) + + self.del_field(f_username) + self.del_field(f_password) + + # after rm fields commit button + # should be disabled again + #self.assertFalse(nextbutton.isEnabled()) + self.assertFalse(self.page.isComplete()) + + def test_validate_page(self): + self.assertFalse(self.page.validatePage()) + # XXX TODO MOAR CASES... + # add errors, False + # change done, False + # not done, do_checks called + # click confirm, True + # done and do_confirm, True + + def test_next_id(self): + self.assertEqual(self.page.nextId(), 1) + + def test_paint_event(self): + self.page.populateErrors = mock.Mock() + self.page.paintEvent(None) + self.page.populateErrors.assert_called_with() + + def test_validation_ready(self): + f_username = self.page.userNameLineEdit + f_password = self.page.userPasswordLineEdit + + self.fill_field(f_username, "testuser") + self.fill_field(f_password, "testpassword") + + self.page.done = True + self.page.on_checks_validation_ready() + self.assertFalse(f_username.isEnabled()) + self.assertFalse(f_password.isEnabled()) + + self.assertEqual(self.page.validationMsg.text(), + "Credentials validated.") + self.assertEqual(self.page.do_confirm_next, True) + + def test_regex(self): + # XXX enter invalid username with key presses + # check text is not updated + pass + + +if __name__ == "__main__": + unittest.main() diff --git a/src/leap/gui/tests/test_firstrun_providerselect.py b/src/leap/gui/tests/test_firstrun_providerselect.py new file mode 100644 index 00000000..18d89010 --- /dev/null +++ b/src/leap/gui/tests/test_firstrun_providerselect.py @@ -0,0 +1,203 @@ +import sys +import unittest + +import mock + +from leap.testing import qunittest +#from leap.testing import pyqt + +from PyQt4 import QtGui +#from PyQt4 import QtCore +#import PyQt4.QtCore # some weirdness with mock module + +from PyQt4.QtTest import QTest +from PyQt4.QtCore import Qt + +from leap.gui import firstrun + +try: + from collections import OrderedDict +except ImportError: + # We must be in 2.6 + from leap.util.dicts import OrderedDict + + +class TestPage(firstrun.providerselect.SelectProviderPage): + pass + + +class SelectProviderPageLogicTestCase(qunittest.TestCase): + + # XXX can spy on signal connections + + def setUp(self): + self.app = QtGui.QApplication(sys.argv) + QtGui.qApp = self.app + self.page = TestPage(None) + self.page.wizard = mock.MagicMock() + + mocknetchecker = mock.Mock() + self.page.wizard().netchecker.return_value = mocknetchecker + self.mocknetchecker = mocknetchecker + + mockpcertchecker = mock.Mock() + self.page.wizard().providercertchecker.return_value = mockpcertchecker + self.mockpcertchecker = mockpcertchecker + + mockeipconfchecker = mock.Mock() + self.page.wizard().eipconfigchecker.return_value = mockeipconfchecker + self.mockeipconfchecker = mockeipconfchecker + + def tearDown(self): + QtGui.qApp = None + self.app = None + self.page = None + + def test__do_checks(self): + eq = self.assertEqual + + self.page.providerNameEdit.setText('test_provider1') + + checks = [x for x in self.page._do_checks()] + eq(len(checks), 5) + labels = [str(x) for (x, y), z in checks] + eq(labels, ['head_sentinel', + 'Checking if it is a valid provider', + 'Checking for a secure connection', + 'Getting info from the provider', + 'end_sentinel']) + progress = [y for (x, y), z in checks] + eq(progress, [0, 20, 40, 80, 100]) + + # normal run, ie, no exceptions + + checkfuns = [z for (x, y), z in checks] + namecheck, httpscheck, fetchinfo = checkfuns[1:-1] + + self.assertTrue(namecheck()) + self.mocknetchecker.check_name_resolution.assert_called_with( + 'test_provider1') + + self.assertTrue(httpscheck()) + self.mockpcertchecker.is_https_working.assert_called_with( + "https://test_provider1", verify=True) + + self.assertTrue(fetchinfo()) + self.mockeipconfchecker.fetch_definition.assert_called_with( + domain="test_provider1") + + # XXX missing: inject failing exceptions + # XXX TODO make it break + + +class SelectProviderPageUITestCase(qunittest.TestCase): + + # XXX can spy on signal connections + __name__ = "Select Provider Page UI tests" + + def setUp(self): + self.app = QtGui.QApplication(sys.argv) + QtGui.qApp = self.app + + self.pagename = "providerselection" + pages = OrderedDict(( + (self.pagename, TestPage), + ('providerinfo', + firstrun.providerinfo.ProviderInfoPage))) + self.wizard = firstrun.wizard.FirstRunWizard(None, pages_dict=pages) + self.page = self.wizard.page(self.wizard.get_page_index(self.pagename)) + + self.page.do_checks = mock.Mock() + + # wizard would do this for us + self.page.initializePage() + + def tearDown(self): + QtGui.qApp = None + self.app = None + self.wizard = None + + def fill_provider(self): + """ + fills provider line edit + """ + keyp = QTest.keyPress + pedit = self.page.providerNameEdit + pedit.setFocus(True) + for c in "testprovider": + keyp(pedit, c) + self.assertEqual(pedit.text(), "testprovider") + + def del_provider(self): + """ + deletes entried provider in + line edit + """ + keyp = QTest.keyPress + pedit = self.page.providerNameEdit + for c in range(len("testprovider")): + keyp(pedit, Qt.Key_Backspace) + self.assertEqual(pedit.text(), "") + + def test_buttons_disabled_until_textentry(self): + nextbutton = self.wizard.button(QtGui.QWizard.NextButton) + checkbutton = self.page.providerCheckButton + + self.assertFalse(nextbutton.isEnabled()) + self.assertFalse(checkbutton.isEnabled()) + + self.fill_provider() + # checkbutton should be enabled + self.assertTrue(checkbutton.isEnabled()) + self.assertFalse(nextbutton.isEnabled()) + + self.del_provider() + # after rm provider checkbutton disabled again + self.assertFalse(checkbutton.isEnabled()) + self.assertFalse(nextbutton.isEnabled()) + + def test_check_button_triggers_tests(self): + checkbutton = self.page.providerCheckButton + self.assertFalse(checkbutton.isEnabled()) + self.assertFalse(self.page.do_checks.called) + + self.fill_provider() + + self.assertTrue(checkbutton.isEnabled()) + mclick = QTest.mouseClick + # click! + mclick(checkbutton, Qt.LeftButton) + self.waitFor(seconds=0.1) + self.assertTrue(self.page.do_checks.called) + + # XXX + # can play with different side_effects for do_checks mock... + # so we can see what happens with errors and so on + + def test_page_completed_after_checks(self): + nextbutton = self.wizard.button(QtGui.QWizard.NextButton) + self.assertFalse(nextbutton.isEnabled()) + + self.assertFalse(self.page.isComplete()) + self.fill_provider() + # simulate checks done + self.page.done = True + self.page.on_checks_validation_ready() + self.assertTrue(self.page.isComplete()) + # cannot test for nexbutton enabled + # cause it's the the wizard loop + # that would do that I think + + def test_validate_page(self): + self.assertTrue(self.page.validatePage()) + + def test_next_id(self): + self.assertEqual(self.page.nextId(), 1) + + def test_paint_event(self): + self.page.populateErrors = mock.Mock() + self.page.paintEvent(None) + self.page.populateErrors.assert_called_with() + +if __name__ == "__main__": + unittest.main() diff --git a/src/leap/gui/tests/test_firstrun_register.py b/src/leap/gui/tests/test_firstrun_register.py new file mode 100644 index 00000000..9d62f808 --- /dev/null +++ b/src/leap/gui/tests/test_firstrun_register.py @@ -0,0 +1,244 @@ +import sys +import unittest + +import mock + +from leap.testing import qunittest +#from leap.testing import pyqt + +from PyQt4 import QtGui +#from PyQt4 import QtCore +#import PyQt4.QtCore # some weirdness with mock module + +from PyQt4.QtTest import QTest +from PyQt4.QtCore import Qt + +from leap.gui import firstrun + +try: + from collections import OrderedDict +except ImportError: + # We must be in 2.6 + from leap.util.dicts import OrderedDict + + +class TestPage(firstrun.register.RegisterUserPage): + + def field(self, field): + if field == "provider_domain": + return "testprovider" + + +class RegisterUserPageLogicTestCase(qunittest.TestCase): + + # XXX can spy on signal connections + __name__ = "register user page logic tests" + + def setUp(self): + self.app = QtGui.QApplication(sys.argv) + QtGui.qApp = self.app + self.page = TestPage(None) + self.page.wizard = mock.MagicMock() + + #mocknetchecker = mock.Mock() + #self.page.wizard().netchecker.return_value = mocknetchecker + #self.mocknetchecker = mocknetchecker +# + #mockpcertchecker = mock.Mock() + #self.page.wizard().providercertchecker.return_value = mockpcertchecker + #self.mockpcertchecker = mockpcertchecker +# + #mockeipconfchecker = mock.Mock() + #self.page.wizard().eipconfigchecker.return_value = mockeipconfchecker + #self.mockeipconfchecker = mockeipconfchecker + + def tearDown(self): + QtGui.qApp = None + self.app = None + self.page = None + + def test__do_checks(self): + eq = self.assertEqual + + self.page.userNameLineEdit.setText('testuser') + self.page.userPasswordLineEdit.setText('testpassword') + self.page.userPassword2LineEdit.setText('testpassword') + + # fake register process + with mock.patch('leap.base.auth.LeapSRPRegister') as mockAuth: + mockSignup = mock.MagicMock() + + reqMockup = mock.Mock() + # XXX should inject bad json to get error + reqMockup.content = '{"errors": null}' + mockSignup.register_user.return_value = (True, reqMockup) + mockAuth.return_value = mockSignup + checks = [x for x in self.page._do_checks()] + + eq(len(checks), 3) + labels = [str(x) for (x, y), z in checks] + eq(labels, ['head_sentinel', + 'Registering username', + 'end_sentinel']) + progress = [y for (x, y), z in checks] + eq(progress, [0, 40, 100]) + + # normal run, ie, no exceptions + + checkfuns = [z for (x, y), z in checks] + passcheck, register = checkfuns[:-1] + + self.assertTrue(passcheck()) + #self.mocknetchecker.check_name_resolution.assert_called_with( + #'test_provider1') + + self.assertTrue(register()) + #self.mockpcertchecker.is_https_working.assert_called_with( + #"https://test_provider1", verify=True) + + # XXX missing: inject failing exceptions + # XXX TODO make it break + + +class RegisterUserPageUITestCase(qunittest.TestCase): + + # XXX can spy on signal connections + __name__ = "Register User Page UI tests" + + def setUp(self): + self.app = QtGui.QApplication(sys.argv) + QtGui.qApp = self.app + + self.pagename = "signup" + pages = OrderedDict(( + (self.pagename, TestPage), + ('connect', + firstrun.connect.ConnectionPage))) + self.wizard = firstrun.wizard.FirstRunWizard(None, pages_dict=pages) + self.page = self.wizard.page(self.wizard.get_page_index(self.pagename)) + + self.page.do_checks = mock.Mock() + + # wizard would do this for us + self.page.initializePage() + + def tearDown(self): + QtGui.qApp = None + self.app = None + self.wizard = None + + def fill_field(self, field, text): + """ + fills a field (line edit) that is passed along + :param field: the qLineEdit + :param text: the text to be filled + :type field: QLineEdit widget + :type text: str + """ + keyp = QTest.keyPress + field.setFocus(True) + for c in text: + keyp(field, c) + self.assertEqual(field.text(), text) + + def del_field(self, field): + """ + deletes entried text in + field line edit + :param field: the QLineEdit + :type field: QLineEdit widget + """ + keyp = QTest.keyPress + for c in range(len(field.text())): + keyp(field, Qt.Key_Backspace) + self.assertEqual(field.text(), "") + + def test_buttons_disabled_until_textentry(self): + # it's a commit button this time + nextbutton = self.wizard.button(QtGui.QWizard.CommitButton) + + self.assertFalse(nextbutton.isEnabled()) + + f_username = self.page.userNameLineEdit + f_password = self.page.userPasswordLineEdit + f_passwor2 = self.page.userPassword2LineEdit + + self.fill_field(f_username, "testuser") + self.fill_field(f_password, "testpassword") + self.fill_field(f_passwor2, "testpassword") + + # commit should be enabled + # XXX Need a workaround here + # because the isComplete is not being evaluated... + # (no event loop running??) + #import ipdb;ipdb.set_trace() + #self.assertTrue(nextbutton.isEnabled()) + self.assertTrue(self.page.isComplete()) + + self.del_field(f_username) + self.del_field(f_password) + self.del_field(f_passwor2) + + # after rm fields commit button + # should be disabled again + #self.assertFalse(nextbutton.isEnabled()) + self.assertFalse(self.page.isComplete()) + + @unittest.skip + def test_check_button_triggers_tests(self): + checkbutton = self.page.providerCheckButton + self.assertFalse(checkbutton.isEnabled()) + self.assertFalse(self.page.do_checks.called) + + self.fill_provider() + + self.assertTrue(checkbutton.isEnabled()) + mclick = QTest.mouseClick + # click! + mclick(checkbutton, Qt.LeftButton) + self.waitFor(seconds=0.1) + self.assertTrue(self.page.do_checks.called) + + # XXX + # can play with different side_effects for do_checks mock... + # so we can see what happens with errors and so on + + def test_validate_page(self): + self.assertFalse(self.page.validatePage()) + # XXX TODO MOAR CASES... + # add errors, False + # change done, False + # not done, do_checks called + # click confirm, True + # done and do_confirm, True + + def test_next_id(self): + self.assertEqual(self.page.nextId(), 1) + + def test_paint_event(self): + self.page.populateErrors = mock.Mock() + self.page.paintEvent(None) + self.page.populateErrors.assert_called_with() + + def test_validation_ready(self): + f_username = self.page.userNameLineEdit + f_password = self.page.userPasswordLineEdit + f_passwor2 = self.page.userPassword2LineEdit + + self.fill_field(f_username, "testuser") + self.fill_field(f_password, "testpassword") + self.fill_field(f_passwor2, "testpassword") + + self.page.done = True + self.page.on_checks_validation_ready() + self.assertFalse(f_username.isEnabled()) + self.assertFalse(f_password.isEnabled()) + self.assertFalse(f_passwor2.isEnabled()) + + self.assertEqual(self.page.validationMsg.text(), + "Registration succeeded!") + self.assertEqual(self.page.do_confirm_next, True) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/leap/gui/tests/test_firstrun_wizard.py b/src/leap/gui/tests/test_firstrun_wizard.py new file mode 100644 index 00000000..395604d3 --- /dev/null +++ b/src/leap/gui/tests/test_firstrun_wizard.py @@ -0,0 +1,137 @@ +import sys +import unittest + +import mock + +from leap.testing import qunittest +from leap.testing import pyqt + +from PyQt4 import QtGui +#from PyQt4 import QtCore +import PyQt4.QtCore # some weirdness with mock module + +from PyQt4.QtTest import QTest +#from PyQt4.QtCore import Qt + +from leap.gui import firstrun + + +class TestWizard(firstrun.wizard.FirstRunWizard): + pass + + +PAGES_DICT = dict(( + ('intro', firstrun.intro.IntroPage), + ('providerselection', + firstrun.providerselect.SelectProviderPage), + ('login', firstrun.login.LogInPage), + ('providerinfo', firstrun.providerinfo.ProviderInfoPage), + ('providersetupvalidation', + firstrun.providersetup.ProviderSetupValidationPage), + ('signup', firstrun.register.RegisterUserPage), + ('connect', + firstrun.connect.ConnectionPage), + ('lastpage', firstrun.last.LastPage) +)) + + +mockQSettings = mock.MagicMock() +mockQSettings().setValue.return_value = True + +#PyQt4.QtCore.QSettings = mockQSettings + + +class FirstRunWizardTestCase(qunittest.TestCase): + + # XXX can spy on signal connections + + def setUp(self): + self.app = QtGui.QApplication(sys.argv) + QtGui.qApp = self.app + self.wizard = TestWizard(None) + + def tearDown(self): + QtGui.qApp = None + self.app = None + self.wizard = None + + def test_defaults(self): + self.assertEqual(self.wizard.pages_dict, PAGES_DICT) + + @mock.patch('PyQt4.QtCore.QSettings', mockQSettings) + def test_accept(self): + """ + test the main accept method + that gets called when user has gone + thru all the wizard and click on finish button + """ + + self.wizard.success_cb = mock.Mock() + self.wizard.success_cb.return_value = True + + # dummy values; we inject them in the field + # mocks (where wizard gets them) and then + # we check that they are passed to QSettings.setValue + field_returns = ["testuser", "1234", "testprovider", True] + + def field_side_effects(*args): + return field_returns.pop(0) + + self.wizard.field = mock.Mock(side_effect=field_side_effects) + self.wizard.get_random_str = mock.Mock() + RANDOMSTR = "thisisarandomstringTM" + self.wizard.get_random_str.return_value = RANDOMSTR + + # mocked settings (see decorator on this method) + mqs = PyQt4.QtCore.QSettings + + # go! call accept... + self.wizard.accept() + + # did settings().setValue get called with the proper + # arguments? + call = mock.call + calls = [call("FirstRunWizardDone", True), + call("provider_domain", "testprovider"), + call("remember_user_and_pass", True), + call("username", "testuser@testprovider"), + call("testprovider_seed", RANDOMSTR)] + mqs().setValue.assert_has_calls(calls, any_order=True) + + # assert success callback is success oh boy + self.wizard.success_cb.assert_called_with() + + def test_random_str(self): + r = self.wizard.get_random_str(42) + self.assertTrue(len(r) == 42) + + def test_page_index(self): + """ + we test both the get_page_index function + and the correct ordering of names + """ + # remember it's implemented as an ordered dict + + pagenames = ('intro', 'providerselection', 'login', 'providerinfo', + 'providersetupvalidation', 'signup', 'connect', + 'lastpage') + eq = self.assertEqual + w = self.wizard + for index, name in enumerate(pagenames): + eq(w.get_page_index(name), index) + + def test_validation_errors(self): + """ + tests getters and setters for validation errors + """ + page = "testpage" + eq = self.assertEqual + w = self.wizard + eq(w.get_validation_error(page), None) + w.set_validation_error(page, "error") + eq(w.get_validation_error(page), "error") + w.clean_validation_error(page) + eq(w.get_validation_error(page), None) + +if __name__ == "__main__": + unittest.main() diff --git a/src/leap/gui/test_mainwindow_rc.py b/src/leap/gui/tests/test_mainwindow_rc.py index 88ae5854..67b9fae0 100644 --- a/src/leap/gui/test_mainwindow_rc.py +++ b/src/leap/gui/tests/test_mainwindow_rc.py @@ -1,8 +1,11 @@ import unittest import hashlib -import sip -sip.setapi('QVariant', 2) +try: + import sip + sip.setapi('QVariant', 2) +except ValueError: + pass from leap.gui import mainwindow_rc @@ -23,4 +26,7 @@ class MainWindowResourcesTest(unittest.TestCase): def test_mainwindow_resources_hash(self): self.assertEqual( hashlib.md5(mainwindow_rc.qt_resource_data).hexdigest(), - 'd74eb99247b9d5cd2f00b2f695ca6b59') + '53e196f29061d8f08f112e5a2e64eb53') + +if __name__ == "__main__": + unittest.main() diff --git a/src/leap/gui/tests/test_progress.py b/src/leap/gui/tests/test_progress.py new file mode 100644 index 00000000..1f9f9e38 --- /dev/null +++ b/src/leap/gui/tests/test_progress.py @@ -0,0 +1,449 @@ +from collections import namedtuple +import sys +import unittest +import Queue + +import mock + +from leap.testing import qunittest +from leap.testing import pyqt + +from PyQt4 import QtGui +from PyQt4 import QtCore +from PyQt4.QtTest import QTest +from PyQt4.QtCore import Qt + +from leap.gui import progress + + +class ProgressStepTestCase(unittest.TestCase): + + def test_step_attrs(self): + ps = progress.ProgressStep + step = ps('test', False, 1) + # instance + self.assertEqual(step.index, 1) + self.assertEqual(step.name, "test") + self.assertEqual(step.done, False) + step = ps('test2', True, 2) + self.assertEqual(step.index, 2) + self.assertEqual(step.name, "test2") + self.assertEqual(step.done, True) + + # class methods and attrs + self.assertEqual(ps.columns(), ('name', 'done')) + self.assertEqual(ps.NAME, 0) + self.assertEqual(ps.DONE, 1) + + +class ProgressStepContainerTestCase(unittest.TestCase): + def setUp(self): + self.psc = progress.ProgressStepContainer() + + def addSteps(self, number): + Step = progress.ProgressStep + for n in range(number): + self.psc.addStep(Step("%s" % n, False, n)) + + def test_attrs(self): + self.assertEqual(self.psc.columns, + ('name', 'done')) + + def test_add_steps(self): + Step = progress.ProgressStep + self.assertTrue(len(self.psc) == 0) + self.psc.addStep(Step('one', False, 0)) + self.assertTrue(len(self.psc) == 1) + self.psc.addStep(Step('two', False, 1)) + self.assertTrue(len(self.psc) == 2) + + def test_del_all_steps(self): + self.assertTrue(len(self.psc) == 0) + self.addSteps(5) + self.assertTrue(len(self.psc) == 5) + self.psc.removeAllSteps() + self.assertTrue(len(self.psc) == 0) + + def test_del_step(self): + Step = progress.ProgressStep + self.addSteps(5) + self.assertTrue(len(self.psc) == 5) + self.psc.removeStep(self.psc.step(4)) + self.assertTrue(len(self.psc) == 4) + self.psc.removeStep(self.psc.step(4)) + self.psc.removeStep(Step('none', False, 5)) + self.psc.removeStep(self.psc.step(4)) + + def test_iter(self): + self.addSteps(10) + self.assertEqual( + [x.index for x in self.psc], + [x for x in range(10)]) + + +class StepsTableWidgetTestCase(unittest.TestCase): + + def setUp(self): + self.app = QtGui.QApplication(sys.argv) + QtGui.qApp = self.app + self.stw = progress.StepsTableWidget() + + def tearDown(self): + QtGui.qApp = None + self.app = None + + def test_defaults(self): + self.assertTrue(isinstance(self.stw, QtGui.QTableWidget)) + self.assertEqual(self.stw.focusPolicy(), 0) + + +class TestWithStepsClass(QtGui.QWidget, progress.WithStepsMixIn): + + def __init__(self, parent=None): + super(TestWithStepsClass, self).__init__(parent=parent) + self.setupStepsProcessingQueue() + self.statuses = [] + self.current_page = "testpage" + + def onStepStatusChanged(self, *args): + """ + blank out this gui method + that will add status lines + """ + self.statuses.append(args) + + +class WithStepsMixInTestCase(qunittest.TestCase): + + TIMER_WAIT = 2 * progress.WithStepsMixIn.STEPS_TIMER_MS / 1000.0 + + # XXX can spy on signal connections + + def setUp(self): + self.app = QtGui.QApplication(sys.argv) + QtGui.qApp = self.app + self.stepy = TestWithStepsClass() + #self.connects = [] + #pyqt.enableSignalDebugging( + #connectCall=lambda *args: self.connects.append(args)) + #self.assertEqual(self.connects, []) + #self.stepy.stepscheck_timer.timeout.disconnect( + #self.stepy.processStepsQueue) + + def tearDown(self): + QtGui.qApp = None + self.app = None + + def test_has_queue(self): + s = self.stepy + self.assertTrue(hasattr(s, 'steps_queue')) + self.assertTrue(isinstance(s.steps_queue, Queue.Queue)) + self.assertTrue(isinstance(s.stepscheck_timer, QtCore.QTimer)) + + def test_do_checks_delegation(self): + s = self.stepy + + _do_checks = mock.Mock() + _do_checks.return_value = ( + (("test", 0), lambda: None), + (("test", 0), lambda: None)) + s._do_checks = _do_checks + s.do_checks() + self.waitFor(seconds=self.TIMER_WAIT) + _do_checks.assert_called_with() + self.assertEqual(len(s.statuses), 2) + + # test that a failed test interrupts the run + + s.statuses = [] + _do_checks = mock.Mock() + _do_checks.return_value = ( + (("test", 0), lambda: None), + (("test", 0), lambda: False), + (("test", 0), lambda: None)) + s._do_checks = _do_checks + s.do_checks() + self.waitFor(seconds=self.TIMER_WAIT) + _do_checks.assert_called_with() + self.assertEqual(len(s.statuses), 2) + + def test_process_queue(self): + s = self.stepy + q = s.steps_queue + s.set_failed_icon = mock.MagicMock() + with self.assertRaises(AssertionError): + q.put('foo') + self.waitFor(seconds=self.TIMER_WAIT) + s.set_failed_icon.assert_called_with() + q.put("failed") + self.waitFor(seconds=self.TIMER_WAIT) + s.set_failed_icon.assert_called_with() + + def test_on_checks_validation_ready_called(self): + s = self.stepy + s.on_checks_validation_ready = mock.MagicMock() + + _do_checks = mock.Mock() + _do_checks.return_value = ( + (("test", 0), lambda: None),) + s._do_checks = _do_checks + s.do_checks() + + self.waitFor(seconds=self.TIMER_WAIT) + s.on_checks_validation_ready.assert_called_with() + + def test_fail(self): + s = self.stepy + + s.wizard = mock.Mock() + wizard = s.wizard.return_value + wizard.set_validation_error.return_value = True + s.completeChanged = mock.Mock() + s.completeChanged.emit.return_value = True + + self.assertFalse(s.fail(err="foo")) + self.waitFor(seconds=self.TIMER_WAIT) + wizard.set_validation_error.assert_called_with('testpage', 'foo') + s.completeChanged.emit.assert_called_with() + + # with no args + s.wizard = mock.Mock() + wizard = s.wizard.return_value + wizard.set_validation_error.return_value = True + s.completeChanged = mock.Mock() + s.completeChanged.emit.return_value = True + + self.assertFalse(s.fail()) + self.waitFor(seconds=self.TIMER_WAIT) + with self.assertRaises(AssertionError): + wizard.set_validation_error.assert_called_with() + s.completeChanged.emit.assert_called_with() + + def test_done(self): + s = self.stepy + s.done = False + + s.completeChanged = mock.Mock() + s.completeChanged.emit.return_value = True + + self.assertFalse(s.is_done()) + s.set_done() + self.assertTrue(s.is_done()) + s.completeChanged.emit.assert_called_with() + + s.completeChanged = mock.Mock() + s.completeChanged.emit.return_value = True + s.set_undone() + self.assertFalse(s.is_done()) + + def test_back_and_next(self): + s = self.stepy + s.wizard = mock.Mock() + wizard = s.wizard.return_value + wizard.back.return_value = True + wizard.next.return_value = True + s.go_back() + wizard.back.assert_called_with() + s.go_next() + wizard.next.assert_called_with() + + def test_on_step_statuschanged_slot(self): + s = self.stepy + s.onStepStatusChanged = progress.WithStepsMixIn.onStepStatusChanged + s.add_status_line = mock.Mock() + s.set_checked_icon = mock.Mock() + s.progress = mock.Mock() + s.progress.setValue.return_value = True + s.progress.update.return_value = True + + s.onStepStatusChanged(s, "end_sentinel") + s.set_checked_icon.assert_called_with() + + s.onStepStatusChanged(s, "foo") + s.add_status_line.assert_called_with("foo") + + s.onStepStatusChanged(s, "bar", 42) + s.progress.setValue.assert_called_with(42) + s.progress.update.assert_called_with() + + def test_steps_and_errors(self): + s = self.stepy + s.setupSteps() + self.assertTrue(isinstance(s.steps, progress.ProgressStepContainer)) + self.assertEqual(s.errors, {}) + s.set_error('fooerror', 'barerror') + self.assertEqual(s.errors, {'fooerror': 'barerror'}) + s.set_error('2', 42) + self.assertEqual(s.errors, {'fooerror': 'barerror', '2': 42}) + fe = s.pop_first_error() + self.assertEqual(fe, ('fooerror', 'barerror')) + self.assertEqual(s.errors, {'2': 42}) + s.clean_errors() + self.assertEqual(s.errors, {}) + + def test_launch_chechs_slot(self): + s = self.stepy + s.do_checks = mock.Mock() + s.launch_checks() + s.do_checks.assert_called_with() + + def test_clean_wizard_errors(self): + s = self.stepy + s.wizard = mock.Mock() + wizard = s.wizard.return_value + wizard.set_validation_error.return_value = True + s.clean_wizard_errors(pagename="foopage") + wizard.set_validation_error.assert_called_with("foopage", None) + + def test_clear_table(self): + s = self.stepy + s.stepsTableWidget = mock.Mock() + s.stepsTableWidget.clearContents.return_value = True + s.clearTable() + s.stepsTableWidget.clearContents.assert_called_with() + + def test_populate_steps_table(self): + s = self.stepy + Step = namedtuple('Step', ['name', 'done']) + + class Steps(object): + columns = ("name", "done") + _items = (Step('step1', False), Step('step2', False)) + + def __len__(self): + return 2 + + def __iter__(self): + for i in self._items: + yield i + + s.steps = Steps() + + s.stepsTableWidget = mock.Mock() + s.stepsTableWidget.setItem.return_value = True + s.resizeTable = mock.Mock() + s.update = mock.Mock() + s.populateStepsTable() + s.update.assert_called_with() + s.resizeTable.assert_called_with() + + # assert stepsTableWidget.setItem called ... + # we do not want to get into the actual + # <QTableWidgetItem object at 0x92a565c> + call_list = s.stepsTableWidget.setItem.call_args_list + indexes = [(y, z) for y, z, xx in [x[0] for x in call_list]] + self.assertEqual(indexes, + [(0, 0), (0, 1), (1, 0), (1, 1)]) + + def test_add_status_line(self): + s = self.stepy + s.steps = progress.ProgressStepContainer() + s.stepsTableWidget = mock.Mock() + s.stepsTableWidget.width.return_value = 100 + s.set_item = mock.Mock() + s.set_item_icon = mock.Mock() + s.add_status_line("new status") + s.set_item_icon.assert_called_with(current=False) + + def test_set_item_icon(self): + s = self.stepy + s.steps = progress.ProgressStepContainer() + s.stepsTableWidget = mock.Mock() + s.stepsTableWidget.setCellWidget.return_value = True + s.stepsTableWidget.width.return_value = 100 + #s.set_item = mock.Mock() + #s.set_item_icon = mock.Mock() + s.add_status_line("new status") + s.add_status_line("new 2 status") + s.add_status_line("new 3 status") + call_list = s.stepsTableWidget.setCellWidget.call_args_list + indexes = [(y, z) for y, z, xx in [x[0] for x in call_list]] + self.assertEqual( + indexes, + [(0, 1), (-1, 1), (1, 1), (0, 1), (2, 1), (1, 1)]) + + +class TestInlineValidationPage(progress.InlineValidationPage): + pass + + +class InlineValidationPageTestCase(unittest.TestCase): + + def setUp(self): + self.app = QtGui.QApplication(sys.argv) + QtGui.qApp = self.app + self.page = TestInlineValidationPage() + + def tearDown(self): + QtGui.qApp = None + self.app = None + + def test_defaults(self): + self.assertFalse(self.page.done) + # if setupProcessingQueue was called + self.assertTrue(isinstance(self.page.stepscheck_timer, QtCore.QTimer)) + self.assertTrue(isinstance(self.page.steps_queue, Queue.Queue)) + + def test_validation_frame(self): + # test frame creation + self.page.stepsTableWidget = progress.StepsTableWidget( + parent=self.page) + self.page.setupValidationFrame() + self.assertTrue(isinstance(self.page.valFrame, QtGui.QFrame)) + + # test show steps calls frame.show + self.page.valFrame = mock.Mock() + self.page.valFrame.show.return_value = True + self.page.showStepsFrame() + self.page.valFrame.show.assert_called_with() + + +class TestValidationPage(progress.ValidationPage): + pass + + +class ValidationPageTestCase(unittest.TestCase): + + def setUp(self): + self.app = QtGui.QApplication(sys.argv) + QtGui.qApp = self.app + self.page = TestValidationPage() + + def tearDown(self): + QtGui.qApp = None + self.app = None + + def test_defaults(self): + self.assertFalse(self.page.done) + # if setupProcessingQueue was called + self.assertTrue(isinstance(self.page.timer, QtCore.QTimer)) + self.assertTrue(isinstance(self.page.stepscheck_timer, QtCore.QTimer)) + self.assertTrue(isinstance(self.page.steps_queue, Queue.Queue)) + + def test_is_complete(self): + self.assertFalse(self.page.isComplete()) + self.page.done = True + self.assertTrue(self.page.isComplete()) + self.page.done = False + self.assertFalse(self.page.isComplete()) + + def test_show_hide_progress(self): + p = self.page + p.progress = mock.Mock() + p.progress.show.return_code = True + p.show_progress() + p.progress.show.assert_called_with() + p.progress.hide.return_code = True + p.hide_progress() + p.progress.hide.assert_called_with() + + def test_initialize_page(self): + p = self.page + p.timer = mock.Mock() + p.timer.singleShot.return_code = True + p.initializePage() + p.timer.singleShot.assert_called_with(0, p.do_checks) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/leap/gui/tests/test_threads.py b/src/leap/gui/tests/test_threads.py new file mode 100644 index 00000000..06c19606 --- /dev/null +++ b/src/leap/gui/tests/test_threads.py @@ -0,0 +1,27 @@ +import unittest + +import mock +from leap.gui import threads + + +class FunThreadTestCase(unittest.TestCase): + + def setUp(self): + self.fun = mock.MagicMock() + self.fun.return_value = "foo" + self.t = threads.FunThread(fun=self.fun) + + def test_thread(self): + self.t.begin() + self.t.wait() + self.fun.assert_called() + del self.t + + def test_run(self): + # this is called by PyQt + self.t.run() + del self.t + self.fun.assert_called() + +if __name__ == "__main__": + unittest.main() diff --git a/src/leap/gui/threads.py b/src/leap/gui/threads.py new file mode 100644 index 00000000..8aad8866 --- /dev/null +++ b/src/leap/gui/threads.py @@ -0,0 +1,21 @@ +from PyQt4 import QtCore + + +class FunThread(QtCore.QThread): + + def __init__(self, fun=None, parent=None): + + QtCore.QThread.__init__(self, parent) + self.exiting = False + self.fun = fun + + def __del__(self): + self.exiting = True + self.wait() + + def run(self): + if self.fun: + self.fun() + + def begin(self): + self.start() diff --git a/src/leap/gui/utils.py b/src/leap/gui/utils.py new file mode 100644 index 00000000..f91ac3ef --- /dev/null +++ b/src/leap/gui/utils.py @@ -0,0 +1,34 @@ +""" +utility functions to work with gui objects +""" +from PyQt4 import QtCore + + +def layout_widgets(layout): + """ + return a generator with all widgets in a layout + """ + return (layout.itemAt(i) for i in range(layout.count())) + + +DELAY_MSECS = 50 + + +def delay(obj, method_str=None, call_args=None): + """ + Triggers a function or slot with a small delay. + this is a mainly a hack to get responsiveness in the ui + in cases in which the event loop freezes and the task + is not heavy enough to setup a processing queue. + """ + if callable(obj) and not method_str: + fun = lambda: obj() + + if method_str: + invoke = QtCore.QMetaObject.invokeMethod + if call_args: + fun = lambda: invoke(obj, method_str, call_args) + else: + fun = lambda: invoke(obj, method_str) + + QtCore.QTimer().singleShot(DELAY_MSECS, fun) |