1
# -*- coding: utf-8 -*-
3
# Copyright 2010-2012 Canonical Ltd.
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.
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.
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/>.
18
"""The Ubuntu Single Sign On GTK+ graphical user interface."""
26
from functools import wraps, partial
30
# pylint: disable=E0611,F0401
31
from gi.repository import Gdk, Gtk
32
from gi.repository.GdkX11 import X11Window
33
# pylint: enable=E0611,F0401
35
from ubuntu_sso import (
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 (
47
CAPTCHA_RELOAD_TOOLTIP,
53
FORGOTTEN_PASSWORD_BUTTON,
54
GENERIC_BACKEND_ERROR,
55
is_min_required_password,
67
REQUEST_PASSWORD_TOKEN_LABEL,
69
SET_NEW_PASSWORD_LABEL,
78
# Instance of 'UbuntuSSOClientGUI' has no 'yyy' member
79
# pylint: disable=E1101
82
logger = setup_gui_logging('ubuntu_sso.gui.gtk')
85
# pylint: disable=C0103
86
def parse_color(color):
87
"""Parse a string color into Gdk.Color."""
89
result = c.parse(color)
91
logger.warning('Could not parse color %r.', color)
93
# pylint: enable=C0103
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>'
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'
109
"""Decorator to log call funtions."""
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)
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()
130
def get_data_file(*args):
131
result = os.path.abspath(os.path.join(os.path.dirname(__file__),
133
if not os.path.exists(result):
134
import softwarecenter.paths
135
result = softwarecenter.paths.datadir
137
result = os.path.join(result, 'ui', 'sso', *args)
138
logger.info('Using data dir: %r', result)
142
class LabeledEntry(Gtk.Entry):
143
"""An entry that displays the label within itself ina grey color."""
145
# Use of super on an old style class
146
# pylint: disable=E1002
148
def __init__(self, label, is_password=False, *args, **kwargs):
150
self.is_password = is_password
153
super(LabeledEntry, self).__init__(*args, **kwargs)
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)
163
def _clear_text(self, *args, **kwargs):
164
"""Clear text and restore text color."""
165
self.set_text(self.get_text())
167
# restore to theme's default
168
self.override_color(Gtk.StateFlags.NORMAL, None)
171
self.set_visibility(False)
173
return False # propagate the event further
175
def _set_label(self, *args, **kwargs):
176
"""Set the proper label and proper coloring."""
180
self.set_text(self.label)
181
self.override_color(Gtk.StateFlags.NORMAL, HELP_TEXT_COLOR)
184
self.set_visibility(True)
186
return False # propagate the event further
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():
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)
203
def clear_warning(self):
204
"""Remove any warning."""
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)
212
class UbuntuSSOClientGUI(object):
213
"""Ubuntu single sign-on GUI."""
215
def __init__(self, app_name, **kwargs):
216
"""Create the GUI and initialize widgets."""
217
logger.debug('UbuntuSSOClientGUI: app_name %r, kwargs %r.',
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
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)
234
self.user_email = None
235
self.user_password = None
237
ui_filename = get_data_file('sso.ui')
238
builder = Gtk.Builder()
239
builder.add_from_file(ui_filename)
240
builder.connect_signals(self)
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)
251
logging.warn("%s has no name (??)", obj)
253
self.widgets.append(name)
254
setattr(self, name, obj)
255
if 'warning' in name:
256
self.warnings.append(obj)
258
if 'cancel_button' in name:
259
obj.connect('clicked', self.on_close_clicked)
260
self.cancels.append(obj)
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)
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')
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)
282
self.window.set_icon_name('ubuntu-logo')
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)
292
self._filter_by_app_name(self.on_captcha_generated),
293
'CaptchaGenerationError':
294
self._filter_by_app_name(self.on_captcha_generation_error),
296
self._filter_by_app_name(self.on_user_registered),
297
'UserRegistrationError':
298
self._filter_by_app_name(self.on_user_registration_error),
300
self._filter_by_app_name(self.on_email_validated),
301
'EmailValidationError':
302
self._filter_by_app_name(self.on_email_validation_error),
304
self._filter_by_app_name(self.on_logged_in),
306
self._filter_by_app_name(self.on_login_error),
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),
314
self._filter_by_app_name(self.on_password_changed),
315
'PasswordChangeError':
316
self._filter_by_app_name(self.on_password_change_error),
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.
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)
336
self.yes_to_updates_checkbutton.hide()
339
def start_backend(self):
340
"""Start the backend, show the window when ready."""
341
self.backend = get_sso_client()
343
logger.debug('UbuntuSSOClientGUI: backend created: %r', self.backend)
345
self._setup_signals()
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
358
def error_vbox(self):
359
"""The error page."""
360
self.finish_vbox.label.set_markup(LARGE_MARKUP % ERROR)
361
return self.finish_vbox
365
def _filter_by_app_name(self, f):
366
"""Excecute the decorated function only for 'self.app_name'."""
369
def inner(app_name, *args, **kwargs):
370
"""Execute 'f' only if 'app_name' matches 'self.app_name'."""
372
if app_name == self.app_name:
373
result = f(app_name, *args, **kwargs)
375
logger.info('%s: ignoring call since received app_name '
377
f.__name__, app_name, self.app_name)
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)
390
match = self.backend.connect_to_signal(signal, method)
391
self._signals_receivers[signal] = match
393
def _add_spinner_to_container(self, container, legend=None):
394
"""Add a spinner to 'container'."""
395
spinner = Gtk.Spinner()
400
label.set_text(legend)
402
label.set_text(LOADING)
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)
408
alignment = Gtk.Alignment(xalign=0.5, yalign=0.5,
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)
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)
427
def _clear_warnings(self):
428
"""Clear all warning messages."""
429
for widget in self.warnings:
431
for widget in self.entries:
432
getattr(self, widget).clear_warning()
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())
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)
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())
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)
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)
467
def _append_page(self, page):
468
"""Append 'page' to the 'window'."""
469
self.content.append_page(page, None)
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
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)
481
if hasattr(current_page, 'help_text'):
482
self.help_label.set_markup(current_page.help_text)
484
if warning_text is not None:
485
self._set_warning_message(self.warning_label, warning_text)
487
self.warning_label.set_text('')
489
self.content.set_current_page(self.content.page_num(current_page))
491
if current_page.default_widget is not None:
492
current_page.default_widget.grab_default()
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()
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)
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()
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)
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)
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)
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)
551
if not os.path.exists(self._captcha_filename):
552
self._generate_captcha()
554
self._set_captcha_image()
556
msg = YES_TO_UPDATES % {'app_name': self.app_name}
557
self.yes_to_updates_checkbutton.set_label(msg)
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)
565
self.login_button.set_label(LOGIN_BUTTON_LABEL)
567
return self.enter_details_vbox
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
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
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)
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
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
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)
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)
614
msg = FORGOTTEN_PASSWORD_BUTTON
615
self.forgotten_password_button.set_label(msg)
616
self.login_ok_button.grab_focus()
618
return self.login_vbox
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
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)
637
return self.request_password_token_vbox
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
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)
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)
660
self.set_new_password_ok_button.set_label(RESET_PASSWORD)
661
self.set_new_password_ok_button.set_sensitive(False)
663
return self.set_new_password_vbox
665
def _validate_email(self, email1, email2=None):
666
"""Validate 'email1', return error message if not valid.
668
If 'email2' is given, must match 'email1'.
670
if email2 is not None and email1 != email2:
671
return EMAIL_MISMATCH
674
return FIELD_REQUIRED
676
if not is_correct_email(email1):
679
def _validate_password(self, password1, password2=None):
680
"""Validate 'password1', return error message if not valid.
682
If 'password2' is given, must match 'email1'.
684
if password2 is not None and password1 != password2:
685
return PASSWORD_MISMATCH
687
if not is_min_required_password(password1):
688
return PASSWORD_TOO_WEAK
693
"""Destroy this UI."""
695
self.window.destroy()
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)
703
def finish_success(self):
704
"""The whole process was completed succesfully. Show success page."""
706
self._set_current_page(self.success_vbox)
708
def finish_error(self):
709
"""The whole process was not completed succesfully. Show error page."""
711
self._set_current_page(self.error_vbox)
713
def on_activate_link(self, button):
714
"""Do nothing, used for LinkButtons that are used as regular ones."""
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)
722
for signal, match in self._signals_receivers.items():
723
self.backend.disconnect_from_signal(signal, match)
725
# hide the main window
726
if self.window is not None:
729
# process any pending events before callbacking with result
730
while Gtk.events_pending():
733
return_code = USER_SUCCESS
735
return_code = USER_CANCELLATION
736
logger.info('Return code will be %r.', return_code)
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)
743
sys.exit(return_code)
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)
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():
754
self._clear_warnings()
758
name = self.name_entry.get_text()
760
self.name_entry.set_warning(FIELD_REQUIRED)
761
logger.warning('on_join_ok_button_clicked: name not set.')
765
email1 = self.email1_entry.get_text()
766
email2 = self.email2_entry.get_text()
767
msg = self._validate_email(email1, email2)
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.')
775
password1 = self.password1_entry.get_text()
776
password2 = self.password2_entry.get_text()
777
msg = self._validate_password(password1, password2)
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.')
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 '
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 '
800
logger.warning('on_join_ok_button_clicked: validation failed.')
803
logger.info('on_join_ok_button_clicked: validation success!')
805
self._set_current_page(self.processing_vbox)
806
self.user_email = email1
807
self.user_password = password1
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)
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)
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():
825
self._clear_warnings()
827
email_token = self.email_token_entry.get_text()
829
self.email_token_entry.set_warning(FIELD_REQUIRED)
832
email = self.user_email
833
password = self.user_password
835
args = (self.app_name, email, password, email_token)
837
f = self.backend.validate_email_and_ping
838
args = args + (self.ping_url,)
840
f = self.backend.validate_email
842
logger.info('Calling validate_email with email %r, password <hidden>, '
843
'app_name %r and email_token %r.', email, self.app_name,
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)
849
self._set_current_page(self.processing_vbox)
851
def on_login_connect_button_clicked(self, *args, **kwargs):
852
"""User wants to connect!"""
853
if not self.login_ok_button.is_sensitive():
856
self._clear_warnings()
860
email = self.login_email_entry.get_text()
861
msg = self._validate_email(email)
863
self.login_email_entry.set_warning(msg)
866
password = self.login_password_entry.get_text()
868
self.login_password_entry.set_warning(FIELD_REQUIRED)
874
args = (self.app_name, email, password)
876
f = self.backend.login_and_ping
877
args = args + (self.ping_url,)
879
f = self.backend.login
881
error_handler = partial(self._handle_error, f, self.on_login_error)
882
f(*args, reply_handler=NO_OP, error_handler=error_handler)
884
self._set_current_page(self.processing_vbox)
885
self.user_email = email
886
self.user_password = password
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)
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)
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():
901
self._clear_warnings()
903
email = self.reset_email_entry.get_text()
904
msg = self._validate_email(email)
906
self.reset_email_entry.set_warning(msg)
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)
916
self._set_current_page(self.processing_vbox)
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)
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)
927
def on_set_new_password_entries_changed(self, *args, **kwargs):
928
"""User is changing the 'widget' entry in the reset password page."""
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)
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():
941
self._clear_warnings()
945
token = self.reset_code_entry.get_text()
947
self.reset_code_entry.set_warning(FIELD_REQUIRED)
950
password1 = self.reset_password1_entry.get_text()
951
password2 = self.reset_password2_entry.get_text()
952
msg = self._validate_password(password1, password2)
954
self.reset_password1_entry.set_warning(msg)
955
self.reset_password2_entry.set_warning(msg)
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)
970
self._set_current_page(self.processing_vbox)
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
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)
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
987
self._webkit_init_ssl()
989
browser = WebKit.WebView()
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)
996
settings = browser.get_settings()
997
settings.set_property("enable-plugins", False)
998
settings.set_property("enable-default-context-menu", False)
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)
1005
self.tc_browser_window.add(browser)
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)
1013
self._set_current_page(self.tc_browser_vbox)
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)
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
1023
if browser.get_load_status().real == WebKit.LoadStatus.FINISHED:
1024
self._set_current_page(self.tc_browser_vbox)
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
1031
if action is not None and \
1032
action.get_reason() == WebKit.WebNavigationReason.LINK_CLICKED:
1033
if decision is not None:
1035
url = action.get_original_uri()
1036
webbrowser.open(url)
1038
if decision is not None:
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)
1050
def on_captcha_reload_button_clicked(self, *args, **kwargs):
1051
"""User clicked the reload captcha button."""
1052
self._generate_captcha()
1056
def _build_general_error_message(self, errordict):
1057
"""Concatenate __all__ and message from the errordict."""
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))
1064
result = msg1 if msg1 is not None else msg2
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()
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()
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,
1087
self.verify_email_vbox.help_text = help_text
1088
self._set_current_page(self.verify_email_vbox)
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')
1095
self.email1_entry.set_warning(msg)
1096
self.email2_entry.set_warning(msg)
1098
msg = error.get('password')
1100
self.password1_entry.set_warning(msg)
1101
self.password2_entry.set_warning(msg)
1103
msg = self._build_general_error_message(error)
1104
self._generate_captcha()
1105
self._set_current_page(self.enter_details_vbox, warning_text=msg)
1108
def on_email_validated(self, app_name, email, *args, **kwargs):
1109
"""User email was successfully verified."""
1110
self.finish_success()
1113
def on_email_validation_error(self, app_name, error, *args, **kwargs):
1114
"""User email validation failed."""
1115
msg = error.get('email_token')
1117
self.email_token_entry.set_warning(msg)
1119
msg = self._build_general_error_message(error)
1120
self._set_current_page(self.verify_email_vbox, warning_text=msg)
1123
def on_logged_in(self, app_name, email, *args, **kwargs):
1124
"""User was successfully logged in."""
1125
self.finish_success()
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)
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)
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)
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)
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)
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,
1166
"""Start the GTK mainloop and open the main window."""
1167
UbuntuSSOClientGUI(close_callback=Gtk.main_quit, **kwargs)