/*
* 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 {
GtkBin 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_BIN)
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);
}