~ubuntu-branches/ubuntu/raring/software-center/raring-proposed

« back to all changes in this revision

Viewing changes to softwarecenter/sso/gui.py

  • Committer: Package Import Robot
  • Author(s): Michael Vogt
  • Date: 2012-10-11 15:33:05 UTC
  • mfrom: (195.1.18 quantal)
  • Revision ID: package-import@ubuntu.com-20121011153305-fm5ln7if3rpzts4n
Tags: 5.4.1.1
* lp:~mvo/software-center/reinstall-previous-purchase-token-fix:
  - fix reinstall previous purchases that have a system-wide
    license key LP: #1065481
* lp:~mvo/software-center/lp1060106:
  - Add missing gettext init for utils/update-software-center-agent
    (LP: #1060106)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
#
 
3
# Copyright 2010-2012 Canonical Ltd.
 
4
#
 
5
# This program is free software: you can redistribute it and/or modify it
 
6
# under the terms of the GNU General Public License version 3, as published
 
7
# by the Free Software Foundation.
 
8
#
 
9
# This program is distributed in the hope that it will be useful, but
 
10
# WITHOUT ANY WARRANTY; without even the implied warranties of
 
11
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
 
12
# PURPOSE.  See the GNU General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU General Public License along
 
15
# with this program.  If not, see <http://www.gnu.org/licenses/>.
 
16
#
 
17
 
 
18
"""The Ubuntu Single Sign On GTK+ graphical user interface."""
 
19
 
 
20
import logging
 
21
import os
 
22
import sys
 
23
import tempfile
 
24
import webbrowser
 
25
 
 
26
from functools import wraps, partial
 
27
 
 
28
import dbus
 
29
 
 
30
# pylint: disable=E0611,F0401
 
31
from gi.repository import Gdk, Gtk
 
32
from gi.repository.GdkX11 import X11Window
 
33
# pylint: enable=E0611,F0401
 
34
 
 
35
from ubuntu_sso import (
 
36
    DBUS_BUS_NAME,
 
37
    DBUS_ACCOUNT_PATH,
 
38
    DBUS_IFACE_USER_NAME,
 
39
    NO_OP,
 
40
    USER_CANCELLATION,
 
41
    USER_SUCCESS,
 
42
)
 
43
from ubuntu_sso.logger import setup_gui_logging
 
44
from ubuntu_sso.utils import ui as ui_strings
 
45
from ubuntu_sso.utils.ui import (
 
46
    CAPTCHA_LOAD_ERROR,
 
47
    CAPTCHA_RELOAD_TOOLTIP,
 
48
    CONNECT_HELP_LABEL,
 
49
    EMAIL_MISMATCH,
 
50
    EMAIL_INVALID,
 
51
    ERROR,
 
52
    FIELD_REQUIRED,
 
53
    FORGOTTEN_PASSWORD_BUTTON,
 
54
    GENERIC_BACKEND_ERROR,
 
55
    is_min_required_password,
 
56
    is_correct_email,
 
57
    JOIN_HEADER_LABEL,
 
58
    LOADING,
 
59
    LOGIN_BUTTON_LABEL,
 
60
    LOGIN_HEADER_LABEL,
 
61
    NEXT,
 
62
    ONE_MOMENT_PLEASE,
 
63
    PASSWORD_CHANGED,
 
64
    PASSWORD_HELP,
 
65
    PASSWORD_MISMATCH,
 
66
    PASSWORD_TOO_WEAK,
 
67
    REQUEST_PASSWORD_TOKEN_LABEL,
 
68
    RESET_PASSWORD,
 
69
    SET_NEW_PASSWORD_LABEL,
 
70
    SUCCESS,
 
71
    TC_BUTTON,
 
72
    TC_NOT_ACCEPTED,
 
73
    VERIFY_EMAIL_LABEL,
 
74
    YES_TO_TC,
 
75
    YES_TO_UPDATES,
 
76
)
 
77
 
 
78
# Instance of 'UbuntuSSOClientGUI' has no 'yyy' member
 
79
# pylint: disable=E1101
 
80
 
 
81
 
 
82
logger = setup_gui_logging('ubuntu_sso.gui.gtk')
 
83
 
 
84
 
 
85
# pylint: disable=C0103
 
86
def parse_color(color):
 
87
    """Parse a string color into Gdk.Color."""
 
88
    c = Gdk.RGBA()
 
89
    result = c.parse(color)
 
90
    if not result:
 
91
        logger.warning('Could not parse color %r.', color)
 
92
    return c
 
93
# pylint: enable=C0103
 
94
 
 
95
DEFAULT_WIDTH = 30
 
96
# To be replaced by values from the theme (LP: #616526)
 
97
HELP_TEXT_COLOR = parse_color("#bfbfbf")
 
98
WARNING_TEXT_COLOR = parse_color("red")
 
99
LARGE_MARKUP = u'<span size="x-large">%s</span>'
 
100
 
 
101
 
 
102
# SSL properties and certs location
 
103
STRICT_SSL_PROP = 'ssl-strict'
 
104
CERTS_FILE_PROP = 'ssl-ca-file'
 
105
CA_CERT_FILE = '/etc/ssl/certs/ca-certificates.crt'
 
106
 
 
107
 
 
108
def log_call(f):
 
109
    """Decorator to log call funtions."""
 
110
 
 
111
    @wraps(f)
 
112
    def inner(*args, **kwargs):
 
113
        """Execute 'f' logging the call as INFO."""
 
114
        logger.info('%s: args %r, kwargs %r.', f.__name__, args, kwargs)
 
115
        return f(*args, **kwargs)
 
116
 
 
117
    return inner
 
118
 
 
119
 
 
120
def get_sso_client():
 
121
    bus = dbus.SessionBus()
 
122
    obj = bus.get_object(bus_name=DBUS_BUS_NAME,
 
123
                         object_path=DBUS_ACCOUNT_PATH,
 
124
                         follow_name_owner_changes=True)
 
125
    result = dbus.Interface(obj, dbus_interface=DBUS_IFACE_USER_NAME)
 
126
    result.disconnect_from_signal = lambda _, sig: sig.remove()
 
127
    return result
 
128
 
 
129
 
 
130
def get_data_file(*args):
 
131
    result = os.path.abspath(os.path.join(os.path.dirname(__file__),
 
132
                                          '..', '..', 'data'))
 
133
    if not os.path.exists(result):
 
134
        import softwarecenter.paths
 
135
        result = softwarecenter.paths.datadir
 
136
 
 
137
    result = os.path.join(result, 'ui', 'sso', *args)
 
138
    logger.info('Using data dir: %r', result)
 
139
    return result
 
140
 
 
141
 
 
142
class LabeledEntry(Gtk.Entry):
 
143
    """An entry that displays the label within itself ina grey color."""
 
144
 
 
145
    # Use of super on an old style class
 
146
    # pylint: disable=E1002
 
147
 
 
148
    def __init__(self, label, is_password=False, *args, **kwargs):
 
149
        self.label = label
 
150
        self.is_password = is_password
 
151
        self.warning = None
 
152
 
 
153
        super(LabeledEntry, self).__init__(*args, **kwargs)
 
154
 
 
155
        self.set_width_chars(DEFAULT_WIDTH)
 
156
        self._set_label(self, None)
 
157
        self.set_tooltip_text(self.label)
 
158
        self.connect('focus-in-event', self._clear_text)
 
159
        self.connect('focus-out-event', self._set_label)
 
160
        self.clear_warning()
 
161
        self.show()
 
162
 
 
163
    def _clear_text(self, *args, **kwargs):
 
164
        """Clear text and restore text color."""
 
165
        self.set_text(self.get_text())
 
166
 
 
167
        # restore to theme's default
 
168
        self.override_color(Gtk.StateFlags.NORMAL, None)
 
169
 
 
170
        if self.is_password:
 
171
            self.set_visibility(False)
 
172
 
 
173
        return False  # propagate the event further
 
174
 
 
175
    def _set_label(self, *args, **kwargs):
 
176
        """Set the proper label and proper coloring."""
 
177
        if self.get_text():
 
178
            return
 
179
 
 
180
        self.set_text(self.label)
 
181
        self.override_color(Gtk.StateFlags.NORMAL, HELP_TEXT_COLOR)
 
182
 
 
183
        if self.is_password:
 
184
            self.set_visibility(True)
 
185
 
 
186
        return False  # propagate the event further
 
187
 
 
188
    def get_text(self):
 
189
        """Get text only if it's not the label nor empty."""
 
190
        result = super(LabeledEntry, self).get_text().decode('utf8')
 
191
        if result == self.label or result.isspace():
 
192
            result = u''
 
193
        return result
 
194
 
 
195
    def set_warning(self, warning_msg):
 
196
        """Display warning as secondary icon, set 'warning_msg' as tooltip."""
 
197
        self.warning = warning_msg
 
198
        self.set_property('secondary-icon-stock', Gtk.STOCK_DIALOG_WARNING)
 
199
        self.set_property('secondary-icon-sensitive', True)
 
200
        self.set_property('secondary-icon-activatable', False)
 
201
        self.set_property('secondary-icon-tooltip-text', warning_msg)
 
202
 
 
203
    def clear_warning(self):
 
204
        """Remove any warning."""
 
205
        self.warning = None
 
206
        self.set_property('secondary-icon-stock', None)
 
207
        self.set_property('secondary-icon-sensitive', False)
 
208
        self.set_property('secondary-icon-activatable', False)
 
209
        self.set_property('secondary-icon-tooltip-text', None)
 
210
 
 
211
 
 
212
class UbuntuSSOClientGUI(object):
 
213
    """Ubuntu single sign-on GUI."""
 
214
 
 
215
    def __init__(self, app_name, **kwargs):
 
216
        """Create the GUI and initialize widgets."""
 
217
        logger.debug('UbuntuSSOClientGUI: app_name %r, kwargs %r.',
 
218
                     app_name, kwargs)
 
219
 
 
220
        self._captcha_filename = tempfile.mktemp()
 
221
        self._captcha_id = None
 
222
        self._signals_receivers = {}
 
223
        self._done = False  # whether the whole process was completed or not
 
224
 
 
225
        self.app_name = app_name
 
226
        self.app_label = u'<b>%s</b>' % self.app_name
 
227
        self.ping_url = kwargs.get('ping_url', u'')
 
228
        self.tc_url = kwargs.get('tc_url', u'')
 
229
        self.help_text = kwargs.get('help_text', u'')
 
230
        self.login_only = kwargs.get('login_only', False)
 
231
        window_id = kwargs.get('window_id', 0)
 
232
        self.close_callback = kwargs.get('close_callback', NO_OP)
 
233
        self.backend = None
 
234
        self.user_email = None
 
235
        self.user_password = None
 
236
 
 
237
        ui_filename = get_data_file('sso.ui')
 
238
        builder = Gtk.Builder()
 
239
        builder.add_from_file(ui_filename)
 
240
        builder.connect_signals(self)
 
241
 
 
242
        self.widgets = []
 
243
        self.warnings = []
 
244
        self.cancels = []
 
245
        for obj in builder.get_objects():
 
246
            name = getattr(obj, 'name', None)
 
247
            if name is None and isinstance(obj, Gtk.Buildable):
 
248
                # work around bug lp:507739
 
249
                name = Gtk.Buildable.get_name(obj)
 
250
            if name is None:
 
251
                logging.warn("%s has no name (??)", obj)
 
252
            else:
 
253
                self.widgets.append(name)
 
254
                setattr(self, name, obj)
 
255
                if 'warning' in name:
 
256
                    self.warnings.append(obj)
 
257
                    obj.set_text('')
 
258
                if 'cancel_button' in name:
 
259
                    obj.connect('clicked', self.on_close_clicked)
 
260
                    self.cancels.append(obj)
 
261
 
 
262
        # Connect the activate-link signal here
 
263
        # GtkBuilder in GTK 3 seems to not do this
 
264
        self.login_button.connect('activate-link', self.on_activate_link)
 
265
        self.forgotten_password_button.connect('activate-link',
 
266
                                               self.on_activate_link)
 
267
 
 
268
        self.entries = (u'name_entry', u'email1_entry', u'email2_entry',
 
269
                        u'password1_entry', u'password2_entry',
 
270
                        u'captcha_solution_entry', u'email_token_entry',
 
271
                        u'login_email_entry', u'login_password_entry',
 
272
                        u'reset_email_entry', u'reset_code_entry',
 
273
                        u'reset_password1_entry', u'reset_password2_entry')
 
274
 
 
275
        for name in self.entries:
 
276
            label = getattr(ui_strings, name.upper())
 
277
            is_password = 'password' in name
 
278
            entry = LabeledEntry(label=label, is_password=is_password)
 
279
            entry.set_activates_default(True)
 
280
            setattr(self, name, entry)
 
281
 
 
282
        self.window.set_icon_name('ubuntu-logo')
 
283
 
 
284
        self.pages = (self.enter_details_vbox, self.processing_vbox,
 
285
                      self.verify_email_vbox, self.finish_vbox,
 
286
                      self.tc_browser_vbox, self.login_vbox,
 
287
                      self.request_password_token_vbox,
 
288
                      self.set_new_password_vbox)
 
289
 
 
290
        self._signals = {
 
291
            'CaptchaGenerated':
 
292
             self._filter_by_app_name(self.on_captcha_generated),
 
293
            'CaptchaGenerationError':
 
294
             self._filter_by_app_name(self.on_captcha_generation_error),
 
295
            'UserRegistered':
 
296
             self._filter_by_app_name(self.on_user_registered),
 
297
            'UserRegistrationError':
 
298
             self._filter_by_app_name(self.on_user_registration_error),
 
299
            'EmailValidated':
 
300
             self._filter_by_app_name(self.on_email_validated),
 
301
            'EmailValidationError':
 
302
             self._filter_by_app_name(self.on_email_validation_error),
 
303
            'LoggedIn':
 
304
             self._filter_by_app_name(self.on_logged_in),
 
305
            'LoginError':
 
306
             self._filter_by_app_name(self.on_login_error),
 
307
            'UserNotValidated':
 
308
             self._filter_by_app_name(self.on_user_not_validated),
 
309
            'PasswordResetTokenSent':
 
310
             self._filter_by_app_name(self.on_password_reset_token_sent),
 
311
            'PasswordResetError':
 
312
             self._filter_by_app_name(self.on_password_reset_error),
 
313
            'PasswordChanged':
 
314
             self._filter_by_app_name(self.on_password_changed),
 
315
            'PasswordChangeError':
 
316
             self._filter_by_app_name(self.on_password_change_error),
 
317
        }
 
318
 
 
319
        if window_id != 0:
 
320
            # be as robust as possible:
 
321
            # if the window_id is not "good", set_transient_for will fail
 
322
            # awfully, and we don't want that: if the window_id is bad we can
 
323
            # still do everything as a standalone window. Also,
 
324
            # window_foreign_new may return None breaking set_transient_for.
 
325
            try:
 
326
                display = Gdk.Display.get_default()
 
327
                # this is not working, we need to create a XLib.window
 
328
                # as a second parameter to foreign_new_for_display
 
329
                win = X11Window.foreign_new_for_display(display, None)
 
330
                self.window.realize()
 
331
                self.window.window.set_transient_for(win)
 
332
            except:  # pylint: disable=W0702
 
333
                msg = 'UbuntuSSOClientGUI: failed set_transient_for win id %r'
 
334
                logger.exception(msg, window_id)
 
335
 
 
336
        self.yes_to_updates_checkbutton.hide()
 
337
        self.start_backend()
 
338
 
 
339
    def start_backend(self):
 
340
        """Start the backend, show the window when ready."""
 
341
        self.backend = get_sso_client()
 
342
 
 
343
        logger.debug('UbuntuSSOClientGUI: backend created: %r', self.backend)
 
344
 
 
345
        self._setup_signals()
 
346
        self._append_pages()
 
347
        self.window.show()
 
348
 
 
349
    @property
 
350
    def success_vbox(self):
 
351
        """The success page."""
 
352
        message = SUCCESS % {'app_name': self.app_name}
 
353
        message = LARGE_MARKUP % message
 
354
        self.finish_vbox.label.set_markup(message)
 
355
        return self.finish_vbox
 
356
 
 
357
    @property
 
358
    def error_vbox(self):
 
359
        """The error page."""
 
360
        self.finish_vbox.label.set_markup(LARGE_MARKUP % ERROR)
 
361
        return self.finish_vbox
 
362
 
 
363
    # helpers
 
364
 
 
365
    def _filter_by_app_name(self, f):
 
366
        """Excecute the decorated function only for 'self.app_name'."""
 
367
 
 
368
        @wraps(f)
 
369
        def inner(app_name, *args, **kwargs):
 
370
            """Execute 'f' only if 'app_name' matches 'self.app_name'."""
 
371
            result = None
 
372
            if app_name == self.app_name:
 
373
                result = f(app_name, *args, **kwargs)
 
374
            else:
 
375
                logger.info('%s: ignoring call since received app_name '
 
376
                            '%r (expected %r)',
 
377
                            f.__name__, app_name, self.app_name)
 
378
            return result
 
379
 
 
380
        return inner
 
381
 
 
382
    def _setup_signals(self):
 
383
        """Bind signals to callbacks to be able to test the pages."""
 
384
        for signal, method in self._signals.items():
 
385
            actual = self._signals_receivers.get(signal)
 
386
            if actual is not None:
 
387
                msg = 'Signal %r is already connected with %r.'
 
388
                logger.warning(msg, signal, actual)
 
389
 
 
390
            match = self.backend.connect_to_signal(signal, method)
 
391
            self._signals_receivers[signal] = match
 
392
 
 
393
    def _add_spinner_to_container(self, container, legend=None):
 
394
        """Add a spinner to 'container'."""
 
395
        spinner = Gtk.Spinner()
 
396
        spinner.start()
 
397
 
 
398
        label = Gtk.Label()
 
399
        if legend:
 
400
            label.set_text(legend)
 
401
        else:
 
402
            label.set_text(LOADING)
 
403
 
 
404
        hbox = Gtk.HBox(spacing=5)
 
405
        hbox.pack_start(spinner, expand=False, fill=True, padding=0)
 
406
        hbox.pack_start(label, expand=False, fill=True, padding=0)
 
407
 
 
408
        alignment = Gtk.Alignment(xalign=0.5, yalign=0.5,
 
409
                                  xscale=0, yscale=0)
 
410
        alignment.add(hbox)
 
411
        alignment.show_all()
 
412
 
 
413
        # remove children to avoid:
 
414
        # GtkWarning: Attempting to add a widget with type GtkAlignment to a
 
415
        # GtkEventBox, but as a GtkBin subclass a GtkEventBox can only contain
 
416
        # one widget at a time
 
417
        for child in container.get_children():
 
418
            container.remove(child)
 
419
        container.add(alignment)
 
420
 
 
421
    def _set_warning_message(self, widget, message):
 
422
        """Set 'message' as text for 'widget'."""
 
423
        widget.set_text(message)
 
424
        widget.override_color(Gtk.StateFlags.NORMAL, WARNING_TEXT_COLOR)
 
425
        widget.show()
 
426
 
 
427
    def _clear_warnings(self):
 
428
        """Clear all warning messages."""
 
429
        for widget in self.warnings:
 
430
            widget.set_text('')
 
431
        for widget in self.entries:
 
432
            getattr(self, widget).clear_warning()
 
433
 
 
434
    def _non_empty_input(self, widget):
 
435
        """Return weather widget has non empty content."""
 
436
        text = widget.get_text()
 
437
        return bool(text and not text.isspace())
 
438
 
 
439
    def _handle_error(self, remote_call, handler, error):
 
440
        """Handle any error when calling the remote backend."""
 
441
        logger.error('Remote call %r failed with: %r', remote_call, error)
 
442
        errordict = {'message': GENERIC_BACKEND_ERROR}
 
443
        handler(self.app_name, errordict)
 
444
 
 
445
    # build pages
 
446
 
 
447
    def _append_pages(self):
 
448
        """Append all the requires pages to main widget."""
 
449
        self._append_page(self._build_processing_page())
 
450
        self._append_page(self._build_finish_page())
 
451
        self._append_page(self._build_login_page())
 
452
        self._append_page(self._build_request_password_token_page())
 
453
        self._append_page(self._build_set_new_password_page())
 
454
        self._append_page(self._build_verify_email_page())
 
455
 
 
456
        if not self.login_only:
 
457
            self._append_page(self._build_enter_details_page())
 
458
            self._append_page(self._build_tc_page())
 
459
            self.login_button.grab_focus()
 
460
            self._set_current_page(self.enter_details_vbox)
 
461
        else:
 
462
            self.login_back_button.hide()
 
463
            self.login_ok_button.grab_focus()
 
464
            self.login_vbox.help_text = self.help_text
 
465
            self._set_current_page(self.login_vbox)
 
466
 
 
467
    def _append_page(self, page):
 
468
        """Append 'page' to the 'window'."""
 
469
        self.content.append_page(page, None)
 
470
 
 
471
    def _set_header(self, header):
 
472
        """Set 'header' as the window title and header."""
 
473
        self.header_label.set_markup(LARGE_MARKUP % header)
 
474
        self.window.set_title(self.header_label.get_text())  # avoid markup
 
475
 
 
476
    def _set_current_page(self, current_page, warning_text=None):
 
477
        """Hide all the pages but 'current_page'."""
 
478
        if hasattr(current_page, 'header'):
 
479
            self._set_header(current_page.header)
 
480
 
 
481
        if hasattr(current_page, 'help_text'):
 
482
            self.help_label.set_markup(current_page.help_text)
 
483
 
 
484
        if warning_text is not None:
 
485
            self._set_warning_message(self.warning_label, warning_text)
 
486
        else:
 
487
            self.warning_label.set_text('')
 
488
 
 
489
        self.content.set_current_page(self.content.page_num(current_page))
 
490
 
 
491
        if current_page.default_widget is not None:
 
492
            current_page.default_widget.grab_default()
 
493
 
 
494
    def _generate_captcha(self):
 
495
        """Ask for a new captcha; update the ui to reflect the fact."""
 
496
        logger.info('Calling generate_captcha with filename path at %r',
 
497
                    self._captcha_filename)
 
498
        self.warning_label.set_text('')
 
499
        f = self.backend.generate_captcha
 
500
        error_handler = partial(self._handle_error, f,
 
501
                                self.on_captcha_generation_error)
 
502
        f(self.app_name, self._captcha_filename,
 
503
            reply_handler=NO_OP, error_handler=error_handler)
 
504
        self._set_captcha_loading()
 
505
 
 
506
    def _set_captcha_loading(self):
 
507
        """Present a spinner to the user while the captcha is downloaded."""
 
508
        self.captcha_image.hide()
 
509
        self._add_spinner_to_container(self.captcha_loading)
 
510
        self.captcha_loading.override_background_color(Gtk.StateFlags.NORMAL,
 
511
                                                       parse_color('white'))
 
512
        self.captcha_loading.show_all()
 
513
        self.join_ok_button.set_sensitive(False)
 
514
 
 
515
    def _set_captcha_image(self):
 
516
        """Present a captcha image to the user to be resolved."""
 
517
        self.captcha_loading.hide()
 
518
        self.join_ok_button.set_sensitive(True)
 
519
        self.captcha_image.set_from_file(self._captcha_filename)
 
520
        self.captcha_image.show()
 
521
 
 
522
    def _build_enter_details_page(self):
 
523
        """Build the enter details page."""
 
524
        d = {'app_name': self.app_label}
 
525
        self.enter_details_vbox.header = JOIN_HEADER_LABEL % d
 
526
        self.enter_details_vbox.help_text = self.help_text
 
527
        self.enter_details_vbox.default_widget = self.join_ok_button
 
528
        self.join_ok_button.set_can_default(True)
 
529
 
 
530
        self.enter_details_vbox.pack_start(self.name_entry,
 
531
            expand=False, fill=True, padding=0)
 
532
        self.enter_details_vbox.reorder_child(self.name_entry, 0)
 
533
        entry = self.captcha_solution_entry
 
534
        self.captcha_solution_vbox.pack_start(entry,
 
535
            expand=False, fill=True, padding=0)
 
536
        msg = CAPTCHA_RELOAD_TOOLTIP
 
537
        self.captcha_reload_button.set_tooltip_text(msg)
 
538
 
 
539
        self.emails_hbox.pack_start(self.email1_entry,
 
540
            expand=False, fill=True, padding=0)
 
541
        self.emails_hbox.pack_start(self.email2_entry,
 
542
            expand=False, fill=True, padding=0)
 
543
 
 
544
        self.passwords_hbox.pack_start(self.password1_entry,
 
545
            expand=False, fill=True, padding=0)
 
546
        self.passwords_hbox.pack_start(self.password2_entry,
 
547
            expand=False, fill=True, padding=0)
 
548
        help_msg = '<small>%s</small>' % PASSWORD_HELP
 
549
        self.password_help_label.set_markup(help_msg)
 
550
 
 
551
        if not os.path.exists(self._captcha_filename):
 
552
            self._generate_captcha()
 
553
        else:
 
554
            self._set_captcha_image()
 
555
 
 
556
        msg = YES_TO_UPDATES % {'app_name': self.app_name}
 
557
        self.yes_to_updates_checkbutton.set_label(msg)
 
558
 
 
559
        msg = YES_TO_TC % {'app_name': self.app_name}
 
560
        self.yes_to_tc_checkbutton.set_label(msg)
 
561
        self.tc_button.set_label(TC_BUTTON)
 
562
 
 
563
        if not self.tc_url:
 
564
            self.tc_vbox.hide()
 
565
        self.login_button.set_label(LOGIN_BUTTON_LABEL)
 
566
 
 
567
        return self.enter_details_vbox
 
568
 
 
569
    def _build_tc_page(self):
 
570
        """Build the Terms & Conditions page."""
 
571
        self.tc_browser_vbox.help_text = ''
 
572
        self.tc_browser_vbox.default_widget = self.tc_back_button
 
573
        self.tc_browser_vbox.default_widget.set_can_default(True)
 
574
        return self.tc_browser_vbox
 
575
 
 
576
    def _build_processing_page(self):
 
577
        """Build the processing page with a spinner."""
 
578
        self.processing_vbox.default_widget = None
 
579
        self._add_spinner_to_container(self.processing_vbox,
 
580
                                       legend=ONE_MOMENT_PLEASE)
 
581
        return self.processing_vbox
 
582
 
 
583
    def _build_verify_email_page(self):
 
584
        """Build the verify email page."""
 
585
        self.verify_email_vbox.default_widget = self.verify_token_button
 
586
        self.verify_email_vbox.default_widget.set_can_default(True)
 
587
 
 
588
        self.verify_email_details_vbox.pack_start(self.email_token_entry,
 
589
            expand=False, fill=True, padding=0)
 
590
        return self.verify_email_vbox
 
591
 
 
592
    def _build_finish_page(self):
 
593
        """Build the success page."""
 
594
        self.finish_vbox.default_widget = self.finish_close_button
 
595
        self.finish_vbox.default_widget.set_can_default(True)
 
596
        self.finish_vbox.label = self.finish_label
 
597
        return self.finish_vbox
 
598
 
 
599
    def _build_login_page(self):
 
600
        """Build the login page."""
 
601
        d = {'app_name': self.app_label}
 
602
        self.login_vbox.header = LOGIN_HEADER_LABEL % d
 
603
        self.login_vbox.help_text = CONNECT_HELP_LABEL % d
 
604
        self.login_vbox.default_widget = self.login_ok_button
 
605
        self.login_vbox.default_widget.set_can_default(True)
 
606
 
 
607
        self.login_details_vbox.pack_start(self.login_email_entry,
 
608
            expand=True, fill=True, padding=0)
 
609
        self.login_details_vbox.reorder_child(self.login_email_entry, 0)
 
610
        self.login_details_vbox.pack_start(self.login_password_entry,
 
611
            expand=True, fill=True, padding=0)
 
612
        self.login_details_vbox.reorder_child(self.login_password_entry, 1)
 
613
 
 
614
        msg = FORGOTTEN_PASSWORD_BUTTON
 
615
        self.forgotten_password_button.set_label(msg)
 
616
        self.login_ok_button.grab_focus()
 
617
 
 
618
        return self.login_vbox
 
619
 
 
620
    def _build_request_password_token_page(self):
 
621
        """Build the login page."""
 
622
        self.request_password_token_vbox.header = RESET_PASSWORD
 
623
        text = REQUEST_PASSWORD_TOKEN_LABEL % {'app_name': self.app_label}
 
624
        self.request_password_token_vbox.help_text = text
 
625
        btn = self.request_password_token_ok_button
 
626
        btn.set_can_default(True)
 
627
        self.request_password_token_vbox.default_widget = btn
 
628
 
 
629
        entry = self.reset_email_entry
 
630
        self.request_password_token_details_vbox.pack_start(entry,
 
631
            expand=False, fill=True, padding=0)
 
632
        cb = self.on_reset_email_entry_changed
 
633
        self.reset_email_entry.connect('changed', cb)
 
634
        self.request_password_token_ok_button.set_label(NEXT)
 
635
        self.request_password_token_ok_button.set_sensitive(False)
 
636
 
 
637
        return self.request_password_token_vbox
 
638
 
 
639
    def _build_set_new_password_page(self):
 
640
        """Build the login page."""
 
641
        self.set_new_password_vbox.header = RESET_PASSWORD
 
642
        self.set_new_password_vbox.help_text = SET_NEW_PASSWORD_LABEL
 
643
        btn = self.set_new_password_ok_button
 
644
        btn.set_can_default(True)
 
645
        self.set_new_password_vbox.default_widget = btn
 
646
 
 
647
        for entry in (self.reset_code_entry,
 
648
                      self.reset_password1_entry,
 
649
                      self.reset_password2_entry):
 
650
            self.set_new_password_details_vbox.pack_start(entry,
 
651
                expand=False, fill=True, padding=0)
 
652
 
 
653
        cb = self.on_set_new_password_entries_changed
 
654
        self.reset_code_entry.connect('changed', cb)
 
655
        self.reset_password1_entry.connect('changed', cb)
 
656
        self.reset_password2_entry.connect('changed', cb)
 
657
        help_msg = '<small>%s</small>' % PASSWORD_HELP
 
658
        self.reset_password_help_label.set_markup(help_msg)
 
659
 
 
660
        self.set_new_password_ok_button.set_label(RESET_PASSWORD)
 
661
        self.set_new_password_ok_button.set_sensitive(False)
 
662
 
 
663
        return self.set_new_password_vbox
 
664
 
 
665
    def _validate_email(self, email1, email2=None):
 
666
        """Validate 'email1', return error message if not valid.
 
667
 
 
668
        If 'email2' is given, must match 'email1'.
 
669
        """
 
670
        if email2 is not None and email1 != email2:
 
671
            return EMAIL_MISMATCH
 
672
 
 
673
        if not email1:
 
674
            return FIELD_REQUIRED
 
675
 
 
676
        if not is_correct_email(email1):
 
677
            return EMAIL_INVALID
 
678
 
 
679
    def _validate_password(self, password1, password2=None):
 
680
        """Validate 'password1', return error message if not valid.
 
681
 
 
682
        If 'password2' is given, must match 'email1'.
 
683
        """
 
684
        if password2 is not None and password1 != password2:
 
685
            return PASSWORD_MISMATCH
 
686
 
 
687
        if not is_min_required_password(password1):
 
688
            return PASSWORD_TOO_WEAK
 
689
 
 
690
    # GTK callbacks
 
691
 
 
692
    def destroy(self):
 
693
        """Destroy this UI."""
 
694
        self.window.hide()
 
695
        self.window.destroy()
 
696
 
 
697
    def connect(self, signal_name, handler, *args, **kwargs):
 
698
        """Connect 'signal_name' with 'handler'."""
 
699
        logger.debug('connect: signal %r, handler %r, args  %r, kwargs, %r',
 
700
                     signal_name, handler, args, kwargs)
 
701
        self.window.connect(signal_name, handler, *args, **kwargs)
 
702
 
 
703
    def finish_success(self):
 
704
        """The whole process was completed succesfully. Show success page."""
 
705
        self._done = True
 
706
        self._set_current_page(self.success_vbox)
 
707
 
 
708
    def finish_error(self):
 
709
        """The whole process was not completed succesfully. Show error page."""
 
710
        self._done = True
 
711
        self._set_current_page(self.error_vbox)
 
712
 
 
713
    def on_activate_link(self, button):
 
714
        """Do nothing, used for LinkButtons that are used as regular ones."""
 
715
        return True
 
716
 
 
717
    def on_close_clicked(self, *args, **kwargs):
 
718
        """Call self.close_callback if defined."""
 
719
        if os.path.exists(self._captcha_filename):
 
720
            os.remove(self._captcha_filename)
 
721
 
 
722
        for signal, match in self._signals_receivers.items():
 
723
            self.backend.disconnect_from_signal(signal, match)
 
724
 
 
725
        # hide the main window
 
726
        if self.window is not None:
 
727
            self.window.hide()
 
728
 
 
729
        # process any pending events before callbacking with result
 
730
        while Gtk.events_pending():
 
731
            Gtk.main_iteration()
 
732
 
 
733
        return_code = USER_SUCCESS
 
734
        if not self._done:
 
735
            return_code = USER_CANCELLATION
 
736
        logger.info('Return code will be %r.', return_code)
 
737
 
 
738
        # call user defined callback
 
739
        logger.debug('Calling custom close_callback %r with params %r, %r',
 
740
                    self.close_callback, args, kwargs)
 
741
        self.close_callback(*args, **kwargs)
 
742
 
 
743
        sys.exit(return_code)
 
744
 
 
745
    def on_sign_in_button_clicked(self, *args, **kwargs):
 
746
        """User wants to sign in, present the Login page."""
 
747
        self._set_current_page(self.login_vbox)
 
748
 
 
749
    def on_join_ok_button_clicked(self, *args, **kwargs):
 
750
        """Submit info for processing, present the processing vbox."""
 
751
        if not self.join_ok_button.is_sensitive():
 
752
            return
 
753
 
 
754
        self._clear_warnings()
 
755
 
 
756
        error = False
 
757
 
 
758
        name = self.name_entry.get_text()
 
759
        if not name:
 
760
            self.name_entry.set_warning(FIELD_REQUIRED)
 
761
            logger.warning('on_join_ok_button_clicked: name not set.')
 
762
            error = True
 
763
 
 
764
        # check email
 
765
        email1 = self.email1_entry.get_text()
 
766
        email2 = self.email2_entry.get_text()
 
767
        msg = self._validate_email(email1, email2)
 
768
        if msg is not None:
 
769
            self.email1_entry.set_warning(msg)
 
770
            self.email2_entry.set_warning(msg)
 
771
            logger.warning('on_join_ok_button_clicked: email is not valid.')
 
772
            error = True
 
773
 
 
774
        # check password
 
775
        password1 = self.password1_entry.get_text()
 
776
        password2 = self.password2_entry.get_text()
 
777
        msg = self._validate_password(password1, password2)
 
778
        if msg is not None:
 
779
            self.password1_entry.set_warning(msg)
 
780
            self.password2_entry.set_warning(msg)
 
781
            logger.warning('on_join_ok_button_clicked: password is not valid.')
 
782
            error = True
 
783
 
 
784
        # check T&C
 
785
        if self.tc_url and not self.yes_to_tc_checkbutton.get_active():
 
786
            self._set_warning_message(self.tc_warning_label,
 
787
                TC_NOT_ACCEPTED % {'app_name': self.app_name})
 
788
            logger.warning('on_join_ok_button_clicked: terms and conditions '
 
789
                           'not accepted.')
 
790
            error = True
 
791
 
 
792
        captcha_solution = self.captcha_solution_entry.get_text()
 
793
        if not captcha_solution:
 
794
            self.captcha_solution_entry.set_warning(FIELD_REQUIRED)
 
795
            logger.warning('on_join_ok_button_clicked: captcha solution not '
 
796
                           'set.')
 
797
            error = True
 
798
 
 
799
        if error:
 
800
            logger.warning('on_join_ok_button_clicked: validation failed.')
 
801
            return
 
802
 
 
803
        logger.info('on_join_ok_button_clicked: validation success!')
 
804
 
 
805
        self._set_current_page(self.processing_vbox)
 
806
        self.user_email = email1
 
807
        self.user_password = password1
 
808
 
 
809
        logger.info('Calling register_user with email %r, password <hidden>,'
 
810
                    ' name %r, captcha_id %r and captcha_solution %r.', email1,
 
811
                    name, self._captcha_id, captcha_solution)
 
812
 
 
813
        f = self.backend.register_user
 
814
        error_handler = partial(self._handle_error, f,
 
815
                                self.on_user_registration_error)
 
816
        f(self.app_name, self.user_email, self.user_password, name,
 
817
          self._captcha_id, captcha_solution,
 
818
          reply_handler=NO_OP, error_handler=error_handler)
 
819
 
 
820
    def on_verify_token_button_clicked(self, *args, **kwargs):
 
821
        """The user entered the email token, let's verify!"""
 
822
        if not self.verify_token_button.is_sensitive():
 
823
            return
 
824
 
 
825
        self._clear_warnings()
 
826
 
 
827
        email_token = self.email_token_entry.get_text()
 
828
        if not email_token:
 
829
            self.email_token_entry.set_warning(FIELD_REQUIRED)
 
830
            return
 
831
 
 
832
        email = self.user_email
 
833
        password = self.user_password
 
834
 
 
835
        args = (self.app_name, email, password, email_token)
 
836
        if self.ping_url:
 
837
            f = self.backend.validate_email_and_ping
 
838
            args = args + (self.ping_url,)
 
839
        else:
 
840
            f = self.backend.validate_email
 
841
 
 
842
        logger.info('Calling validate_email with email %r, password <hidden>, '
 
843
                    'app_name %r and email_token %r.', email, self.app_name,
 
844
                    email_token)
 
845
        error_handler = partial(self._handle_error, f,
 
846
                                self.on_email_validation_error)
 
847
        f(*args, reply_handler=NO_OP, error_handler=error_handler)
 
848
 
 
849
        self._set_current_page(self.processing_vbox)
 
850
 
 
851
    def on_login_connect_button_clicked(self, *args, **kwargs):
 
852
        """User wants to connect!"""
 
853
        if not self.login_ok_button.is_sensitive():
 
854
            return
 
855
 
 
856
        self._clear_warnings()
 
857
 
 
858
        error = False
 
859
 
 
860
        email = self.login_email_entry.get_text()
 
861
        msg = self._validate_email(email)
 
862
        if msg is not None:
 
863
            self.login_email_entry.set_warning(msg)
 
864
            error = True
 
865
 
 
866
        password = self.login_password_entry.get_text()
 
867
        if not password:
 
868
            self.login_password_entry.set_warning(FIELD_REQUIRED)
 
869
            error = True
 
870
 
 
871
        if error:
 
872
            return
 
873
 
 
874
        args = (self.app_name, email, password)
 
875
        if self.ping_url:
 
876
            f = self.backend.login_and_ping
 
877
            args = args + (self.ping_url,)
 
878
        else:
 
879
            f = self.backend.login
 
880
 
 
881
        error_handler = partial(self._handle_error, f, self.on_login_error)
 
882
        f(*args, reply_handler=NO_OP, error_handler=error_handler)
 
883
 
 
884
        self._set_current_page(self.processing_vbox)
 
885
        self.user_email = email
 
886
        self.user_password = password
 
887
 
 
888
    def on_login_back_button_clicked(self, *args, **kwargs):
 
889
        """User wants to go to the previous page."""
 
890
        self._set_current_page(self.enter_details_vbox)
 
891
 
 
892
    def on_forgotten_password_button_clicked(self, *args, **kwargs):
 
893
        """User wants to reset the password."""
 
894
        self._set_current_page(self.request_password_token_vbox)
 
895
 
 
896
    def on_request_password_token_ok_button_clicked(self, *args, **kwargs):
 
897
        """User entered the email address to reset the password."""
 
898
        if not self.request_password_token_ok_button.is_sensitive():
 
899
            return
 
900
 
 
901
        self._clear_warnings()
 
902
 
 
903
        email = self.reset_email_entry.get_text()
 
904
        msg = self._validate_email(email)
 
905
        if msg is not None:
 
906
            self.reset_email_entry.set_warning(msg)
 
907
            return
 
908
 
 
909
        logger.info('Calling request_password_reset_token with %r.', email)
 
910
        f = self.backend.request_password_reset_token
 
911
        error_handler = partial(self._handle_error, f,
 
912
                                self.on_password_reset_error)
 
913
        f(self.app_name, email,
 
914
          reply_handler=NO_OP, error_handler=error_handler)
 
915
 
 
916
        self._set_current_page(self.processing_vbox)
 
917
 
 
918
    def on_request_password_token_back_button_clicked(self, *args, **kwargs):
 
919
        """User wants to go to the previous page."""
 
920
        self._set_current_page(self.login_vbox)
 
921
 
 
922
    def on_reset_email_entry_changed(self, widget, *args, **kwargs):
 
923
        """User is changing the 'widget' entry in the reset email page."""
 
924
        sensitive = self._non_empty_input(widget)
 
925
        self.request_password_token_ok_button.set_sensitive(sensitive)
 
926
 
 
927
    def on_set_new_password_entries_changed(self, *args, **kwargs):
 
928
        """User is changing the 'widget' entry in the reset password page."""
 
929
        sensitive = True
 
930
        for entry in (self.reset_code_entry,
 
931
                      self.reset_password1_entry,
 
932
                      self.reset_password2_entry):
 
933
            sensitive &= self._non_empty_input(entry)
 
934
        self.set_new_password_ok_button.set_sensitive(sensitive)
 
935
 
 
936
    def on_set_new_password_ok_button_clicked(self, *args, **kwargs):
 
937
        """User entered reset code and new passwords."""
 
938
        if not self.set_new_password_ok_button.is_sensitive():
 
939
            return
 
940
 
 
941
        self._clear_warnings()
 
942
 
 
943
        error = False
 
944
 
 
945
        token = self.reset_code_entry.get_text()
 
946
        if not token:
 
947
            self.reset_code_entry.set_warning(FIELD_REQUIRED)
 
948
            error = True
 
949
 
 
950
        password1 = self.reset_password1_entry.get_text()
 
951
        password2 = self.reset_password2_entry.get_text()
 
952
        msg = self._validate_password(password1, password2)
 
953
        if msg is not None:
 
954
            self.reset_password1_entry.set_warning(msg)
 
955
            self.reset_password2_entry.set_warning(msg)
 
956
            error = True
 
957
 
 
958
        if error:
 
959
            return
 
960
 
 
961
        email = self.reset_email_entry.get_text()
 
962
        logger.info('Calling set_new_password with email %r, token %r and '
 
963
                    'new password: <hidden>.', email, token)
 
964
        f = self.backend.set_new_password
 
965
        error_handler = partial(self._handle_error, f,
 
966
                                self.on_password_change_error)
 
967
        f(self.app_name, email, token, password1,
 
968
          reply_handler=NO_OP, error_handler=error_handler)
 
969
 
 
970
        self._set_current_page(self.processing_vbox)
 
971
 
 
972
    def _webkit_init_ssl(self):
 
973
        """Set the WebKit ssl strictness."""
 
974
        # delay the import of webkit to be able to build without it
 
975
        from gi.repository import WebKit  # pylint: disable=E0611
 
976
 
 
977
        # Set the Soup session to be strict and use system CA certs
 
978
        session = WebKit.get_default_session()
 
979
        session.set_property(STRICT_SSL_PROP, True)
 
980
        session.set_property(CERTS_FILE_PROP, CA_CERT_FILE)
 
981
 
 
982
    def _add_webkit_browser(self):
 
983
        """Add the webkit browser for the t&c."""
 
984
        # delay the import of webkit to be able to build without it
 
985
        from gi.repository import WebKit  # pylint: disable=E0611
 
986
 
 
987
        self._webkit_init_ssl()
 
988
 
 
989
        browser = WebKit.WebView()
 
990
 
 
991
        browser.connect('notify::load-status',
 
992
                        self.on_tc_browser_notify_load_status)
 
993
        browser.connect('navigation-policy-decision-requested',
 
994
                        self.on_tc_browser_navigation_requested)
 
995
 
 
996
        settings = browser.get_settings()
 
997
        settings.set_property("enable-plugins", False)
 
998
        settings.set_property("enable-default-context-menu", False)
 
999
 
 
1000
        # webkit_web_view_open has been deprecated since version 1.1.1 and
 
1001
        # should not be used in newly-written code. Use
 
1002
        # webkit_web_view_load_uri() instead.
 
1003
        browser.load_uri(self.tc_url)
 
1004
        browser.show()
 
1005
        self.tc_browser_window.add(browser)
 
1006
 
 
1007
    def on_tc_button_clicked(self, *args, **kwargs):
 
1008
        """The T&C button was clicked, create the browser and load terms."""
 
1009
        if self.tc_browser_window.get_child() is None:
 
1010
            self._add_webkit_browser()
 
1011
            self._set_current_page(self.processing_vbox)
 
1012
        else:
 
1013
            self._set_current_page(self.tc_browser_vbox)
 
1014
 
 
1015
    def on_tc_back_button_clicked(self, *args, **kwargs):
 
1016
        """T & C 'back' button was clicked, return to the previous page."""
 
1017
        self._set_current_page(self.enter_details_vbox)
 
1018
 
 
1019
    def on_tc_browser_notify_load_status(self, browser, *args, **kwargs):
 
1020
        """The T&C page is being loaded."""
 
1021
        from gi.repository import WebKit  # pylint: disable=E0611
 
1022
 
 
1023
        if browser.get_load_status().real == WebKit.LoadStatus.FINISHED:
 
1024
            self._set_current_page(self.tc_browser_vbox)
 
1025
 
 
1026
    def on_tc_browser_navigation_requested(self, browser, frame, request,
 
1027
                                           action, decision, *args, **kwargs):
 
1028
        """The user wants to navigate within the T&C browser."""
 
1029
        from gi.repository import WebKit  # pylint: disable=E0611
 
1030
 
 
1031
        if action is not None and \
 
1032
           action.get_reason() == WebKit.WebNavigationReason.LINK_CLICKED:
 
1033
            if decision is not None:
 
1034
                decision.ignore()
 
1035
            url = action.get_original_uri()
 
1036
            webbrowser.open(url)
 
1037
        else:
 
1038
            if decision is not None:
 
1039
                decision.use()
 
1040
 
 
1041
    def on_tc_browser_vbox_hide(self, *args, **kwargs):
 
1042
        """The T&C page is no longer being shown."""
 
1043
        children = self.tc_browser_window.get_children()
 
1044
        if len(children) > 0:
 
1045
            browser = children[0]
 
1046
            self.tc_browser_window.remove(browser)
 
1047
            browser.destroy()
 
1048
            del browser
 
1049
 
 
1050
    def on_captcha_reload_button_clicked(self, *args, **kwargs):
 
1051
        """User clicked the reload captcha button."""
 
1052
        self._generate_captcha()
 
1053
 
 
1054
    # backend callbacks
 
1055
 
 
1056
    def _build_general_error_message(self, errordict):
 
1057
        """Concatenate __all__ and message from the errordict."""
 
1058
        result = None
 
1059
        msg1 = errordict.get('__all__')
 
1060
        msg2 = errordict.get('message')
 
1061
        if msg1 is not None and msg2 is not None:
 
1062
            result = '\n'.join((msg1, msg2))
 
1063
        else:
 
1064
            result = msg1 if msg1 is not None else msg2
 
1065
        return result
 
1066
 
 
1067
    @log_call
 
1068
    def on_captcha_generated(self, app_name, captcha_id, *args, **kwargs):
 
1069
        """Captcha image has been generated and is available to be shown."""
 
1070
        if captcha_id is None:
 
1071
            logger.warning('on_captcha_generated: captcha_id is None for '
 
1072
                           'app_name %r.', app_name)
 
1073
        self._captcha_id = captcha_id
 
1074
        self._set_captcha_image()
 
1075
 
 
1076
    @log_call
 
1077
    def on_captcha_generation_error(self, app_name, error, *args, **kwargs):
 
1078
        """Captcha image generation failed."""
 
1079
        self._set_warning_message(self.warning_label, CAPTCHA_LOAD_ERROR)
 
1080
        self._generate_captcha()
 
1081
 
 
1082
    @log_call
 
1083
    def on_user_registered(self, app_name, email, *args, **kwargs):
 
1084
        """Registration can go on, user needs to verify email."""
 
1085
        help_text = VERIFY_EMAIL_LABEL % {'app_name': self.app_name,
 
1086
                                          'email': email}
 
1087
        self.verify_email_vbox.help_text = help_text
 
1088
        self._set_current_page(self.verify_email_vbox)
 
1089
 
 
1090
    @log_call
 
1091
    def on_user_registration_error(self, app_name, error, *args, **kwargs):
 
1092
        """Error in the data provided for registration."""
 
1093
        msg = error.get('email')
 
1094
        if msg is not None:
 
1095
            self.email1_entry.set_warning(msg)
 
1096
            self.email2_entry.set_warning(msg)
 
1097
 
 
1098
        msg = error.get('password')
 
1099
        if msg is not None:
 
1100
            self.password1_entry.set_warning(msg)
 
1101
            self.password2_entry.set_warning(msg)
 
1102
 
 
1103
        msg = self._build_general_error_message(error)
 
1104
        self._generate_captcha()
 
1105
        self._set_current_page(self.enter_details_vbox, warning_text=msg)
 
1106
 
 
1107
    @log_call
 
1108
    def on_email_validated(self, app_name, email, *args, **kwargs):
 
1109
        """User email was successfully verified."""
 
1110
        self.finish_success()
 
1111
 
 
1112
    @log_call
 
1113
    def on_email_validation_error(self, app_name, error, *args, **kwargs):
 
1114
        """User email validation failed."""
 
1115
        msg = error.get('email_token')
 
1116
        if msg is not None:
 
1117
            self.email_token_entry.set_warning(msg)
 
1118
 
 
1119
        msg = self._build_general_error_message(error)
 
1120
        self._set_current_page(self.verify_email_vbox, warning_text=msg)
 
1121
 
 
1122
    @log_call
 
1123
    def on_logged_in(self, app_name, email, *args, **kwargs):
 
1124
        """User was successfully logged in."""
 
1125
        self.finish_success()
 
1126
 
 
1127
    @log_call
 
1128
    def on_login_error(self, app_name, error, *args, **kwargs):
 
1129
        """User was not successfully logged in."""
 
1130
        msg = self._build_general_error_message(error)
 
1131
        self._set_current_page(self.login_vbox, warning_text=msg)
 
1132
 
 
1133
    @log_call
 
1134
    def on_user_not_validated(self, app_name, email, *args, **kwargs):
 
1135
        """User was not validated."""
 
1136
        self.on_user_registered(app_name, email)
 
1137
 
 
1138
    @log_call
 
1139
    def on_password_reset_token_sent(self, app_name, email, *args, **kwargs):
 
1140
        """Password reset token was successfully sent."""
 
1141
        msg = SET_NEW_PASSWORD_LABEL % {'email': email}
 
1142
        self.set_new_password_vbox.help_text = msg
 
1143
        self._set_current_page(self.set_new_password_vbox)
 
1144
 
 
1145
    @log_call
 
1146
    def on_password_reset_error(self, app_name, error, *args, **kwargs):
 
1147
        """Password reset failed."""
 
1148
        msg = self._build_general_error_message(error)
 
1149
        self._set_current_page(self.login_vbox, warning_text=msg)
 
1150
 
 
1151
    @log_call
 
1152
    def on_password_changed(self, app_name, email, *args, **kwargs):
 
1153
        """Password was successfully changed."""
 
1154
        self._set_current_page(self.login_vbox,
 
1155
                               warning_text=PASSWORD_CHANGED)
 
1156
 
 
1157
    @log_call
 
1158
    def on_password_change_error(self, app_name, error, *args, **kwargs):
 
1159
        """Password reset failed."""
 
1160
        msg = self._build_general_error_message(error)
 
1161
        self._set_current_page(self.request_password_token_vbox,
 
1162
                               warning_text=msg)
 
1163
 
 
1164
 
 
1165
def run(**kwargs):
 
1166
    """Start the GTK mainloop and open the main window."""
 
1167
    UbuntuSSOClientGUI(close_callback=Gtk.main_quit, **kwargs)
 
1168
    Gtk.main()