diff options
Diffstat (limited to 'gnome-initial-setup/pages/account')
21 files changed, 5278 insertions, 0 deletions
diff --git a/gnome-initial-setup/pages/account/account.gresource.xml b/gnome-initial-setup/pages/account/account.gresource.xml new file mode 100644 index 0000000..d698ba9 --- /dev/null +++ b/gnome-initial-setup/pages/account/account.gresource.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gresources> + <gresource prefix="/org/gnome/initial-setup"> + <file preprocess="xml-stripblanks" alias="gis-account-avatar-chooser.ui">gis-account-avatar-chooser.ui</file> + <file preprocess="xml-stripblanks" alias="gis-account-page.ui">gis-account-page.ui</file> + <file preprocess="xml-stripblanks" alias="gis-account-page-local.ui">gis-account-page-local.ui</file> + <file preprocess="xml-stripblanks" alias="gis-account-page-enterprise.ui">gis-account-page-enterprise.ui</file> + </gresource> +</gresources> diff --git a/gnome-initial-setup/pages/account/gis-account-avatar-chooser.ui b/gnome-initial-setup/pages/account/gis-account-avatar-chooser.ui new file mode 100644 index 0000000..dff58a1 --- /dev/null +++ b/gnome-initial-setup/pages/account/gis-account-avatar-chooser.ui @@ -0,0 +1,51 @@ +<?xml version="1.0"?> +<interface> + <template class="UmPhotoDialog" parent="GtkPopover"> + <property name="height-request">360</property> + <property name="width-request">480</property> + <child> + <object class="GtkBox"> + <property name="orientation">GTK_ORIENTATION_VERTICAL</property> + <child> + <object class="GtkFlowBox" id="recent_pictures"> + <property name="halign">start</property> + <property name="margin-top">20</property> + <property name="margin-start">20</property> + <property name="margin-end">20</property> + <property name="margin-bottom">0</property> + <property name="selection-mode">none</property> + </object> + </child> + <child> + <object class="GtkFlowBox" id="flowbox"> + <property name="margin-top">12</property> + <property name="margin-bottom">12</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="selection-mode">none</property> + </object> + </child> + <child> + <object class="GtkBox"> + <property name="halign">GTK_ALIGN_CENTER</property> + <property name="margin-top">12</property> + <property name="margin-bottom">12</property> + <property name="margin-start">12</property> + <property name="margin-end">12</property> + <property name="spacing">12</property> + <child> + <object class="GtkButton" id="take_picture_button"> + <property name="visible">False</property> + <property name="label" translatable="yes">Take a Picture…</property> + <signal name="clicked" handler="webcam_icon_selected" swapped="yes"/> + <style> + <class name="suggested-action"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> diff --git a/gnome-initial-setup/pages/account/gis-account-page-enterprise.c b/gnome-initial-setup/pages/account/gis-account-page-enterprise.c new file mode 100644 index 0000000..02a4f3b --- /dev/null +++ b/gnome-initial-setup/pages/account/gis-account-page-enterprise.c @@ -0,0 +1,863 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2013 Red Hat + * + * 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: + * Jasper St. Pierre <jstpierre@mecheye.net> + */ + +#include "config.h" + +#include "gis-account-page-enterprise.h" +#include "gnome-initial-setup.h" + +#include <glib/gi18n.h> +#include <gio/gio.h> + +#include <act/act-user-manager.h> + +#include "um-realm-manager.h" +#include "um-utils.h" + +#include "gis-page-header.h" + +static void join_show_prompt (GisAccountPageEnterprise *page, + GError *error); + +static void on_join_login (GObject *source, + GAsyncResult *result, + gpointer user_data); + +static void on_realm_joined (GObject *source, + GAsyncResult *result, + gpointer user_data); + +struct _GisAccountPageEnterprise +{ + AdwBin parent; + + GtkWidget *header; + GtkWidget *login; + GtkWidget *password; + GtkWidget *domain; + GtkWidget *domain_entry; + GtkTreeModel *realms_model; + + GtkWidget *join_dialog; + GtkWidget *join_name; + GtkWidget *join_password; + GtkWidget *join_domain; + GtkWidget *join_computer; + + ActUserManager *act_client; + ActUser *act_user; + + guint realmd_watch; + UmRealmManager *realm_manager; + gboolean domain_chosen; + + /* Valid during apply */ + UmRealmObject *realm; + GCancellable *cancellable; + gboolean join_prompted; + + GisPageApplyCallback apply_complete_callback; + gpointer apply_complete_data; +}; + +G_DEFINE_TYPE (GisAccountPageEnterprise, gis_account_page_enterprise, ADW_TYPE_BIN); + +enum { + VALIDATION_CHANGED, + USER_CACHED, + LAST_SIGNAL, +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +static void +clear_password_validation_error (GtkWidget *entry) +{ + gtk_widget_remove_css_class (entry, "error"); + gtk_widget_set_tooltip_text (entry, NULL); +} + +static void +set_password_validation_error (GtkWidget *entry, + const gchar *text) +{ + gtk_widget_add_css_class (entry, "error"); + gtk_widget_set_tooltip_text (entry, text); +} + +static void +validation_changed (GisAccountPageEnterprise *page) +{ + g_signal_emit (page, signals[VALIDATION_CHANGED], 0); +} + +static void +apply_complete (GisAccountPageEnterprise *page, + gboolean valid) +{ + page->apply_complete_callback (NULL, valid, page->apply_complete_data); +} + +static void +show_error_dialog (GisAccountPageEnterprise *page, + const gchar *message, + GError *error) +{ + GtkWidget *dialog; + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (page))), + GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, + 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)); +} + +gboolean +gis_account_page_enterprise_validate (GisAccountPageEnterprise *page) +{ + const gchar *name; + gboolean valid_name; + gboolean valid_domain; + GtkTreeIter iter; + + name = gtk_editable_get_text (GTK_EDITABLE (page->login)); + valid_name = is_valid_name (name); + + if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX (page->domain), &iter)) { + gtk_tree_model_get (gtk_combo_box_get_model (GTK_COMBO_BOX (page->domain)), + &iter, 0, &name, -1); + } else { + name = gtk_editable_get_text (GTK_EDITABLE (page->domain_entry)); + } + + valid_domain = is_valid_name (name); + return valid_name && valid_domain; +} + +static void +on_permit_user_login (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GisAccountPageEnterprise *page = user_data; + UmRealmCommon *common; + GError *error = NULL; + gchar *login; + + common = UM_REALM_COMMON (source); + um_realm_common_call_change_login_policy_finish (common, result, &error); + if (error == 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. + */ + login = um_realm_calculate_login (common, gtk_editable_get_text (GTK_EDITABLE (page->login))); + g_return_if_fail (login != NULL); + + g_debug ("Caching remote user: %s", login); + + page->act_user = act_user_manager_cache_user (page->act_client, login, NULL); + act_user_set_account_type (page->act_user, ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR); + g_signal_emit (page, signals[USER_CACHED], 0, page->act_user, gtk_editable_get_text (GTK_EDITABLE (page->password))); + apply_complete (page, TRUE); + + g_free (login); + } else { + show_error_dialog (page, _("Failed to register account"), error); + g_message ("Couldn't permit logins on account: %s", error->message); + g_error_free (error); + apply_complete (page, FALSE); + } +} + +static void +enterprise_permit_user_login (GisAccountPageEnterprise *page, UmRealmObject *realm) +{ + UmRealmCommon *common; + gchar *login; + const gchar *add[2]; + const gchar *remove[1]; + GVariant *options; + + common = um_realm_object_get_common (realm); + + login = um_realm_calculate_login (common, gtk_editable_get_text (GTK_EDITABLE (page->login))); + 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); + + um_realm_common_call_change_login_policy (common, "", + add, remove, options, + page->cancellable, + on_permit_user_login, + page); + + g_object_unref (common); + g_free (login); +} + +static void +on_set_static_hostname (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GisAccountPageEnterprise *page = user_data; + GError *error = NULL; + GVariant *retval; + + retval = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source), result, &error); + if (error != NULL) { + join_show_prompt (page, error); + g_error_free (error); + return; + } + + g_variant_unref (retval); + + /* Prompted for some admin credentials, try to use them to log in */ + um_realm_login (page->realm, + gtk_editable_get_text (GTK_EDITABLE (page->join_name)), + gtk_editable_get_text (GTK_EDITABLE (page->join_password)), + page->cancellable, on_join_login, page); +} + +static void +on_join_response (GtkDialog *dialog, + gint response, + gpointer user_data) +{ + GisAccountPageEnterprise *page = user_data; + GDBusConnection *connection; + GError *error = NULL; + gchar hostname[128]; + const gchar *name; + + gtk_widget_hide (GTK_WIDGET (dialog)); + if (response != GTK_RESPONSE_OK) { + apply_complete (page, FALSE); + return; + } + + name = gtk_editable_get_text (GTK_EDITABLE (page->join_computer)); + if (gethostname (hostname, sizeof (hostname)) == 0 && + !g_str_equal (name, hostname)) { + g_debug ("Setting StaticHostname to '%s'", name); + + connection = g_bus_get_sync (G_BUS_TYPE_SYSTEM, page->cancellable, &error); + if (error != NULL) { + apply_complete (page, FALSE); + g_warning ("Could not get DBus connection: %s", error->message); + g_error_free (error); + return; + } + + g_dbus_connection_call (connection, "org.freedesktop.hostname1", + "/org/freedesktop/hostname1", "org.freedesktop.hostname1", + "SetStaticHostname", + g_variant_new ("(sb)", name, TRUE), + G_VARIANT_TYPE ("()"), + G_DBUS_CALL_FLAGS_NONE, + G_MAXINT, NULL, on_set_static_hostname, page); + + } else { + name = gtk_editable_get_text (GTK_EDITABLE (page->join_name)); + g_debug ("Logging in as admin user: %s", name); + + /* Prompted for some admin credentials, try to use them to log in */ + um_realm_login (page->realm, name, + gtk_editable_get_text (GTK_EDITABLE (page->join_password)), + NULL, on_join_login, page); + } +} + +static void +join_show_prompt (GisAccountPageEnterprise *page, + GError *error) +{ + UmRealmKerberosMembership *membership; + UmRealmKerberos *kerberos; + gchar hostname[128]; + const gchar *name; + + gtk_editable_set_text (GTK_EDITABLE (page->join_password), ""); + gtk_widget_grab_focus (GTK_WIDGET (page->join_password)); + + kerberos = um_realm_object_get_kerberos (page->realm); + membership = um_realm_object_get_kerberos_membership (page->realm); + + gtk_label_set_text (GTK_LABEL (page->join_domain), + um_realm_kerberos_get_domain_name (kerberos)); + + if (gethostname (hostname, sizeof (hostname)) == 0) + gtk_editable_set_text (GTK_EDITABLE (page->join_computer), hostname); + + clear_entry_validation_error (GTK_ENTRY (page->join_name)); + clear_password_validation_error (page->join_password); + + if (!page->join_prompted) { + name = um_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 (page->join_name), name); + } else { + gtk_widget_grab_focus (GTK_WIDGET (page->join_name)); + } + + } else if (g_error_matches (error, UM_REALM_ERROR, UM_REALM_ERROR_BAD_HOSTNAME)) { + g_debug ("Bad host name: %s", error->message); + set_entry_validation_error (GTK_ENTRY (page->join_computer), error->message); + + } else if (g_error_matches (error, UM_REALM_ERROR, UM_REALM_ERROR_BAD_PASSWORD)) { + g_debug ("Bad admin password: %s", error->message); + set_password_validation_error (page->join_password, error->message); + + } else { + g_debug ("Admin login failure: %s", error->message); + g_dbus_error_strip_remote_error (error); + set_entry_validation_error (GTK_ENTRY (page->join_name), error->message); + } + + g_debug ("Showing admin password dialog"); + gtk_window_set_transient_for (GTK_WINDOW (page->join_dialog), + GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (page)))); + gtk_window_set_modal (GTK_WINDOW (page->join_dialog), TRUE); + gtk_window_present (GTK_WINDOW (page->join_dialog)); + + page->join_prompted = TRUE; + g_object_unref (kerberos); + g_object_unref (membership); + + /* And now we wait for on_join_response() */ +} + +static void +on_join_login (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GisAccountPageEnterprise *page = user_data; + GError *error = NULL; + GBytes *creds; + + um_realm_login_finish (page->realm, result, &creds, &error); + + /* Logged in as admin successfully, use creds to join domain */ + if (error == NULL) { + if (!um_realm_join_as_admin (page->realm, + gtk_editable_get_text (GTK_EDITABLE (page->join_name)), + gtk_editable_get_text (GTK_EDITABLE (page->join_password)), + creds, NULL, on_realm_joined, page)) { + show_error_dialog (page, _("No supported way to authenticate with this domain"), NULL); + g_message ("Authenticating as admin is not supported by the realm"); + } + + g_bytes_unref (creds); + + /* Couldn't login as admin, show prompt again */ + } else { + join_show_prompt (page, error); + g_message ("Couldn't log in as admin to join domain: %s", error->message); + g_error_free (error); + } +} + +static void +on_realm_joined (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GisAccountPageEnterprise *page = user_data; + GError *error = NULL; + + um_realm_join_finish (page->realm, result, &error); + + /* Yay, joined the domain, register the user locally */ + if (error == NULL) { + g_debug ("Joining realm completed successfully"); + enterprise_permit_user_login (page, page->realm); + + /* Credential failure while joining domain, prompt for admin creds */ + } else if (g_error_matches (error, UM_REALM_ERROR, UM_REALM_ERROR_BAD_LOGIN) || + g_error_matches (error, UM_REALM_ERROR, UM_REALM_ERROR_BAD_PASSWORD) || + g_error_matches (error, UM_REALM_ERROR, UM_REALM_ERROR_BAD_HOSTNAME)) { + g_debug ("Joining realm failed due to credentials or host name"); + + join_show_prompt (page, error); + + /* Other failure */ + } else { + show_error_dialog (page, _("Failed to join domain"), error); + g_message ("Failed to join the domain: %s", error->message); + apply_complete (page, FALSE); + } + + g_clear_error (&error); +} + +static void +on_realm_login (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GisAccountPageEnterprise *page = user_data; + GError *error = NULL; + GBytes *creds = NULL; + + um_realm_login_finish (page->realm, result, &creds, &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, UM_REALM_ERROR, UM_REALM_ERROR_CANNOT_AUTH)) { + g_clear_error (&error); + creds = NULL; + } + + if (error == NULL) { + + /* Already joined to the domain, just register this user */ + if (um_realm_is_configured (page->realm)) { + g_debug ("Already joined to this realm"); + enterprise_permit_user_login (page, page->realm); + + /* Join the domain, try using the user's creds */ + } else if (creds == NULL || + !um_realm_join_as_user (page->realm, + gtk_editable_get_text (GTK_EDITABLE (page->login)), + gtk_editable_get_text (GTK_EDITABLE (page->password)), + creds, page->cancellable, + on_realm_joined, + page)) { + + /* If we can't do user auth, try to authenticate as admin */ + g_debug ("Cannot join with user credentials"); + + join_show_prompt (page, error); + } + + g_bytes_unref (creds); + + /* A problem with the user's login name or password */ + } else if (g_error_matches (error, UM_REALM_ERROR, UM_REALM_ERROR_BAD_LOGIN)) { + g_debug ("Problem with the user's login: %s", error->message); + set_entry_validation_error (GTK_ENTRY (page->login), error->message); + gtk_widget_grab_focus (page->login); + apply_complete (page, FALSE); + + } else if (g_error_matches (error, UM_REALM_ERROR, UM_REALM_ERROR_BAD_PASSWORD)) { + g_debug ("Problem with the user's password: %s", error->message); + set_password_validation_error (page->password, error->message); + gtk_widget_grab_focus (page->password); + apply_complete (page, FALSE); + + /* Other login failure */ + } else { + show_error_dialog (page, _("Failed to log into domain"), error); + g_message ("Couldn't log in as user: %s", error->message); + apply_complete (page, FALSE); + } + + g_clear_error (&error); +} + +static void +enterprise_check_login (GisAccountPageEnterprise *page) +{ + + g_assert (page->realm); + + um_realm_login (page->realm, + gtk_editable_get_text (GTK_EDITABLE (page->login)), + gtk_editable_get_text (GTK_EDITABLE (page->password)), + page->cancellable, + on_realm_login, + page); +} + +static void +on_realm_discover_input (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GisAccountPageEnterprise *page = user_data; + GError *error = NULL; + GList *realms; + + realms = um_realm_manager_discover_finish (page->realm_manager, + result, &error); + + /* Found a realm, log user into domain */ + if (error == NULL) { + g_assert (realms != NULL); + page->realm = g_object_ref (realms->data); + enterprise_check_login (page); + g_list_free_full (realms, g_object_unref); + + } else { + /* The domain is likely invalid */ + g_dbus_error_strip_remote_error (error); + g_message ("Couldn't discover domain: %s", error->message); + gtk_widget_grab_focus (page->domain_entry); + set_entry_validation_error (GTK_ENTRY (page->domain_entry), error->message); + apply_complete (page, FALSE); + g_error_free (error); + } +} + +static void +enterprise_add_user (GisAccountPageEnterprise *page) +{ + GtkTreeIter iter; + + page->join_prompted = FALSE; + g_clear_object (&page->realm); + + /* Already know about this realm, try to login as user */ + if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX (page->domain), &iter)) { + gtk_tree_model_get (gtk_combo_box_get_model (GTK_COMBO_BOX (page->domain)), + &iter, 1, &page->realm, -1); + enterprise_check_login (page); + + /* Something the user typed, we need to discover realm */ + } else { + um_realm_manager_discover (page->realm_manager, + gtk_editable_get_text (GTK_EDITABLE (page->domain_entry)), + page->cancellable, + on_realm_discover_input, + page); + } +} + +gboolean +gis_account_page_enterprise_apply (GisAccountPageEnterprise *page, + GCancellable *cancellable, + GisPageApplyCallback callback, + gpointer data) +{ + GisPage *account_page = GIS_PAGE (data); + + /* Parental controls are not enterprise ready. It’s possible for them to have + * been enabled if the user enabled them, applied the account-local page, and + * then went back and decided to go all enterprise instead. */ + gis_driver_set_parental_controls_enabled (account_page->driver, FALSE); + + page->apply_complete_callback = callback; + page->apply_complete_data = data; + page->cancellable = g_object_ref (cancellable); + enterprise_add_user (page); + return TRUE; +} + +static gchar * +realm_get_name (UmRealmObject *realm) +{ + UmRealmCommon *common; + gchar *name; + + common = um_realm_object_get_common (realm); + name = g_strdup (um_realm_common_get_name (common)); + g_object_unref (common); + + return name; +} + +static gboolean +model_contains_realm (GtkTreeModel *model, + const gchar *realm_name) +{ + gboolean contains = FALSE; + GtkTreeIter iter; + gboolean match; + gchar *name; + gboolean ret; + + ret = gtk_tree_model_get_iter_first (model, &iter); + while (ret) { + gtk_tree_model_get (model, &iter, 0, &name, -1); + match = (g_strcmp0 (name, realm_name) == 0); + g_free (name); + if (match) { + g_debug ("ignoring duplicate realm: %s", realm_name); + contains = TRUE; + break; + } + ret = gtk_tree_model_iter_next (model, &iter); + } + + return contains; +} + +static void +enterprise_add_realm (GisAccountPageEnterprise *page, + UmRealmObject *realm) +{ + GtkTreeIter iter; + gchar *name; + + name = realm_get_name (realm); + + /* + * Don't add a second realm if we already have one with this name. + * Sometimes realmd returns two 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. + */ + + if (!model_contains_realm (GTK_TREE_MODEL (page->realms_model), name)) { + gtk_list_store_append (GTK_LIST_STORE (page->realms_model), &iter); + gtk_list_store_set (GTK_LIST_STORE (page->realms_model), &iter, + 0, name, + 1, realm, + -1); + + if (!page->domain_chosen && um_realm_is_configured (realm)) + gtk_combo_box_set_active_iter (GTK_COMBO_BOX (page->domain), &iter); + + g_debug ("added realm to drop down: %s %s", name, + g_dbus_object_get_object_path (G_DBUS_OBJECT (realm))); + } + + g_free (name); +} + +static void +on_manager_realm_added (UmRealmManager *manager, + UmRealmObject *realm, + gpointer user_data) +{ + GisAccountPageEnterprise *page = user_data; + enterprise_add_realm (page, realm); +} + +static void +on_realm_manager_created (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GisAccountPageEnterprise *page = user_data; + GError *error = NULL; + GList *realms, *l; + + g_clear_object (&page->realm_manager); + page->realm_manager = um_realm_manager_new_finish (result, &error); + + if (error != NULL) { + g_warning ("Couldn't contact realmd service: %s", error->message); + g_error_free (error); + return; + } + + /* Lookup all the realm objects */ + realms = um_realm_manager_get_realms (page->realm_manager); + for (l = realms; l != NULL; l = g_list_next (l)) + enterprise_add_realm (page, l->data); + + g_list_free (realms); + g_signal_connect (page->realm_manager, "realm-added", + G_CALLBACK (on_manager_realm_added), page); + + /* When no realms try to discover a sensible default, triggers realm-added signal */ + um_realm_manager_discover (page->realm_manager, "", NULL, NULL, NULL); + gtk_widget_set_visible (GTK_WIDGET (page), TRUE); +} + +static void +on_realmd_appeared (GDBusConnection *connection, + const gchar *name, + const gchar *name_owner, + gpointer user_data) +{ + GisAccountPageEnterprise *page = user_data; + um_realm_manager_new (NULL, on_realm_manager_created, page); +} + +static void +on_realmd_disappeared (GDBusConnection *unused1, + const gchar *unused2, + gpointer user_data) +{ + GisAccountPageEnterprise *page = user_data; + + if (page->realm_manager != NULL) { + g_signal_handlers_disconnect_by_func (page->realm_manager, + on_manager_realm_added, + page); + g_clear_object (&page->realm_manager); + } + + gtk_widget_set_visible (GTK_WIDGET (page), FALSE); +} + +static void +on_domain_changed (GtkComboBox *widget, + gpointer user_data) +{ + GisAccountPageEnterprise *page = user_data; + + page->domain_chosen = TRUE; + validation_changed (page); + clear_entry_validation_error (GTK_ENTRY (gtk_combo_box_get_child (widget))); +} + +static void +on_entry_changed (GtkEditable *editable, + gpointer user_data) +{ + GisAccountPageEnterprise *page = user_data; + validation_changed (page); + clear_entry_validation_error (GTK_ENTRY (editable)); +} + +static void +on_password_changed (GtkEditable *editable, + gpointer user_data) +{ + clear_password_validation_error (GTK_WIDGET (editable)); +} + +static void +gis_account_page_enterprise_realize (GtkWidget *widget) +{ + GisAccountPageEnterprise *page = GIS_ACCOUNT_PAGE_ENTERPRISE (widget); + GtkWidget *gis_page; + + gis_page = gtk_widget_get_ancestor (widget, GIS_TYPE_PAGE); + g_object_bind_property (gis_page, "small-screen", + page->header, "show-icon", + G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN); + + GTK_WIDGET_CLASS (gis_account_page_enterprise_parent_class)->realize (widget); +} + +static void +gis_account_page_enterprise_constructed (GObject *object) +{ + GisAccountPageEnterprise *page = GIS_ACCOUNT_PAGE_ENTERPRISE (object); + + G_OBJECT_CLASS (gis_account_page_enterprise_parent_class)->constructed (object); + + page->act_client = act_user_manager_get_default (); + + page->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, + page, NULL); + + g_signal_connect (page->join_dialog, "response", + G_CALLBACK (on_join_response), page); + g_signal_connect (page->domain, "changed", + G_CALLBACK (on_domain_changed), page); + g_signal_connect (page->login, "changed", + G_CALLBACK (on_entry_changed), page); + g_signal_connect (page->password, "changed", + G_CALLBACK (on_password_changed), page); + g_signal_connect (page->join_password, "changed", + G_CALLBACK (on_password_changed), page); +} + +static void +gis_account_page_enterprise_dispose (GObject *object) +{ + GisAccountPageEnterprise *page = GIS_ACCOUNT_PAGE_ENTERPRISE (object); + + if (page->realmd_watch) + g_bus_unwatch_name (page->realmd_watch); + + page->realmd_watch = 0; + + g_cancellable_cancel (page->cancellable); + + g_clear_object (&page->realm_manager); + g_clear_object (&page->realm); + g_clear_object (&page->cancellable); + + G_OBJECT_CLASS (gis_account_page_enterprise_parent_class)->dispose (object); +} + +static void +gis_account_page_enterprise_class_init (GisAccountPageEnterpriseClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->constructed = gis_account_page_enterprise_constructed; + object_class->dispose = gis_account_page_enterprise_dispose; + + widget_class->realize = gis_account_page_enterprise_realize; + + gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (klass), "/org/gnome/initial-setup/gis-account-page-enterprise.ui"); + + gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageEnterprise, login); + gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageEnterprise, password); + gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageEnterprise, domain); + gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageEnterprise, realms_model); + gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageEnterprise, header); + + gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageEnterprise, join_dialog); + gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageEnterprise, join_name); + gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageEnterprise, join_password); + gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageEnterprise, join_domain); + gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageEnterprise, join_computer); + + signals[VALIDATION_CHANGED] = g_signal_new ("validation-changed", GIS_TYPE_ACCOUNT_PAGE_ENTERPRISE, + G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); + signals[USER_CACHED] = g_signal_new ("user-cached", GIS_TYPE_ACCOUNT_PAGE_ENTERPRISE, + G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, + G_TYPE_NONE, 2, ACT_TYPE_USER, G_TYPE_STRING); +} + +static void +gis_account_page_enterprise_init (GisAccountPageEnterprise *page) +{ + g_type_ensure (GIS_TYPE_PAGE_HEADER); + gtk_widget_init_template (GTK_WIDGET (page)); + + page->domain_entry = gtk_combo_box_get_child (GTK_COMBO_BOX (page->domain)); +} + +void +gis_account_page_enterprise_shown (GisAccountPageEnterprise *page) +{ + gtk_widget_grab_focus (page->domain_entry); +} diff --git a/gnome-initial-setup/pages/account/gis-account-page-enterprise.h b/gnome-initial-setup/pages/account/gis-account-page-enterprise.h new file mode 100644 index 0000000..c156f44 --- /dev/null +++ b/gnome-initial-setup/pages/account/gis-account-page-enterprise.h @@ -0,0 +1,42 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2013 Red Hat + * + * 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: + * Jasper St. Pierre <jstpierre@mecheye.net> + */ + +#pragma once + +#include <adwaita.h> + +/* For GisPageApplyCallback */ +#include "gis-page.h" + +G_BEGIN_DECLS + +#define GIS_TYPE_ACCOUNT_PAGE_ENTERPRISE (gis_account_page_enterprise_get_type ()) +G_DECLARE_FINAL_TYPE (GisAccountPageEnterprise, gis_account_page_enterprise, GIS, ACCOUNT_PAGE_ENTERPRISE, AdwBin) + +gboolean gis_account_page_enterprise_validate (GisAccountPageEnterprise *enterprise); +gboolean gis_account_page_enterprise_apply (GisAccountPageEnterprise *enterprise, + GCancellable *cancellable, + GisPageApplyCallback callback, + gpointer data); +void gis_account_page_enterprise_shown (GisAccountPageEnterprise *enterprise); + +G_END_DECLS + diff --git a/gnome-initial-setup/pages/account/gis-account-page-enterprise.ui b/gnome-initial-setup/pages/account/gis-account-page-enterprise.ui new file mode 100644 index 0000000..e5ef0db --- /dev/null +++ b/gnome-initial-setup/pages/account/gis-account-page-enterprise.ui @@ -0,0 +1,280 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="GisAccountPageEnterprise" parent="AdwBin"> + <child> + <object class="GtkBox" id="area"> + <property name="orientation">vertical</property> + <property name="valign">fill</property> + <child> + <object class="GisPageHeader" id="header"> + <property name="margin_bottom">26</property> + <property name="margin_top">24</property> + <property name="title" translatable="yes">Enterprise Login</property> + <property name="subtitle" 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">dialog-password-symbolic</property> + <property name="show_icon">True</property> + </object> + </child> + <child> + <object class="GtkGrid" id="form"> + <property name="row_spacing">12</property> + <property name="column_spacing">12</property> + <property name="margin_bottom">32</property> + <child> + <object class="GtkLabel" id="label4"> + <property name="halign">end</property> + <property name="xalign">1</property> + <property name="label" translatable="yes">_Domain</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">domain</property> + <layout> + <property name="column">0</property> + <property name="row">1</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel" id="label8"> + <property name="halign">end</property> + <property name="xalign">1</property> + <property name="label" translatable="yes">_Username</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">login</property> + <layout> + <property name="column">0</property> + <property name="row">3</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel" id="label9"> + <property name="halign">end</property> + <property name="xalign">1</property> + <property name="label" translatable="yes">_Password</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">password</property> + <layout> + <property name="column">0</property> + <property name="row">4</property> + </layout> + </object> + </child> + <child> + <object class="GtkEntry" id="login"> + <property name="hexpand">True</property> + <property name="max-length">255</property> + <property name="width-chars">25</property> + <layout> + <property name="column">1</property> + <property name="row">3</property> + </layout> + </object> + </child> + <child> + <object class="GtkPasswordEntry" id="password"> + <property name="hexpand">True</property> + <property name="width-chars">25</property> + <layout> + <property name="column">1</property> + <property name="row">4</property> + </layout> + </object> + </child> + <child> + <object class="GtkComboBox" id="domain"> + <property name="hexpand">True</property> + <property name="model">realms_model</property> + <property name="has_entry">True</property> + <property name="entry_text_column">0</property> + <layout> + <property name="column">1</property> + <property name="row">1</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel" id="label10"> + <property name="margin_bottom">12</property> + <property name="xalign">0</property> + <property name="label" translatable="yes">Enterprise domain or realm name</property> + <layout> + <property name="column">1</property> + <property name="row">2</property> + </layout> + <attributes> + <attribute name="scale" value="0.8"/> + </attributes> + </object> + </child> + <child> + <object class="GtkLabel" id="filler"> + <layout> + <property name="column">2</property> + <property name="row">4</property> + </layout> + </object> + </child> + </object> + </child> + </object> + </child> + </template> + <object class="GtkDialog" id="join_dialog"> + <property name="resizable">False</property> + <property name="modal">True</property> + <property name="destroy_with_parent">True</property> + <property name="title" translatable="yes">Domain Administrator Login</property> + <child internal-child="content_area"> + <object class="GtkBox" id="dialog-vbox1"> + <property name="orientation">vertical</property> + <property name="margin-top">18</property> + <property name="margin-bottom">18</property> + <property name="margin-start">18</property> + <property name="margin-end">18</property> + <property name="spacing">10</property> + <child> + <object class="GtkLabel" id="label12"> + <property name="xalign">0.5</property> + <property name="yalign">0</property> + <property name="wrap">True</property> + <property name="max-width-chars">60</property> + <property name="label" translatable="yes">In order to use enterprise logins, this computer needs to be enrolled in a domain. Please have your network administrator type the domain password here, and choose a unique computer name for your computer.</property> + </object> + </child> + <child> + <object class="GtkGrid" id="grid1"> + <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="xalign">1</property> + <property name="label" translatable="yes">_Domain</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">join_domain</property> + <layout> + <property name="column">0</property> + <property name="row">0</property> + </layout> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkLabel" id="join_domain"> + <property name="margin_top">5</property> + <property name="margin_bottom">5</property> + <property name="xalign">0</property> + <layout> + <property name="column">1</property> + <property name="row">0</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel" id="label18"> + <property name="xalign">1</property> + <property name="label" translatable="yes">_Computer</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">join_computer</property> + <layout> + <property name="column">0</property> + <property name="row">1</property> + </layout> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkEntry" id="join_computer"> + <property name="hexpand">True</property> + <layout> + <property name="column">1</property> + <property name="row">1</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel" id="label14"> + <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> + <layout> + <property name="column">0</property> + <property name="row">2</property> + </layout> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkEntry" id="join_name"> + <property name="hexpand">True</property> + <layout> + <property name="column">1</property> + <property name="row">2</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel" id="label15"> + <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> + <layout> + <property name="column">0</property> + <property name="row">3</property> + </layout> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + <child> + <object class="GtkPasswordEntry" id="join_password"> + <property name="hexpand">True</property> + <layout> + <property name="column">1</property> + <property name="row">3</property> + </layout> + </object> + </child> + </object> + </child> + </object> + </child> + <child type="action"> + <object class="GtkButton" id="button_cancel"> + <property name="label">_Cancel</property> + <property name="use_underline">True</property> + </object> + </child> + <child type="action"> + <object class="GtkButton" id="button_ok"> + <property name="label" translatable="yes">C_ontinue</property> + <property name="use_underline">True</property> + <style> + <class name="suggested-action"/> + </style> + </object> + </child> + <action-widgets> + <action-widget response="-5" default="true">button_ok</action-widget> + <action-widget response="-6">button_cancel</action-widget> + </action-widgets> + </object> + <object class="GtkListStore" id="realms_model"> + <columns> + <!-- column-name gchararray --> + <column type="gchararray"/> + <!-- column-name gobject --> + <column type="GObject"/> + </columns> + </object> +</interface> diff --git a/gnome-initial-setup/pages/account/gis-account-page-local.c b/gnome-initial-setup/pages/account/gis-account-page-local.c new file mode 100644 index 0000000..21bb597 --- /dev/null +++ b/gnome-initial-setup/pages/account/gis-account-page-local.c @@ -0,0 +1,721 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2013 Red Hat + * + * 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: + * Jasper St. Pierre <jstpierre@mecheye.net> + */ + +#include "config.h" + +#include "gis-page.h" +#include "gis-account-page-local.h" +#include "gnome-initial-setup.h" + +#include <glib/gi18n.h> +#include <gio/gio.h> + +#include <string.h> +#include <act/act-user-manager.h> +#include "um-utils.h" +#include "um-photo-dialog.h" + +#include "gis-page-header.h" + +#define GOA_API_IS_SUBJECT_TO_CHANGE +#include <goa/goa.h> + +#include <rest/rest-proxy.h> +#include <json-glib/json-glib.h> + +#define VALIDATION_TIMEOUT 600 + +struct _GisAccountPageLocal +{ + AdwBin parent; + + GtkWidget *avatar_button; + GtkWidget *avatar_image; + GtkWidget *header; + GtkWidget *fullname_entry; + GtkWidget *username_combo; + GtkWidget *enable_parental_controls_box; + GtkWidget *enable_parental_controls_check_button; + gboolean has_custom_username; + GtkWidget *username_explanation; + UmPhotoDialog *photo_dialog; + + gint timeout_id; + + GdkPixbuf *avatar_pixbuf; + gchar *avatar_filename; + + ActUserManager *act_client; + + GoaClient *goa_client; + + gboolean valid_name; + gboolean valid_username; + ActUserAccountType account_type; +}; + +G_DEFINE_TYPE (GisAccountPageLocal, gis_account_page_local, ADW_TYPE_BIN); + +enum { + VALIDATION_CHANGED, + MAIN_USER_CREATED, + PARENT_USER_CREATED, + CONFIRM, + LAST_SIGNAL, +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +static void +validation_changed (GisAccountPageLocal *page) +{ + g_signal_emit (page, signals[VALIDATION_CHANGED], 0); +} + +static gboolean +get_profile_sync (const gchar *access_token, + gchar **out_name, + gchar **out_picture, + GCancellable *cancellable, + GError **error) +{ + GError *identity_error; + RestProxy *proxy; + RestProxyCall *call; + JsonParser *parser; + JsonObject *json_object; + gboolean ret; + + ret = FALSE; + + identity_error = NULL; + proxy = NULL; + call = NULL; + parser = NULL; + + /* TODO: cancellable */ + + proxy = rest_proxy_new ("https://www.googleapis.com/oauth2/v2/userinfo", FALSE); + call = rest_proxy_new_call (proxy); + rest_proxy_call_set_method (call, "GET"); + rest_proxy_call_add_param (call, "access_token", access_token); + + if (!rest_proxy_call_sync (call, error)) + goto out; + + if (rest_proxy_call_get_status_code (call) != 200) + { + g_set_error (error, + GOA_ERROR, + GOA_ERROR_FAILED, + "Expected status 200 when requesting your identity, instead got status %d (%s)", + rest_proxy_call_get_status_code (call), + rest_proxy_call_get_status_message (call)); + goto out; + } + + parser = json_parser_new (); + if (!json_parser_load_from_data (parser, + rest_proxy_call_get_payload (call), + rest_proxy_call_get_payload_length (call), + &identity_error)) + { + g_warning ("json_parser_load_from_data() failed: %s (%s, %d)", + identity_error->message, + g_quark_to_string (identity_error->domain), + identity_error->code); + g_set_error (error, + GOA_ERROR, + GOA_ERROR_FAILED, + "Could not parse response"); + goto out; + } + + ret = TRUE; + + json_object = json_node_get_object (json_parser_get_root (parser)); + if (out_name != NULL) + *out_name = g_strdup (json_object_get_string_member (json_object, "name")); + + if (out_picture != NULL) + *out_picture = g_strdup (json_object_get_string_member (json_object, "picture")); + + out: + g_clear_error (&identity_error); + if (call != NULL) + g_object_unref (call); + if (proxy != NULL) + g_object_unref (proxy); + + return ret; +} + +static void +prepopulate_account_page (GisAccountPageLocal *page) +{ + gchar *name = NULL; + gchar *picture = NULL; + GdkPixbuf *pixbuf = NULL; + + if (page->goa_client) { + GList *accounts, *l; + accounts = goa_client_get_accounts (page->goa_client); + for (l = accounts; l != NULL; l = l->next) { + GoaOAuth2Based *oa2; + oa2 = goa_object_get_oauth2_based (GOA_OBJECT (l->data)); + if (oa2) { + gchar *token = NULL; + GError *error = NULL; + if (!goa_oauth2_based_call_get_access_token_sync (oa2, &token, NULL, NULL, &error)) + { + g_warning ("Couldn't get oauth2 token: %s", error->message); + g_error_free (error); + } + else if (!get_profile_sync (token, &name, &picture, NULL, &error)) + { + g_warning ("Couldn't get profile information: %s", error->message); + g_error_free (error); + } + /* FIXME: collect information from more than one account + * and present at least the pictures in the avatar chooser + */ + break; + } + } + g_list_free_full (accounts, (GDestroyNotify) g_object_unref); + } + + if (name) { + g_object_set (page->header, "subtitle", _("Please check the name and username. You can choose a picture too."), NULL); + gtk_editable_set_text (GTK_EDITABLE (page->fullname_entry), name); + } + + if (picture) { + GFile *file; + GFileInputStream *stream; + GError *error = NULL; + file = g_file_new_for_uri (picture); + stream = g_file_read (file, NULL, &error); + if (!stream) + { + g_warning ("Failed to read picture %s: %s", picture, error->message); + g_error_free (error); + } + else + { + pixbuf = gdk_pixbuf_new_from_stream_at_scale (G_INPUT_STREAM (stream), -1, 96, TRUE, NULL, NULL); + g_object_unref (stream); + } + g_object_unref (file); + } + + if (pixbuf) { + GdkPixbuf *rounded = round_image (pixbuf); + + gtk_image_set_from_pixbuf (GTK_IMAGE (page->avatar_image), rounded); + g_object_unref (rounded); + page->avatar_pixbuf = pixbuf; + } + + g_free (name); + g_free (picture); +} + +static void +accounts_changed (GoaClient *client, GoaObject *object, gpointer data) +{ + GisAccountPageLocal *page = data; + + prepopulate_account_page (page); +} + +static gboolean +validate (GisAccountPageLocal *page) +{ + GtkWidget *entry; + const gchar *name, *username; + gboolean parental_controls_enabled; + gchar *tip; + + g_clear_handle_id (&page->timeout_id, g_source_remove); + + entry = gtk_combo_box_get_child (GTK_COMBO_BOX (page->username_combo)); + + name = gtk_editable_get_text (GTK_EDITABLE (page->fullname_entry)); + username = gtk_combo_box_text_get_active_text (GTK_COMBO_BOX_TEXT (page->username_combo)); +#ifdef HAVE_PARENTAL_CONTROLS + parental_controls_enabled = gtk_check_button_get_active (GTK_CHECK_BUTTON (page->enable_parental_controls_check_button)); +#else + parental_controls_enabled = FALSE; +#endif + + page->valid_name = is_valid_name (name); + if (page->valid_name) + set_entry_validation_checkmark (GTK_ENTRY (page->fullname_entry)); + + page->valid_username = is_valid_username (username, parental_controls_enabled, &tip); + if (page->valid_username) + set_entry_validation_checkmark (GTK_ENTRY (entry)); + + gtk_label_set_text (GTK_LABEL (page->username_explanation), tip); + g_free (tip); + + um_photo_dialog_generate_avatar (page->photo_dialog, name); + + validation_changed (page); + + return G_SOURCE_REMOVE; +} + +static gboolean +on_focusout (GisAccountPageLocal *page) +{ + validate (page); + + return FALSE; +} + +static void +fullname_changed (GtkWidget *w, + GParamSpec *pspec, + GisAccountPageLocal *page) +{ + GtkWidget *entry; + GtkTreeModel *model; + const char *name; + + name = gtk_editable_get_text (GTK_EDITABLE (w)); + + entry = gtk_combo_box_get_child (GTK_COMBO_BOX (page->username_combo)); + model = gtk_combo_box_get_model (GTK_COMBO_BOX (page->username_combo)); + + gtk_list_store_clear (GTK_LIST_STORE (model)); + + if ((name == NULL || strlen (name) == 0) && !page->has_custom_username) { + gtk_editable_set_text (GTK_EDITABLE (entry), ""); + } + else if (name != NULL && strlen (name) != 0) { + generate_username_choices (name, GTK_LIST_STORE (model)); + if (!page->has_custom_username) + gtk_combo_box_set_active (GTK_COMBO_BOX (page->username_combo), 0); + } + + clear_entry_validation_error (GTK_ENTRY (w)); + + page->valid_name = FALSE; + + /* username_changed() is called consequently due to changes */ +} + +static void +username_changed (GtkComboBoxText *combo, + GisAccountPageLocal *page) +{ + GtkWidget *entry; + const gchar *username; + + entry = gtk_combo_box_get_child (GTK_COMBO_BOX (combo)); + username = gtk_editable_get_text (GTK_EDITABLE (entry)); + if (*username == '\0') + page->has_custom_username = FALSE; + else if (gtk_widget_has_focus (entry) || + gtk_combo_box_get_active (GTK_COMBO_BOX (page->username_combo)) > 0) + page->has_custom_username = TRUE; + + clear_entry_validation_error (GTK_ENTRY (entry)); + + page->valid_username = FALSE; + validation_changed (page); + + if (page->timeout_id != 0) + g_source_remove (page->timeout_id); + page->timeout_id = g_timeout_add (VALIDATION_TIMEOUT, (GSourceFunc)validate, page); +} + +static void +avatar_callback (GdkPixbuf *pixbuf, + const gchar *filename, + gpointer user_data) +{ + GisAccountPageLocal *page = user_data; + g_autoptr(GdkPixbuf) tmp = NULL; + g_autoptr(GdkPixbuf) rounded = NULL; + + g_clear_object (&page->avatar_pixbuf); + g_clear_pointer (&page->avatar_filename, g_free); + + if (pixbuf) { + page->avatar_pixbuf = g_object_ref (pixbuf); + rounded = round_image (pixbuf); + } + else if (filename) { + page->avatar_filename = g_strdup (filename); + tmp = gdk_pixbuf_new_from_file_at_size (filename, 96, 96, NULL); + + if (tmp != NULL) + rounded = round_image (tmp); + } + + if (rounded != NULL) { + gtk_image_set_from_pixbuf (GTK_IMAGE (page->avatar_image), rounded); + } + else { + /* Fallback. */ + gtk_image_set_pixel_size (GTK_IMAGE (page->avatar_image), 96); + gtk_image_set_from_icon_name (GTK_IMAGE (page->avatar_image), "avatar-default-symbolic"); + } +} + +static void +confirm (GisAccountPageLocal *page) +{ + if (gis_account_page_local_validate (page)) + g_signal_emit (page, signals[CONFIRM], 0); +} + +static void +enable_parental_controls_check_button_toggled_cb (GtkCheckButton *check_button, + gpointer user_data) +{ + GisAccountPageLocal *page = GIS_ACCOUNT_PAGE_LOCAL (user_data); + gboolean parental_controls_enabled = gtk_check_button_get_active (GTK_CHECK_BUTTON (page->enable_parental_controls_check_button)); + + /* This sets the account type of the main user. When we save_data(), we create + * two users if parental controls are enabled: the first user is always an + * admin, and the second user is the main user using this @account_type. */ + page->account_type = parental_controls_enabled ? ACT_USER_ACCOUNT_TYPE_STANDARD : ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR; + + validate (page); +} + +static void +track_focus_out (GisAccountPageLocal *page, + GtkWidget *widget) +{ + GtkEventController *focus_controller; + + focus_controller = gtk_event_controller_focus_new (); + gtk_widget_add_controller (widget, focus_controller); + + g_signal_connect_swapped (focus_controller, "leave", G_CALLBACK (on_focusout), page); +} + + +static void +gis_account_page_local_constructed (GObject *object) +{ + GisAccountPageLocal *page = GIS_ACCOUNT_PAGE_LOCAL (object); + + G_OBJECT_CLASS (gis_account_page_local_parent_class)->constructed (object); + + page->act_client = act_user_manager_get_default (); + + g_signal_connect (page->fullname_entry, "notify::text", + G_CALLBACK (fullname_changed), page); + track_focus_out (page, page->fullname_entry); + + g_signal_connect_swapped (page->fullname_entry, "activate", + G_CALLBACK (validate), page); + g_signal_connect (page->username_combo, "changed", + G_CALLBACK (username_changed), page); + track_focus_out (page, page->username_combo); + + g_signal_connect_swapped (gtk_combo_box_get_child (GTK_COMBO_BOX (page->username_combo)), + "activate", G_CALLBACK (confirm), page); + g_signal_connect_swapped (page->fullname_entry, "activate", + G_CALLBACK (confirm), page); + g_signal_connect (page->enable_parental_controls_check_button, "toggled", + G_CALLBACK (enable_parental_controls_check_button_toggled_cb), page); + + /* Disable parental controls if support is not compiled in. */ +#ifndef HAVE_PARENTAL_CONTROLS + gtk_widget_hide (page->enable_parental_controls_box); +#endif + + page->valid_name = FALSE; + page->valid_username = FALSE; + + /* FIXME: change this for a large deployment scenario; maybe through a GSetting? */ + page->account_type = ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR; + + g_object_set (page->header, "subtitle", _("We need a few details to complete setup."), NULL); + gtk_editable_set_text (GTK_EDITABLE (page->fullname_entry), ""); + gtk_list_store_clear (GTK_LIST_STORE (gtk_combo_box_get_model (GTK_COMBO_BOX (page->username_combo)))); + page->has_custom_username = FALSE; + + gtk_image_set_pixel_size (GTK_IMAGE (page->avatar_image), 96); + gtk_image_set_from_icon_name (GTK_IMAGE (page->avatar_image), "avatar-default-symbolic"); + + page->goa_client = goa_client_new_sync (NULL, NULL); + if (page->goa_client) { + g_signal_connect (page->goa_client, "account-added", + G_CALLBACK (accounts_changed), page); + g_signal_connect (page->goa_client, "account-removed", + G_CALLBACK (accounts_changed), page); + prepopulate_account_page (page); + } + + page->photo_dialog = um_photo_dialog_new (avatar_callback, page); + um_photo_dialog_generate_avatar (page->photo_dialog, ""); + gtk_menu_button_set_popover (GTK_MENU_BUTTON (page->avatar_button), + GTK_WIDGET (page->photo_dialog)); + + validate (page); +} + +static void +gis_account_page_local_dispose (GObject *object) +{ + GisAccountPageLocal *page = GIS_ACCOUNT_PAGE_LOCAL (object); + + g_clear_object (&page->goa_client); + g_clear_object (&page->avatar_pixbuf); + g_clear_pointer (&page->avatar_filename, g_free); + g_clear_handle_id (&page->timeout_id, g_source_remove); + + G_OBJECT_CLASS (gis_account_page_local_parent_class)->dispose (object); +} + +static void +set_user_avatar (GisAccountPageLocal *page, + ActUser *user) +{ + GFile *file = NULL; + GFileIOStream *io_stream = NULL; + GOutputStream *stream = NULL; + GError *error = NULL; + + if (page->avatar_filename != NULL) { + act_user_set_icon_file (user, page->avatar_filename); + return; + } + + if (page->avatar_pixbuf == NULL) { + return; + } + + file = g_file_new_tmp ("usericonXXXXXX", &io_stream, &error); + if (error != NULL) + goto out; + + stream = g_io_stream_get_output_stream (G_IO_STREAM (io_stream)); + if (!gdk_pixbuf_save_to_stream (page->avatar_pixbuf, stream, "png", NULL, &error, NULL)) + goto out; + + act_user_set_icon_file (user, g_file_get_path (file)); + + out: + if (error != NULL) { + g_warning ("failed to save image: %s", error->message); + g_error_free (error); + } + g_clear_object (&io_stream); + g_clear_object (&file); +} + +static gboolean +local_create_user (GisAccountPageLocal *local, + GisPage *page, + GError **error) +{ + const gchar *username; + const gchar *fullname; + gboolean parental_controls_enabled; + g_autoptr(ActUser) main_user = NULL; + g_autoptr(ActUser) parent_user = NULL; + + username = gtk_combo_box_text_get_active_text (GTK_COMBO_BOX_TEXT (local->username_combo)); + fullname = gtk_editable_get_text (GTK_EDITABLE (local->fullname_entry)); + parental_controls_enabled = gis_driver_get_parental_controls_enabled (page->driver); + + /* Always create the admin user first, in case of failure part-way through + * this function, which would leave us with no admin user at all. */ + if (parental_controls_enabled) { + g_autoptr(GError) local_error = NULL; + g_autoptr(GDBusConnection) connection = NULL; + const gchar *parent_username = "administrator"; + const gchar *parent_fullname = _("Administrator"); + + parent_user = act_user_manager_create_user (local->act_client, parent_username, parent_fullname, ACT_USER_ACCOUNT_TYPE_ADMINISTRATOR, error); + if (parent_user == NULL) + { + g_prefix_error (error, + _("Failed to create user '%s': "), + parent_username); + return FALSE; + } + + /* Make the admin account usable in case g-i-s crashes. If all goes + * according to plan a password will be set on it in gis-password-page.c */ + act_user_set_password_mode (parent_user, ACT_USER_PASSWORD_MODE_SET_AT_LOGIN); + + /* Mark it as the parent user account. + * FIXME: This should be async. */ + connection = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, &local_error); + if (connection != NULL) { + g_dbus_connection_call_sync (connection, + "org.freedesktop.Accounts", + act_user_get_object_path (parent_user), + "org.freedesktop.DBus.Properties", + "Set", + g_variant_new ("(ssv)", + "com.endlessm.ParentalControls.AccountInfo", + "IsParent", + g_variant_new_boolean (TRUE)), + NULL, /* reply type */ + G_DBUS_CALL_FLAGS_NONE, + -1, /* default timeout */ + NULL, /* cancellable */ + &local_error); + } + if (local_error != NULL) { + /* Make this non-fatal, since the correct accounts-service interface + * might not be installed, depending on which version of malcontent is installed. */ + g_warning ("Failed to mark user as parent: %s", local_error->message); + g_clear_error (&local_error); + } + + g_signal_emit (local, signals[PARENT_USER_CREATED], 0, parent_user, ""); + } + + /* Now create the main user. */ + main_user = act_user_manager_create_user (local->act_client, username, fullname, local->account_type, error); + if (main_user == NULL) + { + g_prefix_error (error, + _("Failed to create user '%s': "), + username); + /* FIXME: Could we delete the @parent_user at this point to reset the state + * and allow g-i-s to be run again after a reboot? */ + return FALSE; + } + + set_user_avatar (local, main_user); + + g_signal_emit (local, signals[MAIN_USER_CREATED], 0, main_user, ""); + + return TRUE; +} + +static void +gis_account_page_local_class_init (GisAccountPageLocalClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (klass), "/org/gnome/initial-setup/gis-account-page-local.ui"); + + gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageLocal, avatar_button); + gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageLocal, avatar_image); + gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageLocal, header); + gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageLocal, fullname_entry); + gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageLocal, username_combo); + gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageLocal, username_explanation); + gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageLocal, enable_parental_controls_box); + gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPageLocal, enable_parental_controls_check_button); + + object_class->constructed = gis_account_page_local_constructed; + object_class->dispose = gis_account_page_local_dispose; + + signals[VALIDATION_CHANGED] = g_signal_new ("validation-changed", GIS_TYPE_ACCOUNT_PAGE_LOCAL, + G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); + + signals[MAIN_USER_CREATED] = g_signal_new ("main-user-created", GIS_TYPE_ACCOUNT_PAGE_LOCAL, + G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, + G_TYPE_NONE, 2, ACT_TYPE_USER, G_TYPE_STRING); + + signals[PARENT_USER_CREATED] = g_signal_new ("parent-user-created", GIS_TYPE_ACCOUNT_PAGE_LOCAL, + G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, + G_TYPE_NONE, 2, ACT_TYPE_USER, G_TYPE_STRING); + + signals[CONFIRM] = g_signal_new ("confirm", GIS_TYPE_ACCOUNT_PAGE_LOCAL, + G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +static void +gis_account_page_local_init (GisAccountPageLocal *page) +{ + g_type_ensure (GIS_TYPE_PAGE_HEADER); + gtk_widget_init_template (GTK_WIDGET (page)); +} + +gboolean +gis_account_page_local_validate (GisAccountPageLocal *page) +{ + return page->valid_name && page->valid_username; +} + +gboolean +gis_account_page_local_create_user (GisAccountPageLocal *local, + GisPage *page, + GError **error) +{ + return local_create_user (local, page, error); +} + +gboolean +gis_account_page_local_apply (GisAccountPageLocal *local, GisPage *page) +{ + const gchar *username, *full_name; + gboolean parental_controls_enabled; + + username = gtk_combo_box_text_get_active_text (GTK_COMBO_BOX_TEXT (local->username_combo)); + gis_driver_set_username (GIS_PAGE (page)->driver, username); + + full_name = gtk_editable_get_text (GTK_EDITABLE (local->fullname_entry)); + gis_driver_set_full_name (GIS_PAGE (page)->driver, full_name); + + if (local->avatar_pixbuf != NULL) + { + g_autoptr(GdkTexture) texture = NULL; + + texture = gdk_texture_new_for_pixbuf (local->avatar_pixbuf); + gis_driver_set_avatar (GIS_PAGE (page)->driver, GDK_PAINTABLE (texture)); + } + else if (local->avatar_filename != NULL) + { + g_autoptr(GdkTexture) texture = NULL; + g_autoptr(GError) error = NULL; + + texture = gdk_texture_new_from_filename (local->avatar_filename, &error); + + if (!error) + gis_driver_set_avatar (GIS_PAGE (page)->driver, GDK_PAINTABLE (texture)); + else + g_warning ("Error loading avatar: %s", error->message); + } + +#ifdef HAVE_PARENTAL_CONTROLS + parental_controls_enabled = gtk_check_button_get_active (GTK_CHECK_BUTTON (local->enable_parental_controls_check_button)); +#else + parental_controls_enabled = FALSE; +#endif + gis_driver_set_parental_controls_enabled (GIS_PAGE (page)->driver, parental_controls_enabled); + + return FALSE; +} + +void +gis_account_page_local_shown (GisAccountPageLocal *local) +{ + gtk_widget_grab_focus (local->fullname_entry); +} diff --git a/gnome-initial-setup/pages/account/gis-account-page-local.h b/gnome-initial-setup/pages/account/gis-account-page-local.h new file mode 100644 index 0000000..9d8dea0 --- /dev/null +++ b/gnome-initial-setup/pages/account/gis-account-page-local.h @@ -0,0 +1,39 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2013 Red Hat + * + * 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: + * Jasper St. Pierre <jstpierre@mecheye.net> + */ + +#pragma once + +#include <adwaita.h> + +G_BEGIN_DECLS + +#define GIS_TYPE_ACCOUNT_PAGE_LOCAL (gis_account_page_local_get_type ()) +G_DECLARE_FINAL_TYPE (GisAccountPageLocal, gis_account_page_local, GIS, ACCOUNT_PAGE_LOCAL, AdwBin) + +gboolean gis_account_page_local_validate (GisAccountPageLocal *local); +gboolean gis_account_page_local_apply (GisAccountPageLocal *local, GisPage *page); +gboolean gis_account_page_local_create_user (GisAccountPageLocal *local, + GisPage *page, + GError **error); +void gis_account_page_local_shown (GisAccountPageLocal *local); + +G_END_DECLS + diff --git a/gnome-initial-setup/pages/account/gis-account-page-local.ui b/gnome-initial-setup/pages/account/gis-account-page-local.ui new file mode 100644 index 0000000..67aaf9a --- /dev/null +++ b/gnome-initial-setup/pages/account/gis-account-page-local.ui @@ -0,0 +1,145 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="GisAccountPageLocal" parent="AdwBin"> + <child> + <object class="GtkBox" id="area"> + <property name="halign">center</property> + <property name="valign">fill</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkMenuButton" id="avatar_button"> + <property name="margin_top">24</property> + <property name="halign">center</property> + <style> + <class name="flat"/> + </style> + <accessibility> + <property name="description" translatable="yes">Avatar image</property> + </accessibility> + <child> + <object class="GtkImage" id="avatar_image"> + <property name="pixel_size">96</property> + <property name="icon_name">avatar-default-symbolic</property> + </object> + </child> + </object> + </child> + <child> + <object class="GisPageHeader" id="header"> + <property name="show-icon">False</property> + <property name="margin_top">18</property> + <property name="title" translatable="yes">About You</property> + <property name="subtitle" translatable="yes">Please provide a name and username. You can choose a picture too.</property> + </object> + </child> + <child> + <object class="GtkGrid" id="form"> + <property name="column_spacing">12</property> + <property name="row_spacing">6</property> + <property name="margin_top">42</property> + <child> + <object class="GtkLabel" id="fullname_label"> + <property name="halign">end</property> + <property name="xalign">1</property> + <property name="label" translatable="yes">_Full Name</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">fullname_entry</property> + <layout> + <property name="column">0</property> + <property name="row">3</property> + </layout> + </object> + </child> + <child> + <object class="GtkEntry" id="fullname_entry"> + <property name="max_length">255</property> + <property name="width-chars">25</property> + <layout> + <property name="column">1</property> + <property name="row">3</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel" id="username_label"> + <property name="halign">end</property> + <property name="xalign">1</property> + <property name="label" translatable="yes">_Username</property> + <property name="use_underline">True</property> + <property name="mnemonic_widget">username_combo</property> + <property name="margin_top">6</property> + <layout> + <property name="column">0</property> + <property name="row">4</property> + </layout> + </object> + </child> + <child> + <object class="GtkComboBoxText" id="username_combo"> + <property name="has_entry">True</property> + <property name="entry_text_column">0</property> + <property name="id_column">1</property> + <property name="margin_top">6</property> + <layout> + <property name="column">1</property> + <property name="row">4</property> + </layout> + </object> + </child> + <child> + <object class="GtkLabel" id="username_explanation"> + <property name="yalign">0</property> + <property name="xalign">0</property> + <property name="width-chars">35</property> + <property name="max-width-chars">35</property> + <property name="height-request">50</property> + <property name="wrap">True</property> + <property name="wrap_mode">word-char</property> + <layout> + <property name="column">1</property> + <property name="row">5</property> + </layout> + <style> + <class name="dim-label"/> + </style> + <attributes> + <attribute name="scale" value="0.8"/> + </attributes> + </object> + </child> + <child> + <object class="GtkBox" id="enable_parental_controls_box"> + <property name="orientation">vertical</property> + <layout> + <property name="column">1</property> + <property name="row">6</property> + </layout> + <child> + <object class="GtkCheckButton" id="enable_parental_controls_check_button"> + <property name="label" translatable="yes">Set up _parental controls for this user</property> + <property name="use-underline">True</property> + </object> + </child> + <child> + <object class="GtkLabel"> + <property name="label" translatable="yes">For use by a parent or supervisor, who must set up their own password.</property> + <property name="yalign">0</property> + <property name="xalign">0</property> + <property name="margin-start">24</property> + <style> + <class name="dim-label"/> + </style> + <attributes> + <attribute name="scale" value="0.8"/> + </attributes> + </object> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </template> +</interface> + diff --git a/gnome-initial-setup/pages/account/gis-account-page.c b/gnome-initial-setup/pages/account/gis-account-page.c new file mode 100644 index 0000000..04a8e54 --- /dev/null +++ b/gnome-initial-setup/pages/account/gis-account-page.c @@ -0,0 +1,309 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2012 Red Hat + * + * 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: + * Jasper St. Pierre <jstpierre@mecheye.net> + */ + +/* Account page {{{1 */ + +#define PAGE_ID "account" + +#include "config.h" +#include "account-resources.h" +#include "gis-account-page.h" +#include "gis-account-page-local.h" +#include "gis-account-page-enterprise.h" + +#include <glib/gi18n.h> +#include <gio/gio.h> + +struct _GisAccountPage +{ + GisPage parent; + + GtkWidget *page_local; + GtkWidget *page_enterprise; + GtkWidget *stack; + + GtkWidget *page_toggle; + GtkWidget *offline_label; + GtkWidget *offline_stack; + + UmAccountMode mode; +}; + +G_DEFINE_TYPE (GisAccountPage, gis_account_page, GIS_TYPE_PAGE); + +static void +enterprise_apply_complete (GisPage *dummy, + gboolean valid, + gpointer user_data) +{ + GisAccountPage *page = GIS_ACCOUNT_PAGE (user_data); + gis_driver_set_username (GIS_PAGE (page)->driver, NULL); + gis_page_apply_complete (GIS_PAGE (page), valid); +} + +static gboolean +page_validate (GisAccountPage *page) +{ + switch (page->mode) { + case UM_LOCAL: + return gis_account_page_local_validate (GIS_ACCOUNT_PAGE_LOCAL (page->page_local)); + case UM_ENTERPRISE: + return gis_account_page_enterprise_validate (GIS_ACCOUNT_PAGE_ENTERPRISE (page->page_enterprise)); + default: + g_assert_not_reached (); + } +} + +static void +update_page_validation (GisAccountPage *page) +{ + gis_page_set_complete (GIS_PAGE (page), page_validate (page)); +} + +static void +on_validation_changed (gpointer page_area, + GisAccountPage *page) +{ + update_page_validation (page); +} + +static void +set_mode (GisAccountPage *page, + UmAccountMode mode) +{ + if (page->mode == mode) + return; + + page->mode = mode; + gis_driver_set_account_mode (GIS_PAGE (page)->driver, mode); + + switch (mode) + { + case UM_LOCAL: + gtk_stack_set_visible_child (GTK_STACK (page->stack), page->page_local); + gis_account_page_local_shown (GIS_ACCOUNT_PAGE_LOCAL (page->page_local)); + break; + case UM_ENTERPRISE: + gtk_stack_set_visible_child (GTK_STACK (page->stack), page->page_enterprise); + gis_account_page_enterprise_shown (GIS_ACCOUNT_PAGE_ENTERPRISE (page->page_enterprise)); + break; + default: + g_assert_not_reached (); + } + + update_page_validation (page); +} + +static void +toggle_mode (GtkToggleButton *button, + gpointer user_data) +{ + set_mode (GIS_ACCOUNT_PAGE (user_data), + gtk_toggle_button_get_active (button) ? UM_ENTERPRISE : UM_LOCAL); +} + +static gboolean +gis_account_page_apply (GisPage *gis_page, + GCancellable *cancellable) +{ + GisAccountPage *page = GIS_ACCOUNT_PAGE (gis_page); + + switch (page->mode) { + case UM_LOCAL: + return gis_account_page_local_apply (GIS_ACCOUNT_PAGE_LOCAL (page->page_local), gis_page); + case UM_ENTERPRISE: + return gis_account_page_enterprise_apply (GIS_ACCOUNT_PAGE_ENTERPRISE (page->page_enterprise), cancellable, + enterprise_apply_complete, page); + default: + g_assert_not_reached (); + break; + } +} + +static gboolean +gis_account_page_save_data (GisPage *gis_page, + GError **error) +{ + GisAccountPage *page = GIS_ACCOUNT_PAGE (gis_page); + + switch (page->mode) { + case UM_LOCAL: + return gis_account_page_local_create_user (GIS_ACCOUNT_PAGE_LOCAL (page->page_local), gis_page, error); + case UM_ENTERPRISE: + /* Nothing to do. */ + return TRUE; + default: + g_assert_not_reached (); + return FALSE; + } +} + +static void +gis_account_page_shown (GisPage *gis_page) +{ + GisAccountPage *page = GIS_ACCOUNT_PAGE (gis_page); + + gis_account_page_local_shown (GIS_ACCOUNT_PAGE_LOCAL (page->page_local)); +} + +static void +on_local_main_user_created (GtkWidget *page_local, + ActUser *user, + const gchar *password, + GisAccountPage *page) +{ + const gchar *language; + + language = gis_driver_get_user_language (GIS_PAGE (page)->driver); + if (language) + act_user_set_language (user, language); + + gis_driver_set_user_permissions (GIS_PAGE (page)->driver, user, password); +} + +static void +on_local_parent_user_created (GtkWidget *page_local, + ActUser *user, + const gchar *password, + GisAccountPage *page) +{ + const gchar *language; + + language = gis_driver_get_user_language (GIS_PAGE (page)->driver); + if (language) + act_user_set_language (user, language); + + gis_driver_set_parent_permissions (GIS_PAGE (page)->driver, user, password); +} + +static void +on_local_page_confirmed (GisAccountPageLocal *local, + GisAccountPage *page) +{ + gis_assistant_next_page (gis_driver_get_assistant (GIS_PAGE (page)->driver)); +} + +static void +on_local_user_cached (GtkWidget *page_local, + ActUser *user, + char *password, + GisAccountPage *page) +{ + const gchar *language; + + language = gis_driver_get_user_language (GIS_PAGE (page)->driver); + if (language) + act_user_set_language (user, language); + + gis_driver_set_user_permissions (GIS_PAGE (page)->driver, user, password); +} + +static void +on_network_changed (GNetworkMonitor *monitor, + gboolean available, + GisAccountPage *page) +{ + if (!available && page->mode != UM_ENTERPRISE) + gtk_stack_set_visible_child (GTK_STACK (page->offline_stack), page->offline_label); + else + gtk_stack_set_visible_child (GTK_STACK (page->offline_stack), page->page_toggle); +} + +static void +gis_account_page_constructed (GObject *object) +{ + GisAccountPage *page = GIS_ACCOUNT_PAGE (object); + GNetworkMonitor *monitor; + gboolean available; + + G_OBJECT_CLASS (gis_account_page_parent_class)->constructed (object); + + g_signal_connect (page->page_local, "validation-changed", + G_CALLBACK (on_validation_changed), page); + g_signal_connect (page->page_local, "main-user-created", + G_CALLBACK (on_local_main_user_created), page); + g_signal_connect (page->page_local, "parent-user-created", + G_CALLBACK (on_local_parent_user_created), page); + g_signal_connect (page->page_local, "confirm", + G_CALLBACK (on_local_page_confirmed), page); + + g_signal_connect (page->page_enterprise, "validation-changed", + G_CALLBACK (on_validation_changed), page); + g_signal_connect (page->page_enterprise, "user-cached", + G_CALLBACK (on_local_user_cached), page); + + update_page_validation (page); + + g_signal_connect (page->page_toggle, "toggled", G_CALLBACK (toggle_mode), page); + g_object_bind_property (page, "applying", page->page_toggle, "sensitive", G_BINDING_INVERT_BOOLEAN); + g_object_bind_property (page->page_enterprise, "visible", page->offline_stack, "visible", G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + /* force a refresh by setting to an invalid value */ + page->mode = NUM_MODES; + set_mode (page, UM_LOCAL); + + monitor = g_network_monitor_get_default (); + available = g_network_monitor_get_network_available (monitor); + on_network_changed (monitor, available, page); + g_signal_connect_object (monitor, "network-changed", G_CALLBACK (on_network_changed), page, 0); + + gtk_widget_show (GTK_WIDGET (page)); +} + +static void +gis_account_page_locale_changed (GisPage *page) +{ + gis_page_set_title (GIS_PAGE (page), _("About You")); +} + +static void +gis_account_page_class_init (GisAccountPageClass *klass) +{ + GisPageClass *page_class = GIS_PAGE_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (klass), "/org/gnome/initial-setup/gis-account-page.ui"); + + gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPage, page_local); + gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPage, page_enterprise); + gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPage, stack); + + gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPage, page_toggle); + gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPage, offline_label); + gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GisAccountPage, offline_stack); + + page_class->page_id = PAGE_ID; + page_class->locale_changed = gis_account_page_locale_changed; + page_class->apply = gis_account_page_apply; + page_class->save_data = gis_account_page_save_data; + page_class->shown = gis_account_page_shown; + object_class->constructed = gis_account_page_constructed; +} + +static void +gis_account_page_init (GisAccountPage *page) +{ + g_resources_register (account_get_resource ()); + g_type_ensure (GIS_TYPE_ACCOUNT_PAGE_LOCAL); + g_type_ensure (GIS_TYPE_ACCOUNT_PAGE_ENTERPRISE); + + gtk_widget_init_template (GTK_WIDGET (page)); +} diff --git a/gnome-initial-setup/pages/account/gis-account-page.h b/gnome-initial-setup/pages/account/gis-account-page.h new file mode 100644 index 0000000..17a86fa --- /dev/null +++ b/gnome-initial-setup/pages/account/gis-account-page.h @@ -0,0 +1,33 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2012 Red Hat + * + * 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: + * Jasper St. Pierre <jstpierre@mecheye.net> + */ + +#pragma once + +#include <glib-object.h> + +#include "gnome-initial-setup.h" + +G_BEGIN_DECLS + +#define GIS_TYPE_ACCOUNT_PAGE (gis_account_page_get_type ()) +G_DECLARE_FINAL_TYPE (GisAccountPage, gis_account_page, GIS, ACCOUNT_PAGE, GisPage) + +G_END_DECLS diff --git a/gnome-initial-setup/pages/account/gis-account-page.ui b/gnome-initial-setup/pages/account/gis-account-page.ui new file mode 100644 index 0000000..782e4bd --- /dev/null +++ b/gnome-initial-setup/pages/account/gis-account-page.ui @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <template class="GisAccountPage" parent="GisPage"> + <child> + <object class="AdwPreferencesPage"> + + <child> + <object class="AdwPreferencesGroup"> + <child> + <object class="GtkStack" id="stack"> + <property name="valign">start</property> + <property name="vexpand">True</property> + <child> + <object class="GisAccountPageLocal" id="page_local" /> + </child> + <child> + <object class="GisAccountPageEnterprise" id="page_enterprise" /> + </child> + </object> + </child> + </object> + </child> + + <child> + <object class="AdwPreferencesGroup"> + <child> + <object class="GtkStack" id="offline_stack"> + <property name="valign">end</property> + <child> + <object class="GtkToggleButton" id="page_toggle"> + <property name="use_underline">True</property> + <property name="label" translatable="yes">_Enterprise Login</property> + <property name="halign">center</property> + <property name="valign">center</property> + </object> + </child> + <child> + <object class="GtkLabel" id="offline_label"> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="label" translatable="yes">Go online to set up Enterprise Login.</property> + <style> + <class name="dim-label"/> + </style> + </object> + </child> + </object> + </child> + </object> + </child> + + </object> + </child> + + </template> +</interface> diff --git a/gnome-initial-setup/pages/account/gis-account-pages.c b/gnome-initial-setup/pages/account/gis-account-pages.c new file mode 100644 index 0000000..d9cc8d9 --- /dev/null +++ b/gnome-initial-setup/pages/account/gis-account-pages.c @@ -0,0 +1,32 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2013 Red Hat + * + * 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: + * Jasper St. Pierre <jstpierre@mecheye.net> + */ + +#include "config.h" +#include "gis-account-pages.h" +#include "gis-account-page.h" + +GisPage * +gis_prepare_account_page (GisDriver *driver) +{ + return g_object_new (GIS_TYPE_ACCOUNT_PAGE, + "driver", driver, + NULL); +} diff --git a/gnome-initial-setup/pages/account/gis-account-pages.h b/gnome-initial-setup/pages/account/gis-account-pages.h new file mode 100644 index 0000000..9cf41bb --- /dev/null +++ b/gnome-initial-setup/pages/account/gis-account-pages.h @@ -0,0 +1,33 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2013 Red Hat + * + * 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: + * Jasper St. Pierre <jstpierre@mecheye.net> + */ + +#pragma once + +#include <glib-object.h> + +#include "gnome-initial-setup.h" + +G_BEGIN_DECLS + +GisPage *gis_prepare_account_page (GisDriver *driver); + +G_END_DECLS + diff --git a/gnome-initial-setup/pages/account/meson.build b/gnome-initial-setup/pages/account/meson.build new file mode 100644 index 0000000..1130465 --- /dev/null +++ b/gnome-initial-setup/pages/account/meson.build @@ -0,0 +1,33 @@ +realmd_namespace = 'org.freedesktop.realmd' +sources += gnome.gdbus_codegen( + 'um-realm-generated', + realmd_namespace + '.xml', + interface_prefix: realmd_namespace + '.', + namespace: 'UmRealm', + object_manager: true, + annotations: ['org.freedesktop.realmd.Realm', 'org.gtk.GDBus.C.Name', 'Common'] +) + +sources += gnome.compile_resources( + 'account-resources', + files('account.gresource.xml'), + c_name: 'account' +) + +sources += files( + 'gis-account-page.c', + 'gis-account-page.h', + 'gis-account-pages.c', + 'gis-account-pages.h', + 'gis-account-page-local.c', + 'gis-account-page-local.h', + 'gis-account-page-enterprise.c', + 'gis-account-page-enterprise.h', + 'um-realm-manager.c', + 'um-realm-manager.h', + 'um-utils.c', + 'um-photo-dialog.c', + 'um-photo-dialog.h' +) + +account_sources_dir = meson.current_source_dir() diff --git a/gnome-initial-setup/pages/account/org.freedesktop.realmd.xml b/gnome-initial-setup/pages/account/org.freedesktop.realmd.xml new file mode 100644 index 0000000..316213a --- /dev/null +++ b/gnome-initial-setup/pages/account/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/gnome-initial-setup/pages/account/um-photo-dialog.c b/gnome-initial-setup/pages/account/um-photo-dialog.c new file mode 100644 index 0000000..383101d --- /dev/null +++ b/gnome-initial-setup/pages/account/um-photo-dialog.c @@ -0,0 +1,314 @@ +/* -*- 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA + * + * Written by: Matthias Clasen <mclasen@redhat.com> + */ + +#include "config.h" + +#include <stdlib.h> + +#include <glib.h> +#include <glib/gi18n.h> +#include <gtk/gtk.h> + +#include "um-photo-dialog.h" +#include "um-utils.h" + +#define ROW_SPAN 5 +#define AVATAR_PIXEL_SIZE 72 + +struct _UmPhotoDialog { + GtkPopover parent; + + GtkWidget *take_picture_button; + GtkWidget *flowbox; + GtkWidget *recent_pictures; + + GListStore *recent_faces; + GListStore *faces; + GFile *generated_avatar; + gboolean custom_avatar_was_chosen; + + SelectAvatarCallback *callback; + gpointer data; +}; + +G_DEFINE_TYPE (UmPhotoDialog, um_photo_dialog, GTK_TYPE_POPOVER) + + +static void +webcam_icon_selected (UmPhotoDialog *um) +{ + g_warning ("Webcam icon selected, but compiled without Cheese support"); +} + +static void +face_widget_activated (GtkFlowBox *flowbox, + GtkFlowBoxChild *child, + UmPhotoDialog *um) +{ + const char *filename; + GtkWidget *image; + + image = gtk_flow_box_child_get_child (child); + filename = g_object_get_data (G_OBJECT (image), "filename"); + + um->callback (NULL, filename, um->data); + um->custom_avatar_was_chosen = TRUE; + + gtk_popover_popdown (GTK_POPOVER (um)); +} + +static void +generated_avatar_activated (GtkFlowBox *flowbox, + GtkFlowBoxChild *child, + UmPhotoDialog *um) +{ + face_widget_activated (flowbox, child, um); + um->custom_avatar_was_chosen = FALSE; +} + +static GtkWidget * +create_face_widget (gpointer item, + gpointer user_data) +{ + g_autoptr(GdkPixbuf) pixbuf = NULL; + GtkWidget *image; + g_autofree gchar *path = g_file_get_path (G_FILE (item)); + + pixbuf = gdk_pixbuf_new_from_file_at_size (path, + AVATAR_PIXEL_SIZE, + AVATAR_PIXEL_SIZE, + NULL); + + if (pixbuf != NULL) + image = gtk_image_new_from_pixbuf (round_image (pixbuf)); + else + image = gtk_image_new (); + + gtk_image_set_pixel_size (GTK_IMAGE (image), AVATAR_PIXEL_SIZE); + + gtk_widget_show (image); + + g_object_set_data_full (G_OBJECT (image), + "filename", g_steal_pointer (&path), + (GDestroyNotify) g_free); + + return image; +} + +static GStrv +get_settings_facesdirs (void) +{ + g_autoptr(GSettingsSchema) schema = NULL; + g_autoptr(GPtrArray) facesdirs = g_ptr_array_new (); + g_autoptr(GSettings) settings = g_settings_new ("org.gnome.desktop.interface"); + g_auto(GStrv) settings_dirs = g_settings_get_strv (settings, "avatar-directories"); + + if (settings_dirs != NULL) { + int i; + for (i = 0; settings_dirs[i] != NULL; i++) { + char *path = settings_dirs[i]; + if (path != NULL && g_strcmp0 (path, "") != 0) + g_ptr_array_add (facesdirs, g_strdup (path)); + } + } + + // NULL terminated array + g_ptr_array_add (facesdirs, NULL); + return (GStrv) g_steal_pointer (&facesdirs->pdata); +} + +static GStrv +get_system_facesdirs (void) +{ + g_autoptr(GPtrArray) facesdirs = NULL; + const char * const * data_dirs; + 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); + } + + // NULL terminated array + g_ptr_array_add (facesdirs, NULL); + return (GStrv) g_steal_pointer (&facesdirs->pdata); +} + +static gboolean +add_faces_from_dirs (GListStore *faces, GStrv facesdirs, gboolean add_all) +{ + gboolean added_faces = FALSE; + const gchar *target; + int i; + GFileType type; + + for (i = 0; facesdirs[i] != NULL; i++) { + g_autoptr(GFileEnumerator) enumerator = NULL; + g_autoptr(GFile) dir = NULL; + const char *path = facesdirs[i]; + gpointer infoptr; + + dir = g_file_new_for_path (path); + 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 ((infoptr = g_file_enumerator_next_file (enumerator, NULL, NULL)) != NULL) { + g_autoptr (GFileInfo) info = infoptr; + g_autoptr (GFile) face_file = NULL; + + 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; + + face_file = g_file_get_child (dir, g_file_info_get_name (info)); + g_list_store_append (faces, face_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 (UmPhotoDialog *um) +{ + g_auto(GStrv) facesdirs; + gboolean added_faces = FALSE; + + um->faces = g_list_store_new (G_TYPE_FILE); + gtk_flow_box_bind_model (GTK_FLOW_BOX (um->flowbox), + G_LIST_MODEL (um->faces), + create_face_widget, + um, + NULL); + + g_signal_connect (um->flowbox, "child-activated", + G_CALLBACK (face_widget_activated), um); + + um->recent_faces = g_list_store_new (G_TYPE_FILE); + gtk_flow_box_bind_model (GTK_FLOW_BOX (um->recent_pictures), + G_LIST_MODEL (um->recent_faces), + create_face_widget, + um, + NULL); + g_signal_connect (um->recent_pictures, "child-activated", + G_CALLBACK (generated_avatar_activated), um); + um->custom_avatar_was_chosen = FALSE; + + facesdirs = get_settings_facesdirs (); + added_faces = add_faces_from_dirs (um->faces, facesdirs, TRUE); + + if (!added_faces) { + facesdirs = get_system_facesdirs (); + add_faces_from_dirs (um->faces, facesdirs, FALSE); + } +} + +void +um_photo_dialog_generate_avatar (UmPhotoDialog *um, + const gchar *name) +{ + cairo_surface_t *surface; + gchar *filename; + + surface = generate_user_picture (name); + + /* Save into a tmp file that later gets copied by AccountsService */ + filename = g_build_filename (g_get_user_runtime_dir (), "avatar.png", NULL); + um->generated_avatar = g_file_new_for_path (filename); + cairo_surface_write_to_png (surface, g_file_get_path (um->generated_avatar)); + g_free (filename); + + /* Overwrite the first item */ + if (g_list_model_get_item (G_LIST_MODEL (um->recent_faces), 0) != NULL) + g_list_store_remove (um->recent_faces, 0); + + g_list_store_insert (um->recent_faces, 0, + um->generated_avatar); + + if (!um->custom_avatar_was_chosen) { + um->callback (NULL, g_file_get_path (um->generated_avatar), um->data); + } +} + +UmPhotoDialog * +um_photo_dialog_new (SelectAvatarCallback callback, + gpointer data) +{ + UmPhotoDialog *um; + + um = g_object_new (UM_TYPE_PHOTO_DIALOG, NULL); + + setup_photo_popup (um); + + um->callback = callback; + um->data = data; + + return um; +} + +void +um_photo_dialog_dispose (GObject *object) +{ + G_OBJECT_CLASS (um_photo_dialog_parent_class)->dispose (object); +} + +static void +um_photo_dialog_init (UmPhotoDialog *um) +{ + gtk_widget_init_template (GTK_WIDGET (um)); +} + +static void +um_photo_dialog_class_init (UmPhotoDialogClass *klass) +{ + GtkWidgetClass *wclass = GTK_WIDGET_CLASS (klass); + GObjectClass *oclass = G_OBJECT_CLASS (klass); + + gtk_widget_class_set_template_from_resource (wclass, "/org/gnome/initial-setup/gis-account-avatar-chooser.ui"); + + gtk_widget_class_bind_template_child (wclass, UmPhotoDialog, flowbox); + gtk_widget_class_bind_template_child (wclass, UmPhotoDialog, recent_pictures); + gtk_widget_class_bind_template_child (wclass, UmPhotoDialog, take_picture_button); + gtk_widget_class_bind_template_callback (wclass, webcam_icon_selected); + + oclass->dispose = um_photo_dialog_dispose; +} diff --git a/gnome-initial-setup/pages/account/um-photo-dialog.h b/gnome-initial-setup/pages/account/um-photo-dialog.h new file mode 100644 index 0000000..75061ff --- /dev/null +++ b/gnome-initial-setup/pages/account/um-photo-dialog.h @@ -0,0 +1,48 @@ +/* -*- 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA + * + * Written by: Matthias Clasen <mclasen@redhat.com> + */ + +#ifndef __UM_PHOTO_DIALOG_H__ +#define __UM_PHOTO_DIALOG_H__ + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define UM_TYPE_PHOTO_DIALOG (um_photo_dialog_get_type()) + +G_DECLARE_FINAL_TYPE (UmPhotoDialog, um_photo_dialog, UM, PHOTO_DIALOG, GtkPopover) + +typedef struct _UmPhotoDialog UmPhotoDialog; +typedef void (SelectAvatarCallback) (GdkPixbuf *pixbuf, + const gchar *filename, + gpointer data); + +UmPhotoDialog *um_photo_dialog_new (SelectAvatarCallback callback, + gpointer data); +void um_photo_dialog_free (UmPhotoDialog *dialog); + +void um_photo_dialog_generate_avatar (UmPhotoDialog *dialog, + const gchar *name); + +G_END_DECLS + +#endif diff --git a/gnome-initial-setup/pages/account/um-realm-manager.c b/gnome-initial-setup/pages/account/um-realm-manager.c new file mode 100644 index 0000000..bc4fd33 --- /dev/null +++ b/gnome-initial-setup/pages/account/um-realm-manager.c @@ -0,0 +1,878 @@ +/* -*- 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: Matthias Clasen <mclasen@redhat.com> + * Stef Walter <stefw@gnome.org> + */ + +#include "config.h" + +#include "um-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 _UmRealmManager { + UmRealmObjectManagerClient parent; + UmRealmProvider *provider; +}; + +typedef struct { + UmRealmProviderProxyClass parent_class; +} UmRealmManagerClass; + +enum { + REALM_ADDED, + NUM_SIGNALS, +}; + +static gint signals[NUM_SIGNALS] = { 0, }; + +G_DEFINE_TYPE (UmRealmManager, um_realm_manager, UM_REALM_TYPE_OBJECT_MANAGER_CLIENT); + +GQuark +um_realm_error_get_quark (void) +{ + static GQuark quark = 0; + if (quark == 0) + quark = g_quark_from_static_string ("um-realm-error"); + return quark; +} + +static gboolean +is_realm_with_kerberos_and_membership (gpointer object) +{ + GDBusInterface *interface; + + if (!G_IS_DBUS_OBJECT (object)) + return FALSE; + + interface = g_dbus_object_get_interface (object, "org.freedesktop.realmd.Kerberos"); + if (interface == NULL) + return FALSE; + g_object_unref (interface); + + interface = g_dbus_object_get_interface (object, "org.freedesktop.realmd.KerberosMembership"); + if (interface == NULL) + return FALSE; + g_object_unref (interface); + + return TRUE; +} + +static void +on_interface_added (GDBusObjectManager *manager, + GDBusObject *object, + GDBusInterface *interface) +{ + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (interface), G_MAXINT); +} + +static void +on_object_added (GDBusObjectManager *manager, + GDBusObject *object, + gpointer user_data) +{ + GList *interfaces, *l; + + interfaces = g_dbus_object_get_interfaces (object); + for (l = interfaces; l != NULL; l = g_list_next (l)) + on_interface_added (manager, 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 (user_data, signals[REALM_ADDED], 0, object); + } +} + +static void +um_realm_manager_init (UmRealmManager *self) +{ + g_signal_connect (self, "object-added", G_CALLBACK (on_object_added), self); + g_signal_connect (self, "interface-added", G_CALLBACK (on_interface_added), self); +} + +static void +um_realm_manager_dispose (GObject *obj) +{ + UmRealmManager *self = UM_REALM_MANAGER (obj); + + g_clear_object (&self->provider); + + G_OBJECT_CLASS (um_realm_manager_parent_class)->dispose (obj); +} + +static void +um_realm_manager_class_init (UmRealmManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = um_realm_manager_dispose; + + signals[REALM_ADDED] = g_signal_new ("realm-added", UM_TYPE_REALM_MANAGER, + G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + g_cclosure_marshal_generic, + G_TYPE_NONE, 1, UM_REALM_TYPE_OBJECT); +} + +typedef struct { + GCancellable *cancellable; + UmRealmManager *manager; +} NewClosure; + +static void +new_closure_free (gpointer data) +{ + NewClosure *closure = data; + g_clear_object (&closure->cancellable); + g_clear_object (&closure->manager); + g_slice_free (NewClosure, closure); +} + +static void +on_provider_new (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GSimpleAsyncResult *async = G_SIMPLE_ASYNC_RESULT (user_data); + NewClosure *closure = g_simple_async_result_get_op_res_gpointer (async); + GError *error = NULL; + UmRealmProvider *provider; + + provider = um_realm_provider_proxy_new_finish (result, &error); + closure->manager->provider = provider; + + if (error == NULL) { + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (closure->manager->provider), -1); + g_debug ("Created realm manager"); + } else { + g_simple_async_result_take_error (async, error); + } + g_simple_async_result_complete (async); + + g_object_unref (async); +} + +static void +on_manager_new (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GSimpleAsyncResult *async = G_SIMPLE_ASYNC_RESULT (user_data); + NewClosure *closure = g_simple_async_result_get_op_res_gpointer (async); + GDBusConnection *connection; + GError *error = NULL; + GObject *object; + + object = g_async_initable_new_finish (G_ASYNC_INITABLE (source), result, &error); + if (error == NULL) { + closure->manager = UM_REALM_MANAGER (object); + connection = g_dbus_object_manager_client_get_connection (G_DBUS_OBJECT_MANAGER_CLIENT (object)); + + g_debug ("Connected to realmd"); + + um_realm_provider_proxy_new (connection, + G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES, + "org.freedesktop.realmd", + "/org/freedesktop/realmd", + closure->cancellable, + on_provider_new, g_object_ref (async)); + } else { + g_simple_async_result_take_error (async, error); + g_simple_async_result_complete (async); + } + + g_object_unref (async); +} + +void +um_realm_manager_new (GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GSimpleAsyncResult *async; + NewClosure *closure; + + g_debug ("Connecting to realmd..."); + + async = g_simple_async_result_new (NULL, callback, user_data, + um_realm_manager_new); + closure = g_slice_new (NewClosure); + closure->cancellable = cancellable ? g_object_ref (cancellable) : NULL; + g_simple_async_result_set_op_res_gpointer (async, closure, new_closure_free); + + g_async_initable_new_async (UM_TYPE_REALM_MANAGER, G_PRIORITY_DEFAULT, + cancellable, on_manager_new, g_object_ref (async), + "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", um_realm_object_manager_client_get_proxy_type, + NULL); + + g_object_unref (async); +} + +UmRealmManager * +um_realm_manager_new_finish (GAsyncResult *result, + GError **error) +{ + GSimpleAsyncResult *async; + NewClosure *closure; + + g_return_val_if_fail (g_simple_async_result_is_valid (result, NULL, + um_realm_manager_new), NULL); + + async = G_SIMPLE_ASYNC_RESULT (result); + if (g_simple_async_result_propagate_error (async, error)) + return NULL; + + closure = g_simple_async_result_get_op_res_gpointer (async); + return g_object_ref (closure->manager); +} + +typedef struct { + UmRealmManager *manager; + GCancellable *cancellable; + GList *realms; +} DiscoverClosure; + +static void +discover_closure_free (gpointer data) +{ + DiscoverClosure *discover = data; + g_object_unref (discover->manager); + g_clear_object (&discover->cancellable); + g_list_free_full (discover->realms, g_object_unref); + g_slice_free (DiscoverClosure, discover); +} + +static void +on_provider_discover (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GSimpleAsyncResult *async = G_SIMPLE_ASYNC_RESULT (user_data); + DiscoverClosure *discover = g_simple_async_result_get_op_res_gpointer (async); + GDBusObject *object; + GError *error = NULL; + gboolean no_membership = FALSE; + gchar **realms; + gint relevance; + gint i; + + um_realm_provider_call_discover_finish (UM_REALM_PROVIDER (source), &relevance, + &realms, result, &error); + if (error == NULL) { + for (i = 0; realms[i]; i++) { + object = g_dbus_object_manager_get_object (G_DBUS_OBJECT_MANAGER (discover->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]); + discover->realms = g_list_prepend (discover->realms, object); + } else { + g_debug ("Realm does not support kerberos membership: %s", realms[i]); + no_membership = TRUE; + g_object_unref (object); + } + } + } + g_strfreev (realms); + + if (!discover->realms && no_membership) { + g_simple_async_result_set_error (async, UM_REALM_ERROR, UM_REALM_ERROR_GENERIC, + _("Cannot automatically join this type of domain")); + } + } else { + g_simple_async_result_take_error (async, error); + } + + g_simple_async_result_complete (async); + g_object_unref (async); +} + +void +um_realm_manager_discover (UmRealmManager *self, + const gchar *input, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GSimpleAsyncResult *res; + DiscoverClosure *discover; + GVariant *options; + + g_return_if_fail (UM_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); + + res = g_simple_async_result_new (G_OBJECT (self), callback, user_data, + um_realm_manager_discover); + discover = g_slice_new0 (DiscoverClosure); + discover->manager = g_object_ref (self); + discover->cancellable = cancellable ? g_object_ref (cancellable) : NULL; + g_simple_async_result_set_op_res_gpointer (res, discover, discover_closure_free); + + options = g_variant_new_array (G_VARIANT_TYPE ("{sv}"), NULL, 0); + + um_realm_provider_call_discover (self->provider, input, options, cancellable, + on_provider_discover, g_object_ref (res)); + + g_object_unref (res); +} + +GList * +um_realm_manager_discover_finish (UmRealmManager *self, + GAsyncResult *result, + GError **error) +{ + GSimpleAsyncResult *async; + DiscoverClosure *discover; + GList *realms; + + g_return_val_if_fail (UM_IS_REALM_MANAGER (self), NULL); + g_return_val_if_fail (g_simple_async_result_is_valid (result, G_OBJECT (self), + um_realm_manager_discover), NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + async = G_SIMPLE_ASYNC_RESULT (result); + if (g_simple_async_result_propagate_error (async, error)) + return NULL; + + discover = g_simple_async_result_get_op_res_gpointer (async); + if (!discover->realms) { + g_set_error (error, UM_REALM_ERROR, UM_REALM_ERROR_GENERIC, + _("No such domain or realm found")); + return NULL; + } + + realms = g_list_reverse (discover->realms); + discover->realms = NULL; + return realms; +} + +GList * +um_realm_manager_get_realms (UmRealmManager *self) +{ + GList *objects; + GList *realms = NULL; + GList *l; + + g_return_val_if_fail (UM_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 * +um_realm_calculate_login (UmRealmCommon *realm, + const gchar *username) +{ + GString *string; + const gchar *const *formats; + gchar *login = NULL; + + formats = um_realm_common_get_login_formats (realm); + if (formats[0] != NULL) { + string = g_string_new (formats[0]); + string_replace (string, "%U", username); + string_replace (string, "%D", um_realm_common_get_name (realm)); + login = g_string_free (string, FALSE); + } + + return login; + +} + +gboolean +um_realm_is_configured (UmRealmObject *realm) +{ + UmRealmCommon *common; + const gchar *configured; + gboolean is = FALSE; + + common = um_realm_object_get_common (realm); + if (common) { + configured = um_realm_common_get_configured (common); + is = configured != NULL && !g_str_equal (configured, ""); + g_object_unref (common); + } + + return is; +} + +static const gchar * +find_supported_credentials (UmRealmKerberosMembership *membership, + const gchar *owner) +{ + const gchar *cred_owner; + const gchar *cred_type; + GVariant *supported; + GVariantIter iter; + + supported = um_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 void +on_realm_join_complete (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GSimpleAsyncResult *async = G_SIMPLE_ASYNC_RESULT (user_data); + + g_debug ("Completed Join() method call"); + + g_simple_async_result_set_op_res_gpointer (async, g_object_ref (result), g_object_unref); + g_simple_async_result_complete_in_idle (async); + g_object_unref (async); +} + +static gboolean +realm_join_as_owner (UmRealmObject *realm, + const gchar *owner, + const gchar *login, + const gchar *password, + GBytes *credentials, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + UmRealmKerberosMembership *membership; + GSimpleAsyncResult *async; + GVariant *contents; + GVariant *options; + GVariant *option; + GVariant *creds; + const gchar *type; + + membership = um_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); + g_object_unref (membership); + return FALSE; + } + + async = g_simple_async_result_new (G_OBJECT (realm), callback, user_data, + realm_join_as_owner); + + 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); + + um_realm_kerberos_membership_call_join (membership, creds, options, + cancellable, on_realm_join_complete, + g_object_ref (async)); + + g_object_unref (async); + g_object_unref (membership); + + return TRUE; +} + +gboolean +um_realm_join_as_user (UmRealmObject *realm, + const gchar *login, + const gchar *password, + GBytes *credentials, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_return_val_if_fail (UM_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 +um_realm_join_as_admin (UmRealmObject *realm, + const gchar *login, + const gchar *password, + GBytes *credentials, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_return_val_if_fail (UM_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 +um_realm_join_finish (UmRealmObject *realm, + GAsyncResult *result, + GError **error) +{ + UmRealmKerberosMembership *membership; + GError *call_error = NULL; + gchar *dbus_error; + GAsyncResult *async; + + g_return_val_if_fail (UM_REALM_IS_OBJECT (realm), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + membership = um_realm_object_get_kerberos_membership (realm); + g_return_val_if_fail (membership != NULL, FALSE); + + async = g_simple_async_result_get_op_res_gpointer (G_SIMPLE_ASYNC_RESULT (result)); + um_realm_kerberos_membership_call_join_finish (membership, async, &call_error); + g_object_unref (membership); + + if (call_error == NULL) + 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, 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, UM_REALM_ERROR, UM_REALM_ERROR_BAD_LOGIN, + "%s", call_error->message); + g_error_free (call_error); + } else if (g_str_equal (dbus_error, "org.freedesktop.realmd.Error.BadHostname")) { + g_debug ("Join() failed because of invalid/conflicting host name"); + g_set_error (error, UM_REALM_ERROR, UM_REALM_ERROR_BAD_HOSTNAME, + "%s", call_error->message); + g_error_free (call_error); + } else { + g_debug ("Join() failed because of %s", call_error->message); + g_propagate_error (error, call_error); + } + + g_free (dbus_error); + return FALSE; +} + +typedef struct { + gchar *domain; + gchar *realm; + gchar *user; + gchar *password; + GBytes *credentials; +} LoginClosure; + +static void +login_closure_free (gpointer data) +{ + LoginClosure *login = data; + g_free (login->domain); + g_free (login->realm); + g_free (login->user); + g_free (login->password); + g_bytes_unref (login->credentials); + 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; + gchar *name; + + 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)); + g_free (name); + return code; + } + + g_debug ("Using principal name to kinit: %s", name); + g_free (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 *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + LoginClosure *login = task_data; + krb5_context k5 = NULL; + krb5_error_code code; + GError *error = NULL; + 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_free (filename); + filename = NULL; + } 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_file_get_contents (filename, &contents, &length, &error); + if (error == NULL) { + login->credentials = g_bytes_new_take (contents, length); + g_debug ("Read in credential cache: %s", filename); + } else { + g_warning ("Couldn't read credential cache: %s: %s", + filename, error->message); + g_error_free (error); + } + } + g_task_return_boolean (task, TRUE); + break; + + case KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN: + case KRB5KDC_ERR_POLICY: + g_task_return_new_error (task, UM_REALM_ERROR, UM_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, UM_REALM_ERROR, UM_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, UM_REALM_ERROR, UM_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, UM_REALM_ERROR, UM_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); + g_free (filename); + } + + if (k5) + krb5_free_context (k5); +} + +void +um_realm_login (UmRealmObject *realm, + const gchar *user, + const gchar *password, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + LoginClosure *login; + UmRealmKerberos *kerberos; + + g_return_if_fail (UM_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 = um_realm_object_get_kerberos (realm); + g_return_if_fail (kerberos != NULL); + + task = g_task_new (realm, cancellable, callback, user_data); + login = g_slice_new0 (LoginClosure); + login->domain = g_strdup (um_realm_kerberos_get_domain_name (kerberos)); + login->realm = g_strdup (um_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_check_cancellable (task, TRUE); + g_task_set_return_on_cancel (task, TRUE); + + g_task_run_in_thread (task, kinit_thread_func); + + g_object_unref (task); + g_object_unref (kerberos); +} + +gboolean +um_realm_login_finish (UmRealmObject *realm, + GAsyncResult *result, + GBytes **credentials, + GError **error) +{ + GTask *task; + LoginClosure *login; + + g_return_val_if_fail (g_task_is_valid (result, realm), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + task = G_TASK (result); + if (!g_task_propagate_boolean (task, error)) + return FALSE; + + login = g_task_get_task_data (task); + if (credentials) { + if (login->credentials) + *credentials = g_bytes_ref (login->credentials); + else + *credentials = NULL; + } + + return TRUE; +} diff --git a/gnome-initial-setup/pages/account/um-realm-manager.h b/gnome-initial-setup/pages/account/um-realm-manager.h new file mode 100644 index 0000000..952bd2f --- /dev/null +++ b/gnome-initial-setup/pages/account/um-realm-manager.h @@ -0,0 +1,108 @@ +/* -*- 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> + */ + +#ifndef __UM_REALM_MANAGER_H__ +#define __UM_REALM_MANAGER_H__ + +#include "um-realm-generated.h" + +G_BEGIN_DECLS + +typedef enum { + UM_REALM_ERROR_BAD_LOGIN, + UM_REALM_ERROR_BAD_PASSWORD, + UM_REALM_ERROR_CANNOT_AUTH, + UM_REALM_ERROR_BAD_HOSTNAME, + UM_REALM_ERROR_GENERIC, +} UmRealmErrors; + +#define UM_REALM_ERROR (um_realm_error_get_quark ()) + +GQuark um_realm_error_get_quark (void) G_GNUC_CONST; + +#define UM_TYPE_REALM_MANAGER (um_realm_manager_get_type ()) +#define UM_REALM_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), UM_TYPE_REALM_MANAGER, UmRealmManager)) +#define UM_IS_REALM_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), UM_TYPE_REALM_MANAGER)) + +typedef struct _UmRealmManager UmRealmManager; + +GType um_realm_manager_get_type (void) G_GNUC_CONST; + +void um_realm_manager_new (GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +UmRealmManager * um_realm_manager_new_finish (GAsyncResult *result, + GError **error); + +void um_realm_manager_discover (UmRealmManager *self, + const gchar *input, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +GList * um_realm_manager_discover_finish (UmRealmManager *self, + GAsyncResult *result, + GError **error); + +GList * um_realm_manager_get_realms (UmRealmManager *self); + +void um_realm_login (UmRealmObject *realm, + const gchar *login, + const gchar *password, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +gboolean um_realm_login_finish (UmRealmObject *realm, + GAsyncResult *result, + GBytes **credentials, + GError **error); + +gboolean um_realm_join_as_user (UmRealmObject *realm, + const gchar *login, + const gchar *password, + GBytes *credentials, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) + G_GNUC_WARN_UNUSED_RESULT; + +gboolean um_realm_join_as_admin (UmRealmObject *realm, + const gchar *login, + const gchar *password, + GBytes *credentials, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) + G_GNUC_WARN_UNUSED_RESULT; + +gboolean um_realm_join_finish (UmRealmObject *realm, + GAsyncResult *result, + GError **error); + +gboolean um_realm_is_configured (UmRealmObject *realm); + +gchar * um_realm_calculate_login (UmRealmCommon *realm, + const gchar *username); + +G_END_DECLS + +#endif /* __UM_REALM_H__ */ diff --git a/gnome-initial-setup/pages/account/um-utils.c b/gnome-initial-setup/pages/account/um-utils.c new file mode 100644 index 0000000..2877d94 --- /dev/null +++ b/gnome-initial-setup/pages/account/um-utils.c @@ -0,0 +1,568 @@ +/* -*- 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA + * + * Written by: Matthias Clasen <mclasen@redhat.com> + */ + +#include "config.h" + +#include <math.h> +#include <stdlib.h> +#include <sys/types.h> +#include <pwd.h> +#include <utmp.h> + +#include <glib.h> +#include <glib/gi18n.h> + +#include "um-utils.h" + +void +set_entry_validation_checkmark (GtkEntry *entry) +{ + gtk_entry_set_icon_from_icon_name (entry, + GTK_ENTRY_ICON_SECONDARY, + "object-select-symbolic"); +} + +void +set_entry_validation_error (GtkEntry *entry, + const gchar *text) +{ + gtk_entry_set_icon_from_icon_name (entry, + GTK_ENTRY_ICON_SECONDARY, + "dialog-warning-symbolic"); + gtk_entry_set_icon_tooltip_text (entry, + GTK_ENTRY_ICON_SECONDARY, + text); +} + +void +clear_entry_validation_error (GtkEntry *entry) +{ + gtk_entry_set_icon_from_paintable (entry, + GTK_ENTRY_ICON_SECONDARY, + NULL); +} + +#define MAXNAMELEN (UT_NAMESIZE - 1) + +static 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; + const gchar *c; + + /* Valid names must contain: + * 1) at least one character. + * 2) at least one non-"space" character. + */ + 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 (!g_unichar_isspace (unichar)) { + is_empty = FALSE; + break; + } + } + + return !is_empty; +} + +gboolean +is_valid_username (const gchar *username, gboolean parental_controls_enabled, gchar **tip) +{ + gboolean empty; + gboolean in_use; + gboolean too_long; + gboolean valid; + gboolean parental_controls_conflict; + const gchar *c; + + if (username == NULL || username[0] == '\0') { + empty = TRUE; + in_use = FALSE; + too_long = FALSE; + } else { + empty = FALSE; + in_use = is_username_used (username); + too_long = strlen (username) > MAXNAMELEN; + } + valid = TRUE; + + if (!in_use && !empty && !too_long) { + /* First char must be a lower case letter, and it must only be + * composed of lower case letters, digits, '-', and '_'. + */ + for (c = username; *c; c++) { + if (c == username) { + if (! (*c >= 'a' && *c <= 'z')) + valid = FALSE; + } else { + if (! ((*c >= 'a' && *c <= 'z') || + (*c >= '0' && *c <= '9') || + (*c == '_') || (*c == '-'))) + valid = FALSE; + } + } + } + + parental_controls_conflict = (parental_controls_enabled && g_strcmp0 (username, "administrator") == 0); + + valid = !empty && !in_use && !too_long && !parental_controls_conflict && valid; + + if (!empty && (in_use || too_long || parental_controls_conflict || !valid)) { + if (in_use) { + *tip = g_strdup (_("Sorry, that user name isn’t available. Please try another.")); + } + else if (too_long) { + *tip = g_strdup_printf (_("The username is too long.")); + } + else if (!(username[0] >= 'a' && username[0] <= 'z')) { + *tip = g_strdup (_("The username must start with a lower case letter from a-z.")); + } + else if (parental_controls_conflict) { + *tip = g_strdup (_("That username isn’t available. Please try another.")); + } + else { + *tip = g_strdup (_("The username should only consist of lower case letters from a-z, digits, and the following characters: - _")); + } + } + else { + *tip = g_strdup (_("This will be used to name your home folder and can’t be changed.")); + } + + return valid; +} + +void +generate_username_choices (const gchar *name, + GtkListStore *store) +{ + gboolean in_use, same_as_initial; + char *lc_name, *ascii_name, *stripped_name; + char **words1; + char **words2 = NULL; + char **w1, **w2; + char *c; + char *unicode_fallback = "?"; + GString *first_word, *last_word; + GString *item0, *item1, *item2, *item3, *item4; + int len; + int nwords1, nwords2, i; + GHashTable *items; + GtkTreeIter iter; + + gtk_list_store_clear (store); + + ascii_name = g_convert_with_fallback (name, -1, "ASCII//TRANSLIT", "UTF-8", + unicode_fallback, NULL, NULL, NULL); + + 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) { + g_free (ascii_name); + g_free (lc_name); + g_free (stripped_name); + 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)); + + g_free (ascii_name); + g_free (lc_name); + g_free (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++) { + 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); + } + + g_strfreev (words2); + } + item2 = g_string_append (item2, last_word->str); + item3 = g_string_append (item3, first_word->str); + item4 = g_string_prepend (item4, last_word->str); + + 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); + } + } + + g_hash_table_destroy (items); + g_strfreev (words1); + g_string_free (first_word, TRUE); + g_string_free (last_word, TRUE); + g_string_free (item0, TRUE); + g_string_free (item1, TRUE); + g_string_free (item2, TRUE); + g_string_free (item3, TRUE); + g_string_free (item4, TRUE); +} + +#define IMAGE_SIZE 512 +#define IMAGE_BORDER_WIDTH 7.2 /* At least 1px when avatar rendered at 72px */ + +/* U+1F464 "bust in silhouette" + * U+FE0E Variant Selector 15 to force text style (monochrome) emoji + */ +#define PLACEHOLDER "\U0001F464\U0000FE0E" + +static gchar * +extract_initials_from_name (const gchar *name) +{ + GString *initials = g_string_new (""); + gchar *p; + gchar *normalized; + gunichar unichar; + + if (name == NULL || name[0] == '\0') { + g_string_free (initials, TRUE); + return g_strdup (PLACEHOLDER); + } + + p = g_utf8_strup (name, -1); + normalized = g_utf8_normalize (g_strstrip (p), -1, G_NORMALIZE_DEFAULT_COMPOSE); + g_clear_pointer (&p, g_free); + if (normalized == NULL) { + g_free (normalized); + g_string_free (initials, TRUE); + return g_strdup (PLACEHOLDER); + } + + unichar = g_utf8_get_char (normalized); + g_string_append_unichar (initials, unichar); + + p = g_utf8_strrchr (normalized, -1, ' '); + if (p != NULL) { + p = g_utf8_next_char (p); + + unichar = g_utf8_get_char (p); + g_string_append_unichar (initials, unichar); + } + + g_free (normalized); + + return g_string_free (initials, FALSE); +} + +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 || name[0] == '\0') { + idx = 5; + } else { + 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 void +darken_color (GdkRGBA *color, gdouble fraction) { + color->red = CLAMP (color->red + color->red * fraction, 0, 255); + color->green = CLAMP (color->green + color->green * fraction, 0, 255); + color->blue = CLAMP (color->blue + color->blue * fraction, 0, 255); +} + +cairo_surface_t * +generate_user_picture (const gchar *name) { + PangoFontDescription *font_desc; + g_autofree gchar *initials = extract_initials_from_name (name); + g_autofree gchar *font = g_strdup_printf ("Sans %d", (int)ceil (IMAGE_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, + IMAGE_SIZE, + IMAGE_SIZE); + cr = cairo_create (surface); + + cairo_arc (cr, IMAGE_SIZE/2, IMAGE_SIZE/2, IMAGE_SIZE/2, 0, 2 * G_PI); + cairo_set_source_rgb (cr, color.red/255.0, color.green/255.0, color.blue/255.0); + cairo_fill (cr); + + cairo_arc (cr, IMAGE_SIZE / 2, IMAGE_SIZE / 2, IMAGE_SIZE / 2 - IMAGE_BORDER_WIDTH / 2, + 0, 2 * G_PI); + darken_color (&color, -0.3); + cairo_set_source_rgb (cr, color.red / 255.0, color.green / 255.0, color.blue / 255.0); + cairo_set_line_width (cr, IMAGE_BORDER_WIDTH); + cairo_stroke (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, IMAGE_SIZE/2, IMAGE_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; +} + +GdkPixbuf * +round_image (GdkPixbuf *image) +{ + GdkPixbuf *dest = NULL; + cairo_surface_t *surface; + cairo_t *cr; + gint size; + + size = gdk_pixbuf_get_width (image); + 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, image, 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; +} diff --git a/gnome-initial-setup/pages/account/um-utils.h b/gnome-initial-setup/pages/account/um-utils.h new file mode 100644 index 0000000..8a8c7cd --- /dev/null +++ b/gnome-initial-setup/pages/account/um-utils.h @@ -0,0 +1,50 @@ +/* -*- 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA + * + * Written by: Matthias Clasen <mclasen@redhat.com> + */ + +#ifndef __UM_UTILS_H__ +#define __UM_UTILS_H__ + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +void set_entry_validation_error (GtkEntry *entry, + const gchar *text); +void set_entry_validation_checkmark (GtkEntry *entry); +void clear_entry_validation_error (GtkEntry *entry); + +gboolean is_valid_name (const gchar *name); +gboolean is_valid_username (const gchar *name, + gboolean parental_controls_enabled, + gchar **tip); + +void generate_username_choices (const gchar *name, + GtkListStore *store); + +cairo_surface_t *generate_user_picture (const gchar *name); + +GdkPixbuf *round_image (GdkPixbuf *image); + + +G_END_DECLS + +#endif |