diff options
Diffstat (limited to '')
97 files changed, 13700 insertions, 0 deletions
diff --git a/panels/user-accounts/cc-add-user-dialog.c b/panels/user-accounts/cc-add-user-dialog.c new file mode 100644 index 0000000..e462015 --- /dev/null +++ b/panels/user-accounts/cc-add-user-dialog.c @@ -0,0 +1,1717 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright 2009-2010 Red Hat, Inc, + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see <http://www.gnu.org/licenses/>. + * + * Written by: Matthias Clasen <mclasen@redhat.com> + */ + +#include "config.h" + +#include <adwaita.h> +#include <glib.h> +#include <glib/gi18n.h> +#include <gtk/gtk.h> +#include <act/act.h> + +#include "cc-add-user-dialog.h" +#include "cc-realm-manager.h" +#include "user-utils.h" +#include "pw-utils.h" + +#define PASSWORD_CHECK_TIMEOUT 600 +#define DOMAIN_DEFAULT_HINT _("Should match the web address of your login provider.") + +typedef enum { + MODE_LOCAL, + MODE_ENTERPRISE, + MODE_OFFLINE +} AccountMode; + +static void mode_change (CcAddUserDialog *self, + AccountMode mode); + +static void dialog_validate (CcAddUserDialog *self); + +static void on_join_login (GObject *source, + GAsyncResult *result, + gpointer user_data); + +static void on_realm_joined (GObject *source, + GAsyncResult *result, + gpointer user_data); + +static void add_button_clicked_cb (CcAddUserDialog *self); + +struct _CcAddUserDialog { + GtkDialog parent_instance; + + GtkButton *add_button; + AdwActionRow *enterprise_button; + GtkComboBox *enterprise_domain_combo; + GtkEntry *enterprise_domain_entry; + GtkLabel *enterprise_domain_hint; + AdwActionRow *enterprise_domain_row; + GtkImage *enterprise_domain_status_icon; + AdwPreferencesGroup *enterprise_group; + AdwPreferencesPage *enterprise_page; + AdwPreferencesGroup *enterprise_login_group; + GtkEntry *enterprise_login_entry; + GtkImage *enterprise_login_status_icon; + GtkPasswordEntry *enterprise_password_entry; + GtkImage *enterprise_password_status_icon; + GtkListStore *enterprise_realm_model; + GtkSwitch *local_account_type_switch; + GtkEntry *local_name_entry; + GtkImage *local_name_status_icon; + AdwPreferencesPage *local_page; + AdwActionRow *local_password_row; + GtkImage *local_password_status_icon; + GtkLevelBar *local_strength_indicator; + GtkComboBoxText *local_username_combo; + GtkListStore *local_username_model; + GtkPasswordEntry *local_password_entry; + GtkLabel *local_password_hint; + GtkCheckButton *local_password_radio; + GtkEntry *local_username_entry; + AdwActionRow *local_username_row; + GtkImage *local_username_status_icon; + GtkPasswordEntry *local_verify_entry; + AdwActionRow *local_verify_password_row; + GtkImage *local_verify_status_icon; + AdwPreferencesPage *offline_page; + AdwPreferencesGroup *password_group; + GtkSpinner *spinner; + GtkStack *stack; + + GCancellable *cancellable; + GPermission *permission; + AccountMode mode; + ActUser *user; + + gboolean has_custom_username; + gint local_name_timeout_id; + gint local_username_timeout_id; + ActUserPasswordMode local_password_mode; + gint local_password_timeout_id; + gboolean local_valid_username; + + guint realmd_watch; + CcRealmManager *realm_manager; + CcRealmObject *selected_realm; + gboolean enterprise_check_credentials; + gint enterprise_domain_timeout_id; + gboolean enterprise_domain_chosen; + + /* Join credential dialog */ + GtkDialog *join_dialog; + GtkLabel *join_domain; + GtkEntry *join_name; + GtkEntry *join_password; + gboolean join_prompted; +}; + +G_DEFINE_TYPE (CcAddUserDialog, cc_add_user_dialog, GTK_TYPE_DIALOG); + +static void +show_error_dialog (CcAddUserDialog *self, + const gchar *message, + GError *error) +{ + GtkWidget *dialog; + + dialog = gtk_message_dialog_new (GTK_WINDOW (self), + GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_USE_HEADER_BAR, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_CLOSE, + "%s", message); + + if (error != NULL) { + g_dbus_error_strip_remote_error (error); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + "%s", error->message); + } + + g_signal_connect (dialog, "response", G_CALLBACK (gtk_window_destroy), NULL); + gtk_window_present (GTK_WINDOW (dialog)); +} + +static void +begin_action (CcAddUserDialog *self) +{ + g_debug ("Beginning action, disabling dialog controls"); + + if (self->enterprise_check_credentials) { + gtk_widget_set_sensitive (GTK_WIDGET (self->stack), FALSE); + } + gtk_widget_set_sensitive (GTK_WIDGET (self->enterprise_button), FALSE); + gtk_widget_set_sensitive (GTK_WIDGET (self->add_button), FALSE); + + gtk_widget_show (GTK_WIDGET (self->spinner)); + gtk_spinner_start (self->spinner); +} + +static void +finish_action (CcAddUserDialog *self) +{ + g_debug ("Completed domain action"); + + if (self->enterprise_check_credentials) { + gtk_widget_set_sensitive (GTK_WIDGET (self->stack), TRUE); + } + gtk_widget_set_sensitive (GTK_WIDGET (self->enterprise_button), TRUE); + gtk_widget_set_sensitive (GTK_WIDGET (self->add_button), TRUE); + + gtk_widget_hide (GTK_WIDGET (self->spinner)); + gtk_spinner_stop (self->spinner); +} + +static void +user_loaded_cb (CcAddUserDialog *self, + GParamSpec *pspec, + ActUser *user) +{ + const gchar *password; + + finish_action (self); + + /* Set a password for the user */ + password = gtk_editable_get_text (GTK_EDITABLE (self->local_password_entry)); + act_user_set_password_mode (user, self->local_password_mode); + if (self->local_password_mode == ACT_USER_PASSWORD_MODE_REGULAR) + act_user_set_password (user, password, ""); + + self->user = g_object_ref (user); + gtk_dialog_response (GTK_DIALOG (self), GTK_RESPONSE_CLOSE); +} + +static void +create_user_done (ActUserManager *manager, + GAsyncResult *res, + CcAddUserDialog *self) +{ + ActUser *user; + g_autoptr(GError) error = NULL; + + /* Note that user is returned without an extra reference */ + + user = act_user_manager_create_user_finish (manager, res, &error); + + if (user == NULL) { + finish_action (self); + g_debug ("Failed to create user: %s", error->message); + if (!g_error_matches (error, ACT_USER_MANAGER_ERROR, ACT_USER_MANAGER_ERROR_PERMISSION_DENIED)) + show_error_dialog (self, _("Failed to add account"), error); + gtk_widget_grab_focus (GTK_WIDGET (self->local_name_entry)); + } else { + g_debug ("Created user: %s", act_user_get_user_name (user)); + + /* Check if the returned object is fully loaded before returning it */ + if (act_user_is_loaded (user)) + user_loaded_cb (self, NULL, user); + else + g_signal_connect_object (user, "notify::is-loaded", G_CALLBACK (user_loaded_cb), self, G_CONNECT_SWAPPED); + } +} + +static void +local_create_user (CcAddUserDialog *self) +{ + ActUserManager *manager; + const gchar *username; + const gchar *name; + gint account_type; + + begin_action (self); + + name = gtk_editable_get_text (GTK_EDITABLE (self->local_name_entry)); + username = gtk_combo_box_text_get_active_text (self->local_username_combo); + account_type = gtk_switch_get_active (self->local_account_type_switch) ? ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR : ACT_USER_ACCOUNT_TYPE_STANDARD; + + g_debug ("Creating local user: %s", username); + + manager = act_user_manager_get_default (); + act_user_manager_create_user_async (manager, + username, + name, + account_type, + self->cancellable, + (GAsyncReadyCallback)create_user_done, + self); +} + +static gint +update_password_strength (CcAddUserDialog *self) +{ + const gchar *password; + const gchar *username; + const gchar *hint; + const gchar *verify; + gint strength_level; + + password = gtk_editable_get_text (GTK_EDITABLE (self->local_password_entry)); + username = gtk_combo_box_text_get_active_text (self->local_username_combo); + + pw_strength (password, NULL, username, &hint, &strength_level); + + gtk_level_bar_set_value (self->local_strength_indicator, strength_level); + gtk_label_set_label (self->local_password_hint, hint); + + if (strength_level > 1) { + gtk_image_set_from_icon_name (self->local_password_status_icon, "emblem-ok-symbolic"); + } else if (strlen (password) == 0) { + gtk_image_set_from_icon_name (self->local_password_status_icon, "dialog-warning-symbolic"); + } else { + gtk_image_set_from_icon_name (self->local_password_status_icon, "dialog-warning-symbolic"); + } + + verify = gtk_editable_get_text (GTK_EDITABLE (self->local_verify_entry)); + if (strlen (verify) == 0) { + gtk_widget_set_sensitive (GTK_WIDGET (self->local_verify_entry), strength_level > 1); + } + + return strength_level; +} + +static gboolean +local_validate (CcAddUserDialog *self) +{ + gboolean valid_name; + gboolean valid_password; + const gchar *name; + const gchar *password; + const gchar *verify; + gint strength; + + if (self->local_valid_username) { + gtk_image_set_from_icon_name (self->local_username_status_icon, "emblem-ok-symbolic"); + } + + name = gtk_editable_get_text (GTK_EDITABLE (self->local_name_entry)); + valid_name = is_valid_name (name); + if (valid_name) { + gtk_image_set_from_icon_name (self->local_name_status_icon, "emblem-ok-symbolic"); + } + + password = gtk_editable_get_text (GTK_EDITABLE (self->local_password_entry)); + verify = gtk_editable_get_text (GTK_EDITABLE (self->local_verify_entry)); + if (self->local_password_mode == ACT_USER_PASSWORD_MODE_REGULAR) { + strength = update_password_strength (self); + valid_password = strength > 1 && strcmp (password, verify) == 0; + } else { + valid_password = TRUE; + } + + return valid_name && self->local_valid_username && valid_password; +} + +static void local_username_is_valid_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(CcAddUserDialog) self = CC_ADD_USER_DIALOG (user_data); + g_autoptr(GError) error = NULL; + g_autofree gchar *tip = NULL; + g_autofree gchar *name = NULL; + g_autofree gchar *username = NULL; + gboolean valid; + + valid = is_valid_username_finish (result, &tip, &username, &error); + if (error != NULL) { + g_warning ("Could not check username by usermod: %s", error->message); + valid = TRUE; + } + + name = gtk_combo_box_text_get_active_text (self->local_username_combo); + if (g_strcmp0 (name, username) == 0) { + self->local_valid_username = valid; + adw_action_row_set_subtitle (ADW_ACTION_ROW (self->local_username_row), tip); + dialog_validate (self); + } +} + +static gboolean +local_username_timeout (CcAddUserDialog *self) +{ + g_autofree gchar *name = NULL; + + self->local_username_timeout_id = 0; + + name = gtk_combo_box_text_get_active_text (self->local_username_combo); + is_valid_username_async (name, NULL, local_username_is_valid_cb, g_object_ref (self)); + + return FALSE; +} + +static gboolean +local_username_combo_focus_out_event_cb (CcAddUserDialog *self) +{ + if (self->local_username_timeout_id != 0) { + g_source_remove (self->local_username_timeout_id); + self->local_username_timeout_id = 0; + } + + local_username_timeout (self); + + return FALSE; +} + +static void +local_username_combo_changed_cb (CcAddUserDialog *self) +{ + const gchar *username; + + username = gtk_editable_get_text (GTK_EDITABLE (self->local_username_entry)); + if (*username == '\0') + self->has_custom_username = FALSE; + else if (gtk_widget_has_focus (GTK_WIDGET (self->local_username_entry)) || + gtk_combo_box_get_active (GTK_COMBO_BOX (self->local_username_combo)) > 0) + self->has_custom_username = TRUE; + + if (self->local_username_timeout_id != 0) { + g_source_remove (self->local_username_timeout_id); + self->local_username_timeout_id = 0; + } + + gtk_image_set_from_icon_name (self->local_username_status_icon, "dialog-warning-symbolic"); + gtk_widget_set_sensitive (GTK_WIDGET (self->add_button), FALSE); + + self->local_valid_username = FALSE; + self->local_username_timeout_id = g_timeout_add (PASSWORD_CHECK_TIMEOUT, (GSourceFunc) local_username_timeout, self); +} + +static gboolean +local_name_timeout (CcAddUserDialog *self) +{ + self->local_name_timeout_id = 0; + + dialog_validate (self); + + return FALSE; +} + +static gboolean +local_name_entry_focus_out_event_cb (CcAddUserDialog *self) +{ + if (self->local_name_timeout_id != 0) { + g_source_remove (self->local_name_timeout_id); + self->local_name_timeout_id = 0; + } + + local_name_timeout (self); + + return FALSE; +} + +static void +generate_username_choices (const gchar *name, + GtkListStore *store) +{ + gboolean in_use, same_as_initial; + g_autofree gchar *lc_name = NULL; + g_autofree gchar *ascii_name = NULL; + g_autofree gchar *stripped_name = NULL; + g_auto(GStrv) words1 = NULL; + char **w1, **w2; + char *c; + char *unicode_fallback = "?"; + g_autoptr(GString) first_word = NULL; + g_autoptr(GString) last_word = NULL; + g_autoptr(GString) item0 = NULL; + g_autoptr(GString) item1 = NULL; + g_autoptr(GString) item2 = NULL; + g_autoptr(GString) item3 = NULL; + g_autoptr(GString) item4 = NULL; + int len; + int nwords1, nwords2, i; + g_autoptr(GHashTable) items = NULL; + GtkTreeIter iter; + gsize max_name_length; + + gtk_list_store_clear (store); + + ascii_name = g_convert_with_fallback (name, -1, "ASCII//TRANSLIT", "UTF-8", + unicode_fallback, NULL, NULL, NULL); + /* Re-try without TRANSLIT. musl does not implement it */ + if (ascii_name == NULL) + ascii_name = g_convert_with_fallback (name, -1, "ASCII", "UTF-8", + unicode_fallback, NULL, NULL, NULL); + if (ascii_name == NULL) + return; + + lc_name = g_ascii_strdown (ascii_name, -1); + + /* Remove all non ASCII alphanumeric chars from the name, + * apart from the few allowed symbols. + * + * We do remove '.', even though it is usually allowed, + * since it often comes in via an abbreviated middle name, + * and the dot looks just wrong in the proposals then. + */ + stripped_name = g_strnfill (strlen (lc_name) + 1, '\0'); + i = 0; + for (c = lc_name; *c; c++) { + if (!(g_ascii_isdigit (*c) || g_ascii_islower (*c) || + *c == ' ' || *c == '-' || *c == '_' || + /* used to track invalid words, removed below */ + *c == '?') ) + continue; + + stripped_name[i] = *c; + i++; + } + + if (strlen (stripped_name) == 0) { + return; + } + + /* we split name on spaces, and then on dashes, so that we can treat + * words linked with dashes the same way, i.e. both fully shown, or + * both abbreviated + */ + words1 = g_strsplit_set (stripped_name, " ", -1); + len = g_strv_length (words1); + + /* The default item is a concatenation of all words without ? */ + item0 = g_string_sized_new (strlen (stripped_name)); + + /* Concatenate the whole first word with the first letter of each + * word (item1), and the last word with the first letter of each + * word (item2). item3 and item4 are symmetrical respectively to + * item1 and item2. + * + * Constant 5 is the max reasonable number of words we may get when + * splitting on dashes, since we can't guess it at this point, + * and reallocating would be too bad. + */ + item1 = g_string_sized_new (strlen (words1[0]) + len - 1 + 5); + item3 = g_string_sized_new (strlen (words1[0]) + len - 1 + 5); + + item2 = g_string_sized_new (strlen (words1[len - 1]) + len - 1 + 5); + item4 = g_string_sized_new (strlen (words1[len - 1]) + len - 1 + 5); + + /* again, guess at the max size of names */ + first_word = g_string_sized_new (20); + last_word = g_string_sized_new (20); + + nwords1 = 0; + nwords2 = 0; + for (w1 = words1; *w1; w1++) { + g_auto(GStrv) words2 = NULL; + + if (strlen (*w1) == 0) + continue; + + /* skip words with string '?', most likely resulting + * from failed transliteration to ASCII + */ + if (strstr (*w1, unicode_fallback) != NULL) + continue; + + nwords1++; /* count real words, excluding empty string */ + + item0 = g_string_append (item0, *w1); + + words2 = g_strsplit_set (*w1, "-", -1); + /* reset last word if a new non-empty word has been found */ + if (strlen (*words2) > 0) + last_word = g_string_set_size (last_word, 0); + + for (w2 = words2; *w2; w2++) { + if (strlen (*w2) == 0) + continue; + + nwords2++; + + /* part of the first "toplevel" real word */ + if (nwords1 == 1) { + item1 = g_string_append (item1, *w2); + first_word = g_string_append (first_word, *w2); + } + else { + item1 = g_string_append_unichar (item1, + g_utf8_get_char (*w2)); + item3 = g_string_append_unichar (item3, + g_utf8_get_char (*w2)); + } + + /* not part of the last "toplevel" word */ + if (w1 != words1 + len - 1) { + item2 = g_string_append_unichar (item2, + g_utf8_get_char (*w2)); + item4 = g_string_append_unichar (item4, + g_utf8_get_char (*w2)); + } + + /* always save current word so that we have it if last one reveals empty */ + last_word = g_string_append (last_word, *w2); + } + } + item2 = g_string_append (item2, last_word->str); + item3 = g_string_append (item3, first_word->str); + item4 = g_string_prepend (item4, last_word->str); + + max_name_length = get_username_max_length (); + + g_string_truncate (first_word, max_name_length); + g_string_truncate (last_word, max_name_length); + + g_string_truncate (item0, max_name_length); + g_string_truncate (item1, max_name_length); + g_string_truncate (item2, max_name_length); + g_string_truncate (item3, max_name_length); + g_string_truncate (item4, max_name_length); + + items = g_hash_table_new (g_str_hash, g_str_equal); + + in_use = is_username_used (item0->str); + if (!in_use && !g_ascii_isdigit (item0->str[0])) { + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, 0, item0->str, -1); + g_hash_table_insert (items, item0->str, item0->str); + } + + in_use = is_username_used (item1->str); + same_as_initial = (g_strcmp0 (item0->str, item1->str) == 0); + if (!same_as_initial && nwords2 > 0 && !in_use && !g_ascii_isdigit (item1->str[0])) { + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, 0, item1->str, -1); + g_hash_table_insert (items, item1->str, item1->str); + } + + /* if there's only one word, would be the same as item1 */ + if (nwords2 > 1) { + /* add other items */ + in_use = is_username_used (item2->str); + if (!in_use && !g_ascii_isdigit (item2->str[0]) && + !g_hash_table_lookup (items, item2->str)) { + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, 0, item2->str, -1); + g_hash_table_insert (items, item2->str, item2->str); + } + + in_use = is_username_used (item3->str); + if (!in_use && !g_ascii_isdigit (item3->str[0]) && + !g_hash_table_lookup (items, item3->str)) { + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, 0, item3->str, -1); + g_hash_table_insert (items, item3->str, item3->str); + } + + in_use = is_username_used (item4->str); + if (!in_use && !g_ascii_isdigit (item4->str[0]) && + !g_hash_table_lookup (items, item4->str)) { + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, 0, item4->str, -1); + g_hash_table_insert (items, item4->str, item4->str); + } + + /* add the last word */ + in_use = is_username_used (last_word->str); + if (!in_use && !g_ascii_isdigit (last_word->str[0]) && + !g_hash_table_lookup (items, last_word->str)) { + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, 0, last_word->str, -1); + g_hash_table_insert (items, last_word->str, last_word->str); + } + + /* ...and the first one */ + in_use = is_username_used (first_word->str); + if (!in_use && !g_ascii_isdigit (first_word->str[0]) && + !g_hash_table_lookup (items, first_word->str)) { + gtk_list_store_append (store, &iter); + gtk_list_store_set (store, &iter, 0, first_word->str, -1); + g_hash_table_insert (items, first_word->str, first_word->str); + } + } +} + +static void +local_name_entry_changed_cb (CcAddUserDialog *self) +{ + const char *name; + + gtk_list_store_clear (self->local_username_model); + + name = gtk_editable_get_text (GTK_EDITABLE (self->local_name_entry)); + if ((name == NULL || strlen (name) == 0) && !self->has_custom_username) { + gtk_editable_set_text (GTK_EDITABLE (self->local_username_entry), ""); + } else if (name != NULL && strlen (name) != 0) { + generate_username_choices (name, self->local_username_model); + if (!self->has_custom_username) + gtk_combo_box_set_active (GTK_COMBO_BOX (self->local_username_combo), 0); + } + + if (self->local_name_timeout_id != 0) { + g_source_remove (self->local_name_timeout_id); + self->local_name_timeout_id = 0; + } + + gtk_image_set_from_icon_name (self->local_name_status_icon, "dialog-warning-symbolic"); + gtk_widget_set_sensitive (GTK_WIDGET (self->add_button), FALSE); + + self->local_name_timeout_id = g_timeout_add (PASSWORD_CHECK_TIMEOUT, (GSourceFunc) local_name_timeout, self); +} + +static void +update_password_match (CcAddUserDialog *self) +{ + const gchar *password; + const gchar *verify; + const gchar *message = ""; + + password = gtk_editable_get_text (GTK_EDITABLE (self->local_password_entry)); + verify = gtk_editable_get_text (GTK_EDITABLE (self->local_verify_entry)); + if (strlen (verify) != 0) { + if (strcmp (password, verify) != 0) { + message = _("The passwords do not match."); + } else { + gtk_image_set_from_icon_name (self->local_verify_status_icon, "emblem-ok-symbolic"); + } + } + adw_action_row_set_subtitle (ADW_ACTION_ROW (self->local_verify_password_row), message); +} + +static void +generate_password (CcAddUserDialog *self) +{ + g_autofree gchar *pwd = NULL; + + pwd = pw_generate (); + if (pwd == NULL) + return; + + gtk_editable_set_text (GTK_EDITABLE (self->local_password_entry), pwd); + gtk_editable_set_text (GTK_EDITABLE (self->local_verify_entry), pwd); + gtk_widget_set_sensitive (GTK_WIDGET (self->local_verify_entry), TRUE); +} + +static gboolean +local_password_timeout (CcAddUserDialog *self) +{ + self->local_password_timeout_id = 0; + + dialog_validate (self); + update_password_match (self); + + return FALSE; +} + +static gboolean +password_focus_out_event_cb (CcAddUserDialog *self) +{ + if (self->local_password_timeout_id != 0) { + g_source_remove (self->local_password_timeout_id); + self->local_password_timeout_id = 0; + } + + local_password_timeout (self); + + return FALSE; +} + +static gboolean +local_password_entry_key_press_event_cb (GtkEventControllerKey *controller, + guint keyval, + guint keycode, + GdkModifierType state, + CcAddUserDialog *self) +{ + if (keyval == GDK_KEY_Tab) + local_password_timeout (self); + + return FALSE; +} + +static void +recheck_password_match (CcAddUserDialog *self) +{ + if (self->local_password_timeout_id != 0) { + g_source_remove (self->local_password_timeout_id); + self->local_password_timeout_id = 0; + } + + gtk_widget_set_sensitive (GTK_WIDGET (self->add_button), FALSE); + + self->local_password_timeout_id = g_timeout_add (PASSWORD_CHECK_TIMEOUT, (GSourceFunc) local_password_timeout, self); +} + +static void +local_password_entry_changed_cb (CcAddUserDialog *self) +{ + gtk_image_set_from_icon_name (self->local_password_status_icon, "dialog-warning-symbolic"); + gtk_image_set_from_icon_name (self->local_verify_status_icon, "dialog-warning-symbolic"); + recheck_password_match (self); +} + +static void +local_verify_entry_changed_cb (CcAddUserDialog *self) +{ + gtk_image_set_from_icon_name (self->local_verify_status_icon, "dialog-warning-symbolic"); + recheck_password_match (self); +} + +static void +local_password_radio_changed_cb (CcAddUserDialog *self) +{ + gboolean active; + + active = gtk_check_button_get_active (GTK_CHECK_BUTTON (self->local_password_radio)); + self->local_password_mode = active ? ACT_USER_PASSWORD_MODE_REGULAR : ACT_USER_PASSWORD_MODE_SET_AT_LOGIN; + + gtk_widget_set_sensitive (GTK_WIDGET (self->password_group), active); + + dialog_validate (self); +} + +static gboolean +enterprise_validate (CcAddUserDialog *self) +{ + const gchar *name; + gboolean valid_name; + gboolean valid_domain; + GtkTreeIter iter; + + name = gtk_editable_get_text (GTK_EDITABLE (self->enterprise_login_entry)); + valid_name = is_valid_name (name); + + if (gtk_combo_box_get_active_iter (self->enterprise_domain_combo, &iter)) { + gtk_tree_model_get (GTK_TREE_MODEL (self->enterprise_realm_model), + &iter, 0, &name, -1); + } else { + name = gtk_editable_get_text (GTK_EDITABLE (self->enterprise_domain_entry)); + } + + valid_domain = is_valid_name (name) && self->selected_realm != NULL; + return valid_name && valid_domain; +} + +static void +enterprise_add_realm (CcAddUserDialog *self, + CcRealmObject *realm) +{ + GtkTreeModel *model; + GtkTreeIter iter; + g_autoptr(CcRealmCommon) common = NULL; + const gchar *realm_name; + gboolean match; + gboolean ret; + + common = cc_realm_object_get_common (realm); + g_return_if_fail (common != NULL); + + realm_name = cc_realm_common_get_name (common); + + /* + * Don't add a second realm if we already have one with this name. + * Sometimes realmd returns to realms for the same name, if it has + * different ways to use that realm. The first one that realmd + * returns is the one it prefers. + */ + + model = GTK_TREE_MODEL (self->enterprise_realm_model); + ret = gtk_tree_model_get_iter_first (model, &iter); + while (ret) { + g_autofree gchar *name = NULL; + + gtk_tree_model_get (model, &iter, 0, &name, -1); + match = (g_strcmp0 (name, realm_name) == 0); + if (match) { + g_debug ("ignoring duplicate realm: %s", realm_name); + return; + } + ret = gtk_tree_model_iter_next (model, &iter); + } + + gtk_list_store_append (self->enterprise_realm_model, &iter); + gtk_list_store_set (self->enterprise_realm_model, &iter, + 0, realm_name, + 1, realm, + -1); + + /* Prefill domain entry by the existing one */ + if (!self->enterprise_domain_chosen && cc_realm_is_configured (realm)) { + gtk_editable_set_text (GTK_EDITABLE (self->enterprise_domain_entry), realm_name); + } + + g_debug ("added realm to drop down: %s %s", realm_name, + g_dbus_object_get_object_path (G_DBUS_OBJECT (realm))); +} + +static void +on_manager_realm_added (CcAddUserDialog *self, + CcRealmObject *realm) +{ + enterprise_add_realm (self, realm); +} + + +static void +on_register_user (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(CcAddUserDialog) self = CC_ADD_USER_DIALOG (user_data); + g_autoptr(GError) error = NULL; + ActUser *user; + + if (g_cancellable_is_cancelled (self->cancellable)) { + return; + } + + user = act_user_manager_cache_user_finish (ACT_USER_MANAGER (source), result, &error); + + /* This is where we're finally done */ + if (user != NULL) { + g_debug ("Successfully cached remote user: %s", act_user_get_user_name (user)); + finish_action (self); + self->user = g_object_ref (user); + gtk_dialog_response (GTK_DIALOG (self), GTK_RESPONSE_CLOSE); + } else { + show_error_dialog (self, _("Failed to register account"), error); + g_message ("Couldn't cache user account: %s", error->message); + finish_action (self); + } +} + +static void +on_permit_user_login (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(CcAddUserDialog) self = CC_ADD_USER_DIALOG (user_data); + CcRealmCommon *common; + ActUserManager *manager; + g_autoptr(GError) error = NULL; + + if (g_cancellable_is_cancelled (self->cancellable)) { + return; + } + + common = CC_REALM_COMMON (source); + if (cc_realm_common_call_change_login_policy_finish (common, result, &error)) { + g_autofree gchar *login = NULL; + + /* + * Now tell the account service about this user. The account service + * should also lookup information about this via the realm and make + * sure all that is functional. + */ + manager = act_user_manager_get_default (); + login = cc_realm_calculate_login (common, gtk_editable_get_text (GTK_EDITABLE (self->enterprise_login_entry))); + g_return_if_fail (login != NULL); + + g_debug ("Caching remote user: %s", login); + + act_user_manager_cache_user_async (manager, login, self->cancellable, + on_register_user, g_object_ref (self)); + + } else { + show_error_dialog (self, _("Failed to register account"), error); + g_message ("Couldn't permit logins on account: %s", error->message); + finish_action (self); + } +} + +static void +enterprise_permit_user_login (CcAddUserDialog *self) +{ + g_autoptr(CcRealmCommon) common = NULL; + g_autofree gchar *login = NULL; + const gchar *add[2]; + const gchar *remove[1]; + GVariant *options; + + common = cc_realm_object_get_common (self->selected_realm); + if (common == NULL) { + g_debug ("Failed to register account: failed to get d-bus interface"); + show_error_dialog (self, _("Failed to register account"), NULL); + finish_action (self); + return; + } + + login = cc_realm_calculate_login (common, gtk_editable_get_text (GTK_EDITABLE (self->enterprise_login_entry))); + g_return_if_fail (login != NULL); + + add[0] = login; + add[1] = NULL; + remove[0] = NULL; + + g_debug ("Permitting login for: %s", login); + options = g_variant_new_array (G_VARIANT_TYPE ("{sv}"), NULL, 0); + + cc_realm_common_call_change_login_policy (common, "", + add, remove, options, + self->cancellable, + on_permit_user_login, + g_object_ref (self)); +} + +static void +on_join_response (CcAddUserDialog *self, + gint response, + GtkDialog *dialog) +{ + gtk_widget_hide (GTK_WIDGET (dialog)); + if (response != GTK_RESPONSE_OK) { + finish_action (self); + return; + } + + g_debug ("Logging in as admin user: %s", gtk_editable_get_text (GTK_EDITABLE (self->join_name))); + + /* Prompted for some admin credentials, try to use them to log in */ + cc_realm_login (self->selected_realm, + gtk_editable_get_text (GTK_EDITABLE (self->join_name)), + gtk_editable_get_text (GTK_EDITABLE (self->join_password)), + self->cancellable, + on_join_login, + g_object_ref (self)); +} + +static void +join_show_prompt (CcAddUserDialog *self, + GError *error) +{ + g_autoptr(CcRealmKerberosMembership) membership = NULL; + g_autoptr(CcRealmKerberos) kerberos = NULL; + const gchar *name; + + gtk_editable_set_text (GTK_EDITABLE (self->join_password), ""); + gtk_widget_grab_focus (GTK_WIDGET (self->join_password)); + + kerberos = cc_realm_object_get_kerberos (self->selected_realm); + membership = cc_realm_object_get_kerberos_membership (self->selected_realm); + + gtk_label_set_text (self->join_domain, + cc_realm_kerberos_get_domain_name (kerberos)); + + //clear_entry_validation_error (self->join_name); + //clear_entry_validation_error (self->join_password); + + if (!self->join_prompted) { + name = cc_realm_kerberos_membership_get_suggested_administrator (membership); + if (name && !g_str_equal (name, "")) { + g_debug ("Suggesting admin user: %s", name); + gtk_editable_set_text (GTK_EDITABLE (self->join_name), name); + } else { + gtk_widget_grab_focus (GTK_WIDGET (self->join_name)); + } + + } else if (g_error_matches (error, CC_REALM_ERROR, CC_REALM_ERROR_BAD_PASSWORD)) { + g_debug ("Bad admin password: %s", error->message); + //set_entry_validation_error (self->join_password, error->message); + + } else { + g_debug ("Admin login failure: %s", error->message); + g_dbus_error_strip_remote_error (error); + //set_entry_validation_error (self->join_name, error->message); + } + + g_debug ("Showing admin password dialog"); + gtk_window_set_transient_for (GTK_WINDOW (self->join_dialog), GTK_WINDOW (self)); + gtk_window_set_modal (GTK_WINDOW (self->join_dialog), TRUE); + gtk_window_present (GTK_WINDOW (self->join_dialog)); + + self->join_prompted = TRUE; + + /* And now we wait for on_join_response() */ +} + +static void +on_join_login (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(CcAddUserDialog) self = CC_ADD_USER_DIALOG (user_data); + g_autoptr(GError) error = NULL; + g_autoptr(GBytes) creds = NULL; + + if (g_cancellable_is_cancelled (self->cancellable)) { + return; + } + + creds = cc_realm_login_finish (result, &error); + + /* Logged in as admin successfully, use creds to join domain */ + if (creds != NULL) { + if (!cc_realm_join_as_admin (self->selected_realm, + gtk_editable_get_text (GTK_EDITABLE (self->join_name)), + gtk_editable_get_text (GTK_EDITABLE (self->join_password)), + creds, self->cancellable, on_realm_joined, + g_object_ref (self))) { + show_error_dialog (self, _("No supported way to authenticate with this domain"), NULL); + g_message ("Authenticating as admin is not supported by the realm"); + finish_action (self); + } + + /* Couldn't login as admin, show prompt again */ + } else { + join_show_prompt (self, error); + g_message ("Couldn't log in as admin to join domain: %s", error->message); + } +} + +static void +join_init (CcAddUserDialog *self) +{ + g_autoptr(GtkBuilder) builder = NULL; + g_autoptr(GError) error = NULL; + + builder = gtk_builder_new (); + + if (!gtk_builder_add_from_resource (builder, + "/org/gnome/control-center/user-accounts/join-dialog.ui", + &error)) { + g_error ("%s", error->message); + return; + } + + self->join_dialog = GTK_DIALOG (gtk_builder_get_object (builder, "join-dialog")); + self->join_domain = GTK_LABEL (gtk_builder_get_object (builder, "join-domain")); + self->join_name = GTK_ENTRY (gtk_builder_get_object (builder, "join-name")); + self->join_password = GTK_ENTRY (gtk_builder_get_object (builder, "join-password")); + + g_signal_connect_object (self->join_dialog, "response", + G_CALLBACK (on_join_response), self, G_CONNECT_SWAPPED); +} + +static void +on_realm_joined (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(CcAddUserDialog) self = CC_ADD_USER_DIALOG (user_data); + g_autoptr(GError) error = NULL; + + if (g_cancellable_is_cancelled (self->cancellable)) { + return; + } + + cc_realm_join_finish (self->selected_realm, + result, &error); + + /* Yay, joined the domain, register the user locally */ + if (error == NULL) { + g_debug ("Joining realm completed successfully"); + enterprise_permit_user_login (self); + + /* Credential failure while joining domain, prompt for admin creds */ + } else if (g_error_matches (error, CC_REALM_ERROR, CC_REALM_ERROR_BAD_LOGIN) || + g_error_matches (error, CC_REALM_ERROR, CC_REALM_ERROR_BAD_PASSWORD)) { + g_debug ("Joining realm failed due to credentials"); + join_show_prompt (self, error); + + /* Other failure */ + } else { + show_error_dialog (self, _("Failed to join domain"), error); + g_message ("Failed to join the domain: %s", error->message); + finish_action (self); + } +} + +static void +on_realm_login (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(CcAddUserDialog) self = CC_ADD_USER_DIALOG (user_data); + g_autoptr(GError) error = NULL; + g_autoptr(GBytes) creds = NULL; + const gchar *message; + + if (g_cancellable_is_cancelled (self->cancellable)) { + return; + } + + creds = cc_realm_login_finish (result, &error); + + /* + * User login is valid, but cannot authenticate right now (eg: user needs + * to change password at next login etc.) + */ + if (g_error_matches (error, CC_REALM_ERROR, CC_REALM_ERROR_CANNOT_AUTH)) { + g_clear_error (&error); + creds = NULL; + } + + if (error == NULL) { + + /* Already joined to the domain, just register this user */ + if (cc_realm_is_configured (self->selected_realm)) { + g_debug ("Already joined to this realm"); + enterprise_permit_user_login (self); + + /* Join the domain, try using the user's creds */ + } else if (creds == NULL || + !cc_realm_join_as_user (self->selected_realm, + gtk_editable_get_text (GTK_EDITABLE (self->enterprise_login_entry)), + gtk_editable_get_text (GTK_EDITABLE (self->enterprise_password_entry)), + creds, self->cancellable, + on_realm_joined, + g_object_ref (self))) { + + /* If we can't do user auth, try to authenticate as admin */ + g_debug ("Cannot join with user credentials"); + join_show_prompt (self, NULL); + } + + /* A problem with the user's login name or password */ + } else if (g_error_matches (error, CC_REALM_ERROR, CC_REALM_ERROR_BAD_LOGIN)) { + g_debug ("Problem with the user's login: %s", error->message); + message = _("That login name didn’t work.\nPlease try again."); + adw_preferences_group_set_description (self->enterprise_login_group, message); + finish_action (self); + gtk_widget_grab_focus (GTK_WIDGET (self->enterprise_login_entry)); + + } else if (g_error_matches (error, CC_REALM_ERROR, CC_REALM_ERROR_BAD_PASSWORD)) { + g_debug ("Problem with the user's password: %s", error->message); + message = _("That login password didn’t work.\nPlease try again."); + adw_preferences_group_set_description (self->enterprise_login_group, message); + finish_action (self); + gtk_widget_grab_focus (GTK_WIDGET (self->enterprise_password_entry)); + + /* Other login failure */ + } else { + g_dbus_error_strip_remote_error (error); + show_error_dialog (self, _("Failed to log into domain"), error); + g_message ("Couldn't log in as user: %s", error->message); + finish_action (self); + } +} + +static void +enterprise_check_login (CcAddUserDialog *self) +{ + g_assert (self->selected_realm); + + cc_realm_login (self->selected_realm, + gtk_editable_get_text (GTK_EDITABLE (self->enterprise_login_entry)), + gtk_editable_get_text (GTK_EDITABLE (self->enterprise_password_entry)), + self->cancellable, + on_realm_login, + g_object_ref (self)); +} + +static void +on_realm_discover_input (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(CcAddUserDialog) self = CC_ADD_USER_DIALOG (user_data); + g_autoptr(GError) error = NULL; + GList *realms; + + if (g_cancellable_is_cancelled (self->cancellable)) { + return; + } + + realms = cc_realm_manager_discover_finish (self->realm_manager, + result, &error); + + /* Found a realm, log user into domain */ + if (error == NULL) { + g_assert (realms != NULL); + self->selected_realm = g_object_ref (realms->data); + + if (self->enterprise_check_credentials) { + enterprise_check_login (self); + } + gtk_image_set_from_icon_name (self->enterprise_domain_status_icon, "emblem-ok-symbolic"); + gtk_label_set_text (self->enterprise_domain_hint, DOMAIN_DEFAULT_HINT); + g_list_free_full (realms, g_object_unref); + + /* The domain is likely invalid*/ + } else { + g_autofree gchar *message = NULL; + + g_message ("Couldn't discover domain: %s", error->message); + g_dbus_error_strip_remote_error (error); + + if (g_error_matches (error, CC_REALM_ERROR, CC_REALM_ERROR_GENERIC)) { + message = g_strdup (_("Unable to find the domain. Maybe you misspelled it?")); + } else { + message = g_strdup_printf ("%s.", error->message); + } + gtk_label_set_text (self->enterprise_domain_hint, message); + + if (self->enterprise_check_credentials) { + finish_action (self); + self->enterprise_check_credentials = FALSE; + } + } + + if (!self->enterprise_check_credentials) { + finish_action (self); + dialog_validate (self); + } +} + +static void +enterprise_check_domain (CcAddUserDialog *self) +{ + const gchar *domain; + + domain = gtk_editable_get_text (GTK_EDITABLE (self->enterprise_domain_entry)); + if (strlen (domain) == 0) { + gtk_label_set_text (self->enterprise_domain_hint, DOMAIN_DEFAULT_HINT); + return; + } + + begin_action (self); + + self->join_prompted = FALSE; + cc_realm_manager_discover (self->realm_manager, + domain, + self->cancellable, + on_realm_discover_input, + g_object_ref (self)); +} + +static void +enterprise_add_user (CcAddUserDialog *self) +{ + self->join_prompted = FALSE; + self->enterprise_check_credentials = TRUE; + begin_action (self); + enterprise_check_login (self); + +} + +static void +clear_realm_manager (CcAddUserDialog *self) +{ + if (self->realm_manager) { + g_signal_handlers_disconnect_by_func (self->realm_manager, + on_manager_realm_added, + self); + g_clear_object (&self->realm_manager); + } +} + +static void +on_realm_manager_created (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(CcAddUserDialog) self = CC_ADD_USER_DIALOG (user_data); + g_autoptr(GError) error = NULL; + GList *realms, *l; + + clear_realm_manager (self); + + self->realm_manager = cc_realm_manager_new_finish (result, &error); + if (error != NULL) { + g_warning ("Couldn't contact realmd service: %s", error->message); + return; + } + + if (g_cancellable_is_cancelled (self->cancellable)) { + return; + } + + /* Lookup all the realm objects */ + realms = cc_realm_manager_get_realms (self->realm_manager); + for (l = realms; l != NULL; l = g_list_next (l)) + enterprise_add_realm (self, l->data); + g_list_free (realms); + g_signal_connect_object (self->realm_manager, "realm-added", + G_CALLBACK (on_manager_realm_added), self, G_CONNECT_SWAPPED); + + /* When no realms try to discover a sensible default, triggers realm-added signal */ + cc_realm_manager_discover (self->realm_manager, "", self->cancellable, + NULL, NULL); + + /* Show the 'Enterprise Login' stuff, and update mode */ + gtk_widget_show (GTK_WIDGET (self->enterprise_group)); + mode_change (self, self->mode); +} + +static void +on_realmd_appeared (GDBusConnection *connection, + const gchar *name, + const gchar *name_owner, + gpointer user_data) +{ + CcAddUserDialog *self = CC_ADD_USER_DIALOG (user_data); + cc_realm_manager_new (self->cancellable, on_realm_manager_created, + g_object_ref (self)); +} + +static void +on_realmd_disappeared (GDBusConnection *unused1, + const gchar *unused2, + gpointer user_data) +{ + CcAddUserDialog *self = CC_ADD_USER_DIALOG (user_data); + + clear_realm_manager (self); + gtk_list_store_clear (self->enterprise_realm_model); + gtk_widget_hide (GTK_WIDGET (self->enterprise_group)); + mode_change (self, MODE_LOCAL); +} + +static void +on_network_changed (GNetworkMonitor *monitor, + gboolean available, + gpointer user_data) +{ + CcAddUserDialog *self = CC_ADD_USER_DIALOG (user_data); + + if (self->mode != MODE_LOCAL) + mode_change (self, MODE_ENTERPRISE); +} + +static gboolean +enterprise_domain_timeout (CcAddUserDialog *self) +{ + GtkTreeIter iter; + + self->enterprise_domain_timeout_id = 0; + + if (gtk_combo_box_get_active_iter (self->enterprise_domain_combo, &iter)) { + gtk_tree_model_get (GTK_TREE_MODEL (self->enterprise_realm_model), &iter, 1, &self->selected_realm, -1); + gtk_image_set_from_icon_name (self->enterprise_domain_status_icon, "emblem-ok-symbolic"); + gtk_label_set_text (self->enterprise_domain_hint, DOMAIN_DEFAULT_HINT); + } + else { + enterprise_check_domain (self); + } + + return FALSE; +} + +static void +enterprise_domain_combo_changed_cb (CcAddUserDialog *self) +{ + if (self->enterprise_domain_timeout_id != 0) { + g_source_remove (self->enterprise_domain_timeout_id); + self->enterprise_domain_timeout_id = 0; + } + + g_clear_object (&self->selected_realm); + gtk_image_set_from_icon_name (self->enterprise_domain_status_icon, "dialog-warning-symbolic"); + self->enterprise_domain_timeout_id = g_timeout_add (PASSWORD_CHECK_TIMEOUT, (GSourceFunc) enterprise_domain_timeout, self); + gtk_widget_set_sensitive (GTK_WIDGET (self->add_button), FALSE); + + self->enterprise_domain_chosen = TRUE; + dialog_validate (self); +} + +static gboolean +enterprise_domain_combo_focus_out_event_cb (CcAddUserDialog *self) +{ + if (self->enterprise_domain_timeout_id != 0) { + g_source_remove (self->enterprise_domain_timeout_id); + self->enterprise_domain_timeout_id = 0; + } + + if (self->selected_realm == NULL) { + enterprise_check_domain (self); + } + + return FALSE; +} + +static void +enterprise_login_entry_changed_cb (CcAddUserDialog *self) +{ + dialog_validate (self); + gtk_image_set_from_icon_name (self->enterprise_login_status_icon, "dialog-warning-symbolic"); + gtk_image_set_from_icon_name (self->enterprise_password_status_icon, "dialog-warning-symbolic"); +} + +static void +enterprise_password_entry_changed_cb (CcAddUserDialog *self) +{ + dialog_validate (self); + gtk_image_set_from_icon_name (self->enterprise_password_status_icon, "dialog-warning-symbolic"); +} + +static void +dialog_validate (CcAddUserDialog *self) +{ + gboolean valid = FALSE; + + switch (self->mode) { + case MODE_LOCAL: + valid = local_validate (self); + break; + case MODE_ENTERPRISE: + valid = enterprise_validate (self); + break; + default: + valid = FALSE; + break; + } + + gtk_widget_set_sensitive (GTK_WIDGET (self->add_button), valid); +} + +static void +mode_change (CcAddUserDialog *self, + AccountMode mode) +{ + gboolean available; + GNetworkMonitor *monitor; + + if (mode != MODE_LOCAL) { + monitor = g_network_monitor_get_default (); + available = g_network_monitor_get_network_available (monitor); + mode = available ? MODE_ENTERPRISE : MODE_OFFLINE; + } + + switch (mode) { + default: + case MODE_LOCAL: + gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->local_page)); + gtk_widget_grab_focus (GTK_WIDGET (self->local_name_entry)); + break; + case MODE_ENTERPRISE: + gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->enterprise_page)); + gtk_widget_grab_focus (GTK_WIDGET (self->enterprise_domain_entry)); + break; + case MODE_OFFLINE: + gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->offline_page)); + break; + } + + self->mode = mode; + dialog_validate (self); +} + +static void +enterprise_button_toggled_cb (CcAddUserDialog *self) +{ + mode_change (self, MODE_ENTERPRISE); +} + +static void +cc_add_user_dialog_init (CcAddUserDialog *self) +{ + GNetworkMonitor *monitor; + + gtk_widget_init_template (GTK_WIDGET (self)); + + self->cancellable = g_cancellable_new (); + + self->local_password_mode = ACT_USER_PASSWORD_MODE_SET_AT_LOGIN; + dialog_validate (self); + update_password_strength (self); + local_username_timeout (self); + + enterprise_check_domain (self); + + self->realmd_watch = g_bus_watch_name (G_BUS_TYPE_SYSTEM, "org.freedesktop.realmd", + G_BUS_NAME_WATCHER_FLAGS_AUTO_START, + on_realmd_appeared, on_realmd_disappeared, + self, NULL); + + monitor = g_network_monitor_get_default (); + g_signal_connect_object (monitor, "network-changed", G_CALLBACK (on_network_changed), self, 0); + + join_init (self); + + mode_change (self, MODE_LOCAL); +} + +static void +on_permission_acquired (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(CcAddUserDialog) self = CC_ADD_USER_DIALOG (user_data); + g_autoptr(GError) error = NULL; + + /* Paired with begin_action in cc_add_user_dialog_response () */ + finish_action (self); + + if (g_permission_acquire_finish (self->permission, res, &error)) { + g_return_if_fail (g_permission_get_allowed (self->permission)); + add_button_clicked_cb (self); + } else if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_warning ("Failed to acquire permission: %s", error->message); + } +} + +static void +add_button_clicked_cb (CcAddUserDialog *self) +{ + /* We don't (or no longer) have necessary permissions */ + if (self->permission && !g_permission_get_allowed (self->permission)) { + begin_action (self); + g_permission_acquire_async (self->permission, self->cancellable, + on_permission_acquired, g_object_ref (self)); + return; + } + + switch (self->mode) { + case MODE_LOCAL: + local_create_user (self); + break; + case MODE_ENTERPRISE: + enterprise_add_user (self); + break; + default: + g_assert_not_reached (); + } +} + +static void +cc_add_user_dialog_dispose (GObject *obj) +{ + CcAddUserDialog *self = CC_ADD_USER_DIALOG (obj); + + if (self->cancellable) + g_cancellable_cancel (self->cancellable); + + g_clear_object (&self->user); + + if (self->realmd_watch) + g_bus_unwatch_name (self->realmd_watch); + self->realmd_watch = 0; + + if (self->realm_manager) { + g_signal_handlers_disconnect_by_func (self->realm_manager, + on_manager_realm_added, + self); + g_clear_object (&self->realm_manager); + } + + if (self->local_password_timeout_id != 0) { + g_source_remove (self->local_password_timeout_id); + self->local_password_timeout_id = 0; + } + + if (self->local_name_timeout_id != 0) { + g_source_remove (self->local_name_timeout_id); + self->local_name_timeout_id = 0; + } + + if (self->local_username_timeout_id != 0) { + g_source_remove (self->local_username_timeout_id); + self->local_username_timeout_id = 0; + } + + if (self->enterprise_domain_timeout_id != 0) { + g_source_remove (self->enterprise_domain_timeout_id); + self->enterprise_domain_timeout_id = 0; + } + + if (self->join_dialog != NULL) { + gtk_window_destroy (GTK_WINDOW (self->join_dialog)); + } + + G_OBJECT_CLASS (cc_add_user_dialog_parent_class)->dispose (obj); +} + +static void +cc_add_user_dialog_finalize (GObject *obj) +{ + CcAddUserDialog *self = CC_ADD_USER_DIALOG (obj); + + g_clear_object (&self->cancellable); + g_clear_object (&self->permission); + + G_OBJECT_CLASS (cc_add_user_dialog_parent_class)->finalize (obj); +} + +static void +cc_add_user_dialog_class_init (CcAddUserDialogClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = cc_add_user_dialog_dispose; + object_class->finalize = cc_add_user_dialog_finalize; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/user-accounts/cc-add-user-dialog.ui"); + + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, add_button); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, enterprise_button); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, enterprise_domain_combo); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, enterprise_domain_entry); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, enterprise_domain_hint); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, enterprise_domain_row); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, enterprise_domain_status_icon); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, enterprise_group); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, enterprise_page); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, enterprise_login_group); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, enterprise_login_entry); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, enterprise_login_status_icon); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, enterprise_password_entry); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, enterprise_password_status_icon); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, enterprise_realm_model); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_account_type_switch); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_page); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_password_hint); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_password_row); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_password_status_icon); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_name_entry); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_name_status_icon); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_username_combo); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_username_model); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_password_entry); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_password_radio); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_username_entry); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_username_row); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_username_status_icon); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_strength_indicator); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_verify_entry); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_verify_password_row); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, local_verify_status_icon); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, offline_page); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, password_group); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, spinner); + gtk_widget_class_bind_template_child (widget_class, CcAddUserDialog, stack); + + gtk_widget_class_bind_template_callback (widget_class, add_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, dialog_validate); + gtk_widget_class_bind_template_callback (widget_class, enterprise_button_toggled_cb); + gtk_widget_class_bind_template_callback (widget_class, enterprise_domain_combo_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, enterprise_domain_combo_focus_out_event_cb); + gtk_widget_class_bind_template_callback (widget_class, enterprise_login_entry_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, enterprise_password_entry_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, generate_password); + gtk_widget_class_bind_template_callback (widget_class, local_name_entry_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, local_name_entry_focus_out_event_cb); + gtk_widget_class_bind_template_callback (widget_class, local_password_entry_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, local_password_entry_key_press_event_cb); + gtk_widget_class_bind_template_callback (widget_class, local_password_radio_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, local_username_combo_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, local_username_combo_focus_out_event_cb); + gtk_widget_class_bind_template_callback (widget_class, local_verify_entry_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, password_focus_out_event_cb); +} + +CcAddUserDialog * +cc_add_user_dialog_new (GPermission *permission) +{ + CcAddUserDialog *self; + + self = g_object_new (CC_TYPE_ADD_USER_DIALOG, "use-header-bar", 1, NULL); + + if (permission != NULL) + self->permission = g_object_ref (permission); + + return self; +} + +ActUser * +cc_add_user_dialog_get_user (CcAddUserDialog *self) +{ + g_return_val_if_fail (CC_IS_ADD_USER_DIALOG (self), NULL); + return self->user; +} diff --git a/panels/user-accounts/cc-add-user-dialog.h b/panels/user-accounts/cc-add-user-dialog.h new file mode 100644 index 0000000..c666d4d --- /dev/null +++ b/panels/user-accounts/cc-add-user-dialog.h @@ -0,0 +1,34 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright 2009-2010 Red Hat, Inc, + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see <http://www.gnu.org/licenses/>. + * + * Written by: Matthias Clasen <mclasen@redhat.com> + */ + +#pragma once + +#include <act/act.h> +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define CC_TYPE_ADD_USER_DIALOG (cc_add_user_dialog_get_type ()) +G_DECLARE_FINAL_TYPE (CcAddUserDialog, cc_add_user_dialog, CC, ADD_USER_DIALOG, GtkDialog) + +CcAddUserDialog *cc_add_user_dialog_new (GPermission *permission); +ActUser *cc_add_user_dialog_get_user (CcAddUserDialog *dialog); + +G_END_DECLS diff --git a/panels/user-accounts/cc-add-user-dialog.ui b/panels/user-accounts/cc-add-user-dialog.ui new file mode 100644 index 0000000..afdc65f --- /dev/null +++ b/panels/user-accounts/cc-add-user-dialog.ui @@ -0,0 +1,411 @@ +<?xml version="1.0"?> +<interface> + <!-- interface-requires gtk+ 3.8 --> + <!-- interface-naming-policy toplevel-contextual --> + <object class="GtkListStore" id="local_username_model"> + <columns> + <!-- column-name gchararray --> + <column type="gchararray"/> + </columns> + </object> + <template class="CcAddUserDialog" parent="GtkDialog"> + <property name="resizable">False</property> + <property name="modal">True</property> + <property name="hide-on-close">True</property> + <property name="title" translatable="yes">Add User</property> + <property name="icon_name">system-users</property> + <property name="default-width">500</property> + <property name="use_header_bar">1</property> + <child type="titlebar"> + <object class="AdwHeaderBar"> + <property name="show-end-title-buttons">False</property> + <property name="show-start-title-buttons">False</property> + <child type="start"> + <object class="GtkButton" id="cancel_button"> + <property name="label" translatable="yes">_Cancel</property> + <property name="visible">True</property> + <property name="visible">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <property name="valign">center</property> + <signal name="clicked" handler="gtk_window_destroy" object="CcAddUserDialog"/> + <style> + <class name="text-button"/> + </style> + </object> + </child> + <child type="end"> + <object class="GtkButton" id="add_button"> + <property name="label" translatable="yes">_Add</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <property name="valign">center</property> + <signal name="clicked" handler="add_button_clicked_cb" object="CcAddUserDialog" swapped="yes"/> + <style> + <class name="text-button"/> + <class name="suggested-action"/> + </style> + </object> + </child> + <child type="end"> + <object class="GtkSpinner" id="spinner"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="valign">center</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkStack" id="stack"> + <child> + <object class="AdwPreferencesPage" id="local_page"> + <child> + <object class="AdwPreferencesGroup"> + <child> + <object class="AdwActionRow" id="local_name_row"> + <property name="activatable-widget">local_name_entry</property> + <property name="title" translatable="yes">Name</property> + <child> + <object class="GtkEntry" id="local_name_entry"> + <property name="valign">center</property> + <property name="max-length">255</property> + <property name="activates_default">True</property> + <property name="hexpand">True</property> + <signal name="changed" handler="local_name_entry_changed_cb" object="CcAddUserDialog" swapped="yes"/> + <signal name="activate" handler="dialog_validate" object="CcAddUserDialog" swapped="yes"/> + <child> + <object class="GtkEventControllerFocus"> + <signal name="leave" handler="local_name_entry_focus_out_event_cb" object="CcAddUserDialog" swapped="yes"/> + </object> + </child> + </object> + </child> + <child type="suffix"> + <object class="GtkImage" id="local_name_status_icon"> + <property name="icon-name">dialog-warning-symbolic</property> + </object> + </child> + </object> + </child> + <child> + <object class="AdwActionRow" id="local_username_row"> + <property name="activatable-widget">local_username_combo</property> + <property name="title" translatable="yes">Username</property> + <property name="subtitle-lines">2</property> + <child> + <object class="GtkComboBoxText" id="local_username_combo"> + <property name="valign">center</property> + <property name="has_entry">True</property> + <property name="entry_text_column">0</property> + <property name="model">local_username_model</property> + <signal name="changed" handler="local_username_combo_changed_cb" object="CcAddUserDialog" swapped="yes"/> + <child> + <object class="GtkEventControllerFocus"> + <signal name="leave" handler="local_username_combo_focus_out_event_cb" object="CcAddUserDialog" swapped="yes"/> + </object> + </child> + <child internal-child="entry"> + <object class="GtkEntry" id="local_username_entry"> + <property name="activates_default">True</property> + <property name="hexpand">True</property> + <signal name="activate" handler="dialog_validate" object="CcAddUserDialog" swapped="yes"/> + </object> + </child> + </object> + </child> + <child type="suffix"> + <object class="GtkImage" id="local_username_status_icon"> + <property name="icon-name">dialog-warning-symbolic</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="AdwPreferencesGroup"> + <child> + <object class="AdwActionRow"> + <property name="activatable-widget">local_account_type_switch</property> + <property name="title" translatable="yes">Administrator</property> + <property name="subtitle" translatable="yes">Administrators can add and remove other users, and can change settings for all users. Parental controls cannot be applied to administrators.</property> + <property name="subtitle-lines">3</property> + <child type="suffix"> + <object class="GtkSwitch" id="local_account_type_switch"> + <property name="valign">center</property> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="AdwPreferencesGroup"> + <property name="title" translatable="yes">Password</property> + <child> + <object class="AdwActionRow"> + <property name="activatable-widget">local_password_login_radio</property> + <property name="title" translatable="yes">User sets password on first login</property> + <child type="prefix"> + <object class="GtkCheckButton" id="local_password_login_radio"> + <property name="valign">center</property> + <property name="active">True</property> + </object> + </child> + </object> + </child> + <child> + <object class="AdwActionRow"> + <property name="activatable-widget">local_password_radio</property> + <property name="title" translatable="yes">Set password now</property> + <child type="prefix"> + <object class="GtkCheckButton" id="local_password_radio"> + <property name="valign">center</property> + <property name="use_underline">True</property> + <property name="group">local_password_login_radio</property> + <signal name="toggled" handler="local_password_radio_changed_cb" object="CcAddUserDialog" swapped="yes"/> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="AdwPreferencesGroup" id="password_group"> + <property name="sensitive">False</property> + <child> + <object class="AdwActionRow" id="local_password_row"> + <property name="activatable-widget">local_password_entry</property> + <property name="title" translatable="yes">Password</property> + <property name="subtitle-lines">2</property> + <child> + <object class="GtkPasswordEntry" id="local_password_entry"> + <property name="hexpand">True</property> + <property name="has_tooltip">True</property> + <property name="valign">center</property> + <property name="show-peek-icon">True</property> + <signal name="notify::text" handler="local_password_entry_changed_cb" object="CcAddUserDialog" swapped="yes"/> + <signal name="activate" handler="dialog_validate" object="CcAddUserDialog" swapped="yes"/> + <child> + <object class="GtkEventControllerKey"> + <signal name="key-pressed" handler="local_password_entry_key_press_event_cb" object="CcAddUserDialog" swapped="no"/> + </object> + </child> + <child> + <object class="GtkEventControllerFocus"> + <signal name="leave" handler="password_focus_out_event_cb" object="CcAddUserDialog" swapped="yes"/> + </object> + </child> + </object> + </child> + <child type="suffix"> + <object class="GtkButton"> + <property name="visible">False</property> + <property name="icon-name">system-run-symbolic</property> + <property name="valign">center</property> + <signal name="clicked" handler="generate_password" object="CcAddUserDialog" swapped="yes"/> + <style> + <class name="flat"/> + </style> + </object> + </child> + <child type="suffix"> + <object class="GtkImage" id="local_password_status_icon"> + <property name="icon-name">dialog-warning-symbolic</property> + </object> + </child> + </object> + </child> + <child> + <object class="AdwActionRow" id="local_verify_password_row"> + <property name="activatable-widget">local_verify_entry</property> + <property name="title" translatable="yes">Confirm</property> + <child> + <object class="GtkPasswordEntry" id="local_verify_entry"> + <property name="hexpand">True</property> + <property name="valign">center</property> + <property name="sensitive">False</property> + <property name="show-peek-icon">True</property> + <signal name="notify::text" handler="local_verify_entry_changed_cb" object="CcAddUserDialog" swapped="yes"/> + <signal name="activate" handler="dialog_validate" object="CcAddUserDialog" swapped="yes"/> + <child> + <object class="GtkEventControllerFocus"> + <signal name="leave" handler="password_focus_out_event_cb" object="CcAddUserDialog" swapped="yes"/> + </object> + </child> + </object> + </child> + <child type="suffix"> + <object class="GtkImage" id="local_verify_status_icon"> + <property name="icon-name">dialog-warning-symbolic</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkLevelBar" id="local_strength_indicator"> + <property name="mode">continuous</property> + <property name="max-value">5</property> + <property name="margin-top">12</property> + <offsets> + <offset name="strength-weak" value="1"/> + <offset name="strength-low" value="2"/> + <offset name="strength-medium" value="3"/> + <offset name="strength-good" value="4"/> + <offset name="strength-high" value="5"/> + </offsets> + </object> + </child> + <child> + <object class="GtkLabel" id="local_password_hint"> + <property name="halign">start</property> + <property name="wrap">True</property> + <property name="margin-top">12</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="AdwPreferencesGroup" id="enterprise_group"> + <property name="visible">False</property> + <child> + <object class="AdwActionRow" id="enterprise_button"> + <property name="title" translatable="yes">Enterprise Login</property> + <property name="subtitle" translatable="yes">User accounts which are managed by a company or organization.</property> + <property name="activatable">True</property> + <signal name="activated" handler="enterprise_button_toggled_cb" object="CcAddUserDialog" swapped="yes"/> + <child> + <object class="GtkImage"> + <property name="icon-name">go-next-symbolic</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="AdwPreferencesPage" id="enterprise_page"> + <property name="visible">True</property> + <child> + <object class="AdwPreferencesGroup"> + <child> + <object class="AdwActionRow" id="enterprise_domain_row"> + <property name="title" translatable="yes">Domain</property> + <property name="activatable_widget">enterprise_domain_combo</property> + <child> + <object class="GtkComboBox" id="enterprise_domain_combo"> + <property name="valign">center</property> + <property name="has_entry">True</property> + <property name="entry_text_column">0</property> + <property name="model">enterprise_realm_model</property> + <signal name="changed" handler="enterprise_domain_combo_changed_cb" object="CcAddUserDialog" swapped="yes"/> + <child internal-child="entry"> + <object class="GtkEntry" id="enterprise_domain_entry"> + </object> + </child> + <child> + <object class="GtkEventControllerFocus"> + <signal name="leave" handler="enterprise_domain_combo_focus_out_event_cb" object="CcAddUserDialog" swapped="yes"/> + </object> + </child> + </object> + </child> + <child type="suffix"> + <object class="GtkImage" id="enterprise_domain_status_icon"> + <property name="icon-name">dialog-warning-symbolic</property> + </object> + </child> + </object> + </child> + <child> + <object class="GtkLabel" id="enterprise_domain_hint"> + <property name="halign">start</property> + <property name="margin-top">12</property> + <property name="wrap">True</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="AdwPreferencesGroup" id="enterprise_login_group"> + <child> + <object class="AdwActionRow"> + <property name="title" translatable="yes">Username</property> + <property name="activatable_widget">enterprise_login_entry</property> + <child> + <object class="GtkEntry" id="enterprise_login_entry"> + <property name="valign">center</property> + <property name="invisible_char">●</property> + <property name="activates_default">True</property> + <property name="invisible_char_set">True</property> + <property name="input_purpose">password</property> + <signal name="changed" handler="enterprise_login_entry_changed_cb" object="CcAddUserDialog" swapped="yes"/> + </object> + </child> + <child type="suffix"> + <object class="GtkImage" id="enterprise_login_status_icon"> + <property name="icon-name">dialog-warning-symbolic</property> + </object> + </child> + </object> + </child> + <child> + <object class="AdwActionRow"> + <property name="title" translatable="yes">Password</property> + <property name="activatable_widget">enterprise_password_entry</property> + <child> + <object class="GtkPasswordEntry" id="enterprise_password_entry"> + <property name="valign">center</property> + <property name="activates_default">True</property> + <signal name="changed" handler="enterprise_password_entry_changed_cb" object="CcAddUserDialog" swapped="yes"/> + </object> + </child> + <child type="suffix"> + <object class="GtkImage" id="enterprise_password_status_icon"> + <property name="icon-name">dialog-warning-symbolic</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="AdwPreferencesPage" id="offline_page"> + <child> + <object class="AdwStatusPage"> + <property name="title" translatable="yes">You are Offline</property> + <property name="description" translatable="yes">Enterprise login allows an existing centrally managed user account to be used on this device. You can also use this account to access company resources on the internet.</property> + <property name="icon-name">network-offline-symbolic</property> + </object> + </child> + </object> + </child> + </object> + </child> + </template> + <object class="GtkSizeGroup"> + <widgets> + <widget name="cancel_button"/> + <widget name="add_button"/> + </widgets> + </object> + <object class="GtkListStore" id="enterprise_realm_model"> + <columns> + <!-- column-name title --> + <column type="gchararray"/> + <!-- column-name realm --> + <column type="GObject"/> + </columns> + </object> +</interface> diff --git a/panels/user-accounts/cc-avatar-chooser.c b/panels/user-accounts/cc-avatar-chooser.c new file mode 100644 index 0000000..218d73f --- /dev/null +++ b/panels/user-accounts/cc-avatar-chooser.c @@ -0,0 +1,452 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright 2009-2010 Red Hat, Inc, + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see <http://www.gnu.org/licenses/>. + * + * Written by: Matthias Clasen <mclasen@redhat.com> + */ + +#include "config.h" + +#include <stdlib.h> + +#include <adwaita.h> +#include <gio/gunixoutputstream.h> +#include <glib.h> +#include <glib/gi18n.h> +#include <glib/gstdio.h> +#include <gtk/gtk.h> +#include <act/act.h> +#define GNOME_DESKTOP_USE_UNSTABLE_API +#include <libgnome-desktop/gnome-desktop-thumbnail.h> + +#include "cc-avatar-chooser.h" +#include "cc-crop-area.h" +#include "user-utils.h" + +#define ROW_SPAN 5 +#define AVATAR_CHOOSER_PIXEL_SIZE 80 +#define PIXEL_SIZE 512 + +struct _CcAvatarChooser { + GtkPopover parent; + + GtkWidget *transient_for; + + GtkWidget *crop_area; + GtkWidget *user_flowbox; + GtkWidget *flowbox; + + GnomeDesktopThumbnailFactory *thumb_factory; + GListStore *faces; + + ActUser *user; +}; + +G_DEFINE_TYPE (CcAvatarChooser, cc_avatar_chooser, GTK_TYPE_POPOVER) + +static void +crop_dialog_response (CcAvatarChooser *self, + gint response_id, + GtkWidget *dialog) +{ + g_autoptr(GdkPixbuf) pb = NULL; + g_autoptr(GdkPixbuf) pb2 = NULL; + + if (response_id != GTK_RESPONSE_ACCEPT) { + self->crop_area = NULL; + gtk_window_destroy (GTK_WINDOW (dialog)); + return; + } + + pb = cc_crop_area_create_pixbuf (CC_CROP_AREA (self->crop_area)); + pb2 = gdk_pixbuf_scale_simple (pb, PIXEL_SIZE, PIXEL_SIZE, GDK_INTERP_BILINEAR); + + set_user_icon_data (self->user, pb2); + + self->crop_area = NULL; + gtk_window_destroy (GTK_WINDOW (dialog)); + + gtk_popover_popdown (GTK_POPOVER (self)); +} + +static void +cc_avatar_chooser_crop (CcAvatarChooser *self, + GdkPixbuf *pixbuf) +{ + GtkWidget *dialog; + GtkWindow *toplevel; + + toplevel = (GtkWindow *)gtk_widget_get_native (GTK_WIDGET (self->transient_for)); + dialog = gtk_dialog_new_with_buttons ("", + toplevel, + GTK_DIALOG_USE_HEADER_BAR, + _("_Cancel"), + GTK_RESPONSE_CANCEL, + _("Select"), + GTK_RESPONSE_ACCEPT, + NULL); + gtk_window_set_modal (GTK_WINDOW (dialog), TRUE); + + gtk_window_set_icon_name (GTK_WINDOW (dialog), "system-users"); + + g_signal_connect_object (G_OBJECT (dialog), "response", + G_CALLBACK (crop_dialog_response), self, G_CONNECT_SWAPPED); + + /* Content */ + self->crop_area = cc_crop_area_new (); + gtk_widget_show (self->crop_area); + cc_crop_area_set_min_size (CC_CROP_AREA (self->crop_area), 48, 48); + cc_crop_area_set_paintable (CC_CROP_AREA (self->crop_area), + GDK_PAINTABLE (gdk_texture_new_for_pixbuf (pixbuf))); + gtk_box_prepend (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), + self->crop_area); + gtk_widget_set_hexpand (self->crop_area, TRUE); + gtk_widget_set_vexpand (self->crop_area, TRUE); + + gtk_window_set_default_size (GTK_WINDOW (dialog), 400, 300); + + gtk_widget_show (dialog); +} + +static void +file_chooser_response (CcAvatarChooser *self, + gint response, + GtkDialog *chooser) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GdkPixbuf) pixbuf = NULL; + g_autoptr(GdkPixbuf) pixbuf2 = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GFileInputStream) stream = NULL; + + if (response != GTK_RESPONSE_ACCEPT) { + gtk_window_destroy (GTK_WINDOW (chooser)); + return; + } + + file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (chooser)); + stream = g_file_read (file, NULL, &error); + pixbuf = gdk_pixbuf_new_from_stream (G_INPUT_STREAM (stream), + NULL, &error); + if (pixbuf == NULL) { + g_warning ("Failed to load %s: %s", g_file_get_uri (file), error->message); + } + + pixbuf2 = gdk_pixbuf_apply_embedded_orientation (pixbuf); + + gtk_window_destroy (GTK_WINDOW (chooser)); + + cc_avatar_chooser_crop (self, pixbuf2); +} + +static void +cc_avatar_chooser_select_file (CcAvatarChooser *self) +{ + g_autoptr(GFile) folder = NULL; + GtkWidget *chooser; + GtkFileFilter *filter; + GtkWindow *toplevel; + + toplevel = (GtkWindow*) gtk_widget_get_native (GTK_WIDGET (self->transient_for)); + chooser = gtk_file_chooser_dialog_new (_("Browse for more pictures"), + toplevel, + GTK_FILE_CHOOSER_ACTION_OPEN, + _("_Cancel"), GTK_RESPONSE_CANCEL, + _("_Open"), GTK_RESPONSE_ACCEPT, + NULL); + + gtk_window_set_modal (GTK_WINDOW (chooser), TRUE); + + folder = g_file_new_for_path (g_get_user_special_dir (G_USER_DIRECTORY_PICTURES)); + if (folder) + gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (chooser), + folder, + NULL); + + filter = gtk_file_filter_new (); + gtk_file_filter_add_pixbuf_formats (filter); + gtk_file_chooser_set_filter (GTK_FILE_CHOOSER (chooser), filter); + + g_signal_connect_object (chooser, "response", + G_CALLBACK (file_chooser_response), self, G_CONNECT_SWAPPED); + + gtk_popover_popdown (GTK_POPOVER (self)); + gtk_window_present (GTK_WINDOW (chooser)); +} + +static void +face_widget_activated (CcAvatarChooser *self, + GtkFlowBoxChild *child) +{ + const gchar *filename; + GtkWidget *image; + + image = gtk_flow_box_child_get_child (child); + filename = g_object_get_data (G_OBJECT (image), "filename"); + + act_user_set_icon_file (self->user, filename); + + gtk_popover_popdown (GTK_POPOVER (self)); +} + +static GtkWidget * +create_face_widget (gpointer item, + gpointer user_data) +{ + g_autofree gchar *image_path = NULL; + g_autoptr(GdkPixbuf) source_pixbuf = NULL; + g_autoptr(GdkPixbuf) pixbuf = NULL; + GtkWidget *image; + + image_path = g_file_get_path (G_FILE (item)); + + source_pixbuf = gdk_pixbuf_new_from_file_at_size (image_path, + AVATAR_CHOOSER_PIXEL_SIZE, + AVATAR_CHOOSER_PIXEL_SIZE, + NULL); + if (source_pixbuf == NULL) { + image = gtk_image_new_from_icon_name ("image-missing"); + gtk_image_set_pixel_size (GTK_IMAGE (image), AVATAR_CHOOSER_PIXEL_SIZE); + gtk_widget_show (image); + + g_object_set_data_full (G_OBJECT (image), + "filename", g_steal_pointer (&image_path), g_free); + + return image; + } + + pixbuf = round_image (source_pixbuf); + image = gtk_image_new_from_pixbuf (pixbuf); + gtk_image_set_pixel_size (GTK_IMAGE (image), AVATAR_CHOOSER_PIXEL_SIZE); + gtk_widget_show (image); + + g_object_set_data_full (G_OBJECT (image), + "filename", g_steal_pointer (&image_path), g_free); + + return image; +} + +static GStrv +get_settings_facesdirs (void) +{ + g_autoptr(GSettings) settings = g_settings_new ("org.gnome.desktop.interface"); + g_auto(GStrv) settings_dirs = g_settings_get_strv (settings, "avatar-directories"); + GPtrArray *facesdirs = g_ptr_array_new (); + + if (settings_dirs != NULL) { + int i; + for (i = 0; settings_dirs[i] != NULL; i++) { + char *path = settings_dirs[i]; + if (g_strcmp0 (path, "") != 0) + g_ptr_array_add (facesdirs, g_strdup (path)); + } + } + g_ptr_array_add (facesdirs, NULL); + + return (GStrv) g_ptr_array_steal (facesdirs, NULL); +} + +static GStrv +get_system_facesdirs (void) +{ + const char * const * data_dirs; + GPtrArray *facesdirs; + int i; + + facesdirs = g_ptr_array_new (); + + data_dirs = g_get_system_data_dirs (); + for (i = 0; data_dirs[i] != NULL; i++) { + char *path = g_build_filename (data_dirs[i], "pixmaps", "faces", NULL); + g_ptr_array_add (facesdirs, path); + } + g_ptr_array_add (facesdirs, NULL); + return (GStrv) g_ptr_array_steal (facesdirs, NULL); +} + +static gboolean +add_faces_from_dirs (GListStore *faces, GStrv facesdirs, gboolean add_all) +{ + GFile *file; + GFileType type; + const gchar *target; + guint i; + gboolean added_faces = FALSE; + + for (i = 0; facesdirs[i] != NULL; i++) { + g_autoptr(GFile) dir = NULL; + g_autoptr(GFileEnumerator) enumerator = NULL; + + dir = g_file_new_for_path (facesdirs[i]); + + enumerator = g_file_enumerate_children (dir, + G_FILE_ATTRIBUTE_STANDARD_NAME "," + G_FILE_ATTRIBUTE_STANDARD_TYPE "," + G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK "," + G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET, + G_FILE_QUERY_INFO_NONE, + NULL, NULL); + if (enumerator == NULL) { + continue; + } + + while (TRUE) { + g_autoptr(GFileInfo) info = g_file_enumerator_next_file (enumerator, NULL, NULL); + if (info == NULL) { + break; + } + + type = g_file_info_get_file_type (info); + if (type != G_FILE_TYPE_REGULAR && + type != G_FILE_TYPE_SYMBOLIC_LINK) { + continue; + } + + target = g_file_info_get_symlink_target (info); + if (target != NULL && g_str_has_prefix (target , "legacy/")) { + continue; + } + + file = g_file_get_child (dir, g_file_info_get_name (info)); + g_list_store_append (faces, file); + + added_faces = TRUE; + } + + g_file_enumerator_close (enumerator, NULL, NULL); + + if (added_faces && !add_all) + break; + } + return added_faces; +} + + +static void +setup_photo_popup (CcAvatarChooser *self) +{ + g_auto(GStrv) settings_facesdirs = NULL; + + self->faces = g_list_store_new (G_TYPE_FILE); + gtk_flow_box_bind_model (GTK_FLOW_BOX (self->flowbox), + G_LIST_MODEL (self->faces), + create_face_widget, + self, + NULL); + + g_signal_connect_object (self->flowbox, "child-activated", + G_CALLBACK (face_widget_activated), self, G_CONNECT_SWAPPED); + + settings_facesdirs = get_settings_facesdirs (); + + if (!add_faces_from_dirs (self->faces, settings_facesdirs, TRUE)) { + g_auto(GStrv) system_facesdirs = get_system_facesdirs (); + add_faces_from_dirs (self->faces, system_facesdirs, FALSE); + } +} + +CcAvatarChooser * +cc_avatar_chooser_new (GtkWidget *transient_for) +{ + CcAvatarChooser *self; + + self = g_object_new (CC_TYPE_AVATAR_CHOOSER, + NULL); + self->transient_for = transient_for; + self->thumb_factory = gnome_desktop_thumbnail_factory_new (GNOME_DESKTOP_THUMBNAIL_SIZE_NORMAL); + + setup_photo_popup (self); + + return self; +} + +static void +cc_avatar_chooser_dispose (GObject *object) +{ + CcAvatarChooser *self = CC_AVATAR_CHOOSER (object); + + g_clear_object (&self->thumb_factory); + g_clear_object (&self->user); + + G_OBJECT_CLASS (cc_avatar_chooser_parent_class)->dispose (object); +} + +static void +cc_avatar_chooser_init (CcAvatarChooser *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + +static void +cc_avatar_chooser_class_init (CcAvatarChooserClass *klass) +{ + GtkWidgetClass *wclass = GTK_WIDGET_CLASS (klass); + GObjectClass *oclass = G_OBJECT_CLASS (klass); + + gtk_widget_class_set_template_from_resource (wclass, "/org/gnome/control-center/user-accounts/cc-avatar-chooser.ui"); + + gtk_widget_class_bind_template_child (wclass, CcAvatarChooser, user_flowbox); + gtk_widget_class_bind_template_child (wclass, CcAvatarChooser, flowbox); + + gtk_widget_class_bind_template_callback (wclass, cc_avatar_chooser_select_file); + + oclass->dispose = cc_avatar_chooser_dispose; +} + +static void +user_flowbox_activated (CcAvatarChooser *self) +{ + set_default_avatar (self->user); + + gtk_popover_popdown (GTK_POPOVER (self)); +} + +void +cc_avatar_chooser_set_user (CcAvatarChooser *self, + ActUser *user) +{ + g_autoptr(GdkPixbuf) pixbuf = NULL; + const gchar *name; + GtkWidget *avatar; + + g_return_if_fail (self != NULL); + + if (self->user) { + GtkWidget *child; + + child = gtk_widget_get_first_child (GTK_WIDGET (self->user_flowbox)); + while (child) { + GtkWidget *next = gtk_widget_get_next_sibling (child); + + if (GTK_FLOW_BOX_CHILD (child)) + gtk_flow_box_remove (GTK_FLOW_BOX (self->user_flowbox), child); + + child = next; + } + + g_clear_object (&self->user); + } + self->user = g_object_ref (user); + + name = act_user_get_real_name (user); + if (name == NULL) + name = act_user_get_user_name (user); + avatar = adw_avatar_new (AVATAR_CHOOSER_PIXEL_SIZE, name, TRUE); + gtk_flow_box_append (GTK_FLOW_BOX (self->user_flowbox), avatar); + + g_signal_connect_object (self->user_flowbox, "child-activated", G_CALLBACK (user_flowbox_activated), self, G_CONNECT_SWAPPED); +} + diff --git a/panels/user-accounts/cc-avatar-chooser.h b/panels/user-accounts/cc-avatar-chooser.h new file mode 100644 index 0000000..879e482 --- /dev/null +++ b/panels/user-accounts/cc-avatar-chooser.h @@ -0,0 +1,39 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright 2009-2010 Red Hat, Inc, + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see <http://www.gnu.org/licenses/>. + * + * Written by: Matthias Clasen <mclasen@redhat.com> + */ + +#pragma once + +#include <gtk/gtk.h> +#include <act/act.h> + +G_BEGIN_DECLS + +#define CC_TYPE_AVATAR_CHOOSER (cc_avatar_chooser_get_type()) + +G_DECLARE_FINAL_TYPE (CcAvatarChooser, cc_avatar_chooser, CC, AVATAR_CHOOSER, GtkPopover) + +typedef struct _CcAvatarChooser CcAvatarChooser; + +CcAvatarChooser *cc_avatar_chooser_new (GtkWidget *transient_for); +void cc_avatar_chooser_free (CcAvatarChooser *dialog); +void cc_avatar_chooser_set_user (CcAvatarChooser *dialog, + ActUser *user); + +G_END_DECLS diff --git a/panels/user-accounts/cc-avatar-chooser.ui b/panels/user-accounts/cc-avatar-chooser.ui new file mode 100644 index 0000000..fe1696f --- /dev/null +++ b/panels/user-accounts/cc-avatar-chooser.ui @@ -0,0 +1,41 @@ +<?xml version="1.0"?> +<interface> + <!-- interface-requires gtk+ 3.8 --> + <template class="CcAvatarChooser" parent="GtkPopover"> + <property name="visible">False</property> + <child> + <object class="GtkBox"> + <property name="orientation">GTK_ORIENTATION_VERTICAL</property> + <property name="spacing">10</property> + <child> + <object class="GtkFlowBox" id="user_flowbox"> + <property name="selection-mode">none</property> + <property name="homogeneous">True</property> + <property name="max-children-per-line">5</property> + <property name="column-spacing">10</property> + </object> + </child> + <child> + <object class="GtkFlowBox" id="flowbox"> + <property name="selection-mode">none</property> + <property name="homogeneous">True</property> + <property name="max-children-per-line">5</property> + <property name="column-spacing">10</property> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="halign">GTK_ALIGN_CENTER</property> + <property name="spacing">20</property> + <child> + <object class="GtkButton"> + <property name="label" translatable="yes">Select a File…</property> + <signal name="clicked" handler="cc_avatar_chooser_select_file" swapped="yes"/> + </object> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/panels/user-accounts/cc-crop-area.c b/panels/user-accounts/cc-crop-area.c new file mode 100644 index 0000000..a644b60 --- /dev/null +++ b/panels/user-accounts/cc-crop-area.c @@ -0,0 +1,717 @@ +/* + * Copyright 2021 Red Hat, Inc, + * + * Authors: + * - Matthias Clasen <mclasen@redhat.com> + * - Niels De Graef <nielsdg@redhat.com> + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see <http://www.gnu.org/licenses/>. + */ + +#include "config.h" + +#include <glib.h> +#include <glib/gi18n.h> +#include <gtk/gtk.h> +#include <gsk/gl/gskglrenderer.h> + +#include "cc-crop-area.h" + +/** + * CcCropArea: + * + * A widget that shows a [iface@Gdk.Paintable] and allows the user specify a + * cropping rectangle to effectively crop to that given area. + */ + +/* Location of the cursor relative to the cropping rectangle/circle */ +typedef enum { + OUTSIDE, + INSIDE, + TOP, + TOP_LEFT, + TOP_RIGHT, + BOTTOM, + BOTTOM_LEFT, + BOTTOM_RIGHT, + LEFT, + RIGHT +} Location; + +struct _CcCropArea { + GtkWidget parent_instance; + + GdkPaintable *paintable; + + double scale; /* scale factor to go from paintable size to widget size */ + + const char *current_cursor; + Location active_region; + double drag_offx; + double drag_offy; + + /* In source coordinates. See get_scaled_crop() for widget coordinates */ + GdkRectangle crop; + + /* In widget coordinates */ + GdkRectangle image; + int min_crop_width; + int min_crop_height; +}; + +G_DEFINE_TYPE (CcCropArea, cc_crop_area, GTK_TYPE_WIDGET); + +static void +update_image_and_crop (CcCropArea *area) +{ + GtkAllocation allocation; + int width, height; + int dest_width, dest_height; + double scale; + + if (area->paintable == NULL) + return; + + gtk_widget_get_allocation (GTK_WIDGET (area), &allocation); + + /* Get the size of the paintable */ + width = gdk_paintable_get_intrinsic_width (area->paintable); + height = gdk_paintable_get_intrinsic_height (area->paintable); + + /* Find out the scale to convert to widget width/height */ + scale = allocation.height / (double) height; + if (scale * width > allocation.width) + scale = allocation.width / (double) width; + + dest_width = width * scale; + dest_height = height * scale; + + if (area->scale == 0.0) { + double scale_to_80, scale_to_image, crop_scale; + + /* Start with a crop area of 80% of the area, unless it's larger than min_size */ + scale_to_80 = MIN ((double) dest_width * 0.8, (double) dest_height * 0.8); + scale_to_image = MIN ((double) area->min_crop_width, (double) area->min_crop_height); + crop_scale = MAX (scale_to_80, scale_to_image); + + /* Divide by `scale` to get back to paintable coordinates */ + area->crop.width = crop_scale / scale; + area->crop.height = crop_scale / scale; + area->crop.x = (width - area->crop.width) / 2; + area->crop.y = (height - area->crop.height) / 2; + } + + area->scale = scale; + area->image.x = (allocation.width - dest_width) / 2; + area->image.y = (allocation.height - dest_height) / 2; + area->image.width = dest_width; + area->image.height = dest_height; +} + +/* Returns area->crop in widget coordinates (vs paintable coordsinates) */ +static void +get_scaled_crop (CcCropArea *area, + GdkRectangle *crop) +{ + crop->x = area->image.x + area->crop.x * area->scale; + crop->y = area->image.y + area->crop.y * area->scale; + crop->width = area->crop.width * area->scale; + crop->height = area->crop.height * area->scale; +} + +typedef enum { + BELOW, + LOWER, + BETWEEN, + UPPER, + ABOVE +} Range; + +static Range +find_range (int x, + int min, + int max) +{ + int tolerance = 12; + + if (x < min - tolerance) + return BELOW; + if (x <= min + tolerance) + return LOWER; + if (x < max - tolerance) + return BETWEEN; + if (x <= max + tolerance) + return UPPER; + return ABOVE; +} + +/* Finds the location of (@x, @y) relative to the crop @rect */ +static Location +find_location (GdkRectangle *rect, + int x, + int y) +{ + Range x_range, y_range; + Location location[5][5] = { + { OUTSIDE, OUTSIDE, OUTSIDE, OUTSIDE, OUTSIDE }, + { OUTSIDE, TOP_LEFT, TOP, TOP_RIGHT, OUTSIDE }, + { OUTSIDE, LEFT, INSIDE, RIGHT, OUTSIDE }, + { OUTSIDE, BOTTOM_LEFT, BOTTOM, BOTTOM_RIGHT, OUTSIDE }, + { OUTSIDE, OUTSIDE, OUTSIDE, OUTSIDE, OUTSIDE } + }; + + x_range = find_range (x, rect->x, rect->x + rect->width); + y_range = find_range (y, rect->y, rect->y + rect->height); + + return location[y_range][x_range]; +} + +static void +update_cursor (CcCropArea *area, + int x, + int y) +{ + const char *cursor_type; + GdkRectangle crop; + int region; + + region = area->active_region; + if (region == OUTSIDE) { + get_scaled_crop (area, &crop); + region = find_location (&crop, x, y); + } + + switch (region) { + case OUTSIDE: + cursor_type = "default"; + break; + case TOP_LEFT: + cursor_type = "nw-resize"; + break; + case TOP: + cursor_type = "n-resize"; + break; + case TOP_RIGHT: + cursor_type = "ne-resize"; + break; + case LEFT: + cursor_type = "w-resize"; + break; + case INSIDE: + cursor_type = "move"; + break; + case RIGHT: + cursor_type = "e-resize"; + break; + case BOTTOM_LEFT: + cursor_type = "sw-resize"; + break; + case BOTTOM: + cursor_type = "s-resize"; + break; + case BOTTOM_RIGHT: + cursor_type = "se-resize"; + break; + default: + g_assert_not_reached (); + } + + if (cursor_type != area->current_cursor) { + GtkNative *native; + g_autoptr (GdkCursor) cursor = NULL; + + native = gtk_widget_get_native (GTK_WIDGET (area)); + if (!native) { + g_warning ("Can't adjust cursor: no GtkNative found"); + return; + } + cursor = gdk_cursor_new_from_name (cursor_type, NULL); + gdk_surface_set_cursor (gtk_native_get_surface (native), cursor); + area->current_cursor = cursor_type; + } +} + +static int +eval_radial_line (double center_x, double center_y, + double bounds_x, double bounds_y, + double user_x) +{ + double decision_slope; + double decision_intercept; + + decision_slope = (bounds_y - center_y) / (bounds_x - center_x); + decision_intercept = -(decision_slope * bounds_x); + + return (int) (decision_slope * user_x + decision_intercept); +} + +static gboolean +on_motion (GtkEventControllerMotion *controller, + double event_x, + double event_y, + void *user_data) +{ + CcCropArea *area = CC_CROP_AREA (user_data); + + if (area->paintable == NULL) + return FALSE; + + update_cursor (area, event_x, event_y); + + return FALSE; +} + +static void +on_leave (GtkEventControllerMotion *controller, + void *user_data) +{ + CcCropArea *area = CC_CROP_AREA (user_data); + + if (area->paintable == NULL) + return; + + /* Restore 'default' cursor */ + update_cursor (area, 0, 0); +} + +static void +on_drag_begin (GtkGestureDrag *gesture, + double start_x, + double start_y, + void *user_data) +{ + CcCropArea *area = CC_CROP_AREA (user_data); + GdkRectangle crop; + + if (area->paintable == NULL) + return; + + update_cursor (area, start_x, start_y); + + get_scaled_crop (area, &crop); + + area->active_region = find_location (&crop, start_x, start_y); + + area->drag_offx = 0.0; + area->drag_offy = 0.0; +} + +static void +on_drag_update (GtkGestureDrag *gesture, + double offset_x, + double offset_y, + void *user_data) +{ + CcCropArea *area = CC_CROP_AREA (user_data); + double start_x, start_y; + int x, y, delta_x, delta_y; + int width, height; + int adj_width, adj_height; + int pb_width, pb_height; + int left, right, top, bottom; + double new_width, new_height; + double center_x, center_y; + int min_width, min_height; + + pb_width = gdk_paintable_get_intrinsic_width (area->paintable); + pb_height = gdk_paintable_get_intrinsic_height (area->paintable); + + gtk_gesture_drag_get_start_point (gesture, &start_x, &start_y); + + /* Get the x, y, dx, dy in paintable coords */ + x = (start_x + offset_x - area->image.x) / area->scale; + y = (start_y + offset_y - area->image.y) / area->scale; + delta_x = (offset_x - area->drag_offx) / area->scale; + delta_y = (offset_y - area->drag_offy) / area->scale; + + /* Helper variables */ + left = area->crop.x; + right = area->crop.x + area->crop.width - 1; + top = area->crop.y; + bottom = area->crop.y + area->crop.height - 1; + + center_x = (left + right) / 2.0; + center_y = (top + bottom) / 2.0; + + /* What we have to do depends on where the user started dragging */ + switch (area->active_region) { + case INSIDE: + width = right - left + 1; + height = bottom - top + 1; + + left = MAX (left + delta_x, 0); + right = MIN (right + delta_x, pb_width); + top = MAX (top + delta_y, 0); + bottom = MIN (bottom + delta_y, pb_height); + + adj_width = right - left + 1; + adj_height = bottom - top + 1; + if (adj_width != width) { + if (delta_x < 0) + right = left + width - 1; + else + left = right - width + 1; + } + if (adj_height != height) { + if (delta_y < 0) + bottom = top + height - 1; + else + top = bottom - height + 1; + } + + break; + + case TOP_LEFT: + if (y < eval_radial_line (center_x, center_y, left, top, x)) { + top = y; + new_width = bottom - top; + left = right - new_width; + } else { + left = x; + new_height = right - left; + top = bottom - new_height; + } + break; + + case TOP: + top = y; + new_width = bottom - top; + right = left + new_width; + break; + + case TOP_RIGHT: + if (y < eval_radial_line (center_x, center_y, right, top, x)) { + top = y; + new_width = bottom - top; + right = left + new_width; + } else { + right = x; + new_height = right - left; + top = bottom - new_height; + } + break; + + case LEFT: + left = x; + new_height = right - left; + bottom = top + new_height; + break; + + case BOTTOM_LEFT: + if (y < eval_radial_line (center_x, center_y, left, bottom, x)) { + left = x; + new_height = right - left; + bottom = top + new_height; + } else { + bottom = y; + new_width = bottom - top; + left = right - new_width; + } + break; + + case RIGHT: + right = x; + new_height = right - left; + bottom = top + new_height; + break; + + case BOTTOM_RIGHT: + if (y < eval_radial_line (center_x, center_y, right, bottom, x)) { + right = x; + new_height = right - left; + bottom = top + new_height; + } else { + bottom = y; + new_width = bottom - top; + right = left + new_width; + } + break; + + case BOTTOM: + bottom = y; + new_width = bottom - top; + right= left + new_width; + break; + + default: + return; + } + + min_width = area->min_crop_width / area->scale; + min_height = area->min_crop_height / area->scale; + + width = right - left + 1; + height = bottom - top + 1; + if (left < 0 || top < 0 || + right > pb_width || bottom > pb_height || + width < min_width || height < min_height) { + left = area->crop.x; + right = area->crop.x + area->crop.width - 1; + top = area->crop.y; + bottom = area->crop.y + area->crop.height - 1; + } + + area->crop.x = left; + area->crop.y = top; + area->crop.width = right - left + 1; + area->crop.height = bottom - top + 1; + + area->drag_offx = offset_x; + area->drag_offy = offset_y; + + gtk_widget_queue_draw (GTK_WIDGET (area)); +} + +static void +on_drag_end (GtkGestureDrag *gesture, + double offset_x, + double offset_y, + void *user_data) +{ + CcCropArea *area = CC_CROP_AREA (user_data); + + area->active_region = OUTSIDE; + area->drag_offx = 0.0; + area->drag_offy = 0.0; +} + +static void +on_drag_cancel (GtkGesture *gesture, + GdkEventSequence *sequence, + void *user_data) +{ + CcCropArea *area = CC_CROP_AREA (user_data); + + area->active_region = OUTSIDE; + area->drag_offx = 0; + area->drag_offy = 0; +} + +static void +cc_crop_area_snapshot (GtkWidget *widget, + GtkSnapshot *snapshot) +{ + CcCropArea *area = CC_CROP_AREA (widget); + cairo_t *cr; + GdkRectangle crop; + + if (area->paintable == NULL) + return; + + update_image_and_crop (area); + + + gtk_snapshot_save (snapshot); + + /* First draw the picture */ + gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (area->image.x, area->image.y)); + + gdk_paintable_snapshot (area->paintable, snapshot, area->image.width, area->image.height); + + /* Draw the cropping UI on top with cairo */ + cr = gtk_snapshot_append_cairo (snapshot, &GRAPHENE_RECT_INIT (0, 0, area->image.width, area->image.height)); + + get_scaled_crop (area, &crop); + crop.x -= area->image.x; + crop.y -= area->image.y; + + /* Draw the circle */ + cairo_save (cr); + cairo_arc (cr, crop.x + crop.width / 2, crop.y + crop.width / 2, crop.width / 2, 0, 2 * G_PI); + cairo_rectangle (cr, 0, 0, area->image.width, area->image.height); + cairo_set_source_rgba (cr, 0, 0, 0, 0.4); + cairo_set_fill_rule (cr, CAIRO_FILL_RULE_EVEN_ODD); + cairo_fill (cr); + cairo_restore (cr); + + /* draw the four corners */ + cairo_set_source_rgb (cr, 1, 1, 1); + cairo_set_line_width (cr, 4.0); + + /* top left corner */ + cairo_move_to (cr, crop.x + 15, crop.y); + cairo_line_to (cr, crop.x, crop.y); + cairo_line_to (cr, crop.x, crop.y + 15); + /* top right corner */ + cairo_move_to (cr, crop.x + crop.width - 15, crop.y); + cairo_line_to (cr, crop.x + crop.width, crop.y); + cairo_line_to (cr, crop.x + crop.width, crop.y + 15); + /* bottom right corner */ + cairo_move_to (cr, crop.x + crop.width - 15, crop.y + crop.height); + cairo_line_to (cr, crop.x + crop.width, crop.y + crop.height); + cairo_line_to (cr, crop.x + crop.width, crop.y + crop.height - 15); + /* bottom left corner */ + cairo_move_to (cr, crop.x + 15, crop.y + crop.height); + cairo_line_to (cr, crop.x, crop.y + crop.height); + cairo_line_to (cr, crop.x, crop.y + crop.height - 15); + + cairo_stroke (cr); + + gtk_snapshot_restore (snapshot); +} + +static void +cc_crop_area_finalize (GObject *object) +{ + CcCropArea *area = CC_CROP_AREA (object); + + g_clear_object (&area->paintable); +} + +static void +cc_crop_area_class_init (CcCropAreaClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = cc_crop_area_finalize; + + widget_class->snapshot = cc_crop_area_snapshot; +} + +static void +cc_crop_area_init (CcCropArea *area) +{ + GtkGesture *gesture; + GtkEventController *controller; + + /* Add handlers for dragging */ + gesture = gtk_gesture_drag_new (); + g_signal_connect (gesture, "drag-begin", G_CALLBACK (on_drag_begin), area); + g_signal_connect (gesture, "drag-update", G_CALLBACK (on_drag_update), + area); + g_signal_connect (gesture, "drag-end", G_CALLBACK (on_drag_end), area); + g_signal_connect (gesture, "cancel", G_CALLBACK (on_drag_cancel), area); + gtk_widget_add_controller (GTK_WIDGET (area), GTK_EVENT_CONTROLLER (gesture)); + + /* Add handlers for motion events */ + controller = gtk_event_controller_motion_new (); + g_signal_connect (controller, "motion", G_CALLBACK (on_motion), area); + g_signal_connect (controller, "leave", G_CALLBACK (on_leave), area); + gtk_widget_add_controller (GTK_WIDGET (area), GTK_EVENT_CONTROLLER (controller)); + + area->scale = 0.0; + area->image.x = 0; + area->image.y = 0; + area->image.width = 0; + area->image.height = 0; + area->active_region = OUTSIDE; + area->min_crop_width = 48; + area->min_crop_height = 48; + + gtk_widget_set_size_request (GTK_WIDGET (area), 48, 48); +} + +GtkWidget * +cc_crop_area_new (void) +{ + return g_object_new (CC_TYPE_CROP_AREA, NULL); +} + +/** + * cc_crop_area_create_pixbuf: + * @area: A crop area + * + * Renders the area's paintable, with the cropping applied by the user, into a + * GdkPixbuf. + * + * Returns: (transfer full): The cropped picture + */ +GdkPixbuf * +cc_crop_area_create_pixbuf (CcCropArea *area) +{ + g_autoptr (GtkSnapshot) snapshot = NULL; + g_autoptr (GskRenderNode) node = NULL; + g_autoptr (GskRenderer) renderer = NULL; + g_autoptr (GdkTexture) texture = NULL; + g_autoptr (GError) error = NULL; + graphene_rect_t viewport; + + g_return_val_if_fail (CC_IS_CROP_AREA (area), NULL); + + snapshot = gtk_snapshot_new (); + gdk_paintable_snapshot (area->paintable, snapshot, + gdk_paintable_get_intrinsic_width (area->paintable), + gdk_paintable_get_intrinsic_height (area->paintable)); + node = gtk_snapshot_free_to_node (g_steal_pointer (&snapshot)); + + renderer = gsk_gl_renderer_new (); + if (!gsk_renderer_realize (renderer, NULL, &error)) { + g_warning ("Couldn't realize GL renderer: %s", error->message); + return NULL; + } + viewport = GRAPHENE_RECT_INIT (area->crop.x, area->crop.y, + area->crop.width, area->crop.height); + texture = gsk_renderer_render_texture (renderer, node, &viewport); + gsk_renderer_unrealize (renderer); + + return gdk_pixbuf_get_from_texture (texture); +} + +/** + * cc_crop_area_get_paintable: + * @area: A crop area + * + * Returns the area's paintable, unmodified. + * + * Returns: (transfer none) (nullable): The paintable which the user can crop + */ +GdkPaintable * +cc_crop_area_get_paintable (CcCropArea *area) +{ + g_return_val_if_fail (CC_IS_CROP_AREA (area), NULL); + + return area->paintable; +} + +void +cc_crop_area_set_paintable (CcCropArea *area, + GdkPaintable *paintable) +{ + g_return_if_fail (CC_IS_CROP_AREA (area)); + g_return_if_fail (GDK_IS_PAINTABLE (paintable)); + + g_set_object (&area->paintable, paintable); + + area->scale = 0.0; + area->image.x = 0; + area->image.y = 0; + area->image.width = 0; + area->image.height = 0; + + gtk_widget_queue_draw (GTK_WIDGET (area)); +} + +/** + * cc_crop_area_set_min_size: + * @area: A crop widget + * @width: The minimal width + * @height: The minimal height + * + * Sets the minimal size of the crop rectangle (in paintable coordinates) + */ +void +cc_crop_area_set_min_size (CcCropArea *area, + int width, + int height) +{ + g_return_if_fail (CC_IS_CROP_AREA (area)); + + area->min_crop_width = width; + area->min_crop_height = height; + + gtk_widget_set_size_request (GTK_WIDGET (area), + area->min_crop_width, + area->min_crop_height); +} diff --git a/panels/user-accounts/cc-crop-area.h b/panels/user-accounts/cc-crop-area.h new file mode 100644 index 0000000..da80024 --- /dev/null +++ b/panels/user-accounts/cc-crop-area.h @@ -0,0 +1,42 @@ +/* + * Copyright © 2009 Bastien Nocera <hadess@hadess.net> + * + * Licensed under the GNU General Public License Version 2 + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef _CC_CROP_AREA_H_ +#define _CC_CROP_AREA_H_ + +#include <glib-object.h> +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define CC_TYPE_CROP_AREA (cc_crop_area_get_type ()) +G_DECLARE_FINAL_TYPE (CcCropArea, cc_crop_area, CC, CROP_AREA, GtkWidget) + +GtkWidget * cc_crop_area_new (void); +GdkPaintable * cc_crop_area_get_paintable (CcCropArea *area); +void cc_crop_area_set_paintable (CcCropArea *area, + GdkPaintable *paintable); +void cc_crop_area_set_min_size (CcCropArea *area, + int width, + int height); +GdkPixbuf * cc_crop_area_create_pixbuf (CcCropArea *area); + +G_END_DECLS + +#endif /* _CC_CROP_AREA_H_ */ diff --git a/panels/user-accounts/cc-fingerprint-dialog.c b/panels/user-accounts/cc-fingerprint-dialog.c new file mode 100644 index 0000000..b8ebba6 --- /dev/null +++ b/panels/user-accounts/cc-fingerprint-dialog.c @@ -0,0 +1,1527 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2020 Canonical Ltd. + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authors: Marco Trevisan <marco.trevisan@canonical.com> + */ + +#include <glib/gi18n.h> +#include <cairo/cairo.h> + +#include "cc-fingerprint-dialog.h" + +#include "cc-fingerprint-manager.h" +#include "cc-fprintd-generated.h" +#include "cc-list-row.h" + +#include "config.h" + +#define CC_FPRINTD_NAME "net.reactivated.Fprint" + +/* Translate fprintd strings */ +#define TR(s) dgettext ("fprintd", s) +#include "fingerprint-strings.h" + +typedef enum { + DIALOG_STATE_NONE = 0, + DIALOG_STATE_DEVICES_LISTING = (1 << 0), + DIALOG_STATE_DEVICE_CLAIMING = (1 << 1), + DIALOG_STATE_DEVICE_CLAIMED = (1 << 2), + DIALOG_STATE_DEVICE_PRINTS_LISTING = (1 << 3), + DIALOG_STATE_DEVICE_RELEASING = (1 << 4), + DIALOG_STATE_DEVICE_ENROLL_STARTING = (1 << 5), + DIALOG_STATE_DEVICE_ENROLLING = (1 << 6), + DIALOG_STATE_DEVICE_ENROLL_STOPPING = (1 << 7), + DIALOG_STATE_DEVICE_DELETING = (1 << 8), + + DIALOG_STATE_IDLE = DIALOG_STATE_DEVICE_CLAIMED | DIALOG_STATE_DEVICE_ENROLLING, +} DialogState; + +struct _CcFingerprintDialog +{ + GtkWindow parent_instance; + + GtkButton *back_button; + GtkButton *cancel_button; + GtkButton *delete_prints_button; + GtkButton *done_button; + GtkBox *add_print_popover_box; + GtkEntry *enroll_print_entry; + GtkFlowBox *prints_gallery; + GtkHeaderBar *titlebar; + GtkImage *enroll_result_image; + GtkLabel *enroll_message; + GtkLabel *enroll_result_message; + GtkLabel *infobar_error; + GtkLabel *title; + GtkListBox *devices_list; + GtkPopover *add_print_popover; + GtkSpinner *spinner; + GtkStack *stack; + GtkWidget *add_print_icon; + GtkWidget *delete_confirmation_infobar; + GtkWidget *device_selector; + GtkWidget *enroll_print_bin; + GtkWidget *enroll_result_icon; + GtkWidget *enrollment_view; + GtkWidget *error_infobar; + GtkWidget *no_devices_found; + GtkWidget *prints_manager; + + CcFingerprintManager *manager; + DialogState dialog_state; + CcFprintdDevice *device; + gulong device_signal_id; + gulong device_name_owner_id; + GCancellable *cancellable; + GStrv enrolled_fingers; + guint enroll_stages_passed; + guint enroll_stage_passed_id; + gdouble enroll_progress; +}; + +/* TODO - fprintd and API changes required: + - Identify the finger when the enroll dialog is visible + + Only if device supports identification + · And only in such case support enrolling more than one finger + - Delete a single fingerprint | and remove the "Delete all" button + - Highlight the finger when the sensor is touched during enrollment + - Add customized labels to fingerprints + - Devices hotplug (object manager) + */ + +G_DEFINE_TYPE (CcFingerprintDialog, cc_fingerprint_dialog, GTK_TYPE_WINDOW) + +enum { + PROP_0, + PROP_MANAGER, + N_PROPS +}; + +#define N_VALID_FINGERS G_N_ELEMENTS (FINGER_IDS) - 1 +/* The order of the fingers here will affect the UI order */ +const char * FINGER_IDS[] = { + "right-index-finger", + "left-index-finger", + "right-thumb", + "right-middle-finger", + "right-ring-finger", + "right-little-finger", + "left-thumb", + "left-middle-finger", + "left-ring-finger", + "left-little-finger", + "any", +}; + +typedef enum { + ENROLL_STATE_NORMAL, + ENROLL_STATE_RETRY, + ENROLL_STATE_SUCCESS, + ENROLL_STATE_WARNING, + ENROLL_STATE_ERROR, + ENROLL_STATE_COMPLETED, + N_ENROLL_STATES, +} EnrollState; + +const char * ENROLL_STATE_CLASSES[N_ENROLL_STATES] = { + "", + "retry", + "success", + "warning", + "error", + "completed", +}; + +static GParamSpec *properties[N_PROPS]; + +CcFingerprintDialog * +cc_fingerprint_dialog_new (CcFingerprintManager *manager) +{ + return g_object_new (CC_TYPE_FINGERPRINT_DIALOG, + "fingerprint-manager", manager, + NULL); +} + +static gboolean +update_dialog_state (CcFingerprintDialog *self, + DialogState state) +{ + if (self->dialog_state == state) + return FALSE; + + self->dialog_state = state; + + if (self->dialog_state == DIALOG_STATE_NONE || + self->dialog_state == (self->dialog_state & DIALOG_STATE_IDLE)) + { + gtk_spinner_stop (self->spinner); + } + else + { + gtk_spinner_start (self->spinner); + } + + return TRUE; +} + +static gboolean +add_dialog_state (CcFingerprintDialog *self, + DialogState state) +{ + return update_dialog_state (self, (self->dialog_state | state)); +} + +static gboolean +remove_dialog_state (CcFingerprintDialog *self, + DialogState state) +{ + return update_dialog_state (self, (self->dialog_state & ~state)); +} + +typedef struct +{ + CcFingerprintDialog *dialog; + DialogState state; +} DialogStateRemover; + +static DialogStateRemover * +auto_state_remover (CcFingerprintDialog *self, + DialogState state) +{ + DialogStateRemover *state_remover; + + state_remover = g_new0 (DialogStateRemover, 1); + state_remover->dialog = g_object_ref (self); + state_remover->state = state; + + return state_remover; +} + +static void +auto_state_remover_cleanup (DialogStateRemover *state_remover) +{ + remove_dialog_state (state_remover->dialog, state_remover->state); + g_clear_object (&state_remover->dialog); + g_free (state_remover); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (DialogStateRemover, auto_state_remover_cleanup); + +static const char * +dbus_error_to_human (CcFingerprintDialog *self, + GError *error) +{ + g_autofree char *dbus_error = g_dbus_error_get_remote_error (error); + + if (dbus_error == NULL) + { /* Fallback to generic */ } + else if (g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.ClaimDevice")) + return _("the device needs to be claimed to perform this action"); + else if (g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.AlreadyInUse")) + return _("the device is already claimed by another process"); + else if (g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.PermissionDenied")) + return _("you do not have permission to perform the action"); + else if (g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.NoEnrolledPrints")) + return _("no prints have been enrolled"); + else if (g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.NoActionInProgress")) + { /* Fallback to generic */ } + else if (g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.InvalidFingername")) + { /* Fallback to generic */ } + else if (g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.Internal")) + { /* Fallback to generic */ } + + if (self->dialog_state & DIALOG_STATE_DEVICE_ENROLLING) + return _("Failed to communicate with the device during enrollment"); + + if (self->dialog_state & DIALOG_STATE_DEVICE_CLAIMED || + self->dialog_state & DIALOG_STATE_DEVICE_CLAIMING) + return _("Failed to communicate with the fingerprint reader"); + + return _("Failed to communicate with the fingerprint daemon"); +} + +static void +disconnect_device_signals (CcFingerprintDialog *self) +{ + if (!self->device) + return; + + if (self->device_signal_id) + { + g_signal_handler_disconnect (self->device, self->device_signal_id); + self->device_signal_id = 0; + } + + if (self->device_name_owner_id) + { + g_signal_handler_disconnect (self->device, self->device_name_owner_id); + self->device_name_owner_id = 0; + } +} + +static void +cc_fingerprint_dialog_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + CcFingerprintDialog *self = CC_FINGERPRINT_DIALOG (object); + + switch (prop_id) + { + case PROP_MANAGER: + g_value_set_object (value, self->manager); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +cc_fingerprint_dialog_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + CcFingerprintDialog *self = CC_FINGERPRINT_DIALOG (object); + + switch (prop_id) + { + case PROP_MANAGER: + g_set_object (&self->manager, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +notify_error (CcFingerprintDialog *self, + const char *error_message) +{ + if (error_message) + gtk_label_set_label (self->infobar_error, error_message); + + gtk_widget_set_visible (self->error_infobar, error_message != NULL); +} + +static GtkWidget * +fingerprint_icon_new (const char *icon_name, + const char *label_text, + GType icon_widget_type, + gpointer progress_data, + GtkWidget **out_icon, + GtkWidget **out_label) +{ + GtkStyleContext *context; + GtkWidget *box; + GtkWidget *label; + GtkWidget *image; + GtkWidget *icon_widget; + + g_return_val_if_fail (g_type_is_a (icon_widget_type, GTK_TYPE_WIDGET), NULL); + + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10); + gtk_widget_set_name (box, "fingerprint-box"); + gtk_widget_set_hexpand (box, TRUE); + + image = gtk_image_new_from_icon_name (icon_name); + + if (icon_widget_type == GTK_TYPE_IMAGE) + icon_widget = image; + else + icon_widget = g_object_new (icon_widget_type, NULL); + + if (g_type_is_a (icon_widget_type, GTK_TYPE_MENU_BUTTON)) + { + gtk_menu_button_set_child (GTK_MENU_BUTTON (icon_widget), image); + gtk_widget_set_can_focus (icon_widget, FALSE); + } + + gtk_widget_set_halign (icon_widget, GTK_ALIGN_CENTER); + gtk_widget_set_valign (icon_widget, GTK_ALIGN_CENTER); + gtk_widget_set_name (icon_widget, "fingerprint-image"); + + gtk_box_append (GTK_BOX (box), icon_widget); + + context = gtk_widget_get_style_context (icon_widget); + gtk_style_context_add_class (context, "circular"); + + label = gtk_label_new_with_mnemonic (label_text); + gtk_box_append (GTK_BOX (box), label); + + context = gtk_widget_get_style_context (box); + gtk_style_context_add_class (context, "fingerprint-icon"); + + if (out_icon) + *out_icon = icon_widget; + + if (out_label) + *out_label = label; + + return box; +} + +static GtkWidget * +fingerprint_menu_button (const char *icon_name, + const char *label_text) +{ + GtkWidget *flowbox_child; + GtkWidget *button; + GtkWidget *label; + GtkWidget *box; + + box = fingerprint_icon_new (icon_name, label_text, GTK_TYPE_MENU_BUTTON, NULL, + &button, &label); + + flowbox_child = gtk_flow_box_child_new (); + gtk_widget_set_focus_on_click (flowbox_child, FALSE); + gtk_widget_set_name (flowbox_child, "fingerprint-flowbox"); + + gtk_flow_box_child_set_child (GTK_FLOW_BOX_CHILD (flowbox_child), box); + + g_object_set_data (G_OBJECT (flowbox_child), "button", button); + g_object_set_data (G_OBJECT (flowbox_child), "icon", + GTK_IMAGE (gtk_menu_button_get_child (GTK_MENU_BUTTON (button)))); + g_object_set_data (G_OBJECT (flowbox_child), "label", label); + g_object_set_data (G_OBJECT (button), "flowbox-child", flowbox_child); + + return flowbox_child; +} + +static gboolean +prints_visibility_filter (GtkFlowBoxChild *child, + gpointer user_data) +{ + CcFingerprintDialog *self = user_data; + const char *finger_id; + + if (gtk_stack_get_visible_child (self->stack) != self->prints_manager) + return FALSE; + + finger_id = g_object_get_data (G_OBJECT (child), "finger-id"); + + if (!finger_id) + return TRUE; + + if (!self->enrolled_fingers) + return FALSE; + + return g_strv_contains ((const gchar **) self->enrolled_fingers, finger_id); +} + +static GList * +get_container_children (GtkWidget *container) +{ + GtkWidget *child; + GList *list = NULL; + + child = gtk_widget_get_first_child (container); + while (child) { + GtkWidget *next = gtk_widget_get_next_sibling (child); + + list = g_list_append (list, child); + + child = next; + } + + return list; +} + +static void +update_prints_to_add_visibility (CcFingerprintDialog *self) +{ + g_autoptr(GList) print_buttons = NULL; + GList *l; + guint i; + + print_buttons = get_container_children (GTK_WIDGET (self->add_print_popover_box)); + + for (i = 0, l = print_buttons; i < N_VALID_FINGERS && l; ++i, l = l->next) + { + GtkWidget *button = l->data; + gboolean enrolled; + + enrolled = self->enrolled_fingers && + g_strv_contains ((const gchar **) self->enrolled_fingers, + FINGER_IDS[i]); + + gtk_widget_set_visible (button, !enrolled); + } +} + +static void +update_prints_visibility (CcFingerprintDialog *self) +{ + update_prints_to_add_visibility (self); + + gtk_flow_box_invalidate_filter (self->prints_gallery); +} + +static void +list_enrolled_cb (GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + g_auto(GStrv) enrolled_fingers = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(DialogStateRemover) state_remover = NULL; + CcFprintdDevice *fprintd_device = CC_FPRINTD_DEVICE (object); + CcFingerprintDialog *self = user_data; + guint n_enrolled_fingers = 0; + + cc_fprintd_device_call_list_enrolled_fingers_finish (fprintd_device, + &enrolled_fingers, + res, &error); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + state_remover = auto_state_remover (self, DIALOG_STATE_DEVICE_PRINTS_LISTING); + + gtk_widget_set_sensitive (GTK_WIDGET (self->add_print_icon), TRUE); + + if (self->dialog_state & DIALOG_STATE_DEVICE_CLAIMED) + gtk_widget_set_sensitive (GTK_WIDGET (self->prints_manager), TRUE); + + if (error) + { + g_autofree char *dbus_error = g_dbus_error_get_remote_error (error); + + if (!dbus_error || !g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.NoEnrolledPrints")) + { + g_autofree char *error_message = NULL; + + error_message = g_strdup_printf (_("Failed to list fingerprints: %s"), + dbus_error_to_human (self, error)); + g_warning ("Listing of fingerprints on device %s failed: %s", + cc_fprintd_device_get_name (self->device), error->message); + notify_error (self, error_message); + return; + } + } + else + { + n_enrolled_fingers = g_strv_length (enrolled_fingers); + } + + self->enrolled_fingers = g_steal_pointer (&enrolled_fingers); + gtk_flow_box_set_max_children_per_line (self->prints_gallery, + MIN (3, n_enrolled_fingers + 1)); + + update_prints_visibility (self); + + if (n_enrolled_fingers == N_VALID_FINGERS) + gtk_widget_set_sensitive (self->add_print_icon, FALSE); + + if (n_enrolled_fingers > 0) + gtk_widget_show (GTK_WIDGET (self->delete_prints_button)); +} + +static void +update_prints_store (CcFingerprintDialog *self) +{ + ActUser *user; + + g_assert_true (CC_FPRINTD_IS_DEVICE (self->device)); + + if (!add_dialog_state (self, DIALOG_STATE_DEVICE_PRINTS_LISTING)) + return; + + gtk_widget_set_sensitive (GTK_WIDGET (self->add_print_icon), FALSE); + gtk_widget_hide (GTK_WIDGET (self->delete_prints_button)); + + g_clear_pointer (&self->enrolled_fingers, g_strfreev); + + user = cc_fingerprint_manager_get_user (self->manager); + cc_fprintd_device_call_list_enrolled_fingers (self->device, + act_user_get_user_name (user), + self->cancellable, + list_enrolled_cb, + self); +} + +static void +delete_prints_cb (GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GError) error = NULL; + CcFprintdDevice *fprintd_device = CC_FPRINTD_DEVICE (object); + CcFingerprintDialog *self = user_data; + + cc_fprintd_device_call_delete_enrolled_fingers2_finish (fprintd_device, res, &error); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + if (error) + { + g_autofree char *error_message = NULL; + + error_message = g_strdup_printf (_("Failed to delete saved fingerprints: %s"), + dbus_error_to_human (self, error)); + g_warning ("Deletion of fingerprints on device %s failed: %s", + cc_fprintd_device_get_name (self->device), error->message); + notify_error (self, error_message); + } + + update_prints_store (self); + cc_fingerprint_manager_update_state (self->manager, NULL, NULL); +} + +static void +delete_enrolled_prints (CcFingerprintDialog *self) +{ + g_return_if_fail (self->dialog_state & DIALOG_STATE_DEVICE_CLAIMED); + + if (!add_dialog_state (self, DIALOG_STATE_DEVICE_DELETING)) + return; + + gtk_widget_set_sensitive (GTK_WIDGET (self->prints_manager), FALSE); + + cc_fprintd_device_call_delete_enrolled_fingers2 (self->device, + self->cancellable, + delete_prints_cb, + self); +} + +static const char * +get_finger_name (const char *finger_id) +{ + if (g_str_equal (finger_id, "left-thumb")) + return _("Left thumb"); + if (g_str_equal (finger_id, "left-middle-finger")) + return _("Left middle finger"); + if (g_str_equal (finger_id, "left-index-finger")) + return _("_Left index finger"); + if (g_str_equal (finger_id, "left-ring-finger")) + return _("Left ring finger"); + if (g_str_equal (finger_id, "left-little-finger")) + return _("Left little finger"); + if (g_str_equal (finger_id, "right-thumb")) + return _("Right thumb"); + if (g_str_equal (finger_id, "right-middle-finger")) + return _("Right middle finger"); + if (g_str_equal (finger_id, "right-index-finger")) + return _("_Right index finger"); + if (g_str_equal (finger_id, "right-ring-finger")) + return _("Right ring finger"); + if (g_str_equal (finger_id, "right-little-finger")) + return _("Right little finger"); + + g_return_val_if_reached (_("Unknown Finger")); +} + +static gboolean +have_multiple_devices (CcFingerprintDialog *self) +{ + g_autoptr(GList) devices_rows = NULL; + + devices_rows = get_container_children (GTK_WIDGET (self->devices_list)); + + return devices_rows && devices_rows->next; +} + +static void +set_enroll_result_message (CcFingerprintDialog *self, + EnrollState enroll_state, + const char *message) +{ + GtkStyleContext *style_context; + const char *icon_name; + guint i; + + g_return_if_fail (enroll_state >= 0 && enroll_state < N_ENROLL_STATES); + + style_context = gtk_widget_get_style_context (self->enroll_result_icon); + + switch (enroll_state) + { + case ENROLL_STATE_WARNING: + case ENROLL_STATE_ERROR: + icon_name = "fingerprint-detection-warning-symbolic"; + break; + case ENROLL_STATE_COMPLETED: + icon_name = "fingerprint-detection-complete-symbolic"; + break; + default: + icon_name = "fingerprint-detection-symbolic"; + } + + for (i = 0; i < N_ENROLL_STATES; ++i) + gtk_style_context_remove_class (style_context, ENROLL_STATE_CLASSES[i]); + + gtk_style_context_add_class (style_context, ENROLL_STATE_CLASSES[enroll_state]); + + gtk_image_set_from_icon_name (self->enroll_result_image, icon_name); + gtk_label_set_label (self->enroll_result_message, message); +} + +static gboolean +stage_passed_timeout_cb (gpointer user_data) +{ + CcFingerprintDialog *self = user_data; + const char *current_message; + + current_message = gtk_label_get_label (self->enroll_result_message); + set_enroll_result_message (self, ENROLL_STATE_NORMAL, current_message); + self->enroll_stage_passed_id = 0; + + return FALSE; +} + +static void +handle_enroll_signal (CcFingerprintDialog *self, + const char *result, + gboolean done) +{ + gboolean completed; + + g_return_if_fail (self->dialog_state & DIALOG_STATE_DEVICE_ENROLLING); + + g_debug ("Device enroll result message: %s, done: %d", result, done); + + completed = g_str_equal (result, "enroll-completed"); + g_clear_handle_id (&self->enroll_stage_passed_id, g_source_remove); + + if (g_str_equal (result, "enroll-stage-passed") || completed) + { + guint enroll_stages; + + enroll_stages = cc_fprintd_device_get_num_enroll_stages (self->device); + + self->enroll_stages_passed++; + + if (enroll_stages > 0) + self->enroll_progress = + MIN (1.0f, self->enroll_stages_passed / (double) enroll_stages); + else + g_warning ("The device %s requires an invalid number of enroll stages (%u)", + cc_fprintd_device_get_name (self->device), enroll_stages); + + g_debug ("Enroll state passed, %u/%u (%.2f%%)", + self->enroll_stages_passed, (guint) enroll_stages, + self->enroll_progress); + + if (!completed) + { + set_enroll_result_message (self, ENROLL_STATE_SUCCESS, NULL); + + self->enroll_stage_passed_id = + g_timeout_add (750, stage_passed_timeout_cb, self); + } + else + { + if (!G_APPROX_VALUE (self->enroll_progress, 1.0f, FLT_EPSILON)) + { + g_warning ("Device marked enroll as completed, but progress is at %.2f", + self->enroll_progress); + self->enroll_progress = 1.0f; + } + } + } + else if (!done) + { + const char *scan_type; + const char *message; + gboolean is_swipe; + + scan_type = cc_fprintd_device_get_scan_type (self->device); + is_swipe = g_str_equal (scan_type, "swipe"); + + message = enroll_result_str_to_msg (result, is_swipe); + set_enroll_result_message (self, ENROLL_STATE_RETRY, message); + + self->enroll_stage_passed_id = + g_timeout_add (850, stage_passed_timeout_cb, self); + } + + if (done) + { + if (completed) + { + /* TRANSLATORS: This is the message shown when the fingerprint + * enrollment has been completed successfully */ + set_enroll_result_message (self, ENROLL_STATE_COMPLETED, + C_("Fingerprint enroll state", "Complete")); + gtk_widget_set_sensitive (GTK_WIDGET (self->cancel_button), FALSE); + gtk_widget_set_sensitive (GTK_WIDGET (self->done_button), TRUE); + gtk_widget_grab_focus (GTK_WIDGET (self->done_button)); + } + else + { + const char *message; + + if (g_str_equal (result, "enroll-disconnected")) + { + message = _("Fingerprint device disconnected"); + remove_dialog_state (self, DIALOG_STATE_DEVICE_CLAIMED | + DIALOG_STATE_DEVICE_ENROLLING); + } + else if (g_str_equal (result, "enroll-data-full")) + { + message = _("Fingerprint device storage is full"); + } + else + { + message = _("Failed to enroll new fingerprint"); + } + + set_enroll_result_message (self, ENROLL_STATE_WARNING, message); + } + } +} + +static void +enroll_start_cb (GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GError) error = NULL; + g_autoptr(DialogStateRemover) state_remover = NULL; + CcFprintdDevice *fprintd_device = CC_FPRINTD_DEVICE (object); + CcFingerprintDialog *self = user_data; + + cc_fprintd_device_call_enroll_start_finish (fprintd_device, res, &error); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + state_remover = auto_state_remover (self, DIALOG_STATE_DEVICE_ENROLL_STARTING); + + if (error) + { + g_autofree char *error_message = NULL; + + remove_dialog_state (self, DIALOG_STATE_DEVICE_ENROLLING); + + error_message = g_strdup_printf (_("Failed to start enrollment: %s"), + dbus_error_to_human (self, error)); + g_warning ("Enrollment on device %s failed: %s", + cc_fprintd_device_get_name (self->device), error->message); + notify_error (self, error_message); + + set_enroll_result_message (self, ENROLL_STATE_ERROR, + C_("Fingerprint enroll state", + "Failed to enroll new fingerprint")); + gtk_widget_set_sensitive (self->enrollment_view, FALSE); + + return; + } +} + +static void +enroll_stop_cb (GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GError) error = NULL; + g_autoptr(DialogStateRemover) state_remover = NULL; + CcFprintdDevice *fprintd_device = CC_FPRINTD_DEVICE (object); + CcFingerprintDialog *self = user_data; + + cc_fprintd_device_call_enroll_stop_finish (fprintd_device, res, &error); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + state_remover = auto_state_remover (self, DIALOG_STATE_DEVICE_ENROLLING | + DIALOG_STATE_DEVICE_ENROLL_STOPPING); + gtk_widget_set_sensitive (self->enrollment_view, TRUE); + gtk_stack_set_visible_child (self->stack, self->prints_manager); + + if (error) + { + g_autofree char *error_message = NULL; + + error_message = g_strdup_printf (_("Failed to stop enrollment: %s"), + dbus_error_to_human (self, error)); + g_warning ("Stopping enrollment on device %s failed: %s", + cc_fprintd_device_get_name (self->device), error->message); + notify_error (self, error_message); + + return; + } + + cc_fingerprint_manager_update_state (self->manager, NULL, NULL); +} + +static void +enroll_stop (CcFingerprintDialog *self) +{ + g_return_if_fail (self->dialog_state & DIALOG_STATE_DEVICE_ENROLLING); + + if (!add_dialog_state (self, DIALOG_STATE_DEVICE_ENROLL_STOPPING)) + return; + + gtk_widget_set_sensitive (self->enrollment_view, FALSE); + cc_fprintd_device_call_enroll_stop (self->device, self->cancellable, + enroll_stop_cb, self); +} + +static char * +get_enrollment_string (CcFingerprintDialog *self, + const char *finger_id) +{ + char *ret; + const char *scan_type; + const char *device_name; + gboolean is_swipe; + + device_name = NULL; + scan_type = cc_fprintd_device_get_scan_type (self->device); + is_swipe = g_str_equal (scan_type, "swipe"); + + if (have_multiple_devices (self)) + device_name = cc_fprintd_device_get_name (self->device); + + ret = finger_str_to_msg (finger_id, device_name, is_swipe); + + if (ret) + return ret; + + return g_strdup (_("Repeatedly lift and place your finger on the reader to enroll your fingerprint")); +} + +static void +enroll_finger (CcFingerprintDialog *self, + const char *finger_id) +{ + g_auto(GStrv) tmp_finger_name = NULL; + g_autofree char *finger_name = NULL; + g_autofree char *enroll_message = NULL; + + g_return_if_fail (finger_id); + + if (!add_dialog_state (self, DIALOG_STATE_DEVICE_ENROLLING | + DIALOG_STATE_DEVICE_ENROLL_STARTING)) + return; + + self->enroll_progress = 0; + self->enroll_stages_passed = 0; + + g_debug ("Enrolling finger %s", finger_id); + + enroll_message = get_enrollment_string (self, finger_id); + tmp_finger_name = g_strsplit (get_finger_name (finger_id), "_", -1); + finger_name = g_strjoinv ("", tmp_finger_name); + + set_enroll_result_message (self, ENROLL_STATE_NORMAL, NULL); + gtk_stack_set_visible_child (self->stack, self->enrollment_view); + gtk_label_set_label (self->enroll_message, enroll_message); + gtk_editable_set_text (GTK_EDITABLE (self->enroll_print_entry), finger_name); + + cc_fprintd_device_call_enroll_start (self->device, finger_id, self->cancellable, + enroll_start_cb, self); +} + +static void +populate_enrollment_view (CcFingerprintDialog *self) +{ + GtkStyleContext *style_context; + + self->enroll_result_icon = + fingerprint_icon_new ("fingerprint-detection-symbolic", + NULL, + GTK_TYPE_IMAGE, + &self->enroll_progress, + (GtkWidget **) &self->enroll_result_image, + (GtkWidget **) &self->enroll_result_message); + + gtk_box_prepend (GTK_BOX (self->enroll_print_bin), self->enroll_result_icon); + + style_context = gtk_widget_get_style_context (self->enroll_result_icon); + gtk_style_context_add_class (style_context, "enroll-status"); +} + +static void +on_print_activated_cb (GtkFlowBox *flowbox, + GtkFlowBoxChild *child, + CcFingerprintDialog *self) +{ + GtkWidget *selected_button; + + selected_button = g_object_get_data (G_OBJECT (child), "button"); + g_signal_emit_by_name (GTK_MENU_BUTTON (selected_button), "activate"); +} + +static void +on_enroll_cb (CcFingerprintDialog *self, + GtkMenuButton *button) +{ + const char *finger_id; + + finger_id = g_object_get_data (G_OBJECT (button), "finger-id"); + enroll_finger (self, finger_id); +} + +static void +populate_add_print_popover (CcFingerprintDialog *self) +{ + guint i; + + for (i = 0; i < N_VALID_FINGERS; ++i) + { + GtkWidget *finger_item; + + finger_item = gtk_button_new (); + gtk_button_set_label (GTK_BUTTON (finger_item), get_finger_name (FINGER_IDS[i])); + gtk_button_set_use_underline (GTK_BUTTON (finger_item), TRUE); + g_object_set_data (G_OBJECT (finger_item), "finger-id", (gpointer) FINGER_IDS[i]); + gtk_box_prepend (GTK_BOX (self->add_print_popover_box), finger_item); + + g_signal_connect_object (finger_item, "clicked", G_CALLBACK (on_enroll_cb), + self, G_CONNECT_SWAPPED); + } +} + +static void +populate_prints_gallery (CcFingerprintDialog *self) +{ + const char *add_print_label; + GtkWidget *button; + GtkStyleContext *style_context; + guint i; + + g_return_if_fail (!GTK_IS_WIDGET (self->add_print_icon)); + + for (i = 0; i < N_VALID_FINGERS; ++i) + { + GtkWidget *flowbox_child; + GtkWidget *popover; + GtkWidget *reenroll_button; + + flowbox_child = fingerprint_menu_button ("fingerprint-detection-symbolic", + get_finger_name (FINGER_IDS[i])); + + button = g_object_get_data (G_OBJECT (flowbox_child), "button"); + + popover = gtk_popover_new (); + reenroll_button = gtk_button_new (); + gtk_button_set_use_underline (GTK_BUTTON (reenroll_button), TRUE); + gtk_button_set_label (GTK_BUTTON (reenroll_button), _("_Re-enroll this finger…")); + g_object_set_data (G_OBJECT (reenroll_button), "finger-id", + (gpointer) FINGER_IDS[i]); + g_signal_connect_object (reenroll_button, "clicked", G_CALLBACK (on_enroll_cb), self, G_CONNECT_SWAPPED); + gtk_popover_set_child (GTK_POPOVER (popover), reenroll_button); + + gtk_menu_button_set_popover (GTK_MENU_BUTTON (button), + popover); + g_object_set_data (G_OBJECT (flowbox_child), "finger-id", + (gpointer) FINGER_IDS[i]); + + gtk_flow_box_insert (self->prints_gallery, flowbox_child, i); + } + + /* TRANSLATORS: This is the label for the button to enroll a new finger */ + add_print_label = _("Scan new fingerprint"); + self->add_print_icon = fingerprint_menu_button ("list-add-symbolic", + add_print_label); + style_context = gtk_widget_get_style_context (self->add_print_icon); + gtk_style_context_add_class (style_context, "fingerprint-print-add"); + + populate_add_print_popover (self); + button = g_object_get_data (G_OBJECT (self->add_print_icon), "button"); + gtk_menu_button_set_popover (GTK_MENU_BUTTON (button), + GTK_WIDGET (self->add_print_popover)); + + gtk_flow_box_insert (self->prints_gallery, self->add_print_icon, -1); + gtk_flow_box_set_max_children_per_line (self->prints_gallery, 1); + + gtk_flow_box_set_filter_func (self->prints_gallery, prints_visibility_filter, + self, NULL); + + update_prints_visibility (self); +} + +static void +release_device_cb (GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GError) error = NULL; + CcFprintdDevice *fprintd_device = CC_FPRINTD_DEVICE (object); + CcFingerprintDialog *self = user_data; + + cc_fprintd_device_call_release_finish (fprintd_device, res, &error); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + if (error) + { + g_autofree char *error_message = NULL; + + error_message = g_strdup_printf (_("Failed to release fingerprint device %s: %s"), + cc_fprintd_device_get_name (fprintd_device), + dbus_error_to_human (self, error)); + g_warning ("Releasing device %s failed: %s", + cc_fprintd_device_get_name (self->device), error->message); + + notify_error (self, error_message); + return; + } + + remove_dialog_state (self, DIALOG_STATE_DEVICE_CLAIMED); +} + +static void +release_device (CcFingerprintDialog *self) +{ + if (!self->device || !(self->dialog_state & DIALOG_STATE_DEVICE_CLAIMED)) + return; + + disconnect_device_signals (self); + + cc_fprintd_device_call_release (self->device, + self->cancellable, + release_device_cb, + self); +} + +static void +on_device_signal (CcFingerprintDialog *self, + gchar *sender_name, + gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + if (g_str_equal (signal_name, "EnrollStatus")) + { + const char *result; + gboolean done; + + if (!g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(sb)"))) + { + g_warning ("Unexpected enroll parameters type %s", + g_variant_get_type_string (parameters)); + return; + } + + g_variant_get (parameters, "(&sb)", &result, &done); + handle_enroll_signal (self, result, done); + } +} + +static void claim_device (CcFingerprintDialog *self); + +static void +on_device_owner_changed (CcFprintdDevice *device, + GParamSpec *spec, + CcFingerprintDialog *self) +{ + g_autofree char *name_owner = NULL; + + name_owner = g_dbus_proxy_get_name_owner (G_DBUS_PROXY (device)); + + if (!name_owner) + { + if (self->dialog_state & DIALOG_STATE_DEVICE_CLAIMED) + { + disconnect_device_signals (self); + + if (self->dialog_state & DIALOG_STATE_DEVICE_ENROLLING) + { + set_enroll_result_message (self, ENROLL_STATE_ERROR, + C_("Fingerprint enroll state", + "Problem Reading Device")); + } + + remove_dialog_state (self, DIALOG_STATE_DEVICE_CLAIMED); + claim_device (self); + } + } +} + +static void +claim_device_cb (GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GError) error = NULL; + g_autoptr(DialogStateRemover) state_remover = NULL; + CcFprintdDevice *fprintd_device = CC_FPRINTD_DEVICE (object); + CcFingerprintDialog *self = user_data; + + cc_fprintd_device_call_claim_finish (fprintd_device, res, &error); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + state_remover = auto_state_remover (self, DIALOG_STATE_DEVICE_CLAIMING); + + if (error) + { + g_autofree char *dbus_error = g_dbus_error_get_remote_error (error); + g_autofree char *error_message = NULL; + + if (dbus_error && g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.AlreadyInUse") && + (self->dialog_state & DIALOG_STATE_DEVICE_CLAIMED)) + return; + + error_message = g_strdup_printf (_("Failed to claim fingerprint device %s: %s"), + cc_fprintd_device_get_name (self->device), + dbus_error_to_human (self, error)); + g_warning ("Claiming device %s failed: %s", + cc_fprintd_device_get_name (self->device), error->message); + notify_error (self, error_message); + return; + } + + if (!add_dialog_state (self, DIALOG_STATE_DEVICE_CLAIMED)) + return; + + gtk_widget_set_sensitive (self->prints_manager, TRUE); + self->device_signal_id = g_signal_connect_object (self->device, "g-signal", + G_CALLBACK (on_device_signal), + self, G_CONNECT_SWAPPED); + self->device_name_owner_id = g_signal_connect_object (self->device, "notify::g-name-owner", + G_CALLBACK (on_device_owner_changed), + self, 0); +} + +static void +claim_device (CcFingerprintDialog *self) +{ + ActUser *user; + + g_return_if_fail (!(self->dialog_state & DIALOG_STATE_DEVICE_CLAIMED)); + + if (!add_dialog_state (self, DIALOG_STATE_DEVICE_CLAIMING)) + return; + + user = cc_fingerprint_manager_get_user (self->manager); + gtk_widget_set_sensitive (self->prints_manager, FALSE); + + cc_fprintd_device_call_claim (self->device, + act_user_get_user_name (user), + self->cancellable, + claim_device_cb, + self); +} + +static void +on_stack_child_changed (CcFingerprintDialog *self) +{ + GtkWidget *visible_child = gtk_stack_get_visible_child (self->stack); + + g_debug ("Fingerprint dialog child changed: %s", + gtk_stack_get_visible_child_name (self->stack)); + + gtk_widget_hide (GTK_WIDGET (self->back_button)); + gtk_widget_hide (GTK_WIDGET (self->cancel_button)); + gtk_widget_hide (GTK_WIDGET (self->done_button)); + + adw_header_bar_set_show_start_title_buttons (ADW_HEADER_BAR (self->titlebar), TRUE); + adw_header_bar_set_show_end_title_buttons (ADW_HEADER_BAR (self->titlebar), TRUE); + gtk_flow_box_invalidate_filter (self->prints_gallery); + + if (visible_child == self->prints_manager) + { + gtk_widget_set_visible (GTK_WIDGET (self->back_button), + have_multiple_devices (self)); + notify_error (self, NULL); + update_prints_store (self); + + if (!(self->dialog_state & DIALOG_STATE_DEVICE_CLAIMED)) + claim_device (self); + } + else if (visible_child == self->enrollment_view) + { + adw_header_bar_set_show_start_title_buttons (ADW_HEADER_BAR (self->titlebar), FALSE); + adw_header_bar_set_show_end_title_buttons (ADW_HEADER_BAR (self->titlebar), FALSE); + + gtk_widget_show (GTK_WIDGET (self->cancel_button)); + gtk_widget_set_sensitive (GTK_WIDGET (self->cancel_button), TRUE); + + gtk_widget_show (GTK_WIDGET (self->done_button)); + gtk_widget_set_sensitive (GTK_WIDGET (self->done_button), FALSE); + } + else + { + release_device (self); + g_clear_object (&self->device); + } +} + +static void +cc_fingerprint_dialog_init (CcFingerprintDialog *self) +{ + g_autoptr(GtkCssProvider) provider = NULL; + + self->cancellable = g_cancellable_new (); + + gtk_widget_init_template (GTK_WIDGET (self)); + + provider = gtk_css_provider_new (); + gtk_css_provider_load_from_resource (provider, + "/org/gnome/control-center/user-accounts/cc-fingerprint-dialog.css"); + gtk_style_context_add_provider_for_display (gdk_display_get_default (), + GTK_STYLE_PROVIDER (provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + on_stack_child_changed (self); + g_signal_connect_object (self->stack, "notify::visible-child", + G_CALLBACK (on_stack_child_changed), self, + G_CONNECT_SWAPPED); + + g_object_bind_property (self->stack, "visible-child-name", + self->title, "label", G_BINDING_SYNC_CREATE); + + populate_prints_gallery (self); + populate_enrollment_view (self); +} + +static void +select_device_row (CcFingerprintDialog *self, + GtkListBoxRow *row, + GtkListBox *listbox) +{ + CcFprintdDevice *device = g_object_get_data (G_OBJECT (row), "device"); + + g_return_if_fail (CC_FPRINTD_DEVICE (device)); + + g_set_object (&self->device, device); + gtk_stack_set_visible_child (self->stack, self->prints_manager); +} + +static void +on_devices_list (GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + g_autolist (CcFprintdDevice) fprintd_devices = NULL; + g_autoptr(DialogStateRemover) state_remover = NULL; + g_autoptr(GError) error = NULL; + CcFingerprintManager *fingerprint_manager = CC_FINGERPRINT_MANAGER (object); + CcFingerprintDialog *self = CC_FINGERPRINT_DIALOG (user_data); + + fprintd_devices = cc_fingerprint_manager_get_devices_finish (fingerprint_manager, + res, &error); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + state_remover = auto_state_remover (self, DIALOG_STATE_DEVICES_LISTING); + + if (fprintd_devices == NULL) + { + if (error) + { + g_autofree char *error_message = NULL; + + error_message = g_strdup_printf (_("Failed to get fingerprint devices: %s"), + dbus_error_to_human (self, error)); + g_warning ("Retrieving fingerprint devices failed: %s", error->message); + notify_error (self, error_message); + } + + gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->no_devices_found)); + } + else if (fprintd_devices->next == NULL) + { + /* We have just one device... Skip devices selection */ + self->device = g_object_ref (fprintd_devices->data); + gtk_stack_set_visible_child (self->stack, self->prints_manager); + } + else + { + GList *l; + + for (l = fprintd_devices; l; l = l->next) + { + CcFprintdDevice *device = l->data; + CcListRow *device_row; + + device_row = g_object_new (CC_TYPE_LIST_ROW, + "visible", TRUE, + "icon-name", "go-next-symbolic", + "title", cc_fprintd_device_get_name (device), + NULL); + + gtk_list_box_insert (self->devices_list, GTK_WIDGET (device_row), -1); + g_object_set_data_full (G_OBJECT (device_row), "device", + g_object_ref (device), g_object_unref); + } + + gtk_stack_set_visible_child (self->stack, self->device_selector); + } +} + +static void +cc_fingerprint_dialog_constructed (GObject *object) +{ + CcFingerprintDialog *self = CC_FINGERPRINT_DIALOG (object); + + bindtextdomain ("fprintd", GNOMELOCALEDIR); + bind_textdomain_codeset ("fprintd", "UTF-8"); + + add_dialog_state (self, DIALOG_STATE_DEVICES_LISTING); + cc_fingerprint_manager_get_devices (self->manager, self->cancellable, + on_devices_list, self); +} + +static void +back_button_clicked_cb (CcFingerprintDialog *self) +{ + if (gtk_stack_get_visible_child (self->stack) == self->prints_manager) + { + notify_error (self, NULL); + gtk_stack_set_visible_child (self->stack, self->device_selector); + return; + } + + g_return_if_reached (); +} + +static void +confirm_deletion_button_clicked_cb (CcFingerprintDialog *self) +{ + gtk_widget_hide (self->delete_confirmation_infobar); + delete_enrolled_prints (self); +} + +static void +cancel_deletion_button_clicked_cb (CcFingerprintDialog *self) +{ + gtk_widget_set_sensitive (self->prints_manager, TRUE); + gtk_widget_hide (self->delete_confirmation_infobar); +} + +static void +delete_prints_button_clicked_cb (CcFingerprintDialog *self) +{ + gtk_widget_set_sensitive (self->prints_manager, FALSE); + gtk_widget_show (self->delete_confirmation_infobar); +} + +static void +cancel_button_clicked_cb (CcFingerprintDialog *self) +{ + if (self->dialog_state & DIALOG_STATE_DEVICE_ENROLLING) + { + g_cancellable_cancel (self->cancellable); + g_set_object (&self->cancellable, g_cancellable_new ()); + + g_debug ("Cancelling enroll operation"); + enroll_stop (self); + } + else + { + gtk_stack_set_visible_child (self->stack, self->prints_manager); + } +} + +static void +done_button_clicked_cb (CcFingerprintDialog *self) +{ + g_return_if_fail (self->dialog_state & DIALOG_STATE_DEVICE_ENROLLING); + + g_debug ("Completing enroll operation"); + enroll_stop (self); +} + +static gboolean +cc_fingerprint_dialog_close_request (GtkWindow *window) +{ + CcFingerprintDialog *self = CC_FINGERPRINT_DIALOG (window); + + cc_fingerprint_manager_update_state (self->manager, NULL, NULL); + + g_clear_handle_id (&self->enroll_stage_passed_id, g_source_remove); + + if (self->device && (self->dialog_state & DIALOG_STATE_DEVICE_CLAIMED)) + { + disconnect_device_signals (self); + + if (self->dialog_state & DIALOG_STATE_DEVICE_ENROLLING) + cc_fprintd_device_call_enroll_stop_sync (self->device, NULL, NULL); + cc_fprintd_device_call_release (self->device, NULL, NULL, NULL); + } + + g_clear_object (&self->manager); + g_clear_object (&self->device); + g_clear_pointer (&self->enrolled_fingers, g_strfreev); + + g_cancellable_cancel (self->cancellable); + g_clear_object (&self->cancellable); + + return GTK_WINDOW_CLASS (cc_fingerprint_dialog_parent_class)->close_request (window); +} + +static void +cc_fingerprint_dialog_class_init (CcFingerprintDialogClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkWindowClass *window_class = GTK_WINDOW_CLASS (klass); + + gtk_widget_class_set_template_from_resource (widget_class, + "/org/gnome/control-center/user-accounts/cc-fingerprint-dialog.ui"); + + object_class->constructed = cc_fingerprint_dialog_constructed; + object_class->get_property = cc_fingerprint_dialog_get_property; + object_class->set_property = cc_fingerprint_dialog_set_property; + + window_class->close_request = cc_fingerprint_dialog_close_request; + + properties[PROP_MANAGER] = + g_param_spec_object ("fingerprint-manager", + "FingerprintManager", + "The CC fingerprint manager", + CC_TYPE_FINGERPRINT_MANAGER, + G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (object_class, N_PROPS, properties); + + gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, add_print_popover); + gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, add_print_popover_box); + gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, back_button); + gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, cancel_button); + gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, delete_confirmation_infobar); + gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, delete_prints_button); + gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, device_selector); + gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, devices_list); + gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, done_button); + gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, enroll_message); + gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, enroll_print_bin); + gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, enroll_print_entry); + gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, enrollment_view); + gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, error_infobar); + gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, infobar_error); + gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, no_devices_found); + gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, prints_gallery); + gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, prints_manager); + gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, spinner); + gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, stack); + gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, title); + gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, titlebar); + + gtk_widget_class_bind_template_callback (widget_class, back_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, cancel_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, cancel_deletion_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, confirm_deletion_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, delete_prints_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, done_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, on_print_activated_cb); + gtk_widget_class_bind_template_callback (widget_class, select_device_row); +} diff --git a/panels/user-accounts/cc-fingerprint-dialog.h b/panels/user-accounts/cc-fingerprint-dialog.h new file mode 100644 index 0000000..9afac0b --- /dev/null +++ b/panels/user-accounts/cc-fingerprint-dialog.h @@ -0,0 +1,37 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2020 Canonical Ltd. + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authors: Marco Trevisan <marco.trevisan@canonical.com> + */ + +#pragma once + +#include <gtk/gtk.h> +#include "cc-fingerprint-manager.h" + +G_BEGIN_DECLS + +#define CC_TYPE_FINGERPRINT_DIALOG (cc_fingerprint_dialog_get_type ()) + +G_DECLARE_FINAL_TYPE (CcFingerprintDialog, cc_fingerprint_dialog, + CC, FINGERPRINT_DIALOG, GtkWindow) + +CcFingerprintDialog *cc_fingerprint_dialog_new (CcFingerprintManager *manager); + +G_END_DECLS diff --git a/panels/user-accounts/cc-fingerprint-dialog.ui b/panels/user-accounts/cc-fingerprint-dialog.ui new file mode 100644 index 0000000..9c74f84 --- /dev/null +++ b/panels/user-accounts/cc-fingerprint-dialog.ui @@ -0,0 +1,346 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="CcFingerprintDialog" parent="GtkWindow"> + <style> + <class name="fingerprint" /> + </style> + <property name="name">fingerprint-dialog</property> + <property name="title" translatable="yes">Fingerprint Manager</property> + <property name="destroy-with-parent">True</property> + <property name="default-width">600</property> + <property name="default-height">400</property> + <property name="modal">True</property> + <property name="hide-on-close">True</property> + <child type="titlebar"> + <object class="AdwHeaderBar" id="titlebar"> + <property name="show-end-title-buttons">True</property> + <property name="show-start-title-buttons">True</property> + <child type="title"> + <object class="GtkLabel" id="title"> + <property name="label" translatable="yes">Fingerprint</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + <child type="start"> + <object class="GtkButton" id="cancel_button"> + <property name="can_focus">False</property> + <property name="receives_default">False</property> + <property name="valign">center</property> + <property name="use-underline">True</property> + <property name="label" translatable="yes">_Cancel</property> + <signal name="clicked" handler="cancel_button_clicked_cb" object="CcFingerprintDialog" swapped="yes" /> + </object> + </child> + <child type="start"> + <object class="GtkButton" id="back_button"> + <property name="can_focus">False</property> + <property name="receives_default">False</property> + <property name="valign">center</property> + <property name="use-underline">True</property> + <property name="icon_name">go-previous-symbolic</property> + <accessibility> + <property name="label" translatable="yes">Back</property> + </accessibility> + <signal name="clicked" handler="back_button_clicked_cb" object="CcFingerprintDialog" swapped="yes" /> + <style> + <class name="image-button"/> + </style> + </object> + </child> + + <child type="end"> + <object class="GtkButton" id="done_button"> + <property name="use-underline">True</property> + <property name="sensitive">False</property> + <property name="label" translatable="yes">_Done</property> + <signal name="clicked" handler="done_button_clicked_cb" object="CcFingerprintDialog" swapped="yes" /> + <style> + <class name="suggested-action"/> + </style> + </object> + </child> + + <child> + <object class="GtkSpinner" id="spinner"> + <property name="can_focus">False</property> + </object> + </child> + </object> + </child> + + <child> + <object class="GtkBox"> + <property name="orientation">vertical</property> + + <child> + <object class="GtkInfoBar" id="delete_confirmation_infobar"> + <property name="visible">False</property> + <child> + <object class="GtkBox"> + <child> + <object class="GtkButton"> + <signal name="clicked" handler="cancel_deletion_button_clicked_cb" object="CcFingerprintDialog" swapped="yes"/> + <property name="label" translatable="yes">_No</property> + <property name="use-underline">True</property> + </object> + </child> + <child> + <object class="GtkButton"> + <signal name="clicked" handler="confirm_deletion_button_clicked_cb" object="CcFingerprintDialog" swapped="yes"/> + <property name="receives_default">True</property> + <property name="label" translatable="yes">_Yes</property> + <property name="use-underline">True</property> + <style> + <class name="destructive-action"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="can_focus">False</property> + <property name="spacing">16</property> + <property name="margin-start">12</property> + <child> + <object class="GtkLabel"> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="hexpand">False</property> + <property name="wrap">True</property> + <property name="label" translatable="yes">Do you want to delete your registered fingerprints so fingerprint login is disabled?</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + </object> + </child> + </object> + </child> + + <child> + <object class="GtkInfoBar" id="error_infobar"> + <property name="name">error_infobar</property> + <property name="visible">False</property> + <property name="can_focus">False</property> + <style> + <class name="error"/> + </style> + <child> + <object class="GtkBox"> + <property name="can_focus">False</property> + <property name="spacing">16</property> + <child> + <object class="GtkLabel" id="infobar_error"> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="hexpand">False</property> + <property name="wrap">True</property> + </object> + </child> + </object> + </child> + </object> + </child> + + <child> + <object class="GtkScrolledWindow"> + <property name="halign">fill</property> + <property name="valign">fill</property> + <property name="propagate-natural-width">True</property> + <property name="can-focus">False</property> + <property name="hscrollbar-policy">never</property> + + <child> + <object class="GtkStack" id="stack"> + <property name="transition_duration">300</property> + <property name="margin-start">20</property> + <property name="margin-end">20</property> + <property name="margin-top">30</property> + <property name="margin-bottom">30</property> + <property name="width_request">360</property> + <property name="halign">center</property> + + <child> + <object class="GtkBox" id="no_devices_found"> + <property name="name" translatable="yes">No fingerprint device</property> + <property name="orientation">vertical</property> + <property name="valign">center</property> + <property name="spacing">12</property> + <style> + <class name="dim-label"/> + </style> + <child> + <object class="GtkImage"> + <property name="icon_name">fingerprint-detection-symbolic</property> + <property name="pixel_size">192</property> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes" comments="Translators: This is the empty state page label which states that there are no devices ready.">No Fingerprint device</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="1.6"/> + </attributes> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Ensure the device is properly connected.</property> + </object> + </child> + </object> + </child> + + <child> + <object class="GtkBox" id="device_selector"> + <property name="name" translatable="yes">Fingerprint Device</property> + <property name="can_focus">False</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="spacing">10</property> + <property name="orientation">vertical</property> + + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Choose the fingerprint device you want to configure</property> + <property name="halign">start</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + </child> + + <child> + <object class="GtkScrolledWindow"> + <property name="can-focus">False</property> + <property name="hscrollbar-policy">never</property> + <property name="propagate-natural-height">True</property> + <child> + <object class="GtkListBox" id="devices_list"> + <property name="selection-mode">none</property> + <property name="valign">center</property> + <signal name="row-activated" handler="select_device_row" object="CcFingerprintDialog" swapped="yes"/> + <style> + <class name="frame" /> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + + <child> + <object class="GtkBox" id="prints_manager"> + <property name="name" translatable="yes">Fingerprint Login</property> + <property name="orientation">vertical</property> + <property name="valign">fill</property> + <property name="spacing">12</property> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Fingerprint login allows you to unlock and log into your computer with your finger</property> + </object> + </child> + <child> + <object class="GtkFlowBox" id="prints_gallery"> + <style> + <class name="prints-gallery" /> + </style> + <property name="column-spacing">12</property> + <property name="row-spacing">12</property> + <property name="homogeneous">True</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="vexpand">True</property> + <property name="min-children-per-line">1</property> + <property name="max-children-per-line">3</property> + <property name="activate-on-single-click">True</property> + <property name="selection-mode">none</property> + <signal name="child-activated" handler="on_print_activated_cb" object="CcFingerprintDialog" swapped="no" /> + </object> + </child> + + <child> + <object class="GtkButton" id="delete_prints_button"> + <property name="visible">False</property> + <property name="halign">end</property> + <property name="use-underline">True</property> + <property name="label" translatable="yes">_Delete Fingerprints</property> + <property name="margin-top">10</property> + <property name="margin-bottom">10</property> + <signal name="clicked" handler="delete_prints_button_clicked_cb" object="CcFingerprintDialog" swapped="yes"/> + <style> + <class name="destructive-action"/> + </style> + </object> + </child> + </object> + </child> + + <child> + <object class="GtkBox" id="enrollment_view"> + <property name="name" translatable="yes">Fingerprint Enroll</property> + <property name="orientation">vertical</property> + <property name="valign">fill</property> + <property name="spacing">12</property> + <style> + <class name="enrollment" /> + </style> + <child> + <object class="GtkLabel" id="enroll_message"> + <property name="wrap">True</property> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="spacing">12</property> + <property name="halign">fill</property> + <property name="valign">center</property> + <property name="vexpand">True</property> + <property name="hexpand">True</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="enroll_print_bin"> + <property name="hexpand">True</property> + </object> + </child> + <child> + <object class="GtkEntry" id="enroll_print_entry"> + <property name="valign">end</property> + <property name="halign">center</property> + <property name="editable">False</property> + <property name="sensitive">False</property> + <property name="width-request">200</property> + </object> + </child> + </object> + </child> + </object> + </child> + + </object> + </child> + </object> + </child> + + </object> + </child> + </template> + + <object class="GtkPopover" id="add_print_popover"> + <property name="position">bottom</property> + <child> + <object class="GtkBox" id="add_print_popover_box"> + <property name="can_focus">False</property> + <property name="spacing">6</property> + <property name="orientation">vertical</property> + </object> + </child> + </object> + +</interface> diff --git a/panels/user-accounts/cc-fingerprint-manager.c b/panels/user-accounts/cc-fingerprint-manager.c new file mode 100644 index 0000000..07a50e7 --- /dev/null +++ b/panels/user-accounts/cc-fingerprint-manager.c @@ -0,0 +1,597 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2020 Canonical Ltd. + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authors: Marco Trevisan <marco.trevisan@canonical.com> + */ + +#include "cc-fingerprint-manager.h" + +#include "cc-fprintd-generated.h" +#include "cc-user-accounts-enum-types.h" + +#define CC_FPRINTD_NAME "net.reactivated.Fprint" +#define CC_FPRINTD_MANAGER_PATH "/net/reactivated/Fprint/Manager" + +struct _CcFingerprintManager +{ + GObject parent_instance; +}; + +typedef struct +{ + ActUser *user; + GTask *current_task; + CcFingerprintState state; + GList *cached_devices; +} CcFingerprintManagerPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (CcFingerprintManager, cc_fingerprint_manager, G_TYPE_OBJECT) + +enum { + PROP_0, + PROP_USER, + PROP_STATE, + N_PROPS +}; + +static GParamSpec *properties[N_PROPS]; + +static void cleanup_cached_devices (CcFingerprintManager *self); + +CcFingerprintManager * +cc_fingerprint_manager_new (ActUser *user) +{ + return g_object_new (CC_TYPE_FINGERPRINT_MANAGER, "user", user, NULL); +} + +static void +cc_fingerprint_manager_dispose (GObject *object) +{ + CcFingerprintManager *self = CC_FINGERPRINT_MANAGER (object); + CcFingerprintManagerPrivate *priv = cc_fingerprint_manager_get_instance_private (self); + + if (priv->current_task) + { + g_cancellable_cancel (g_task_get_cancellable (priv->current_task)); + priv->current_task = NULL; + } + + g_clear_object (&priv->user); + cleanup_cached_devices (self); + + G_OBJECT_CLASS (cc_fingerprint_manager_parent_class)->dispose (object); +} + +static void +cc_fingerprint_manager_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + CcFingerprintManager *self = CC_FINGERPRINT_MANAGER (object); + CcFingerprintManagerPrivate *priv = cc_fingerprint_manager_get_instance_private (self); + + switch (prop_id) + { + case PROP_STATE: + g_value_set_enum (value, priv->state); + break; + + case PROP_USER: + g_value_set_object (value, priv->user); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +cc_fingerprint_manager_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + CcFingerprintManager *self = CC_FINGERPRINT_MANAGER (object); + CcFingerprintManagerPrivate *priv = cc_fingerprint_manager_get_instance_private (self); + + switch (prop_id) + { + case PROP_USER: + g_set_object (&priv->user, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +cc_fingerprint_manager_constructed (GObject *object) +{ + cc_fingerprint_manager_update_state (CC_FINGERPRINT_MANAGER (object), NULL, NULL); +} + +static void +cc_fingerprint_manager_class_init (CcFingerprintManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = cc_fingerprint_manager_constructed; + object_class->dispose = cc_fingerprint_manager_dispose; + object_class->get_property = cc_fingerprint_manager_get_property; + object_class->set_property = cc_fingerprint_manager_set_property; + + properties[PROP_USER] = + g_param_spec_object ("user", + "User", + "The user account we manage the fingerprint for", + ACT_TYPE_USER, + G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + properties[PROP_STATE] = + g_param_spec_enum ("state", + "State", + "The state of the fingerprint for the user", + CC_TYPE_FINGERPRINT_STATE, CC_FINGERPRINT_STATE_NONE, + G_PARAM_STATIC_STRINGS | G_PARAM_READABLE); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +cc_fingerprint_manager_init (CcFingerprintManager *self) +{ +} + +typedef struct +{ + guint waiting_devices; + GList *devices; +} DeviceListData; + +static void +object_list_destroy_notify (gpointer data) +{ + GList *list = data; + g_list_free_full (list, g_object_unref); +} + +static void +on_device_owner_changed (CcFingerprintManager *self, + GParamSpec *spec, + CcFprintdDevice *device) +{ + g_autofree char *name_owner = NULL; + + name_owner = g_dbus_proxy_get_name_owner (G_DBUS_PROXY (device)); + + if (!name_owner) + { + g_debug ("Fprintd daemon disappeared, cleaning cache..."); + cleanup_cached_devices (self); + } +} + +static void +cleanup_cached_devices (CcFingerprintManager *self) +{ + CcFingerprintManagerPrivate *priv = cc_fingerprint_manager_get_instance_private (self); + CcFprintdDevice *target_device; + + if (!priv->cached_devices) + return; + + g_return_if_fail (CC_FPRINTD_IS_DEVICE (priv->cached_devices->data)); + + target_device = CC_FPRINTD_DEVICE (priv->cached_devices->data); + + g_signal_handlers_disconnect_by_func (target_device, on_device_owner_changed, self); + g_list_free_full (g_steal_pointer (&priv->cached_devices), g_object_unref); +} + +static void +cache_devices (CcFingerprintManager *self, + GList *devices) +{ + CcFingerprintManagerPrivate *priv = cc_fingerprint_manager_get_instance_private (self); + CcFprintdDevice *target_device; + + g_return_if_fail (devices && CC_FPRINTD_IS_DEVICE (devices->data)); + + cleanup_cached_devices (self); + priv->cached_devices = g_list_copy_deep (devices, (GCopyFunc) g_object_ref, NULL); + + /* We can monitor just the first device name, as the owner is just the same */ + target_device = CC_FPRINTD_DEVICE (priv->cached_devices->data); + + g_signal_connect_object (target_device, "notify::g-name-owner", + G_CALLBACK (on_device_owner_changed), self, + G_CONNECT_SWAPPED); +} + +static void +on_device_proxy (GObject *object, GAsyncResult *res, gpointer user_data) +{ + g_autoptr(CcFprintdDevice) fprintd_device = NULL; + g_autoptr(GTask) task = G_TASK (user_data); + g_autoptr(GError) error = NULL; + CcFingerprintManager *self = g_task_get_source_object (task); + DeviceListData *list_data = g_task_get_task_data (task); + + fprintd_device = cc_fprintd_device_proxy_new_for_bus_finish (res, &error); + list_data->waiting_devices--; + + if (error) + { + if (list_data->waiting_devices == 0) + g_task_return_error (task, g_steal_pointer (&error)); + else if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Impossible to ge the device proxy: %s", error->message); + + return; + } + + g_debug ("Got fingerprint device %s", cc_fprintd_device_get_name (fprintd_device)); + + list_data->devices = g_list_append (list_data->devices, g_steal_pointer (&fprintd_device)); + + if (list_data->waiting_devices == 0) + { + cache_devices (self, list_data->devices); + g_task_return_pointer (task, g_steal_pointer (&list_data->devices), object_list_destroy_notify); + } +} + +static void +on_devices_list (GObject *object, GAsyncResult *res, gpointer user_data) +{ + CcFprintdManager *fprintd_manager = CC_FPRINTD_MANAGER (object); + g_autoptr(GTask) task = G_TASK (user_data); + g_autoptr(GError) error = NULL; + g_auto(GStrv) devices_list = NULL; + DeviceListData *list_data; + guint i; + + cc_fprintd_manager_call_get_devices_finish (fprintd_manager, &devices_list, res, &error); + + if (error) + { + g_task_return_error (task, g_steal_pointer (&error)); + return; + } + + if (!devices_list || !devices_list[0]) + { + g_task_return_pointer (task, NULL, NULL); + return; + } + + list_data = g_new0 (DeviceListData, 1); + g_task_set_task_data (task, list_data, g_free); + + g_debug ("Fprintd replied with %u device(s)", g_strv_length (devices_list)); + + for (i = 0; devices_list[i] != NULL; ++i) + { + const char *device_path = devices_list[i]; + + list_data->waiting_devices++; + + cc_fprintd_device_proxy_new_for_bus (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + CC_FPRINTD_NAME, + device_path, + g_task_get_cancellable (task), + on_device_proxy, + g_object_ref (task)); + } +} + +static void +on_manager_proxy (GObject *object, GAsyncResult *res, gpointer user_data) +{ + g_autoptr(GTask) task = G_TASK (user_data); + g_autoptr(CcFprintdManager) fprintd_manager = NULL; + g_autoptr(GError) error = NULL; + + fprintd_manager = cc_fprintd_manager_proxy_new_for_bus_finish (res, &error); + + if (error) + { + g_task_return_error (task, g_steal_pointer (&error)); + return; + } + + g_debug ("Fprintd manager connected"); + + cc_fprintd_manager_call_get_devices (fprintd_manager, + g_task_get_cancellable (task), + on_devices_list, + g_object_ref (task)); +} + +static void +fprintd_manager_connect (CcFingerprintManager *self, + GAsyncReadyCallback callback, + GTask *task) +{ + g_assert (G_IS_TASK (task)); + + cc_fprintd_manager_proxy_new_for_bus (G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, + CC_FPRINTD_NAME, CC_FPRINTD_MANAGER_PATH, + g_task_get_cancellable (task), + callback, + task); +} + +void +cc_fingerprint_manager_get_devices (CcFingerprintManager *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + CcFingerprintManagerPrivate *priv = cc_fingerprint_manager_get_instance_private (self); + g_autoptr(GTask) task = NULL; + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_source_tag (task, cc_fingerprint_manager_get_devices); + + if (priv->cached_devices) + { + GList *devices; + + devices = g_list_copy_deep (priv->cached_devices, (GCopyFunc) g_object_ref, NULL); + g_task_return_pointer (task, devices, object_list_destroy_notify); + return; + } + + fprintd_manager_connect (self, on_manager_proxy, g_steal_pointer (&task)); +} + +/** + * cc_fingerprint_manager_get_devices_finish: + * @self: The #CcFingerprintManager + * @result: A #GAsyncResult + * @error: Return location for errors, or %NULL to ignore + * + * Finish an asynchronous operation to list all devices. + * + * Returns: (element-type CcFprintdDevice) (transfer full): List of prints or %NULL on error + */ +GList * +cc_fingerprint_manager_get_devices_finish (CcFingerprintManager *self, + GAsyncResult *res, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (res, self), NULL); + + return g_task_propagate_pointer (G_TASK (res), error); +} + +static void +set_state (CcFingerprintManager *self, + CcFingerprintState state) +{ + CcFingerprintManagerPrivate *priv = cc_fingerprint_manager_get_instance_private (self); + + if (priv->state == state) + return; + + g_debug ("Fingerprint manager state changed to %d", state); + + priv->state = state; + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_STATE]); +} + +typedef struct +{ + guint waiting_devices; + CcFingerprintStateUpdated callback; + gpointer user_data; +} UpdateStateData; + +static void +update_state_callback (GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + CcFingerprintManager *self = CC_FINGERPRINT_MANAGER (object); + CcFingerprintManagerPrivate *priv = cc_fingerprint_manager_get_instance_private (self); + g_autoptr(GError) error = NULL; + CcFingerprintState state; + UpdateStateData *data; + GTask *task; + + g_return_if_fail (g_task_is_valid (res, self)); + + task = G_TASK (res); + g_assert (g_steal_pointer (&priv->current_task) == task); + + state = g_task_propagate_int (task, &error); + data = g_task_get_task_data (task); + + if (error) + { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_warning ("Impossible to update fingerprint manager state: %s", + error->message); + + state = CC_FINGERPRINT_STATE_NONE; + } + + set_state (self, state); + + if (data->callback) + data->callback (self, state, data->user_data, error); +} + +static void +on_device_list_enrolled (GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + CcFprintdDevice *fprintd_device = CC_FPRINTD_DEVICE (object); + g_autoptr(GTask) task = G_TASK (user_data); + g_autoptr(GError) error = NULL; + g_auto(GStrv) enrolled_fingers = NULL; + UpdateStateData *data = g_task_get_task_data (task); + guint num_enrolled_fingers; + + cc_fprintd_device_call_list_enrolled_fingers_finish (fprintd_device, + &enrolled_fingers, + res, &error); + + if (data->waiting_devices == 0) + return; + + data->waiting_devices--; + + if (error) + { + g_autofree char *dbus_error = g_dbus_error_get_remote_error (error); + + if (!g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.NoEnrolledPrints")) + { + if (data->waiting_devices == 0) + g_task_return_error (task, g_steal_pointer (&error)); + else if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Impossible to list enrolled fingers: %s", error->message); + + return; + } + } + + num_enrolled_fingers = enrolled_fingers ? g_strv_length (enrolled_fingers) : 0; + + g_debug ("Device %s has %u enrolled fingers", + cc_fprintd_device_get_name (fprintd_device), + num_enrolled_fingers); + + if (num_enrolled_fingers > 0) + { + data->waiting_devices = 0; + g_task_return_int (task, CC_FINGERPRINT_STATE_ENABLED); + } + else if (data->waiting_devices == 0) + { + g_task_return_int (task, CC_FINGERPRINT_STATE_DISABLED); + } +} + +static void +on_manager_devices_list (GObject *object, + GAsyncResult *res, + gpointer user_data) +{ + CcFingerprintManager *self = CC_FINGERPRINT_MANAGER (object); + CcFingerprintManagerPrivate *priv = cc_fingerprint_manager_get_instance_private (self); + g_autolist(CcFprintdDevice) fprintd_devices = NULL; + g_autoptr(GTask) task = G_TASK (user_data); + g_autoptr(GError) error = NULL; + UpdateStateData *data = g_task_get_task_data (task); + const char *user_name; + GList *l; + + fprintd_devices = cc_fingerprint_manager_get_devices_finish (self, res, &error); + + if (error) + { + g_task_return_error (task, g_steal_pointer (&error)); + return; + } + + if (fprintd_devices == NULL) + { + g_debug ("No fingerprint devices found"); + g_task_return_int (task, CC_FINGERPRINT_STATE_NONE); + return; + } + + user_name = act_user_get_user_name (priv->user); + + for (l = fprintd_devices; l; l = l->next) + { + CcFprintdDevice *device = l->data; + + g_debug ("Connected to device %s, looking for enrolled fingers", + cc_fprintd_device_get_name (device)); + + data->waiting_devices++; + cc_fprintd_device_call_list_enrolled_fingers (device, user_name, + g_task_get_cancellable (task), + on_device_list_enrolled, + g_object_ref (task)); + } +} + +void +cc_fingerprint_manager_update_state (CcFingerprintManager *self, + CcFingerprintStateUpdated callback, + gpointer user_data) +{ + CcFingerprintManagerPrivate *priv = cc_fingerprint_manager_get_instance_private (self); + g_autoptr(GCancellable) cancellable = NULL; + UpdateStateData *data; + + g_return_if_fail (priv->current_task == NULL); + + if (act_user_get_uid (priv->user) != getuid () || + !act_user_is_local_account (priv->user)) + { + set_state (self, CC_FINGERPRINT_STATE_NONE); + return; + } + + cancellable = g_cancellable_new (); + data = g_new0 (UpdateStateData, 1); + data->callback = callback; + data->user_data = user_data; + + priv->current_task = g_task_new (self, cancellable, update_state_callback, NULL); + g_task_set_source_tag (priv->current_task, cc_fingerprint_manager_update_state); + g_task_set_task_data (priv->current_task, data, g_free); + + set_state (self, CC_FINGERPRINT_STATE_UPDATING); + + cc_fingerprint_manager_get_devices (self, cancellable, on_manager_devices_list, + priv->current_task); +} + +CcFingerprintState +cc_fingerprint_manager_get_state (CcFingerprintManager *self) +{ + CcFingerprintManagerPrivate *priv = cc_fingerprint_manager_get_instance_private (self); + + g_return_val_if_fail (CC_IS_FINGERPRINT_MANAGER (self), CC_FINGERPRINT_STATE_NONE); + + return priv->state; +} + +ActUser * +cc_fingerprint_manager_get_user (CcFingerprintManager *self) +{ + CcFingerprintManagerPrivate *priv = cc_fingerprint_manager_get_instance_private (self); + + g_return_val_if_fail (CC_IS_FINGERPRINT_MANAGER (self), NULL); + + return priv->user; +} diff --git a/panels/user-accounts/cc-fingerprint-manager.h b/panels/user-accounts/cc-fingerprint-manager.h new file mode 100644 index 0000000..d12f52c --- /dev/null +++ b/panels/user-accounts/cc-fingerprint-manager.h @@ -0,0 +1,74 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- + * + * Copyright (C) 2020 Canonical Ltd. + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see <http://www.gnu.org/licenses/>. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authors: Marco Trevisan <marco.trevisan@canonical.com> + */ + +#pragma once + +#include <glib-object.h> +#include <act/act.h> + +G_BEGIN_DECLS + +#define CC_TYPE_FINGERPRINT_MANAGER (cc_fingerprint_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (CcFingerprintManager, cc_fingerprint_manager, CC, FINGERPRINT_MANAGER, GObject) + +/** + * CcFingerprintManager: + * @CC_FINGERPRINT_STATE_NONE: Fingerprint recognition is not available + * @CC_FINGERPRINT_STATE_UPDATING: Fingerprint recognition is being fetched + * @CC_FINGERPRINT_STATE_ENABLED: Fingerprint recognition is enabled + * @CC_FINGERPRINT_STATE_DISABLED: Fingerprint recognition is disabled + * + * The status of the fingerprint support. + */ +typedef enum { + CC_FINGERPRINT_STATE_NONE, + CC_FINGERPRINT_STATE_UPDATING, + CC_FINGERPRINT_STATE_ENABLED, + CC_FINGERPRINT_STATE_DISABLED, +} CcFingerprintState; + +typedef void (*CcFingerprintStateUpdated) (CcFingerprintManager *fp_manager, + CcFingerprintState state, + gpointer user_data, + GError *error); + +CcFingerprintManager * cc_fingerprint_manager_new (ActUser *user); + +CcFingerprintState cc_fingerprint_manager_get_state (CcFingerprintManager *fp_manager); + +ActUser * cc_fingerprint_manager_get_user (CcFingerprintManager *fp_manager); + +void cc_fingerprint_manager_update_state (CcFingerprintManager *fp_manager, + CcFingerprintStateUpdated callback, + gpointer user_data); + +void cc_fingerprint_manager_get_devices (CcFingerprintManager *fp_manager, + GCancellable *cancellable, + GAsyncReadyCallback res, + gpointer user_data); + +GList *cc_fingerprint_manager_get_devices_finish (CcFingerprintManager *fp_manager, + GAsyncResult *res, + GError **error); + +G_END_DECLS diff --git a/panels/user-accounts/cc-login-history-dialog.c b/panels/user-accounts/cc-login-history-dialog.c new file mode 100644 index 0000000..fcb7457 --- /dev/null +++ b/panels/user-accounts/cc-login-history-dialog.c @@ -0,0 +1,346 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright 2012 Red Hat, Inc, + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see <http://www.gnu.org/licenses/>. + * + * Written by: Ondrej Holy <oholy@redhat.com> + */ + +#include "config.h" + +#include <unistd.h> +#include <stdlib.h> +#include <sys/types.h> +#include <sys/wait.h> + +#include <adwaita.h> +#include <glib.h> +#include <glib/gi18n.h> +#include <gtk/gtk.h> +#include <act/act.h> + +#include "cc-login-history-dialog.h" +#include "cc-user-accounts-resources.h" +#include "cc-util.h" +#include "user-utils.h" + +struct _CcLoginHistoryDialog +{ + GtkDialog parent_instance; + + GtkHeaderBar *header_bar; + GtkLabel *title_label; + GtkListBox *history_box; + GtkButton *next_button; + GtkButton *previous_button; + + GDateTime *week; + GDateTime *current_week; + + ActUser *user; +}; + +G_DEFINE_TYPE (CcLoginHistoryDialog, cc_login_history_dialog, GTK_TYPE_DIALOG) + +typedef struct { + gint64 login_time; + gint64 logout_time; + const gchar *type; +} CcLoginHistory; + +static void +show_week_label (CcLoginHistoryDialog *self) +{ + g_autofree gchar *label = NULL; + GTimeSpan span; + + span = g_date_time_difference (self->current_week, self->week); + if (span == 0) { + label = g_strdup (_("This Week")); + } + else if (span == G_TIME_SPAN_DAY * 7) { + label = g_strdup (_("Last Week")); + } + else { + g_autofree gchar *from = NULL; + g_autofree gchar *to = NULL; + g_autoptr(GDateTime) date = NULL; + + date = g_date_time_add_days (self->week, 6); + /* Translators: This is a date format string in the style of "Feb 18", + shown as the first day of a week on login history dialog. */ + from = g_date_time_format (self->week, C_("login history week label","%b %e")); + if (g_date_time_get_year (self->week) == g_date_time_get_year (self->current_week)) { + /* Translators: This is a date format string in the style of "Feb 24", + shown as the last day of a week on login history dialog. */ + to = g_date_time_format (date, C_("login history week label","%b %e")); + } + else { + /* Translators: This is a date format string in the style of "Feb 24, 2013", + shown as the last day of a week on login history dialog. */ + to = g_date_time_format (date, C_("login history week label","%b %e, %Y")); + } + + /* Translators: This indicates a week label on a login history. + The first %s is the first day of a week, and the second %s the last day. */ + label = g_strdup_printf(C_("login history week label", "%s — %s"), from, to); + } + + gtk_label_set_label (self->title_label, label); +} + +static void +clear_history (CcLoginHistoryDialog *self) +{ + GtkWidget *child; + + child = gtk_widget_get_first_child (GTK_WIDGET (self->history_box)); + while (child) { + GtkWidget *next = gtk_widget_get_next_sibling (child); + + if (ADW_ACTION_ROW (child)) + gtk_list_box_remove (self->history_box, GTK_WIDGET (child)); + + child = next; + } +} + +static GArray * +get_login_history (ActUser *user) +{ + GArray *login_history; + GVariantIter *iter, *iter2; + GVariant *variant; + const GVariant *value; + const gchar *key; + CcLoginHistory history; + + login_history = NULL; + value = act_user_get_login_history (user); + g_variant_get ((GVariant *) value, "a(xxa{sv})", &iter); + while (g_variant_iter_loop (iter, "(xxa{sv})", &history.login_time, &history.logout_time, &iter2)) { + while (g_variant_iter_loop (iter2, "{&sv}", &key, &variant)) { + if (g_strcmp0 (key, "type") == 0) { + history.type = g_variant_get_string (variant, NULL); + } + } + + if (login_history == NULL) { + login_history = g_array_new (FALSE, TRUE, sizeof (CcLoginHistory)); + } + + g_array_append_val (login_history, history); + } + + return login_history; +} + +static void +set_sensitivity (CcLoginHistoryDialog *self) +{ + g_autoptr(GArray) login_history = NULL; + CcLoginHistory history; + gboolean sensitive = FALSE; + + login_history = get_login_history (self->user); + if (login_history != NULL) { + history = g_array_index (login_history, CcLoginHistory, 0); + sensitive = g_date_time_to_unix (self->week) > history.login_time; + } + gtk_widget_set_sensitive (GTK_WIDGET (self->previous_button), sensitive); + + sensitive = (g_date_time_compare (self->current_week, self->week) == 1); + gtk_widget_set_sensitive (GTK_WIDGET (self->next_button), sensitive); +} + +static void +add_record (CcLoginHistoryDialog *self, GDateTime *datetime, gchar *record_string, gint line) +{ + g_autofree gchar *date = NULL; + g_autofree gchar *time = NULL; + g_autofree gchar *str = NULL; + GtkWidget *row; + + date = cc_util_get_smart_date (datetime); + /* Translators: This is a time format string in the style of "22:58". + It indicates a login time which follows a date. */ + time = g_date_time_format (datetime, C_("login date-time", "%k:%M")); + /* Translators: This indicates a login date-time. + The first %s is a date, and the second %s a time. */ + str = g_strdup_printf(C_("login date-time", "%s, %s"), date, time); + + row = adw_action_row_new (); + adw_preferences_row_set_title (ADW_PREFERENCES_ROW (row), record_string); + adw_action_row_set_subtitle (ADW_ACTION_ROW (row), str); + + gtk_list_box_insert (self->history_box, row, line); +} + +static void +show_week (CcLoginHistoryDialog *self) +{ + g_autoptr(GArray) login_history = NULL; + g_autoptr(GDateTime) datetime = NULL; + g_autoptr(GDateTime) temp = NULL; + gint64 from, to; + gint i, line; + CcLoginHistory history; + + show_week_label (self); + clear_history (self); + set_sensitivity (self); + + login_history = get_login_history (self->user); + if (login_history == NULL) { + return; + } + + /* Find first record for week */ + from = g_date_time_to_unix (self->week); + temp = g_date_time_add_weeks (self->week, 1); + to = g_date_time_to_unix (temp); + for (i = login_history->len - 1; i >= 0; i--) { + history = g_array_index (login_history, CcLoginHistory, i); + if (history.login_time < to) { + break; + } + } + + /* Add new session records */ + line = 0; + for (;i >= 0; i--) { + history = g_array_index (login_history, CcLoginHistory, i); + + /* Display only x-session and tty records */ + if (!g_str_has_prefix (history.type, ":") && + !g_str_has_prefix (history.type, "tty")) { + continue; + } + + if (history.logout_time > 0 && history.logout_time < from) { + break; + } + + if (history.logout_time > 0 && history.logout_time < to) { + datetime = g_date_time_new_from_unix_local (history.logout_time); + add_record (self, datetime, _("Session Ended"), line); + line++; + } + + if (history.login_time >= from) { + datetime = g_date_time_new_from_unix_local (history.login_time); + add_record (self, datetime, _("Session Started"), line); + line++; + } + } +} + +static void +previous_button_clicked_cb (CcLoginHistoryDialog *self) +{ + g_autoptr(GDateTime) temp = NULL; + + temp = self->week; + self->week = g_date_time_add_weeks (self->week, -1); + + show_week (self); +} + +static void +next_button_clicked_cb (CcLoginHistoryDialog *self) +{ + g_autoptr(GDateTime) temp = NULL; + + temp = self->week; + self->week = g_date_time_add_weeks (self->week, 1); + + show_week (self); +} + +static void +cc_login_history_dialog_dispose (GObject *object) +{ + CcLoginHistoryDialog *self = CC_LOGIN_HISTORY_DIALOG (object); + + g_clear_object (&self->user); + g_clear_pointer (&self->week, g_date_time_unref); + g_clear_pointer (&self->current_week, g_date_time_unref); + + G_OBJECT_CLASS (cc_login_history_dialog_parent_class)->dispose (object); +} + +void +cc_login_history_dialog_class_init (CcLoginHistoryDialogClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = cc_login_history_dialog_dispose; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/user-accounts/cc-login-history-dialog.ui"); + + gtk_widget_class_bind_template_child (widget_class, CcLoginHistoryDialog, header_bar); + gtk_widget_class_bind_template_child (widget_class, CcLoginHistoryDialog, title_label); + gtk_widget_class_bind_template_child (widget_class, CcLoginHistoryDialog, history_box); + gtk_widget_class_bind_template_child (widget_class, CcLoginHistoryDialog, next_button); + gtk_widget_class_bind_template_child (widget_class, CcLoginHistoryDialog, previous_button); + + gtk_widget_class_bind_template_callback (widget_class, next_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, previous_button_clicked_cb); +} + +void +cc_login_history_dialog_init (CcLoginHistoryDialog *self) +{ + g_resources_register (cc_user_accounts_get_resource ()); + + gtk_widget_init_template (GTK_WIDGET (self)); +} + +CcLoginHistoryDialog * +cc_login_history_dialog_new (ActUser *user) +{ + CcLoginHistoryDialog *self; + g_autoptr(GDateTime) temp = NULL; + g_autoptr(GDateTime) local = NULL; + g_autofree gchar *title = NULL; + + g_return_val_if_fail (ACT_IS_USER (user), NULL); + + self = g_object_new (CC_TYPE_LOGIN_HISTORY_DIALOG, + "use-header-bar", 1, + NULL); + + self->user = g_object_ref (user); + + /* Set the first day of this week */ + local = g_date_time_new_now_local (); + temp = g_date_time_new_local (g_date_time_get_year (local), + g_date_time_get_month (local), + g_date_time_get_day_of_month (local), + 0, 0, 0); + self->week = g_date_time_add_days (temp, 1 - g_date_time_get_day_of_week (temp)); + self->current_week = g_date_time_ref (self->week); + + /* Translators: This is the title of the "Account Activity" dialog. + The %s is the user real name. */ + title = g_strdup_printf (_("%s — Account Activity"), + act_user_get_real_name (self->user)); + gtk_label_set_label (self->title_label, title); + + show_week (self); + + return self; +} diff --git a/panels/user-accounts/cc-login-history-dialog.h b/panels/user-accounts/cc-login-history-dialog.h new file mode 100644 index 0000000..e71f160 --- /dev/null +++ b/panels/user-accounts/cc-login-history-dialog.h @@ -0,0 +1,33 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright 2012 Red Hat, Inc, + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see <http://www.gnu.org/licenses/>. + * + * Written by: Ondrej Holy <oholy@redhat.com> + */ + +#pragma once + +#include <gtk/gtk.h> +#include <act/act-user.h> + +G_BEGIN_DECLS + +#define CC_TYPE_LOGIN_HISTORY_DIALOG (cc_login_history_dialog_get_type ()) +G_DECLARE_FINAL_TYPE (CcLoginHistoryDialog, cc_login_history_dialog, CC, LOGIN_HISTORY_DIALOG, GtkDialog) + +CcLoginHistoryDialog *cc_login_history_dialog_new (ActUser *user); + +G_END_DECLS diff --git a/panels/user-accounts/cc-login-history-dialog.ui b/panels/user-accounts/cc-login-history-dialog.ui new file mode 100644 index 0000000..f12f9f1 --- /dev/null +++ b/panels/user-accounts/cc-login-history-dialog.ui @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="CcLoginHistoryDialog" parent="GtkDialog"> + <property name="default-width">400</property> + <property name="default-height">400</property> + <property name="modal">True</property> + <property name="icon_name">system-users</property> + <accessibility> + <relation name="labelled-by">title_label</relation> + </accessibility> + <child internal-child="headerbar"> + <object class="GtkHeaderBar" id="header_bar"> + <property name="title-widget"> + <object class="GtkLabel" id="title_label"> + <style> + <class name="title"/> + </style> + </object> + </property> + <child> + <object class="GtkBox"> + <style> + <class name="linked"/> + </style> + <child> + <object class="GtkButton" id="previous_button"> + <property name="icon_name">go-previous-symbolic</property> + <property name="valign">center</property> + <accessibility> + <property name="label" translatable="yes">Previous</property> + </accessibility> + <signal name="clicked" handler="previous_button_clicked_cb" object="CcLoginHistoryDialog" swapped="yes"/> + </object> + </child> + <child> + <object class="GtkButton" id="next_button"> + <property name="icon_name">go-next-symbolic</property> + <property name="valign">center</property> + <accessibility> + <property name="label" translatable="yes">Next</property> + </accessibility> + <signal name="clicked" handler="next_button_clicked_cb" object="CcLoginHistoryDialog" swapped="yes"/> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="AdwPreferencesPage"> + <child> + <object class="AdwPreferencesGroup"> + <child> + <object class="GtkListBox" id="history_box"> + <property name="hexpand">True</property> + <style> + <class name="boxed-list" /> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/panels/user-accounts/cc-password-dialog.c b/panels/user-accounts/cc-password-dialog.c new file mode 100644 index 0000000..1f71825 --- /dev/null +++ b/panels/user-accounts/cc-password-dialog.c @@ -0,0 +1,530 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright 2009-2010 Red Hat, Inc, + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see <http://www.gnu.org/licenses/>. + * + * Written by: Matthias Clasen <mclasen@redhat.com> + */ + +#include "config.h" + +#include <unistd.h> +#include <stdlib.h> +#include <sys/types.h> +#include <sys/wait.h> + +#include <adwaita.h> +#include <glib.h> +#include <glib/gi18n.h> +#include <gtk/gtk.h> +#include <act/act.h> + +#include "cc-password-dialog.h" +#include "cc-user-accounts-resources.h" +#include "pw-utils.h" +#include "run-passwd.h" +#include "user-utils.h" + +#define PASSWORD_CHECK_TIMEOUT 600 + +struct _CcPasswordDialog +{ + AdwWindow parent_instance; + + GtkCheckButton *action_login_radio; + GtkCheckButton *action_now_radio; + GtkButton *generate_password_button; + GtkButton *ok_button; + AdwPasswordEntryRow *old_password_entry; + AdwPreferencesGroup *password_group; + AdwPreferencesGroup *password_on_next_login_group; + AdwPasswordEntryRow *password_entry; + GtkLabel *password_hint_label; + GtkLevelBar *strength_indicator; + AdwPasswordEntryRow *verify_entry; + GtkLabel *verify_label; + + gint password_entry_timeout_id; + + ActUser *user; + ActUserPasswordMode password_mode; + + gboolean old_password_ok; + gint old_password_entry_timeout_id; + + PasswdHandler *passwd_handler; +}; + +G_DEFINE_TYPE (CcPasswordDialog, cc_password_dialog, ADW_TYPE_WINDOW) + +static gint +update_password_strength (CcPasswordDialog *self) +{ + const gchar *password; + const gchar *old_password; + const gchar *username; + gint strength_level; + const gchar *hint; + const gchar *verify; + + password = gtk_editable_get_text (GTK_EDITABLE (self->password_entry)); + old_password = gtk_editable_get_text (GTK_EDITABLE (self->old_password_entry)); + username = act_user_get_user_name (self->user); + + pw_strength (password, old_password, username, + &hint, &strength_level); + + gtk_level_bar_set_value (self->strength_indicator, strength_level); + gtk_label_set_label (self->password_hint_label, hint); + + if (strength_level > 1) { + gtk_widget_remove_css_class (GTK_WIDGET (self->password_entry), "error"); + } else if (strlen (password) == 0) { + //gtk_widget_hide (GTK_WIDGET (self->password_entry_status_icon)); + //gtk_widget_show (GTK_WIDGET (self->generate_password_button)); + } else { + gtk_widget_add_css_class (GTK_WIDGET (self->password_entry), "error"); + } + + verify = gtk_editable_get_text (GTK_EDITABLE (self->verify_entry)); + if (strlen (verify) == 0) { + gtk_widget_set_sensitive (GTK_WIDGET (self->verify_entry), strength_level > 1); + } + + return strength_level; +} + +static void +password_changed_cb (PasswdHandler *handler, + GError *error, + CcPasswordDialog *self) +{ + GtkWidget *dialog; + const gchar *primary_text; + const gchar *secondary_text; + + gtk_widget_set_sensitive (GTK_WIDGET (self), TRUE); + + if (!error) { + gtk_window_close (GTK_WINDOW (self)); + return; + } + + if (error->code == PASSWD_ERROR_REJECTED) { + primary_text = error->message; + secondary_text = _("Please choose another password."); + + gtk_editable_set_text (GTK_EDITABLE (self->password_entry), ""); + gtk_widget_grab_focus (GTK_WIDGET (self->password_entry)); + + gtk_editable_set_text (GTK_EDITABLE (self->verify_entry), ""); + } + else if (error->code == PASSWD_ERROR_AUTH_FAILED) { + primary_text = error->message; + secondary_text = _("Please type your current password again."); + + gtk_editable_set_text (GTK_EDITABLE (self->old_password_entry), ""); + gtk_widget_grab_focus (GTK_WIDGET (self->old_password_entry)); + } + else { + primary_text = _("Password could not be changed"); + secondary_text = error->message; + } + + dialog = gtk_message_dialog_new (GTK_WINDOW (self), + GTK_DIALOG_MODAL, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_CLOSE, + "%s", primary_text); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + "%s", secondary_text); + gtk_window_present (GTK_WINDOW (dialog)); +} + +static void +ok_button_clicked_cb (CcPasswordDialog *self) +{ + const gchar *password; + + password = gtk_editable_get_text (GTK_EDITABLE (self->password_entry)); + + switch (self->password_mode) { + case ACT_USER_PASSWORD_MODE_REGULAR: + if (act_user_get_uid (self->user) == getuid ()) { + /* When setting a password for the current user, + * use passwd directly, to preserve the audit trail + * and to e.g. update the keyring password. + */ + passwd_change_password (self->passwd_handler, password, + (PasswdCallback) password_changed_cb, self); + gtk_widget_set_sensitive (GTK_WIDGET (self), FALSE); + return; + } + + act_user_set_password_mode (self->user, ACT_USER_PASSWORD_MODE_REGULAR); + act_user_set_password (self->user, password, ""); + break; + + case ACT_USER_PASSWORD_MODE_SET_AT_LOGIN: + act_user_set_password_mode (self->user, self->password_mode); + act_user_set_automatic_login (self->user, FALSE); + break; + + default: + g_assert_not_reached (); + } + + gtk_window_close (GTK_WINDOW (self)); +} + +static void +update_sensitivity (CcPasswordDialog *self) +{ + const gchar *password, *verify; + gboolean can_change; + int strength; + + password = gtk_editable_get_text (GTK_EDITABLE (self->password_entry)); + verify = gtk_editable_get_text (GTK_EDITABLE (self->verify_entry)); + + if (self->password_mode == ACT_USER_PASSWORD_MODE_REGULAR) { + strength = update_password_strength (self); + can_change = strength > 1 && strcmp (password, verify) == 0 && + (self->old_password_ok || !gtk_widget_get_visible (GTK_WIDGET (self->old_password_entry))); + } + else { + can_change = TRUE; + } + + gtk_widget_set_sensitive (GTK_WIDGET (self->ok_button), can_change); +} + +static void +mode_change (CcPasswordDialog *self, + ActUserPasswordMode mode) +{ + gboolean active; + + active = (mode == ACT_USER_PASSWORD_MODE_REGULAR); + gtk_widget_set_sensitive (GTK_WIDGET (self->password_entry), active); + gtk_widget_set_sensitive (GTK_WIDGET (self->verify_entry), active); + gtk_widget_set_sensitive (GTK_WIDGET (self->old_password_entry), active); + gtk_check_button_set_active (GTK_CHECK_BUTTON (self->action_now_radio), active); + gtk_check_button_set_active (GTK_CHECK_BUTTON (self->action_login_radio), !active); + + self->password_mode = mode; + update_sensitivity (self); +} + +static void +action_now_radio_toggled_cb (CcPasswordDialog *self) +{ + gint active; + ActUserPasswordMode mode; + + active = gtk_check_button_get_active (GTK_CHECK_BUTTON (self->action_now_radio)); + mode = active ? ACT_USER_PASSWORD_MODE_REGULAR : ACT_USER_PASSWORD_MODE_SET_AT_LOGIN; + mode_change (self, mode); +} + +static void +update_password_match (CcPasswordDialog *self) +{ + const gchar *password; + const gchar *verify; + + password = gtk_editable_get_text (GTK_EDITABLE (self->password_entry)); + verify = gtk_editable_get_text (GTK_EDITABLE (self->verify_entry)); + + if (strlen (verify) > 0) { + if (strcmp (password, verify) != 0) { + gtk_widget_set_visible (GTK_WIDGET (self->verify_label), TRUE); + gtk_widget_add_css_class (GTK_WIDGET (self->verify_entry), "error"); + } + else { + gtk_widget_set_visible (GTK_WIDGET (self->verify_label), FALSE); + gtk_widget_remove_css_class (GTK_WIDGET (self->verify_entry), "error"); + + } + } +} + +static gboolean +password_entry_timeout (CcPasswordDialog *self) +{ + update_password_strength (self); + update_sensitivity (self); + update_password_match (self); + + self->password_entry_timeout_id = 0; + + return FALSE; +} + +static void +recheck_password_match (CcPasswordDialog *self) +{ + if (self->password_entry_timeout_id != 0) { + g_source_remove (self->password_entry_timeout_id); + self->password_entry_timeout_id = 0; + } + + gtk_widget_set_sensitive (GTK_WIDGET (self->ok_button), FALSE); + + self->password_entry_timeout_id = g_timeout_add (PASSWORD_CHECK_TIMEOUT, + (GSourceFunc) password_entry_timeout, + self); +} + +static void +password_entry_changed (CcPasswordDialog *self) +{ + gtk_widget_add_css_class (GTK_WIDGET (self->password_entry), "error"); + gtk_widget_add_css_class (GTK_WIDGET (self->verify_entry), "error"); + recheck_password_match (self); +} + +static void +verify_entry_changed (CcPasswordDialog *self) +{ + gtk_widget_add_css_class (GTK_WIDGET (self->verify_entry), "error"); + recheck_password_match (self); +} + +static gboolean +password_entry_focus_out_cb (CcPasswordDialog *self) +{ + if (self->password_entry_timeout_id != 0) { + g_source_remove (self->password_entry_timeout_id); + self->password_entry_timeout_id = 0; + } + + if (self->user != NULL) + password_entry_timeout (self); + + return FALSE; +} + + +static gboolean +password_entry_key_press_cb (GtkEventControllerKey *controller, + guint keyval, + guint keycode, + GdkModifierType state, + CcPasswordDialog *self) +{ + if (self->password_entry_timeout_id != 0) { + g_source_remove (self->password_entry_timeout_id); + self->password_entry_timeout_id = 0; + } + + if (keyval == GDK_KEY_Tab) + password_entry_timeout (self); + + return FALSE; +} + +static void +auth_cb (PasswdHandler *handler, + GError *error, + CcPasswordDialog *self) +{ + if (error) { + self->old_password_ok = FALSE; + } + else { + self->old_password_ok = TRUE; + gtk_widget_remove_css_class (GTK_WIDGET (self->old_password_entry), "error"); + } + + update_sensitivity (self); +} + +static gboolean +old_password_entry_timeout (CcPasswordDialog *self) +{ + const gchar *text; + + update_sensitivity (self); + + text = gtk_editable_get_text (GTK_EDITABLE (self->old_password_entry)); + if (!self->old_password_ok) { + passwd_authenticate (self->passwd_handler, text, (PasswdCallback)auth_cb, self); + } + + self->old_password_entry_timeout_id = 0; + + return FALSE; +} + +static gboolean +old_password_entry_focus_out_cb (CcPasswordDialog *self) +{ + if (self->old_password_entry_timeout_id != 0) { + g_source_remove (self->old_password_entry_timeout_id); + self->old_password_entry_timeout_id = 0; + } + + if (self->user != NULL) + old_password_entry_timeout (self); + + return FALSE; +} + +static void +old_password_entry_changed (CcPasswordDialog *self) +{ + if (self->old_password_entry_timeout_id != 0) { + g_source_remove (self->old_password_entry_timeout_id); + self->old_password_entry_timeout_id = 0; + } + + gtk_widget_add_css_class (GTK_WIDGET (self->old_password_entry), "error"); + gtk_widget_set_sensitive (GTK_WIDGET (self->ok_button), FALSE); + + self->old_password_ok = FALSE; + self->old_password_entry_timeout_id = g_timeout_add (PASSWORD_CHECK_TIMEOUT, + (GSourceFunc) old_password_entry_timeout, + self); +} + +static void +generate_password (CcPasswordDialog *self) +{ + g_autofree gchar *pwd = NULL; + + pwd = pw_generate (); + if (pwd == NULL) + return; + + gtk_editable_set_text (GTK_EDITABLE (self->password_entry), pwd); + gtk_editable_set_text (GTK_EDITABLE (self->verify_entry), pwd); + gtk_widget_set_sensitive (GTK_WIDGET (self->verify_entry), TRUE); + + gtk_widget_hide (GTK_WIDGET (self->generate_password_button)); +} + +static void +cc_password_dialog_dispose (GObject *object) +{ + CcPasswordDialog *self = CC_PASSWORD_DIALOG (object); + + g_clear_object (&self->user); + + if (self->passwd_handler) { + passwd_destroy (self->passwd_handler); + self->passwd_handler = NULL; + } + + if (self->old_password_entry_timeout_id != 0) { + g_source_remove (self->old_password_entry_timeout_id); + self->old_password_entry_timeout_id = 0; + } + + if (self->password_entry_timeout_id != 0) { + g_source_remove (self->password_entry_timeout_id); + self->password_entry_timeout_id = 0; + } + + G_OBJECT_CLASS (cc_password_dialog_parent_class)->dispose (object); +} + +static void +cc_password_dialog_class_init (CcPasswordDialogClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = cc_password_dialog_dispose; + + gtk_widget_class_add_binding_action (widget_class, GDK_KEY_Escape, 0, "window.close", NULL); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/user-accounts/cc-password-dialog.ui"); + + gtk_widget_class_bind_template_child (widget_class, CcPasswordDialog, action_login_radio); + gtk_widget_class_bind_template_child (widget_class, CcPasswordDialog, action_now_radio); + gtk_widget_class_bind_template_child (widget_class, CcPasswordDialog, generate_password_button); + gtk_widget_class_bind_template_child (widget_class, CcPasswordDialog, ok_button); + gtk_widget_class_bind_template_child (widget_class, CcPasswordDialog, old_password_entry); + gtk_widget_class_bind_template_child (widget_class, CcPasswordDialog, password_group); + gtk_widget_class_bind_template_child (widget_class, CcPasswordDialog, password_on_next_login_group); + gtk_widget_class_bind_template_child (widget_class, CcPasswordDialog, password_entry); + gtk_widget_class_bind_template_child (widget_class, CcPasswordDialog, password_hint_label); + gtk_widget_class_bind_template_child (widget_class, CcPasswordDialog, strength_indicator); + gtk_widget_class_bind_template_child (widget_class, CcPasswordDialog, verify_entry); + gtk_widget_class_bind_template_child (widget_class, CcPasswordDialog, verify_label); + + gtk_widget_class_bind_template_callback (widget_class, action_now_radio_toggled_cb); + gtk_widget_class_bind_template_callback (widget_class, generate_password); + gtk_widget_class_bind_template_callback (widget_class, old_password_entry_changed); + gtk_widget_class_bind_template_callback (widget_class, old_password_entry_focus_out_cb); + gtk_widget_class_bind_template_callback (widget_class, ok_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, password_entry_changed); + gtk_widget_class_bind_template_callback (widget_class, password_entry_focus_out_cb); + gtk_widget_class_bind_template_callback (widget_class, password_entry_key_press_cb); + gtk_widget_class_bind_template_callback (widget_class, verify_entry_changed); +} + +static void +cc_password_dialog_init (CcPasswordDialog *self) +{ + g_resources_register (cc_user_accounts_get_resource ()); + + gtk_widget_init_template (GTK_WIDGET (self)); +} + +CcPasswordDialog * +cc_password_dialog_new (ActUser *user) +{ + CcPasswordDialog *self; + GtkWindow *window; + + g_return_val_if_fail (ACT_IS_USER (user), NULL); + + self = g_object_new (CC_TYPE_PASSWORD_DIALOG, + NULL); + + self->user = g_object_ref (user); + + if (act_user_get_uid (self->user) == getuid ()) { + gboolean visible; + + mode_change (self, ACT_USER_PASSWORD_MODE_REGULAR); + gtk_widget_hide (GTK_WIDGET (self->password_on_next_login_group)); + + visible = (act_user_get_password_mode (user) != ACT_USER_PASSWORD_MODE_NONE); + gtk_widget_set_visible (GTK_WIDGET (self->old_password_entry), visible); + self->old_password_ok = !visible; + + self->passwd_handler = passwd_init (); + } + else { + mode_change (self, ACT_USER_PASSWORD_MODE_SET_AT_LOGIN); + gtk_widget_show (GTK_WIDGET (self->password_on_next_login_group)); + + gtk_widget_hide (GTK_WIDGET (self->old_password_entry)); + self->old_password_ok = TRUE; + } + + if (self->old_password_ok == FALSE) + gtk_widget_grab_focus (GTK_WIDGET (self->old_password_entry)); + else + gtk_widget_grab_focus (GTK_WIDGET (self->password_entry)); + + window = (GtkWindow *) gtk_widget_get_native (GTK_WIDGET (self)); + gtk_window_set_default_widget (window, GTK_WIDGET (self->ok_button)); + + return self; +} diff --git a/panels/user-accounts/cc-password-dialog.h b/panels/user-accounts/cc-password-dialog.h new file mode 100644 index 0000000..3820d6d --- /dev/null +++ b/panels/user-accounts/cc-password-dialog.h @@ -0,0 +1,34 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright 2009-2010 Red Hat, Inc, + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see <http://www.gnu.org/licenses/>. + * + * Written by: Matthias Clasen <mclasen@redhat.com> + */ + +#pragma once + +#include <adwaita.h> +#include <gtk/gtk.h> +#include <act/act.h> + +G_BEGIN_DECLS + +#define CC_TYPE_PASSWORD_DIALOG (cc_password_dialog_get_type ()) +G_DECLARE_FINAL_TYPE (CcPasswordDialog, cc_password_dialog, CC, PASSWORD_DIALOG, AdwWindow) + +CcPasswordDialog *cc_password_dialog_new (ActUser *user); + +G_END_DECLS diff --git a/panels/user-accounts/cc-password-dialog.ui b/panels/user-accounts/cc-password-dialog.ui new file mode 100644 index 0000000..84e1455 --- /dev/null +++ b/panels/user-accounts/cc-password-dialog.ui @@ -0,0 +1,184 @@ +<?xml version="1.0"?> +<interface> + <template class="CcPasswordDialog" parent="AdwWindow"> + <property name="title" translatable="yes">Change Password</property> + <property name="modal">True</property> + <property name="hide-on-close">True</property> + <property name="destroy_with_parent">True</property> + <property name="icon_name">system-users</property> + <property name="default-width">640</property> + <property name="default-height">420</property> + <property name="content"> + <object class="GtkBox"> + <property name="orientation">vertical</property> + <child> + <object class="AdwHeaderBar"> + <property name="show-start-title-buttons">False</property> + <property name="show-end-title-buttons">False</property> + <child type="start"> + <object class="GtkButton"> + <property name="label" translatable="yes">_Cancel</property> + <property name="visible">True</property> + <property name="visible">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <property name="valign">center</property> + <signal name="clicked" handler="gtk_window_destroy" object="CcPasswordDialog" swapped="yes"/> + <style> + <class name="text-button"/> + </style> + </object> + </child> + <child type="end"> + <object class="GtkButton" id="ok_button"> + <property name="label" translatable="yes">Ch_ange</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <property name="valign">center</property> + <signal name="clicked" handler="ok_button_clicked_cb" object="CcPasswordDialog" swapped="yes"/> + <style> + <class name="text-button"/> + <class name="suggested-action"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="AdwPreferencesPage"> + <child> + <object class="AdwPreferencesGroup" id="password_group"> + <child> + <object class="AdwPasswordEntryRow" id="old_password_entry"> + <property name="title" translatable="yes">Current Password</property> + <signal name="notify::text" handler="old_password_entry_changed" object="CcPasswordDialog" swapped="yes"/> + <signal name="activate" handler="old_password_entry_focus_out_cb" object="CcPasswordDialog" swapped="yes"/> + <style> + <class name="error"/> + </style> + <child> + <object class="GtkEventControllerFocus"> + <signal name="leave" handler="old_password_entry_focus_out_cb" object="CcPasswordDialog" swapped="yes"/> + </object> + </child> + </object> + </child> + <child> + <object class="AdwPasswordEntryRow" id="password_entry"> + <property name="title" translatable="yes">New Password</property> + <signal name="notify::text" handler="password_entry_changed" object="CcPasswordDialog" swapped="yes"/> + <signal name="activate" handler="password_entry_focus_out_cb" object="CcPasswordDialog" swapped="yes"/> + <style> + <class name="error"/> + </style> + <child> + <object class="GtkEventControllerKey"> + <signal name="key-pressed" handler="password_entry_key_press_cb" object="CcPasswordDialog" swapped="no"/> + </object> + </child> + <child> + <object class="GtkEventControllerFocus"> + <signal name="leave" handler="password_entry_focus_out_cb" object="CcPasswordDialog" swapped="yes"/> + </object> + </child> + <child type="suffix"> + <object class="GtkButton" id="generate_password_button"> + <property name="visible">False</property> + <property name="icon-name">emblem-system-symbolic</property> + <property name="valign">center</property> + <signal name="clicked" handler="generate_password" object="CcPasswordDialog" swapped="yes"/> + <style> + <class name="flat"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="AdwPasswordEntryRow" id="verify_entry"> + <property name="title" translatable="yes">Confirm Password</property> + <signal name="notify::text" handler="verify_entry_changed" object="CcPasswordDialog" swapped="yes"/> + <signal name="activate" handler="password_entry_focus_out_cb" object="CcPasswordDialog" swapped="yes"/> + <style> + <class name="error"/> + </style> + <child> + <object class="GtkEventControllerFocus"> + <signal name="leave" handler="password_entry_focus_out_cb" object="CcPasswordDialog" swapped="yes"/> + </object> + </child> + </object> + </child> + <child> + <object class="GtkLabel" id="verify_label"> + <property name="label" translatable="yes">The passwords do not match.</property> + <property name="visible">False</property> + <property name="margin-top">12</property> + <style> + <class name="error"/> + </style> + </object> + </child> + <child> + <object class="GtkLevelBar" id="strength_indicator"> + <property name="mode">continuous</property> + <property name="max-value">5</property> + <property name="margin-top">12</property> + <offsets> + <offset name="strength-weak" value="1"/> + <offset name="strength-low" value="2"/> + <offset name="strength-medium" value="3"/> + <offset name="strength-good" value="4"/> + <offset name="strength-high" value="5"/> + </offsets> + </object> + </child> + <child> + <object class="GtkLabel" id="password_hint_label"> + <property name="halign">start</property> + <property name="margin-top">12</property> + <property name="wrap">True</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="AdwPreferencesGroup" id="password_on_next_login_group"> + <property name="visible">False</property> + <child> + <object class="AdwActionRow" id="action_login_row"> + <property name="title" translatable="yes">Allow user to change their password on next login</property> + <property name="activatable_widget">action_login_radio</property> + <child type="prefix"> + <object class="GtkCheckButton" id="action_login_radio"> + <property name="receives_default">False</property> + <property name="active">True</property> + </object> + </child> + </object> + </child> + <child> + <object class="AdwActionRow" id="action_now_row"> + <property name="title" translatable="yes">Set a password now</property> + <property name="activatable_widget">action_now_radio</property> + <child type="prefix"> + <object class="GtkCheckButton" id="action_now_radio"> + <property name="receives_default">False</property> + <property name="active">True</property> + <property name="group">action_login_radio</property> + <signal name="toggled" handler="action_now_radio_toggled_cb" object="CcPasswordDialog" swapped="yes"/> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </property> + </template> +</interface> diff --git a/panels/user-accounts/cc-realm-manager.c b/panels/user-accounts/cc-realm-manager.c new file mode 100644 index 0000000..bc43e6d --- /dev/null +++ b/panels/user-accounts/cc-realm-manager.c @@ -0,0 +1,788 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright 2009-2012 Red Hat, Inc. + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see <http://www.gnu.org/licenses/>. + * + * Written by: Stef Walter <stefw@gnome.org> + */ + +#include "config.h" + +#include "cc-realm-manager.h" + +#include <krb5/krb5.h> + +#include <glib.h> +#include <glib/gi18n.h> +#include <glib/gstdio.h> + +#include <sys/types.h> +#include <sys/stat.h> +#include <errno.h> +#include <fcntl.h> + + +struct _CcRealmManager { + CcRealmObjectManagerClient parent_instance; + + CcRealmProvider *provider; + guint diagnostics_sig; +}; + +enum { + REALM_ADDED, + NUM_SIGNALS, +}; + +static gint signals[NUM_SIGNALS] = { 0, }; + +G_DEFINE_TYPE (CcRealmManager, cc_realm_manager, CC_REALM_TYPE_OBJECT_MANAGER_CLIENT); + +GQuark +cc_realm_error_get_quark (void) +{ + static GQuark quark = 0; + if (quark == 0) + quark = g_quark_from_static_string ("cc-realm-error"); + return quark; +} + +static gboolean +is_realm_with_kerberos_and_membership (gpointer object) +{ + g_autoptr(GDBusInterface) kerberos_interface = NULL; + g_autoptr(GDBusInterface) kerberos_membership_interface = NULL; + + if (!G_IS_DBUS_OBJECT (object)) + return FALSE; + + kerberos_interface = g_dbus_object_get_interface (object, "org.freedesktop.realmd.Kerberos"); + if (kerberos_interface == NULL) + return FALSE; + + kerberos_membership_interface = g_dbus_object_get_interface (object, "org.freedesktop.realmd.KerberosMembership"); + if (kerberos_membership_interface == NULL) + return FALSE; + + return TRUE; +} + +static void +on_interface_added (CcRealmManager *self, + GDBusObject *object, + GDBusInterface *interface) +{ + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (interface), G_MAXINT); +} + +static void +on_object_added (CcRealmManager *self, + GDBusObject *object) +{ + GList *interfaces, *l; + + interfaces = g_dbus_object_get_interfaces (object); + for (l = interfaces; l != NULL; l = g_list_next (l)) + on_interface_added (self, object, l->data); + g_list_free_full (interfaces, g_object_unref); + + if (is_realm_with_kerberos_and_membership (object)) { + g_debug ("Saw realm: %s", g_dbus_object_get_object_path (object)); + g_signal_emit (self, signals[REALM_ADDED], 0, object); + } +} + +static void +cc_realm_manager_init (CcRealmManager *self) +{ + g_signal_connect (self, "object-added", G_CALLBACK (on_object_added), NULL); + g_signal_connect (self, "interface-added", G_CALLBACK (on_interface_added), NULL); +} + +static void +cc_realm_manager_dispose (GObject *obj) +{ + CcRealmManager *self = CC_REALM_MANAGER (obj); + GDBusConnection *connection; + + g_clear_object (&self->provider); + + if (self->diagnostics_sig) { + connection = g_dbus_object_manager_client_get_connection (G_DBUS_OBJECT_MANAGER_CLIENT (self)); + if (connection != NULL) + g_dbus_connection_signal_unsubscribe (connection, self->diagnostics_sig); + self->diagnostics_sig = 0; + } + + G_OBJECT_CLASS (cc_realm_manager_parent_class)->dispose (obj); +} + +static void +cc_realm_manager_class_init (CcRealmManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = cc_realm_manager_dispose; + + signals[REALM_ADDED] = g_signal_new ("realm-added", CC_TYPE_REALM_MANAGER, + G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_NONE, 1, CC_REALM_TYPE_OBJECT); +} + +static void +on_realm_diagnostics (GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + const gchar *message; + const gchar *unused; + + if (g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(ss)"))) { + /* Data is already formatted appropriately for stderr */ + g_variant_get (parameters, "(&s&s)", &message, &unused); + g_printerr ("%s", message); + } +} + +static void +on_provider_new (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = G_TASK (user_data); + CcRealmManager *manager = g_task_get_task_data (task); + GError *error = NULL; + + manager->provider = cc_realm_provider_proxy_new_finish (result, &error); + if (error == NULL) { + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (manager->provider), -1); + g_debug ("Created realm manager"); + g_task_return_pointer (task, g_object_ref (manager), g_object_unref); + } else { + g_task_return_error (task, error); + } +} + +static void +on_manager_new (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = G_TASK (user_data); + CcRealmManager *manager; + GDBusConnection *connection; + GError *error = NULL; + GObject *object; + guint sig; + + object = g_async_initable_new_finish (G_ASYNC_INITABLE (source), result, &error); + if (error == NULL) { + manager = CC_REALM_MANAGER (object); + connection = g_dbus_object_manager_client_get_connection (G_DBUS_OBJECT_MANAGER_CLIENT (object)); + + g_debug ("Connected to realmd"); + + sig = g_dbus_connection_signal_subscribe (connection, + "org.freedesktop.realmd", + "org.freedesktop.realmd.Service", + "Diagnostics", + NULL, + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + on_realm_diagnostics, + NULL, + NULL); + manager->diagnostics_sig = sig; + + g_task_set_task_data (task, manager, g_object_unref); + + cc_realm_provider_proxy_new (connection, + G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES, + "org.freedesktop.realmd", + "/org/freedesktop/realmd", + g_task_get_cancellable (task), + on_provider_new, task); + g_steal_pointer (&task); + } else { + g_task_return_error (task, error); + } +} + +void +cc_realm_manager_new (GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + + g_debug ("Connecting to realmd..."); + + task = g_task_new (NULL, cancellable, callback, user_data); + g_task_set_source_tag (task, cc_realm_manager_new); + + g_async_initable_new_async (CC_TYPE_REALM_MANAGER, G_PRIORITY_DEFAULT, + cancellable, on_manager_new, task, + "flags", G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_NONE, + "name", "org.freedesktop.realmd", + "bus-type", G_BUS_TYPE_SYSTEM, + "object-path", "/org/freedesktop/realmd", + "get-proxy-type-func", cc_realm_object_manager_client_get_proxy_type, + NULL); +} + +CcRealmManager * +cc_realm_manager_new_finish (GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, NULL), NULL); + g_return_val_if_fail (g_async_result_is_tagged (result, cc_realm_manager_new), NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +static void +realms_free (gpointer data) +{ + g_list_free_full (data, g_object_unref); +} + +static void +on_provider_discover (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(GTask) task = G_TASK (user_data); + CcRealmManager *manager = g_task_get_source_object (task); + GError *error = NULL; + gboolean no_membership = FALSE; + gchar **realms; + gint relevance; + gint i; + GList *kerberos_realms = NULL; + + cc_realm_provider_call_discover_finish (CC_REALM_PROVIDER (source), &relevance, + &realms, result, &error); + if (error == NULL) { + for (i = 0; realms[i]; i++) { + g_autoptr(GDBusObject) object = NULL; + + object = g_dbus_object_manager_get_object (G_DBUS_OBJECT_MANAGER (manager), realms[i]); + if (object == NULL) { + g_warning ("Realm is not in object manager: %s", realms[i]); + } else { + if (is_realm_with_kerberos_and_membership (object)) { + g_debug ("Discovered realm: %s", realms[i]); + kerberos_realms = g_list_prepend (kerberos_realms, g_steal_pointer (&object)); + } else { + g_debug ("Realm does not support kerberos membership: %s", realms[i]); + no_membership = TRUE; + } + } + } + g_strfreev (realms); + + if (!kerberos_realms && no_membership) { + g_task_return_new_error (task, CC_REALM_ERROR, CC_REALM_ERROR_GENERIC, + _("Cannot automatically join this type of domain")); + } else if (!kerberos_realms) { + g_task_return_new_error (task, CC_REALM_ERROR, CC_REALM_ERROR_GENERIC, + _("No such domain or realm found")); + } else { + kerberos_realms = g_list_reverse (kerberos_realms); + g_task_return_pointer (task, kerberos_realms, realms_free); + } + } else { + g_task_return_error (task, error); + } +} + +void +cc_realm_manager_discover (CcRealmManager *self, + const gchar *input, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + GVariant *options; + + g_return_if_fail (CC_IS_REALM_MANAGER (self)); + g_return_if_fail (input != NULL); + g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); + + g_debug ("Discovering realms for: %s", input); + + task = g_task_new (G_OBJECT (self), cancellable, callback, user_data); + g_task_set_source_tag (task, cc_realm_manager_discover); + + options = g_variant_new_array (G_VARIANT_TYPE ("{sv}"), NULL, 0); + + cc_realm_provider_call_discover (self->provider, input, options, cancellable, + on_provider_discover, task); +} + +GList * +cc_realm_manager_discover_finish (CcRealmManager *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (CC_IS_REALM_MANAGER (self), NULL); + g_return_val_if_fail (g_task_is_valid (result, G_OBJECT (self)), NULL); + g_return_val_if_fail (g_async_result_is_tagged (result, cc_realm_manager_discover), NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +GList * +cc_realm_manager_get_realms (CcRealmManager *self) +{ + GList *objects; + GList *realms = NULL; + GList *l; + + g_return_val_if_fail (CC_IS_REALM_MANAGER (self), NULL); + + objects = g_dbus_object_manager_get_objects (G_DBUS_OBJECT_MANAGER (self)); + for (l = objects; l != NULL; l = g_list_next (l)) { + if (is_realm_with_kerberos_and_membership (l->data)) + realms = g_list_prepend (realms, g_object_ref (l->data)); + } + + g_list_free_full (objects, g_object_unref); + return realms; +} + +static void +string_replace (GString *string, + const gchar *find, + const gchar *replace) +{ + const gchar *at; + gssize pos; + + at = strstr (string->str, find); + if (at != NULL) { + pos = at - string->str; + g_string_erase (string, pos, strlen (find)); + g_string_insert (string, pos, replace); + } +} + +gchar * +cc_realm_calculate_login (CcRealmCommon *realm, + const gchar *username) +{ + const gchar *const *formats; + + formats = cc_realm_common_get_login_formats (realm); + if (formats[0] != NULL) { + GString *string = g_string_new (formats[0]); + string_replace (string, "%U", username); + string_replace (string, "%D", cc_realm_common_get_name (realm)); + return g_string_free (string, FALSE); + } + + return NULL; +} + +gboolean +cc_realm_is_configured (CcRealmObject *realm) +{ + g_autoptr(CcRealmCommon) common = NULL; + const gchar *configured; + gboolean is = FALSE; + + common = cc_realm_object_get_common (realm); + if (common != NULL) { + configured = cc_realm_common_get_configured (common); + is = configured != NULL && !g_str_equal (configured, ""); + } + + return is; +} + +static const gchar * +find_supported_credentials (CcRealmKerberosMembership *membership, + const gchar *owner) +{ + const gchar *cred_owner; + const gchar *cred_type; + GVariant *supported; + GVariantIter iter; + + supported = cc_realm_kerberos_membership_get_supported_join_credentials (membership); + g_return_val_if_fail (supported != NULL, NULL); + + g_variant_iter_init (&iter, supported); + while (g_variant_iter_loop (&iter, "(&s&s)", &cred_type, &cred_owner)) { + if (g_str_equal (owner, cred_owner)) { + if (g_str_equal (cred_type, "ccache") || + g_str_equal (cred_type, "password")) { + return g_intern_string (cred_type); + } + } + } + + return NULL; +} + +static gboolean +realm_join_as_owner (CcRealmObject *realm, + const gchar *owner, + const gchar *login, + const gchar *password, + GBytes *credentials, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(CcRealmKerberosMembership) membership = NULL; + GVariant *contents; + GVariant *options; + GVariant *option; + GVariant *creds; + const gchar *type; + + membership = cc_realm_object_get_kerberos_membership (realm); + g_return_val_if_fail (membership != NULL, FALSE); + + type = find_supported_credentials (membership, owner); + if (type == NULL) { + g_debug ("Couldn't find supported credential type for owner: %s", owner); + return FALSE; + } + + if (g_str_equal (type, "ccache")) { + g_debug ("Using a kerberos credential cache to join the realm"); + contents = g_variant_new_from_data (G_VARIANT_TYPE ("ay"), + g_bytes_get_data (credentials, NULL), + g_bytes_get_size (credentials), + TRUE, (GDestroyNotify)g_bytes_unref, credentials); + + } else if (g_str_equal (type, "password")) { + g_debug ("Using a user/password to join the realm"); + contents = g_variant_new ("(ss)", login, password); + + } else { + g_assert_not_reached (); + } + + creds = g_variant_new ("(ssv)", type, owner, contents); + option = g_variant_new ("{sv}", "manage-system", g_variant_new_boolean (FALSE)); + options = g_variant_new_array (G_VARIANT_TYPE ("{sv}"), &option, 1); + + g_debug ("Calling the Join() method with %s credentials", owner); + + cc_realm_kerberos_membership_call_join (membership, creds, options, + cancellable, callback, user_data); + + return TRUE; +} + +gboolean +cc_realm_join_as_user (CcRealmObject *realm, + const gchar *login, + const gchar *password, + GBytes *credentials, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_return_val_if_fail (CC_REALM_IS_OBJECT (realm), FALSE); + g_return_val_if_fail (credentials != NULL, FALSE); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE); + g_return_val_if_fail (login != NULL, FALSE); + g_return_val_if_fail (password != NULL, FALSE); + g_return_val_if_fail (credentials != NULL, FALSE); + + return realm_join_as_owner (realm, "user", login, password, + credentials, cancellable, callback, user_data); +} + +gboolean +cc_realm_join_as_admin (CcRealmObject *realm, + const gchar *login, + const gchar *password, + GBytes *credentials, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_return_val_if_fail (CC_REALM_IS_OBJECT (realm), FALSE); + g_return_val_if_fail (credentials != NULL, FALSE); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE); + g_return_val_if_fail (login != NULL, FALSE); + g_return_val_if_fail (password != NULL, FALSE); + g_return_val_if_fail (credentials != NULL, FALSE); + + return realm_join_as_owner (realm, "administrator", login, password, credentials, + cancellable, callback, user_data); +} + +gboolean +cc_realm_join_finish (CcRealmObject *realm, + GAsyncResult *result, + GError **error) +{ + g_autoptr(CcRealmKerberosMembership) membership = NULL; + g_autoptr(GError) call_error = NULL; + g_autofree gchar *dbus_error = NULL; + + g_return_val_if_fail (CC_REALM_IS_OBJECT (realm), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + membership = cc_realm_object_get_kerberos_membership (realm); + g_return_val_if_fail (membership != NULL, FALSE); + + if (cc_realm_kerberos_membership_call_join_finish (membership, result, &call_error)) { + g_debug ("Completed Join() method call"); + return TRUE; + } + + dbus_error = g_dbus_error_get_remote_error (call_error); + if (dbus_error == NULL) { + g_debug ("Join() failed because of %s", call_error->message); + g_propagate_error (error, g_steal_pointer (&call_error)); + return FALSE; + } + + g_dbus_error_strip_remote_error (call_error); + + if (g_str_equal (dbus_error, "org.freedesktop.realmd.Error.AuthenticationFailed")) { + g_debug ("Join() failed because of invalid/insufficient credentials"); + g_set_error (error, CC_REALM_ERROR, CC_REALM_ERROR_BAD_LOGIN, + "%s", call_error->message); + } else { + g_debug ("Join() failed because of %s", call_error->message); + g_propagate_error (error, g_steal_pointer (&call_error)); + } + + return FALSE; +} + +typedef struct { + gchar *domain; + gchar *realm; + gchar *user; + gchar *password; +} LoginClosure; + +static void +login_closure_free (gpointer data) +{ + LoginClosure *login = data; + g_clear_pointer (&login->domain, g_free); + g_clear_pointer (&login->realm, g_free); + g_clear_pointer (&login->user, g_free); + g_clear_pointer (&login->password, g_free); + g_slice_free (LoginClosure, login); +} + +static krb5_error_code +login_perform_kinit (krb5_context k5, + const gchar *realm, + const gchar *login, + const gchar *password, + const gchar *filename) +{ + krb5_get_init_creds_opt *opts; + krb5_error_code code; + krb5_principal principal; + krb5_ccache ccache; + krb5_creds creds; + g_autofree gchar *name = NULL; + + name = g_strdup_printf ("%s@%s", login, realm); + code = krb5_parse_name (k5, name, &principal); + + if (code != 0) { + g_debug ("Couldn't parse principal name: %s: %s", + name, krb5_get_error_message (k5, code)); + return code; + } + + g_debug ("Using principal name to kinit: %s", name); + + if (filename == NULL) + code = krb5_cc_default (k5, &ccache); + else + code = krb5_cc_resolve (k5, filename, &ccache); + + if (code != 0) { + krb5_free_principal (k5, principal); + g_debug ("Couldn't open credential cache: %s: %s", + filename ? filename : "<default>", + krb5_get_error_message (k5, code)); + return code; + } + + code = krb5_get_init_creds_opt_alloc (k5, &opts); + g_return_val_if_fail (code == 0, code); + + code = krb5_get_init_creds_opt_set_out_ccache (k5, opts, ccache); + g_return_val_if_fail (code == 0, code); + + code = krb5_get_init_creds_password (k5, &creds, principal, + (char *)password, + NULL, 0, 0, NULL, opts); + + krb5_get_init_creds_opt_free (k5, opts); + krb5_cc_close (k5, ccache); + krb5_free_principal (k5, principal); + + if (code == 0) { + g_debug ("kinit succeeded"); + krb5_free_cred_contents (k5, &creds); + } else { + g_debug ("kinit failed: %s", krb5_get_error_message (k5, code)); + } + + return code; +} + +static void +kinit_thread_func (GTask *t, + gpointer object, + gpointer task_data, + GCancellable *cancellable) +{ + g_autoptr(GTask) task = t; + LoginClosure *login = task_data; + krb5_context k5 = NULL; + krb5_error_code code; + g_autofree gchar *filename = NULL; + gchar *contents; + gsize length; + gint temp_fd; + + filename = g_build_filename (g_get_user_runtime_dir (), + "um-krb5-creds.XXXXXX", NULL); + temp_fd = g_mkstemp_full (filename, O_RDWR, S_IRUSR | S_IWUSR); + if (temp_fd == -1) { + g_warning ("Couldn't create credential cache file: %s: %s", + filename, g_strerror (errno)); + g_clear_pointer (&filename, g_free); + } else { + close (temp_fd); + } + + code = krb5_init_context (&k5); + if (code == 0) { + code = login_perform_kinit (k5, login->realm, login->user, + login->password, filename); + } + + switch (code) { + case 0: + if (filename != NULL) { + g_autoptr(GError) error = NULL; + + if (g_file_get_contents (filename, &contents, &length, &error)) { + g_debug ("Read in credential cache: %s", filename); + } else { + g_warning ("Couldn't read credential cache: %s: %s", + filename, error->message); + } + + g_task_return_pointer (task, g_bytes_new_take (contents, length), (GDestroyNotify) g_bytes_unref); + } + break; + + case KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN: + case KRB5KDC_ERR_POLICY: + g_task_return_new_error (task, CC_REALM_ERROR, CC_REALM_ERROR_BAD_LOGIN, + _("Cannot log in as %s at the %s domain"), + login->user, login->domain); + break; + case KRB5KDC_ERR_PREAUTH_FAILED: + case KRB5KRB_AP_ERR_BAD_INTEGRITY: + g_task_return_new_error (task, CC_REALM_ERROR, CC_REALM_ERROR_BAD_PASSWORD, + _("Invalid password, please try again")); + break; + case KRB5_PREAUTH_FAILED: + case KRB5KDC_ERR_KEY_EXP: + case KRB5KDC_ERR_CLIENT_REVOKED: + case KRB5KDC_ERR_ETYPE_NOSUPP: + case KRB5_PROG_ETYPE_NOSUPP: + g_task_return_new_error (task, CC_REALM_ERROR, CC_REALM_ERROR_CANNOT_AUTH, + _("Cannot log in as %s at the %s domain"), + login->user, login->domain); + break; + default: + g_task_return_new_error (task, CC_REALM_ERROR, CC_REALM_ERROR_GENERIC, + _("Couldn’t connect to the %s domain: %s"), + login->domain, krb5_get_error_message (k5, code)); + break; + } + + if (filename) { + g_unlink (filename); + g_debug ("Deleted credential cache: %s", filename); + } + + if (k5) + krb5_free_context (k5); +} + +void +cc_realm_login (CcRealmObject *realm, + const gchar *user, + const gchar *password, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + LoginClosure *login; + g_autoptr(CcRealmKerberos) kerberos = NULL; + + g_return_if_fail (CC_REALM_IS_OBJECT (realm)); + g_return_if_fail (user != NULL); + g_return_if_fail (password != NULL); + g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); + + kerberos = cc_realm_object_get_kerberos (realm); + g_return_if_fail (kerberos != NULL); + + task = g_task_new (NULL, cancellable, callback, user_data); + g_task_set_source_tag (task, cc_realm_login); + + login = g_slice_new0 (LoginClosure); + login->domain = g_strdup (cc_realm_kerberos_get_domain_name (kerberos)); + login->realm = g_strdup (cc_realm_kerberos_get_realm_name (kerberos)); + login->user = g_strdup (user); + login->password = g_strdup (password); + g_task_set_task_data (task, login, login_closure_free); + + g_task_set_return_on_cancel (task, TRUE); + g_task_run_in_thread (task, kinit_thread_func); +} + +GBytes * +cc_realm_login_finish (GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, NULL), FALSE); + g_return_val_if_fail (g_async_result_is_tagged (result, cc_realm_login), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + return g_task_propagate_pointer (G_TASK (result), error); +} diff --git a/panels/user-accounts/cc-realm-manager.h b/panels/user-accounts/cc-realm-manager.h new file mode 100644 index 0000000..7e68e8e --- /dev/null +++ b/panels/user-accounts/cc-realm-manager.h @@ -0,0 +1,97 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright 2012 Red Hat, Inc. + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see <http://www.gnu.org/licenses/>. + * + * Written by: Stef Walter <stefw@gnome.org> + */ + +#pragma once + +#include "cc-realm-generated.h" + +G_BEGIN_DECLS + +typedef enum { + CC_REALM_ERROR_BAD_LOGIN, + CC_REALM_ERROR_BAD_PASSWORD, + CC_REALM_ERROR_CANNOT_AUTH, + CC_REALM_ERROR_GENERIC, +} CcRealmErrors; + +#define CC_REALM_ERROR (cc_realm_error_get_quark ()) + +GQuark cc_realm_error_get_quark (void) G_GNUC_CONST; + +#define CC_TYPE_REALM_MANAGER (cc_realm_manager_get_type ()) +G_DECLARE_FINAL_TYPE (CcRealmManager, cc_realm_manager, CC, REALM_MANAGER, CcRealmObjectManagerClient) + +void cc_realm_manager_new (GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +CcRealmManager * cc_realm_manager_new_finish (GAsyncResult *result, + GError **error); + +void cc_realm_manager_discover (CcRealmManager *self, + const gchar *input, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +GList * cc_realm_manager_discover_finish (CcRealmManager *self, + GAsyncResult *result, + GError **error); + +GList * cc_realm_manager_get_realms (CcRealmManager *self); + +void cc_realm_login (CcRealmObject *realm, + const gchar *login, + const gchar *password, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +GBytes * cc_realm_login_finish (GAsyncResult *result, + GError **error); + +gboolean cc_realm_join_as_user (CcRealmObject *realm, + const gchar *login, + const gchar *password, + GBytes *credentials, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) + G_GNUC_WARN_UNUSED_RESULT; + +gboolean cc_realm_join_as_admin (CcRealmObject *realm, + const gchar *login, + const gchar *password, + GBytes *credentials, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) + G_GNUC_WARN_UNUSED_RESULT; + +gboolean cc_realm_join_finish (CcRealmObject *realm, + GAsyncResult *result, + GError **error); + +gboolean cc_realm_is_configured (CcRealmObject *realm); + +gchar * cc_realm_calculate_login (CcRealmCommon *realm, + const gchar *username); + +G_END_DECLS diff --git a/panels/user-accounts/cc-user-panel.c b/panels/user-accounts/cc-user-panel.c new file mode 100644 index 0000000..4eed0da --- /dev/null +++ b/panels/user-accounts/cc-user-panel.c @@ -0,0 +1,1603 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright 2009-2010 Red Hat, Inc, + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see <http://www.gnu.org/licenses/>. + * + * Written by: Matthias Clasen <mclasen@redhat.com> + */ + +#include "config.h" + +#include "cc-user-panel.h" + +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/types.h> +#include <locale.h> +#include <errno.h> + +#include <glib.h> +#include <glib/gi18n.h> +#include <gtk/gtk.h> +#include <polkit/polkit.h> +#include <act/act.h> +#include <cairo-gobject.h> + +#define GNOME_DESKTOP_USE_UNSTABLE_API +#include <libgnome-desktop/gnome-languages.h> + +#ifdef HAVE_MALCONTENT +#include <libmalcontent/malcontent.h> +#endif + +#include "cc-add-user-dialog.h" +#include "cc-avatar-chooser.h" +#include "cc-language-chooser.h" +#include "cc-login-history-dialog.h" +#include "cc-password-dialog.h" +#include "cc-realm-manager.h" +#include "cc-user-accounts-resources.h" +#include "cc-fingerprint-manager.h" +#include "cc-fingerprint-dialog.h" +#include "user-utils.h" + +#include "cc-common-language.h" +#include "cc-permission-infobar.h" +#include "cc-util.h" + +#define USER_ACCOUNTS_PERMISSION "org.gnome.controlcenter.user-accounts.administration" + +struct _CcUserPanel { + CcPanel parent_instance; + + ActUserManager *um; + GSettings *login_screen_settings; + + GtkBox *account_settings_box; + GtkListBoxRow *account_type_row; + GtkSwitch *account_type_switch; + GtkWidget *add_user_button; + GtkListBoxRow *autologin_row; + GtkSwitch *autologin_switch; + GtkButton *back_button; + GtkLabel *fingerprint_state_label; + GtkListBoxRow *fingerprint_row; + GtkStack *full_name_stack; + GtkLabel *full_name_label; + GtkToggleButton *full_name_edit_button; + GtkEntry *full_name_entry; + GtkLabel *language_button_label; + GtkListBoxRow *language_row; + GtkLabel *last_login_button_label; + GtkListBoxRow *last_login_row; + GtkWidget *no_users_box; + GtkRevealer *notification_revealer; + AdwPreferencesGroup *other_users; + GtkListBox *other_users_listbox; + AdwPreferencesRow *other_users_row; + GtkLabel *password_button_label; +#ifdef HAVE_MALCONTENT + GtkLabel *parental_controls_button_label; + GtkListBoxRow *parental_controls_row; +#endif + GtkListBoxRow *password_row; + CcPermissionInfobar *permission_infobar; + GtkButton *remove_user_button; + GtkStack *stack; + AdwAvatar *user_avatar; + GtkMenuButton *user_avatar_edit_button; + GtkOverlay *users_overlay; + + ActUser *selected_user; + ActUser *pending_show_user; + GPermission *permission; + CcLanguageChooser *language_chooser; + GListStore *other_users_model; + + CcAvatarChooser *avatar_chooser; + + CcFingerprintManager *fingerprint_manager; +}; + +CC_PANEL_REGISTER (CcUserPanel, cc_user_panel) + +static void show_restart_notification (CcUserPanel *self, const gchar *locale); + +typedef struct { + CcUserPanel *self; + GCancellable *cancellable; + gchar *login; +} AsyncDeleteData; + +static void +async_delete_data_free (AsyncDeleteData *data) +{ + g_clear_object (&data->self); + g_clear_object (&data->cancellable); + g_clear_pointer (&data->login, g_free); + g_slice_free (AsyncDeleteData, data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (AsyncDeleteData, async_delete_data_free) + +static void +show_error_dialog (CcUserPanel *self, + const gchar *message, + GError *error) +{ + GtkWidget *dialog; + + dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_native (GTK_WIDGET (self))), + GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_USE_HEADER_BAR, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_CLOSE, + "%s", message); + + if (error != NULL) { + g_dbus_error_strip_remote_error (error); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + "%s", error->message); + } + + g_signal_connect (dialog, "response", G_CALLBACK (gtk_window_destroy), NULL); + gtk_window_present (GTK_WINDOW (dialog)); +} + +static void show_user (ActUser *user, CcUserPanel *self); + +static ActUser * +get_selected_user (CcUserPanel *self) +{ + return self->selected_user; +} + +static void +set_selected_user (CcUserPanel *self, + AdwActionRow *row) +{ + uid_t uid; + + uid = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (row), "uid")); + g_set_object (&self->selected_user, + act_user_manager_get_user_by_id (self->um, uid)); + show_user (self->selected_user, self); +} + +static void +show_current_user (CcUserPanel *self) +{ + ActUser *user; + + user = act_user_manager_get_user_by_id (self->um, getuid ()); + if (user != NULL) + show_user (user, self); +} + + +static void +on_back_button_clicked_cb (CcUserPanel *self) +{ + + if (act_user_get_uid (self->selected_user) == getuid ()) { + gtk_widget_activate_action (GTK_WIDGET (self), + "window.navigate", + "i", + ADW_NAVIGATION_DIRECTION_BACK); + } else { + show_current_user (self); + } +} + +static const gchar * +get_real_or_user_name (ActUser *user) +{ + const gchar *name; + + name = act_user_get_real_name (user); + if (name == NULL) + name = act_user_get_user_name (user); + + return name; +} + +static void +setup_avatar_for_user (AdwAvatar *avatar, ActUser *user) +{ + const gchar *avatar_file; + + adw_avatar_set_custom_image (avatar, NULL); + adw_avatar_set_text (avatar, get_real_or_user_name (user)); + + avatar_file = act_user_get_icon_file (user); + if (avatar_file) { + g_autoptr(GdkPixbuf) pixbuf = NULL; + + pixbuf = gdk_pixbuf_new_from_file_at_size (avatar_file, + adw_avatar_get_size (avatar), + adw_avatar_get_size (avatar), + NULL); + if (pixbuf) { + adw_avatar_set_custom_image (avatar, + GDK_PAINTABLE (gdk_texture_new_for_pixbuf (pixbuf))); + } + } +} + +static GtkWidget * +create_user_row (gpointer item, + gpointer user_data) +{ + ActUser *user = ACT_USER (item); + GtkWidget *row, *user_image; + + row = adw_action_row_new (); + g_object_set_data (G_OBJECT (row), "uid", GINT_TO_POINTER (act_user_get_uid (user))); + gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (row), TRUE); + adw_preferences_row_set_title (ADW_PREFERENCES_ROW (row), + get_real_or_user_name (user)); + user_image = adw_avatar_new (48, NULL, TRUE); + setup_avatar_for_user (ADW_AVATAR (user_image), user); + adw_action_row_add_prefix (ADW_ACTION_ROW (row), user_image); + + return row; +} + +static gint +sort_users (gconstpointer a, gconstpointer b, gpointer user_data) +{ + ActUser *ua, *ub; + + ua = ACT_USER (a); + ub = ACT_USER (b); + + /* Make sure the current user is shown first */ + if (act_user_get_uid (ua) == getuid ()) { + return -G_MAXINT32; + } + else if (act_user_get_uid (ub) == getuid ()) { + return G_MAXINT32; + } + else { + g_autofree gchar *name1 = NULL; + g_autofree gchar *name2 = NULL; + + name1 = g_utf8_collate_key (get_real_or_user_name (ua), -1); + name2 = g_utf8_collate_key (get_real_or_user_name (ub), -1); + + return strcmp (name1, name2); + } +} + +static void +user_changed (CcUserPanel *self, ActUser *user) +{ + GSList *user_list, *l; + gboolean show; + + g_list_store_remove_all (self->other_users_model); + user_list = act_user_manager_list_users (self->um); + for (l = user_list; l; l = l->next) { + ActUser *other_user = ACT_USER (l->data); + + if (act_user_is_system_account (other_user)) { + continue; + } + + if (act_user_get_uid (other_user) == getuid ()) { + continue; + } + + g_list_store_insert_sorted (self->other_users_model, + other_user, + sort_users, + self); + } + + if (self->selected_user == user) + show_user (user, self); + + show = g_list_model_get_n_items (G_LIST_MODEL (self->other_users_model)) > 0; + gtk_widget_set_visible (GTK_WIDGET (self->other_users_row), show); +} + +static void +on_add_user_dialog_response (CcUserPanel *self, + gint response, + CcAddUserDialog *dialog) +{ + ActUser *user; + + user = cc_add_user_dialog_get_user (dialog); + if (user != NULL) { + set_default_avatar (user); + show_user (user, self); + } + + gtk_window_destroy (GTK_WINDOW (dialog)); +} + +static void +add_user (CcUserPanel *self) +{ + CcAddUserDialog *dialog; + GtkWindow *toplevel; + + dialog = cc_add_user_dialog_new (self->permission); + toplevel = GTK_WINDOW (gtk_widget_get_native (GTK_WIDGET (self))); + gtk_window_set_transient_for (GTK_WINDOW (dialog), toplevel); + + gtk_window_present (GTK_WINDOW (dialog)); + g_signal_connect_object (dialog, "response", G_CALLBACK (on_add_user_dialog_response), + self, G_CONNECT_SWAPPED); +} + +static void +delete_user_done (ActUserManager *manager, + GAsyncResult *res, + CcUserPanel *self) +{ + g_autoptr(GError) error = NULL; + + if (!act_user_manager_delete_user_finish (manager, res, &error)) { + if (!g_error_matches (error, ACT_USER_MANAGER_ERROR, + ACT_USER_MANAGER_ERROR_PERMISSION_DENIED)) + show_error_dialog (self, _("Failed to delete user"), error); + } + + show_current_user (self); +} + +static void +delete_user_response (CcUserPanel *self, + gint response_id, + GtkWidget *dialog) +{ + ActUser *user; + gboolean remove_files; + + gtk_window_destroy (GTK_WINDOW (dialog)); + + if (response_id == GTK_RESPONSE_CANCEL) { + return; + } + else if (response_id == GTK_RESPONSE_NO) { + remove_files = TRUE; + } + else { + remove_files = FALSE; + } + + user = get_selected_user (self); + + /* remove autologin */ + if (act_user_get_automatic_login (user)) { + act_user_set_automatic_login (user, FALSE); + } + + act_user_manager_delete_user_async (self->um, + user, + remove_files, + NULL, + (GAsyncReadyCallback)delete_user_done, + self); +} + +static void +enterprise_user_revoked (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(AsyncDeleteData) data = user_data; + CcUserPanel *self = data->self; + CcRealmCommon *common = CC_REALM_COMMON (source); + g_autoptr(GError) error = NULL; + + if (g_cancellable_is_cancelled (data->cancellable)) { + return; + } + + cc_realm_common_call_change_login_policy_finish (common, result, &error); + if (error != NULL) { + show_error_dialog (self, _("Failed to revoke remotely managed user"), error); + } +} + +static CcRealmCommon * +find_matching_realm (CcRealmManager *realm_manager, const gchar *login) +{ + CcRealmCommon *common = NULL; + GList *realms; + + realms = cc_realm_manager_get_realms (realm_manager); + for (GList *l = realms; l != NULL; l = g_list_next (l)) { + const gchar * const *permitted_logins; + gint i; + + common = cc_realm_object_get_common (l->data); + if (common == NULL) + continue; + + permitted_logins = cc_realm_common_get_permitted_logins (common); + for (i = 0; permitted_logins[i] != NULL; i++) { + if (g_strcmp0 (permitted_logins[i], login) == 0) + break; + } + + if (permitted_logins[i] != NULL) + break; + + g_clear_object (&common); + } + g_list_free_full (realms, g_object_unref); + + return common; +} + +static void +realm_manager_found (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(AsyncDeleteData) data = user_data; + CcUserPanel *self = data->self; + g_autoptr(CcRealmCommon) common = NULL; + CcRealmManager *realm_manager; + const gchar *add[1]; + const gchar *remove[2]; + GVariant *options; + g_autoptr(GError) error = NULL; + + if (g_cancellable_is_cancelled (data->cancellable)) { + return; + } + + realm_manager = cc_realm_manager_new_finish (result, &error); + if (error != NULL) { + show_error_dialog (self, _("Failed to revoke remotely managed user"), error); + return; + } + + /* Find matching realm */ + common = find_matching_realm (realm_manager, data->login); + if (common == NULL) { + /* The realm was probably left */ + return; + } + + /* Remove the user from permitted logins */ + g_debug ("Denying future login for: %s", data->login); + + add[0] = NULL; + remove[0] = data->login; + remove[1] = NULL; + + options = g_variant_new_array (G_VARIANT_TYPE ("{sv}"), NULL, 0); + cc_realm_common_call_change_login_policy (common, "", + add, remove, options, + data->cancellable, + enterprise_user_revoked, + g_steal_pointer (&data)); +} + +static void +enterprise_user_uncached (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(AsyncDeleteData) data = user_data; + CcUserPanel *self = data->self; + ActUserManager *manager = ACT_USER_MANAGER (source); + g_autoptr(GError) error = NULL; + + if (g_cancellable_is_cancelled (data->cancellable)) { + return; + } + + act_user_manager_uncache_user_finish (manager, res, &error); + if (error == NULL) { + /* Find realm manager */ + cc_realm_manager_new (cc_panel_get_cancellable (CC_PANEL (self)), realm_manager_found, g_steal_pointer (&data)); + } + else { + show_error_dialog (self, _("Failed to revoke remotely managed user"), error); + } +} + +static void +delete_enterprise_user_response (CcUserPanel *self, + gint response_id, + GtkWidget *dialog) +{ + AsyncDeleteData *data; + ActUser *user; + + gtk_window_destroy (GTK_WINDOW (dialog)); + + if (response_id != GTK_RESPONSE_ACCEPT) { + return; + } + + user = get_selected_user (self); + + data = g_slice_new (AsyncDeleteData); + data->self = g_object_ref (self); + data->cancellable = g_object_ref (cc_panel_get_cancellable (CC_PANEL (self))); + data->login = g_strdup (act_user_get_user_name (user)); + + /* Uncache the user account from the accountsservice */ + g_debug ("Uncaching remote user: %s", data->login); + + act_user_manager_uncache_user_async (self->um, data->login, + data->cancellable, + enterprise_user_uncached, + data); +} + +static void +delete_user (CcUserPanel *self) +{ + ActUser *user; + GtkWidget *dialog; + + user = get_selected_user (self); + if (user == NULL) { + return; + } + else if (act_user_get_uid (user) == getuid ()) { + dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_native (GTK_WIDGET (self))), + 0, + GTK_MESSAGE_INFO, + GTK_BUTTONS_CLOSE, + _("You cannot delete your own account.")); + g_signal_connect (dialog, "response", + G_CALLBACK (gtk_window_destroy), NULL); + } + else if (act_user_is_logged_in_anywhere (user)) { + dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_native (GTK_WIDGET (self))), + 0, + GTK_MESSAGE_INFO, + GTK_BUTTONS_CLOSE, + _("%s is still logged in"), + get_real_or_user_name (user)); + + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + _("Deleting a user while they are logged in can leave the system in an inconsistent state.")); + g_signal_connect (dialog, "response", + G_CALLBACK (gtk_window_destroy), NULL); + } + else if (act_user_is_local_account (user)) { + dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_native (GTK_WIDGET (self))), + 0, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_NONE, + _("Do you want to keep %s’s files?"), + get_real_or_user_name (user)); + + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + _("It is possible to keep the home directory, mail spool and temporary files around when deleting a user account.")); + + gtk_dialog_add_buttons (GTK_DIALOG (dialog), + _("_Delete Files"), GTK_RESPONSE_NO, + _("_Keep Files"), GTK_RESPONSE_YES, + _("_Cancel"), GTK_RESPONSE_CANCEL, + NULL); + + gtk_window_set_icon_name (GTK_WINDOW (dialog), "system-users"); + + g_signal_connect_object (dialog, "response", + G_CALLBACK (delete_user_response), self, G_CONNECT_SWAPPED); + } + else { + dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_native (GTK_WIDGET (self))), + 0, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_NONE, + _("Are you sure you want to revoke remotely managed %s’s account?"), + get_real_or_user_name (user)); + + gtk_dialog_add_buttons (GTK_DIALOG (dialog), + _("_Delete"), GTK_RESPONSE_ACCEPT, + _("_Cancel"), GTK_RESPONSE_CANCEL, + NULL); + + gtk_window_set_icon_name (GTK_WINDOW (dialog), "system-users"); + + g_signal_connect_object (dialog, "response", + G_CALLBACK (delete_enterprise_user_response), self, G_CONNECT_SWAPPED); + } + + g_signal_connect (dialog, "close", + G_CALLBACK (gtk_window_destroy), NULL); + + gtk_window_set_modal (GTK_WINDOW (dialog), TRUE); + + gtk_window_present (GTK_WINDOW (dialog)); +} + +static const gchar * +get_invisible_text (void) +{ + GtkWidget *entry; + gunichar invisible_char; + static gchar invisible_text[40]; + gchar *p; + gint i; + + entry = gtk_entry_new (); + invisible_char = gtk_entry_get_invisible_char (GTK_ENTRY (entry)); + if (invisible_char == 0) + invisible_char = 0x2022; + + g_object_ref_sink (entry); + g_object_unref (entry); + + /* five bullets */ + p = invisible_text; + for (i = 0; i < 5; i++) + p += g_unichar_to_utf8 (invisible_char, p); + *p = 0; + + return invisible_text; +} + +static const gchar * +get_password_mode_text (ActUser *user) +{ + const gchar *text; + + if (act_user_get_locked (user)) { + text = C_("Password mode", "Account disabled"); + } + else { + switch (act_user_get_password_mode (user)) { + case ACT_USER_PASSWORD_MODE_REGULAR: + text = get_invisible_text (); + break; + case ACT_USER_PASSWORD_MODE_SET_AT_LOGIN: + text = C_("Password mode", "To be set at next login"); + break; + case ACT_USER_PASSWORD_MODE_NONE: + text = C_("Password mode", "None"); + break; + default: + g_assert_not_reached (); + } + } + + return text; +} + +static void +autologin_changed (CcUserPanel *self) +{ + gboolean active; + ActUser *user; + + active = gtk_switch_get_active (self->autologin_switch); + user = get_selected_user (self); + + if (active != act_user_get_automatic_login (user)) { + act_user_set_automatic_login (user, active); + if (act_user_get_automatic_login (user)) { + GSList *list; + GSList *l; + list = act_user_manager_list_users (self->um); + for (l = list; l != NULL; l = l->next) { + ActUser *u = l->data; + if (act_user_get_uid (u) != act_user_get_uid (user)) { + act_user_set_automatic_login (user, FALSE); + } + } + g_slist_free (list); + } + } +} + +static gchar * +get_login_time_text (ActUser *user) +{ + gint64 time; + + time = act_user_get_login_time (user); + if (act_user_is_logged_in (user)) { + return g_strdup (_("Logged in")); + } + else if (time > 0) { + g_autoptr(GDateTime) date_time = NULL; + g_autofree gchar *date_str = NULL; + g_autofree gchar *time_str = NULL; + + date_time = g_date_time_new_from_unix_local (time); + date_str = cc_util_get_smart_date (date_time); + + /* Translators: This is a time format string in the style of "22:58". + It indicates a login time which follows a date. */ + time_str = g_date_time_format (date_time, C_("login date-time", "%k:%M")); + + /* Translators: This indicates a login date-time. + The first %s is a date, and the second %s a time. */ + return g_strdup_printf(C_("login date-time", "%s, %s"), date_str, time_str); + } + else { + return g_strdup ("—"); + } +} + +static gboolean +get_autologin_possible (ActUser *user) +{ + gboolean locked; + gboolean set_password_at_login; + + locked = act_user_get_locked (user); + set_password_at_login = (act_user_get_password_mode (user) == ACT_USER_PASSWORD_MODE_SET_AT_LOGIN); + + return !(locked || set_password_at_login); +} + +static void on_permission_changed (CcUserPanel *self); +static void full_name_edit_button_toggled (CcUserPanel *self); + +#ifdef HAVE_MALCONTENT +static gboolean +is_parental_controls_enabled_for_user (ActUser *user) +{ + g_autoptr(MctManager) manager = NULL; + g_autoptr(MctAppFilter) app_filter = NULL; + g_autoptr(GDBusConnection) system_bus = NULL; + g_autoptr(GError) error = NULL; + + /* FIXME: should become asynchronous */ + system_bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &error); + if (system_bus == NULL) { + g_warning ("Error getting system bus while trying to show user details: %s", error->message); + return FALSE; + } + + manager = mct_manager_new (system_bus); + app_filter = mct_manager_get_app_filter (manager, + act_user_get_uid (user), + MCT_GET_APP_FILTER_FLAGS_NONE, + NULL, + &error); + if (error) { + if (!g_error_matches (error, MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_DISABLED)) + g_warning ("Error retrieving app filter for user %s: %s", + act_user_get_user_name (user), + error->message); + + return FALSE; + } + + return mct_app_filter_is_enabled (app_filter); +} +#endif + +static void +update_fingerprint_row_state (CcUserPanel *self, GParamSpec *spec, CcFingerprintManager *fingerprint_manager) +{ + CcFingerprintState state = cc_fingerprint_manager_get_state (fingerprint_manager); + + if (state != CC_FINGERPRINT_STATE_UPDATING) { + gtk_widget_set_visible (GTK_WIDGET (self->fingerprint_row), + state != CC_FINGERPRINT_STATE_NONE); + } + + gtk_widget_set_sensitive (GTK_WIDGET (self->fingerprint_row), + state != CC_FINGERPRINT_STATE_UPDATING); + + if (state == CC_FINGERPRINT_STATE_ENABLED) + gtk_label_set_text (self->fingerprint_state_label, _("Enabled")); + else if (state == CC_FINGERPRINT_STATE_DISABLED) + gtk_label_set_text (self->fingerprint_state_label, _("Disabled")); +} + +static void +show_or_hide_back_button (CcUserPanel *self) +{ + gboolean show; + gboolean folded; + + g_object_get(self, "folded", &folded, NULL); + + show = folded || act_user_get_uid (self->selected_user) != getuid(); + + gtk_widget_set_visible (GTK_WIDGET (self->back_button), show); +} + +static void +on_pending_show_user_is_loaded (ActUser *user, + GParamSpec *param, + CcUserPanel *self) +{ + if (!act_user_is_loaded (user)) { + return; + } + + show_user (user, self); +} + +static void +show_user (ActUser *user, CcUserPanel *self) +{ + g_autofree gchar *lang = NULL; + g_autofree gchar *name = NULL; + gboolean show, enable; + ActUser *current; +#ifdef HAVE_MALCONTENT + g_autofree gchar *malcontent_control_path = NULL; +#endif + + if (self->pending_show_user != NULL) { + g_signal_handlers_disconnect_by_func (G_OBJECT (self->pending_show_user), + on_pending_show_user_is_loaded, + self); + g_clear_object (&self->pending_show_user); + } + + if (!act_user_is_loaded (user)) { + g_set_object (&self->pending_show_user, user); + g_signal_connect_object (G_OBJECT (self->pending_show_user), + "notify::is-loaded", + G_CALLBACK (on_pending_show_user_is_loaded), + self, + 0); + return; + } + + g_set_object (&self->selected_user, user); + + setup_avatar_for_user (self->user_avatar, user); + cc_avatar_chooser_set_user (self->avatar_chooser, user); + + gtk_label_set_label (self->full_name_label, get_real_or_user_name (user)); + gtk_editable_set_text (GTK_EDITABLE (self->full_name_entry), gtk_label_get_label (self->full_name_label)); + gtk_widget_set_tooltip_text (GTK_WIDGET (self->full_name_label), get_real_or_user_name (user)); + + g_signal_handlers_block_by_func (self->full_name_edit_button, full_name_edit_button_toggled, self); + gtk_stack_set_visible_child (self->full_name_stack, GTK_WIDGET (self->full_name_label)); + gtk_toggle_button_set_active (self->full_name_edit_button, FALSE); + g_signal_handlers_unblock_by_func (self->full_name_edit_button, full_name_edit_button_toggled, self); + + enable = (act_user_get_account_type (user) == ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR); + gtk_switch_set_active (self->account_type_switch, enable); + + gtk_label_set_label (self->password_button_label, get_password_mode_text (user)); + enable = act_user_is_local_account (user); + gtk_widget_set_sensitive (GTK_WIDGET (self->password_button_label), enable); + + g_signal_handlers_block_by_func (self->autologin_switch, autologin_changed, self); + gtk_switch_set_active (self->autologin_switch, act_user_get_automatic_login (user)); + g_signal_handlers_unblock_by_func (self->autologin_switch, autologin_changed, self); + gtk_widget_set_sensitive (GTK_WIDGET (self->autologin_switch), get_autologin_possible (user)); + + lang = g_strdup (act_user_get_language (user)); + if (lang && *lang != '\0') { + name = gnome_get_language_from_locale (lang, NULL); + } else { + name = g_strdup ("—"); + } + gtk_label_set_label (self->language_button_label, name); + + /* Fingerprint: show when self, local, enabled, and possible */ + show = (act_user_get_uid (user) == getuid() && + act_user_is_local_account (user) && + (self->login_screen_settings && + g_settings_get_boolean (self->login_screen_settings, + "enable-fingerprint-authentication"))); + + if (show) { + if (!self->fingerprint_manager) { + self->fingerprint_manager = cc_fingerprint_manager_new (user); + g_signal_connect_object (self->fingerprint_manager, + "notify::state", + G_CALLBACK (update_fingerprint_row_state), + self, G_CONNECT_SWAPPED); + } + + update_fingerprint_row_state (self, NULL, self->fingerprint_manager); + } else { + gtk_widget_set_visible (GTK_WIDGET (self->fingerprint_row), FALSE); + } + + /* Autologin: show when local account */ + show = act_user_is_local_account (user); + gtk_widget_set_visible (GTK_WIDGET (self->autologin_row), show); + +#ifdef HAVE_MALCONTENT + /* Parental Controls: Unavailable if user is admin or if + * malcontent-control is not available (which can happen if + * libmalcontent is installed but malcontent-control is not). */ + malcontent_control_path = g_find_program_in_path ("malcontent-control"); + + if (act_user_get_account_type (user) == ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR || + malcontent_control_path == NULL) { + gtk_widget_hide (GTK_WIDGET (self->parental_controls_row)); + } else { + GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self->parental_controls_button_label)); + + if (is_parental_controls_enabled_for_user (user)) + /* TRANSLATORS: Status of Parental Controls setup */ + gtk_label_set_text (self->parental_controls_button_label, _("Enabled")); + else + /* TRANSLATORS: Status of Parental Controls setup */ + gtk_label_set_text (self->parental_controls_button_label, _("Disabled")); + + gtk_style_context_remove_class (context, "dim-label"); + gtk_widget_show (GTK_WIDGET (self->parental_controls_row)); + } +#endif + + /* Current user */ + show = act_user_get_uid (user) == getuid(); + gtk_widget_set_visible (GTK_WIDGET (self->account_settings_box), !show); + gtk_widget_set_visible (GTK_WIDGET (self->remove_user_button), !show); + gtk_widget_set_visible (GTK_WIDGET (self->back_button), !show); + show_or_hide_back_button(self); + gtk_widget_set_visible (GTK_WIDGET (self->other_users), show); + + /* Last login: show when administrator or current user */ + current = act_user_manager_get_user_by_id (self->um, getuid ()); + show = act_user_get_uid (user) == getuid () || + act_user_get_account_type (current) == ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR; + if (show) { + g_autofree gchar *text = NULL; + + text = get_login_time_text (user); + gtk_label_set_label (self->last_login_button_label, text); + } + gtk_widget_set_visible (GTK_WIDGET (self->last_login_row), show); + + enable = act_user_get_login_history (user) != NULL; + gtk_widget_set_sensitive (GTK_WIDGET (self->last_login_row), enable); + + if (self->permission != NULL) + on_permission_changed (self); +} + +static void +full_name_entry_activate (CcUserPanel *self) +{ + const gchar *text; + ActUser *user; + + user = get_selected_user (self); + text = gtk_editable_get_text (GTK_EDITABLE (self->full_name_entry)); + if (g_strcmp0 (text, act_user_get_real_name (user)) != 0 && + is_valid_name (text)) { + act_user_set_real_name (user, text); + } + + gtk_toggle_button_set_active (self->full_name_edit_button, FALSE); +} + +static void +full_name_edit_button_toggled (CcUserPanel *self) +{ + if (gtk_stack_get_visible_child (self->full_name_stack) == GTK_WIDGET (self->full_name_label)) { + gtk_stack_set_visible_child (self->full_name_stack, GTK_WIDGET (self->full_name_entry)); + + gtk_widget_grab_focus (GTK_WIDGET (self->full_name_entry)); + } else { + gtk_stack_set_visible_child (self->full_name_stack, GTK_WIDGET (self->full_name_label)); + + full_name_entry_activate (self); + } +} + +static gboolean +full_name_entry_key_press_cb (GtkEventController *controller, + guint keyval, + guint keycode, + GdkModifierType state, + CcUserPanel *self) +{ + if (keyval == GDK_KEY_Escape) { + gtk_editable_set_text (GTK_EDITABLE (self->full_name_entry), act_user_get_real_name (self->selected_user)); + + full_name_entry_activate (self); + + return TRUE; + } + + return FALSE; +} + +static void +account_type_changed (CcUserPanel *self) +{ + ActUser *user; + gboolean self_selected; + gboolean is_admin; + ActUserAccountType account_type; + + user = get_selected_user (self); + self_selected = act_user_get_uid (user) == geteuid (); + is_admin = gtk_switch_get_active (self->account_type_switch); + + account_type = is_admin ? ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR : ACT_USER_ACCOUNT_TYPE_STANDARD; + if (account_type != act_user_get_account_type (user)) { + act_user_set_account_type (user, account_type); + + if (self_selected) + show_restart_notification (self, NULL); + } +} + +static void +dismiss_notification (CcUserPanel *self) +{ + gtk_revealer_set_reveal_child (self->notification_revealer, FALSE); +} + +static void +restart_now (CcUserPanel *self) +{ + g_autoptr(GDBusConnection) bus = NULL; + + gtk_revealer_set_reveal_child (self->notification_revealer, FALSE); + + bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); + g_dbus_connection_call (bus, + "org.gnome.SessionManager", + "/org/gnome/SessionManager", + "org.gnome.SessionManager", + "Logout", + g_variant_new ("(u)", 0), + NULL, 0, G_MAXINT, + NULL, NULL, NULL); +} + +static void +show_restart_notification (CcUserPanel *self, const gchar *locale) +{ + locale_t current_locale; + locale_t new_locale; + + if (locale) { + new_locale = newlocale (LC_MESSAGES_MASK, locale, (locale_t) 0); + if (new_locale == (locale_t) 0) + g_warning ("Failed to create locale %s: %s", locale, g_strerror (errno)); + else + current_locale = uselocale (new_locale); + } + + gtk_revealer_set_reveal_child (self->notification_revealer, TRUE); + + if (locale && new_locale != (locale_t) 0) { + uselocale (current_locale); + freelocale (new_locale); + } +} + +static void +language_response (CcUserPanel *self, + gint response_id, + GtkDialog *dialog) +{ + ActUser *user; + const gchar *lang, *account_language; + + if (response_id != GTK_RESPONSE_OK) { + gtk_widget_hide (GTK_WIDGET (dialog)); + return; + } + + user = get_selected_user (self); + account_language = act_user_get_language (user); + + lang = cc_language_chooser_get_language (CC_LANGUAGE_CHOOSER (dialog)); + if (lang) { + g_autofree gchar *name = NULL; + if (g_strcmp0 (lang, account_language) != 0) { + act_user_set_language (user, lang); + } + + name = gnome_get_language_from_locale (lang, NULL); + gtk_label_set_label (self->language_button_label, name); + } + + gtk_widget_hide (GTK_WIDGET (dialog)); +} + +static void +change_language (CcUserPanel *self) +{ + const gchar *current_language; + ActUser *user; + + user = get_selected_user (self); + current_language = act_user_get_language (user); + + if (self->language_chooser) { + cc_language_chooser_clear_filter (self->language_chooser); + cc_language_chooser_set_language (self->language_chooser, NULL); + } + else { + self->language_chooser = cc_language_chooser_new (); + gtk_window_set_transient_for (GTK_WINDOW (self->language_chooser), + GTK_WINDOW (gtk_widget_get_native (GTK_WIDGET (self)))); + + g_signal_connect_object (self->language_chooser, "response", + G_CALLBACK (language_response), self, G_CONNECT_SWAPPED); + } + + if (current_language && *current_language != '\0') + cc_language_chooser_set_language (self->language_chooser, current_language); + gtk_window_present (GTK_WINDOW (self->language_chooser)); +} + +static void +change_password (CcUserPanel *self) +{ + ActUser *user; + CcPasswordDialog *dialog; + GtkWindow *parent; + + user = get_selected_user (self); + dialog = cc_password_dialog_new (user); + + parent = (GtkWindow *) gtk_widget_get_native (GTK_WIDGET (self)); + gtk_window_set_transient_for (GTK_WINDOW (dialog), parent); + + gtk_window_present (GTK_WINDOW (dialog)); +} + +static void +change_fingerprint (CcUserPanel *self) +{ + ActUser *user; + GtkWindow *parent; + CcFingerprintDialog *dialog; + + user = get_selected_user (self); + parent = (GtkWindow *) gtk_widget_get_native (GTK_WIDGET (self)); + + g_assert (g_strcmp0 (g_get_user_name (), act_user_get_user_name (user)) == 0); + + dialog = cc_fingerprint_dialog_new (self->fingerprint_manager); + gtk_window_set_transient_for (GTK_WINDOW (dialog), parent); + gtk_window_present (GTK_WINDOW (dialog)); +} + +static void +show_history (CcUserPanel *self) +{ + CcLoginHistoryDialog *dialog; + ActUser *user; + GtkWindow *parent; + + user = get_selected_user (self); + dialog = cc_login_history_dialog_new (user); + + parent = (GtkWindow *) gtk_widget_get_native (GTK_WIDGET (self)); + gtk_window_set_transient_for (GTK_WINDOW (dialog), parent); + + gtk_window_present (GTK_WINDOW (dialog)); +} + +#ifdef HAVE_MALCONTENT +static void +spawn_malcontent_control (CcUserPanel *self) +{ + ActUser *user; + + user = get_selected_user (self); + + /* no-op if the user is administrator */ + if (act_user_get_account_type (user) != ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR) { + const gchar *argv[] = { + "malcontent-control", +#ifdef HAVE_MALCONTENT_0_10 + "--user", + act_user_get_user_name (user), +#endif /* HAVE_MALCONTENT_0_10 */ + NULL + }; + g_autoptr(GError) error = NULL; + if (!g_spawn_async (NULL, (char **)argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, &error)) + g_debug ("Couldn't launch malcontent-control: %s", error->message); + } else { + g_debug ("Not launching malcontent because selected user is an admin"); + } +} +#endif + +static void +users_loaded (CcUserPanel *self) +{ + GtkWidget *dialog; + + if (act_user_manager_no_service (self->um)) { + GtkWidget *toplevel; + + toplevel = (GtkWidget *)gtk_widget_get_native (GTK_WIDGET (self)); + dialog = gtk_message_dialog_new (GTK_WINDOW (toplevel ), + GTK_DIALOG_MODAL, + GTK_MESSAGE_OTHER, + GTK_BUTTONS_CLOSE, + _("Failed to contact the accounts service")); + gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), + _("Please make sure that the AccountService is installed and enabled.")); + g_signal_connect (dialog, "response", + G_CALLBACK (gtk_window_destroy), + NULL); + gtk_widget_show (dialog); + + gtk_stack_set_visible_child (self->stack, self->no_users_box); + } else { + gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->users_overlay)); + show_current_user (self); + } + + g_signal_connect_object (self->um, "user-changed", G_CALLBACK (user_changed), self, G_CONNECT_SWAPPED); + g_signal_connect_object (self->um, "user-is-logged-in-changed", G_CALLBACK (user_changed), self, G_CONNECT_SWAPPED); + g_signal_connect_object (self->um, "user-added", G_CALLBACK (user_changed), self, G_CONNECT_SWAPPED); + g_signal_connect_object (self->um, "user-removed", G_CALLBACK (user_changed), self, G_CONNECT_SWAPPED); +} + +static void +add_unlock_tooltip (GtkWidget *widget) +{ + gtk_widget_set_tooltip_text (widget, + _("This panel must be unlocked to change this setting")); +} + +static void +remove_unlock_tooltip (GtkWidget *widget) +{ + gtk_widget_set_tooltip_text (widget, NULL); +} + +static guint +get_num_active_admin (ActUserManager *um) +{ + GSList *list; + GSList *l; + guint num_admin = 0; + + list = act_user_manager_list_users (um); + for (l = list; l != NULL; l = l->next) { + ActUser *u = l->data; + if (act_user_get_account_type (u) == ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR && !act_user_get_locked (u)) { + num_admin++; + } + } + g_slist_free (list); + + return num_admin; +} + +static gboolean +would_demote_only_admin (ActUser *user) +{ + ActUserManager *um = act_user_manager_get_default (); + + /* Prevent the user from demoting the only admin account. + * Returns TRUE when user is an administrator and there is only + * one enabled administrator. */ + + if (act_user_get_account_type (user) == ACT_USER_ACCOUNT_TYPE_STANDARD || + act_user_get_locked (user)) + return FALSE; + + if (get_num_active_admin (um) > 1) + return FALSE; + + return TRUE; +} + +static void +on_permission_changed (CcUserPanel *self) +{ + gboolean is_authorized; + gboolean self_selected; + ActUser *user; + + is_authorized = g_permission_get_allowed (G_PERMISSION (self->permission)); + + gtk_widget_set_sensitive (self->add_user_button, is_authorized); + + user = get_selected_user (self); + if (!user) { + return; + } + + self_selected = act_user_get_uid (user) == geteuid (); + gtk_widget_set_sensitive (GTK_WIDGET (self->remove_user_button), is_authorized && !self_selected + && !would_demote_only_admin (user)); + if (is_authorized) { + gtk_widget_set_tooltip_text (GTK_WIDGET (self->remove_user_button), _("Delete the selected user account")); + } + else { + gtk_widget_set_tooltip_text (GTK_WIDGET (self->remove_user_button), + _("To delete the selected user account,\nclick the * icon first")); + } + + if (!act_user_is_local_account (user)) { + gtk_widget_set_visible (GTK_WIDGET (self->account_type_row), FALSE); + gtk_widget_set_visible (GTK_WIDGET (self->autologin_row), FALSE); + } else if (is_authorized && act_user_is_local_account (user)) { + if (would_demote_only_admin (user)) { + gtk_widget_set_visible (GTK_WIDGET (self->account_type_row), FALSE); + } else { + gtk_widget_set_visible (GTK_WIDGET (self->account_type_row), TRUE); + } + + if (get_autologin_possible (user)) { + gtk_widget_set_visible (GTK_WIDGET (self->autologin_row), TRUE); + gtk_widget_set_sensitive (GTK_WIDGET (self->autologin_row), TRUE); + } + } + else { + gtk_widget_set_visible (GTK_WIDGET (self->account_type_row), FALSE); + if (would_demote_only_admin (user)) { + gtk_widget_set_visible (GTK_WIDGET (self->account_type_row), FALSE); + } else { + gtk_widget_set_visible (GTK_WIDGET (self->account_type_row), TRUE); + } + gtk_widget_set_sensitive (GTK_WIDGET (self->autologin_row), FALSE); + add_unlock_tooltip (GTK_WIDGET (self->autologin_row)); + } + + /* The full name entry: insensitive if remote or not authorized and not self */ + if (!act_user_is_local_account (user)) { + gtk_widget_set_sensitive (GTK_WIDGET (self->full_name_edit_button), FALSE); + remove_unlock_tooltip (GTK_WIDGET (self->full_name_stack)); + + } else if (is_authorized || self_selected) { + gtk_widget_set_sensitive (GTK_WIDGET (self->full_name_edit_button), TRUE); + remove_unlock_tooltip (GTK_WIDGET (self->full_name_stack)); + + } else { + gtk_widget_set_sensitive (GTK_WIDGET (self->full_name_edit_button), FALSE); + add_unlock_tooltip (GTK_WIDGET (self->full_name_stack)); + } + + if (is_authorized || self_selected) { + CcFingerprintState fingerprint_state = CC_FINGERPRINT_STATE_NONE; + + if (self->fingerprint_manager) + fingerprint_state = cc_fingerprint_manager_get_state (self->fingerprint_manager); + + gtk_widget_set_sensitive (GTK_WIDGET (self->user_avatar_edit_button), TRUE); + remove_unlock_tooltip (GTK_WIDGET (self->user_avatar_edit_button)); + + gtk_widget_set_sensitive (GTK_WIDGET (self->language_row), TRUE); + remove_unlock_tooltip (GTK_WIDGET (self->language_row)); + + gtk_widget_set_sensitive (GTK_WIDGET (self->password_row), TRUE); + remove_unlock_tooltip (GTK_WIDGET (self->password_row)); + + gtk_widget_set_sensitive (GTK_WIDGET (self->fingerprint_row), + fingerprint_state != CC_FINGERPRINT_STATE_UPDATING); + remove_unlock_tooltip (GTK_WIDGET (self->fingerprint_row)); + + gtk_widget_set_sensitive (GTK_WIDGET (self->last_login_row), TRUE); + remove_unlock_tooltip (GTK_WIDGET (self->last_login_row)); + } + else { + gtk_widget_set_sensitive (GTK_WIDGET (self->user_avatar_edit_button), FALSE); + add_unlock_tooltip (GTK_WIDGET (self->user_avatar_edit_button)); + + gtk_widget_set_sensitive (GTK_WIDGET (self->language_row), FALSE); + add_unlock_tooltip (GTK_WIDGET (self->language_row)); + + gtk_widget_set_sensitive (GTK_WIDGET (self->password_row), FALSE); + add_unlock_tooltip (GTK_WIDGET (self->password_row)); + + gtk_widget_set_sensitive (GTK_WIDGET (self->fingerprint_row), FALSE); + add_unlock_tooltip (GTK_WIDGET (self->fingerprint_row)); + + gtk_widget_set_sensitive (GTK_WIDGET (self->last_login_row), FALSE); + add_unlock_tooltip (GTK_WIDGET (self->last_login_row)); + } +} + +static void +setup_main_window (CcUserPanel *self) +{ + g_autoptr(GError) error = NULL; + gboolean loaded; + + self->other_users_model = g_list_store_new (ACT_TYPE_USER); + gtk_list_box_bind_model (self->other_users_listbox, + G_LIST_MODEL (self->other_users_model), + (GtkListBoxCreateWidgetFunc)create_user_row, + self, + NULL); + + add_unlock_tooltip (GTK_WIDGET (self->user_avatar)); + + self->permission = (GPermission *)polkit_permission_new_sync (USER_ACCOUNTS_PERMISSION, NULL, NULL, &error); + if (self->permission != NULL) { + g_signal_connect_object (self->permission, "notify", + G_CALLBACK (on_permission_changed), self, G_CONNECT_SWAPPED); + on_permission_changed (self); + } else { + g_warning ("Cannot create '%s' permission: %s", USER_ACCOUNTS_PERMISSION, error->message); + } + +#ifdef HAVE_MALCONTENT + g_signal_connect_object (self->parental_controls_row, "activated", G_CALLBACK (spawn_malcontent_control), self, G_CONNECT_SWAPPED); +#endif + + gtk_widget_set_tooltip_text (GTK_WIDGET (self->remove_user_button), + _("To delete the selected user account,\nclick the * icon first")); + + self->avatar_chooser = cc_avatar_chooser_new (GTK_WIDGET (self)); + gtk_menu_button_set_popover (self->user_avatar_edit_button, + GTK_WIDGET (self->avatar_chooser)); + + g_object_get (self->um, "is-loaded", &loaded, NULL); + if (loaded) { + users_loaded (self); + user_changed (self, NULL); + } else { + g_signal_connect_object (self->um, "notify::is-loaded", G_CALLBACK (users_loaded), self, G_CONNECT_SWAPPED); + } +} + +static GSettings * +settings_or_null (const gchar *schema) +{ + GSettingsSchemaSource *source = NULL; + gchar **non_relocatable = NULL; + gchar **relocatable = NULL; + GSettings *settings = NULL; + + source = g_settings_schema_source_get_default (); + if (!source) + return NULL; + + g_settings_schema_source_list_schemas (source, TRUE, &non_relocatable, &relocatable); + + if (g_strv_contains ((const gchar * const *)non_relocatable, schema) || + g_strv_contains ((const gchar * const *)relocatable, schema)) + settings = g_settings_new (schema); + + g_strfreev (non_relocatable); + g_strfreev (relocatable); + return settings; +} + +static void +cc_user_panel_constructed (GObject *object) +{ + CcUserPanel *self = CC_USER_PANEL (object); + + G_OBJECT_CLASS (cc_user_panel_parent_class)->constructed (object); + + cc_permission_infobar_set_permission (self->permission_infobar, self->permission); + cc_permission_infobar_set_title (self->permission_infobar, _("Unlock to Add Users and Change Settings")); +} + +static void +cc_user_panel_init (CcUserPanel *self) +{ + volatile GType type G_GNUC_UNUSED; + g_autoptr(GtkCssProvider) provider = NULL; + + g_resources_register (cc_user_accounts_get_resource ()); + + /* register types that the builder might need */ + type = cc_permission_infobar_get_type (); + + gtk_widget_init_template (GTK_WIDGET (self)); + + self->um = act_user_manager_get_default (); + + provider = gtk_css_provider_new (); + gtk_css_provider_load_from_resource (provider, "/org/gnome/control-center/user-accounts/user-accounts-dialog.css"); + gtk_style_context_add_provider_for_display (gdk_display_get_default (), + GTK_STYLE_PROVIDER (provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + self->login_screen_settings = settings_or_null ("org.gnome.login-screen"); + + setup_main_window (self); + + g_signal_connect_swapped (self, + "notify::folded", + G_CALLBACK (show_or_hide_back_button), + self); +} + +static void +cc_user_panel_dispose (GObject *object) +{ + CcUserPanel *self = CC_USER_PANEL (object); + + g_clear_object (&self->selected_user); + g_clear_object (&self->pending_show_user); + g_clear_object (&self->login_screen_settings); + g_clear_pointer ((GtkWindow **)&self->language_chooser, gtk_window_destroy); + g_clear_object (&self->permission); + + G_OBJECT_CLASS (cc_user_panel_parent_class)->dispose (object); +} + +static const char * +cc_user_panel_get_help_uri (CcPanel *panel) +{ + return "help:gnome-help/user-accounts"; +} + +static void +cc_user_panel_class_init (CcUserPanelClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + CcPanelClass *panel_class = CC_PANEL_CLASS (klass); + + object_class->dispose = cc_user_panel_dispose; + object_class->constructed = cc_user_panel_constructed; + + panel_class->get_help_uri = cc_user_panel_get_help_uri; + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/user-accounts/cc-user-panel.ui"); + + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, account_settings_box); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, account_type_row); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, account_type_switch); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, add_user_button); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, autologin_row); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, autologin_switch); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, back_button); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, fingerprint_state_label); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, fingerprint_row); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, full_name_stack); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, full_name_label); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, full_name_edit_button); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, full_name_entry); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, language_button_label); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, language_row); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, last_login_button_label); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, last_login_row); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, no_users_box); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, notification_revealer); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, other_users); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, other_users_row); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, other_users_listbox); +#ifdef HAVE_MALCONTENT + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, parental_controls_button_label); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, parental_controls_row); +#endif + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, password_button_label); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, password_row); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, permission_infobar); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, remove_user_button); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, stack); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, user_avatar); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, user_avatar_edit_button); + gtk_widget_class_bind_template_child (widget_class, CcUserPanel, users_overlay); + + gtk_widget_class_bind_template_callback (widget_class, account_type_changed); + gtk_widget_class_bind_template_callback (widget_class, add_user); + gtk_widget_class_bind_template_callback (widget_class, autologin_changed); + gtk_widget_class_bind_template_callback (widget_class, change_fingerprint); + gtk_widget_class_bind_template_callback (widget_class, change_language); + gtk_widget_class_bind_template_callback (widget_class, full_name_edit_button_toggled); + gtk_widget_class_bind_template_callback (widget_class, full_name_entry_activate); + gtk_widget_class_bind_template_callback (widget_class, full_name_entry_key_press_cb); + gtk_widget_class_bind_template_callback (widget_class, change_password); + gtk_widget_class_bind_template_callback (widget_class, delete_user); + gtk_widget_class_bind_template_callback (widget_class, dismiss_notification); + gtk_widget_class_bind_template_callback (widget_class, restart_now); + gtk_widget_class_bind_template_callback (widget_class, set_selected_user); + gtk_widget_class_bind_template_callback (widget_class, on_back_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, show_history); +} diff --git a/panels/user-accounts/cc-user-panel.h b/panels/user-accounts/cc-user-panel.h new file mode 100644 index 0000000..6f0aa5a --- /dev/null +++ b/panels/user-accounts/cc-user-panel.h @@ -0,0 +1,29 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright 2009-2010 Red Hat, Inc, + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see <http://www.gnu.org/licenses/>. + * + * Written by: Matthias Clasen <mclasen@redhat.com> + */ + +#pragma once + +#include <shell/cc-panel.h> + +G_BEGIN_DECLS + +G_DECLARE_FINAL_TYPE (CcUserPanel, cc_user_panel, CC, USER_PANEL, CcPanel) + +G_END_DECLS diff --git a/panels/user-accounts/cc-user-panel.ui b/panels/user-accounts/cc-user-panel.ui new file mode 100644 index 0000000..3f0362b --- /dev/null +++ b/panels/user-accounts/cc-user-panel.ui @@ -0,0 +1,407 @@ +<interface> + <object class="GtkListStore" id="shortname-model"> + <columns> + <!-- column-name gchararray --> + <column type="gchararray"/> + </columns> + </object> + <object class="GtkListStore" id="language-model"> + <columns> + <!-- column-name gchararray --> + <column type="gchararray"/> + <!-- column-name gchararray1 --> + <column type="gchararray"/> + </columns> + </object> + <template class="CcUserPanel" parent="CcPanel"> + <child type="titlebar"> + <object class="AdwHeaderBar"> + <property name="show-end-title-buttons">True</property> + <property name="show-start-title-buttons">False</property> + <property name="title-widget"> + <object class="AdwWindowTitle"> + <property name="title" translatable="yes">Users</property> + </object> + </property> + <child type="start"> + <object class="GtkButton" id="back_button"> + <property name="visible">False</property> + <property name="icon-name">go-previous-symbolic</property> + <accessibility> + <property name="label" translatable="yes">Back</property> + </accessibility> + <signal name="clicked" handler="on_back_button_clicked_cb" object="CcUserPanel" swapped="yes"/> + </object> + </child> + </object> + </child> + <child type="content"> + <object class="GtkBox"> + <property name="orientation">vertical</property> + <child> + <object class="CcPermissionInfobar" id="permission_infobar"/> + </child> + <child> + <object class="GtkStack" id="stack"> + <property name="visible-child">no_users_box</property> + <child> + <object class="GtkOverlay" id="users_overlay"> + <child type="overlay"> + <object class="GtkRevealer" id="notification_revealer"> + <property name="halign">GTK_ALIGN_CENTER</property> + <property name="valign">GTK_ALIGN_START</property> + <child> + <object class="GtkBox"> + <property name="spacing">6</property> + <style> + <class name="app-notification"/> + </style> + <child> + <object class="GtkLabel"> + <property name="wrap">True</property> + <property name="max_width_chars">30</property> + <property name="label" translatable="yes">Your session needs to be restarted for changes to take effect</property> + </object> + </child> + <child> + <object class="GtkButton"> + <property name="can_focus">True</property> + <property name="valign">GTK_ALIGN_CENTER</property> + <property name="label" translatable="yes">Restart Now</property> + <signal name="clicked" handler="restart_now" object="CcUserPanel" swapped="yes"/> + </object> + </child> + <child> + <object class="GtkButton" id="dismiss_button"> + <property name="valign">GTK_ALIGN_CENTER</property> + <property name="icon_name">window-close-symbolic</property> + <accessibility> + <property name="label" translatable="yes">Close</property> + </accessibility> + <signal name="clicked" handler="dismiss_notification" object="CcUserPanel" swapped="yes"/> + <style> + <class name="flat"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="AdwPreferencesPage"> + <child> + <object class="AdwPreferencesGroup"> + <child> + <object class="GtkOverlay"> + <property name="halign">center</property> + <child> + <object class="AdwAvatar" id="user_avatar"> + <property name="show-initials">True</property> + <property name="size">120</property> + <property name="halign">center</property> + </object> + </child> + <child type="overlay"> + <object class="AdwBin"> + <style> + <class name="cutout-button"/> + </style> + <property name="halign">end</property> + <property name="valign">end</property> + <child> + <object class="GtkMenuButton" id="user_avatar_edit_button"> + <property name="sensitive">False</property> + <property name="icon-name">document-edit-symbolic</property> + <property name="popover"> + <object class="CcAvatarChooser" id="avatar_chooser"/> + </property> + <accessibility> + <property name="label" translatable="yes">Edit avatar</property> + </accessibility> + <style> + <class name="circular"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="AdwPreferencesGroup" id="authentication_and_login_box"> + <child> + <object class="AdwActionRow"> + <property name="title" translatable="yes">Name</property> + <child> + <object class="GtkStack" id="full_name_stack"> + <property name="hhomogeneous">False</property> + <child> + <object class="GtkLabel" id="full_name_label"> + </object> + </child> + <child> + <object class="GtkEntry" id="full_name_entry"> + <property name="max-length">255</property> + <property name="width-chars">18</property> + <property name="max-width-chars">30</property> + <property name="valign">GTK_ALIGN_CENTER</property> + <accessibility> + <property name="label" translatable="yes">Full name</property> + </accessibility> + <signal name="activate" handler="full_name_entry_activate" object="CcUserPanel" swapped="yes"/> + <child> + <object class="GtkEventControllerKey"> + <property name="propagation-phase">capture</property> + <signal name="key-pressed" handler="full_name_entry_key_press_cb" object="CcUserPanel" swapped="no"/> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkToggleButton" id="full_name_edit_button"> + <signal name="toggled" handler="full_name_edit_button_toggled" object="CcUserPanel" swapped="yes"/> + <property name="icon-name">document-edit-symbolic</property> + <property name="valign">GTK_ALIGN_CENTER</property> + <accessibility> + <property name="label" translatable="yes">Edit</property> + </accessibility> + <style> + <class name="flat"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="AdwActionRow" id="password_row"> + <property name="title" translatable="yes">_Password</property> + <property name="use_underline">True</property> + <property name="activatable">True</property> + <signal name="activated" handler="change_password" object="CcUserPanel" swapped="yes"/> + <child> + <object class="GtkLabel" id="password_button_label"/> + </child> + <child> + <object class="GtkImage"> + <property name="icon-name">go-next-symbolic</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="AdwActionRow" id="fingerprint_row"> + <property name="visible">True</property> <!-- FIXME --> + <property name="title" translatable="yes">_Fingerprint Login</property> + <property name="use_underline">True</property> + <property name="activatable">True</property> + <signal name="activated" handler="change_fingerprint" object="CcUserPanel" swapped="yes"/> + <child> + <object class="GtkLabel" id="fingerprint_state_label"> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkImage"> + <property name="icon-name">go-next-symbolic</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="AdwActionRow" id="autologin_row"> + <property name="title" translatable="yes">A_utomatic Login</property> + <property name="use_underline">True</property> + <property name="activatable_widget">autologin_switch</property> + <child> + <object class="GtkSwitch" id="autologin_switch"> + <property name="valign">center</property> + <signal name="notify::active" handler="autologin_changed" object="CcUserPanel" swapped="yes"/> + </object> + </child> + </object> + </child> + <child> + <object class="AdwActionRow" id="last_login_row"> + <property name="title" translatable="yes">Account Activity</property> + <property name="activatable">True</property> + <property name="use_underline">True</property> + <signal name="activated" handler="show_history" object="CcUserPanel" swapped="yes"/> + <child> + <object class="GtkLabel" id="last_login_button_label"> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkImage"> + <property name="icon-name">go-next-symbolic</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + + <child> + <object class="AdwPreferencesGroup" id="account_settings_box"> + <child> + <object class="AdwActionRow" id="account_type_row"> + <property name="title" translatable="yes">_Administrator</property> + <property name="subtitle" translatable="yes">Administrators can add and remove other users, and can change settings for all users.</property> + <property name="subtitle-lines">0</property> + <property name="use_underline">True</property> + <property name="activatable-widget">account_type_switch</property> + <child> + <object class="GtkSwitch" id="account_type_switch"> + <property name="valign">center</property> + <signal name="notify::active" handler="account_type_changed" object="CcUserPanel" swapped="yes"/> + </object> + </child> + </object> + </child> + + <child> + <object class="AdwActionRow" id="parental_controls_row"> + <property name="visible">False</property> + <property name="title" translatable="yes">_Parental Controls</property> + <property name="subtitle" translatable="yes">Open the Parental Controls application.</property> + <property name="subtitle-lines">0</property> + <property name="use_underline">True</property> + <property name="activatable">True</property> + <child> + <object class="GtkLabel" id="parental_controls_button_label"> + <property name="valign">0.5</property> + </object> + </child> + <child> + <object class="GtkImage" id="parental_control_go_next"> + <property name="icon-name">go-next-symbolic</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + </object> + </child> + + <child> + <object class="AdwActionRow" id="language_row"> + <property name="title" translatable="yes">_Language</property> + <property name="use_underline">True</property> + <property name="activatable">True</property> + <signal name="activated" handler="change_language" object="CcUserPanel" swapped="yes"/> + <child> + <object class="GtkLabel" id="language_button_label"> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkImage"> + <property name="icon-name">go-next-symbolic</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + + <child> + <object class="AdwPreferencesGroup"> + <child> + <object class="GtkButton" id="remove_user_button"> + <property name="visible">False</property> + <property name="label" translatable="yes">Remove User…</property> + <signal name="clicked" handler="delete_user" object="CcUserPanel" swapped="yes"/> + <style> + <class name="destructive-action"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="AdwPreferencesGroup" id="other_users"> + <property name="visible">False</property> + <property name="title" translatable="yes">Other Users</property> + <child> + <object class="AdwPreferencesRow" id="other_users_row"> + <child> + <object class="GtkListBox" id="other_users_listbox"> + <property name="selection-mode">none</property> + <signal name="row-activated" handler="set_selected_user" object="CcUserPanel" swapped="yes"/> + </object> + </child> + </object> + </child> + <child> + <object class="AdwActionRow" id="add_user_button"> + <property name="title" translatable="yes">Add User…</property> + <property name="icon-name">list-add-symbolic</property> + <property name="activatable">True</property> + <signal name="activated" handler="add_user" object="CcUserPanel" swapped="yes"/> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox" id="no_users_box"> + <property name="orientation">GTK_ORIENTATION_VERTICAL</property> + <property name="valign">GTK_ALIGN_CENTER</property> + <property name="spacing">12</property> + <style> + <class name="dim-label"/> + </style> + <child> + <object class="GtkImage"> + <property name="icon_name">avatar-default-symbolic</property> + <property name="pixel_size">192</property> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes" comments="Translators: This is the empty state page label which states that there are no users to show in the panel.">No Users Found</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="1.6"/> + </attributes> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">Unlock to add a user account.</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/panels/user-accounts/data/cc-fingerprint-dialog.css b/panels/user-accounts/data/cc-fingerprint-dialog.css new file mode 100644 index 0000000..800d658 --- /dev/null +++ b/panels/user-accounts/data/cc-fingerprint-dialog.css @@ -0,0 +1,83 @@ +.fingerprint-icon { + padding: 3px; +} + +.fingerprint-icon > button, +.fingerprint-icon > image { + padding: 15px; + min-width: 32px; + min-height: 32px; + border-radius: 64px; + border: 1px solid @borders; + background-color: @theme_base_color; + color: @insensitive_fg_color; +} + +.fingerprint-print-add image:not(:disabled):not(:backdrop), +.fingerprint-print-add button:not(:disabled):not(:backdrop) { + color: @theme_fg_color; +} + +.fingerprint-icon.enroll-status image { + outline-color: @theme_selected_bg_color; + outline-offset: 0px; + outline-width: 4px; +} + +.fingerprint-icon.enroll-status image:backdrop { + outline-color: @theme_unfocused_selected_bg_color; +} + +.fingerprint-icon.enroll-status { + font-weight: bold; +} + +.fingerprint-icon.enroll-status.completed image { + outline-color: @success_color; +} + +.fingerprint-icon.enroll-status.warning image { + outline-color: @warning_color; +} + +.fingerprint-icon.enroll-status.error image { + outline-color: @error_color; + /* Given we don't have an error image, we can just recolorize the warning one */ + -gtk-icon-palette: warning @error_color; +} + +.fingerprint-icon.enroll-status.success image:not(:backdrop) { + color: @theme_selected_bg_color; +} + +.fingerprint-icon.enroll-status.warning image:not(:backdrop), +.fingerprint-icon.enroll-status.warning label:not(:backdrop) { + color: @warning_color; +} + +.fingerprint-icon.enroll-status.error image:not(:backdrop), +.fingerprint-icon.enroll-status.error label:not(:backdrop) { + color: @error_color; +} + +@keyframes wiggle { + /* Unfortunately we can't use translation or xalign, so here's the workaround */ + 0% { padding-left: 0; padding-right: 0; } + 10% { padding-left: 0; padding-right: 2px; } + 20% { padding-left: 4px; padding-right: 0; } + 30% { padding-left: 0; padding-right: 8px; } + 40% { padding-left: 8px; padding-right: 0; } + 50% { padding-left: 0; padding-right: 8px; } + 60% { padding-left: 8px; padding-right: 0; } + 70% { padding-left: 0; padding-right: 8px; } + 80% { padding-left: 4px; padding-right: 0; } + 90% { padding-left: 0; padding-right: 2px; } + 100% { padding-left: 0; padding-right: 0; } +} + +.fingerprint-icon.enroll-status.retry label { + animation-name: wiggle; + animation-duration: 850ms; + animation-timing-function: ease-in; + animation-iteration-count: 1; +} diff --git a/panels/user-accounts/data/faces/bicycle.jpg b/panels/user-accounts/data/faces/bicycle.jpg Binary files differnew file mode 100644 index 0000000..c598251 --- /dev/null +++ b/panels/user-accounts/data/faces/bicycle.jpg diff --git a/panels/user-accounts/data/faces/book.jpg b/panels/user-accounts/data/faces/book.jpg Binary files differnew file mode 100644 index 0000000..abda4b7 --- /dev/null +++ b/panels/user-accounts/data/faces/book.jpg diff --git a/panels/user-accounts/data/faces/calculator.jpg b/panels/user-accounts/data/faces/calculator.jpg Binary files differnew file mode 100644 index 0000000..43ece1e --- /dev/null +++ b/panels/user-accounts/data/faces/calculator.jpg diff --git a/panels/user-accounts/data/faces/cat.jpg b/panels/user-accounts/data/faces/cat.jpg Binary files differnew file mode 100644 index 0000000..99275b2 --- /dev/null +++ b/panels/user-accounts/data/faces/cat.jpg diff --git a/panels/user-accounts/data/faces/coffee2.jpg b/panels/user-accounts/data/faces/coffee2.jpg Binary files differnew file mode 100644 index 0000000..4be830d --- /dev/null +++ b/panels/user-accounts/data/faces/coffee2.jpg diff --git a/panels/user-accounts/data/faces/flower2.jpg b/panels/user-accounts/data/faces/flower2.jpg Binary files differnew file mode 100644 index 0000000..b77e717 --- /dev/null +++ b/panels/user-accounts/data/faces/flower2.jpg diff --git a/panels/user-accounts/data/faces/gamepad.jpg b/panels/user-accounts/data/faces/gamepad.jpg Binary files differnew file mode 100644 index 0000000..9843758 --- /dev/null +++ b/panels/user-accounts/data/faces/gamepad.jpg diff --git a/panels/user-accounts/data/faces/guitar2.jpg b/panels/user-accounts/data/faces/guitar2.jpg Binary files differnew file mode 100644 index 0000000..f1ad374 --- /dev/null +++ b/panels/user-accounts/data/faces/guitar2.jpg diff --git a/panels/user-accounts/data/faces/headphones.jpg b/panels/user-accounts/data/faces/headphones.jpg Binary files differnew file mode 100644 index 0000000..2ad9175 --- /dev/null +++ b/panels/user-accounts/data/faces/headphones.jpg diff --git a/panels/user-accounts/data/faces/hummingbird.jpg b/panels/user-accounts/data/faces/hummingbird.jpg Binary files differnew file mode 100644 index 0000000..d06a3cf --- /dev/null +++ b/panels/user-accounts/data/faces/hummingbird.jpg diff --git a/panels/user-accounts/data/faces/legacy/astronaut.jpg b/panels/user-accounts/data/faces/legacy/astronaut.jpg Binary files differnew file mode 100644 index 0000000..4b79f0e --- /dev/null +++ b/panels/user-accounts/data/faces/legacy/astronaut.jpg diff --git a/panels/user-accounts/data/faces/legacy/baseball.png b/panels/user-accounts/data/faces/legacy/baseball.png Binary files differnew file mode 100644 index 0000000..0d6dfdb --- /dev/null +++ b/panels/user-accounts/data/faces/legacy/baseball.png diff --git a/panels/user-accounts/data/faces/legacy/butterfly.png b/panels/user-accounts/data/faces/legacy/butterfly.png Binary files differnew file mode 100644 index 0000000..66b813c --- /dev/null +++ b/panels/user-accounts/data/faces/legacy/butterfly.png diff --git a/panels/user-accounts/data/faces/legacy/cat-eye.jpg b/panels/user-accounts/data/faces/legacy/cat-eye.jpg Binary files differnew file mode 100644 index 0000000..c818bd5 --- /dev/null +++ b/panels/user-accounts/data/faces/legacy/cat-eye.jpg diff --git a/panels/user-accounts/data/faces/legacy/chess.jpg b/panels/user-accounts/data/faces/legacy/chess.jpg Binary files differnew file mode 100644 index 0000000..7abb8a4 --- /dev/null +++ b/panels/user-accounts/data/faces/legacy/chess.jpg diff --git a/panels/user-accounts/data/faces/legacy/coffee.jpg b/panels/user-accounts/data/faces/legacy/coffee.jpg Binary files differnew file mode 100644 index 0000000..46e8fc5 --- /dev/null +++ b/panels/user-accounts/data/faces/legacy/coffee.jpg diff --git a/panels/user-accounts/data/faces/legacy/dice.jpg b/panels/user-accounts/data/faces/legacy/dice.jpg Binary files differnew file mode 100644 index 0000000..641b124 --- /dev/null +++ b/panels/user-accounts/data/faces/legacy/dice.jpg diff --git a/panels/user-accounts/data/faces/legacy/energy-arc.jpg b/panels/user-accounts/data/faces/legacy/energy-arc.jpg Binary files differnew file mode 100644 index 0000000..9f4c892 --- /dev/null +++ b/panels/user-accounts/data/faces/legacy/energy-arc.jpg diff --git a/panels/user-accounts/data/faces/legacy/fish.jpg b/panels/user-accounts/data/faces/legacy/fish.jpg Binary files differnew file mode 100644 index 0000000..fc363d6 --- /dev/null +++ b/panels/user-accounts/data/faces/legacy/fish.jpg diff --git a/panels/user-accounts/data/faces/legacy/flake.jpg b/panels/user-accounts/data/faces/legacy/flake.jpg Binary files differnew file mode 100644 index 0000000..5546d7e --- /dev/null +++ b/panels/user-accounts/data/faces/legacy/flake.jpg diff --git a/panels/user-accounts/data/faces/legacy/flower.jpg b/panels/user-accounts/data/faces/legacy/flower.jpg Binary files differnew file mode 100644 index 0000000..3e41ba4 --- /dev/null +++ b/panels/user-accounts/data/faces/legacy/flower.jpg diff --git a/panels/user-accounts/data/faces/legacy/grapes.jpg b/panels/user-accounts/data/faces/legacy/grapes.jpg Binary files differnew file mode 100644 index 0000000..3d31daf --- /dev/null +++ b/panels/user-accounts/data/faces/legacy/grapes.jpg diff --git a/panels/user-accounts/data/faces/legacy/guitar.jpg b/panels/user-accounts/data/faces/legacy/guitar.jpg Binary files differnew file mode 100644 index 0000000..9e8834f --- /dev/null +++ b/panels/user-accounts/data/faces/legacy/guitar.jpg diff --git a/panels/user-accounts/data/faces/legacy/launch.jpg b/panels/user-accounts/data/faces/legacy/launch.jpg Binary files differnew file mode 100644 index 0000000..7c7bf43 --- /dev/null +++ b/panels/user-accounts/data/faces/legacy/launch.jpg diff --git a/panels/user-accounts/data/faces/legacy/leaf.jpg b/panels/user-accounts/data/faces/legacy/leaf.jpg Binary files differnew file mode 100644 index 0000000..5354103 --- /dev/null +++ b/panels/user-accounts/data/faces/legacy/leaf.jpg diff --git a/panels/user-accounts/data/faces/legacy/lightning.jpg b/panels/user-accounts/data/faces/legacy/lightning.jpg Binary files differnew file mode 100644 index 0000000..736ccd5 --- /dev/null +++ b/panels/user-accounts/data/faces/legacy/lightning.jpg diff --git a/panels/user-accounts/data/faces/legacy/penguin.jpg b/panels/user-accounts/data/faces/legacy/penguin.jpg Binary files differnew file mode 100644 index 0000000..2a8dfd6 --- /dev/null +++ b/panels/user-accounts/data/faces/legacy/penguin.jpg diff --git a/panels/user-accounts/data/faces/legacy/puppy.jpg b/panels/user-accounts/data/faces/legacy/puppy.jpg Binary files differnew file mode 100644 index 0000000..ab55a8b --- /dev/null +++ b/panels/user-accounts/data/faces/legacy/puppy.jpg diff --git a/panels/user-accounts/data/faces/legacy/sky.jpg b/panels/user-accounts/data/faces/legacy/sky.jpg Binary files differnew file mode 100644 index 0000000..841f90e --- /dev/null +++ b/panels/user-accounts/data/faces/legacy/sky.jpg diff --git a/panels/user-accounts/data/faces/legacy/soccerball.png b/panels/user-accounts/data/faces/legacy/soccerball.png Binary files differnew file mode 100644 index 0000000..56588a9 --- /dev/null +++ b/panels/user-accounts/data/faces/legacy/soccerball.png diff --git a/panels/user-accounts/data/faces/legacy/sunflower.jpg b/panels/user-accounts/data/faces/legacy/sunflower.jpg Binary files differnew file mode 100644 index 0000000..6102b8b --- /dev/null +++ b/panels/user-accounts/data/faces/legacy/sunflower.jpg diff --git a/panels/user-accounts/data/faces/legacy/sunset.jpg b/panels/user-accounts/data/faces/legacy/sunset.jpg Binary files differnew file mode 100644 index 0000000..48b6223 --- /dev/null +++ b/panels/user-accounts/data/faces/legacy/sunset.jpg diff --git a/panels/user-accounts/data/faces/legacy/tennis-ball.png b/panels/user-accounts/data/faces/legacy/tennis-ball.png Binary files differnew file mode 100644 index 0000000..a1beb50 --- /dev/null +++ b/panels/user-accounts/data/faces/legacy/tennis-ball.png diff --git a/panels/user-accounts/data/faces/legacy/yellow-rose.jpg b/panels/user-accounts/data/faces/legacy/yellow-rose.jpg Binary files differnew file mode 100644 index 0000000..7f1de96 --- /dev/null +++ b/panels/user-accounts/data/faces/legacy/yellow-rose.jpg diff --git a/panels/user-accounts/data/faces/mountain.jpg b/panels/user-accounts/data/faces/mountain.jpg Binary files differnew file mode 100644 index 0000000..8425e0d --- /dev/null +++ b/panels/user-accounts/data/faces/mountain.jpg diff --git a/panels/user-accounts/data/faces/plane.jpg b/panels/user-accounts/data/faces/plane.jpg Binary files differnew file mode 100644 index 0000000..a0c4506 --- /dev/null +++ b/panels/user-accounts/data/faces/plane.jpg diff --git a/panels/user-accounts/data/faces/surfer.jpg b/panels/user-accounts/data/faces/surfer.jpg Binary files differnew file mode 100644 index 0000000..47f19b0 --- /dev/null +++ b/panels/user-accounts/data/faces/surfer.jpg diff --git a/panels/user-accounts/data/faces/tomatoes.jpg b/panels/user-accounts/data/faces/tomatoes.jpg Binary files differnew file mode 100644 index 0000000..ebdb6ba --- /dev/null +++ b/panels/user-accounts/data/faces/tomatoes.jpg diff --git a/panels/user-accounts/data/faces/tree.jpg b/panels/user-accounts/data/faces/tree.jpg Binary files differnew file mode 100644 index 0000000..8a619d3 --- /dev/null +++ b/panels/user-accounts/data/faces/tree.jpg diff --git a/panels/user-accounts/data/gnome-user-accounts-panel.desktop.in.in b/panels/user-accounts/data/gnome-user-accounts-panel.desktop.in.in new file mode 100644 index 0000000..b91b37f --- /dev/null +++ b/panels/user-accounts/data/gnome-user-accounts-panel.desktop.in.in @@ -0,0 +1,19 @@ +[Desktop Entry] +Name=Users +Comment=Add or remove users and change your password +Exec=gnome-control-center user-accounts +# Translators: Do NOT translate or transliterate this text (this is an icon file name)! +Icon=org.gnome.Settings-users-symbolic +Terminal=false +Type=Application +NoDisplay=true +StartupNotify=true +Categories=GNOME;GTK;Settings;X-GNOME-Settings-Panel;X-GNOME-DetailsSettings; +OnlyShowIn=GNOME;Unity; +X-GNOME-Bugzilla-Bugzilla=GNOME +X-GNOME-Bugzilla-Product=gnome-control-center +X-GNOME-Bugzilla-Component=user-accounts +X-GNOME-Bugzilla-Version=@VERSION@ +X-GNOME-Settings-Panel=user-accounts +# Translators: Search terms to find the Users panel. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! +Keywords=Login;Name;Fingerprint;Avatar;Logo;Face;Password;Parental Controls;Screen Time;App Restrictions;Web Restrictions;Usage;Usage Limit;Kid;Child; diff --git a/panels/user-accounts/data/icons/fingerprint-detection-complete-symbolic.svg b/panels/user-accounts/data/icons/fingerprint-detection-complete-symbolic.svg new file mode 100644 index 0000000..d7f6ce4 --- /dev/null +++ b/panels/user-accounts/data/icons/fingerprint-detection-complete-symbolic.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 8.467 8.467"><path style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;white-space:normal;shape-padding:0;isolation:auto;mix-blend-mode:normal;solid-color:#000;solid-opacity:1" d="M16.133.99a12.982 12.982 0 0 0-6.477 1.653c-1.173.646-.194 2.404.973 1.748A10.999 10.999 0 0 1 27 13.994v2c0 1.334 2 1.334 2 0v-2c0-.033-.001-.066-.004-.1a13.007 13.007 0 0 0-6.418-11.119A12.995 12.995 0 0 0 16.133.99ZM5.762 6.64a1 1 0 0 0-.801.485 12.999 12.999 0 0 0-1.957 6.77 1 1 0 0 0-.004.1v10.128c0 1.334 2 1.334 2 0V13.994c0-2.055.575-4.07 1.66-5.814a1 1 0 0 0-.898-1.54ZM16 6.993c-3.813 0-6.928 3.082-6.994 6.881a1 1 0 0 0-.006.121v2c0 1.333 2 1.333 2 0v-2a4.986 4.986 0 0 1 5-5.002c2.773 0 5 2.228 5 5.002v8.131a3 3 0 0 1 1.123.752l.877.877v-9.76a.994.994 0 0 0-.006-.115c-.062-3.802-3.179-6.887-6.994-6.887Zm-.016 5.987A1 1 0 0 0 15 13.994v10.004s0 1.094.27 2.445c.27 1.351.787 3.028 2.023 4.264.942.982 2.395-.471 1.414-1.414-.69-.69-1.139-1.835-1.39-2.898l-.003-.002a3 3 0 0 1-.302-2.14 17.252 17.252 0 0 0-.012-.255V13.994a1 1 0 0 0-1.016-1.015zm-6 8.001A1 1 0 0 0 9 21.996v6.002c0 1.334 2 1.334 2 0v-6.002a1 1 0 0 0-1.016-1.016zm20.998.006a1 1 0 0 0-.687.303l-6.297 6.29-3.291-3.294a1 1 0 1 0-1.41 1.418l4.701 4.703 7.707-7.707a1 1 0 0 0-.723-1.713z" transform="scale(.26458)" class="success" color="#000" font-weight="400" font-family="sans-serif" overflow="visible" fill="#33d17a"/></svg>
\ No newline at end of file diff --git a/panels/user-accounts/data/icons/fingerprint-detection-symbolic.svg b/panels/user-accounts/data/icons/fingerprint-detection-symbolic.svg new file mode 100644 index 0000000..d91ce24 --- /dev/null +++ b/panels/user-accounts/data/icons/fingerprint-detection-symbolic.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 8.467 8.467"><path d="M4.268 288.795a3.435 3.435 0 0 0-1.713.438.265.265 0 1 0 .257.462 2.91 2.91 0 0 1 4.332 2.541v.53a.265.265 0 1 0 .529 0v-.53a.265.265 0 0 0-.001-.026 3.442 3.442 0 0 0-3.404-3.415zm-2.744 1.495a.265.265 0 0 0-.211.128 3.44 3.44 0 0 0-.518 1.792.265.265 0 0 0-.001.026v2.68a.265.265 0 0 0 .529 0v-2.68c0-.544.152-1.077.44-1.538a.265.265 0 0 0-.239-.408zm2.71.093a1.856 1.856 0 0 0-1.851 1.82.265.265 0 0 0-.002.033v.529a.265.265 0 1 0 .53 0v-.53c0-.733.589-1.323 1.322-1.323.734 0 1.323.59 1.323 1.324v2.647s.002.182.08.414c.077.232.231.535.527.831a.265.265 0 1 0 .374-.374 1.595 1.595 0 0 1-.399-.624c-.055-.165-.053-.247-.053-.247v-2.647a.265.265 0 0 0-.001-.03 1.856 1.856 0 0 0-1.85-1.823zm-.005 1.584a.265.265 0 0 0-.26.269v2.647s0 .29.071.647c.072.357.208.8.535 1.128a.265.265 0 1 0 .375-.374c-.203-.203-.33-.553-.391-.858-.06-.304-.061-.543-.061-.543v-2.647a.265.265 0 0 0-.269-.269zm-1.587 2.117a.265.265 0 0 0-.26.27v1.587a.265.265 0 1 0 .528 0v-1.588a.265.265 0 0 0-.268-.269zm4.762 0a.265.265 0 0 0-.26.268v.53a.265.265 0 1 0 .529 0v-.53a.265.265 0 0 0-.269-.268z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;white-space:normal;shape-padding:0;isolation:auto;mix-blend-mode:normal;solid-color:#000;solid-opacity:1" color="#000" font-weight="400" font-family="sans-serif" overflow="visible" fill="#3d3846" transform="translate(0 -288.533)"/></svg>
\ No newline at end of file diff --git a/panels/user-accounts/data/icons/fingerprint-detection-warning-symbolic.svg b/panels/user-accounts/data/icons/fingerprint-detection-warning-symbolic.svg new file mode 100644 index 0000000..4c6e281 --- /dev/null +++ b/panels/user-accounts/data/icons/fingerprint-detection-warning-symbolic.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 8.467 8.467"><path overflow="visible" font-weight="400" d="M4.268.262a3.435 3.435 0 0 0-1.713.437.265.265 0 1 0 .257.463 2.91 2.91 0 0 1 4.332 2.54v.53a.265.265 0 1 0 .529 0v-.53a.265.265 0 0 0-.001-.026A3.441 3.441 0 0 0 4.268.262ZM1.524 1.757a.265.265 0 0 0-.211.128 3.44 3.44 0 0 0-.518 1.791.265.265 0 0 0-.001.027v2.68a.265.265 0 0 0 .529 0v-2.68c0-.544.152-1.077.44-1.539a.265.265 0 0 0-.239-.407Zm2.71.093a1.856 1.856 0 0 0-1.851 1.82.265.265 0 0 0-.002.033v.529a.265.265 0 0 0 .53 0v-.53c0-.733.589-1.323 1.322-1.323.734 0 1.323.59 1.323 1.324v.708a1.05 1.05 0 0 1 .53-.385v-.323a.265.265 0 0 0-.002-.03 1.856 1.856 0 0 0-1.85-1.823Zm-.005 1.584a.265.265 0 0 0-.26.269v2.646s0 .29.071.647c.009.044.022.092.033.138l.425-.787V3.703a.265.265 0 0 0-.269-.27Zm2.096 1.089c-.139-.008-.275.075-.38.256L4.29 7.846c-.141.255.013.62.29.62h3.48c.26 0 .504-.306.323-.62l-1.67-3.05c-.106-.171-.25-.266-.389-.273ZM2.642 5.55a.265.265 0 0 0-.26.269v1.588a.265.265 0 1 0 .528 0V5.82a.265.265 0 0 0-.268-.269Zm3.7.013a.275.275 0 0 1 .272.273V6.88a.272.272 0 0 1-.264.265.272.272 0 0 1-.265-.265V5.837a.273.273 0 0 1 .215-.264.195.195 0 0 1 .042-.008zm.008 1.844a.265.265 0 1 1 0 .53.265.265 0 0 1 0-.53z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;white-space:normal;shape-padding:0;isolation:auto;mix-blend-mode:normal;solid-color:#000;solid-opacity:1" class="warning" color="#000" font-family="sans-serif" fill="#ff7800"/></svg>
\ No newline at end of file diff --git a/panels/user-accounts/data/icons/left-index-finger.svg b/panels/user-accounts/data/icons/left-index-finger.svg new file mode 100644 index 0000000..7a79add --- /dev/null +++ b/panels/user-accounts/data/icons/left-index-finger.svg @@ -0,0 +1 @@ +<svg version="1.0" width="48" height="48" viewBox="0 0 40.425 46.214" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="a"><stop offset="0" style="stop-color:#fff;stop-opacity:1"/><stop style="stop-color:#27dc16;stop-opacity:1" offset="1"/></linearGradient></defs><circle style="fill:#8ff0a4;stroke:#2ec27e;stroke-width:.962791;stroke-miterlimit:4;stroke-dasharray:none;stop-color:#000" cx="26.461" cy="9.203" r="5.783"/><g style="stroke-width:1.06881"><path style="fill:#241f31;stroke:none;stroke-width:1.06881px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M437.016 124.536h.029a14.986 14.986 45 0 1 14.985 14.986v54.153a4.504 4.504 45 0 0 4.504 4.504 4.216 4.216 130.946 0 0 3.908-4.504v-63.748a14 14 135 0 1 14-14 13.257 13.257 48.306 0 1 12.47 14v63.897a4.355 4.355 45 0 0 4.355 4.355 4.149 4.149 132.08 0 0 3.932-4.355v-63.897a14 14 135 0 1 14-14 13.11 13.11 49.036 0 1 12.155 14v64.019a4.233 4.233 45 0 0 4.233 4.233h.202a4.233 4.233 135 0 0 4.233-4.233v-40.661a14 14 135 0 1 14-14 13.005 13.005 49.527 0 1 11.946 14L555.833 260a30.038 30.038 135.036 0 1-30.039 30H450a48.284 48.284 22.5 0 1-34.142-14.142l-37.801-37.801a15.702 15.702 86.658 0 1-1.217-20.839 14.154 14.154 173.662 0 1 19.896-2.21l21.76 17.2a2.181 2.181 154.16 0 0 3.534-1.712v-90.974a14.986 14.986 135 0 1 14.986-14.986z" transform="matrix(-.22259 0 0 .22259 123.725 -22.001)"/><path style="fill:#000;fill-opacity:.470966;stroke:none;stroke-width:1.06881px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M374.444 222.919c-1.992 5.044.075 11.54 3.91 15.375l37.503 37.563A48.284 48.284 0 0 0 450 290h75.794c16.569 0 30-13.431 30-30v-10c0 16.569-13.431 30-30 30H450a48.284 48.284 0 0 1-34.143-14.143l-38.377-38.313a13.61 13.61 0 0 1-3.036-4.625z" transform="matrix(-.22259 0 0 .22259 123.725 -22.001)"/></g></svg>
\ No newline at end of file diff --git a/panels/user-accounts/data/icons/left-little-finger.svg b/panels/user-accounts/data/icons/left-little-finger.svg new file mode 100644 index 0000000..106e8ea --- /dev/null +++ b/panels/user-accounts/data/icons/left-little-finger.svg @@ -0,0 +1 @@ +<svg version="1.0" width="48" height="48" viewBox="0 0 40.425 46.214" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="a"><stop offset="0" style="stop-color:#fff;stop-opacity:1"/><stop style="stop-color:#27dc16;stop-opacity:1" offset="1"/></linearGradient></defs><circle style="fill:#8ff0a4;stroke:#2ec27e;stroke-width:.962792;stroke-miterlimit:4;stroke-dasharray:none;stop-color:#000" cx="2.853" cy="11.617" r="5.207"/><g style="stroke-width:1.06881"><path style="fill:#241f31;stroke:none;stroke-width:1.06881px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M437.016 124.536h.029a14.986 14.986 45 0 1 14.985 14.986v54.153a4.504 4.504 45 0 0 4.504 4.504 4.216 4.216 130.946 0 0 3.908-4.504v-63.748a14 14 135 0 1 14-14 13.257 13.257 48.306 0 1 12.47 14v63.897a4.355 4.355 45 0 0 4.355 4.355 4.149 4.149 132.08 0 0 3.932-4.355v-63.897a14 14 135 0 1 14-14 13.11 13.11 49.036 0 1 12.155 14v64.019a4.233 4.233 45 0 0 4.233 4.233h.202a4.233 4.233 135 0 0 4.233-4.233v-40.661a14 14 135 0 1 14-14 13.005 13.005 49.527 0 1 11.946 14L555.833 260a30.038 30.038 135.036 0 1-30.039 30H450a48.284 48.284 22.5 0 1-34.142-14.142l-37.801-37.801a15.702 15.702 86.658 0 1-1.217-20.839 14.154 14.154 173.662 0 1 19.896-2.21l21.76 17.2a2.181 2.181 154.16 0 0 3.534-1.712v-90.974a14.986 14.986 135 0 1 14.986-14.986z" transform="matrix(-.22259 0 0 .22259 123.725 -22.001)"/><path style="fill:#000;fill-opacity:.470966;stroke:none;stroke-width:1.06881px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M374.444 222.919c-1.992 5.044.075 11.54 3.91 15.375l37.503 37.563A48.284 48.284 0 0 0 450 290h75.794c16.569 0 30-13.431 30-30v-10c0 16.569-13.431 30-30 30H450a48.284 48.284 0 0 1-34.143-14.143l-38.377-38.313a13.61 13.61 0 0 1-3.036-4.625z" transform="matrix(-.22259 0 0 .22259 123.725 -22.001)"/></g></svg>
\ No newline at end of file diff --git a/panels/user-accounts/data/icons/left-middle-finger.svg b/panels/user-accounts/data/icons/left-middle-finger.svg new file mode 100644 index 0000000..5ae2b1c --- /dev/null +++ b/panels/user-accounts/data/icons/left-middle-finger.svg @@ -0,0 +1 @@ +<svg version="1.0" width="48" height="48" viewBox="0 0 40.425 46.214" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="a"><stop offset="0" style="stop-color:#fff;stop-opacity:1"/><stop style="stop-color:#27dc16;stop-opacity:1" offset="1"/></linearGradient></defs><circle style="fill:#8ff0a4;stroke:#2ec27e;stroke-width:.885327;stroke-miterlimit:4;stroke-dasharray:none;stop-color:#000" cx="18.281" cy="6.839" r="5.318"/><g style="stroke-width:1.06881"><path style="fill:#241f31;stroke:none;stroke-width:1.06881px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M437.016 124.536h.029a14.986 14.986 45 0 1 14.985 14.986v54.153a4.504 4.504 45 0 0 4.504 4.504 4.216 4.216 130.946 0 0 3.908-4.504v-63.748a14 14 135 0 1 14-14 13.257 13.257 48.306 0 1 12.47 14v63.897a4.355 4.355 45 0 0 4.355 4.355 4.149 4.149 132.08 0 0 3.932-4.355v-63.897a14 14 135 0 1 14-14 13.11 13.11 49.036 0 1 12.155 14v64.019a4.233 4.233 45 0 0 4.233 4.233h.202a4.233 4.233 135 0 0 4.233-4.233v-40.661a14 14 135 0 1 14-14 13.005 13.005 49.527 0 1 11.946 14L555.833 260a30.038 30.038 135.036 0 1-30.039 30H450a48.284 48.284 22.5 0 1-34.142-14.142l-37.801-37.801a15.702 15.702 86.658 0 1-1.217-20.839 14.154 14.154 173.662 0 1 19.896-2.21l21.76 17.2a2.181 2.181 154.16 0 0 3.534-1.712v-90.974a14.986 14.986 135 0 1 14.986-14.986z" transform="matrix(-.22259 0 0 .22259 123.725 -22.001)"/><path style="fill:#000;fill-opacity:.470966;stroke:none;stroke-width:1.06881px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M374.444 222.919c-1.992 5.044.075 11.54 3.91 15.375l37.503 37.563A48.284 48.284 0 0 0 450 290h75.794c16.569 0 30-13.431 30-30v-10c0 16.569-13.431 30-30 30H450a48.284 48.284 0 0 1-34.143-14.143l-38.377-38.313a13.61 13.61 0 0 1-3.036-4.625z" transform="matrix(-.22259 0 0 .22259 123.725 -22.001)"/></g></svg>
\ No newline at end of file diff --git a/panels/user-accounts/data/icons/left-ring-finger.svg b/panels/user-accounts/data/icons/left-ring-finger.svg new file mode 100644 index 0000000..7a9bb4e --- /dev/null +++ b/panels/user-accounts/data/icons/left-ring-finger.svg @@ -0,0 +1 @@ +<svg version="1.0" width="48" height="48" viewBox="0 0 40.425 46.214" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="a"><stop offset="0" style="stop-color:#fff;stop-opacity:1"/><stop style="stop-color:#27dc16;stop-opacity:1" offset="1"/></linearGradient></defs><circle style="fill:#8ff0a4;stroke:#2ec27e;stroke-width:.96279167;stroke-miterlimit:4;stroke-dasharray:none;stop-color:#000" cx="10.432" cy="6.745" r="5.247"/><g style="stroke-width:1.06881"><path style="fill:#241f31;stroke:none;stroke-width:1.06881px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M437.016 124.536h.029a14.986 14.986 45 0 1 14.985 14.986v54.153a4.504 4.504 45 0 0 4.504 4.504 4.216 4.216 130.946 0 0 3.908-4.504v-63.748a14 14 135 0 1 14-14 13.257 13.257 48.306 0 1 12.47 14v63.897a4.355 4.355 45 0 0 4.355 4.355 4.149 4.149 132.08 0 0 3.932-4.355v-63.897a14 14 135 0 1 14-14 13.11 13.11 49.036 0 1 12.155 14v64.019a4.233 4.233 45 0 0 4.233 4.233h.202a4.233 4.233 135 0 0 4.233-4.233v-40.661a14 14 135 0 1 14-14 13.005 13.005 49.527 0 1 11.946 14L555.833 260a30.038 30.038 135.036 0 1-30.039 30H450a48.284 48.284 22.5 0 1-34.142-14.142l-37.801-37.801a15.702 15.702 86.658 0 1-1.217-20.839 14.154 14.154 173.662 0 1 19.896-2.21l21.76 17.2a2.181 2.181 154.16 0 0 3.534-1.712v-90.974a14.986 14.986 135 0 1 14.986-14.986z" transform="matrix(-.22259 0 0 .22259 123.725 -22.001)"/><path style="fill:#000;fill-opacity:.470966;stroke:none;stroke-width:1.06881px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M374.444 222.919c-1.992 5.044.075 11.54 3.91 15.375l37.503 37.563A48.284 48.284 0 0 0 450 290h75.794c16.569 0 30-13.431 30-30v-10c0 16.569-13.431 30-30 30H450a48.284 48.284 0 0 1-34.143-14.143l-38.377-38.313a13.61 13.61 0 0 1-3.036-4.625z" transform="matrix(-.22259 0 0 .22259 123.725 -22.001)"/></g></svg>
\ No newline at end of file diff --git a/panels/user-accounts/data/icons/left-thumb.svg b/panels/user-accounts/data/icons/left-thumb.svg new file mode 100644 index 0000000..bf28739 --- /dev/null +++ b/panels/user-accounts/data/icons/left-thumb.svg @@ -0,0 +1 @@ +<svg version="1.0" width="48" height="48" viewBox="0 0 40.425 46.214" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="a"><stop offset="0" style="stop-color:#fff;stop-opacity:1"/><stop style="stop-color:#27dc16;stop-opacity:1" offset="1"/></linearGradient></defs><circle style="fill:#8ff0a4;stroke:#2ec27e;stroke-width:.949928;stroke-miterlimit:4;stroke-dasharray:none;stop-color:#000" cx="37.026" cy="28.463" r="5.706"/><g style="stroke-width:1.06881"><path style="fill:#241f31;stroke:none;stroke-width:1.06881px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M437.016 124.536h.029a14.986 14.986 45 0 1 14.985 14.986v54.153a4.504 4.504 45 0 0 4.504 4.504 4.216 4.216 130.946 0 0 3.908-4.504v-63.748a14 14 135 0 1 14-14 13.257 13.257 48.306 0 1 12.47 14v63.897a4.355 4.355 45 0 0 4.355 4.355 4.149 4.149 132.08 0 0 3.932-4.355v-63.897a14 14 135 0 1 14-14 13.11 13.11 49.036 0 1 12.155 14v64.019a4.233 4.233 45 0 0 4.233 4.233h.202a4.233 4.233 135 0 0 4.233-4.233v-40.661a14 14 135 0 1 14-14 13.005 13.005 49.527 0 1 11.946 14L555.833 260a30.038 30.038 135.036 0 1-30.039 30H450a48.284 48.284 22.5 0 1-34.142-14.142l-37.801-37.801a15.702 15.702 86.658 0 1-1.217-20.839 14.154 14.154 173.662 0 1 19.896-2.21l21.76 17.2a2.181 2.181 154.16 0 0 3.534-1.712v-90.974a14.986 14.986 135 0 1 14.986-14.986z" transform="matrix(-.22259 0 0 .22259 123.725 -22.001)"/><path style="fill:#000;fill-opacity:.470966;stroke:none;stroke-width:1.06881px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M374.444 222.919c-1.992 5.044.075 11.54 3.91 15.375l37.503 37.563A48.284 48.284 0 0 0 450 290h75.794c16.569 0 30-13.431 30-30v-10c0 16.569-13.431 30-30 30H450a48.284 48.284 0 0 1-34.143-14.143l-38.377-38.313a13.61 13.61 0 0 1-3.036-4.625z" transform="matrix(-.22259 0 0 .22259 123.725 -22.001)"/></g></svg>
\ No newline at end of file diff --git a/panels/user-accounts/data/icons/print_error.svg b/panels/user-accounts/data/icons/print_error.svg new file mode 100644 index 0000000..5a9d22b --- /dev/null +++ b/panels/user-accounts/data/icons/print_error.svg @@ -0,0 +1 @@ +<svg version="1.0" width="48" height="48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="f"><stop style="stop-color:#fffffc;stop-opacity:1" offset="0"/><stop style="stop-color:#fffffc;stop-opacity:0" offset="1"/></linearGradient><linearGradient id="e"><stop style="stop-color:#73d216;stop-opacity:1" offset="0"/><stop offset=".315" style="stop-color:#73d216;stop-opacity:1"/><stop style="stop-color:#4e9a06;stop-opacity:1" offset="1"/></linearGradient><linearGradient id="c"><stop offset="0" style="stop-color:#fff;stop-opacity:1"/><stop offset="1" style="stop-color:#fff;stop-opacity:0"/></linearGradient><linearGradient id="b"><stop style="stop-color:#52a714;stop-opacity:1" offset="0"/><stop style="stop-color:#398800;stop-opacity:1" offset="1"/></linearGradient><linearGradient id="a"><stop offset="0" style="stop-color:#398800;stop-opacity:1"/><stop offset="1" style="stop-color:#84c706;stop-opacity:1"/></linearGradient><linearGradient id="d"><stop offset="0" style="stop-color:white;stop-opacity:1"/><stop offset="1" style="stop-color:white;stop-opacity:0"/></linearGradient></defs><path style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000;solid-opacity:1;vector-effect:none;fill:#77767b;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:56;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000" d="M17 3c-2.402-.025-4.81.57-6.977 1.781a2 2 0 0 0 1.95 3.492 9.994 9.994 0 0 1 9.941.102A9.991 9.991 0 0 1 26.855 17v1.145l3.739-3.739a13.986 13.986 0 0 0-6.657-9.484A13.986 13.986 0 0 0 17 3zM6.602 8.66a2 2 0 0 0-1.633.942 13.997 13.997 0 0 0-2.094 7.113v.004a2 2 0 0 0-.02.281v10.125a2 2 0 1 0 4 0V17c0-1.868.525-3.7 1.512-5.285A2 2 0 0 0 6.602 8.66zM16.855 9c-4.262 0-7.752 3.4-7.96 7.613a2 2 0 0 0-.04.387v2a2 2 0 1 0 4 0v-2c0-2.233 1.768-4 4-4 2.233 0 4 1.767 4 4v7.145l4-4V17a2 2 0 0 0-.039-.398C24.602 12.393 21.114 9 16.856 9zm0 6a2 2 0 0 0-2 2v10s0 1.19.29 2.64c.01.058.026.118.039.176l3.671-3.671V17a2 2 0 0 0-2-2zm-6 8a2 2 0 0 0-2 2v6a2 2 0 1 0 4 0v-6a2 2 0 0 0-2-2z"/><g style="display:inline;enable-background:new;fill:#ed333b;fill-opacity:1" transform="matrix(1.5 0 0 -1.5 6.149 1050.007)"><path style="display:inline;fill:#ed333b;fill-opacity:1;stroke-width:1.5;stroke-linecap:square;stroke-linejoin:round;enable-background:new" d="M19.794 668.005c-4.377 0-7.893 3.623-7.893 8a8 8 0 1 0 8-8h-.107zm-.56 3.346h1.334c.37 0 .668.297.668.667v1.335c0 .37-.298.667-.668.667h-1.335a.666.666 0 0 1-.667-.667v-1.335c0-.37.298-.667.667-.667zm-.666 3.987h2.666v6h-2.666z"/></g><path style="fill:none;stroke:#77767b;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m9 36 25-25"/></svg>
\ No newline at end of file diff --git a/panels/user-accounts/data/icons/print_ok.svg b/panels/user-accounts/data/icons/print_ok.svg new file mode 100644 index 0000000..4d84ff0 --- /dev/null +++ b/panels/user-accounts/data/icons/print_ok.svg @@ -0,0 +1 @@ +<svg version="1.0" width="48" height="48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="f"><stop style="stop-color:#fffffc;stop-opacity:1" offset="0"/><stop style="stop-color:#fffffc;stop-opacity:0" offset="1"/></linearGradient><linearGradient id="e"><stop style="stop-color:#73d216;stop-opacity:1" offset="0"/><stop offset=".315" style="stop-color:#73d216;stop-opacity:1"/><stop style="stop-color:#4e9a06;stop-opacity:1" offset="1"/></linearGradient><linearGradient id="c"><stop offset="0" style="stop-color:#fff;stop-opacity:1"/><stop offset="1" style="stop-color:#fff;stop-opacity:0"/></linearGradient><linearGradient id="b"><stop style="stop-color:#52a714;stop-opacity:1" offset="0"/><stop style="stop-color:#398800;stop-opacity:1" offset="1"/></linearGradient><linearGradient id="a"><stop offset="0" style="stop-color:#398800;stop-opacity:1"/><stop offset="1" style="stop-color:#84c706;stop-opacity:1"/></linearGradient><linearGradient id="d"><stop offset="0" style="stop-color:white;stop-opacity:1"/><stop offset="1" style="stop-color:white;stop-opacity:0"/></linearGradient></defs><path style="color:#000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000;solid-opacity:1;vector-effect:none;fill:#77767b;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:56;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000" d="M17 3c-2.402-.025-4.81.57-6.977 1.781a2 2 0 0 0 1.95 3.492 9.994 9.994 0 0 1 9.941.102A9.991 9.991 0 0 1 26.855 17v2a2 2 0 1 0 4 0v-2c0-.077-.006-.154-.015-.23a14.002 14.002 0 0 0-6.902-11.848A13.986 13.986 0 0 0 17 3ZM6.602 8.66a2 2 0 0 0-1.633.942 13.997 13.997 0 0 0-2.094 7.113v.004a2 2 0 0 0-.02.281v10.125a2 2 0 1 0 4 0V17c0-1.868.525-3.7 1.512-5.285A2 2 0 0 0 6.602 8.66ZM16.855 9c-4.262 0-7.752 3.4-7.96 7.613a2 2 0 0 0-.04.387v2a2 2 0 1 0 4 0v-2c0-2.233 1.768-4 4-4 2.233 0 4 1.767 4 4v10s.018.88.352 1.883c.172.515.432 1.107.81 1.728.66-1.69 1.621-3.247 2.838-4.593V17a2 2 0 0 0-.039-.398C24.602 12.393 21.114 9 16.856 9Zm0 6a2 2 0 0 0-2 2v10s0 1.19.29 2.64c.29 1.452.825 3.303 2.296 4.774a2 2 0 0 0 2.829-2.828c-.53-.529-.994-1.678-1.204-2.727-.21-1.048-.21-1.859-.21-1.859V17a2 2 0 0 0-2-2zm-6 8a2 2 0 0 0-2 2v6a2 2 0 1 0 4 0v-6a2 2 0 0 0-2-2z"/><path style="display:inline;fill:#26a269;fill-opacity:1;stroke-width:2.25;stroke-linecap:square;stroke-linejoin:round;enable-background:new" d="M36 24c-6.627 0-12 5.373-12 12 0 6.565 5.275 12 11.84 12H36c6.627 0 12-5.373 12-12s-5.373-12-12-12zm6.154 7.006a2 2 0 0 1 1.364.693 2 2 0 0 1-.217 2.819l-8.406 7.207-5.31-5.31a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l2.69 2.69 5.594-4.793a2 2 0 0 1 1.455-.476z"/></svg>
\ No newline at end of file diff --git a/panels/user-accounts/data/icons/right-index-finger.svg b/panels/user-accounts/data/icons/right-index-finger.svg new file mode 100644 index 0000000..856ffc9 --- /dev/null +++ b/panels/user-accounts/data/icons/right-index-finger.svg @@ -0,0 +1 @@ +<svg version="1.0" width="48" height="48" viewBox="0 0 40.425 46.214" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="a"><stop offset="0" style="stop-color:#fff;stop-opacity:1"/><stop style="stop-color:#27dc16;stop-opacity:1" offset="1"/></linearGradient></defs><circle style="fill:#8ff0a4;stroke:#2ec27e;stroke-width:.962791;stroke-miterlimit:4;stroke-dasharray:none;stop-color:#000" cx="-14.102" cy="9.203" r="5.783" transform="scale(-1 1)"/><g style="stroke-width:1.06881"><path style="fill:#241f31;stroke:none;stroke-width:1.06881px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M437.016 124.536h.029a14.986 14.986 45 0 1 14.985 14.986v54.153a4.504 4.504 45 0 0 4.504 4.504 4.216 4.216 130.946 0 0 3.908-4.504v-63.748a14 14 135 0 1 14-14 13.257 13.257 48.306 0 1 12.47 14v63.897a4.355 4.355 45 0 0 4.355 4.355 4.149 4.149 132.08 0 0 3.932-4.355v-63.897a14 14 135 0 1 14-14 13.11 13.11 49.036 0 1 12.155 14v64.019a4.233 4.233 45 0 0 4.233 4.233h.202a4.233 4.233 135 0 0 4.233-4.233v-40.661a14 14 135 0 1 14-14 13.005 13.005 49.527 0 1 11.946 14L555.833 260a30.038 30.038 135.036 0 1-30.039 30H450a48.284 48.284 22.5 0 1-34.142-14.142l-37.801-37.801a15.702 15.702 86.658 0 1-1.217-20.839 14.154 14.154 173.662 0 1 19.896-2.21l21.76 17.2a2.181 2.181 154.16 0 0 3.534-1.712v-90.974a14.986 14.986 135 0 1 14.986-14.986z" transform="matrix(.22259 0 0 .22259 -83.162 -22.001)"/><path style="fill:#000;fill-opacity:.470966;stroke:none;stroke-width:1.06881px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M374.444 222.919c-1.992 5.044.075 11.54 3.91 15.375l37.503 37.563A48.284 48.284 0 0 0 450 290h75.794c16.569 0 30-13.431 30-30v-10c0 16.569-13.431 30-30 30H450a48.284 48.284 0 0 1-34.143-14.143l-38.377-38.313a13.61 13.61 0 0 1-3.036-4.625z" transform="matrix(.22259 0 0 .22259 -83.162 -22.001)"/></g></svg>
\ No newline at end of file diff --git a/panels/user-accounts/data/icons/right-little-finger.svg b/panels/user-accounts/data/icons/right-little-finger.svg new file mode 100644 index 0000000..953c56d --- /dev/null +++ b/panels/user-accounts/data/icons/right-little-finger.svg @@ -0,0 +1 @@ +<svg version="1.0" width="48" height="48" viewBox="0 0 40.425 46.214" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="a"><stop offset="0" style="stop-color:#fff;stop-opacity:1"/><stop style="stop-color:#27dc16;stop-opacity:1" offset="1"/></linearGradient></defs><circle style="fill:#8ff0a4;stroke:#2ec27e;stroke-width:.962792;stroke-miterlimit:4;stroke-dasharray:none;stop-color:#000" cx="-37.541" cy="11.617" r="5.207" transform="scale(-1 1)"/><g style="stroke-width:1.06881"><path style="fill:#241f31;stroke:none;stroke-width:1.06881px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M437.016 124.536h.029a14.986 14.986 45 0 1 14.985 14.986v54.153a4.504 4.504 45 0 0 4.504 4.504 4.216 4.216 130.946 0 0 3.908-4.504v-63.748a14 14 135 0 1 14-14 13.257 13.257 48.306 0 1 12.47 14v63.897a4.355 4.355 45 0 0 4.355 4.355 4.149 4.149 132.08 0 0 3.932-4.355v-63.897a14 14 135 0 1 14-14 13.11 13.11 49.036 0 1 12.155 14v64.019a4.233 4.233 45 0 0 4.233 4.233h.202a4.233 4.233 135 0 0 4.233-4.233v-40.661a14 14 135 0 1 14-14 13.005 13.005 49.527 0 1 11.946 14L555.833 260a30.038 30.038 135.036 0 1-30.039 30H450a48.284 48.284 22.5 0 1-34.142-14.142l-37.801-37.801a15.702 15.702 86.658 0 1-1.217-20.839 14.154 14.154 173.662 0 1 19.896-2.21l21.76 17.2a2.181 2.181 154.16 0 0 3.534-1.712v-90.974a14.986 14.986 135 0 1 14.986-14.986z" transform="matrix(.22259 0 0 .22259 -83.332 -22.001)"/><path style="fill:#000;fill-opacity:.470966;stroke:none;stroke-width:1.06881px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M374.444 222.919c-1.992 5.044.075 11.54 3.91 15.375l37.503 37.563A48.284 48.284 0 0 0 450 290h75.794c16.569 0 30-13.431 30-30v-10c0 16.569-13.431 30-30 30H450a48.284 48.284 0 0 1-34.143-14.143l-38.377-38.313a13.61 13.61 0 0 1-3.036-4.625z" transform="matrix(.22259 0 0 .22259 -83.332 -22.001)"/></g></svg>
\ No newline at end of file diff --git a/panels/user-accounts/data/icons/right-middle-finger.svg b/panels/user-accounts/data/icons/right-middle-finger.svg new file mode 100644 index 0000000..3cf22e5 --- /dev/null +++ b/panels/user-accounts/data/icons/right-middle-finger.svg @@ -0,0 +1 @@ +<svg version="1.0" width="48" height="48" viewBox="0 0 40.425 46.214" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="a"><stop offset="0" style="stop-color:#fff;stop-opacity:1"/><stop style="stop-color:#27dc16;stop-opacity:1" offset="1"/></linearGradient></defs><circle style="fill:#8ff0a4;stroke:#2ec27e;stroke-width:.885327;stroke-miterlimit:4;stroke-dasharray:none;stop-color:#000" cx="-22.283" cy="6.839" r="5.318" transform="scale(-1 1)"/><g style="stroke-width:1.06881"><path style="fill:#241f31;stroke:none;stroke-width:1.06881px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M437.016 124.536h.029a14.986 14.986 45 0 1 14.985 14.986v54.153a4.504 4.504 45 0 0 4.504 4.504 4.216 4.216 130.946 0 0 3.908-4.504v-63.748a14 14 135 0 1 14-14 13.257 13.257 48.306 0 1 12.47 14v63.897a4.355 4.355 45 0 0 4.355 4.355 4.149 4.149 132.08 0 0 3.932-4.355v-63.897a14 14 135 0 1 14-14 13.11 13.11 49.036 0 1 12.155 14v64.019a4.233 4.233 45 0 0 4.233 4.233h.202a4.233 4.233 135 0 0 4.233-4.233v-40.661a14 14 135 0 1 14-14 13.005 13.005 49.527 0 1 11.946 14L555.833 260a30.038 30.038 135.036 0 1-30.039 30H450a48.284 48.284 22.5 0 1-34.142-14.142l-37.801-37.801a15.702 15.702 86.658 0 1-1.217-20.839 14.154 14.154 173.662 0 1 19.896-2.21l21.76 17.2a2.181 2.181 154.16 0 0 3.534-1.712v-90.974a14.986 14.986 135 0 1 14.986-14.986z" transform="matrix(.22259 0 0 .22259 -83.162 -22.001)"/><path style="fill:#000;fill-opacity:.470966;stroke:none;stroke-width:1.06881px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M374.444 222.919c-1.992 5.044.075 11.54 3.91 15.375l37.503 37.563A48.284 48.284 0 0 0 450 290h75.794c16.569 0 30-13.431 30-30v-10c0 16.569-13.431 30-30 30H450a48.284 48.284 0 0 1-34.143-14.143l-38.377-38.313a13.61 13.61 0 0 1-3.036-4.625z" transform="matrix(.22259 0 0 .22259 -83.162 -22.001)"/></g></svg>
\ No newline at end of file diff --git a/panels/user-accounts/data/icons/right-ring-finger.svg b/panels/user-accounts/data/icons/right-ring-finger.svg new file mode 100644 index 0000000..d237017 --- /dev/null +++ b/panels/user-accounts/data/icons/right-ring-finger.svg @@ -0,0 +1 @@ +<svg version="1.0" width="48" height="48" viewBox="0 0 40.425 46.214" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="a"><stop offset="0" style="stop-color:#fff;stop-opacity:1"/><stop style="stop-color:#27dc16;stop-opacity:1" offset="1"/></linearGradient></defs><circle style="fill:#8ff0a4;stroke:#2ec27e;stroke-width:.962792;stroke-miterlimit:4;stroke-dasharray:none;stop-color:#000" cx="-30.132" cy="6.745" r="5.247" transform="scale(-1 1)"/><g style="stroke-width:1.06881"><path style="fill:#241f31;stroke:none;stroke-width:1.06881px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M437.016 124.536h.029a14.986 14.986 45 0 1 14.985 14.986v54.153a4.504 4.504 45 0 0 4.504 4.504 4.216 4.216 130.946 0 0 3.908-4.504v-63.748a14 14 135 0 1 14-14 13.257 13.257 48.306 0 1 12.47 14v63.897a4.355 4.355 45 0 0 4.355 4.355 4.149 4.149 132.08 0 0 3.932-4.355v-63.897a14 14 135 0 1 14-14 13.11 13.11 49.036 0 1 12.155 14v64.019a4.233 4.233 45 0 0 4.233 4.233h.202a4.233 4.233 135 0 0 4.233-4.233v-40.661a14 14 135 0 1 14-14 13.005 13.005 49.527 0 1 11.946 14L555.833 260a30.038 30.038 135.036 0 1-30.039 30H450a48.284 48.284 22.5 0 1-34.142-14.142l-37.801-37.801a15.702 15.702 86.658 0 1-1.217-20.839 14.154 14.154 173.662 0 1 19.896-2.21l21.76 17.2a2.181 2.181 154.16 0 0 3.534-1.712v-90.974a14.986 14.986 135 0 1 14.986-14.986z" transform="matrix(.22259 0 0 .22259 -83.162 -22.001)"/><path style="fill:#000;fill-opacity:.470966;stroke:none;stroke-width:1.06881px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M374.444 222.919c-1.992 5.044.075 11.54 3.91 15.375l37.503 37.563A48.284 48.284 0 0 0 450 290h75.794c16.569 0 30-13.431 30-30v-10c0 16.569-13.431 30-30 30H450a48.284 48.284 0 0 1-34.143-14.143l-38.377-38.313a13.61 13.61 0 0 1-3.036-4.625z" transform="matrix(.22259 0 0 .22259 -83.162 -22.001)"/></g></svg>
\ No newline at end of file diff --git a/panels/user-accounts/data/icons/right-thumb.svg b/panels/user-accounts/data/icons/right-thumb.svg new file mode 100644 index 0000000..50639f2 --- /dev/null +++ b/panels/user-accounts/data/icons/right-thumb.svg @@ -0,0 +1 @@ +<svg version="1.0" width="48" height="48" viewBox="0 0 40.425 46.214" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="a"><stop offset="0" style="stop-color:#fff;stop-opacity:1"/><stop style="stop-color:#27dc16;stop-opacity:1" offset="1"/></linearGradient></defs><circle style="fill:#8ff0a4;stroke:#2ec27e;stroke-width:.949928;stroke-miterlimit:4;stroke-dasharray:none;stop-color:#000" cx="-3.418" cy="28.463" r="5.706" transform="scale(-1 1)"/><g style="stroke-width:1.06881"><path style="fill:#241f31;stroke:none;stroke-width:1.06881px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M437.016 124.536h.029a14.986 14.986 45 0 1 14.985 14.986v54.153a4.504 4.504 45 0 0 4.504 4.504 4.216 4.216 130.946 0 0 3.908-4.504v-63.748a14 14 135 0 1 14-14 13.257 13.257 48.306 0 1 12.47 14v63.897a4.355 4.355 45 0 0 4.355 4.355 4.149 4.149 132.08 0 0 3.932-4.355v-63.897a14 14 135 0 1 14-14 13.11 13.11 49.036 0 1 12.155 14v64.019a4.233 4.233 45 0 0 4.233 4.233h.202a4.233 4.233 135 0 0 4.233-4.233v-40.661a14 14 135 0 1 14-14 13.005 13.005 49.527 0 1 11.946 14L555.833 260a30.038 30.038 135.036 0 1-30.039 30H450a48.284 48.284 22.5 0 1-34.142-14.142l-37.801-37.801a15.702 15.702 86.658 0 1-1.217-20.839 14.154 14.154 173.662 0 1 19.896-2.21l21.76 17.2a2.181 2.181 154.16 0 0 3.534-1.712v-90.974a14.986 14.986 135 0 1 14.986-14.986z" transform="matrix(.22259 0 0 .22259 -83.281 -22.001)"/><path style="fill:#000;fill-opacity:.470966;stroke:none;stroke-width:1.06881px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M374.444 222.919c-1.992 5.044.075 11.54 3.91 15.375l37.503 37.563A48.284 48.284 0 0 0 450 290h75.794c16.569 0 30-13.431 30-30v-10c0 16.569-13.431 30-30 30H450a48.284 48.284 0 0 1-34.143-14.143l-38.377-38.313a13.61 13.61 0 0 1-3.036-4.625z" transform="matrix(.22259 0 0 .22259 -83.281 -22.001)"/></g></svg>
\ No newline at end of file diff --git a/panels/user-accounts/data/join-dialog.ui b/panels/user-accounts/data/join-dialog.ui new file mode 100644 index 0000000..4da78e2 --- /dev/null +++ b/panels/user-accounts/data/join-dialog.ui @@ -0,0 +1,166 @@ +<?xml version="1.0"?> +<interface> + <!-- interface-requires gtk+ 3.8 --> + <object class="GtkDialog" id="join-dialog"> + <property name="can_focus">False</property> + <property name="resizable">False</property> + <property name="modal">True</property> + <property name="destroy_with_parent">True</property> + <property name="title" translatable="yes">Add User</property> + <property name="use_header_bar">1</property> + <child internal-child="headerbar"> + <object class="GtkHeaderBar" id="join-dialog-header-bar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child type="start"> + <object class="GtkButton" id="button1"> + <property name="label" translatable="yes">_Cancel</property> + <property name="visible">True</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="use_underline">True</property> + <property name="valign">center</property> + <style> + <class name="text-button"/> + </style> + </object> + </child> + <child type="end"> + <object class="GtkButton" id="button2"> + <property name="label" translatable="yes" comments="Translators: This button enrolls the computer in the domain in order to use enterprise logins.">_Enroll</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="use_underline">True</property> + <property name="valign">center</property> + <style> + <class name="text-button"/> + <class name="suggested-action"/> + </style> + </object> + </child> + </object> + </child> + <child> + <object class="GtkBox" id="dialog-vbox1"> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">2</property> + <child> + <object class="GtkBox" id="box2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">10</property> + <child> + <object class="GtkLabel" id="label71"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Domain Administrator Login</property> + <attributes> + <attribute name="weight" value="bold"/> + <attribute name="scale" value="1.2"/> + </attributes> + </object> + </child> + <child> + <object class="GtkLabel" id="label12"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="yalign">0</property> + <property name="label" translatable="yes">In order to use enterprise logins, this computer needs to be +enrolled in the domain. Please have your network administrator +type their domain password here.</property> + </object> + </child> + <child> + <object class="GtkGrid" id="grid1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_start">12</property> + <property name="hexpand">True</property> + <property name="row_spacing">6</property> + <property name="column_spacing">12</property> + <child> + <object class="GtkLabel" id="label13"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">1</property> + <property name="label" translatable="yes">_Domain</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">join-domain</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="join-domain"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">5</property> + <property name="margin_bottom">5</property> + <property name="xalign">0</property> + </object> + </child> + <child> + <object class="GtkLabel" id="label14"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">1</property> + <property name="label" translatable="yes">Administrator _Name</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">join-name</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkEntry" id="join-name"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + <property name="invisible_char">●</property> + <property name="invisible_char_set">True</property> + <property name="activates_default">True</property> + </object> + </child> + <child> + <object class="GtkLabel" id="label15"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">1</property> + <property name="label" translatable="yes">Administrator Password</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">join-password</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkEntry" id="join-password"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + <property name="visibility">False</property> + <property name="invisible_char">●</property> + <property name="activates_default">True</property> + <property name="invisible_char_set">True</property> + <property name="input_purpose">password</property> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + <action-widgets> + <action-widget response="-6">button1</action-widget> + <action-widget response="-5">button2</action-widget> + </action-widgets> + </object> +</interface> diff --git a/panels/user-accounts/data/net.reactivated.Fprint.Device.xml b/panels/user-accounts/data/net.reactivated.Fprint.Device.xml new file mode 100644 index 0000000..786d89c --- /dev/null +++ b/panels/user-accounts/data/net.reactivated.Fprint.Device.xml @@ -0,0 +1,585 @@ +<!DOCTYPE node PUBLIC +"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" +"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd" [ +<!ENTITY ERROR_CLAIM_DEVICE "net.reactivated.Fprint.Error.ClaimDevice"> +<!ENTITY ERROR_ALREADY_IN_USE "net.reactivated.Fprint.Error.AlreadyInUse"> +<!ENTITY ERROR_INTERNAL "net.reactivated.Fprint.Error.Internal"> +<!ENTITY ERROR_PERMISSION_DENIED "net.reactivated.Fprint.Error.PermissionDenied"> +<!ENTITY ERROR_NO_ENROLLED_PRINTS "net.reactivated.Fprint.Error.NoEnrolledPrints"> +<!ENTITY ERROR_NO_ACTION_IN_PROGRESS "net.reactivated.Fprint.Error.NoActionInProgress"> +<!ENTITY ERROR_INVALID_FINGERNAME "net.reactivated.Fprint.Error.InvalidFingername"> +]> + +<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd"> + <interface name="net.reactivated.Fprint.Device"> + value="fprint_device" /> + + <doc:doc> + <doc:title id="polkit-integration"> + PolicyKit integration + </doc:title> + <doc:para> + fprintd uses PolicyKit to check whether users are allowed to access fingerprint data, or the + fingerprint readers itself. + <doc:list> + <doc:item> + <doc:term>net.reactivated.fprint.device.verify</doc:term> + <doc:definition> + Whether the user is allowed to verify fingers against saved fingerprints. + </doc:definition> + </doc:item> + <doc:item> + <doc:term>net.reactivated.fprint.device.enroll</doc:term> + <doc:definition> + Whether the user is allowed to enroll new fingerprints. + </doc:definition> + </doc:item> + <doc:item> + <doc:term>net.reactivated.fprint.device.setusername</doc:term> + <doc:definition> + Whether the user is allowed to query, verify, or enroll fingerprints for users other than itself. + </doc:definition> + </doc:item> + </doc:list> + </doc:para> + + <doc:title id="usernames"> + Usernames + </doc:title> + <doc:para> + When a username argument is used for a method, a PolicyKit check is done on the + <doc:tt>net.reactivated.fprint.device.setusername</doc:tt> PolicyKit + action to see whether the user the client is running as is allowed to access data from other users. + </doc:para> + <doc:para> + By default, only root is allowed to access fingerprint data for users other than itself. For a normal user, + it is recommended that you use an empty string for the username, which will mean "the client the user is + running as". + </doc:para> + <doc:para> + See <doc:ref type="description" to="polkit-integration">PolicyKit integration</doc:ref>. + </doc:para> + + <doc:title id="fingerprint-names"> + Fingerprint names + </doc:title> + <doc:para> + When a finger name argument is used for a method, it refers to either a single finger, or + "any" finger. See the list of possible values below: + <doc:list> + <doc:item> + <doc:term>left-thumb</doc:term> + <doc:definition> + Left thumb + </doc:definition> + </doc:item> + <doc:item> + <doc:term>left-index-finger</doc:term> + <doc:definition> + Left index finger + </doc:definition> + </doc:item> + <doc:item> + <doc:term>left-middle-finger</doc:term> + <doc:definition> + Left middle finger + </doc:definition> + </doc:item> + <doc:item> + <doc:term>left-ring-finger</doc:term> + <doc:definition> + Left ring finger + </doc:definition> + </doc:item> + <doc:item> + <doc:term>left-little-finger</doc:term> + <doc:definition> + Left little finger + </doc:definition> + </doc:item> + <doc:item> + <doc:term>right-thumb</doc:term> + <doc:definition> + Right thumb + </doc:definition> + </doc:item> + <doc:item> + <doc:term>right-index-finger</doc:term> + <doc:definition> + Right index finger + </doc:definition> + </doc:item> + <doc:item> + <doc:term>right-middle-finger</doc:term> + <doc:definition> + Right middle finger + </doc:definition> + </doc:item> + <doc:item> + <doc:term>right-ring-finger</doc:term> + <doc:definition> + Right ring finger + </doc:definition> + </doc:item> + <doc:item> + <doc:term>right-little-finger</doc:term> + <doc:definition> + Right little finger + </doc:definition> + </doc:item> + <doc:item> + <doc:term>any</doc:term> + <doc:definition> + Any finger. This is only used for <doc:ref type="method" to="Device.VerifyStart">Device.VerifyStart</doc:ref> + (select the first finger with a fingerprint associated, or all the fingerprints available for the user when + the device supports it) and <doc:ref type="signal" to="Device::VerifyFingerSelected">Device::VerifyFingerSelected</doc:ref> + (any finger with an associated fingerprint can be used). + </doc:definition> + </doc:item> + </doc:list> + </doc:para> + + <doc:title id="verify-statuses"> + Verify Statuses + </doc:title> + <doc:para> + <doc:list> + Possible values for the result passed through <doc:ref type="signal" to="Device::VerifyResult">Device::VerifyResult</doc:ref> are: + <doc:item> + <doc:term>verify-no-match</doc:term> + <doc:definition> + The verification did not match, <doc:ref type="method" to="Device.VerifyStop">Device.VerifyStop</doc:ref> should now be called. + </doc:definition> + </doc:item> + <doc:item> + <doc:term>verify-match</doc:term> + <doc:definition> + The verification succeeded, <doc:ref type="method" to="Device.VerifyStop">Device.VerifyStop</doc:ref> should now be called. + </doc:definition> + </doc:item> + <doc:item> + <doc:term>verify-retry-scan</doc:term> + <doc:definition> + The user should retry scanning their finger, the verification is still ongoing. + </doc:definition> + </doc:item> + <doc:item> + <doc:term>verify-swipe-too-short</doc:term> + <doc:definition> + The user's swipe was too short. The user should retry scanning their finger, the verification is still ongoing. + </doc:definition> + </doc:item> + <doc:item> + <doc:term>verify-finger-not-centered</doc:term> + <doc:definition> + The user's finger was not centered on the reader. The user should retry scanning their finger, the verification is still ongoing. + </doc:definition> + </doc:item> + <doc:item> + <doc:term>verify-remove-and-retry</doc:term> + <doc:definition> + The user should remove their finger from the reader and retry scanning their finger, the verification is still ongoing. + </doc:definition> + </doc:item> + <doc:item> + <doc:term>verify-disconnected</doc:term> + <doc:definition> + The device was disconnected during the verification, no other actions should be taken, and you shouldn't use the device any more. + </doc:definition> + </doc:item> + <doc:item> + <doc:term>verify-unknown-error</doc:term> + <doc:definition> + An unknown error occurred (usually a driver problem), <doc:ref type="method" to="Device.VerifyStop">Device.VerifyStop</doc:ref> should now be called. + </doc:definition> + </doc:item> + </doc:list> + </doc:para> + + <doc:title id="enroll-statuses"> + Enroll Statuses + </doc:title> + <doc:para> + <doc:list> + Possible values for the result passed through <doc:ref type="signal" to="Device::EnrollResult">Device::EnrollResult</doc:ref> are: + <doc:item> + <doc:term>enroll-completed</doc:term> + <doc:definition> + The enrollment successfully completed, <doc:ref type="method" to="Device.EnrollStop">Device.EnrollStop</doc:ref> should now be called. + </doc:definition> + </doc:item> + <doc:item> + <doc:term>enroll-failed</doc:term> + <doc:definition> + The enrollment failed, <doc:ref type="method" to="Device.EnrollStop">Device.EnrollStop</doc:ref> should now be called. + </doc:definition> + </doc:item> + <doc:item> + <doc:term>enroll-stage-passed</doc:term> + <doc:definition> + One stage of the enrollment passed, the enrollment is still ongoing. + </doc:definition> + </doc:item> + <doc:item> + <doc:term>enroll-retry-scan</doc:term> + <doc:definition> + The user should retry scanning their finger, the enrollment is still ongoing. + </doc:definition> + </doc:item> + <doc:item> + <doc:term>enroll-swipe-too-short</doc:term> + <doc:definition> + The user's swipe was too short. The user should retry scanning their finger, the enrollment is still ongoing. + </doc:definition> + </doc:item> + <doc:item> + <doc:term>enroll-finger-not-centered</doc:term> + <doc:definition> + The user's finger was not centered on the reader. The user should retry scanning their finger, the enrollment is still ongoing. + </doc:definition> + </doc:item> + <doc:item> + <doc:term>enroll-remove-and-retry</doc:term> + <doc:definition> + The user should remove their finger from the reader and retry scanning their finger, the enrollment is still ongoing. + </doc:definition> + </doc:item> + <doc:item> + <doc:term>enroll-data-full</doc:term> + <doc:definition> + No further prints can be enrolled on this device, <doc:ref type="method" to="Device.EnrollStop">Device.EnrollStop</doc:ref> should now be called. + + <doc:ref type="method" to="DeleteEnrolledFingers2">Delete other prints</doc:ref> from the device first to continue + (e.g. from other users). Note that old prints or prints from other operating systems may be deleted automatically + to resolve this error without any notification. + </doc:definition> + </doc:item> + <doc:item> + <doc:term>enroll-disconnected</doc:term> + <doc:definition> + The device was disconnected during the enrollment, no other actions should be taken, and you shouldn't use the device any more. + + </doc:definition> + </doc:item> + <doc:item> + <doc:term>enroll-unknown-error</doc:term> + <doc:definition> + An unknown error occurred (usually a driver problem), <doc:ref type="method" to="Device.EnrollStop">Device.EnrollStop</doc:ref> should now be called. + + </doc:definition> + </doc:item> + </doc:list> + </doc:para> + </doc:doc> + + <!-- ************************************************************ --> + + <method name="ListEnrolledFingers"> + <arg type="s" name="username" direction="in"> + <doc:doc><doc:summary>The username for whom to list the enrolled fingerprints. See <doc:ref type="description" to="usernames">Usernames</doc:ref>.</doc:summary></doc:doc> + </arg> + <arg type="as" name="enrolled_fingers" direction="out"> + <doc:doc><doc:summary>An array of strings representing the enrolled fingerprints. See <doc:ref type="description" to="fingerprint-names">Fingerprint names</doc:ref>.</doc:summary></doc:doc> + </arg> + + <doc:doc> + <doc:description> + <doc:para> + List all the enrolled fingerprints for the chosen user. + </doc:para> + </doc:description> + + <doc:errors> + <doc:error name="&ERROR_PERMISSION_DENIED;">if the caller lacks the appropriate PolicyKit authorization</doc:error> + <doc:error name="&ERROR_NO_ENROLLED_PRINTS;">if the chosen user doesn't have any fingerprints enrolled</doc:error> + </doc:errors> + </doc:doc> + </method> + + <!-- ************************************************************ --> + + <method name="DeleteEnrolledFingers"> + <arg type="s" name="username" direction="in"> + <doc:doc><doc:summary>The username for whom to delete the enrolled fingerprints. See <doc:ref type="description" to="usernames">Usernames</doc:ref>.</doc:summary></doc:doc> + </arg> + + <doc:doc> + <doc:description> + <doc:para> + Delete all the enrolled fingerprints for the chosen user. + </doc:para> + <doc:para> + This call only exists for compatibility reasons, you should instead claim the device using + <doc:ref type="method" to="Device.Claim">Device.Claim</doc:ref> and then call + <doc:ref type="method" to="DeleteEnrolledFingers2">DeleteEnrolledFingers2</doc:ref>. + </doc:para> + </doc:description> + + <doc:errors> + <doc:error name="&ERROR_PERMISSION_DENIED;">if the caller lacks the appropriate PolicyKit authorization</doc:error> + </doc:errors> + </doc:doc> + </method> + + <!-- ************************************************************ --> + + <method name="DeleteEnrolledFingers2"> + <doc:doc> + <doc:description> + <doc:para> + Delete all the enrolled fingerprints for the user currently claiming the device with <doc:ref type="method" to="Device.Claim">Device.Claim</doc:ref>. + </doc:para> + </doc:description> + + <doc:errors> + <doc:error name="&ERROR_PERMISSION_DENIED;">if the caller lacks the appropriate PolicyKit authorization</doc:error> + </doc:errors> + </doc:doc> + </method> + + <!-- ************************************************************ --> + + <method name="Claim"> + <arg type="s" name="username" direction="in"> + <doc:doc><doc:summary>The username for whom to claim the device. See <doc:ref type="description" to="usernames">Usernames</doc:ref>.</doc:summary></doc:doc> + </arg> + + <doc:doc> + <doc:description> + <doc:para> + Claim the device for the chosen user. + </doc:para> + </doc:description> + + <doc:errors> + <doc:error name="&ERROR_PERMISSION_DENIED;">if the caller lacks the appropriate PolicyKit authorization</doc:error> + <doc:error name="&ERROR_ALREADY_IN_USE;">if the device is already claimed</doc:error> + <doc:error name="&ERROR_INTERNAL;">if the device couldn't be claimed</doc:error> + </doc:errors> + </doc:doc> + </method> + + <!-- ************************************************************ --> + + <method name="Release"> + <doc:doc> + <doc:description> + <doc:para> + Release a device claimed with <doc:ref type="method" to="Device.Claim">Device.Claim</doc:ref>. + </doc:para> + </doc:description> + + <doc:errors> + <doc:error name="&ERROR_PERMISSION_DENIED;">if the caller lacks the appropriate PolicyKit authorization</doc:error> + <doc:error name="&ERROR_CLAIM_DEVICE;">if the device was not claimed</doc:error> + </doc:errors> + </doc:doc> + </method> + + <!-- ************************************************************ --> + + <method name="VerifyStart"> + <arg type="s" name="finger_name" direction="in"> + <doc:doc><doc:summary>A string representing the finger to verify. See <doc:ref type="description" to="fingerprint-names">Fingerprint names</doc:ref>.</doc:summary></doc:doc> + </arg> + + <doc:doc> + <doc:description> + <doc:para> + Check the chosen finger against a saved fingerprint. You need to have claimed the device using + <doc:ref type="method" to="Device.Claim">Device.Claim</doc:ref>. The finger selected is sent to the front-end + using <doc:ref type="signal" to="Device::VerifyFingerSelected">Device::VerifyFingerSelected</doc:ref> and + verification status through <doc:ref type="signal" to="Device::VerifyStatus">Device::VerifyStatus</doc:ref>. + </doc:para> + </doc:description> + + <doc:errors> + <doc:error name="&ERROR_PERMISSION_DENIED;">if the caller lacks the appropriate PolicyKit authorization</doc:error> + <doc:error name="&ERROR_CLAIM_DEVICE;">if the device was not claimed</doc:error> + <doc:error name="&ERROR_ALREADY_IN_USE;">if the device was already being used</doc:error> + <doc:error name="&ERROR_NO_ENROLLED_PRINTS;">if there are no enrolled prints for the chosen user</doc:error> + <doc:error name="&ERROR_INTERNAL;">if there was an internal error</doc:error> + </doc:errors> + </doc:doc> + </method> + + <!-- ************************************************************ --> + + <method name="VerifyStop"> + <doc:doc> + <doc:description> + <doc:para> + Stop an on-going fingerprint verification started with <doc:ref type="method" to="Device.VerifyStart">Device.VerifyStart</doc:ref>. + </doc:para> + </doc:description> + + <doc:errors> + <doc:error name="&ERROR_PERMISSION_DENIED;">if the caller lacks the appropriate PolicyKit authorization</doc:error> + <doc:error name="&ERROR_CLAIM_DEVICE;">if the device was not claimed</doc:error> + <doc:error name="&ERROR_NO_ACTION_IN_PROGRESS;">if there was no ongoing verification</doc:error> + <doc:error name="&ERROR_INTERNAL;">if there was an internal error</doc:error> + </doc:errors> + </doc:doc> + </method> + + <!-- ************************************************************ --> + + <signal name="VerifyFingerSelected"> + <arg type="s" name="finger_name"> + <doc:doc> + <doc:summary> + <doc:para> + A string representing the finger select to be verified. + </doc:para> + </doc:summary> + </doc:doc> + </arg> + <doc:doc> + <doc:seealso> + <doc:ref type="description" to="fingerprint-names">Fingerprint names</doc:ref>. + </doc:seealso> + </doc:doc> + </signal> + + <!-- ************************************************************ --> + + <signal name="VerifyStatus"> + <arg type="s" name="result"> + <doc:doc> + <doc:summary> + A string representing the status of the verification. + </doc:summary> + </doc:doc> + </arg> + + <arg type="b" name="done"> + <doc:doc> + <doc:summary> + Whether the verification finished and can be stopped. + </doc:summary> + </doc:doc> + </arg> + + <doc:doc> + <doc:seealso> + <doc:ref type="description" to="verify-statuses">Verify Statuses</doc:ref> and <doc:ref type="method" to="Device.VerifyStop">Device.VerifyStop</doc:ref>. + </doc:seealso> + </doc:doc> + </signal> + + <!-- ************************************************************ --> + + <method name="EnrollStart"> + <arg type="s" name="finger_name" direction="in"> + <doc:doc><doc:summary>A string representing the finger to enroll. See + <doc:ref type="description" to="fingerprint-names">Fingerprint names</doc:ref>. + Note that "any" is not a valid finger name for this method.</doc:summary></doc:doc> + </arg> + + <doc:doc> + <doc:description> + <doc:para> + Start enrollment for the selected finger. You need to have claimed the device using + <doc:ref type="method" to="Device.Claim">Device.Claim</doc:ref> before calling + this method. Enrollment status is sent through <doc:ref type="signal" to="Device::EnrollStatus">Device::EnrollStatus</doc:ref>. + </doc:para> + </doc:description> + + <doc:errors> + <doc:error name="&ERROR_PERMISSION_DENIED;">if the caller lacks the appropriate PolicyKit authorization</doc:error> + <doc:error name="&ERROR_CLAIM_DEVICE;">if the device was not claimed</doc:error> + <doc:error name="&ERROR_ALREADY_IN_USE;">if the device was already being used</doc:error> + <doc:error name="&ERROR_INVALID_FINGERNAME;">if the finger name passed is invalid</doc:error> + <doc:error name="&ERROR_INTERNAL;">if there was an internal error</doc:error> + </doc:errors> + + </doc:doc> + </method> + + <!-- ************************************************************ --> + + <method name="EnrollStop"> + <doc:doc> + <doc:description> + <doc:para> + Stop an on-going fingerprint enrollment started with <doc:ref type="method" to="Device.EnrollStart">Device.EnrollStart</doc:ref>. + </doc:para> + </doc:description> + + <doc:errors> + <doc:error name="&ERROR_PERMISSION_DENIED;">if the caller lacks the appropriate PolicyKit authorization</doc:error> + <doc:error name="&ERROR_CLAIM_DEVICE;">if the device was not claimed</doc:error> + <doc:error name="&ERROR_NO_ACTION_IN_PROGRESS;">if there was no ongoing verification</doc:error> + <doc:error name="&ERROR_INTERNAL;">if there was an internal error</doc:error> + </doc:errors> + </doc:doc> + </method> + + <!-- ************************************************************ --> + + <signal name="EnrollStatus"> + <arg type="s" name="result"> + <doc:doc> + <doc:summary> + A string representing the status of the enrollment. + </doc:summary> + </doc:doc> + </arg> + + <arg type="b" name="done"> + <doc:doc> + <doc:summary> + Whether the enrollment finished and can be stopped. + </doc:summary> + </doc:doc> + </arg> + + <doc:doc> + <doc:seealso> + <doc:ref type="description" to="enroll-statuses">Enrollment Statuses</doc:ref> and <doc:ref type="method" to="Device.EnrollStop">Device.EnrollStop</doc:ref>. + </doc:seealso> + </doc:doc> + </signal> + + <!-- ************************************************************ --> + + <property name="name" type="s" access="read"> + <doc:doc> + <doc:description> + <doc:para> + The product name of the device. + </doc:para> + </doc:description> + </doc:doc> + </property> + + <!-- ************************************************************ --> + + <property name="num-enroll-stages" type="i" access="read"> + <doc:doc> + <doc:description> + <doc:para> + The number of enrollment stages for the device. This is only available when the device has been claimed, otherwise it will be undefined (-1). + </doc:para> + <doc:seealso> + <doc:ref type="method" to="Device.Claim">Device.Claim</doc:ref> and <doc:ref type="method" to="Device.EnrollStart">Device.EnrollStart</doc:ref>. + </doc:seealso> + </doc:description> + </doc:doc> + </property> + + <!-- ************************************************************ --> + + <property name="scan-type" type="s" access="read"> + <doc:doc> + <doc:description> + <doc:para> + The scan type of the device, either "press" if you place your finger on the device, or "swipe" if you have to swipe your finger. + </doc:para> + </doc:description> + </doc:doc> + </property> + + </interface> +</node> + diff --git a/panels/user-accounts/data/net.reactivated.Fprint.Manager.xml b/panels/user-accounts/data/net.reactivated.Fprint.Manager.xml new file mode 100644 index 0000000..f4a38c7 --- /dev/null +++ b/panels/user-accounts/data/net.reactivated.Fprint.Manager.xml @@ -0,0 +1,50 @@ +<!DOCTYPE node PUBLIC +"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" +"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd" [ +<!ENTITY ERROR_NO_SUCH_DEVICE "net.reactivated.Fprint.Error.NoSuchDevice"> +]> +<node name="/" xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd"> + <interface name="net.reactivated.Fprint.Manager"> + <annotation name="org.freedesktop.DBus.GLib.CSymbol" + value="fprint_manager" /> + + <!-- ************************************************************ --> + + <method name="GetDevices"> + <arg type="ao" name="devices" direction="out"> + <doc:doc><doc:summary>An array of object paths for devices.</doc:summary></doc:doc> + </arg> + + <doc:doc> + <doc:description> + <doc:para> + Enumerate all the fingerprint readers attached to the system. If there are + no devices available, an empty array is returned. + </doc:para> + </doc:description> + </doc:doc> + </method> + + <!-- ************************************************************ --> + + <method name="GetDefaultDevice"> + <arg type="o" name="device" direction="out"> + <doc:doc><doc:summary>The object path for the default device.</doc:summary></doc:doc> + </arg> + + <doc:doc> + <doc:description> + <doc:para> + Returns the default fingerprint reader device. + </doc:para> + </doc:description> + + <doc:errors> + <doc:error name="&ERROR_NO_SUCH_DEVICE;">if the device does not exist</doc:error> + </doc:errors> + </doc:doc> + </method> + + </interface> +</node> + diff --git a/panels/user-accounts/data/org.freedesktop.realmd.xml b/panels/user-accounts/data/org.freedesktop.realmd.xml new file mode 100644 index 0000000..316213a --- /dev/null +++ b/panels/user-accounts/data/org.freedesktop.realmd.xml @@ -0,0 +1,666 @@ +<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" + "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> +<node name="/"> + + <!-- + org.freedesktop.realmd.Provider: + @short_description: a realm provider + + Various realm providers represent different software implementations + that provide access to realms or domains. + + This interface is implemented by individual providers, but is + aggregated globally at the system bus name + <literal>org.freedesktop.realmd</literal> + with the object path <literal>/org/freedesktop/realmd</literal> + --> + <interface name="org.freedesktop.realmd.Provider"> + + <!-- + Name: the name of the provider + + The name of the provider. This is not normally displayed + to the user, but may be useful for diagnostics or debugging. + --> + <property name="Name" type="s" access="read"/> + + <!-- + Version: the version of the provider + + The version of the provider. This is not normally used in + logic, but may be useful for diagnostics or debugging. + --> + <property name="Version" type="s" access="read"/> + + <!-- + Realms: a list of realms + + A list of known, enrolled or discovered realms. All realms + that this provider knows about are listed here. As realms + are discovered they are added to this list. + + Each realm is represented by the DBus object path of the + realm object. + --> + <property name="Realms" type="ao" access="read"/> + + <!-- + Discover: + @string: an input string to discover realms for + @options: options for the discovery operation + @relevance: the relevance of the returned results + @realm: a list of realms discovered + + Discover realms for the given string. The input @string is + usually a domain or realm name, perhaps typed by a user. If + an empty string is provided the realm provider should try to + discover a default realm if possible (eg: from DHCP). + + @options can contain, but is not limited to, the following values: + <itemizedlist> + <listitem><para><literal>operation</literal>: a string + identifier chosen by the client, which can then later be + passed to org.freedesktop.realmd.Service.Cancel() in order + to cancel the operation</para></listitem> + </itemizedlist> + + The @relevance returned can be used to rank results from + different discover calls to different providers. Implementors + should return a positive number if the provider highly + recommends that the realms be handled by this provider, + or a zero if it can possibly handle the realms. Negative + should be returned if no realms are found. + + This method does not return an error when no realms are + discovered. It simply returns an @realm list. + + To see diagnostic information about the discovery process + connect to the org.freedesktop.realmd.Service::Diagnostics + signal. + + This method requires authorization for the PolicyKit action + called <literal>org.freedesktop.realmd.discover-realm</literal>. + + In addition to common DBus error results, this method may + return: + <itemizedlist> + <listitem><para><literal>org.freedesktop.realmd.Error.Failed</literal>: + may be returned if the discovery could not be run for some reason.</para></listitem> + <listitem><para><literal>org.freedesktop.realmd.Error.Cancelled</literal>: + returned if the operation was cancelled.</para></listitem> + <listitem><para><literal>org.freedesktop.realmd.Error.NotAuthorized</literal>: + returned if the calling client is not permitted to perform a discovery + operation.</para></listitem> + </itemizedlist> + --> + <method name="Discover"> + <arg name="string" type="s" direction="in"/> + <arg name="options" type="a{sv}" direction="in"/> + <arg name="relevance" type="i" direction="out"/> + <arg name="realm" type="ao" direction="out"/> + </method> + + </interface> + + <!-- + org.freedesktop.realmd.Service: + @short_description: the realmd service + + Global calls for managing the realmd service. Usually you'll want + to use #org.freedesktop.realmd.Provider instead. + + This interface is implemented by the realmd service, and is always + available at the object path <literal>/org/freedesktop/realmd</literal> + + The service also implements the + <literal>org.freedesktop.DBus.ObjectManager</literal> interface which + makes it easy to retrieve all realmd objects and properties in one go. + --> + <interface name="org.freedesktop.realmd.Service"> + + <!-- + Cancel: + @operation: the operation to cancel + + Cancel a realmd operation. To be able to cancel an operation + pass a uniquely chosen <literal>operation</literal> string + identifier as an option in the methods <literal>options</literal> + argument. + + These operation string identifiers should be unique per client + calling the realmd service. + + It is not guaranteed that the service can or will cancel the + operation. For example the operation may have already completed + by the time this method is handled. The caller of the operation + method will receive a + <literal>org.freedesktop.realmd.Error.Cancelled</literal> + if the operation was cancelled. + --> + <method name="Cancel"> + <arg name="operation" type="s" direction="in"/> + </method> + + <!-- + SetLocale: + @locale: the locale for the client + + Set the language @locale for the client. This locale is used + for error messages. The locale is used until the next time + this method is called, the client disconnects, or the client + calls #org.freedesktop.realmd.Service.Release(). + --> + <method name="SetLocale"> + <arg name="locale" type="s" direction="in"/> + </method> + + <!-- + Diagnostics: + @data: diagnostic data + @operation: the operation this data resulted from + + This signal is fired when diagnostics result from an operation + in the provider or one of its realms. + + It is not guaranteed that this signal is emitted once per line. + More than one line may be contained in @data, or a partial + line. New line characters are embedded in @data. + + This signal is sent explicitly to the client which invoked + operation method. In order to tell which operation this + diagnostic data results from, pass a unique + <literal>operation</literal> string identifier in the + <literal>options</literal> argument of the operation method. + That same identifier will be passed back via the @operation + argument of this signal. + --> + <signal name="Diagnostics"> + <arg name="data" type="s"/> + <arg name="operation" type="s"/> + </signal> + + <!-- + Release: + + Normally realmd waits until all clients have disconnected + before exiting itself, sometime later. For long lived clients + they can call this method to allow the realmd service to quit. + This is an optimization. The daemon will not exit immediately. + It is safe to call this multiple times. + --> + <method name="Release"> + <!-- no arguments --> + </method> + + </interface> + + <!-- + org.freedesktop.realmd.Realm: + @short_description: a realm + + Represents one realm. + + Contains generic information about a realm, and useful properties for + introspecting what kind of realm this is and how to work with + the realm. + + Use #org.freedesktop.realmd.Provider:Realms or + #org.freedesktop.realmd.Provider.Discover() to get access to some + kerberos realm objects. + + Realms will always implement additional interfaces, such as + #org.freedesktop.realmd.Kerberos. Do not assume that all realms + implement that kerberos interface. Use the + #org.freedesktop.realmd.Realm:SupportedInterfaces property to see + which interfaces are set. + + Different realms support various ways to configure them on the + system. Use the #org.freedesktop.realmd.Realm:Configured property + to determine if a realm is configured. If it is configured the + property will be set to the interface of the mechanism that was + used to configure it. + + To configure a realm, look in the + #org.freedesktop.realmd.Realm:SupportedInterfaces property for a + recognized purpose specific interface that can be used for + configuration, such as the + #org.freedesktop.realmd.KerberosMembership interface and its + #org.freedesktop.realmd.KerberosMembership.Join() method. + + To deconfigure a realm from the current system, you can use the + #org.freedesktop.realmd.Realm.Deconfigure() method. In additon some + of the configuration specific interfaces provide methods to + deconfigure a realm in a specific way, such as + #org.freedesktop.realmd.KerberosMembership.Leave() method. + + The various properties are guaranteed to have been updated before + the operation methods return, if they change state. + --> + <interface name="org.freedesktop.realmd.Realm"> + + <!-- + Name: the realm name + + This is the name of the realm, appropriate for display to + end users where necessary. + --> + <property name="Name" type="s" access="read"/> + + <!-- + Configured: whether this domain is configured and how + + If this property is an empty string, then the realm is not + configured. Otherwise the realm is configured, and contains + a string which is the interface that represents how it was + configured, for example #org.freedesktop.realmd.KerberosMembership. + --> + <property name="Configured" type="s" access="read"/> + + <!-- + Deconfigure: deconfigure this realm + + Deconfigure this realm from the local machine with standard + default behavior. + + The behavior of this method depends on the which configuration + interface is present in the + #org.freedesktop.realmd.Realm.Configured property. It does not + always delete membership accounts in the realm, but just + reconfigures the local machine so it no longer is configured + for the given realm. In some cases the implementation may try + to update membership accounts, but this is not guaranteed. + + Various configuration interfaces may support more specific ways + to deconfigure a realm in a specific way, such as the + #org.freedesktop.realmd.KerberosMembership.Leave() method. + + @options can contain, but is not limited to, the following values: + <itemizedlist> + <listitem><para><literal>operation</literal>: a string + identifier chosen by the client, which can then later be + passed to org.freedesktop.realmd.Service.Cancel() in order + to cancel the operation</para></listitem> + </itemizedlist> + + This method requires authorization for the PolicyKit action + called <literal>org.freedesktop.realmd.deconfigure-realm</literal>. + + In addition to common DBus error results, this method may return: + <itemizedlist> + <listitem><para><literal>org.freedesktop.realmd.Error.Failed</literal>: + may be returned if the deconfigure failed for a generic reason.</para></listitem> + <listitem><para><literal>org.freedesktop.realmd.Error.Cancelled</literal>: + returned if the operation was cancelled.</para></listitem> + <listitem><para><literal>org.freedesktop.realmd.Error.NotAuthorized</literal>: + returned if the calling client is not permitted to deconfigure a + realm.</para></listitem> + <listitem><para><literal>org.freedesktop.realmd.Error.NotConfigured</literal>: + returned if this realm is not configured on the machine.</para></listitem> + <listitem><para><literal>org.freedesktop.realmd.Error.Busy</literal>: + returned if the service is currently performing another operation like + join or leave.</para></listitem> + </itemizedlist> + --> + <method name="Deconfigure"> + <arg name="options" type="a{sv}" direction="in"/> + </method> + + <!-- + SupportedInterfaces: + + Additional supported interfaces of this realm. This includes + interfaces that contain more information about the realm, + such as #org.freedesktop.realmd.Kerberos and interfaces + which contain methods for configuring a realm, such as + #org.freedesktop.realmd.KerberosMembership. + --> + <property name="SupportedInterfaces" type="as" access="read"/> + + <!-- + Details: informational details about the realm + + Informational details about the realm. The following values + should be present: + <itemizedlist> + <listitem><para><literal>server-software</literal>: + identifier of the software running on the server (eg: + <literal>active-directory</literal>).</para></listitem> + <listitem><para><literal>client-software</literal>: + identifier of the software running on the client (eg: + <literal>sssd</literal>).</para></listitem> + </itemizedlist> + --> + <property name="Details" type="a(ss)" access="read"/> + + <!-- + LoginFormats: supported formats for login names + + Supported formats for login to this realm. This is only + relevant once the realm has been enrolled. The formats + will contain a <literal>%U</literal> in the string, which + indicate where the user name should be placed. The formats + may contain a <literal>%D</literal> in the string which + indicate where a domain name should be placed. + + The first format in the list is the preferred format for + login names. + --> + <property name="LoginFormats" type="as" access="read"/> + + <!-- + LoginPolicy: the policy for logins using this realm + + The policy for logging into this computer using this realm. + + The policy can be changed using the + #org.freedesktop.realmd.Realm.ChangeLoginPolicy() method. + + The following policies are predefined. Not all providers + support all these policies and there may be provider specific + policies or multiple policies represented in the string: + <itemizedlist> + <listitem><para><literal>allow-any-login</literal>: allow + login by any authenticated user present in this + realm.</para></listitem> + <listitem><para><literal>allow-permitted-logins</literal>: + only allow the logins permitted in the + #org.freedesktop.realmd.Realm:PermittedLogins + property.</para></listitem> + <listitem><para><literal>deny-any-login</literal>: + don't allow any logins via authenticated users of this + realm.</para></listitem> + </itemizedlist> + --> + <property name="LoginPolicy" type="s" access="read"/> + + <!-- + PermittedLogins: the permitted login names + + The list of permitted authenticated users allowed to login + into this computer. This is only relevant if the + #org.freedesktop.realmd.Realm:LoginPolicy property + contains the <literal>allow-permitted-logins</literal> + string. + --> + <property name="PermittedLogins" type="as" access="read"/> + + <!-- + ChangeLoginPolicy: + @login_policy: the new login policy, or an empty string + @permitted_add: a list of logins to permit + @permitted_remove: a list of logins to not permit + @options: options for this operation + + Change the login policy and/or permitted logins for this realm. + + Not all realms support the all the various login policies. An + error will be returned if the new login policy is not supported. + You may specify an empty string for the @login_policy argument + which will cause no change in the policy itself. If the policy + is changed, it will be reflected in the + #org.freedesktop.realmd.Realm:LoginPolicy property. + + The @permitted_add and @permitted_remove arguments represent + lists of login names that should be added and removed from + the #org.freedesktop.realmd.Kerberos:PermittedLogins property. + + @options can contain, but is not limited to, the following values: + <itemizedlist> + <listitem><para><literal>operation</literal>: a string + identifier chosen by the client, which can then later be + passed to org.freedesktop.realmd.Service.Cancel() in order + to cancel the operation</para></listitem> + </itemizedlist> + + This method requires authorization for the PolicyKit action + called <literal>org.freedesktop.realmd.login-policy</literal>. + + In addition to common DBus error results, this method may return: + <itemizedlist> + <listitem><para><literal>org.freedesktop.realmd.Error.Failed</literal>: + may be returned if the policy change failed for a generic reason.</para></listitem> + <listitem><para><literal>org.freedesktop.realmd.Error.Cancelled</literal>: + returned if the operation was cancelled.</para></listitem> + <listitem><para><literal>org.freedesktop.realmd.Error.NotAuthorized</literal>: + returned if the calling client is not permitted to change login policy + operation.</para></listitem> + <listitem><para><literal>org.freedesktop.realmd.Error.NotConfigured</literal>: + returned if the realm is not configured.</para></listitem> + <listitem><para><literal>org.freedesktop.realmd.Error.Busy</literal>: + returned if the service is currently performing another operation like + join or leave.</para></listitem> + </itemizedlist> + --> + <method name="ChangeLoginPolicy"> + <arg name="login_policy" type="s" direction="in"/> + <arg name="permitted_add" type="as" direction="in"/> + <arg name="permitted_remove" type="as" direction="in"/> + <arg name="options" type="a{sv}" direction="in"/> + </method> + + </interface> + + <!-- + org.freedesktop.realmd.Kerberos: + @short_description: a kerberos realm + + An interface that describes a kerberos realm in more detail. This + is always implemented on an DBus object path that also implements + the #org.freedesktop.realmd.Realm interface. + --> + <interface name="org.freedesktop.realmd.Kerberos"> + + <!-- + RealmName: the kerberos realm name + + The kerberos name for this realm. This is usually in upper + case. + --> + <property name="RealmName" type="s" access="read"/> + + <!-- + DomainName: the DNS domain name + + The DNS domain name for this realm. + --> + <property name="DomainName" type="s" access="read"/> + + </interface> + + <!-- + org.freedesktop.realmd.KerberosMembership: + + An interface used to configure this machine by joining a realm. + + It sets up a computer/host account in the realm for this machine + and a keytab to track the credentials for that account. + + The various properties are guaranteed to have been updated before + the operation methods return, if they change state. + --> + <interface name="org.freedesktop.realmd.KerberosMembership"> + + <!-- + SuggestedAdministrator: common administrator name + + The common administrator name for this type of realm. This + can be used by clients as a hint when prompting the user for + administrative authentication. + --> + <property name="SuggestedAdministrator" type="s" access="read"/> + + <!-- + SupportedJoinCredentials: credentials supported for joining + + Various kinds of credentials that are supported when calling the + #org.freedesktop.realmd.Kerberos.Join() method. + + Each credential is represented by a type, and an owner. The type + denotes which kind of credential is passed to the method. The + owner indicates to the client how to prompt the user or obtain + the credential, and to the service how to use the credential. + + The various types are: + <itemizedlist> + <listitem><para><literal>ccache</literal>: + the credentials should contain an array of bytes as a + <literal>ay</literal> containing the data from a kerberos + credential cache file.</para></listitem> + <listitem><para><literal>password</literal>: + the credentials should contain a pair of strings as a + <literal>(ss)</literal> representing a name and + password. The name may contain a realm in the standard + kerberos format. If missing, it will default to this + realm. The name may be empty for a computer or one time + password.</para></listitem> + <listitem><para><literal>automatic</literal>: + the credentials should contain an empty string as a + <literal>s</literal>. Using <literal>automatic</literal> + indicates that default or system credentials are to be + used.</para></listitem> + </itemizedlist> + + The various owners are: + <itemizedlist> + <listitem><para><literal>administrator</literal>: + the credentials belong to a kerberos user principal. + The caller may use this as a hint to prompt the user + for administrative credentials.</para></listitem> + <listitem><para><literal>user</literal>: + the credentials belong to a kerberos user principal. + The caller may use this as a hint to prompt the user + for his (possibly non-administrative) + credentials.</para></listitem> + <listitem><para><literal>computer</literal>: + the credentials belong to the computer realmd is + being run on.</para></listitem> + <listitem><para><literal>secret</literal>: + the credentials are a one time password or other secret + used to join or leave the computer.</para></listitem> + </itemizedlist> + --> + <property name="SupportedJoinCredentials" type="a(ss)" access="read"/> + + <!-- + SupportedLeaveCredentials: credentials supported for leaving + + Various kinds of credentials that are supported when calling the + #org.freedesktop.realmd.Kerberos.Leave() method. + + See #org.freedesktop.realmd.Kerberos:SupportedJoinCredentials for + a discussion of what the values represent. + --> + <property name="SupportedLeaveCredentials" type="a(ss)" access="read"/> + + <!-- + Join: + + Join this machine to the realm and enroll the machine. + + If this method returns successfully then the machine will be + joined to the realm. It is not necessary to restart services or the + machine afterward. Relevant properties on the realm will be updated + before the method returns. + + The @credentials should be set according to one of the + supported credentials returned by + #org.freedesktop.realmd.Kerberos:SupportedJoinCredentials. + The first string in the tuple is the type, the second string + is the owner, and the variant contains the credential contents + See the discussion at + #org.freedesktop.realmd.Kerberos:SupportedJoinCredentials + for more information. + + @options can contain, but is not limited to, the following values: + <itemizedlist> + <listitem><para><literal>operation</literal>: a string + identifier chosen by the client, which can then later be + passed to org.freedesktop.realmd.Service.Cancel() in order + to cancel the operation</para></listitem> + </itemizedlist> + + This method requires authorization for the PolicyKit action + called <literal>org.freedesktop.realmd.configure-realm</literal>. + + In addition to common DBus error results, this method may return: + <itemizedlist> + <listitem><para><literal>org.freedesktop.realmd.Error.Failed</literal>: + may be returned if the join failed for a generic reason.</para></listitem> + <listitem><para><literal>org.freedesktop.realmd.Error.Cancelled</literal>: + returned if the operation was cancelled.</para></listitem> + <listitem><para><literal>org.freedesktop.realmd.Error.NotAuthorized</literal>: + returned if the calling client is not permitted to perform an join + operation.</para></listitem> + <listitem><para><literal>org.freedesktop.realmd.Error.AuthenicationFailed</literal>: + returned if the credentials passed did not authenticate against the realm + correctly. It is appropriate to prompt the user again.</para></listitem> + <listitem><para><literal>org.freedesktop.realmd.Error.AlreadyEnrolled</literal>: + returned if already enrolled in this realm, or another realm and enrolling + in multiple realms is not supported.</para></listitem> + <listitem><para><literal>org.freedesktop.realmd.Error.Busy</literal>: + returned if the service is currently performing another operation like + join or leave.</para></listitem> + </itemizedlist> + --> + <method name="Join"> + <arg name="credentials" type="(ssv)" direction="in"/> + <arg name="options" type="a{sv}" direction="in"/> + </method> + + <!-- + Leave: + + Leave the realm and unenroll the machine. + + If this method returns successfully then the machine will have + left the domain and been unenrolled. It is not necessary to restart + services or the machine afterward. Relevant properties on the realm + will be updated before the method returns. + + The @credentials should be set according to one of the + supported credentials returned by + #org.freedesktop.realmd.Kerberos:SupportedUnenrollCredentials. + The first string in the tuple is the type, the second string + is the owner, and the variant contains the credential contents + See the discussion at + #org.freedesktop.realmd.Kerberos:SupportedEnrollCredentials + for more information. + + @options can contain, but is not limited to, the following values: + <itemizedlist> + <listitem><para><literal>operation</literal>: a string + identifier chosen by the client, which can then later be + passed to org.freedesktop.realmd.Service.Cancel() in order + to cancel the operation</para></listitem> + </itemizedlist> + + This method requires authorization for the PolicyKit action + called <literal>org.freedesktop.realmd.deconfigure-realm</literal>. + + In addition to common DBus error results, this method may return: + <itemizedlist> + <listitem><para><literal>org.freedesktop.realmd.Error.Failed</literal>: + may be returned if the unenroll failed for a generic reason.</para></listitem> + <listitem><para><literal>org.freedesktop.realmd.Error.Cancelled</literal>: + returned if the operation was cancelled.</para></listitem> + <listitem><para><literal>org.freedesktop.realmd.Error.NotAuthorized</literal>: + returned if the calling client is not permitted to perform an unenroll + operation.</para></listitem> + <listitem><para><literal>org.freedesktop.realmd.Error.AuthenicationFailed</literal>: + returned if the credentials passed did not authenticate against the realm + correctly. It is appropriate to prompt the user again.</para></listitem> + <listitem><para><literal>org.freedesktop.realmd.Error.NotEnrolled</literal>: + returned if not enrolled in this realm.</para></listitem> + <listitem><para><literal>org.freedesktop.realmd.Error.Busy</literal>: + returned if the service is currently performing another operation like + enroll or unenroll.</para></listitem> + </itemizedlist> + --> + <method name="Leave"> + <arg name="credentials" type="(ssv)" direction="in"/> + <arg name="options" type="a{sv}" direction="in"/> + </method> + + </interface> + +</node> diff --git a/panels/user-accounts/data/user-accounts-dialog.css b/panels/user-accounts/data/user-accounts-dialog.css new file mode 100644 index 0000000..7a984a4 --- /dev/null +++ b/panels/user-accounts/data/user-accounts-dialog.css @@ -0,0 +1,23 @@ +levelbar .strength-weak { + background-color: #cc0000; + border-color: #cc0000; +} + +levelbar .strength-low { + background-color: #f5ce00; + border-color: #f5ce00; +} + +levelbar .strength-medium, +levelbar .strength-good, +levelbar .strength-high { + background-color: #73d216; + border-color: #73d216; +} + +/* This is used for user_avatar_edit_button */ +.cutout-button { + background-color: @window_bg_color; + border-radius: 9999px; + padding: 2px; +} diff --git a/panels/user-accounts/fingerprint-strings.h b/panels/user-accounts/fingerprint-strings.h new file mode 100644 index 0000000..e65491d --- /dev/null +++ b/panels/user-accounts/fingerprint-strings.h @@ -0,0 +1,172 @@ +/* + * Helper functions to translate statuses and actions to strings + * Copyright (C) 2008 Bastien Nocera <hadess@hadess.net> + * + * Experimental code. This will be moved out of fprintd into it's own + * package once the system has matured. + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +struct { + const char *dbus_name; + const char *place_str_generic; + const char *place_str_specific; + const char *swipe_str_generic; + const char *swipe_str_specific; +} fingers[] = { + { "any", + N_("Place your finger on the fingerprint reader"), + N_("Place your finger on %s"), + N_("Swipe your finger across the fingerprint reader"), + N_("Swipe your finger across %s") }, + { "left-thumb", + N_("Place your left thumb on the fingerprint reader"), + N_("Place your left thumb on %s"), + N_("Swipe your left thumb across the fingerprint reader"), + N_("Swipe your left thumb across %s") }, + { "left-index-finger", + N_("Place your left index finger on the fingerprint reader"), + N_("Place your left index finger on %s"), + N_("Swipe your left index finger across the fingerprint reader"), + N_("Swipe your left index finger across %s") }, + { "left-middle-finger", + N_("Place your left middle finger on the fingerprint reader"), + N_("Place your left middle finger on %s"), + N_("Swipe your left middle finger across the fingerprint reader"), + N_("Swipe your left middle finger across %s") }, + { "left-ring-finger", + N_("Place your left ring finger on the fingerprint reader"), + N_("Place your left ring finger on %s"), + N_("Swipe your left ring finger across the fingerprint reader"), + N_("Swipe your left ring finger across %s") }, + { "left-little-finger", + N_("Place your left little finger on the fingerprint reader"), + N_("Place your left little finger on %s"), + N_("Swipe your left little finger across the fingerprint reader"), + N_("Swipe your left little finger across %s") }, + { "right-thumb", + N_("Place your right thumb on the fingerprint reader"), + N_("Place your right thumb on %s"), + N_("Swipe your right thumb across the fingerprint reader"), + N_("Swipe your right thumb across %s") }, + { "right-index-finger", + N_("Place your right index finger on the fingerprint reader"), + N_("Place your right index finger on %s"), + N_("Swipe your right index finger across the fingerprint reader"), + N_("Swipe your right index finger across %s") }, + { "right-middle-finger", + N_("Place your right middle finger on the fingerprint reader"), + N_("Place your right middle finger on %s"), + N_("Swipe your right middle finger across the fingerprint reader"), + N_("Swipe your right middle finger across %s") }, + { "right-ring-finger", + N_("Place your right ring finger on the fingerprint reader"), + N_("Place your right ring finger on %s"), + N_("Swipe your right ring finger across the fingerprint reader"), + N_("Swipe your right ring finger across %s") }, + { "right-little-finger", + N_("Place your right little finger on the fingerprint reader"), + N_("Place your right little finger on %s"), + N_("Swipe your right little finger across the fingerprint reader"), + N_("Swipe your right little finger across %s") }, + { NULL, NULL, NULL, NULL, NULL } +}; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-nonliteral" + +G_GNUC_UNUSED static char *finger_str_to_msg(const char *finger_name, const char *driver_name, gboolean is_swipe) +{ + int i; + + if (finger_name == NULL) + return NULL; + + for (i = 0; fingers[i].dbus_name != NULL; i++) { + if (g_str_equal (fingers[i].dbus_name, finger_name)) { + if (is_swipe == FALSE) { + if (driver_name) + return g_strdup_printf (TR (fingers[i].place_str_specific), driver_name); + else + return g_strdup (TR (fingers[i].place_str_generic)); + } else { + if (driver_name) + return g_strdup_printf (TR (fingers[i].swipe_str_specific), driver_name); + else + return g_strdup (TR (fingers[i].swipe_str_generic)); + } + } + } + + return NULL; +} + +#pragma GCC diagnostic pop + +/* Cases not handled: + * verify-no-match + * verify-match + * verify-unknown-error + */ +G_GNUC_UNUSED static const char *verify_result_str_to_msg(const char *result, gboolean is_swipe) +{ + if (result == NULL) + return NULL; + + if (strcmp (result, "verify-retry-scan") == 0) { + if (is_swipe == FALSE) + return TR (N_("Place your finger on the reader again")); + else + return TR (N_("Swipe your finger again")); + } + if (strcmp (result, "verify-swipe-too-short") == 0) + return TR (N_("Swipe was too short, try again")); + if (strcmp (result, "verify-finger-not-centered") == 0) + return TR (N_("Your finger was not centered, try swiping your finger again")); + if (strcmp (result, "verify-remove-and-retry") == 0) + return TR (N_("Remove your finger, and try swiping your finger again")); + + return NULL; +} + +/* Cases not handled: + * enroll-completed + * enroll-failed + * enroll-unknown-error + */ +G_GNUC_UNUSED static const char *enroll_result_str_to_msg(const char *result, gboolean is_swipe) +{ + if (result == NULL) + return NULL; + + if (strcmp (result, "enroll-retry-scan") == 0 || strcmp (result, "enroll-stage-passed") == 0) { + if (is_swipe == FALSE) + return TR (N_("Place your finger on the reader again")); + else + return TR (N_("Swipe your finger again")); + } + if (strcmp (result, "enroll-swipe-too-short") == 0) + return TR (N_("Swipe was too short, try again")); + if (strcmp (result, "enroll-finger-not-centered") == 0) + return TR (N_("Your finger was not centered, try swiping your finger again")); + if (strcmp (result, "enroll-remove-and-retry") == 0) + return TR (N_("Remove your finger, and try swiping your finger again")); + + return NULL; +} + diff --git a/panels/user-accounts/icons/meson.build b/panels/user-accounts/icons/meson.build new file mode 100644 index 0000000..2daa0b3 --- /dev/null +++ b/panels/user-accounts/icons/meson.build @@ -0,0 +1,4 @@ +install_data( + 'scalable/org.gnome.Settings-users-symbolic.svg', + install_dir: join_paths(control_center_icondir, 'hicolor', 'scalable', 'apps') +) diff --git a/panels/user-accounts/icons/scalable/org.gnome.Settings-users-symbolic.svg b/panels/user-accounts/icons/scalable/org.gnome.Settings-users-symbolic.svg new file mode 100644 index 0000000..15ef7ee --- /dev/null +++ b/panels/user-accounts/icons/scalable/org.gnome.Settings-users-symbolic.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg"> + <path d="m 5 1 c -1.378906 0 -2.5 1.121094 -2.5 2.5 s 1.121094 2.5 2.5 2.5 s 2.5 -1.121094 2.5 -2.5 s -1.121094 -2.5 -2.5 -2.5 z m 6 3 c -1.378906 0 -2.5 1.121094 -2.5 2.5 s 1.121094 2.5 2.5 2.5 s 2.5 -1.121094 2.5 -2.5 s -1.121094 -2.5 -2.5 -2.5 z m -8 3 c -1.660156 0 -3 1.339844 -3 3 v 2 c 0 0.554688 0.445312 1 1 1 h 4.074219 c 0 -2.042969 1.582031 -3.734375 3.582031 -3.910156 c -0.589844 -0.53125 -0.984375 -1.253906 -1.109375 -2.039063 c -0.175781 -0.03125 -0.359375 -0.050781 -0.546875 -0.050781 z m 6 3 c -1.660156 0 -3 1.339844 -3 3 v 2 c 0 0.554688 0.445312 1 1 1 h 8 c 0.554688 0 1 -0.445312 1 -1 v -2 c 0 -1.660156 -1.339844 -3 -3 -3 z m 0 0" fill="#2e3436"/> +</svg> diff --git a/panels/user-accounts/meson.build b/panels/user-accounts/meson.build new file mode 100644 index 0000000..d9efd1f --- /dev/null +++ b/panels/user-accounts/meson.build @@ -0,0 +1,199 @@ +panels_list += cappletname +desktop = 'gnome-@0@-panel.desktop'.format(cappletname) + +desktop_in = configure_file( + input: 'data/' + desktop + '.in.in', + output: desktop + '.in', + configuration: desktop_conf +) + +i18n.merge_file( + type: 'desktop', + input: desktop_in, + output: desktop, + po_dir: po_dir, + install: true, + install_dir: control_center_desktopdir +) + +image_data = files( + 'data/faces/bicycle.jpg', + 'data/faces/book.jpg', + 'data/faces/calculator.jpg', + 'data/faces/cat.jpg', + 'data/faces/coffee2.jpg', + 'data/faces/flower2.jpg', + 'data/faces/gamepad.jpg', + 'data/faces/guitar2.jpg', + 'data/faces/headphones.jpg', + 'data/faces/hummingbird.jpg', + 'data/faces/mountain.jpg', + 'data/faces/plane.jpg', + 'data/faces/surfer.jpg', + 'data/faces/tomatoes.jpg', + 'data/faces/tree.jpg', +) + +legacy_image_data = files( + 'data/faces/legacy/astronaut.jpg', + 'data/faces/legacy/baseball.png', + 'data/faces/legacy/butterfly.png', + 'data/faces/legacy/cat-eye.jpg', + 'data/faces/legacy/chess.jpg', + 'data/faces/legacy/coffee.jpg', + 'data/faces/legacy/dice.jpg', + 'data/faces/legacy/energy-arc.jpg', + 'data/faces/legacy/fish.jpg', + 'data/faces/legacy/flake.jpg', + 'data/faces/legacy/flower.jpg', + 'data/faces/legacy/grapes.jpg', + 'data/faces/legacy/guitar.jpg', + 'data/faces/legacy/launch.jpg', + 'data/faces/legacy/leaf.jpg', + 'data/faces/legacy/lightning.jpg', + 'data/faces/legacy/penguin.jpg', + 'data/faces/legacy/puppy.jpg', + 'data/faces/legacy/sky.jpg', + 'data/faces/legacy/soccerball.png', + 'data/faces/legacy/sunflower.jpg', + 'data/faces/legacy/sunset.jpg', + 'data/faces/legacy/tennis-ball.png', + 'data/faces/legacy/yellow-rose.jpg', +) + +image_dir = join_paths(control_center_datadir, 'pixmaps', 'faces') + +install_data( + image_data, + install_dir: image_dir +) + +legacy_image_dir = join_paths(image_dir, 'legacy') + +install_data( + legacy_image_data, + install_dir: legacy_image_dir +) + +# create symlinks for legacy images to not break current images for people +meson.add_install_script('sh', '-c', + '''for f in $DESTDIR@0@/*; do + ln -sf legacy/$(basename $f) $DESTDIR@1@/$(basename $f); + done'''.format(legacy_image_dir, image_dir)) + +polkit = 'org.gnome.controlcenter.@0@.policy'.format(cappletname) + +i18n.merge_file( + input: polkit + '.in', + output: polkit, + po_dir: po_dir, + install: true, + install_dir: join_paths(control_center_datadir, 'polkit-1', 'actions') +) + +common_sources = files( + 'cc-add-user-dialog.c', + 'cc-realm-manager.c', + 'pw-utils.c', + 'user-utils.c', +) + +resource_data = files( + 'cc-add-user-dialog.ui', + 'cc-avatar-chooser.ui', + 'cc-login-history-dialog.ui', + 'cc-password-dialog.ui', + 'cc-user-panel.ui', + 'cc-fingerprint-dialog.ui', + 'data/icons/fingerprint-detection-complete-symbolic.svg', + 'data/icons/fingerprint-detection-symbolic.svg', + 'data/icons/fingerprint-detection-warning-symbolic.svg', + 'data/join-dialog.ui', + 'data/user-accounts-dialog.css', + 'data/cc-fingerprint-dialog.css', +) + +common_sources += gnome.compile_resources( + 'cc-' + cappletname + '-resources', + cappletname + '.gresource.xml', + c_name: 'cc_' + cappletname.underscorify(), + dependencies: resource_data, + export: true +) + +realmd_namespace = 'org.freedesktop.realmd' + +common_sources += gnome.gdbus_codegen( + 'cc-realm-generated', + 'data/' + realmd_namespace + '.xml', + interface_prefix: realmd_namespace + '.', + namespace: 'CcRealm', + object_manager: true, + annotations: ['org.freedesktop.realmd.Realm', 'org.gtk.GDBus.C.Name', 'Common'] +) + +fprintd_namespace = 'net.reactivated.Fprint' +common_sources += gnome.gdbus_codegen( + 'cc-fprintd-generated', + sources: [ + 'data' / fprintd_namespace + '.Manager.xml', + 'data' / fprintd_namespace + '.Device.xml', + ], + interface_prefix: fprintd_namespace + '.', + namespace: 'CcFprintd', + autocleanup: 'all', +) + +enum_headers = [ + 'cc-fingerprint-manager.h', +] + +sources = common_sources + files( + 'cc-avatar-chooser.c', + 'cc-crop-area.c', + 'cc-fingerprint-manager.c', + 'cc-fingerprint-dialog.c', + 'cc-login-history-dialog.c', + 'cc-password-dialog.c', + 'cc-user-panel.c', + 'run-passwd.c', +) + +sources += gnome.mkenums_simple( + 'cc-user-accounts-enum-types', + sources: files(enum_headers)) + +# Kerberos support +krb_dep = dependency('krb5', required: false) +assert(krb_dep.found(), 'kerberos libraries not found in your path') + +deps = common_deps + [ + accounts_dep, + gdk_pixbuf_dep, + gnome_desktop_dep, + liblanguage_dep, + krb_dep, + m_dep, + polkit_gobject_dep, + pwquality_dep, +] + +if enable_malcontent + deps += malcontent_dep +endif + +cflags += [ + '-DGNOMELOCALEDIR="@0@"'.format(control_center_localedir), + '-DHAVE_LIBPWQUALITY', + '-DUM_PIXMAP_DIR="@0@"'.format(join_paths(control_center_pkgdatadir, 'pixmaps')) +] + +panels_libs += static_library( + cappletname, + sources: sources, + include_directories: [top_inc, shell_inc], + dependencies: deps, + c_args: cflags +) + +subdir('icons') diff --git a/panels/user-accounts/org.gnome.controlcenter.user-accounts.policy.in b/panels/user-accounts/org.gnome.controlcenter.user-accounts.policy.in new file mode 100644 index 0000000..c6f09c1 --- /dev/null +++ b/panels/user-accounts/org.gnome.controlcenter.user-accounts.policy.in @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE policyconfig PUBLIC + "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" + "http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd"> + +<policyconfig> + <vendor>The GNOME Project</vendor> + <vendor_url>http://www.gnome.org/</vendor_url> + + <action id="org.gnome.controlcenter.user-accounts.administration"> + <description>Manage user accounts</description> + <message>Authentication is required to change user data</message> + <defaults> + <allow_any>no</allow_any> + <allow_inactive>no</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.imply">org.freedesktop.accounts.user-administration org.freedesktop.realmd.configure-realm org.freedesktop.realmd.login-policy org.freedesktop.MalcontentControl.administration com.endlessm.ParentalControls.AppFilter.ReadAny com.endlessm.ParentalControls.AppFilter.ChangeAny com.endlessm.ParentalControls.AppFilter.ReadOwn com.endlessm.ParentalControls.AppFilter.ChangeOwn</annotate> + </action> + +</policyconfig> diff --git a/panels/user-accounts/pw-utils.c b/panels/user-accounts/pw-utils.c new file mode 100644 index 0000000..0f4dfd8 --- /dev/null +++ b/panels/user-accounts/pw-utils.c @@ -0,0 +1,177 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright 2012 Red Hat, Inc, + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see <http://www.gnu.org/licenses/>. + * + * Written by: Matthias Clasen <mclasen@redhat.com> + */ + +#include "config.h" + +#include "pw-utils.h" + +#include <glib.h> +#include <glib/gi18n.h> + +#include <pwquality.h> + +static pwquality_settings_t * +get_pwq (void) +{ + static pwquality_settings_t *settings; + + if (settings == NULL) { + gchar *err = NULL; + gint rv = 0; + + settings = pwquality_default_settings (); + pwquality_set_int_value (settings, PWQ_SETTING_MAX_SEQUENCE, 4); + + rv = pwquality_read_config (settings, NULL, (gpointer)&err); + if (rv < 0) { + g_warning ("failed to read pwquality configuration: %s\n", + pwquality_strerror (NULL, 0, rv, err)); + pwquality_free_settings (settings); + + /* Load just default settings in case of failure. */ + settings = pwquality_default_settings (); + pwquality_set_int_value (settings, PWQ_SETTING_MAX_SEQUENCE, 4); + } + } + + return settings; +} + +gint +pw_min_length (void) +{ + gint value = 0; + gint rv; + + rv = pwquality_get_int_value (get_pwq (), PWQ_SETTING_MIN_LENGTH, &value); + if (rv < 0) { + g_warning ("Failed to read pwquality setting: %s\n", + pwquality_strerror (NULL, 0, rv, NULL)); + } + + return value; +} + +gchar * +pw_generate (void) +{ + gchar *res; + gint rv; + + rv = pwquality_generate (get_pwq (), 0, &res); + + if (rv < 0) { + g_warning ("Password generation failed: %s\n", + pwquality_strerror (NULL, 0, rv, NULL)); + return NULL; + } + + return res; +} + +static const gchar * +pw_error_hint (gint error) +{ + switch (error) { + case PWQ_ERROR_SAME_PASSWORD: + return C_("Password hint", "The new password needs to be different from the old one."); + case PWQ_ERROR_CASE_CHANGES_ONLY: + return C_("Password hint", "Try changing some letters and numbers."); + case PWQ_ERROR_TOO_SIMILAR: + return C_("Password hint", "Try changing the password a bit more."); + case PWQ_ERROR_USER_CHECK: + return C_("Password hint", "A password without your user name would be stronger."); + case PWQ_ERROR_GECOS_CHECK: + return C_("Password hint", "Try to avoid using your name in the password."); + case PWQ_ERROR_BAD_WORDS: + return C_("Password hint", "Try to avoid some of the words included in the password."); + case PWQ_ERROR_ROTATED: + return C_("Password hint", "Try changing the password a bit more."); + case PWQ_ERROR_CRACKLIB_CHECK: + return C_("Password hint", "Try to avoid common words."); + case PWQ_ERROR_PALINDROME: + return C_("Password hint", "Try to avoid reordering existing words."); + case PWQ_ERROR_MIN_DIGITS: + return C_("Password hint", "Try to use more numbers."); + case PWQ_ERROR_MIN_UPPERS: + return C_("Password hint", "Try to use more uppercase letters."); + case PWQ_ERROR_MIN_LOWERS: + return C_("Password hint", "Try to use more lowercase letters."); + case PWQ_ERROR_MIN_OTHERS: + return C_("Password hint", "Try to use more special characters, like punctuation."); + case PWQ_ERROR_MIN_CLASSES: + return C_("Password hint", "Try to use a mixture of letters, numbers and punctuation."); + case PWQ_ERROR_MAX_CONSECUTIVE: + return C_("Password hint", "Try to avoid repeating the same character."); + case PWQ_ERROR_MAX_CLASS_REPEAT: + return C_("Password hint", "Try to avoid repeating the same type of character: you need to mix up letters, numbers and punctuation."); + case PWQ_ERROR_MAX_SEQUENCE: + return C_("Password hint", "Try to avoid sequences like 1234 or abcd."); + case PWQ_ERROR_MIN_LENGTH: + return C_("Password hint", "Password needs to be longer. Try to add more letters, numbers and punctuation."); + case PWQ_ERROR_EMPTY_PASSWORD: + return C_("Password hint", "Mix uppercase and lowercase and try to use a number or two."); + default: + return C_("Password hint", "Adding more letters, numbers and punctuation will make the password stronger."); + } +} + +gdouble +pw_strength (const gchar *password, + const gchar *old_password, + const gchar *username, + const gchar **hint, + gint *strength_level) +{ + gint rv, level, length = 0; + gdouble strength = 0.0; + void *auxerror; + + rv = pwquality_check (get_pwq (), + password, old_password, username, + &auxerror); + + if (password != NULL) + length = strlen (password); + + strength = CLAMP (0.01 * rv, 0.0, 1.0); + if (rv < 0) { + level = (length > 0) ? 1 : 0; + } + else if (strength < 0.50) { + level = 2; + } else if (strength < 0.75) { + level = 3; + } else if (strength < 0.90) { + level = 4; + } else { + level = 5; + } + + if (length && length < pw_min_length()) + *hint = pw_error_hint (PWQ_ERROR_MIN_LENGTH); + else + *hint = pw_error_hint (rv); + + if (strength_level) + *strength_level = level; + + return strength; +} diff --git a/panels/user-accounts/pw-utils.h b/panels/user-accounts/pw-utils.h new file mode 100644 index 0000000..d7df491 --- /dev/null +++ b/panels/user-accounts/pw-utils.h @@ -0,0 +1,31 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright 2012 Red Hat, Inc, + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see <http://www.gnu.org/licenses/>. + * + * Written by: Matthias Clasen <mclasen@redhat.com> + */ + +#pragma once + +#include <glib.h> + +gint pw_min_length (void); +gchar *pw_generate (void); +gdouble pw_strength (const gchar *password, + const gchar *old_password, + const gchar *username, + const gchar **hint, + gint *strength_level); diff --git a/panels/user-accounts/run-passwd.c b/panels/user-accounts/run-passwd.c new file mode 100644 index 0000000..edbc998 --- /dev/null +++ b/panels/user-accounts/run-passwd.c @@ -0,0 +1,737 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* run-passwd.c: this file is part of users-admin, a gnome-system-tools frontend + * for user administration. + * + * Copyright (C) 2002 Diego Gonzalez + * Copyright (C) 2006 Johannes H. Jensen + * Copyright (C) 2010 Milan Bouchet-Valat + * + * Written by: Diego Gonzalez <diego@pemas.net> + * Modified by: Johannes H. Jensen <joh@deworks.net>, + * Milan Bouchet-Valat <nalimilan@club.fr>. + * + * This program 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 2, or (at your option) + * any later version. + * + * This program 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 this program; if not, see <http://www.gnu.org/licenses/>. + * + * Most of this code originally comes from gnome-about-me-password.c, + * from gnome-control-center. + */ + +#include <config.h> +#include <glib/gi18n.h> + +#include <unistd.h> +#include <errno.h> +#include <stdio.h> +#include <string.h> +#include <sys/wait.h> + +#if __sun +#include <sys/types.h> +#include <signal.h> +#endif + +#include "run-passwd.h" + +/* Passwd states */ +typedef enum { + PASSWD_STATE_NONE, /* Passwd is not asking for anything */ + PASSWD_STATE_AUTH, /* Passwd is asking for our current password */ + PASSWD_STATE_NEW, /* Passwd is asking for our new password */ + PASSWD_STATE_RETYPE, /* Passwd is asking for our retyped new password */ + PASSWD_STATE_DONE, /* Passwd succeeded but has not yet exited */ + PASSWD_STATE_ERR /* Passwd reported an error but has not yet exited */ +} PasswdState; + +struct PasswdHandler { + const char *current_password; + const char *new_password; + + /* Communication with the passwd program */ + GPid backend_pid; + + GIOChannel *backend_stdin; + GIOChannel *backend_stdout; + + GQueue *backend_stdin_queue; /* Write queue to backend_stdin */ + + /* GMainLoop IDs */ + guint backend_child_watch_id; /* g_child_watch_add (PID) */ + guint backend_stdout_watch_id; /* g_io_add_watch (stdout) */ + + /* State of the passwd program */ + PasswdState backend_state; + gboolean changing_password; + + PasswdCallback auth_cb; + gpointer auth_cb_data; + + PasswdCallback chpasswd_cb; + gpointer chpasswd_cb_data; +}; + +/* Buffer size for backend output */ +#define BUFSIZE 64 + + +static GQuark +passwd_error_quark (void) +{ + static GQuark q = 0; + + if (q == 0) { + q = g_quark_from_static_string("passwd_error"); + } + + return q; +} + +/* Error handling */ +#define PASSWD_ERROR (passwd_error_quark ()) + + +static void +stop_passwd (PasswdHandler *passwd_handler); + +static void +free_passwd_resources (PasswdHandler *passwd_handler); + +static gboolean +io_watch_stdout (GIOChannel *source, GIOCondition condition, PasswdHandler *passwd_handler); + + +/* + * Spawning and closing of backend {{ + */ + +/* Child watcher */ +static void +child_watch_cb (GPid pid, gint status, PasswdHandler *passwd_handler) +{ + if (WIFEXITED (status)) { + if (WEXITSTATUS (status) >= 255) { + g_warning ("Child exited unexpectedly"); + } + if (WEXITSTATUS (status) == 0) { + if (passwd_handler->backend_state == PASSWD_STATE_RETYPE) { + passwd_handler->backend_state = PASSWD_STATE_DONE; + if (passwd_handler->chpasswd_cb) + passwd_handler->chpasswd_cb (passwd_handler, + NULL, + passwd_handler->chpasswd_cb_data); + } + } + } + + free_passwd_resources (passwd_handler); +} + +static void +child_setup_cb (gpointer data) +{ + signal (SIGPIPE, SIG_IGN); + dup2 (fileno (stdout), fileno (stderr)); +} + +/* Spawn passwd backend + * Returns: TRUE on success, FALSE otherwise and sets error appropriately */ +static gboolean +spawn_passwd (PasswdHandler *passwd_handler, GError **error) +{ + gchar *argv[2]; + gchar **envp; + gint my_stdin, my_stdout; + + argv[0] = "/usr/bin/passwd"; /* Is it safe to rely on a hard-coded path? */ + argv[1] = NULL; + + envp = g_get_environ (); + envp = g_environ_setenv (envp, "LC_ALL", "C", TRUE); + + if (!g_spawn_async_with_pipes (NULL, /* Working directory */ + argv, /* Argument vector */ + envp, /* Environment */ + G_SPAWN_DO_NOT_REAP_CHILD, /* Flags */ + child_setup_cb, /* Child setup */ + NULL, /* Data to child setup */ + &passwd_handler->backend_pid, /* PID */ + &my_stdin, /* Stdin */ + &my_stdout, /* Stdout */ + NULL, /* Stderr */ + error)) { /* GError */ + + /* An error occurred */ + free_passwd_resources (passwd_handler); + + g_strfreev (envp); + + return FALSE; + } + + g_strfreev (envp); + + /* Open IO Channels */ + passwd_handler->backend_stdin = g_io_channel_unix_new (my_stdin); + passwd_handler->backend_stdout = g_io_channel_unix_new (my_stdout); + + /* Set raw encoding */ + /* Set nonblocking mode */ + if (g_io_channel_set_encoding (passwd_handler->backend_stdin, NULL, error) != G_IO_STATUS_NORMAL || + g_io_channel_set_encoding (passwd_handler->backend_stdout, NULL, error) != G_IO_STATUS_NORMAL || + g_io_channel_set_flags (passwd_handler->backend_stdin, G_IO_FLAG_NONBLOCK, error) != G_IO_STATUS_NORMAL || + g_io_channel_set_flags (passwd_handler->backend_stdout, G_IO_FLAG_NONBLOCK, error) != G_IO_STATUS_NORMAL ) { + + /* Clean up */ + stop_passwd (passwd_handler); + return FALSE; + } + + /* Turn off buffering */ + g_io_channel_set_buffered (passwd_handler->backend_stdin, FALSE); + g_io_channel_set_buffered (passwd_handler->backend_stdout, FALSE); + + /* Add IO Channel watcher */ + passwd_handler->backend_stdout_watch_id = g_io_add_watch (passwd_handler->backend_stdout, + G_IO_IN | G_IO_PRI, + (GIOFunc) io_watch_stdout, passwd_handler); + + /* Add child watcher */ + passwd_handler->backend_child_watch_id = g_child_watch_add (passwd_handler->backend_pid, (GChildWatchFunc) child_watch_cb, passwd_handler); + + /* Success! */ + + return TRUE; +} + +/* Stop passwd backend */ +static void +stop_passwd (PasswdHandler *passwd_handler) +{ + /* This is the standard way of returning from the dialog with passwd. + * If we return this way we can safely kill passwd as it has completed + * its task. + */ + + if (passwd_handler->backend_pid != -1) { + kill (passwd_handler->backend_pid, 9); + } + + /* We must run free_passwd_resources here and not let our child + * watcher do it, since it will access invalid memory after the + * dialog has been closed and cleaned up. + * + * If we had more than a single thread we'd need to remove + * the child watch before trying to kill the child. + */ + free_passwd_resources (passwd_handler); +} + +/* Clean up passwd resources */ +static void +free_passwd_resources (PasswdHandler *passwd_handler) +{ + /* Remove the child watcher */ + if (passwd_handler->backend_child_watch_id != 0) { + + g_source_remove (passwd_handler->backend_child_watch_id); + + passwd_handler->backend_child_watch_id = 0; + } + + + /* Close IO channels (internal file descriptors are automatically closed) */ + if (passwd_handler->backend_stdin != NULL) { + g_autoptr(GError) error = NULL; + + if (g_io_channel_shutdown (passwd_handler->backend_stdin, TRUE, &error) != G_IO_STATUS_NORMAL) { + g_warning ("Could not shutdown backend_stdin IO channel: %s", error->message); + } + + g_clear_pointer (&passwd_handler->backend_stdin, g_io_channel_unref); + } + + if (passwd_handler->backend_stdout != NULL) { + g_autoptr(GError) error = NULL; + + if (g_io_channel_shutdown (passwd_handler->backend_stdout, TRUE, &error) != G_IO_STATUS_NORMAL) { + g_warning ("Could not shutdown backend_stdout IO channel: %s", error->message); + } + + g_clear_pointer (&passwd_handler->backend_stdout, g_io_channel_unref); + } + + /* Remove IO watcher */ + if (passwd_handler->backend_stdout_watch_id != 0) { + + g_source_remove (passwd_handler->backend_stdout_watch_id); + + passwd_handler->backend_stdout_watch_id = 0; + } + + /* Close PID */ + if (passwd_handler->backend_pid != -1) { + + g_spawn_close_pid (passwd_handler->backend_pid); + + passwd_handler->backend_pid = -1; + } + + /* Clear backend state */ + passwd_handler->backend_state = PASSWD_STATE_NONE; +} + +/* + * }} Spawning and closing of backend + */ + +/* + * Backend communication code {{ + */ + +/* Write the first element of queue through channel */ +static void +io_queue_pop (GQueue *queue, GIOChannel *channel) +{ + g_autofree gchar *buf = NULL; + gsize bytes_written; + g_autoptr(GError) error = NULL; + + buf = g_queue_pop_head (queue); + + if (buf != NULL) { + + if (g_io_channel_write_chars (channel, buf, -1, &bytes_written, &error) != G_IO_STATUS_NORMAL) { + g_warning ("Could not write queue element \"%s\" to channel: %s", buf, error->message); + } + + /* Ensure passwords are cleared from memory */ + memset (buf, 0, strlen (buf)); + } +} + +/* Goes through the argument list, checking if one of them occurs in str + * Returns: TRUE as soon as an element is found to match, FALSE otherwise */ +static gboolean +is_string_complete (gchar *str, ...) +{ + va_list ap; + gchar *arg; + + if (strlen (str) == 0) { + return FALSE; + } + + va_start (ap, str); + + while ((arg = va_arg (ap, char *)) != NULL) { + if (strstr (str, arg) != NULL) { + va_end (ap); + return TRUE; + } + } + + va_end (ap); + + return FALSE; +} + +/* + * IO watcher for stdout, called whenever there is data to read from the backend. + * This is where most of the actual IO handling happens. + */ +static gboolean +io_watch_stdout (GIOChannel *source, GIOCondition condition, PasswdHandler *passwd_handler) +{ + static GString *str = NULL; /* Persistent buffer */ + + gchar buf[BUFSIZE]; /* Temporary buffer */ + gsize bytes_read; + g_autoptr(GError) gio_error = NULL; + + gboolean reinit = FALSE; + + /* Initialize buffer */ + if (str == NULL) { + str = g_string_new (""); + } + + if (g_io_channel_read_chars (source, buf, BUFSIZE, &bytes_read, &gio_error) + != G_IO_STATUS_NORMAL) { + g_warning ("IO Channel read error: %s", gio_error->message); + return TRUE; + } + + str = g_string_append_len (str, buf, bytes_read); + + /* In which state is the backend? */ + switch (passwd_handler->backend_state) { + case PASSWD_STATE_AUTH: + /* Passwd is asking for our current password */ + + if (is_string_complete (str->str, "assword: ", "failure", "wrong", "error", NULL)) { + + if (strstr (str->str, "assword: ") != NULL && + strstr (str->str, "incorrect") == NULL && + strstr (str->str, "urrent") == NULL) { + /* Authentication successful */ + + passwd_handler->backend_state = PASSWD_STATE_NEW; + + /* Trigger callback to update authentication status */ + if (passwd_handler->auth_cb) + passwd_handler->auth_cb (passwd_handler, + NULL, + passwd_handler->auth_cb_data); + + } else { + /* Authentication failed */ + g_autoptr(GError) error = NULL; + + error = g_error_new_literal (PASSWD_ERROR, PASSWD_ERROR_AUTH_FAILED, + _("Authentication failed")); + + passwd_handler->changing_password = FALSE; + + /* This error can happen both while authenticating or while changing password: + * if chpasswd_cb is set, this means we're already changing password */ + if (passwd_handler->chpasswd_cb) + passwd_handler->chpasswd_cb (passwd_handler, + error, + passwd_handler->chpasswd_cb_data); + else if (passwd_handler->auth_cb) + passwd_handler->auth_cb (passwd_handler, + error, + passwd_handler->auth_cb_data); + } + + reinit = TRUE; + } + break; + case PASSWD_STATE_NEW: + /* Passwd is asking for our new password */ + + if (is_string_complete (str->str, "assword: ", NULL)) { + /* Advance to next state */ + passwd_handler->backend_state = PASSWD_STATE_RETYPE; + + /* Pop retyped password from queue and into IO channel */ + io_queue_pop (passwd_handler->backend_stdin_queue, passwd_handler->backend_stdin); + + reinit = TRUE; + } + break; + case PASSWD_STATE_RETYPE: + /* Passwd is asking for our retyped new password */ + + if (is_string_complete (str->str, + "successfully", + "short", + "longer", + "palindrome", + "dictionary", + "simple", + "simplistic", + "similar", + "case", + "different", + "wrapped", + "recovered", + "recent", + "unchanged", + "match", + "1 numeric or special", + "failure", + "DIFFERENT", + "BAD PASSWORD", + NULL)) { + + if (strstr (str->str, "successfully") != NULL) { + /* Hooray! */ + + passwd_handler->backend_state = PASSWD_STATE_DONE; + /* Trigger callback to update status */ + if (passwd_handler->chpasswd_cb) + passwd_handler->chpasswd_cb (passwd_handler, + NULL, + passwd_handler->chpasswd_cb_data); + } + else { + /* Ohnoes! */ + g_autoptr(GError) error = NULL; + + if (strstr (str->str, "recovered") != NULL) { + /* What does this indicate? + * "Authentication information cannot be recovered?" from libpam? */ + error = g_error_new_literal (PASSWD_ERROR, PASSWD_ERROR_UNKNOWN, + str->str); + } else if (strstr (str->str, "short") != NULL || + strstr (str->str, "longer") != NULL) { + error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED, + _("The new password is too short")); + } else if (strstr (str->str, "palindrome") != NULL || + strstr (str->str, "simple") != NULL || + strstr (str->str, "simplistic") != NULL || + strstr (str->str, "dictionary") != NULL) { + error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED, + _("The new password is too simple")); + } else if (strstr (str->str, "similar") != NULL || + strstr (str->str, "different") != NULL || + strstr (str->str, "case") != NULL || + strstr (str->str, "wrapped") != NULL) { + error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED, + _("The old and new passwords are too similar")); + } else if (strstr (str->str, "recent") != NULL) { + error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED, + _("The new password has already been used recently.")); + } else if (strstr (str->str, "1 numeric or special") != NULL) { + error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED, + _("The new password must contain numeric or special characters")); + } else if (strstr (str->str, "unchanged") != NULL || + strstr (str->str, "match") != NULL) { + error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED, + _("The old and new passwords are the same")); + } else if (strstr (str->str, "failure") != NULL) { + /* Authentication failure */ + error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_AUTH_FAILED, + _("Your password has been changed since you initially authenticated!")); + } + else if (strstr (str->str, "DIFFERENT")) { + error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED, + _("The new password does not contain enough different characters")); + } + else { + error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_UNKNOWN, + _("Unknown error")); + } + + /* At this point, passwd might have exited, in which case + * child_watch_cb should clean up for us and remove this watcher. + * On some error conditions though, passwd just re-prompts us + * for our new password. */ + passwd_handler->backend_state = PASSWD_STATE_ERR; + + passwd_handler->changing_password = FALSE; + + /* Trigger callback to update status */ + if (passwd_handler->chpasswd_cb) + passwd_handler->chpasswd_cb (passwd_handler, + error, + passwd_handler->chpasswd_cb_data); + } + + reinit = TRUE; + + /* child_watch_cb should clean up for us now */ + } + break; + case PASSWD_STATE_NONE: + /* Passwd is not asking for anything yet */ + if (is_string_complete (str->str, "assword: ", NULL)) { + + /* If the user does not have a password set, + * passwd will immediately ask for the new password, + * so skip the AUTH phase */ + if (is_string_complete (str->str, "new", "New", NULL)) { + g_autofree gchar *pw = NULL; + + passwd_handler->backend_state = PASSWD_STATE_NEW; + + /* since passwd didn't ask for our old password + * in this case, simply remove it from the queue */ + pw = g_queue_pop_head (passwd_handler->backend_stdin_queue); + + /* Pop the IO queue, i.e. send new password */ + io_queue_pop (passwd_handler->backend_stdin_queue, passwd_handler->backend_stdin); + + } else { + + passwd_handler->backend_state = PASSWD_STATE_AUTH; + + /* Pop the IO queue, i.e. send current password */ + io_queue_pop (passwd_handler->backend_stdin_queue, passwd_handler->backend_stdin); + } + + reinit = TRUE; + } + break; + default: + /* Passwd has returned an error */ + reinit = TRUE; + break; + } + + if (reinit) { + g_string_free (str, TRUE); + str = NULL; + } + + /* Continue calling us */ + return TRUE; +} + +/* + * }} Backend communication code + */ + +/* Adds the current password to the IO queue */ +static void +authenticate (PasswdHandler *passwd_handler) +{ + gchar *s; + + s = g_strdup_printf ("%s\n", passwd_handler->current_password); + + g_queue_push_tail (passwd_handler->backend_stdin_queue, s); +} + +/* Adds the new password twice to the IO queue */ +static void +update_password (PasswdHandler *passwd_handler) +{ + gchar *s; + + s = g_strdup_printf ("%s\n", passwd_handler->new_password); + + g_queue_push_tail (passwd_handler->backend_stdin_queue, s); + /* We need to allocate new space because io_queue_pop() g_free()s + * every element of the queue after it's done */ + g_queue_push_tail (passwd_handler->backend_stdin_queue, g_strdup (s)); +} + + +PasswdHandler * +passwd_init (void) +{ + PasswdHandler *passwd_handler; + + passwd_handler = g_new0 (PasswdHandler, 1); + + /* Initialize backend_pid. -1 means the backend is not running */ + passwd_handler->backend_pid = -1; + + /* Initialize IO Channels */ + passwd_handler->backend_stdin = NULL; + passwd_handler->backend_stdout = NULL; + + /* Initialize write queue */ + passwd_handler->backend_stdin_queue = g_queue_new (); + + /* Initialize watchers */ + passwd_handler->backend_child_watch_id = 0; + passwd_handler->backend_stdout_watch_id = 0; + + /* Initialize backend state */ + passwd_handler->backend_state = PASSWD_STATE_NONE; + passwd_handler->changing_password = FALSE; + + return passwd_handler; +} + +void +passwd_destroy (PasswdHandler *passwd_handler) +{ + g_queue_free (passwd_handler->backend_stdin_queue); + stop_passwd (passwd_handler); + g_free (passwd_handler); +} + +void +passwd_authenticate (PasswdHandler *passwd_handler, + const char *current_password, + PasswdCallback cb, + const gpointer user_data) +{ + g_autoptr(GError) error = NULL; + + /* Don't stop if we've already started changing password */ + if (passwd_handler->changing_password) + return; + + /* Clear data from possible previous attempts to change password */ + passwd_handler->new_password = NULL; + passwd_handler->chpasswd_cb = NULL; + passwd_handler->chpasswd_cb_data = NULL; + g_queue_foreach (passwd_handler->backend_stdin_queue, (GFunc) g_free, NULL); + g_queue_clear (passwd_handler->backend_stdin_queue); + + passwd_handler->current_password = current_password; + passwd_handler->auth_cb = cb; + passwd_handler->auth_cb_data = user_data; + + /* Spawn backend */ + stop_passwd (passwd_handler); + + if (!spawn_passwd (passwd_handler, &error)) { + g_warning ("%s", error->message); + return; + } + + authenticate (passwd_handler); + + /* Our IO watcher should now handle the rest */ +} + +gboolean +passwd_change_password (PasswdHandler *passwd_handler, + const char *new_password, + PasswdCallback cb, + const gpointer user_data) +{ + passwd_handler->changing_password = TRUE; + + passwd_handler->new_password = new_password; + passwd_handler->chpasswd_cb = cb; + passwd_handler->chpasswd_cb_data = user_data; + + /* Stop passwd if an error occurred and it is still running */ + if (passwd_handler->backend_state == PASSWD_STATE_ERR) { + + /* Stop passwd, free resources */ + stop_passwd (passwd_handler); + } + + /* Check that the backend is still running, or that an error + * has occurred but it has not yet exited */ + if (passwd_handler->backend_pid == -1) { + /* If it is not, re-run authentication */ + g_autoptr(GError) error = NULL; + + /* Spawn backend */ + stop_passwd (passwd_handler); + + if (!spawn_passwd (passwd_handler, &error)) { + g_warning ("%s", error->message); + return FALSE; + } + + /* Add current and new passwords to queue */ + authenticate (passwd_handler); + update_password (passwd_handler); + } else { + /* Only add new passwords to queue */ + update_password (passwd_handler); + } + + /* Pop new password through the backend. + * If user has no password, popping the queue would output current + * password, while 'passwd' is waiting for the new one. So wait for + * io_watch_stdout() to remove current password from the queue, + * and output the new one for us. + */ + if (passwd_handler->current_password) + io_queue_pop (passwd_handler->backend_stdin_queue, passwd_handler->backend_stdin); + + /* Our IO watcher should now handle the rest */ + + return TRUE; +} diff --git a/panels/user-accounts/run-passwd.h b/panels/user-accounts/run-passwd.h new file mode 100644 index 0000000..c0362bd --- /dev/null +++ b/panels/user-accounts/run-passwd.h @@ -0,0 +1,54 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* run-passwd.h: this file is part of users-admin, a gnome-system-tools frontend + * for user administration. + * + * Copyright (C) 2010 Milan Bouchet-Valat + * + * This program 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 2 of the + * License, or (at your option) any later version. + * + * This program 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 this program; if not, see <http://www.gnu.org/licenses/>. + * + * Authors: Milan Bouchet-Valat <nalimilan@club.fr> + */ + +#pragma once + +struct PasswdHandler; + +typedef struct PasswdHandler PasswdHandler; + +typedef void (*PasswdCallback) (PasswdHandler *passwd_handler, GError *error, const gpointer user_data); + +/* Error codes */ +typedef enum { + PASSWD_ERROR_REJECTED, /* New password is not secure enough */ + PASSWD_ERROR_AUTH_FAILED, /* Wrong old password, or PAM failure */ + PASSWD_ERROR_REAUTH_FAILED, /* Password has changed since first authentication */ + PASSWD_ERROR_BACKEND, /* Backend error */ + PASSWD_ERROR_UNKNOWN /* General error */ +} PasswdError; + + +PasswdHandler *passwd_init (void); + +void passwd_destroy (PasswdHandler *passwd_handler); + +void passwd_authenticate (PasswdHandler *passwd_handler, + const char *current_password, + PasswdCallback cb, + gpointer user_data); + +gboolean passwd_change_password (PasswdHandler *passwd_handler, + const char *new_password, + PasswdCallback cb, + const gpointer user_data); + diff --git a/panels/user-accounts/user-accounts.gresource.xml b/panels/user-accounts/user-accounts.gresource.xml new file mode 100644 index 0000000..ac8756a --- /dev/null +++ b/panels/user-accounts/user-accounts.gresource.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gresources> + <gresource prefix="/org/gnome/control-center/user-accounts"> + <file preprocess="xml-stripblanks">cc-add-user-dialog.ui</file> + <file preprocess="xml-stripblanks">cc-avatar-chooser.ui</file> + <file preprocess="xml-stripblanks">cc-login-history-dialog.ui</file> + <file preprocess="xml-stripblanks">cc-password-dialog.ui</file> + <file preprocess="xml-stripblanks">cc-user-panel.ui</file> + <file preprocess="xml-stripblanks">cc-fingerprint-dialog.ui</file> + <file alias="join-dialog.ui" preprocess="xml-stripblanks">data/join-dialog.ui</file> + <file alias="user-accounts-dialog.css">data/user-accounts-dialog.css</file> + <file alias="cc-fingerprint-dialog.css">data/cc-fingerprint-dialog.css</file> + </gresource> + + <gresource prefix="/org/gnome/Settings/icons/scalable/status"> + <file preprocess="xml-stripblanks" alias="fingerprint-detection-complete-symbolic.svg">data/icons/fingerprint-detection-complete-symbolic.svg</file> + <file preprocess="xml-stripblanks" alias="fingerprint-detection-symbolic.svg">data/icons/fingerprint-detection-symbolic.svg</file> + <file preprocess="xml-stripblanks" alias="fingerprint-detection-warning-symbolic.svg">data/icons/fingerprint-detection-warning-symbolic.svg</file> + </gresource> +</gresources> diff --git a/panels/user-accounts/user-utils.c b/panels/user-accounts/user-utils.c new file mode 100644 index 0000000..5b7bc1f --- /dev/null +++ b/panels/user-accounts/user-utils.c @@ -0,0 +1,471 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright 2009-2010 Red Hat, Inc, + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see <http://www.gnu.org/licenses/>. + * + * Written by: Matthias Clasen <mclasen@redhat.com> + */ + +#include "config.h" + +#include <math.h> +#include <stdlib.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <limits.h> +#include <unistd.h> +#include <utmpx.h> +#include <pwd.h> + +#ifdef __FreeBSD__ +#include <sysexits.h> +#endif + +#include <gio/gio.h> +#include <gio/gunixoutputstream.h> +#include <glib/gi18n.h> +#include <glib/gstdio.h> + +#include "user-utils.h" + +#define IMAGE_SIZE 512 + +/* Taken from defines.h in shadow-utils. On Linux, this value is much smaller + * than the sysconf limit LOGIN_NAME_MAX, and values larger than this will + * result in failure when running useradd. We could check UT_NAMESIZE instead, + * but that is nonstandard. Better to use POSIX utmpx. + */ +gsize +get_username_max_length (void) +{ + return sizeof (((struct utmpx *)NULL)->ut_user); +} + +gboolean +is_username_used (const gchar *username) +{ + struct passwd *pwent; + + if (username == NULL || username[0] == '\0') { + return FALSE; + } + + pwent = getpwnam (username); + + return pwent != NULL; +} + +gboolean +is_valid_name (const gchar *name) +{ + gboolean is_empty = TRUE; + gboolean found_comma = FALSE; + const gchar *c; + + if (name == NULL) + return is_empty; + + /* Valid names must contain: + * 1) at least one character. + * 2) at least one non-"space" character. + * 3) comma character not allowed. Issue #888 + */ + for (c = name; *c; c++) { + gunichar unichar; + + unichar = g_utf8_get_char_validated (c, -1); + + /* Partial UTF-8 sequence or end of string */ + if (unichar == (gunichar) -1 || unichar == (gunichar) -2) + break; + + /* Check for non-space character */ + if (is_empty && !g_unichar_isspace (unichar)) { + is_empty = FALSE; + } + + if (unichar == ',') { + found_comma = TRUE; + break; + } + } + + return !is_empty && !found_comma; +} + +typedef struct { + gchar *username; + gchar *tip; +} isValidUsernameData; + +static void +is_valid_username_data_free (isValidUsernameData *data) +{ + g_clear_pointer (&data->username, g_free); + g_clear_pointer (&data->tip, g_free); + g_free (data); +} + +#ifdef __FreeBSD__ +/* Taken from pw(8) man page. */ +#define E_SUCCESS EX_OK +#define E_BAD_ARG EX_DATAERR +#define E_NOTFOUND EX_NOUSER +#else +/* Taken from usermod.c in shadow-utils. */ +#define E_SUCCESS 0 +#define E_BAD_ARG 3 +#define E_NOTFOUND 6 +#endif + +static void +is_valid_username_child_watch_cb (GPid pid, + gint status, + gpointer user_data) +{ + g_autoptr(GTask) task = G_TASK (user_data); + isValidUsernameData *data = g_task_get_task_data (task); + GError *error = NULL; + gboolean valid = FALSE; + const gchar *tip = NULL; + + if (WIFEXITED (status)) { + switch (WEXITSTATUS (status)) { + case E_NOTFOUND: + valid = TRUE; + break; + case E_BAD_ARG: + tip = _("The username should usually only consist of lower case letters from a-z, digits and the following characters: - _"); + valid = FALSE; + break; + case E_SUCCESS: + tip = _("Sorry, that user name isn’t available. Please try another."); + valid = FALSE; + break; + } + } + + if (valid || tip != NULL) { + data->tip = g_strdup (tip); + g_task_return_boolean (task, valid); + } + else { + g_spawn_check_wait_status (status, &error); + g_task_return_error (task, error); + } + + g_spawn_close_pid (pid); +} + +void +is_valid_username_async (const gchar *username, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer callback_data) +{ + g_autoptr(GTask) task = NULL; + isValidUsernameData *data; + gchar *argv[6]; + GPid pid; + GError *error = NULL; + + task = g_task_new (NULL, cancellable, callback, callback_data); + g_task_set_source_tag (task, is_valid_username_async); + + data = g_new0 (isValidUsernameData, 1); + data->username = g_strdup (username); + g_task_set_task_data (task, data, (GDestroyNotify) is_valid_username_data_free); + + if (username == NULL || username[0] == '\0') { + g_task_return_boolean (task, FALSE); + return; + } + else if (strlen (username) > get_username_max_length ()) { + data->tip = g_strdup (_("The username is too long.")); + g_task_return_boolean (task, FALSE); + return; + } + +#ifdef __FreeBSD__ + /* Abuse "pw usershow -n <name>" in the same way as the code below. We + * don't use "pw usermod -n <name> -N -l <newname>" here because it has + * a special case for "root" to reject changes to the root user. + */ + argv[0] = "pw"; + argv[1] = "usershow"; + argv[2] = "-n"; + argv[3] = data->username; + argv[4] = NULL; +#else + /* "usermod --login" is meant to be used to change a username, but the + * exit codes can be safely abused to check the validity of username. + * However, the current "usermod" implementation may change in the + * future, so it would be nice to have some official way for this + * instead of relying on the current "--login" implementation. + */ + argv[0] = "/usr/sbin/usermod"; + argv[1] = "--login"; + argv[2] = data->username; + argv[3] = "--"; + argv[4] = data->username; + argv[5] = NULL; +#endif + + if (!g_spawn_async (NULL, argv, NULL, + G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD | + G_SPAWN_STDOUT_TO_DEV_NULL | G_SPAWN_STDERR_TO_DEV_NULL, + NULL, NULL, &pid, &error)) { + g_task_return_error (task, error); + return; + } + + g_child_watch_add (pid, (GChildWatchFunc) is_valid_username_child_watch_cb, task); + g_steal_pointer (&task); +} + +gboolean +is_valid_username_finish (GAsyncResult *result, + gchar **tip, + gchar **username, + GError **error) +{ + GTask *task; + isValidUsernameData *data; + + g_return_val_if_fail (g_task_is_valid (result, NULL), FALSE); + + task = G_TASK (result); + data = g_task_get_task_data (task); + + if (tip != NULL) { + *tip = g_steal_pointer (&data->tip); + } + + if (username != NULL) + *username = g_steal_pointer (&data->username); + + return g_task_propagate_boolean (task, error); +} + +GdkPixbuf * +round_image (GdkPixbuf *pixbuf) +{ + GdkPixbuf *dest = NULL; + cairo_surface_t *surface; + cairo_t *cr; + gint size; + + size = gdk_pixbuf_get_width (pixbuf); + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, size, size); + cr = cairo_create (surface); + + /* Clip a circle */ + cairo_arc (cr, size/2, size/2, size/2, 0, 2 * G_PI); + cairo_clip (cr); + cairo_new_path (cr); + + gdk_cairo_set_source_pixbuf (cr, pixbuf, 0, 0); + cairo_paint (cr); + + dest = gdk_pixbuf_get_from_surface (surface, 0, 0, size, size); + cairo_surface_destroy (surface); + cairo_destroy (cr); + + return dest; +} + +static gchar * +extract_initials_from_name (const gchar *name) +{ + GString *initials; + g_autofree gchar *p = NULL; + g_autofree gchar *normalized = NULL; + gunichar unichar; + gpointer q = NULL; + + g_return_val_if_fail (name != NULL, NULL); + + p = g_utf8_strup (name, -1); + normalized = g_utf8_normalize (g_strstrip (p), -1, G_NORMALIZE_DEFAULT_COMPOSE); + if (normalized == NULL) { + return NULL; + } + + initials = g_string_new (""); + + unichar = g_utf8_get_char (normalized); + g_string_append_unichar (initials, unichar); + + q = g_utf8_strrchr (normalized, -1, ' '); + if (q != NULL && g_utf8_next_char (q) != NULL) { + q = g_utf8_next_char (q); + + unichar = g_utf8_get_char (q); + g_string_append_unichar (initials, unichar); + } + + return g_string_free (initials, FALSE); +} + +static GdkRGBA +get_color_for_name (const gchar *name) +{ + // https://gitlab.gnome.org/Community/Design/HIG-app-icons/blob/master/GNOME%20HIG.gpl + static gdouble gnome_color_palette[][3] = { + { 98, 160, 234 }, + { 53, 132, 228 }, + { 28, 113, 216 }, + { 26, 95, 180 }, + { 87, 227, 137 }, + { 51, 209, 122 }, + { 46, 194, 126 }, + { 38, 162, 105 }, + { 248, 228, 92 }, + { 246, 211, 45 }, + { 245, 194, 17 }, + { 229, 165, 10 }, + { 255, 163, 72 }, + { 255, 120, 0 }, + { 230, 97, 0 }, + { 198, 70, 0 }, + { 237, 51, 59 }, + { 224, 27, 36 }, + { 192, 28, 40 }, + { 165, 29, 45 }, + { 192, 97, 203 }, + { 163, 71, 186 }, + { 129, 61, 156 }, + { 97, 53, 131 }, + { 181, 131, 90 }, + { 152, 106, 68 }, + { 134, 94, 60 }, + { 99, 69, 44 } + }; + + GdkRGBA color = { 255, 255, 255, 1.0 }; + guint hash; + gint number_of_colors; + gint idx; + + if (name == NULL || strlen (name) == 0) + return color; + + hash = g_str_hash (name); + number_of_colors = G_N_ELEMENTS (gnome_color_palette); + idx = hash % number_of_colors; + + color.red = gnome_color_palette[idx][0]; + color.green = gnome_color_palette[idx][1]; + color.blue = gnome_color_palette[idx][2]; + + return color; +} + +static cairo_surface_t * +generate_user_picture (const gchar *name, gint size) +{ + PangoFontDescription *font_desc; + g_autofree gchar *initials = extract_initials_from_name (name); + g_autofree gchar *font = g_strdup_printf ("Sans %d", (int)ceil (size / 2.5)); + PangoLayout *layout; + GdkRGBA color = get_color_for_name (name); + cairo_surface_t *surface; + gint width, height; + cairo_t *cr; + + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, + size, + size); + cr = cairo_create (surface); + cairo_rectangle (cr, 0, 0, size, size); + cairo_set_source_rgb (cr, color.red/255.0, color.green/255.0, color.blue/255.0); + cairo_fill (cr); + + /* Draw the initials on top */ + cairo_set_source_rgb (cr, 1.0, 1.0, 1.0); + layout = pango_cairo_create_layout (cr); + pango_layout_set_text (layout, initials, -1); + font_desc = pango_font_description_from_string (font); + pango_layout_set_font_description (layout, font_desc); + pango_font_description_free (font_desc); + + pango_layout_get_size (layout, &width, &height); + cairo_translate (cr, size/2, size/2); + cairo_move_to (cr, - ((double)width / PANGO_SCALE)/2, - ((double)height/PANGO_SCALE)/2); + pango_cairo_show_layout (cr, layout); + cairo_destroy (cr); + + return surface; +} + +void +set_user_icon_data (ActUser *user, + GdkPixbuf *pixbuf) +{ + g_autofree gchar *path = NULL; + gint fd; + g_autoptr(GOutputStream) stream = NULL; + g_autoptr(GError) error = NULL; + + path = g_build_filename (g_get_tmp_dir (), "gnome-control-center-user-icon-XXXXXX", NULL); + fd = g_mkstemp (path); + + if (fd == -1) { + g_warning ("failed to create temporary file for image data"); + return; + } + + stream = g_unix_output_stream_new (fd, TRUE); + + if (!gdk_pixbuf_save_to_stream (pixbuf, stream, "png", NULL, &error, NULL)) { + g_warning ("failed to save image: %s", error->message); + return; + } + + act_user_set_icon_file (user, path); + + /* if we ever make the dbus call async, the g_remove call needs + * to wait for its completion + */ + g_remove (path); +} + +GdkPixbuf * +generate_default_avatar (ActUser *user, gint size) +{ + const gchar *name; + GdkPixbuf *pixbuf = NULL; + cairo_surface_t *surface; + + name = act_user_get_real_name (user); + if (name == NULL) + name = ""; + surface = generate_user_picture (name, size); + + pixbuf = gdk_pixbuf_get_from_surface (surface, 0, 0, size, size); + cairo_surface_destroy (surface); + + return pixbuf; +} + +void +set_default_avatar (ActUser *user) +{ + g_autoptr(GdkPixbuf) pixbuf = NULL; + + pixbuf = generate_default_avatar (user, IMAGE_SIZE); + + set_user_icon_data (user, pixbuf); +} diff --git a/panels/user-accounts/user-utils.h b/panels/user-accounts/user-utils.h new file mode 100644 index 0000000..0ce08b7 --- /dev/null +++ b/panels/user-accounts/user-utils.h @@ -0,0 +1,52 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright 2009-2010 Red Hat, Inc, + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see <http://www.gnu.org/licenses/>. + * + * Written by: Matthias Clasen <mclasen@redhat.com> + */ + +#pragma once + +#include <gtk/gtk.h> +#include <act/act.h> + +G_BEGIN_DECLS + +void set_entry_generation_icon (GtkEntry *entry); +void set_entry_validation_checkmark (GtkEntry *entry); +void set_entry_validation_error (GtkEntry *entry, + const gchar *text); +void clear_entry_validation_error (GtkEntry *entry); + +gsize get_username_max_length (void); +gboolean is_username_used (const gchar *username); +gboolean is_valid_name (const gchar *name); +void is_valid_username_async (const gchar *username, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer callback_data); +gboolean is_valid_username_finish (GAsyncResult *result, + gchar **tip, + gchar **username, + GError **error); +GdkPixbuf *round_image (GdkPixbuf *pixbuf); +GdkPixbuf *generate_default_avatar (ActUser *user, + gint size); +void set_default_avatar (ActUser *user); +void set_user_icon_data (ActUser *user, + GdkPixbuf *pixbuf); + +G_END_DECLS |