summaryrefslogtreecommitdiffstats
path: root/panels/user-accounts
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:45:20 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:45:20 +0000
commitae1c76ff830d146d41e88d6fba724c0a54bce868 (patch)
tree3c354bec95af07be35fc71a4b738268496f1a1c4 /panels/user-accounts
parentInitial commit. (diff)
downloadgnome-control-center-ae1c76ff830d146d41e88d6fba724c0a54bce868.tar.xz
gnome-control-center-ae1c76ff830d146d41e88d6fba724c0a54bce868.zip
Adding upstream version 1:43.6.upstream/1%43.6upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'panels/user-accounts')
-rw-r--r--panels/user-accounts/cc-add-user-dialog.c1717
-rw-r--r--panels/user-accounts/cc-add-user-dialog.h34
-rw-r--r--panels/user-accounts/cc-add-user-dialog.ui411
-rw-r--r--panels/user-accounts/cc-avatar-chooser.c452
-rw-r--r--panels/user-accounts/cc-avatar-chooser.h39
-rw-r--r--panels/user-accounts/cc-avatar-chooser.ui41
-rw-r--r--panels/user-accounts/cc-crop-area.c717
-rw-r--r--panels/user-accounts/cc-crop-area.h42
-rw-r--r--panels/user-accounts/cc-fingerprint-dialog.c1527
-rw-r--r--panels/user-accounts/cc-fingerprint-dialog.h37
-rw-r--r--panels/user-accounts/cc-fingerprint-dialog.ui346
-rw-r--r--panels/user-accounts/cc-fingerprint-manager.c597
-rw-r--r--panels/user-accounts/cc-fingerprint-manager.h74
-rw-r--r--panels/user-accounts/cc-login-history-dialog.c346
-rw-r--r--panels/user-accounts/cc-login-history-dialog.h33
-rw-r--r--panels/user-accounts/cc-login-history-dialog.ui66
-rw-r--r--panels/user-accounts/cc-password-dialog.c530
-rw-r--r--panels/user-accounts/cc-password-dialog.h34
-rw-r--r--panels/user-accounts/cc-password-dialog.ui184
-rw-r--r--panels/user-accounts/cc-realm-manager.c788
-rw-r--r--panels/user-accounts/cc-realm-manager.h97
-rw-r--r--panels/user-accounts/cc-user-panel.c1603
-rw-r--r--panels/user-accounts/cc-user-panel.h29
-rw-r--r--panels/user-accounts/cc-user-panel.ui407
-rw-r--r--panels/user-accounts/data/cc-fingerprint-dialog.css83
-rw-r--r--panels/user-accounts/data/faces/bicycle.jpgbin0 -> 164797 bytes
-rw-r--r--panels/user-accounts/data/faces/book.jpgbin0 -> 107001 bytes
-rw-r--r--panels/user-accounts/data/faces/calculator.jpgbin0 -> 71432 bytes
-rw-r--r--panels/user-accounts/data/faces/cat.jpgbin0 -> 84614 bytes
-rw-r--r--panels/user-accounts/data/faces/coffee2.jpgbin0 -> 59609 bytes
-rw-r--r--panels/user-accounts/data/faces/flower2.jpgbin0 -> 37545 bytes
-rw-r--r--panels/user-accounts/data/faces/gamepad.jpgbin0 -> 48885 bytes
-rw-r--r--panels/user-accounts/data/faces/guitar2.jpgbin0 -> 39121 bytes
-rw-r--r--panels/user-accounts/data/faces/headphones.jpgbin0 -> 50165 bytes
-rw-r--r--panels/user-accounts/data/faces/hummingbird.jpgbin0 -> 44924 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/astronaut.jpgbin0 -> 3034 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/baseball.pngbin0 -> 12985 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/butterfly.pngbin0 -> 17171 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/cat-eye.jpgbin0 -> 5495 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/chess.jpgbin0 -> 3346 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/coffee.jpgbin0 -> 3467 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/dice.jpgbin0 -> 2807 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/energy-arc.jpgbin0 -> 2041 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/fish.jpgbin0 -> 3225 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/flake.jpgbin0 -> 4216 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/flower.jpgbin0 -> 3572 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/grapes.jpgbin0 -> 4045 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/guitar.jpgbin0 -> 2281 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/launch.jpgbin0 -> 3121 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/leaf.jpgbin0 -> 2627 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/lightning.jpgbin0 -> 2621 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/penguin.jpgbin0 -> 2192 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/puppy.jpgbin0 -> 3461 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/sky.jpgbin0 -> 2964 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/soccerball.pngbin0 -> 9267 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/sunflower.jpgbin0 -> 4105 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/sunset.jpgbin0 -> 2752 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/tennis-ball.pngbin0 -> 13432 bytes
-rw-r--r--panels/user-accounts/data/faces/legacy/yellow-rose.jpgbin0 -> 2727 bytes
-rw-r--r--panels/user-accounts/data/faces/mountain.jpgbin0 -> 42261 bytes
-rw-r--r--panels/user-accounts/data/faces/plane.jpgbin0 -> 65115 bytes
-rw-r--r--panels/user-accounts/data/faces/surfer.jpgbin0 -> 101022 bytes
-rw-r--r--panels/user-accounts/data/faces/tomatoes.jpgbin0 -> 103768 bytes
-rw-r--r--panels/user-accounts/data/faces/tree.jpgbin0 -> 99009 bytes
-rw-r--r--panels/user-accounts/data/gnome-user-accounts-panel.desktop.in.in19
-rw-r--r--panels/user-accounts/data/icons/fingerprint-detection-complete-symbolic.svg1
-rw-r--r--panels/user-accounts/data/icons/fingerprint-detection-symbolic.svg1
-rw-r--r--panels/user-accounts/data/icons/fingerprint-detection-warning-symbolic.svg1
-rw-r--r--panels/user-accounts/data/icons/left-index-finger.svg1
-rw-r--r--panels/user-accounts/data/icons/left-little-finger.svg1
-rw-r--r--panels/user-accounts/data/icons/left-middle-finger.svg1
-rw-r--r--panels/user-accounts/data/icons/left-ring-finger.svg1
-rw-r--r--panels/user-accounts/data/icons/left-thumb.svg1
-rw-r--r--panels/user-accounts/data/icons/print_error.svg1
-rw-r--r--panels/user-accounts/data/icons/print_ok.svg1
-rw-r--r--panels/user-accounts/data/icons/right-index-finger.svg1
-rw-r--r--panels/user-accounts/data/icons/right-little-finger.svg1
-rw-r--r--panels/user-accounts/data/icons/right-middle-finger.svg1
-rw-r--r--panels/user-accounts/data/icons/right-ring-finger.svg1
-rw-r--r--panels/user-accounts/data/icons/right-thumb.svg1
-rw-r--r--panels/user-accounts/data/join-dialog.ui166
-rw-r--r--panels/user-accounts/data/net.reactivated.Fprint.Device.xml585
-rw-r--r--panels/user-accounts/data/net.reactivated.Fprint.Manager.xml50
-rw-r--r--panels/user-accounts/data/org.freedesktop.realmd.xml666
-rw-r--r--panels/user-accounts/data/user-accounts-dialog.css23
-rw-r--r--panels/user-accounts/fingerprint-strings.h172
-rw-r--r--panels/user-accounts/icons/meson.build4
-rw-r--r--panels/user-accounts/icons/scalable/org.gnome.Settings-users-symbolic.svg4
-rw-r--r--panels/user-accounts/meson.build199
-rw-r--r--panels/user-accounts/org.gnome.controlcenter.user-accounts.policy.in21
-rw-r--r--panels/user-accounts/pw-utils.c177
-rw-r--r--panels/user-accounts/pw-utils.h31
-rw-r--r--panels/user-accounts/run-passwd.c737
-rw-r--r--panels/user-accounts/run-passwd.h54
-rw-r--r--panels/user-accounts/user-accounts.gresource.xml20
-rw-r--r--panels/user-accounts/user-utils.c471
-rw-r--r--panels/user-accounts/user-utils.h52
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
new file mode 100644
index 0000000..c598251
--- /dev/null
+++ b/panels/user-accounts/data/faces/bicycle.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/book.jpg b/panels/user-accounts/data/faces/book.jpg
new file mode 100644
index 0000000..abda4b7
--- /dev/null
+++ b/panels/user-accounts/data/faces/book.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/calculator.jpg b/panels/user-accounts/data/faces/calculator.jpg
new file mode 100644
index 0000000..43ece1e
--- /dev/null
+++ b/panels/user-accounts/data/faces/calculator.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/cat.jpg b/panels/user-accounts/data/faces/cat.jpg
new file mode 100644
index 0000000..99275b2
--- /dev/null
+++ b/panels/user-accounts/data/faces/cat.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/coffee2.jpg b/panels/user-accounts/data/faces/coffee2.jpg
new file mode 100644
index 0000000..4be830d
--- /dev/null
+++ b/panels/user-accounts/data/faces/coffee2.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/flower2.jpg b/panels/user-accounts/data/faces/flower2.jpg
new file mode 100644
index 0000000..b77e717
--- /dev/null
+++ b/panels/user-accounts/data/faces/flower2.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/gamepad.jpg b/panels/user-accounts/data/faces/gamepad.jpg
new file mode 100644
index 0000000..9843758
--- /dev/null
+++ b/panels/user-accounts/data/faces/gamepad.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/guitar2.jpg b/panels/user-accounts/data/faces/guitar2.jpg
new file mode 100644
index 0000000..f1ad374
--- /dev/null
+++ b/panels/user-accounts/data/faces/guitar2.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/headphones.jpg b/panels/user-accounts/data/faces/headphones.jpg
new file mode 100644
index 0000000..2ad9175
--- /dev/null
+++ b/panels/user-accounts/data/faces/headphones.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/hummingbird.jpg b/panels/user-accounts/data/faces/hummingbird.jpg
new file mode 100644
index 0000000..d06a3cf
--- /dev/null
+++ b/panels/user-accounts/data/faces/hummingbird.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/astronaut.jpg b/panels/user-accounts/data/faces/legacy/astronaut.jpg
new file mode 100644
index 0000000..4b79f0e
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/astronaut.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/baseball.png b/panels/user-accounts/data/faces/legacy/baseball.png
new file mode 100644
index 0000000..0d6dfdb
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/baseball.png
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/butterfly.png b/panels/user-accounts/data/faces/legacy/butterfly.png
new file mode 100644
index 0000000..66b813c
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/butterfly.png
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/cat-eye.jpg b/panels/user-accounts/data/faces/legacy/cat-eye.jpg
new file mode 100644
index 0000000..c818bd5
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/cat-eye.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/chess.jpg b/panels/user-accounts/data/faces/legacy/chess.jpg
new file mode 100644
index 0000000..7abb8a4
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/chess.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/coffee.jpg b/panels/user-accounts/data/faces/legacy/coffee.jpg
new file mode 100644
index 0000000..46e8fc5
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/coffee.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/dice.jpg b/panels/user-accounts/data/faces/legacy/dice.jpg
new file mode 100644
index 0000000..641b124
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/dice.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/energy-arc.jpg b/panels/user-accounts/data/faces/legacy/energy-arc.jpg
new file mode 100644
index 0000000..9f4c892
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/energy-arc.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/fish.jpg b/panels/user-accounts/data/faces/legacy/fish.jpg
new file mode 100644
index 0000000..fc363d6
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/fish.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/flake.jpg b/panels/user-accounts/data/faces/legacy/flake.jpg
new file mode 100644
index 0000000..5546d7e
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/flake.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/flower.jpg b/panels/user-accounts/data/faces/legacy/flower.jpg
new file mode 100644
index 0000000..3e41ba4
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/flower.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/grapes.jpg b/panels/user-accounts/data/faces/legacy/grapes.jpg
new file mode 100644
index 0000000..3d31daf
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/grapes.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/guitar.jpg b/panels/user-accounts/data/faces/legacy/guitar.jpg
new file mode 100644
index 0000000..9e8834f
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/guitar.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/launch.jpg b/panels/user-accounts/data/faces/legacy/launch.jpg
new file mode 100644
index 0000000..7c7bf43
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/launch.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/leaf.jpg b/panels/user-accounts/data/faces/legacy/leaf.jpg
new file mode 100644
index 0000000..5354103
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/leaf.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/lightning.jpg b/panels/user-accounts/data/faces/legacy/lightning.jpg
new file mode 100644
index 0000000..736ccd5
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/lightning.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/penguin.jpg b/panels/user-accounts/data/faces/legacy/penguin.jpg
new file mode 100644
index 0000000..2a8dfd6
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/penguin.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/puppy.jpg b/panels/user-accounts/data/faces/legacy/puppy.jpg
new file mode 100644
index 0000000..ab55a8b
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/puppy.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/sky.jpg b/panels/user-accounts/data/faces/legacy/sky.jpg
new file mode 100644
index 0000000..841f90e
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/sky.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/soccerball.png b/panels/user-accounts/data/faces/legacy/soccerball.png
new file mode 100644
index 0000000..56588a9
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/soccerball.png
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/sunflower.jpg b/panels/user-accounts/data/faces/legacy/sunflower.jpg
new file mode 100644
index 0000000..6102b8b
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/sunflower.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/sunset.jpg b/panels/user-accounts/data/faces/legacy/sunset.jpg
new file mode 100644
index 0000000..48b6223
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/sunset.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/tennis-ball.png b/panels/user-accounts/data/faces/legacy/tennis-ball.png
new file mode 100644
index 0000000..a1beb50
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/tennis-ball.png
Binary files differ
diff --git a/panels/user-accounts/data/faces/legacy/yellow-rose.jpg b/panels/user-accounts/data/faces/legacy/yellow-rose.jpg
new file mode 100644
index 0000000..7f1de96
--- /dev/null
+++ b/panels/user-accounts/data/faces/legacy/yellow-rose.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/mountain.jpg b/panels/user-accounts/data/faces/mountain.jpg
new file mode 100644
index 0000000..8425e0d
--- /dev/null
+++ b/panels/user-accounts/data/faces/mountain.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/plane.jpg b/panels/user-accounts/data/faces/plane.jpg
new file mode 100644
index 0000000..a0c4506
--- /dev/null
+++ b/panels/user-accounts/data/faces/plane.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/surfer.jpg b/panels/user-accounts/data/faces/surfer.jpg
new file mode 100644
index 0000000..47f19b0
--- /dev/null
+++ b/panels/user-accounts/data/faces/surfer.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/tomatoes.jpg b/panels/user-accounts/data/faces/tomatoes.jpg
new file mode 100644
index 0000000..ebdb6ba
--- /dev/null
+++ b/panels/user-accounts/data/faces/tomatoes.jpg
Binary files differ
diff --git a/panels/user-accounts/data/faces/tree.jpg b/panels/user-accounts/data/faces/tree.jpg
new file mode 100644
index 0000000..8a619d3
--- /dev/null
+++ b/panels/user-accounts/data/faces/tree.jpg
Binary files differ
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