/* -*- 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 . * * Written by: * Jasper St. Pierre */ #include "config.h" #include "gis-page.h" #include "gis-account-page-local.h" #include "gnome-initial-setup.h" #include #include #include #include #include "um-utils.h" #include "um-photo-dialog.h" #include "gis-page-header.h" #define GOA_API_IS_SUBJECT_TO_CHANGE #include #include #include #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); }