1
# -*- coding: utf-8 -*-
3
# ubuntu_sso.gui - GUI for login and registration
5
# Author: Natalia Bidart <natalia.bidart@canonical.com>
7
# Copyright 2010 Canonical Ltd.
9
# This program is free software: you can redistribute it and/or modify it
10
# under the terms of the GNU General Public License version 3, as published
11
# by the Free Software Foundation.
13
# This program is distributed in the hope that it will be useful, but
14
# WITHOUT ANY WARRANTY; without even the implied warranties of
15
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
16
# PURPOSE. See the GNU General Public License for more details.
18
# You should have received a copy of the GNU General Public License along
19
# with this program. If not, see <http://www.gnu.org/licenses/>.
21
"""User registration GUI."""
29
from functools import wraps
37
from dbus.mainloop.glib import DBusGMainLoop
39
from ubuntu_sso import DBUS_ACCOUNT_PATH, DBUS_BUS_NAME, DBUS_IFACE_USER_NAME
40
from ubuntu_sso.logger import setup_logging
43
# Instance of 'UbuntuSSOClientGUI' has no 'yyy' member
44
# pylint: disable=E1101
48
gettext.textdomain('ubuntu-sso-client')
50
DBusGMainLoop(set_as_default=True)
51
logger = setup_logging('ubuntu_sso.gui')
53
# To be removed when Python bindings provide these constants
54
# as per http://code.google.com/p/pywebkitgtk/issues/detail?id=44
56
WEBKIT_LOAD_PROVISIONAL = 0
57
WEBKIT_LOAD_COMMITTED = 1
58
WEBKIT_LOAD_FINISHED = 2
59
WEBKIT_LOAD_FIRST_VISUALLY_NON_EMPTY_LAYOUT = 3
60
WEBKIT_LOAD_FAILED = 4
61
# WebKitWebNavigationReason
62
WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED = 0
63
WEBKIT_WEB_NAVIGATION_REASON_FORM_SUBMITTED = 1
64
WEBKIT_WEB_NAVIGATION_REASON_BACK_FORWARD = 2
65
WEBKIT_WEB_NAVIGATION_REASON_RELOAD = 3
66
WEBKIT_WEB_NAVIGATION_REASON_FORM_RESUBMITTED = 4
67
WEBKIT_WEB_NAVIGATION_REASON_OTHER = 5
70
NO_OP = lambda *args, **kwargs: None
72
# To be replaced by values from the theme (LP: #616526)
73
HELP_TEXT_COLOR = gtk.gdk.Color("#bfbfbf")
74
WARNING_TEXT_COLOR = gtk.gdk.Color("red")
76
SIG_LOGIN_FAILED = 'login-failed'
77
SIG_LOGIN_SUCCEEDED = 'login-succeeded'
78
SIG_REGISTRATION_FAILED = 'registration-failed'
79
SIG_REGISTRATION_SUCCEEDED = 'registration-succeeded'
80
SIG_USER_CANCELATION = 'user-cancelation'
83
(SIG_LOGIN_FAILED, (gobject.TYPE_STRING, gobject.TYPE_STRING)),
84
(SIG_LOGIN_SUCCEEDED, (gobject.TYPE_STRING, gobject.TYPE_STRING,)),
85
(SIG_REGISTRATION_FAILED, (gobject.TYPE_STRING, gobject.TYPE_STRING)),
86
(SIG_REGISTRATION_SUCCEEDED, (gobject.TYPE_STRING, gobject.TYPE_STRING,)),
87
(SIG_USER_CANCELATION, (gobject.TYPE_STRING,)),
90
for sig, sig_args in SIGNAL_ARGUMENTS:
91
gobject.signal_new(sig, gtk.Window, gobject.SIGNAL_RUN_FIRST,
92
gobject.TYPE_NONE, sig_args)
96
"""Build absolute path to the 'data' directory."""
97
module = os.path.dirname(__file__)
98
result = os.path.abspath(os.path.join(module, os.pardir, 'data'))
99
logger.debug('get_data_file: trying to load from %r (exists? %s)',
100
result, os.path.exists(result))
101
if os.path.exists(result):
102
logger.info('get_data_file: returning data dir located at %r.', result)
105
# no local data dir, looking within system data dirs
106
data_dirs = xdg.BaseDirectory.xdg_data_dirs
107
for path in data_dirs:
108
result = os.path.join(path, 'ubuntu-sso-client', 'data')
109
result = os.path.abspath(result)
110
logger.debug('get_data_file: trying to load from %r (exists? %s)',
111
result, os.path.exists(result))
112
if os.path.exists(result):
113
logger.info('get_data_file: data dir located at %r.', result)
116
msg = 'get_data_file: can not build a valid data path. Giving up.' \
117
'__file__ is %r, data_dirs are %r'
118
logger.error(msg, __file__, data_dirs)
121
def get_data_file(filename):
122
"""Build absolute path to 'filename' within the 'data' directory."""
123
return os.path.join(get_data_dir(), filename)
127
"""Decorator to log call funtions."""
130
def inner(*args, **kwargs):
131
"""Execute 'f' logging the call as INFO."""
132
logger.info('%s: args %r, kwargs %r.', f.__name__, args, kwargs)
133
return f(*args, **kwargs)
138
class LabeledEntry(gtk.Entry):
139
"""An entry that displays the label within itself ina grey color."""
141
def __init__(self, label, is_password=False, *args, **kwargs):
143
self.is_password = is_password
146
super(LabeledEntry, self).__init__(*args, **kwargs)
148
self.set_width_chars(DEFAULT_WIDTH)
149
self._set_label(self, None)
150
self.set_tooltip_text(self.label)
151
self.connect('focus-in-event', self._clear_text)
152
self.connect('focus-out-event', self._set_label)
156
def _clear_text(self, *args, **kwargs):
157
"""Clear text and restore text color."""
158
self.set_text(self.get_text())
160
self.modify_text(gtk.STATE_NORMAL, None) # restore to theme's default
163
self.set_visibility(False)
165
return False # propagate the event further
167
def _set_label(self, *args, **kwargs):
168
"""Set the proper label and proper coloring."""
172
self.set_text(self.label)
173
self.modify_text(gtk.STATE_NORMAL, HELP_TEXT_COLOR)
176
self.set_visibility(True)
178
return False # propagate the event further
181
"""Get text only if it's not the label nor empty."""
182
result = super(LabeledEntry, self).get_text()
183
if result == self.label or result.isspace():
187
def set_warning(self, warning_msg):
188
"""Display warning as secondary icon, set 'warning_msg' as tooltip."""
189
self.warning = warning_msg
190
self.set_property('secondary-icon-stock', gtk.STOCK_DIALOG_WARNING)
191
self.set_property('secondary-icon-sensitive', True)
192
self.set_property('secondary-icon-activatable', False)
193
self.set_property('secondary-icon-tooltip-text', warning_msg)
195
def clear_warning(self):
196
"""Remove any warning."""
198
self.set_property('secondary-icon-stock', None)
199
self.set_property('secondary-icon-sensitive', False)
200
self.set_property('secondary-icon-activatable', False)
201
self.set_property('secondary-icon-tooltip-text', None)
204
class UbuntuSSOClientGUI(object):
205
"""Ubuntu single sign on GUI."""
207
CAPTCHA_SOLUTION_ENTRY = _('Type the characters above')
208
CAPTCHA_LOAD_ERROR = _('There was a problem getting the captcha, '
210
CONNECT_HELP_LABEL = _('To connect this computer to %(app_name)s ' \
211
'enter your details below.')
212
EMAIL1_ENTRY = _('Email address')
213
EMAIL2_ENTRY = _('Re-type Email address')
214
EMAIL_MISMATCH = _('The email addresses don\'t match, please double check '
215
'and try entering them again.')
216
EMAIL_INVALID = _('The email must be a valid email address.')
217
EMAIL_TOKEN_ENTRY = _('Enter code verification here')
218
ERROR = _('The process did not finish successfully.')
219
FIELD_REQUIRED = _('This field is required.')
220
FORGOTTEN_PASSWORD_BUTTON = _('I\'ve forgotten my password')
221
JOIN_HEADER_LABEL = _('Create %(app_name)s account')
222
LOADING = _('Loading...')
223
LOGIN_BUTTON_LABEL = _('Already have an account? Click here to sign in')
224
LOGIN_EMAIL_ENTRY = _('Email address')
225
LOGIN_HEADER_LABEL = _('Connect to %(app_name)s')
226
LOGIN_PASSWORD_ENTRY = _('Password')
227
NAME_ENTRY = _('Name')
229
ONE_MOMENT_PLEASE = _('One moment please...')
230
PASSWORD_CHANGED = _('Your password was successfully changed.')
231
PASSWORD1_ENTRY = RESET_PASSWORD1_ENTRY = _('Password')
232
PASSWORD2_ENTRY = RESET_PASSWORD2_ENTRY = _('Re-type Password')
233
PASSWORD_HELP = _('The password must have a minimum of 8 characters and ' \
234
'include one uppercase character and one number.')
235
PASSWORD_MISMATCH = _('The passwords don\'t match, please double check ' \
236
'and try entering them again.')
237
PASSWORD_TOO_WEAK = _('The password is too weak.')
238
REQUEST_PASSWORD_TOKEN_LABEL = _('To reset your %(app_name)s password,'
239
' enter your email address below:')
240
RESET_PASSWORD = _('Reset password')
241
RESET_CODE_ENTRY = _('Reset code')
242
RESET_EMAIL_ENTRY = _('Email address')
243
SET_NEW_PASSWORD_LABEL = _('A password reset code has been sent to ' \
244
'%(email)s.\nPlease enter the code below ' \
245
'along with your new password.')
246
SUCCESS = _('The process finished successfully. Congratulations!')
247
TC_BUTTON = _('Show Terms & Conditions')
248
TC_NOT_ACCEPTED = _('Agreeing to the Ubuntu One Terms & Conditions is ' \
249
'required to subscribe.')
250
UNKNOWN_ERROR = _('There was an error when trying to complete the ' \
251
'process. Please check the information and try again.')
252
VERIFY_EMAIL_LABEL = ('<b>%s</b>\n\n' % _('Enter verification code') +
253
_('Check %(email)s for an email from'
254
' Ubuntu Single Sign On.'
255
' This message contains a verification code.'
256
' Enter the code in the field below and click OK'
257
' to complete creating your %(app_name)s account'))
258
YES_TO_TC = _('I agree with the %(app_name)s terms and conditions')
259
YES_TO_UPDATES = _('Yes! Email me %(app_name)s tips and updates.')
260
CAPTCHA_RELOAD_TOOLTIP = _('Reload')
262
def __init__(self, app_name, tc_url='', help_text='',
263
window_id=0, login_only=False, close_callback=None):
264
"""Create the GUI and initialize widgets."""
265
gtk.link_button_set_uri_hook(NO_OP)
267
self._captcha_filename = tempfile.mktemp()
268
self._captcha_id = None
269
self._signals_receivers = {}
270
self._done = False # whether the whole process was completed or not
272
self.app_name = app_name
273
self.app_label = '<b>%s</b>' % self.app_name
275
self.help_text = help_text
276
self.close_callback = close_callback
277
self.user_email = None
278
self.user_password = None
280
ui_filename = get_data_file('ui.glade')
281
builder = gtk.Builder()
282
builder.add_from_file(ui_filename)
283
builder.connect_signals(self)
289
for obj in builder.get_objects():
290
name = getattr(obj, 'name', None)
291
if name is None and isinstance(obj, gtk.Buildable):
292
# work around bug lp:507739
293
name = gtk.Buildable.get_name(obj)
295
logging.warn("%s has no name (??)", obj)
297
self.widgets.append(name)
298
setattr(self, name, obj)
299
if 'warning' in name:
300
self.warnings.append(obj)
302
if 'cancel_button' in name:
303
obj.connect('clicked', self.on_close_clicked)
304
self.cancels.append(obj)
306
self.labels.append(obj)
308
self.entries = (u'name_entry', u'email1_entry', u'email2_entry',
309
u'password1_entry', u'password2_entry',
310
u'captcha_solution_entry', u'email_token_entry',
311
u'login_email_entry', u'login_password_entry',
312
u'reset_email_entry', u'reset_code_entry',
313
u'reset_password1_entry', u'reset_password2_entry')
315
for name in self.entries:
316
label = getattr(self, name.upper())
317
is_password = 'password' in name
318
entry = LabeledEntry(label=label, is_password=is_password)
319
entry.set_activates_default(True)
320
setattr(self, name, entry)
322
self.window.set_icon_name('ubuntu-logo')
324
self.bus = dbus.SessionBus()
325
obj = self.bus.get_object(bus_name=DBUS_BUS_NAME,
326
object_path=DBUS_ACCOUNT_PATH,
327
follow_name_owner_changes=True)
328
self.iface_name = DBUS_IFACE_USER_NAME
329
self.backend = dbus.Interface(object=obj,
330
dbus_interface=self.iface_name)
331
logger.debug('UbuntuSSOClientGUI: backend created: %r', self.backend)
333
self.pages = (self.enter_details_vbox, self.processing_vbox,
334
self.verify_email_vbox, self.finish_vbox,
335
self.tc_browser_vbox, self.login_vbox,
336
self.request_password_token_vbox,
337
self.set_new_password_vbox)
339
self._append_page(self._build_processing_page())
340
self._append_page(self._build_finish_page())
341
self._append_page(self._build_login_page())
342
self._append_page(self._build_request_password_token_page())
343
self._append_page(self._build_set_new_password_page())
344
self._append_page(self._build_verify_email_page())
348
window_size = (550, 500)
349
self._append_page(self._build_enter_details_page())
350
self._append_page(self._build_tc_page())
351
self.login_button.grab_focus()
352
self._set_current_page(self.enter_details_vbox)
354
window_size = (400, 350)
355
self.login_back_button.hide()
356
self.login_ok_button.grab_focus()
357
self.login_vbox.help_text = help_text
358
self._set_current_page(self.login_vbox)
360
self.window.set_size_request(*window_size)
361
size_req = (int(window_size[0] * 0.9), -1)
362
for label in self.labels:
363
label.set_size_request(*size_req)
367
self._filter_by_app_name(self.on_captcha_generated),
368
'CaptchaGenerationError':
369
self._filter_by_app_name(self.on_captcha_generation_error),
371
self._filter_by_app_name(self.on_user_registered),
372
'UserRegistrationError':
373
self._filter_by_app_name(self.on_user_registration_error),
375
self._filter_by_app_name(self.on_email_validated),
376
'EmailValidationError':
377
self._filter_by_app_name(self.on_email_validation_error),
379
self._filter_by_app_name(self.on_logged_in),
381
self._filter_by_app_name(self.on_login_error),
383
self._filter_by_app_name(self.on_user_not_validated),
384
'PasswordResetTokenSent':
385
self._filter_by_app_name(self.on_password_reset_token_sent),
386
'PasswordResetError':
387
self._filter_by_app_name(self.on_password_reset_error),
389
self._filter_by_app_name(self.on_password_changed),
390
'PasswordChangeError':
391
self._filter_by_app_name(self.on_password_change_error),
393
self._setup_signals()
396
# be as robust as possible:
397
# if the window_id is not "good", set_transient_for will fail
398
# awfully, and we don't want that: if the window_id is bad we can
399
# still do everything as a standalone window. Also,
400
# window_foreign_new may return None breaking set_transient_for.
402
win = gtk.gdk.window_foreign_new(window_id)
403
self.window.realize()
404
self.window.window.set_transient_for(win)
405
except: # pylint: disable=W0702
406
msg = 'UbuntuSSOClientGUI: failed set_transient_for win id %r'
407
logger.exception(msg, window_id)
409
# Hidding unused widgets to save some space (LP #627440).
410
self.name_entry.hide()
411
self.yes_to_updates_checkbutton.hide()
416
def success_vbox(self):
417
"""The success page."""
418
self.finish_vbox.label.set_markup('<span size="x-large">%s</span>' %
420
return self.finish_vbox
423
def error_vbox(self):
424
"""The error page."""
425
self.finish_vbox.label.set_markup('<span size="x-large">%s</span>' %
427
return self.finish_vbox
431
def _filter_by_app_name(self, f):
432
"""Excecute the decorated function only for 'self.app_name'."""
435
def inner(app_name, *args, **kwargs):
436
"""Execute 'f' only if 'app_name' matches 'self.app_name'."""
438
if app_name == self.app_name:
439
result = f(app_name, *args, **kwargs)
441
logger.info('%s: ignoring call since received app_name '\
442
'"%s" (expected "%s")',
443
f.__name__, app_name, self.app_name)
448
def _setup_signals(self):
449
"""Bind signals to callbacks to be able to test the pages."""
450
iface = self.iface_name
451
for signal, method in self._signals.iteritems():
452
actual = self._signals_receivers.get((iface, signal))
453
if actual is not None:
454
msg = 'Signal %r is already connected with %r at iface %r.'
455
logger.warning(msg, signal, actual, iface)
457
match = self.bus.add_signal_receiver(method, signal_name=signal,
458
dbus_interface=iface)
459
logger.debug('Connecting signal %r with method %r at iface %r.' \
460
'Match: %r', signal, method, iface, match)
461
self._signals_receivers[(iface, signal)] = method
463
def _debug(self, *args, **kwargs):
464
"""Do some debugging."""
467
def _add_spinner_to_container(self, container, legend=None):
468
"""Add a spinner to 'container'."""
469
spinner = gtk.Spinner()
474
label.set_text(legend)
476
label.set_text(self.LOADING)
478
hbox = gtk.HBox(spacing=5)
479
hbox.pack_start(spinner, expand=False)
480
hbox.pack_start(label, expand=False)
482
alignment = gtk.Alignment(xalign=0.5, yalign=0.5)
486
# remove children to avoid:
487
# GtkWarning: Attempting to add a widget with type GtkAlignment to a
488
# GtkEventBox, but as a GtkBin subclass a GtkEventBox can only contain
489
# one widget at a time
490
for child in container.get_children():
491
container.remove(child)
492
container.add(alignment)
494
def _set_warning_message(self, widget, message):
495
"""Set 'message' as text for 'widget'."""
496
widget.set_text(message)
497
widget.modify_fg(gtk.STATE_NORMAL, WARNING_TEXT_COLOR)
500
def _clear_warnings(self):
501
"""Clear all warning messages."""
502
for widget in self.warnings:
505
for widget in self.entries:
506
getattr(self, widget).clear_warning()
508
def _non_empty_input(self, widget):
509
"""Return weather widget has non empty content."""
510
text = widget.get_text()
511
return bool(text and not text.isspace())
515
def _append_page(self, page):
516
"""Append 'page' to the 'window'."""
517
self.window.get_children()[0].pack_start(page)
519
def _set_header(self, header):
520
"""Set 'header' as the window title and header."""
521
markup = '<span size="xx-large">%s</span>'
522
self.header_label.set_markup(markup % header)
523
self.window.set_title(self.header_label.get_text()) # avoid markup
525
def _set_current_page(self, current_page, warning_text=None):
526
"""Hide all the pages but 'current_page'."""
527
if hasattr(current_page, 'header'):
528
self._set_header(current_page.header)
530
if hasattr(current_page, 'help_text'):
531
self.help_label.set_markup(current_page.help_text)
533
if warning_text is not None:
534
self._set_warning_message(self.warning_label, warning_text)
536
self.warning_label.hide()
538
for page in self.pages:
539
if page is current_page:
544
if current_page.default_widget is not None:
545
current_page.default_widget.grab_default()
547
def _generate_captcha(self):
548
"""Ask for a new captcha; update the ui to reflect the fact."""
549
logger.info('Calling generate_captcha with filename path at %r',
550
self._captcha_filename)
551
self.backend.generate_captcha(self.app_name, self._captcha_filename,
552
reply_handler=NO_OP, error_handler=NO_OP)
553
self._set_captcha_loading()
555
def _set_captcha_loading(self):
556
"""Present a spinner to the user while the captcha is downloaded."""
557
self.captcha_image.hide()
558
self._add_spinner_to_container(self.captcha_loading)
559
white = gtk.gdk.Color('white')
560
self.captcha_loading.modify_bg(gtk.STATE_NORMAL, white)
561
self.captcha_loading.show_all()
562
self.join_ok_button.set_sensitive(False)
564
def _set_captcha_image(self):
565
"""Present a captcha image to the user to be resolved."""
566
self.captcha_loading.hide()
567
self.join_ok_button.set_sensitive(True)
568
self.captcha_image.set_from_file(self._captcha_filename)
569
self.captcha_image.show()
571
def _build_enter_details_page(self):
572
"""Build the enter details page."""
573
d = {'app_name': self.app_label}
574
self.enter_details_vbox.header = self.JOIN_HEADER_LABEL % d
575
self.enter_details_vbox.help_text = self.help_text
576
self.enter_details_vbox.default_widget = self.join_ok_button
577
self.join_ok_button.set_flags(gtk.CAN_DEFAULT)
579
self.enter_details_vbox.pack_start(self.name_entry, expand=False)
580
self.enter_details_vbox.reorder_child(self.name_entry, 0)
581
entry = self.captcha_solution_entry
582
self.captcha_solution_vbox.pack_start(entry, expand=False)
583
msg = self.CAPTCHA_RELOAD_TOOLTIP
584
self.captcha_reload_button.set_tooltip_text(msg)
586
self.emails_hbox.pack_start(self.email1_entry, expand=False)
587
self.emails_hbox.pack_start(self.email2_entry, expand=False)
589
self.passwords_hbox.pack_start(self.password1_entry, expand=False)
590
self.passwords_hbox.pack_start(self.password2_entry, expand=False)
591
help_msg = '<small>%s</small>' % self.PASSWORD_HELP
592
self.password_help_label.set_markup(help_msg)
594
if not os.path.exists(self._captcha_filename):
595
self._generate_captcha()
597
self._set_captcha_image()
599
msg = self.YES_TO_UPDATES % {'app_name': self.app_name}
600
self.yes_to_updates_checkbutton.set_label(msg)
602
msg = self.YES_TO_TC % {'app_name': self.app_name}
603
self.yes_to_tc_checkbutton.set_label(msg)
604
self.tc_button.set_label(self.TC_BUTTON)
606
self.tc_vbox.hide_all()
607
self.login_button.set_label(self.LOGIN_BUTTON_LABEL)
609
return self.enter_details_vbox
611
def _build_tc_page(self):
612
"""Build the Terms & Conditions page."""
613
self.tc_browser_vbox.help_text = ''
614
self.tc_browser_vbox.default_widget = self.tc_back_button
615
self.tc_browser_vbox.default_widget.set_flags(gtk.CAN_DEFAULT)
616
return self.tc_browser_vbox
618
def _build_processing_page(self):
619
"""Build the processing page with a spinner."""
620
self.processing_vbox.default_widget = None
621
self._add_spinner_to_container(self.processing_vbox,
622
legend=self.ONE_MOMENT_PLEASE)
623
return self.processing_vbox
625
def _build_verify_email_page(self):
626
"""Build the verify email page."""
627
self.verify_email_vbox.default_widget = self.verify_token_button
628
self.verify_email_vbox.default_widget.set_flags(gtk.CAN_DEFAULT)
630
self.verify_email_details_vbox.pack_start(self.email_token_entry,
633
return self.verify_email_vbox
635
def _build_finish_page(self):
636
"""Build the success page."""
637
self.finish_vbox.default_widget = self.finish_close_button
638
self.finish_vbox.default_widget.set_flags(gtk.CAN_DEFAULT)
639
self.finish_vbox.label = self.finish_label
640
return self.finish_vbox
642
def _build_login_page(self):
643
"""Build the login page."""
644
d = {'app_name': self.app_label}
645
self.login_vbox.header = self.LOGIN_HEADER_LABEL % d
646
self.login_vbox.help_text = self.CONNECT_HELP_LABEL % d
647
self.login_vbox.default_widget = self.login_ok_button
648
self.login_vbox.default_widget.set_flags(gtk.CAN_DEFAULT)
650
self.login_details_vbox.pack_start(self.login_email_entry)
651
self.login_details_vbox.reorder_child(self.login_email_entry, 0)
652
self.login_details_vbox.pack_start(self.login_password_entry)
653
self.login_details_vbox.reorder_child(self.login_password_entry, 1)
655
msg = self.FORGOTTEN_PASSWORD_BUTTON
656
self.forgotten_password_button.set_label(msg)
657
self.login_ok_button.grab_focus()
659
return self.login_vbox
661
def _build_request_password_token_page(self):
662
"""Build the login page."""
663
self.request_password_token_vbox.header = self.RESET_PASSWORD
664
text = self.REQUEST_PASSWORD_TOKEN_LABEL % {'app_name': self.app_label}
665
self.request_password_token_vbox.help_text = text
666
btn = self.request_password_token_ok_button
667
btn.set_flags(gtk.CAN_DEFAULT)
668
self.request_password_token_vbox.default_widget = btn
670
entry = self.reset_email_entry
671
self.request_password_token_details_vbox.pack_start(entry,
673
cb = self.on_reset_email_entry_changed
674
self.reset_email_entry.connect('changed', cb)
675
self.request_password_token_ok_button.set_label(self.NEXT)
676
self.request_password_token_ok_button.set_sensitive(False)
678
return self.request_password_token_vbox
680
def _build_set_new_password_page(self):
681
"""Build the login page."""
682
self.set_new_password_vbox.header = self.RESET_PASSWORD
683
self.set_new_password_vbox.help_text = self.SET_NEW_PASSWORD_LABEL
684
btn = self.set_new_password_ok_button
685
btn.set_flags(gtk.CAN_DEFAULT)
686
self.set_new_password_vbox.default_widget = btn
688
for entry in (self.reset_code_entry,
689
self.reset_password1_entry,
690
self.reset_password2_entry):
691
self.set_new_password_details_vbox.pack_start(entry, expand=False)
693
cb = self.on_set_new_password_entries_changed
694
self.reset_code_entry.connect('changed', cb)
695
self.reset_password1_entry.connect('changed', cb)
696
self.reset_password2_entry.connect('changed', cb)
697
help_msg = '<small>%s</small>' % self.PASSWORD_HELP
698
self.reset_password_help_label.set_markup(help_msg)
700
self.set_new_password_ok_button.set_label(self.RESET_PASSWORD)
701
self.set_new_password_ok_button.set_sensitive(False)
703
return self.set_new_password_vbox
705
def _validate_email(self, email1, email2=None):
706
"""Validate 'email1', return error message if not valid.
708
If 'email2' is given, must match 'email1'.
710
if email2 is not None and email1 != email2:
711
return self.EMAIL_MISMATCH
714
return self.FIELD_REQUIRED
716
if '@' not in email1:
717
return self.EMAIL_INVALID
719
def _validate_password(self, password1, password2=None):
720
"""Validate 'password1', return error message if not valid.
722
If 'password2' is given, must match 'email1'.
724
if password2 is not None and password1 != password2:
725
return self.PASSWORD_MISMATCH
727
if (len(password1) < 8 or
728
re.search('[A-Z]', password1) is None or
729
re.search('\d+', password1) is None):
730
return self.PASSWORD_TOO_WEAK
735
"""Run the application."""
738
def connect(self, signal_name, handler, *args, **kwargs):
739
"""Connect 'signal_name' with 'handler'."""
740
logger.debug('connect: signal %r, handler %r, args %r, kwargs, %r',
741
signal_name, handler, args, kwargs)
742
self.window.connect(signal_name, handler, *args, **kwargs)
744
def emit(self, *args, **kwargs):
745
"""Emit a signal proxing the main window."""
746
logger.debug('emit: args %r, kwargs, %r', args, kwargs)
747
self.window.emit(*args, **kwargs)
749
def finish_success(self):
750
"""The whole process was completed succesfully. Show success page."""
752
self._set_current_page(self.success_vbox)
754
def finish_error(self, error):
755
"""The whole process was not completed succesfully. Show error page."""
757
self._set_current_page(self.error_vbox)
759
def on_close_clicked(self, *args, **kwargs):
760
"""Call self.close_callback if defined."""
761
if os.path.exists(self._captcha_filename):
762
os.remove(self._captcha_filename)
764
# remove the signals from DBus
765
remove = self.bus.remove_signal_receiver
766
for (iface, signal) in self._signals_receivers.keys():
767
method = self._signals_receivers.pop((iface, signal))
768
logger.debug('Removing signal %r with method %r at iface %r.',
769
signal, method, iface)
770
remove(method, signal_name=signal, dbus_interface=iface)
772
# hide the main window
773
if self.window is not None:
776
# process any pending events before emitting signals
777
while gtk.events_pending():
781
self.emit(SIG_USER_CANCELATION, self.app_name)
783
# call user defined callback
784
if self.close_callback is not None:
785
logger.info('Calling custom close_callback %r with params %r, %r',
786
self.close_callback, args, kwargs)
787
self.close_callback(*args, **kwargs)
789
def on_sign_in_button_clicked(self, *args, **kwargs):
790
"""User wants to sign in, present the Login page."""
791
self._set_current_page(self.login_vbox)
793
def on_join_ok_button_clicked(self, *args, **kwargs):
794
"""Submit info for processing, present the processing vbox."""
795
if not self.join_ok_button.is_sensitive():
798
self._clear_warnings()
802
# Hidding unused widgets to save some space (LP #627440).
803
#name = self.name_entry.get_text()
805
# self.name_entry.set_warning(self.FIELD_REQUIRED)
809
email1 = self.email1_entry.get_text()
810
email2 = self.email2_entry.get_text()
811
msg = self._validate_email(email1, email2)
813
self.email1_entry.set_warning(msg)
814
self.email2_entry.set_warning(msg)
818
password1 = self.password1_entry.get_text()
819
password2 = self.password2_entry.get_text()
820
msg = self._validate_password(password1, password2)
822
self.password1_entry.set_warning(msg)
823
self.password2_entry.set_warning(msg)
827
if not self.yes_to_tc_checkbutton.get_active():
828
self._set_warning_message(self.tc_warning_label,
829
self.TC_NOT_ACCEPTED)
832
captcha_solution = self.captcha_solution_entry.get_text()
833
if not captcha_solution:
834
self.captcha_solution_entry.set_warning(self.FIELD_REQUIRED)
840
self._set_current_page(self.processing_vbox)
841
self.user_email = email1
842
self.user_password = password1
844
logger.info('Calling register_user with email %r, password <hidden>,' \
845
' captcha_id %r and captcha_solution %r.', email1,
846
self._captcha_id, captcha_solution)
847
f = self.backend.register_user
848
f(self.app_name, email1, password1, self._captcha_id, captcha_solution,
849
reply_handler=NO_OP, error_handler=NO_OP)
851
def on_verify_token_button_clicked(self, *args, **kwargs):
852
"""The user entered the email token, let's verify!"""
853
if not self.verify_token_button.is_sensitive():
856
self._clear_warnings()
858
email_token = self.email_token_entry.get_text()
860
self.email_token_entry.set_warning(self.FIELD_REQUIRED)
863
email = self.user_email
864
password = self.user_password
865
f = self.backend.validate_email
866
logger.info('Calling validate_email with email %r, password <hidden>' \
867
', app_name %r and email_token %r.', email, self.app_name,
869
f(self.app_name, email, password, email_token,
870
reply_handler=NO_OP, error_handler=NO_OP)
872
self._set_current_page(self.processing_vbox)
874
def on_login_connect_button_clicked(self, *args, **kwargs):
875
"""User wants to connect!"""
876
if not self.login_ok_button.is_sensitive():
879
self._clear_warnings()
883
email = self.login_email_entry.get_text()
884
msg = self._validate_email(email)
886
self.login_email_entry.set_warning(msg)
889
password = self.login_password_entry.get_text()
891
self.login_password_entry.set_warning(self.FIELD_REQUIRED)
897
f = self.backend.login
898
f(self.app_name, email, password,
899
reply_handler=NO_OP, error_handler=NO_OP)
901
self._set_current_page(self.processing_vbox)
902
self.user_email = email
903
self.user_password = password
905
def on_login_back_button_clicked(self, *args, **kwargs):
906
"""User wants to go to the previous page."""
907
self._set_current_page(self.enter_details_vbox)
909
def on_forgotten_password_button_clicked(self, *args, **kwargs):
910
"""User wants to reset the password."""
911
self._set_current_page(self.request_password_token_vbox)
913
def on_request_password_token_ok_button_clicked(self, *args, **kwargs):
914
"""User entered the email address to reset the password."""
915
if not self.request_password_token_ok_button.is_sensitive():
918
self._clear_warnings()
920
email = self.reset_email_entry.get_text()
921
msg = self._validate_email(email)
923
self.reset_email_entry.set_warning(msg)
926
logger.info('Calling request_password_reset_token with %r.', email)
927
f = self.backend.request_password_reset_token
928
f(self.app_name, email, reply_handler=NO_OP, error_handler=NO_OP)
930
self._set_current_page(self.processing_vbox)
932
def on_request_password_token_back_button_clicked(self, *args, **kwargs):
933
"""User wants to go to the previous page."""
934
self._set_current_page(self.login_vbox)
936
def on_reset_email_entry_changed(self, widget, *args, **kwargs):
937
"""User is changing the 'widget' entry in the reset email page."""
938
sensitive = self._non_empty_input(widget)
939
self.request_password_token_ok_button.set_sensitive(sensitive)
941
def on_set_new_password_entries_changed(self, *args, **kwargs):
942
"""User is changing the 'widget' entry in the reset password page."""
944
for entry in (self.reset_code_entry,
945
self.reset_password1_entry,
946
self.reset_password2_entry):
947
sensitive &= self._non_empty_input(entry)
948
self.set_new_password_ok_button.set_sensitive(sensitive)
950
def on_set_new_password_ok_button_clicked(self, *args, **kwargs):
951
"""User entered reset code and new passwords."""
952
if not self.set_new_password_ok_button.is_sensitive():
955
self._clear_warnings()
959
token = self.reset_code_entry.get_text()
961
self.reset_code_entry.set_warning(self.FIELD_REQUIRED)
964
password1 = self.reset_password1_entry.get_text()
965
password2 = self.reset_password2_entry.get_text()
966
msg = self._validate_password(password1, password2)
968
self.reset_password1_entry.set_warning(msg)
969
self.reset_password2_entry.set_warning(msg)
975
email = self.reset_email_entry.get_text()
976
logger.info('Calling set_new_password with email %r, token %r and ' \
977
'new password: <hidden>.', email, token)
978
f = self.backend.set_new_password
979
f(self.app_name, email, token, password1,
980
reply_handler=NO_OP, error_handler=NO_OP)
982
self._set_current_page(self.processing_vbox)
984
def on_tc_button_clicked(self, *args, **kwargs):
985
"""The T&C button was clicked, create the browser and load terms."""
986
# delay the import of webkit to be able to build without it
988
browser = webkit.WebView()
990
# The signal WebKitWebView::load-finished is deprecated and should not
991
# be used in newly-written code. Use the "load-status" property
992
# instead. Connect to "notify::load-status" to monitor loading.
994
# nataliabidart (2010-10-04): connecting this signal makes the loading
995
# of the Ubuntu One terms URL to fail. So we're using the deprecated
996
# 'load-finished' for now.
998
#browser.connect('notify::load-status',
999
# self.on_tc_browser_notify_load_status)
1000
browser.connect('load-finished',
1001
self.on_tc_browser_notify_load_status)
1002
browser.connect('navigation-policy-decision-requested',
1003
self.on_tc_browser_navigation_requested)
1005
settings = browser.get_settings()
1006
settings.set_property("enable-plugins", False)
1007
settings.set_property("enable-default-context-menu", False)
1009
# webkit_web_view_open has been deprecated since version 1.1.1 and
1010
# should not be used in newly-written code. Use
1011
# webkit_web_view_load_uri() instead.
1012
browser.load_uri(self.tc_url)
1014
self.tc_browser_window.add(browser)
1015
self._set_current_page(self.processing_vbox)
1017
def on_tc_back_button_clicked(self, *args, **kwargs):
1018
"""T & C 'back' button was clicked, return to the previous page."""
1019
self._set_current_page(self.enter_details_vbox)
1021
def on_tc_browser_notify_load_status(self, browser, *args, **kwargs):
1022
"""The T&C page is being loaded."""
1023
if browser.get_load_status() == WEBKIT_LOAD_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
if action is not None and \
1030
action.get_reason() == WEBKIT_WEB_NAVIGATION_REASON_LINK_CLICKED:
1031
if decision is not None:
1033
url = action.get_original_uri()
1034
webbrowser.open(url)
1036
if decision is not None:
1039
def on_tc_browser_vbox_hide(self, *args, **kwargs):
1040
"""The T&C page is no longer being shown."""
1041
children = self.tc_browser_window.get_children()
1042
if len(children) > 0:
1043
browser = children[0]
1044
self.tc_browser_window.remove(browser)
1048
def on_captcha_reload_button_clicked(self, *args, **kwargs):
1049
"""User clicked the reload captcha button."""
1050
self._generate_captcha()
1054
def _build_general_error_message(self, errordict):
1055
"""Concatenate __all__ and message from the errordict."""
1057
msg1 = errordict.get('__all__')
1058
msg2 = errordict.get('message')
1059
if msg1 is not None and msg2 is not None:
1060
result = '\n'.join((msg1, msg2))
1062
result = msg1 if msg1 is not None else msg2
1066
def on_captcha_generated(self, app_name, captcha_id, *args, **kwargs):
1067
"""Captcha image has been generated and is available to be shown."""
1068
if captcha_id is None:
1069
logger.warning('on_captcha_generated: captcha_id is None for '
1070
'app_name "%s".', app_name)
1071
self._captcha_id = captcha_id
1072
self._set_captcha_image()
1075
def on_captcha_generation_error(self, app_name, error, *args, **kwargs):
1076
"""Captcha image generation failed."""
1077
self._set_warning_message(self.warning_label, self.CAPTCHA_LOAD_ERROR)
1078
self._generate_captcha()
1081
def on_user_registered(self, app_name, email, *args, **kwargs):
1082
"""Registration can go on, user needs to verify email."""
1083
help_text = self.VERIFY_EMAIL_LABEL % {'app_name': self.app_name,
1085
self.verify_email_vbox.help_text = help_text
1086
self._set_current_page(self.verify_email_vbox)
1089
def on_user_registration_error(self, app_name, error, *args, **kwargs):
1090
"""Captcha image generation failed."""
1091
msg = error.get('email')
1093
self.email1_entry.set_warning(msg)
1094
self.email2_entry.set_warning(msg)
1096
msg = error.get('password')
1098
self.password1_entry.set_warning(msg)
1099
self.password2_entry.set_warning(msg)
1101
msg = self._build_general_error_message(error)
1102
self._generate_captcha()
1103
self._set_current_page(self.enter_details_vbox, warning_text=msg)
1106
def on_email_validated(self, app_name, email, *args, **kwargs):
1107
"""User email was successfully verified."""
1109
self.emit(SIG_REGISTRATION_SUCCEEDED, self.app_name, email)
1112
def on_email_validation_error(self, app_name, error, *args, **kwargs):
1113
"""User email validation failed."""
1114
msg = error.get('email_token')
1116
self.email_token_entry.set_warning(msg)
1118
msg = self._build_general_error_message(error)
1119
self._set_current_page(self.verify_email_vbox, warning_text=msg)
1122
def on_logged_in(self, app_name, email, *args, **kwargs):
1123
"""User was successfully logged in."""
1125
self.emit(SIG_LOGIN_SUCCEEDED, self.app_name, email)
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 = self.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=self.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,