# This file is part of Gajim. # # Gajim is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published # by the Free Software Foundation; version 3 only. # # Gajim is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Gajim. If not, see . import logging from gi.repository import Gdk from gi.repository import GLib from gi.repository import Gtk from gi.repository import Gio from gi.repository import GObject from nbxmpp.client import Client from nbxmpp.protocol import JID from nbxmpp.protocol import validate_domainpart from nbxmpp.const import Mode from nbxmpp.const import StreamError from nbxmpp.const import ConnectionProtocol from nbxmpp.const import ConnectionType from nbxmpp.errors import StanzaError from nbxmpp.errors import MalformedStanzaError from nbxmpp.errors import RegisterStanzaError from gajim.common import app from gajim.common import configpaths from gajim.common import helpers from gajim.common.nec import NetworkEvent from gajim.common.helpers import open_uri from gajim.common.helpers import validate_jid from gajim.common.helpers import get_proxy from gajim.common.i18n import _ from gajim.common.const import SASL_ERRORS from gajim.common.const import GIO_TLS_ERRORS from .assistant import Assistant from .assistant import Page from .assistant import SuccessPage from .assistant import ErrorPage from .dataform import DataFormWidget from .util import get_builder from .util import open_window log = logging.getLogger('gajim.gui.account_wizard') class AccountWizard(Assistant): def __init__(self): Assistant.__init__(self, height=500) self._destroyed = False self.add_button('signup', _('Sign Up'), complete=True, css_class='suggested-action') self.add_button('connect', _('Connect'), css_class='suggested-action') self.add_button('next', _('Next'), css_class='suggested-action') self.add_button('login', _('Log In'), complete=True, css_class='suggested-action') self.add_button('back', _('Back')) self.add_pages({'login': Login(), 'signup': Signup(), 'advanced': AdvancedSettings(), 'security-warning': SecurityWarning(), 'form': Form(), 'redirect': Redirect(), 'success': Success(), 'error': Error(), }) self._progress = self.add_default_page('progress') self.get_page('login').connect('clicked', self._on_button_clicked) self.connect('button-clicked', self._on_assistant_button_clicked) self.connect('page-changed', self._on_page_changed) self.connect('destroy', self._on_destroy) self.show_all() self.update_proxy_list() self._client = None self._method = 'login' def get_currenct_method(self): return self._method def _on_button_clicked(self, _page, button_name): if button_name == 'login': if self.get_page('login').is_advanced(): self.show_page('advanced', Gtk.StackTransitionType.SLIDE_LEFT) else: self._test_credentials() elif button_name == 'signup': self.show_page('signup', Gtk.StackTransitionType.SLIDE_LEFT) def _on_assistant_button_clicked(self, _assistant, button_name): page = self.get_current_page() if button_name == 'login': if page == 'advanced': self._test_credentials() elif page == 'security-warning': if self.get_page('security-warning').trust_certificate: app.cert_store.add_certificate( self.get_page('security-warning').cert) self._test_credentials(ignore_all_errors=True) elif button_name == 'signup': if page == 'signup': if self.get_page('signup').is_advanced(): self.show_page('advanced', Gtk.StackTransitionType.SLIDE_LEFT) elif self.get_page('signup').is_anonymous(): self._test_anonymous_server() else: self._register_with_server() elif page == 'advanced': if self.get_page('signup').is_anonymous(): self._test_anonymous_server() else: self._register_with_server() elif page == 'security-warning': if self.get_page('security-warning').trust_certificate: app.cert_store.add_certificate( self.get_page('security-warning').cert) if self.get_page('signup').is_anonymous(): self._test_anonymous_server(ignore_all_errors=True) else: self._register_with_server(ignore_all_errors=True) elif page == 'form': self._show_progress_page(_('Creating Account...'), _('Trying to create account...')) self._submit_form() elif button_name == 'connect': if page == 'success': app.interface.enable_account(self.get_page('success').account) self.destroy() elif button_name == 'back': if page == 'signup': self.show_page('login', Gtk.StackTransitionType.SLIDE_RIGHT) elif page in ('advanced', 'error', 'security-warning'): if (page == 'error' and self._method == 'signup' and self.get_page('form').has_form): self.show_page('form', Gtk.StackTransitionType.SLIDE_RIGHT) else: self.show_page(self._method, Gtk.StackTransitionType.SLIDE_RIGHT) elif page == 'form': self.show_page('signup', Gtk.StackTransitionType.SLIDE_RIGHT) self.get_page('form').remove_form() self._disconnect() elif page == 'redirect': self.show_page('login', Gtk.StackTransitionType.SLIDE_RIGHT) def _on_page_changed(self, _assistant, page_name): if page_name == 'signup': self._method = page_name self.get_page('signup').focus() elif page_name == 'login': self._method = page_name self.get_page('login').focus() elif page_name == 'form': self.get_page('form').focus() def update_proxy_list(self): self.get_page('advanced').update_proxy_list() def _get_base_client(self, domain, username, mode, advanced, ignore_all_errors): client = Client(log_context='Account Wizard') client.set_domain(domain) client.set_username(username) client.set_mode(mode) client.set_ignore_tls_errors(ignore_all_errors) client.set_accepted_certificates( app.cert_store.get_certificates()) if advanced: custom_host = self.get_page('advanced').get_custom_host() if custom_host is not None: client.set_custom_host(*custom_host) proxy_name = self.get_page('advanced').get_proxy() proxy_data = get_proxy(proxy_name) if proxy_data is not None: client.set_proxy(proxy_data) client.subscribe('disconnected', self._on_disconnected) client.subscribe('connection-failed', self._on_connection_failed) client.subscribe('stanza-sent', self._on_stanza_sent) client.subscribe('stanza-received', self._on_stanza_received) return client def _disconnect(self): if self._client is None: return self._client.remove_subscriptions() self._client.disconnect() self._client = None @staticmethod def _on_stanza_sent(_client, _signal_name, stanza): app.nec.push_incoming_event(NetworkEvent('stanza-sent', account='AccountWizard', stanza=stanza)) @staticmethod def _on_stanza_received(_client, _signal_name, stanza): app.nec.push_incoming_event(NetworkEvent('stanza-received', account='AccountWizard', stanza=stanza)) def _test_credentials(self, ignore_all_errors=False): self._show_progress_page(_('Connecting...'), _('Connecting to server...')) address, password = self.get_page('login').get_credentials() jid = JID.from_string(address) advanced = self.get_page('login').is_advanced() self._client = self._get_base_client( jid.domain, jid.localpart, Mode.LOGIN_TEST, advanced, ignore_all_errors) self._client.set_password(password) self._client.subscribe('login-successful', self._on_login_successful) self._client.connect() def _test_anonymous_server(self, ignore_all_errors=False): self._show_progress_page(_('Connecting...'), _('Connecting to server...')) domain = self.get_page('signup').get_server() advanced = self.get_page('signup').is_advanced() self._client = self._get_base_client( domain, None, Mode.ANONYMOUS_TEST, advanced, ignore_all_errors) self._client.subscribe('anonymous-supported', self._on_anonymous_supported) self._client.connect() def _register_with_server(self, ignore_all_errors=False): self._show_progress_page(_('Connecting...'), _('Connecting to server...')) domain = self.get_page('signup').get_server() advanced = self.get_page('signup').is_advanced() self._client = self._get_base_client( domain, None, Mode.REGISTER, advanced, ignore_all_errors) self._client.subscribe('connected', self._on_connected) self._client.connect() def _on_login_successful(self, client, _signal_name): account = self._generate_account_name(client.domain) proxy_name = None if client.proxy is not None: proxy_name = self.get_page('advanced').get_proxy() app.interface.create_account(account, client.username, client.domain, client.password, proxy_name, client.custom_host) self.get_page('success').set_account(account) self.show_page('success', Gtk.StackTransitionType.SLIDE_LEFT) def _on_connected(self, client, _signal_name): client.get_module('Register').request_register_form( callback=self._on_register_form) def _on_anonymous_supported(self, client, _signal_name): account = self._generate_account_name(client.domain) proxy_name = None if client.proxy is not None: proxy_name = self.get_page('advanced').get_proxy() app.interface.create_account(account, None, client.domain, client.password, proxy_name, client.custom_host, anonymous=True) self.get_page('success').set_account(account) self.show_page('success', Gtk.StackTransitionType.SLIDE_LEFT) def _on_disconnected(self, client, _signal_name): domain, error, text = client.get_error() if domain == StreamError.SASL: if error == 'anonymous-not-supported': self._show_error_page(_('Anonymous login not supported'), _('Anonymous login not supported'), _('This server does not support ' 'anonymous login.')) else: self._show_error_page(_('Authentication failed'), SASL_ERRORS.get(error), text or '') elif domain == StreamError.BAD_CERTIFICATE: self.get_page('security-warning').set_warning( self._client.domain, *self._client.peer_certificate) self.show_page('security-warning', Gtk.StackTransitionType.SLIDE_LEFT) elif domain == StreamError.REGISTER: if error == 'register-not-supported': self._show_error_page(_('Signup not allowed'), _('Signup not allowed'), _('This server does not allow signup.')) elif domain == StreamError.STREAM: # The credential test often ends with a stream error, because # after auth there should be a stream restart but nbxmpp ends # the stream with which is considered not-well-formed # by the server. This ignores all stream errors if we already # know that we succeeded. if self.get_current_page() != 'success': self._show_error_page(_('Error'), _('Error'), error) self.get_page('form').remove_form() self._client.destroy() self._client = None def _on_connection_failed(self, _client, _signal_name): self._show_error_page(_('Connection failed'), _('Connection failed'), _('Gajim was not able to reach the server. ' 'Make sure your XMPP address is correct.')) self._client.destroy() self._client = None def _show_error_page(self, title, heading, text): self.get_page('error').set_title(title) self.get_page('error').set_heading(heading) self.get_page('error').set_text(text or '') self.show_page('error', Gtk.StackTransitionType.SLIDE_LEFT) def _show_progress_page(self, title, text): self._progress.set_title(title) self._progress.set_text(text) self.show_page('progress', Gtk.StackTransitionType.SLIDE_LEFT) @staticmethod def _generate_account_name(domain): i = 1 while domain in app.settings.get_accounts(): domain = domain + str(i) i += 1 return domain def _on_register_form(self, task): try: result = task.finish() except (StanzaError, MalformedStanzaError) as error: self._show_error_page(_('Error'), _('Error'), error.get_text()) self._disconnect() return if result.bob_data is not None: algo_hash = result.bob_data.cid.split('@')[0] app.bob_cache[algo_hash] = result.bob_data.data form = result.form if result.form is None: form = result.fields_form if form is not None: self.get_page('form').add_form(form) elif result.oob_url is not None: self.get_page('redirect').set_redirect(result.oob_url, result.instructions) self.show_page('redirect', Gtk.StackTransitionType.SLIDE_LEFT) self._disconnect() return self.show_page('form', Gtk.StackTransitionType.SLIDE_LEFT) def _submit_form(self): self.get_page('progress').set_text(_('Account is being created')) self.show_page('progress', Gtk.StackTransitionType.SLIDE_LEFT) form = self.get_page('form').get_submit_form() self._client.get_module('Register').submit_register_form( form, callback=self._on_register_result) def _on_register_result(self, task): try: task.finish() except RegisterStanzaError as error: self._set_error_text(error) if error.type != 'modify': self.get_page('form').remove_form() self._disconnect() return register_data = error.get_data() form = register_data.form if register_data.form is None: form = register_data.fields_form if form is not None: self.get_page('form').add_form(form) else: self.get_page('form').remove_form() self._disconnect() return except (StanzaError, MalformedStanzaError) as error: self._set_error_text(error) self.get_page('form').remove_form() self._disconnect() return username, password = self.get_page('form').get_credentials() account = self._generate_account_name(self._client.domain) proxy_name = None if self._client.proxy is not None: proxy_name = self.get_page('advanced').get_proxy() app.interface.create_account(account, username, self._client.domain, password, proxy_name, self._client.custom_host) self.get_page('success').set_account(account) self.show_page('success', Gtk.StackTransitionType.SLIDE_LEFT) self.get_page('form').remove_form() self._disconnect() def _set_error_text(self, error): error_text = error.get_text() if not error_text: error_text = _('The server rejected the registration ' 'without an error message') self._show_error_page(_('Error'), _('Error'), error_text) def _on_destroy(self, *args): self._disconnect() self._destroyed = True class Login(Page): __gsignals__ = { 'clicked': (GObject.SignalFlags.RUN_LAST, None, (str,)), } def __init__(self): Page.__init__(self) self.title = _('Add Account') self._ui = get_builder('account_wizard.ui') self._ui.log_in_address_entry.connect( 'activate', self._on_address_entry_activate) self._ui.log_in_address_entry.connect( 'changed', self._on_address_changed) self._ui.log_in_password_entry.connect( 'changed', self._set_complete) self._ui.log_in_password_entry.connect( 'activate', self._on_password_entry_activate) self._create_server_completion() self._ui.log_in_button.connect('clicked', self._on_login) self._ui.sign_up_button.connect('clicked', self._on_signup) self.pack_start(self._ui.login_box, True, True, 0) self.show_all() def focus(self): self._ui.log_in_address_entry.grab_focus() def _on_login(self, *args): self.emit('clicked', 'login') def _on_signup(self, *args): self.emit('clicked', 'signup') def _create_server_completion(self): # Parse servers.json file_path = configpaths.get('DATA') / 'other' / 'servers.json' self._servers = helpers.load_json(file_path, default=[]) # Create a separate model for the address entry, because it will # be updated with our localpart@ address_model = Gtk.ListStore(str) for server in self._servers: address_model.append((server,)) self._ui.log_in_address_entry.get_completion().set_model(address_model) def _on_address_changed(self, entry): self._update_completion(entry) self._set_complete() def _update_completion(self, entry): text = entry.get_text() if '@' not in text: self._show_icon(False) return text = text.split('@', 1)[0] model = entry.get_completion().get_model() model.clear() for server in self._servers: model.append(['%s@%s' % (text, server)]) def _show_icon(self, show): icon = 'dialog-warning-symbolic' if show else None self._ui.log_in_address_entry.set_icon_from_icon_name( Gtk.EntryIconPosition.SECONDARY, icon) def _on_address_entry_activate(self, _widget): GLib.idle_add(self._ui.log_in_password_entry.grab_focus) def _on_password_entry_activate(self, _widget): if self._ui.log_in_button.get_sensitive(): self._ui.log_in_button.activate() def _validate_jid(self, address): if not address: self._show_icon(False) return False try: jid = validate_jid(address, type_='bare') if jid.resource: raise ValueError except ValueError: self._show_icon(True) self._ui.log_in_address_entry.set_icon_tooltip_text( Gtk.EntryIconPosition.SECONDARY, _('Invalid Address')) return False self._show_icon(False) return True def _set_complete(self, *args): address = self._validate_jid(self._ui.log_in_address_entry.get_text()) password = self._ui.log_in_password_entry.get_text() self._ui.log_in_button.set_sensitive(address and password) def is_advanced(self): return self._ui.login_advanced_checkbutton.get_active() def get_credentials(self): data = (self._ui.log_in_address_entry.get_text(), self._ui.log_in_password_entry.get_text()) return data class Signup(Page): def __init__(self): Page.__init__(self) self.complete = False self.title = _('Create New Account') self._ui = get_builder('account_wizard.ui') self._ui.server_comboboxtext_sign_up_entry.set_activates_default(True) self._create_server_completion() self._ui.recommendation_link1.connect( 'activate-link', self._on_activate_link) self._ui.recommendation_link2.connect( 'activate-link', self._on_activate_link) self._ui.visit_server_button.connect('clicked', self._on_visit_server) self._ui.server_comboboxtext_sign_up_entry.connect( 'changed', self._set_complete) self.pack_start(self._ui.signup_grid, True, True, 0) self.show_all() def focus(self): self._ui.server_comboboxtext_sign_up_entry.grab_focus() def _create_server_completion(self): # Parse servers.json file_path = configpaths.get('DATA') / 'other' / 'servers.json' servers = helpers.load_json(file_path, default=[]) # Create servers_model for comboboxes and entries servers_model = Gtk.ListStore(str) for server in servers: servers_model.append((server,)) # Sign up combobox and entry self._ui.server_comboboxtext_sign_up.set_model(servers_model) self._ui.server_comboboxtext_sign_up_entry.get_completion().set_model( servers_model) def _on_visit_server(self, _widget): server = self._ui.server_comboboxtext_sign_up_entry.get_text().strip() server = 'https://' + server open_uri(server) return Gdk.EVENT_STOP def _set_complete(self, *args): try: self.get_server() except Exception: self.complete = False self._ui.visit_server_button.set_visible(False) else: self.complete = True self._ui.visit_server_button.set_visible(True) self.update_page_complete() def is_anonymous(self): return self._ui.sign_up_anonymously.get_active() def is_advanced(self): return self._ui.sign_up_advanced_checkbutton.get_active() def get_server(self): return validate_domainpart( self._ui.server_comboboxtext_sign_up_entry.get_text()) @staticmethod def _on_activate_link(_label, uri): # We have to use this, because the default GTK handler # is not cross-platform compatible open_uri(uri) return Gdk.EVENT_STOP def get_visible_buttons(self): return ['back', 'signup'] def get_default_button(self): return 'signup' class AdvancedSettings(Page): def __init__(self): Page.__init__(self) self.title = _('Advanced settings') self.complete = False self._ui = get_builder('account_wizard.ui') self._ui.manage_proxies_button.connect('clicked', self._on_proxy_manager) self._ui.proxies_combobox.connect('changed', self._set_complete) self._ui.custom_host_entry.connect('changed', self._set_complete) self._ui.custom_port_entry.connect('changed', self._set_complete) self.pack_start(self._ui.advanced_grid, True, True, 0) self.show_all() @staticmethod def _on_proxy_manager(_widget): app.app.activate_action('manage-proxies') def update_proxy_list(self): model = Gtk.ListStore(str) self._ui.proxies_combobox.set_model(model) proxies = app.settings.get_proxies() proxies.insert(0, _('No Proxy')) for proxy in proxies: model.append([proxy]) self._ui.proxies_combobox.set_active(0) def get_proxy(self): active = self._ui.proxies_combobox.get_active() return self._ui.proxies_combobox.get_model()[active][0] def get_custom_host(self): host = self._ui.custom_host_entry.get_text() port = self._ui.custom_port_entry.get_text() if not host or not port: return None con_type = self._ui.con_type_combo.get_active_text() protocol = ConnectionProtocol.TCP if host.startswith('ws://') or host.startswith('wss://'): protocol = ConnectionProtocol.WEBSOCKET return ('%s:%s' % (host, port), protocol, ConnectionType(con_type)) def _show_host_icon(self, show): icon = 'dialog-warning-symbolic' if show else None self._ui.custom_host_entry.set_icon_from_icon_name( Gtk.EntryIconPosition.SECONDARY, icon) def _show_port_icon(self, show): icon = 'dialog-warning-symbolic' if show else None self._ui.custom_port_entry.set_icon_from_icon_name( Gtk.EntryIconPosition.SECONDARY, icon) def _validate_host(self): host = self._ui.custom_host_entry.get_text() try: validate_domainpart(host) except Exception: self._show_host_icon(True) self._ui.custom_host_entry.set_icon_tooltip_text( Gtk.EntryIconPosition.SECONDARY, _('Invalid domain name')) return False self._show_host_icon(False) return True def _validate_port(self): port = self._ui.custom_port_entry.get_text() if not port: self._show_port_icon(False) return False try: port = int(port) except Exception: self._show_port_icon(True) self._ui.custom_port_entry.set_icon_tooltip_text( Gtk.EntryIconPosition.SECONDARY, _('Must be a port number')) return False if port not in range(0, 65535): self._show_port_icon(True) self._ui.custom_port_entry.set_icon_tooltip_text( Gtk.EntryIconPosition.SECONDARY, _('Port must be a number between 0 and 65535')) return False self._show_port_icon(False) return True def _is_custom_host_set(self): host = bool(self._ui.custom_host_entry.get_text()) port = bool(self._ui.custom_port_entry.get_text()) return host or port def _is_proxy_set(self): return self._ui.proxies_combobox.get_active() != 0 def _set_complete(self, *args): self.complete = False if self._is_proxy_set(): self.complete = True if self._is_custom_host_set(): port_valid = self._validate_port() host_valid = self._validate_host() self.complete = port_valid and host_valid self.update_page_complete() def get_visible_buttons(self): return ['back', self.get_toplevel().get_currenct_method()] def get_default_button(self): return self.get_toplevel().get_currenct_method() class SecurityWarning(Page): def __init__(self): Page.__init__(self) self.title = _('Security Warning') self._cert = None self._domain = None self._ui = get_builder('account_wizard.ui') self.pack_start(self._ui.security_warning_box, True, True, 0) self._ui.view_cert_button.connect('clicked', self._on_view_cert) self.show_all() @property def cert(self): return self._cert def set_warning(self, domain, cert, errors): # Clear list self._cert = cert self._domain = domain self._ui.error_list.foreach(self._ui.error_list.remove) unknown_error = _('Unknown TLS error \'%s\'') for error in errors: error_text = GIO_TLS_ERRORS.get(error, unknown_error % error) box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) image = Gtk.Image.new_from_icon_name('dialog-warning-symbolic', Gtk.IconSize.LARGE_TOOLBAR) image.get_style_context().add_class('warning-color') label = Gtk.Label(label=error_text) label.set_line_wrap(True) label.set_xalign(0) label.set_selectable(True) box.add(image) box.add(label) box.show_all() self._ui.error_list.add(box) self._ui.trust_cert_checkbutton.set_visible( Gio.TlsCertificateFlags.UNKNOWN_CA in errors) def _on_view_cert(self, _button): open_window('CertificateDialog', account=self._domain, transient_for=self.get_toplevel(), cert=self._cert) @property def trust_certificate(self): return self._ui.trust_cert_checkbutton.get_active() def get_visible_buttons(self): return ['back', self.get_toplevel().get_currenct_method()] def get_default_button(self): return 'back' class Form(Page): def __init__(self): Page.__init__(self) self.set_valign(Gtk.Align.FILL) self.complete = False self.title = _('Create Account') self._current_form = None heading = Gtk.Label(label=_('Create Account')) heading.get_style_context().add_class('large-header') heading.set_max_width_chars(30) heading.set_line_wrap(True) heading.set_halign(Gtk.Align.CENTER) heading.set_justify(Gtk.Justification.CENTER) self.pack_start(heading, False, True, 0) self.show_all() @property def has_form(self): return self._current_form is not None def _on_is_valid(self, _widget, is_valid): self.complete = is_valid self.update_page_complete() def add_form(self, form): self.remove_form() options = {'hide-fallback-fields': True, 'entry-activates-default': True} self._current_form = DataFormWidget(form, options) self._current_form.connect('is-valid', self._on_is_valid) self._current_form.validate() self.pack_start(self._current_form, True, True, 0) self._current_form.show_all() def get_credentials(self): return (self._current_form.get_form()['username'].value, self._current_form.get_form()['password'].value) def get_submit_form(self): return self._current_form.get_submit_form() def remove_form(self): if self._current_form is None: return self.remove(self._current_form) self._current_form.destroy() self._current_form = None def focus(self): self._current_form.focus_first_entry() def get_visible_buttons(self): return ['back', 'signup'] def get_default_button(self): return 'signup' class Redirect(Page): def __init__(self): Page.__init__(self) self.title = _('Redirect') self._link = None self._ui = get_builder('account_wizard.ui') self.pack_start(self._ui.redirect_box, True, True, 0) self._ui.link_button.connect('clicked', self._on_link_button) self.show_all() def set_redirect(self, link, instructions): if instructions is None: instructions = _('Register on the Website') self._ui.instructions.set_text(instructions) self._link = link def _on_link_button(self, _button): open_uri(self._link) def get_visible_buttons(self): return ['back'] class Success(SuccessPage): def __init__(self): SuccessPage.__init__(self) self.set_title(_('Account Added')) self.set_heading(_('Account has been added successfully')) self._account = None def set_account(self, account): self._account = account @property def account(self): return self._account def get_visible_buttons(self): return ['connect'] class Error(ErrorPage): def __init__(self): ErrorPage.__init__(self) self.set_heading(_('An error occurred during account creation')) def get_visible_buttons(self): return ['back']