/* * Copyright (C) 2021 Purism SPC * * This file is part of Calls. * * Calls 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, either version 3 of the License, or * (at your option) any later version. * * Calls 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 Calls. If not, see . * * Author: Evangelos Ribeiro Tzaras * * SPDX-License-Identifier: GPL-3.0-or-later * */ #define G_LOG_DOMAIN "CallsSipAccountWidget" #include "calls-settings.h" #include "calls-sip-account-widget.h" #include "calls-sip-provider.h" #include "calls-sip-origin.h" #include "calls-sip-util.h" #include /** * Section:calls-sip-account-widget * short_description: A #GtkWidget to edit or add SIP accounts * @Title: CallsSipAccountWidget * * This #GtkWidget allows the user to add a new or edit an existing SIP account. */ enum { PROP_0, PROP_PROVIDER, PROP_ORIGIN, PROP_LAST_PROP }; static GParamSpec *props[PROP_LAST_PROP]; struct _CallsSipAccountWidget { GtkBox parent; /* Header bar */ GtkWidget *header_add; GtkSpinner *spinner_add; GtkWidget *header_edit; GtkSpinner *spinner_edit; GtkWidget *login_btn; GtkWidget *apply_btn; GtkWidget *delete_btn; /* widgets for editing account credentials */ GtkEntry *host; GtkEntry *display_name; GtkEntry *user; GtkEntry *password; GtkEntry *port; char *last_port; HdyComboRow *protocol; GListStore *protocols_store; /* bound model for protocol HdyComboRow */ HdyComboRow *media_encryption; GListStore *media_encryption_store; GtkSwitch *tel_switch; GtkSwitch *auto_connect_switch; /* properties */ CallsSipProvider *provider; CallsSipOrigin *origin; /* nullable to add a new account */ /* misc */ CallsSettings *settings; gboolean connecting; gboolean port_self_change; }; G_DEFINE_TYPE (CallsSipAccountWidget, calls_sip_account_widget, GTK_TYPE_BOX) static gboolean is_form_valid (CallsSipAccountWidget *self) { g_assert (CALLS_IS_SIP_ACCOUNT_WIDGET (self)); /* TODO perform some sanity checks */ return TRUE; } static gboolean is_form_filled (CallsSipAccountWidget *self) { g_assert (CALLS_IS_SIP_ACCOUNT_WIDGET (self)); return g_strcmp0 (gtk_entry_get_text (self->host), "") != 0 && g_strcmp0 (gtk_entry_get_text (self->user), "") != 0 && g_strcmp0 (gtk_entry_get_text (self->password), "") != 0 && g_strcmp0 (gtk_entry_get_text (self->port), "") != 0; } static const char * get_selected_protocol (CallsSipAccountWidget *self) { g_autoptr (HdyValueObject) obj = NULL; const char *protocol = NULL; gint i; if ((i = hdy_combo_row_get_selected_index (self->protocol)) != -1) { obj = g_list_model_get_item (G_LIST_MODEL (self->protocols_store), i); protocol = hdy_value_object_get_string (obj); } return protocol; } static SipMediaEncryption get_selected_media_encryption (CallsSipAccountWidget *self) { g_autoptr (HdyValueObject) obj = NULL; SipMediaEncryption media_encryption = SIP_MEDIA_ENCRYPTION_NONE; gint i; if ((i = hdy_combo_row_get_selected_index (self->media_encryption)) != -1) { obj = g_list_model_get_item (G_LIST_MODEL (self->media_encryption_store), i); media_encryption = (SipMediaEncryption) GPOINTER_TO_INT (g_object_get_data (G_OBJECT (obj), "value")); } return media_encryption; } static void update_media_encryption (CallsSipAccountWidget *self) { gboolean transport_is_tls; gboolean sdes_always_allowed; g_assert (CALLS_IS_SIP_ACCOUNT_WIDGET (self)); transport_is_tls = g_strcmp0 (get_selected_protocol (self), "TLS") == 0; sdes_always_allowed = calls_settings_get_always_allow_sdes (self->settings); gtk_widget_set_sensitive (GTK_WIDGET (self->media_encryption), transport_is_tls | sdes_always_allowed); if (!transport_is_tls && !sdes_always_allowed) hdy_combo_row_set_selected_index (self->media_encryption, 0); } static void on_user_changed (CallsSipAccountWidget *self) { g_assert (CALLS_IS_SIP_ACCOUNT_WIDGET (self)); gtk_widget_set_sensitive (self->login_btn, is_form_filled (self) && is_form_valid (self)); gtk_widget_set_sensitive (self->apply_btn, is_form_filled (self) && is_form_valid (self)); update_media_encryption (self); } static void set_password_visibility (CallsSipAccountWidget *self, gboolean visible) { const char *icon_name; g_assert (CALLS_IS_SIP_ACCOUNT_WIDGET (self)); g_assert (GTK_IS_ENTRY (self->password)); icon_name = visible ? "view-conceal-symbolic" : "view-reveal-symbolic"; gtk_entry_set_visibility (self->password, visible); gtk_entry_set_icon_from_icon_name (self->password, GTK_ENTRY_ICON_SECONDARY, icon_name); } static void on_password_visibility_changed (CallsSipAccountWidget *self, GtkEntryIconPosition icon_pos, GdkEvent *event, GtkEntry *entry) { gboolean visible; g_assert (CALLS_IS_SIP_ACCOUNT_WIDGET (self)); g_assert (GTK_IS_ENTRY (entry)); g_assert (icon_pos == GTK_ENTRY_ICON_SECONDARY); visible = !gtk_entry_get_visibility (entry); set_password_visibility (self, visible); } /* * Stop "insert-text" signal emission if any undesired port * value occurs */ static void on_port_entry_insert_text (CallsSipAccountWidget *self, char *new_text, int new_text_length, gpointer position, GtkEntry *entry) { size_t digit_end, len; int *pos; g_assert (CALLS_IS_SIP_ACCOUNT_WIDGET (self)); g_assert (GTK_IS_ENTRY (entry)); if (!new_text || !*new_text || self->port_self_change) return; pos = (int *) position; g_object_set_data (G_OBJECT (entry), "old-pos", GINT_TO_POINTER (*pos)); if (new_text_length == -1) len = strlen (new_text); else len = new_text_length; digit_end = strspn (new_text, "1234567890"); /* If user inserted something other than a digit, * stop inserting the text and warn the user. */ if (digit_end != len) { g_signal_stop_emission_by_name (entry, "insert-text"); gtk_widget_error_bell (GTK_WIDGET (entry)); } else { g_free (self->last_port); self->last_port = g_strdup (gtk_entry_get_text (entry)); } } static gboolean update_port_cursor_position (GtkEntry *entry) { int pos; pos = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (entry), "old-pos")); gtk_editable_set_position (GTK_EDITABLE (entry), pos); return G_SOURCE_REMOVE; } static int get_port (CallsSipAccountWidget *self) { const char *text; int port = 0; text = gtk_entry_get_text (self->port); port = (int) g_ascii_strtod (text, NULL); return port; } static void on_port_entry_after_insert_text (CallsSipAccountWidget *self, char *new_text, int new_text_length, gpointer position, GtkEntry *entry) { int port = get_port (self); /* Reset to the old value if new port number is invalid */ if ((port < 0 || port > 65535) && self->last_port) { self->port_self_change = TRUE; gtk_entry_set_text (entry, self->last_port); g_idle_add (G_SOURCE_FUNC (update_port_cursor_position), entry); gtk_widget_error_bell (GTK_WIDGET (entry)); self->port_self_change = FALSE; } } static void update_header (CallsSipAccountWidget *self) { g_assert (CALLS_IS_SIP_ACCOUNT_WIDGET (self)); if (self->origin) { gtk_widget_show (self->header_edit); gtk_widget_hide (self->header_add); } else { gtk_widget_show (self->header_add); gtk_widget_hide (self->header_edit); } if (self->connecting) { gtk_spinner_start (self->spinner_add); gtk_spinner_start (self->spinner_edit); } else { gtk_spinner_stop (self->spinner_add); gtk_spinner_stop (self->spinner_edit); } } static gboolean find_protocol (CallsSipAccountWidget *self, const char *protocol, guint *index) { guint len; g_assert (CALLS_IS_SIP_ACCOUNT_WIDGET (self)); len = g_list_model_get_n_items (G_LIST_MODEL (self->protocols_store)); for (guint i = 0; i < len; i++) { g_autoptr (HdyValueObject) obj = g_list_model_get_item (G_LIST_MODEL (self->protocols_store), i); const char *prot = hdy_value_object_get_string (obj); if (g_strcmp0 (protocol, prot) == 0) { if (index) *index = i; return TRUE; } } g_warning ("Could not find protocol '%s'", protocol); return FALSE; } static gboolean find_media_encryption (CallsSipAccountWidget *self, const SipMediaEncryption encryption, guint *index) { guint len; g_assert (CALLS_IS_SIP_ACCOUNT_WIDGET (self)); len = g_list_model_get_n_items (G_LIST_MODEL (self->media_encryption_store)); for (guint i = 0; i < len; i++) { g_autoptr (HdyValueObject) obj = g_list_model_get_item (G_LIST_MODEL (self->media_encryption_store), i); SipMediaEncryption obj_enc = (SipMediaEncryption) GPOINTER_TO_INT (g_object_get_data (G_OBJECT (obj), "value")); if (obj_enc == encryption) { if (index) *index = i; return TRUE; } } g_warning ("Could not find encryption mode %d", encryption); return FALSE; } static void clear_form (CallsSipAccountWidget *self) { g_assert (CALLS_IS_SIP_ACCOUNT_WIDGET (self)); gtk_entry_set_text (self->host, ""); gtk_entry_set_text (self->display_name, ""); gtk_entry_set_text (self->user, ""); gtk_entry_set_text (self->password, ""); gtk_entry_set_text (self->port, "0"); hdy_combo_row_set_selected_index (self->protocol, 0); gtk_widget_set_sensitive (GTK_WIDGET (self->media_encryption), FALSE); hdy_combo_row_set_selected_index (self->media_encryption, 0); gtk_switch_set_state (self->tel_switch, FALSE); gtk_switch_set_state (self->auto_connect_switch, TRUE); self->origin = NULL; update_header (self); if (gtk_widget_get_can_focus (GTK_WIDGET (self->host))) gtk_widget_grab_focus (GTK_WIDGET (self->host)); } static void edit_form (CallsSipAccountWidget *self, CallsSipOrigin *origin) { g_autofree char *host = NULL; g_autofree char *display_name = NULL; g_autofree char *user = NULL; g_autofree char *password = NULL; g_autofree char *port_str = NULL; g_autofree char *protocol = NULL; gint port; SipMediaEncryption encryption; guint encryption_index; guint protocol_index; gboolean can_tel; gboolean auto_connect; g_assert (CALLS_IS_SIP_ACCOUNT_WIDGET (self)); if (!origin) { clear_form (self); return; } g_assert (CALLS_IS_SIP_ORIGIN (origin)); self->origin = origin; g_object_get (origin, "host", &host, "display-name", &display_name, "user", &user, "password", &password, "port", &port, "transport-protocol", &protocol, "media-encryption", &encryption, "can-tel", &can_tel, "auto-connect", &auto_connect, NULL); port_str = g_strdup_printf ("%d", port); /* The following should always succeed, TODO inform user in the error case related issue #275 https://source.puri.sm/Librem5/calls/-/issues/275 */ if (!find_protocol (self, protocol, &protocol_index)) protocol_index = 0; if (!find_media_encryption (self, encryption, &encryption_index)) encryption_index = 0; /* set UI elements */ gtk_entry_set_text (self->host, host); gtk_entry_set_text (self->display_name, display_name ?: ""); gtk_entry_set_text (self->user, user); gtk_entry_set_text (self->password, password); set_password_visibility (self, FALSE); gtk_entry_set_text (self->port, port_str); hdy_combo_row_set_selected_index (self->protocol, protocol_index); hdy_combo_row_set_selected_index (self->media_encryption, encryption_index); gtk_switch_set_state (self->tel_switch, can_tel); gtk_switch_set_state (self->auto_connect_switch, auto_connect); gtk_widget_set_sensitive (self->apply_btn, FALSE); update_header (self); if (gtk_widget_get_can_focus (GTK_WIDGET (self->host))) gtk_widget_grab_focus (GTK_WIDGET (self->host)); } static void on_login_clicked (CallsSipAccountWidget *self) { CallsSipOrigin *origin; g_autofree char *id = g_uuid_string_random (); g_debug ("Logging into newly created account"); origin = calls_sip_provider_add_origin (self->provider, id, gtk_entry_get_text (GTK_ENTRY (self->host)), gtk_entry_get_text (GTK_ENTRY (self->user)), gtk_entry_get_text (GTK_ENTRY (self->password)), gtk_entry_get_text (GTK_ENTRY (self->display_name)), get_selected_protocol (self), get_port (self), get_selected_media_encryption (self), TRUE); self->origin = origin; update_header (self); g_signal_emit_by_name (self->provider, "widget-edit-done"); } static void on_delete_clicked (CallsSipAccountWidget *self) { g_debug ("Deleting account"); calls_sip_provider_remove_origin (self->provider, self->origin); self->origin = NULL; update_header (self); g_signal_emit_by_name (self->provider, "widget-edit-done"); } static void on_apply_clicked (CallsSipAccountWidget *self) { g_debug ("Applying changes to the account"); calls_sip_origin_set_credentials (self->origin, gtk_entry_get_text (self->host), gtk_entry_get_text (self->user), gtk_entry_get_text (self->password), gtk_entry_get_text (self->display_name), get_selected_protocol (self), get_port (self), get_selected_media_encryption (self), gtk_switch_get_state (self->tel_switch), gtk_switch_get_state (self->auto_connect_switch)); update_header (self); calls_sip_provider_save_accounts_to_disk (self->provider); g_signal_emit_by_name (self->provider, "widget-edit-done"); } static void calls_sip_account_widget_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { CallsSipAccountWidget *self = CALLS_SIP_ACCOUNT_WIDGET (object); switch (property_id) { case PROP_PROVIDER: self->provider = g_value_get_object (value); break; case PROP_ORIGIN: calls_sip_account_widget_set_origin (self, g_value_get_object (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void calls_sip_account_widget_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { CallsSipAccountWidget *self = CALLS_SIP_ACCOUNT_WIDGET (object); switch (property_id) { case PROP_ORIGIN: g_value_set_object (value, calls_sip_account_widget_get_origin (self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void calls_sip_account_widget_dispose (GObject *object) { CallsSipAccountWidget *self = CALLS_SIP_ACCOUNT_WIDGET (object); g_clear_pointer (&self->last_port, g_free); g_clear_object (&self->protocols_store); g_clear_object (&self->media_encryption_store); G_OBJECT_CLASS (calls_sip_account_widget_parent_class)->dispose (object); } static void calls_sip_account_widget_class_init (CallsSipAccountWidgetClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); object_class->set_property = calls_sip_account_widget_set_property; object_class->get_property = calls_sip_account_widget_get_property; object_class->dispose = calls_sip_account_widget_dispose; props[PROP_PROVIDER] = g_param_spec_object ("provider", "Provider", "The SIP provider", CALLS_TYPE_SIP_PROVIDER, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); props[PROP_ORIGIN] = g_param_spec_object ("origin", "Origin", "The origin to edit", CALLS_TYPE_SIP_ORIGIN, G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); g_object_class_install_properties (object_class, PROP_LAST_PROP, props); gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Calls/ui/sip-account-widget.ui"); gtk_widget_class_bind_template_child (widget_class, CallsSipAccountWidget, header_add); gtk_widget_class_bind_template_child (widget_class, CallsSipAccountWidget, spinner_add); gtk_widget_class_bind_template_child (widget_class, CallsSipAccountWidget, header_edit); gtk_widget_class_bind_template_child (widget_class, CallsSipAccountWidget, spinner_edit); gtk_widget_class_bind_template_child (widget_class, CallsSipAccountWidget, login_btn); gtk_widget_class_bind_template_child (widget_class, CallsSipAccountWidget, apply_btn); gtk_widget_class_bind_template_child (widget_class, CallsSipAccountWidget, host); gtk_widget_class_bind_template_child (widget_class, CallsSipAccountWidget, display_name); gtk_widget_class_bind_template_child (widget_class, CallsSipAccountWidget, user); gtk_widget_class_bind_template_child (widget_class, CallsSipAccountWidget, password); gtk_widget_class_bind_template_child (widget_class, CallsSipAccountWidget, port); gtk_widget_class_bind_template_child (widget_class, CallsSipAccountWidget, protocol); gtk_widget_class_bind_template_child (widget_class, CallsSipAccountWidget, media_encryption); gtk_widget_class_bind_template_child (widget_class, CallsSipAccountWidget, tel_switch); gtk_widget_class_bind_template_child (widget_class, CallsSipAccountWidget, auto_connect_switch); gtk_widget_class_bind_template_callback (widget_class, on_login_clicked); gtk_widget_class_bind_template_callback (widget_class, on_delete_clicked); gtk_widget_class_bind_template_callback (widget_class, on_apply_clicked); gtk_widget_class_bind_template_callback (widget_class, on_user_changed); gtk_widget_class_bind_template_callback (widget_class, on_password_visibility_changed); gtk_widget_class_bind_template_callback (widget_class, on_port_entry_insert_text); gtk_widget_class_bind_template_callback (widget_class, on_port_entry_after_insert_text); } static void calls_sip_account_widget_init (CallsSipAccountWidget *self) { HdyValueObject *obj; self->settings = calls_settings_get_default (); g_signal_connect_swapped (self->settings, "notify::always-allow-sdes", G_CALLBACK (update_media_encryption), self); gtk_widget_init_template (GTK_WIDGET (self)); self->media_encryption_store = g_list_store_new (HDY_TYPE_VALUE_OBJECT); obj = hdy_value_object_new_string (_("No encryption")); g_object_set_data (G_OBJECT (obj), "value", GINT_TO_POINTER (SIP_MEDIA_ENCRYPTION_NONE)); g_list_store_insert (self->media_encryption_store, 0, obj); g_clear_object (&obj); /* TODO Optional encryption */ obj = hdy_value_object_new_string (_("Force encryption")); g_object_set_data (G_OBJECT (obj), "value", GINT_TO_POINTER (SIP_MEDIA_ENCRYPTION_FORCED)); g_list_store_insert (self->media_encryption_store, 1, obj); g_clear_object (&obj); hdy_combo_row_bind_name_model (self->media_encryption, G_LIST_MODEL (self->media_encryption_store), (HdyComboRowGetNameFunc) hdy_value_object_dup_string, NULL, NULL); self->protocols_store = g_list_store_new (HDY_TYPE_VALUE_OBJECT); obj = hdy_value_object_new_string ("UDP"); g_list_store_insert (self->protocols_store, 0, obj); g_clear_object (&obj); obj = hdy_value_object_new_string ("TCP"); g_list_store_insert (self->protocols_store, 1, obj); g_clear_object (&obj); obj = hdy_value_object_new_string ("TLS"); g_list_store_insert (self->protocols_store, 2, obj); g_clear_object (&obj); hdy_combo_row_bind_name_model (self->protocol, G_LIST_MODEL (self->protocols_store), (HdyComboRowGetNameFunc) hdy_value_object_dup_string, NULL, NULL); } CallsSipAccountWidget * calls_sip_account_widget_new (CallsSipProvider *provider) { g_return_val_if_fail (CALLS_IS_SIP_PROVIDER (provider), NULL); return g_object_new (CALLS_TYPE_SIP_ACCOUNT_WIDGET, "provider", provider, NULL); } CallsSipOrigin * calls_sip_account_widget_get_origin (CallsSipAccountWidget *self) { g_return_val_if_fail (CALLS_IS_SIP_ACCOUNT_WIDGET (self), NULL); return self->origin; } void calls_sip_account_widget_set_origin (CallsSipAccountWidget *self, CallsSipOrigin *origin) { g_return_if_fail (CALLS_IS_SIP_ACCOUNT_WIDGET (self)); g_return_if_fail (!origin || CALLS_IS_SIP_ORIGIN (origin)); edit_form (self, origin); }