[gnome-software/wip/william/cherry-pick-3] WIP
- From: William Hua <williamhua src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-software/wip/william/cherry-pick-3] WIP
- Date: Fri, 3 Jun 2016 07:00:25 +0000 (UTC)
commit 57d09c134865f4c6d91addf309f59a4322b550a7
Author: William Hua <william hua canonical com>
Date: Sat May 28 10:57:17 2016 -0400
WIP
configure.ac | 28 +
po/POTFILES.in | 3 +
src/gnome-software.gresource.xml | 2 +
src/gs-app-list.c | 13 +-
src/gs-app-row.c | 9 -
src/gs-app.c | 3 +
src/gs-application.c | 84 ++-
src/gs-common.c | 2 +-
src/gs-dbus-helper.c | 9 +-
src/gs-menus.ui | 2 +-
src/gs-os-release.c | 21 +
src/gs-os-release.h | 1 +
src/gs-plugin.h | 1 +
src/gs-review-dialog.c | 10 +-
src/gs-shell-details.c | 21 +-
src/gs-shell-search.c | 6 +
src/gs-shell-updates.c | 14 +-
src/gs-update-monitor.c | 29 +-
src/gtk-style-hc.css | 8 +-
src/gtk-style.css | 8 +-
src/plugins/Makefile.am | 65 ++-
src/plugins/com.canonical.Unity.Launcher.xml | 15 +
src/plugins/gs-plugin-appstream.c | 15 +
src/plugins/gs-plugin-apt.cc | 1154 ++++++++++++++++++++++++++
src/plugins/gs-plugin-packagekit-refine.c | 15 -
src/plugins/gs-plugin-packagekit-refresh.c | 3 -
src/plugins/gs-plugin-provenance.c | 1 +
src/plugins/gs-plugin-snappy.c | 476 +++++++++++
src/plugins/gs-plugin-ubuntu-reviews.c | 421 +++++++++-
src/plugins/gs-ubuntu-snapd.c | 277 ++++++
src/plugins/gs-ubuntu-snapd.h | 41 +
src/plugins/gs-ubuntuone-dialog.c | 666 +++++++++++++++
src/plugins/gs-ubuntuone-dialog.h | 45 +
src/plugins/gs-ubuntuone-dialog.ui | 386 +++++++++
src/plugins/gs-ubuntuone.c | 410 +++++++++
src/plugins/gs-ubuntuone.h | 49 ++
src/plugins/ubuntu-one.png | Bin 0 -> 2540 bytes
37 files changed, 4237 insertions(+), 76 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 3f89d83..bbe87aa 100644
--- a/configure.ac
+++ b/configure.ac
@@ -11,6 +11,7 @@ AC_CONFIG_MACRO_DIR([m4])
m4_ifdef([AM_SILENT_RULES],[AM_SILENT_RULES([yes])])
AC_PROG_CC
+AC_PROG_CXX
AC_PROG_INSTALL
LT_INIT
AM_PROG_CC_C_O
@@ -68,6 +69,8 @@ PKG_CHECK_MODULES(SQLITE, sqlite3)
PKG_CHECK_MODULES(SOUP, libsoup-2.4 >= 2.51.92)
PKG_CHECK_MODULES(GSETTINGS_DESKTOP_SCHEMAS, gsettings-desktop-schemas >= 3.11.5)
PKG_CHECK_MODULES(GNOME_DESKTOP, gnome-desktop-3.0 >= 3.17.92)
+PKG_CHECK_MODULES(OAUTH, oauth)
+PKG_CHECK_MODULES(LIBSECRET, libsecret-1)
AC_PATH_PROG(APPSTREAM_UTIL, [appstream-util], [unfound])
AC_ARG_ENABLE(man,
[AS_HELP_STRING([--enable-man],
@@ -118,6 +121,30 @@ AS_IF([test "x$have_packagekit" = "xyes"], [
])
AM_CONDITIONAL(HAVE_PACKAGEKIT, test "$have_packagekit" != no)
+# libapt
+AC_ARG_ENABLE(apt,
+ [AS_HELP_STRING([--enable-apt],
+ [enable apt support [default=auto]])],,
+ enable_apt=maybe)
+AS_IF([test "x$enable_apt" != "xno"], [
+ AC_LANG_PUSH([C++])
+ AC_CHECK_HEADERS([apt-pkg/init.h],
+ [have_apt=yes],
+ [have_apt=no])
+ AC_LANG_POP
+], [
+ have_apt=no
+])
+
+AS_IF([test "x$have_apt" = "xyes"], [
+ AC_DEFINE(HAVE_APT,1,[Build apt support])
+], [
+ AS_IF([test "x$enable_apt" = "xyes"], [
+ AC_MSG_ERROR([apt support requested but 'libapt-pkg' was not found])
+ ])
+])
+AM_CONDITIONAL(HAVE_APT, test "$have_apt" != no)
+
# PolicyKit
AC_ARG_ENABLE(polkit,
[AS_HELP_STRING([--enable-polkit],
@@ -334,6 +361,7 @@ echo "
Dogtail: ${enable_dogtail}
Self tests: ${enable_tests}
PackageKit support: ${have_packagekit}
+ APT support: ${have_apt}
PolicyKit support: ${have_polkit}
Firmware support: ${have_firmware}
Limba support: ${have_limba}
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 5b7e6b9..7fc57f1 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -56,6 +56,9 @@ src/gs-upgrade-banner.c
src/gs-common.c
[type: gettext/glade]src/gs-menus.ui
src/org.gnome.Software.desktop.in
+src/plugins/gs-ubuntuone-dialog.c
+[type: gettext/glade]src/plugins/gs-ubuntuone-dialog.ui
+src/plugins/gs-plugin-snappy.c
src/plugins/menu-spec-common.c
[type: gettext/glade]src/gs-popular-tile.ui
src/gs-shell-loading.c
diff --git a/src/gnome-software.gresource.xml b/src/gnome-software.gresource.xml
index 773ce8e..393c255 100644
--- a/src/gnome-software.gresource.xml
+++ b/src/gnome-software.gresource.xml
@@ -34,5 +34,7 @@
<file preprocess="xml-stripblanks">org.freedesktop.PackageKit.xml</file>
<file>gtk-style.css</file>
<file>gtk-style-hc.css</file>
+ <file>plugins/ubuntu-one.png</file>
+ <file preprocess="xml-stripblanks">plugins/gs-ubuntuone-dialog.ui</file>
</gresource>
</gresources>
diff --git a/src/gs-app-list.c b/src/gs-app-list.c
index f0858c1..075ff82 100644
--- a/src/gs-app-list.c
+++ b/src/gs-app-list.c
@@ -258,9 +258,10 @@ gs_app_list_filter_duplicates (GsAppList *list)
{
guint i;
GsApp *app;
- GsApp *found;
- const gchar *id;
+ GsApp *found, *found_source;
+ const gchar *id, *source;
g_autoptr(GHashTable) hash = NULL;
+ g_autoptr(GHashTable) source_hash = NULL;
g_autoptr(GsAppList) old = NULL;
g_return_if_fail (GS_IS_APP_LIST (list));
@@ -271,18 +272,24 @@ gs_app_list_filter_duplicates (GsAppList *list)
/* create a new list with just the unique items */
hash = g_hash_table_new (g_str_hash, g_str_equal);
+ source_hash = g_hash_table_new (g_str_hash, g_str_equal);
for (i = 0; i < old->array->len; i++) {
app = gs_app_list_index (old, i);
id = gs_app_get_id (app);
+ source = gs_app_get_source_default (app);
if (id == NULL) {
gs_app_list_add (list, app);
continue;
}
found = g_hash_table_lookup (hash, id);
- if (found == NULL) {
+ found_source = source != NULL ? g_hash_table_lookup (source_hash, source) : NULL;
+ if (found == NULL && found_source == NULL) {
gs_app_list_add (list, app);
g_hash_table_insert (hash, (gpointer) id,
GUINT_TO_POINTER (1));
+ if (source != NULL)
+ g_hash_table_insert (source_hash, (gpointer) source,
+ GUINT_TO_POINTER (1));
continue;
}
g_debug ("ignoring duplicate %s", id);
diff --git a/src/gs-app-row.c b/src/gs-app-row.c
index 5f634c5..d04b9a9 100644
--- a/src/gs-app-row.c
+++ b/src/gs-app-row.c
@@ -84,15 +84,6 @@ gs_app_row_get_description (GsAppRow *app_row)
GsAppRowPrivate *priv = gs_app_row_get_instance_private (app_row);
const gchar *tmp = NULL;
- /* convert the markdown update description into PangoMarkup */
- if (priv->show_update &&
- (gs_app_get_state (priv->app) == AS_APP_STATE_UPDATABLE ||
- gs_app_get_state (priv->app) == AS_APP_STATE_UPDATABLE_LIVE)) {
- tmp = gs_app_get_update_details (priv->app);
- if (tmp != NULL && tmp[0] != '\0')
- return g_string_new (tmp);
- }
-
if (gs_app_get_state (priv->app) == AS_APP_STATE_UNAVAILABLE)
return g_string_new (gs_app_get_summary_missing (priv->app));
diff --git a/src/gs-app.c b/src/gs-app.c
index 323fde1..1bfa7f1 100644
--- a/src/gs-app.c
+++ b/src/gs-app.c
@@ -547,6 +547,9 @@ gs_app_set_state_internal (GsApp *app, AsAppState state)
case AS_APP_STATE_INSTALLED:
/* installed has to go into an action state */
if (state == AS_APP_STATE_UNKNOWN ||
+ state == AS_APP_STATE_INSTALLING ||
+ state == AS_APP_STATE_UPDATABLE ||
+ state == AS_APP_STATE_UPDATABLE_LIVE ||
state == AS_APP_STATE_REMOVING)
state_change_ok = TRUE;
break;
diff --git a/src/gs-application.c b/src/gs-application.c
index a68947c..065298b 100644
--- a/src/gs-application.c
+++ b/src/gs-application.c
@@ -247,15 +247,97 @@ gs_application_dbus_unregister (GApplication *application,
}
static void
+refreshed_cb (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GsPluginLoader *loader = GS_PLUGIN_LOADER (source_object);
+
+ if (gs_plugin_loader_refresh_finish (loader, res, NULL)) {
+ gs_plugin_loader_refresh_async (loader,
+ 0,
+ GS_PLUGIN_REFRESH_FLAGS_UI,
+ NULL,
+ NULL,
+ NULL);
+ }
+}
+
+static void
+start_refresh (GsApplication *app)
+{
+ g_action_group_activate_action (G_ACTION_GROUP (app),
+ "set-mode",
+ g_variant_new_string ("updates"));
+
+ gs_plugin_loader_refresh_async (gs_application_get_plugin_loader (app),
+ 0,
+ GS_PLUGIN_REFRESH_FLAGS_METADATA |
+ GS_PLUGIN_REFRESH_FLAGS_PAYLOAD |
+ GS_PLUGIN_REFRESH_FLAGS_UI,
+ NULL,
+ refreshed_cb,
+ app);
+}
+
+#define APP_INFO_PATH "/var/lib/app-info"
+
+static gboolean
+needs_refresh (void)
+{
+ g_autoptr(GDir) dir = g_dir_open (APP_INFO_PATH, 0, NULL);
+
+ return dir == NULL || g_dir_read_name (dir) == NULL;
+}
+
+static gboolean
+ask_refresh (gpointer user_data)
+{
+ GsApplication *app = user_data;
+ GtkWindow *parent;
+ GtkWidget *dialog;
+
+ parent = gtk_application_get_active_window (GTK_APPLICATION (app));
+
+ if (gtk_widget_is_visible (GTK_WIDGET (parent))) {
+ dialog = gtk_message_dialog_new (parent,
+ GTK_DIALOG_MODAL |
+ GTK_DIALOG_DESTROY_WITH_PARENT,
+ GTK_MESSAGE_QUESTION,
+ GTK_BUTTONS_YES_NO,
+ _("An update is needed to show all installable apps.
Download now?"));
+
+ if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_YES)
+ start_refresh (app);
+
+ gtk_widget_destroy (dialog);
+ }
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+first_run_dialog_destroyed_cb (GtkWidget *object,
+ gpointer user_data)
+{
+ g_signal_handlers_disconnect_by_data (object, user_data);
+
+ if (needs_refresh ())
+ gdk_threads_add_idle (ask_refresh, user_data);
+}
+
+static void
gs_application_show_first_run_dialog (GsApplication *app)
{
GtkWidget *dialog;
if (g_settings_get_boolean (app->settings, "first-run") == TRUE) {
dialog = gs_first_run_dialog_new ();
+ g_signal_connect (dialog, "destroy", G_CALLBACK (first_run_dialog_destroyed_cb), app);
gs_shell_modal_dialog_present (app->shell, GTK_DIALOG (dialog));
g_settings_set_boolean (app->settings, "first-run", FALSE);
- }
+ } else if (needs_refresh ())
+ gdk_threads_add_idle (ask_refresh, app);
}
static void
diff --git a/src/gs-common.c b/src/gs-common.c
index 29a6673..92f2f0a 100644
--- a/src/gs-common.c
+++ b/src/gs-common.c
@@ -136,7 +136,7 @@ gs_app_notify_installed (GsApp *app)
* has been successfully installed */
summary = g_strdup_printf (_("%s is now installed"), gs_app_get_name (app));
n = g_notification_new (summary);
- if (gs_app_get_kind (app) == AS_APP_KIND_DESKTOP) {
+ if (gs_app_get_kind (app) == AS_APP_KIND_DESKTOP && !gs_utils_is_current_desktop ("Unity")) {
/* TRANSLATORS: this is button that opens the newly installed application */
g_notification_add_button_with_target (n, _("Launch"),
"app.launch", "s",
diff --git a/src/gs-dbus-helper.c b/src/gs-dbus-helper.c
index 59a2c4f..98dff67 100644
--- a/src/gs-dbus-helper.c
+++ b/src/gs-dbus-helper.c
@@ -32,6 +32,7 @@
#include "gs-packagekit-modify2-generated.h"
#include "gs-resources.h"
#include "gs-shell-extras.h"
+#include "gs-common.h"
#include "gs-utils.h"
struct _GsDbusHelper {
@@ -345,9 +346,11 @@ notify_search_resources (GsShellExtrasMode mode,
n = g_notification_new (title);
g_notification_set_body (n, body);
- /* TRANSLATORS: this is a button that launches gnome-software */
- g_notification_add_button_with_target (n, _("Find in Software"), "app.install-resources", "(s^ass)",
mode_string, resources, "");
- g_notification_set_default_action_and_target (n, "app.install-resources", "(s^ass)", mode_string,
resources, "");
+ if (!gs_utils_is_current_desktop ("Unity")) {
+ /* TRANSLATORS: this is a button that launches gnome-software */
+ g_notification_add_button_with_target (n, _("Find in Software"), "app.install-resources",
"(s^ass)", mode_string, resources, "");
+ g_notification_set_default_action_and_target (n, "app.install-resources", "(s^ass)",
mode_string, resources, "");
+ }
g_application_send_notification (g_application_get_default (), "install-resources", n);
}
diff --git a/src/gs-menus.ui b/src/gs-menus.ui
index af7f63b..f6a68d9 100644
--- a/src/gs-menus.ui
+++ b/src/gs-menus.ui
@@ -4,7 +4,7 @@
<menu id="app-menu">
<section>
<item>
- <attribute name="label" translatable="yes">_Software Sources</attribute>
+ <attribute name="label" translatable="yes">_Software & Updates</attribute>
<attribute name="action">app.sources</attribute>
<attribute name="hidden-when">action-disabled</attribute>
</item>
diff --git a/src/gs-os-release.c b/src/gs-os-release.c
index 3398511..94e9bc4 100644
--- a/src/gs-os-release.c
+++ b/src/gs-os-release.c
@@ -45,6 +45,7 @@ struct _GsOsRelease
gchar *id;
gchar *version_id;
gchar *pretty_name;
+ gchar *ubuntu_codename;
};
static void gs_os_release_initable_iface_init (GInitableIface *iface);
@@ -119,6 +120,10 @@ gs_os_release_initable_init (GInitable *initable,
os_release->pretty_name = g_strdup (tmp);
continue;
}
+ if (g_strcmp0 (lines[i], "UBUNTU_CODENAME") == 0) {
+ os_release->ubuntu_codename = g_strdup (tmp);
+ continue;
+ }
}
return TRUE;
}
@@ -199,6 +204,21 @@ gs_os_release_get_pretty_name (GsOsRelease *os_release)
}
/**
+ * gs_os_release_get_ubuntu_codename:
+ * @os_release: A #GsOsRelease
+ *
+ * Gets the Ubuntu code name from the os-release parser.
+ *
+ * Returns: a string, or %NULL
+ **/
+const gchar *
+gs_os_release_get_ubuntu_codename (GsOsRelease *os_release)
+{
+ g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL);
+ return os_release->ubuntu_codename;
+}
+
+/**
* gs_os_release_finalize:
**/
static void
@@ -210,6 +230,7 @@ gs_os_release_finalize (GObject *object)
g_free (os_release->id);
g_free (os_release->version_id);
g_free (os_release->pretty_name);
+ g_free (os_release->ubuntu_codename);
G_OBJECT_CLASS (gs_os_release_parent_class)->finalize (object);
}
diff --git a/src/gs-os-release.h b/src/gs-os-release.h
index ffd338e..508b81f 100644
--- a/src/gs-os-release.h
+++ b/src/gs-os-release.h
@@ -39,6 +39,7 @@ const gchar *gs_os_release_get_version (GsOsRelease *os_release);
const gchar *gs_os_release_get_id (GsOsRelease *os_release);
const gchar *gs_os_release_get_version_id (GsOsRelease *os_release);
const gchar *gs_os_release_get_pretty_name (GsOsRelease *os_release);
+const gchar *gs_os_release_get_ubuntu_codename (GsOsRelease *os_release);
G_END_DECLS
diff --git a/src/gs-plugin.h b/src/gs-plugin.h
index 230c4a1..a474569 100644
--- a/src/gs-plugin.h
+++ b/src/gs-plugin.h
@@ -195,6 +195,7 @@ typedef enum {
GS_PLUGIN_REFRESH_FLAGS_METADATA = 1 << 0,
GS_PLUGIN_REFRESH_FLAGS_PAYLOAD = 1 << 1,
GS_PLUGIN_REFRESH_FLAGS_INTERACTIVE = 1 << 2,
+ GS_PLUGIN_REFRESH_FLAGS_UI = 1 << 3,
/*< private >*/
GS_PLUGIN_REFRESH_FLAGS_LAST
} GsPluginRefreshFlags;
diff --git a/src/gs-review-dialog.c b/src/gs-review-dialog.c
index 9f41dc1..4a9a620 100644
--- a/src/gs-review-dialog.c
+++ b/src/gs-review-dialog.c
@@ -28,11 +28,11 @@
#include "gs-review-dialog.h"
#include "gs-star-widget.h"
-#define DESCRIPTION_LENGTH_MAX 3000 /* chars */
-#define DESCRIPTION_LENGTH_MIN 15 /* chars */
-#define SUMMARY_LENGTH_MAX 70 /* chars */
-#define SUMMARY_LENGTH_MIN 3 /* chars */
-#define WRITING_TIME_MIN 5 /* seconds */
+#define DESCRIPTION_LENGTH_MAX 10000 /* chars */
+#define DESCRIPTION_LENGTH_MIN 1 /* chars */
+#define SUMMARY_LENGTH_MAX 1000 /* chars */
+#define SUMMARY_LENGTH_MIN 1 /* chars */
+#define WRITING_TIME_MIN 0 /* seconds */
struct _GsReviewDialog
{
diff --git a/src/gs-shell-details.c b/src/gs-shell-details.c
index d14dd4c..9fe8c71 100644
--- a/src/gs-shell-details.c
+++ b/src/gs-shell-details.c
@@ -84,6 +84,7 @@ struct _GsShellDetails
GtkWidget *label_details_category_value;
GtkWidget *label_details_developer_title;
GtkWidget *label_details_developer_value;
+ GtkWidget *label_details_license_title;
GtkWidget *label_details_license_value;
GtkWidget *label_details_origin_title;
GtkWidget *label_details_origin_value;
@@ -91,6 +92,7 @@ struct _GsShellDetails
GtkWidget *label_details_size_installed_value;
GtkWidget *label_details_size_download_title;
GtkWidget *label_details_size_download_value;
+ GtkWidget *label_details_updated_title;
GtkWidget *label_details_updated_value;
GtkWidget *label_details_version_value;
GtkWidget *label_failed;
@@ -841,12 +843,16 @@ gs_shell_details_refresh_all (GsShellDetails *self)
/* TRANSLATORS: this is where the license is not known */
gtk_label_set_label (GTK_LABEL (self->label_details_license_value), C_("license", "Unknown"));
gtk_widget_set_tooltip_text (self->label_details_license_value, NULL);
+ gtk_widget_set_visible (self->label_details_license_title, FALSE);
+ gtk_widget_set_visible (self->label_details_license_value, FALSE);
} else {
g_autofree gchar *license_markup = NULL;
license_markup = gs_shell_details_get_license_markup (tmp);
gtk_label_set_markup (GTK_LABEL (self->label_details_license_value),
license_markup);
gtk_widget_set_tooltip_text (self->label_details_license_value, NULL);
+ gtk_widget_set_visible (self->label_details_license_title, TRUE);
+ gtk_widget_set_visible (self->label_details_license_value, TRUE);
}
/* set version */
@@ -888,6 +894,8 @@ gs_shell_details_refresh_all (GsShellDetails *self)
updated == GS_APP_INSTALL_DATE_UNSET) {
/* TRANSLATORS: this is where the updated date is not known */
gtk_label_set_label (GTK_LABEL (self->label_details_updated_value), C_("updated", "Never"));
+ gtk_widget_set_visible (self->label_details_updated_title, FALSE);
+ gtk_widget_set_visible (self->label_details_updated_value, FALSE);
} else {
g_autoptr(GDateTime) dt = NULL;
g_autofree gchar *updated_str = NULL;
@@ -906,6 +914,12 @@ gs_shell_details_refresh_all (GsShellDetails *self)
gtk_label_set_markup (GTK_LABEL (self->label_details_updated_value), url->str);
g_string_free (url, TRUE);
}
+
+ // Disabled on Ubuntu as we don't have history support
+ // gtk_widget_set_visible (self->label_details_updated_title, TRUE);
+ // gtk_widget_set_visible (self->label_details_updated_value, TRUE);
+ gtk_widget_set_visible (self->label_details_updated_title, FALSE);
+ gtk_widget_set_visible (self->label_details_updated_value, FALSE);
}
/* set the category */
@@ -1019,9 +1033,10 @@ gs_shell_details_refresh_all (GsShellDetails *self)
/* hide the kudo details for non-desktop software */
switch (gs_app_get_kind (self->app)) {
- case AS_APP_KIND_DESKTOP:
+ // Hidden on Ubuntu since don't have appropriate information
+ /*case AS_APP_KIND_DESKTOP:
gtk_widget_set_visible (self->grid_details_kudo, TRUE);
- break;
+ break;*/
default:
gtk_widget_set_visible (self->grid_details_kudo, FALSE);
break;
@@ -1814,6 +1829,7 @@ gs_shell_details_class_init (GsShellDetailsClass *klass)
gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_details_category_value);
gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_details_developer_title);
gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_details_developer_value);
+ gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_details_license_title);
gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_details_license_value);
gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_details_origin_title);
gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_details_origin_value);
@@ -1821,6 +1837,7 @@ gs_shell_details_class_init (GsShellDetailsClass *klass)
gtk_widget_class_bind_template_child (widget_class, GsShellDetails,
label_details_size_download_value);
gtk_widget_class_bind_template_child (widget_class, GsShellDetails,
label_details_size_installed_title);
gtk_widget_class_bind_template_child (widget_class, GsShellDetails,
label_details_size_installed_value);
+ gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_details_updated_title);
gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_details_updated_value);
gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_details_version_value);
gtk_widget_class_bind_template_child (widget_class, GsShellDetails, label_failed);
diff --git a/src/gs-shell-search.c b/src/gs-shell-search.c
index cb17cf8..60da255 100644
--- a/src/gs-shell-search.c
+++ b/src/gs-shell-search.c
@@ -258,6 +258,12 @@ gs_shell_search_get_app_sort_key (GsApp *app)
/* sort installed, removing, other */
key = g_string_sized_new (64);
+ /* sort snaps before other apps */
+ if (g_strcmp0 (gs_app_get_management_plugin (app), "snappy") == 0)
+ g_string_append (key, "9:");
+ else
+ g_string_append (key, "1:");
+
/* sort missing codecs before applications */
switch (gs_app_get_state (app)) {
case AS_APP_STATE_UNAVAILABLE:
diff --git a/src/gs-shell-updates.c b/src/gs-shell-updates.c
index 62fa26a..3badb1d 100644
--- a/src/gs-shell-updates.c
+++ b/src/gs-shell-updates.c
@@ -486,7 +486,7 @@ gs_shell_updates_get_updates_cb (GsPluginLoader *plugin_loader,
self->any_require_reboot = FALSE;
for (i = 0; list != NULL && i < gs_app_list_length (list); i++) {
GsApp *app = gs_app_list_index (list, i);
- if (gs_app_get_state (app) != AS_APP_STATE_UPDATABLE_LIVE)
+ if (gs_app_get_state (app) == AS_APP_STATE_UPDATABLE)
self->all_updates_are_live = FALSE;
if (gs_app_has_quirk (app, AS_APP_QUIRK_NEEDS_REBOOT))
self->any_require_reboot = TRUE;
@@ -998,11 +998,13 @@ gs_shell_updates_perform_update_cb (GsPluginLoader *plugin_loader,
n = g_notification_new (_("Updates have been installed"));
/* TRANSLATORS: the new apps will not be run until we restart */
g_notification_set_body (n, _("A restart is required for them to take effect."));
- /* TRANSLATORS: button text */
- g_notification_add_button (n, _("Not Now"), "app.nop");
- /* TRANSLATORS: button text */
- g_notification_add_button_with_target (n, _("Restart"), "app.reboot", NULL);
- g_notification_set_default_action_and_target (n, "app.set-mode", "s", "updates");
+ if (!gs_utils_is_current_desktop ("Unity")) {
+ /* TRANSLATORS: button text */
+ g_notification_add_button (n, _("Not Now"), "app.nop");
+ /* TRANSLATORS: button text */
+ g_notification_add_button_with_target (n, _("Restart"), "app.reboot", NULL);
+ g_notification_set_default_action_and_target (n, "app.set-mode", "s", "updates");
+ }
g_application_send_notification (g_application_get_default (), "restart-required", n);
}
}
diff --git a/src/gs-update-monitor.c b/src/gs-update-monitor.c
index 3b884e1..40d6fa1 100644
--- a/src/gs-update-monitor.c
+++ b/src/gs-update-monitor.c
@@ -91,17 +91,21 @@ notify_offline_update_available (GsUpdateMonitor *monitor)
body = _("It is recommended that you install important updates now");
n = g_notification_new (title);
g_notification_set_body (n, body);
- g_notification_add_button (n, _("Restart & Install"), "app.reboot-and-install");
- g_notification_set_default_action_and_target (n, "app.set-mode", "s", "updates");
+ if (!gs_utils_is_current_desktop ("Unity")) {
+ g_notification_add_button (n, _("Restart & Install"), "app.reboot-and-install");
+ g_notification_set_default_action_and_target (n, "app.set-mode", "s", "updates");
+ }
g_application_send_notification (monitor->application, "updates-available", n);
} else {
title = _("Software Updates Available");
body = _("Important OS and application updates are ready to be installed");
n = g_notification_new (title);
g_notification_set_body (n, body);
- g_notification_add_button (n, _("Not Now"), "app.nop");
- g_notification_add_button_with_target (n, _("View"), "app.set-mode", "s", "updates");
- g_notification_set_default_action_and_target (n, "app.set-mode", "s", "updates");
+ if (!gs_utils_is_current_desktop ("Unity")) {
+ g_notification_add_button (n, _("Not Now"), "app.nop");
+ g_notification_add_button_with_target (n, _("View"), "app.set-mode", "s", "updates");
+ g_notification_set_default_action_and_target (n, "app.set-mode", "s", "updates");
+ }
g_application_send_notification (monitor->application, "updates-available", n);
}
}
@@ -240,7 +244,8 @@ get_upgrades_finished_cb (GObject *object,
/* TRANSLATORS: this is a distro upgrade */
n = g_notification_new (_("Software Upgrade Available"));
g_notification_set_body (n, body);
- g_notification_set_default_action_and_target (n, "app.set-mode", "s", "updates");
+ if (!gs_utils_is_current_desktop ("Unity"))
+ g_notification_set_default_action_and_target (n, "app.set-mode", "s", "updates");
g_application_send_notification (monitor->application, "upgrades-available", n);
}
@@ -428,8 +433,10 @@ get_updates_historical_cb (GObject *object, GAsyncResult *res, gpointer data)
notification = g_notification_new (_("Software Updates Failed"));
/* TRANSLATORS: message when we offline updates have failed */
g_notification_set_body (notification, _("An important OS update failed to be installed."));
- g_notification_add_button (notification, _("Show Details"), "app.show-offline-update-error");
- g_notification_set_default_action (notification, "app.show-offline-update-error");
+ if (!gs_utils_is_current_desktop ("Unity")) {
+ g_notification_add_button (notification, _("Show Details"),
"app.show-offline-update-error");
+ g_notification_set_default_action (notification, "app.show-offline-update-error");
+ }
g_application_send_notification (monitor->application, "offline-updates", notification);
return;
}
@@ -465,8 +472,10 @@ get_updates_historical_cb (GObject *object, GAsyncResult *res, gpointer data)
* users can't express their opinions here. In some languages
* "Review (evaluate) something" is a different translation than
* "Review (browse) something." */
- g_notification_add_button_with_target (notification, C_("updates", "Review"), "app.set-mode", "s",
"updated");
- g_notification_set_default_action_and_target (notification, "app.set-mode", "s", "updated");
+ if (!gs_utils_is_current_desktop ("Unity")) {
+ g_notification_add_button_with_target (notification, C_("updates", "Review"), "app.set-mode",
"s", "updated");
+ g_notification_set_default_action_and_target (notification, "app.set-mode", "s", "updated");
+ }
g_application_send_notification (monitor->application, "offline-updates", notification);
/* update the timestamp so we don't show again */
diff --git a/src/gtk-style-hc.css b/src/gtk-style-hc.css
index dfc0b87..5869d5b 100644
--- a/src/gtk-style-hc.css
+++ b/src/gtk-style-hc.css
@@ -113,9 +113,15 @@
}
.install-progress {
- background-image: linear-gradient(to top, @theme_selected_bg_color 2px,
alpha(@theme_selected_bg_color, 0) 2px);
+ background-image: linear-gradient(to top, @theme_selected_bg_color 4px,
alpha(@theme_selected_bg_color, 0) 4px);
background-repeat: no-repeat;
background-position: 0 bottom;
+ border: 1px solid;
+ border-radius: 3px;
+ border-image-source: none;
+ border-image-width: 1;
+ border-image-slice: 100%;
+ border-image-repeat: stretch;
transition: none;
}
diff --git a/src/gtk-style.css b/src/gtk-style.css
index 42c3d1b..425c51c 100644
--- a/src/gtk-style.css
+++ b/src/gtk-style.css
@@ -144,9 +144,15 @@
}
.install-progress {
- background-image: linear-gradient(to top, @theme_selected_bg_color 2px,
alpha(@theme_selected_bg_color, 0) 2px);
+ background-image: linear-gradient(to top, @theme_selected_bg_color 4px,
alpha(@theme_selected_bg_color, 0) 4px);
background-repeat: no-repeat;
background-position: 0 bottom;
+ border: 1px solid;
+ border-radius: 3px;
+ border-image-source: none;
+ border-image-width: 1;
+ border-image-slice: 100%;
+ border-image-repeat: stretch;
transition: none;
}
diff --git a/src/plugins/Makefile.am b/src/plugins/Makefile.am
index bab4501..00dc96c 100644
--- a/src/plugins/Makefile.am
+++ b/src/plugins/Makefile.am
@@ -15,6 +15,8 @@ AM_CPPFLAGS = \
$(OSTREE_CFLAGS) \
$(FLATPAK_CFLAGS) \
$(RPM_CFLAGS) \
+ $(OAUTH_CFLAGS) \
+ $(LIBSECRET_CFLAGS) \
-DI_KNOW_THE_GNOME_SOFTWARE_API_IS_SUBJECT_TO_CHANGE \
-DBINDIR=\"$(bindir)\" \
-DDATADIR=\"$(datadir)\" \
@@ -42,7 +44,13 @@ plugin_LTLIBRARIES = \
libgs_plugin_provenance-license.la \
libgs_plugin_fedora-tagger-usage.la \
libgs_plugin_epiphany.la \
- libgs_plugin_icons.la
+ libgs_plugin_icons.la \
+ libgs_plugin_snappy.la
+
+if HAVE_APT
+plugin_LTLIBRARIES += \
+ libgs_plugin_apt.la
+endif
if HAVE_PACKAGEKIT
plugin_LTLIBRARIES += \
@@ -199,6 +207,27 @@ libgs_plugin_steam_la_LDFLAGS = -module -avoid-version
libgs_plugin_steam_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
endif
+if HAVE_APT
+ubuntu-unity-launcher-proxy.c ubuntu-unity-launcher-proxy.h: com.canonical.Unity.Launcher.xml Makefile
+ $(AM_V_GEN) gdbus-codegen --interface-prefix com.canonical.Unity.Launcher \
+ --generate-c-code ubuntu-unity-launcher-proxy \
+ --c-namespace UbuntuUnity \
+ --annotate 'com.canonical.Unity.Launcher' \
+ org.gtk.GDBus.C.Name \
+ Launcher \
+ $<
+
+CLEANFILES = ubuntu-unity-launcher-proxy.h ubuntu-unity-launcher-proxy.c
+
+libgs_plugin_apt_la_SOURCES = \
+ ubuntu-unity-launcher-proxy.c \
+ ubuntu-unity-launcher-proxy.h \
+ gs-plugin-apt.cc
+libgs_plugin_apt_la_LIBADD = $(GS_PLUGIN_LIBS) -lapt-pkg
+libgs_plugin_apt_la_LDFLAGS = -module -avoid-version
+libgs_plugin_apt_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
+endif
+
libgs_plugin_menu_spec_categories_la_SOURCES = \
gs-plugin-menu-spec-categories.c \
menu-spec-common.c \
@@ -232,8 +261,20 @@ libgs_plugin_hardcoded_featured_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
if HAVE_UBUNTU_REVIEWS
libgs_plugin_ubuntu_reviews_la_SOURCES = \
- gs-plugin-ubuntu-reviews.c
-libgs_plugin_ubuntu_reviews_la_LIBADD = $(GS_PLUGIN_LIBS) $(SOUP_LIBS) $(JSON_GLIB_LIBS) $(SQLITE_LIBS)
+ gs-plugin-ubuntu-reviews.c \
+ gs-ubuntuone.h \
+ gs-ubuntuone.c \
+ gs-ubuntuone-dialog.h \
+ gs-ubuntuone-dialog.c \
+ gs-ubuntu-snapd.h \
+ gs-ubuntu-snapd.c
+libgs_plugin_ubuntu_reviews_la_LIBADD = \
+ $(GS_PLUGIN_LIBS) \
+ $(SOUP_LIBS) \
+ $(JSON_GLIB_LIBS) \
+ $(OAUTH_LIBS) \
+ $(SQLITE_LIBS) \
+ $(LIBSECRET_LIBS)
libgs_plugin_ubuntu_reviews_la_LDFLAGS = -module -avoid-version
libgs_plugin_ubuntu_reviews_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
endif
@@ -315,6 +356,22 @@ libgs_plugin_packagekit_proxy_la_LIBADD = $(GS_PLUGIN_LIBS)
libgs_plugin_packagekit_proxy_la_LDFLAGS = -module -avoid-version
libgs_plugin_packagekit_proxy_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
+libgs_plugin_snappy_la_SOURCES = \
+ gs-plugin-snappy.c \
+ gs-ubuntuone.h \
+ gs-ubuntuone.c \
+ gs-ubuntuone-dialog.h \
+ gs-ubuntuone-dialog.c \
+ gs-ubuntu-snapd.h \
+ gs-ubuntu-snapd.c
+libgs_plugin_snappy_la_LIBADD = \
+ $(GS_PLUGIN_LIBS) \
+ $(SOUP_LIBS) \
+ $(JSON_GLIB_LIBS) \
+ $(LIBSECRET_LIBS)
+libgs_plugin_snappy_la_LDFLAGS = -module -avoid-version
+libgs_plugin_snappy_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
+
if ENABLE_TESTS
check_PROGRAMS = \
gs-self-test
@@ -332,4 +389,6 @@ gs_self_test_CFLAGS = $(WARN_CFLAGS)
TESTS = gs-self-test
endif
+EXTRA_DIST = gs-ubuntuone-dialog.h gs-ubuntuone-dialog.ui ubuntu-one.png com.canonical.Unity.Launcher.xml
+
-include $(top_srcdir)/git.mk
diff --git a/src/plugins/com.canonical.Unity.Launcher.xml b/src/plugins/com.canonical.Unity.Launcher.xml
new file mode 100644
index 0000000..b631a2d
--- /dev/null
+++ b/src/plugins/com.canonical.Unity.Launcher.xml
@@ -0,0 +1,15 @@
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
+"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+<node>
+ <interface name='com.canonical.Unity.Launcher'>
+ <method name='AddLauncherItem'>
+ <arg type='s' name='appstream_app_id' direction='in'/>
+ <arg type='s' name='aptdaemon_task' direction='in'/>
+ </method>
+
+ <method name='UpdateLauncherIconFavoriteState'>
+ <arg type='s' name='icon_uri' direction='in'/>
+ <arg type='b' name='is_sticky' direction='in'/>
+ </method>
+ </interface>
+</node>
diff --git a/src/plugins/gs-plugin-appstream.c b/src/plugins/gs-plugin-appstream.c
index 244f223..edb4a45 100644
--- a/src/plugins/gs-plugin-appstream.c
+++ b/src/plugins/gs-plugin-appstream.c
@@ -216,6 +216,21 @@ gs_plugin_setup (GsPlugin *plugin, GCancellable *cancellable, GError **error)
return TRUE;
}
+gboolean
+gs_plugin_refresh (GsPlugin *plugin,
+ guint cache_age,
+ GsPluginRefreshFlags flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ if (flags & GS_PLUGIN_REFRESH_FLAGS_UI) {
+ gs_plugin_setup (plugin, cancellable, error);
+ gs_plugin_updates_changed (plugin);
+ }
+
+ return TRUE;
+}
+
static gboolean
gs_plugin_refine_from_id (GsPlugin *plugin,
GsApp *app,
diff --git a/src/plugins/gs-plugin-apt.cc b/src/plugins/gs-plugin-apt.cc
new file mode 100644
index 0000000..a5eeace
--- /dev/null
+++ b/src/plugins/gs-plugin-apt.cc
@@ -0,0 +1,1154 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <config.h>
+
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+
+#include <apt-pkg/init.h>
+#include <apt-pkg/cachefile.h>
+#include <apt-pkg/cmndline.h>
+#include <apt-pkg/version.h>
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string>
+#include <set>
+#include <vector>
+#include <fstream>
+#include <iomanip>
+#include <algorithm>
+#include <stdexcept>
+
+#include <gs-plugin.h>
+#include <gs-utils.h>
+
+#define LICENSE_URL "http://www.ubuntu.com/about/about-ubuntu/licensing"
+
+#define INFO_DIR "/var/lib/dpkg/info"
+
+typedef struct
+{
+ GMutex pending_mutex;
+ GCond pending_cond;
+
+ GMutex dispatched_mutex;
+ GCond dispatched_cond;
+
+ GMutex hashtable_mutex;
+
+ guint still_to_read;
+ guint dispatched_reads;
+
+ GsPlugin *plugin;
+} ReadListData;
+
+typedef struct {
+ gchar *name;
+ gchar *section;
+ gchar *installed_version;
+ gchar *update_version;
+ gchar *origin;
+ gchar *release;
+ gchar *component;
+ gint installed_size;
+} PackageInfo;
+
+#include "ubuntu-unity-launcher-proxy.h"
+
+struct GsPluginData {
+ GMutex mutex;
+ gboolean loaded;
+ GHashTable *package_info;
+ GHashTable *installed_files;
+ GList *installed_packages;
+ GList *updatable_packages;
+};
+
+const gchar *
+gs_plugin_get_name (void)
+{
+ return "apt";
+}
+
+const gchar **
+gs_plugin_order_after (GsPlugin *plugin)
+{
+ static const gchar *deps[] = {
+ "appstream", /* need pkgname */
+ NULL };
+ return deps;
+}
+
+/**
+ * gs_plugin_get_conflicts:
+ */
+const gchar **
+gs_plugin_get_conflicts (GsPlugin *plugin)
+{
+
+ static const gchar *deps[] = {
+ "packagekit",
+ "packagekit-history",
+ "packagekit-offline",
+ "packagekit-origin",
+ "packagekit-proxy",
+ "packagekit-refine",
+ "packagekit-refresh",
+ "systemd-updates",
+ NULL };
+ return deps;
+}
+
+static void
+free_package_info (gpointer data)
+{
+ PackageInfo *info = (PackageInfo *) data;
+ g_free (info->section);
+ g_free (info->installed_version);
+ g_free (info->update_version);
+ g_free (info->origin);
+ g_free (info->release);
+ g_free (info->component);
+ g_free (info->name);
+ g_free (info);
+}
+
+void
+gs_plugin_initialize (GsPlugin *plugin)
+{
+ GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData));
+
+ priv->package_info = g_hash_table_new_full (g_str_hash,
+ g_str_equal,
+ NULL,
+ free_package_info);
+
+ priv->installed_files = g_hash_table_new_full (g_str_hash,
+ g_str_equal,
+ g_free,
+ g_free);
+
+ g_mutex_init (&priv->mutex);
+
+ pkgInitConfig (*_config);
+ pkgInitSystem (*_config, _system);
+}
+
+void
+gs_plugin_destroy (GsPlugin *plugin)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+
+ g_mutex_lock (&priv->mutex);
+ priv->loaded = FALSE;
+ g_clear_pointer (&priv->package_info, g_hash_table_unref);
+ g_clear_pointer (&priv->installed_files, g_hash_table_unref);
+ g_clear_pointer (&priv->installed_packages, g_list_free);
+ g_clear_pointer (&priv->updatable_packages, g_list_free);
+ g_mutex_unlock (&priv->mutex);
+ g_mutex_clear (&priv->mutex);
+}
+
+
+static void
+read_list_file_cb (GObject *object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ g_autoptr(GFileInputStream) stream = NULL;
+ g_autoptr(GFile) file = NULL;
+ ReadListData *data;
+ g_autofree gchar *buffer = NULL;
+ g_autofree gchar *filename = NULL;
+ g_autoptr(GFileInfo) info = NULL;
+ g_auto(GStrv) file_lines = NULL;
+ g_auto(GStrv) file_components = NULL;
+ gchar *line;
+
+ file = G_FILE (object);
+ data = (ReadListData *) user_data;
+ stream = g_file_read_finish (file, res, NULL);
+
+ info = g_file_input_stream_query_info (stream,
+ G_FILE_ATTRIBUTE_STANDARD_SIZE,
+ NULL,
+ NULL);
+
+ if (!info)
+ return;
+
+ if (!g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_STANDARD_SIZE))
+ return;
+
+ buffer = (gchar *) g_malloc0 (g_file_info_get_size (info) + 1);
+
+ if (!g_input_stream_read_all (G_INPUT_STREAM (stream),
+ buffer,
+ g_file_info_get_size (info),
+ NULL,
+ NULL,
+ NULL))
+ return;
+
+ g_input_stream_close (G_INPUT_STREAM (stream), NULL, NULL);
+
+ file_lines = g_strsplit (buffer, "\n", -1);
+
+ filename = g_file_get_basename (file);
+ file_components = g_strsplit (filename, ".", 2);
+
+ for (int i = 0; file_lines[i]; ++i)
+ if (g_str_has_suffix (file_lines[i], ".desktop") ||
+ g_str_has_suffix (file_lines[i], ".metainfo.xml") ||
+ g_str_has_suffix (file_lines[i], ".appdata.xml"))
+ {
+ g_mutex_lock (&data->hashtable_mutex);
+ /* filename -> package */
+ g_hash_table_insert (gs_plugin_get_data (data->plugin)->installed_files,
+ g_strdup (file_lines[i]),
+ g_strdup (file_components[0]));
+ g_mutex_unlock (&data->hashtable_mutex);
+ }
+
+ g_mutex_lock (&data->dispatched_mutex);
+ (data->dispatched_reads)--;
+ g_cond_signal(&data->dispatched_cond);
+ g_mutex_unlock(&data->dispatched_mutex);
+
+ g_mutex_lock (&data->pending_mutex);
+ (data->still_to_read)--;
+ g_cond_signal (&data->pending_cond);
+ g_mutex_unlock (&data->pending_mutex);
+}
+
+static void
+read_list_file (GList *files,
+ ReadListData *data)
+{
+ GFile *gfile;
+ GList *files_iter;
+
+ for (files_iter = files; files_iter; files_iter = files_iter->next)
+ {
+ /* freed in read_list_file_cb */
+ gfile = g_file_new_for_path ((gchar *) files_iter->data);
+
+ g_mutex_lock (&data->dispatched_mutex);
+ g_file_read_async (gfile,
+ G_PRIORITY_DEFAULT,
+ NULL,
+ read_list_file_cb,
+ data);
+
+ (data->dispatched_reads)++;
+
+ while (data->dispatched_reads >= 500)
+ g_cond_wait (&data->dispatched_cond, &data->dispatched_mutex);
+
+ g_mutex_unlock (&data->dispatched_mutex);
+ }
+}
+
+static void
+look_for_files (GsPlugin *plugin)
+{
+
+ ReadListData data;
+ GList *files = NULL;
+
+ data.still_to_read = 0;
+ data.dispatched_reads = 0;
+ g_cond_init (&data.pending_cond);
+ g_mutex_init (&data.pending_mutex);
+ g_cond_init (&data.dispatched_cond);
+ g_mutex_init (&data.dispatched_mutex);
+ g_mutex_init (&data.hashtable_mutex);
+ data.plugin = plugin;
+
+ g_autoptr (GDir) dir = NULL;
+ const gchar *file;
+
+ dir = g_dir_open (INFO_DIR, 0, NULL);
+
+ while (file = g_dir_read_name (dir))
+ if (g_str_has_suffix (file, ".list") &&
+ /* app-install-data contains loads of .desktop files, but they aren't installed by it */
+ (!g_strcmp0 (file, "app-install-data.list") == 0))
+ {
+ files = g_list_append (files, g_build_filename (INFO_DIR, file, NULL));
+ data.still_to_read++;
+ }
+
+ read_list_file (files, &data);
+
+ /* Wait until all the reads are done */
+ g_mutex_lock (&data.pending_mutex);
+ while (data.still_to_read > 0)
+ g_cond_wait (&data.pending_cond, &data.pending_mutex);
+ g_mutex_unlock (&data.pending_mutex);
+
+ g_mutex_clear (&data.pending_mutex);
+ g_cond_clear (&data.pending_cond);
+ g_mutex_clear (&data.dispatched_mutex);
+ g_cond_clear (&data.dispatched_cond);
+ g_mutex_clear (&data.hashtable_mutex);
+
+ g_list_free_full (files, g_free);
+}
+
+static gboolean
+version_newer (const gchar *v0, const gchar *v1)
+{
+ return v0 ? _system->VS->CmpVersion(v0, v1) < 0 : TRUE;
+}
+
+/* return FALSE for a fatal error */
+static gboolean
+look_at_pkg (const pkgCache::PkgIterator &P,
+ pkgSourceList *list,
+ pkgPolicy *policy,
+ GsPlugin *plugin,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ pkgCache::VerIterator current = P.CurrentVer();
+ pkgCache::VerIterator candidate = policy->GetCandidateVer(P);
+ pkgCache::VerFileIterator VF;
+ FileFd PkgF;
+ pkgTagSection Tags;
+ gchar *name;
+
+ PackageInfo *info;
+
+ if (!candidate || !candidate.FileList ())
+ return TRUE;
+
+ name = g_strdup (P.Name ());
+ info = (PackageInfo *) g_hash_table_lookup (priv->package_info, name);
+ if (info == NULL) {
+ info = g_new0 (PackageInfo, 1);
+ info->name = name;
+ g_hash_table_insert (priv->package_info, name, info);
+ } else
+ g_free (name);
+
+ for (VF = candidate.FileList (); VF.IsGood (); VF++) {
+ // see InRelease for the fields
+ if (VF.File ().Archive ())
+ info->release = g_strdup (VF.File ().Archive ());
+ if (VF.File ().Origin ())
+ info->origin = g_strdup (VF.File ().Origin ());
+ if (VF.File ().Component ())
+ info->component = g_strdup (VF.File ().Component ());
+ // also available: Codename, Label
+ break;
+ }
+
+
+ pkgCache::PkgFileIterator I = VF.File ();
+
+ if (I.IsOk () == false) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ ("apt DB load failed: package file %s is out of sync."), I.FileName ());
+ return FALSE;
+ }
+
+ PkgF.Open (I.FileName (), FileFd::ReadOnly, FileFd::Extension);
+
+ pkgTagFile TagF (&PkgF);
+
+ if (TagF.Jump (Tags, current.FileList ()->Offset) == false) {
+ if (TagF.Jump (Tags, candidate.FileList ()->Offset) == false)
+ return TRUE;
+ }
+
+ if (Tags.FindI ("Installed-Size") > 0)
+ info->installed_size = Tags.FindI ("Installed-Size")*1024;
+ else
+ info->installed_size = 0;
+
+ if (current)
+ info->installed_version = g_strdup (current.VerStr ());
+ if (candidate)
+ info->update_version = g_strdup (candidate.VerStr ());
+
+ info->section = g_strdup (candidate.Section ());
+ if (info->installed_version) {
+ priv->installed_packages = g_list_append (priv->installed_packages, info);
+ }
+
+ /* no upgrade */
+ if (g_strcmp0 (info->installed_version, info->update_version) == 0)
+ g_clear_pointer (&info->update_version, g_free);
+
+ if (info->installed_version && info->update_version)
+ priv->updatable_packages = g_list_append (priv->updatable_packages, info);
+
+ return TRUE;
+}
+
+static gboolean
+load_apt_db (GsPlugin *plugin, GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ pkgSourceList *list;
+ pkgPolicy *policy;
+ pkgCacheFile cachefile;
+ pkgCache *cache;
+ pkgCache::PkgIterator P;
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
+
+ if (priv->loaded)
+ return TRUE;
+
+ _error->Discard();
+ cache = cachefile.GetPkgCache();
+ list = cachefile.GetSourceList();
+ policy = cachefile.GetPolicy();
+ if (cache == NULL || _error->PendingError()) {
+ _error->DumpErrors();
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "apt DB load failed: error while initialising");
+ return FALSE;
+ }
+
+ for (pkgCache::GrpIterator grp = cache->GrpBegin(); grp != cache->GrpEnd(); grp++) {
+ P = grp.FindPreferredPkg();
+ if (P.end())
+ continue;
+ if (!look_at_pkg (P, list, policy, plugin, error))
+ return FALSE;
+ }
+
+ /* load filename -> package map into priv->installed_files */
+ look_for_files (plugin);
+
+ priv->loaded = TRUE;
+ return TRUE;
+}
+
+static void
+unload_apt_db (GsPlugin *plugin)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
+
+ priv->loaded = FALSE;
+ g_hash_table_remove_all (priv->package_info);
+ g_hash_table_remove_all (priv->installed_files);
+ g_clear_pointer (&priv->installed_packages, g_list_free);
+ g_clear_pointer (&priv->updatable_packages, g_list_free);
+}
+
+static void
+get_changelog (GsPlugin *plugin, GsApp *app)
+{
+ guint i;
+ guint status_code;
+ g_autofree gchar *binary_source = NULL;
+ g_autofree gchar *changelog_prefix = NULL;
+ g_autofree gchar *current_version = NULL;
+ g_autofree gchar *source_prefix = NULL;
+ g_autofree gchar *update_version = NULL;
+ g_autofree gchar *uri = NULL;
+ g_auto(GStrv) lines = NULL;
+ g_autoptr(GString) details = NULL;
+ g_autoptr(SoupMessage) msg = NULL;
+
+ // Need to know the source and version to download changelog
+ binary_source = g_strdup (gs_app_get_source_default (app));
+ current_version = g_strdup (gs_app_get_version (app));
+ update_version = g_strdup (gs_app_get_update_version (app));
+ if (binary_source == NULL || update_version == NULL)
+ return;
+
+ if (g_str_has_prefix (binary_source, "lib"))
+ source_prefix = g_strdup_printf ("lib%c", binary_source[3]);
+ else
+ source_prefix = g_strdup_printf ("%c", binary_source[0]);
+ uri = g_strdup_printf ("http://changelogs.ubuntu.com/changelogs/binary/%s/%s/%s/changelog",
source_prefix, binary_source, update_version);
+
+ /* download file */
+ msg = soup_message_new (SOUP_METHOD_GET, uri);
+ status_code = soup_session_send_message (gs_plugin_get_soup_session (plugin), msg);
+ if (status_code != SOUP_STATUS_OK) {
+ g_warning ("Failed to get changelog for %s version %s from changelogs.ubuntu.com: %s",
binary_source, update_version, soup_status_get_phrase (status_code));
+ return;
+ }
+
+ // Extract changelog entries newer than our current version
+ lines = g_strsplit (msg->response_body->data, "\n", -1);
+ details = g_string_new ("");
+ for (i = 0; lines[i] != NULL; i++) {
+ gchar *line = lines[i];
+ const gchar *version_start, *version_end;
+ g_autofree gchar *v = NULL;
+
+ // First line is in the form "package (version) distribution(s); urgency=urgency"
+ version_start = strchr (line, '(');
+ version_end = strchr (line, ')');
+ if (line[0] == ' ' || version_start == NULL || version_end == NULL || version_end <
version_start)
+ continue;
+ v = g_strdup_printf ("%.*s", (int) (version_end - version_start - 1), version_start + 1);
+
+ // We're only interested in new versions
+ if (!version_newer (current_version, v))
+ break;
+
+ g_string_append_printf (details, "%s\n", v);
+ for (i++; lines[i] != NULL; i++) {
+ // Last line is in the form " -- maintainer name <email address> date"
+ if (g_str_has_prefix (lines[i], " -- "))
+ break;
+ g_string_append_printf (details, "%s\n", lines[i]);
+ }
+ }
+
+ gs_app_set_update_details (app, details->str);
+}
+
+static gboolean
+is_official (PackageInfo *info)
+{
+ return g_strcmp0 (info->origin, "Ubuntu") == 0;
+}
+
+static gboolean
+is_open_source (PackageInfo *info)
+{
+ const gchar *open_source_components[] = { "main", "universe", NULL };
+
+ /* There's no valid apps in the libs section */
+ return info->component != NULL && g_strv_contains (open_source_components, info->component);
+}
+
+static gchar *
+get_origin (PackageInfo *info)
+{
+ if (!info->origin)
+ return NULL;
+
+ g_autofree gchar *origin_lower = g_strdup (info->origin);
+ for (int i = 0; origin_lower[i]; ++i)
+ origin_lower[i] = g_ascii_tolower (origin_lower[i]);
+
+ return g_strdup_printf ("%s-%s-%s", origin_lower, info->release, info->component);
+}
+
+gboolean
+gs_plugin_refine (GsPlugin *plugin,
+ GList **list,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ GList *link;
+ GsApp *app;
+ PackageInfo *info;
+ const gchar *tmp;
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ if (!load_apt_db (plugin, error))
+ return FALSE;
+
+ locker = g_mutex_locker_new (&priv->mutex);
+
+ for (link = *list; link; link = link->next) {
+ app = (GsApp *) link->data;
+ g_autofree gchar *fn = NULL;
+ g_autofree gchar *origin = NULL;
+ gchar *package = NULL;
+
+ tmp = gs_app_get_id (app);
+ if (gs_app_get_source_id_default (app) == NULL && tmp) {
+ switch (gs_app_get_kind (app)) {
+ case AS_APP_KIND_DESKTOP:
+ fn = g_strdup_printf ("/usr/share/applications/%s", tmp);
+ break;
+ case AS_APP_KIND_ADDON:
+ fn = g_strdup_printf ("/usr/share/appdata/%s.metainfo.xml", tmp);
+ break;
+ default:
+ break;
+ }
+
+ if (!g_file_test (fn, G_FILE_TEST_EXISTS)) {
+ g_debug ("ignoring %s as does not exist", fn);
+ } else {
+ package = (gchar *) g_hash_table_lookup (priv->installed_files,
+ fn);
+ if (package != NULL) {
+ gs_app_add_source (app, package);
+ gs_app_set_management_plugin (app, "apt");
+ }
+ }
+ }
+
+ if (gs_app_get_source_default (app) == NULL)
+ continue;
+
+ info = (PackageInfo *) g_hash_table_lookup (priv->package_info, gs_app_get_source_default
(app));
+ if (info == NULL)
+ continue;
+
+ origin = get_origin (info);
+ gs_app_set_origin (app, origin);
+ gs_app_set_origin_ui (app, info->origin);
+
+ if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN) {
+ if (info->installed_version != NULL) {
+ if (info->update_version != NULL) {
+ gs_app_set_state (app, AS_APP_STATE_UPDATABLE_LIVE);
+ } else {
+ gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+ }
+ } else {
+ gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+ }
+ }
+ if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN) != 0) {
+ g_autofree gchar *origin = get_origin (info);
+ gs_app_set_origin (app, origin);
+ gs_app_set_origin_ui (app, info->origin);
+ }
+ if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE) != 0 && gs_app_get_size_installed (app) ==
0) {
+ gs_app_set_size_installed (app, info->installed_size);
+ }
+ if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION) != 0) {
+ if (info->installed_version != NULL) {
+ gs_app_set_version (app, info->installed_version);
+ } else {
+ gs_app_set_version (app, info->update_version);
+ }
+ if (info->update_version != NULL) {
+ gs_app_set_update_version (app, info->update_version);
+ }
+ }
+ if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE) != 0 && is_open_source(info)) {
+ gs_app_set_license (app, GS_APP_QUALITY_LOWEST, "@LicenseRef-free=" LICENSE_URL);
+ }
+
+ if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS) != 0) {
+ get_changelog (plugin, app);
+ }
+ }
+
+ return TRUE;
+}
+
+static gboolean
+is_allowed_section (PackageInfo *info)
+{
+ const gchar *section_blacklist[] = { "libs", NULL };
+
+ /* There's no valid apps in the libs section */
+ return info->section == NULL || !g_strv_contains (section_blacklist, info->section);
+}
+
+gboolean
+gs_plugin_add_installed (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ GList *link;
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ if (!load_apt_db (plugin, error))
+ return FALSE;
+
+ locker = g_mutex_locker_new (&priv->mutex);
+
+ for (link = priv->installed_packages; link; link = link->next) {
+ PackageInfo *info = (PackageInfo *) link->data;
+ g_autofree gchar *origin = get_origin (info);
+ g_autoptr(GsApp) app = NULL;
+
+ if (!is_allowed_section (info))
+ continue;
+
+ app = gs_app_new (NULL);
+ gs_app_set_management_plugin (app, "apt");
+ gs_app_set_name (app, GS_APP_QUALITY_LOWEST, info->name);
+ gs_app_add_source (app, info->name);
+ gs_app_set_origin (app, origin);
+ gs_app_set_origin_ui (app, info->origin);
+ gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+ gs_app_list_add (list, app);
+ }
+
+ return TRUE;
+}
+
+typedef struct {
+ GsPlugin *plugin;
+ GMainLoop *loop;
+ GsApp *app;
+ GList *apps;
+ gchar **result;
+} TransactionData;
+
+static void
+transaction_property_changed_cb (GDBusConnection *connection,
+ const gchar *sender_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *signal_name,
+ GVariant *parameters,
+ gpointer user_data)
+{
+ TransactionData *data = (TransactionData *) user_data;
+ const gchar *name;
+ GList *i;
+ g_autoptr(GVariant) value = NULL;
+
+ if (g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(sv)"))) {
+ g_variant_get (parameters, "(&sv)", &name, &value);
+ if (g_strcmp0 (name, "Progress") == 0) {
+ if (data->app)
+ gs_app_set_progress (data->app, g_variant_get_int32 (value));
+ for (i = data->apps; i != NULL; i = i->next)
+ gs_app_set_progress (GS_APP (i->data), g_variant_get_int32 (value));
+ }
+ } else {
+ g_warning ("Unknown parameters in %s.%s: %s", interface_name, signal_name,
g_variant_get_type_string (parameters));
+ }
+}
+
+static void
+transaction_finished_cb (GDBusConnection *connection,
+ const gchar *sender_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *signal_name,
+ GVariant *parameters,
+ gpointer user_data)
+{
+ TransactionData *data = (TransactionData *) user_data;
+
+ if (g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(s)")))
+ g_variant_get (parameters, "(s)", data->result);
+ else
+ g_warning ("Unknown parameters in %s.%s: %s", interface_name, signal_name,
g_variant_get_type_string (parameters));
+
+ g_main_loop_quit (data->loop);
+}
+
+static void
+notify_unity_launcher (GsApp *app, const gchar *transaction_path)
+{
+ UbuntuUnityLauncher *launcher = NULL;
+
+ g_return_if_fail (GS_IS_APP (app));
+ g_return_if_fail (transaction_path);
+
+ launcher = ubuntu_unity_launcher_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION,
+ G_DBUS_PROXY_FLAGS_NONE,
+ "com.canonical.Unity.Launcher",
+ "/com/canonical/Unity/Launcher",
+ NULL, NULL);
+
+ g_return_if_fail (launcher);
+
+ ubuntu_unity_launcher_call_add_launcher_item (launcher,
+ gs_app_get_id (app),
+ transaction_path,
+ NULL, NULL, NULL);
+
+ g_object_unref (launcher);
+}
+
+static gboolean
+aptd_transaction (GsPlugin *plugin,
+ const gchar *method,
+ GsApp *app,
+ GList *apps,
+ GVariant *parameters,
+ GError **error)
+{
+ g_autoptr(GDBusConnection) conn = NULL;
+ g_autoptr(GVariant) result = NULL;
+ g_autofree gchar *transaction_path = NULL, *transaction_result = NULL;
+ g_autoptr(GMainLoop) loop = NULL;
+ guint property_signal, finished_signal;
+ TransactionData data;
+
+ conn = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, error);
+ if (conn == NULL)
+ return FALSE;
+
+ if (parameters == NULL && app != NULL)
+ parameters = g_variant_new_parsed ("([%s],)", gs_app_get_source_default (app));
+
+ result = g_dbus_connection_call_sync (conn,
+ "org.debian.apt",
+ "/org/debian/apt",
+ "org.debian.apt",
+ method,
+ parameters,
+ G_VARIANT_TYPE ("(s)"),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ error);
+ if (result == NULL)
+ return FALSE;
+ g_variant_get (result, "(s)", &transaction_path);
+ g_variant_unref (result);
+
+ if (!g_strcmp0(method, "InstallPackages"))
+ notify_unity_launcher (app, transaction_path);
+
+ loop = g_main_loop_new (NULL, FALSE);
+
+ data.plugin = plugin;
+ data.app = app;
+ data.apps = apps;
+ data.loop = loop;
+ data.result = &transaction_result;
+ property_signal = g_dbus_connection_signal_subscribe (conn,
+ "org.debian.apt",
+ "org.debian.apt.transaction",
+ "PropertyChanged",
+ transaction_path,
+ NULL,
+ G_DBUS_SIGNAL_FLAGS_NONE,
+ transaction_property_changed_cb,
+ &data,
+ NULL);
+ finished_signal = g_dbus_connection_signal_subscribe (conn,
+ "org.debian.apt",
+ "org.debian.apt.transaction",
+ "Finished",
+ transaction_path,
+ NULL,
+ G_DBUS_SIGNAL_FLAGS_NONE,
+ transaction_finished_cb,
+ &data,
+ NULL);
+ result = g_dbus_connection_call_sync (conn,
+ "org.debian.apt",
+ transaction_path,
+ "org.debian.apt.transaction",
+ "Run",
+ g_variant_new ("()"),
+ G_VARIANT_TYPE ("()"),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ error);
+ if (result != NULL)
+ g_main_loop_run (loop);
+ g_dbus_connection_signal_unsubscribe (conn, property_signal);
+ g_dbus_connection_signal_unsubscribe (conn, finished_signal);
+ if (result == NULL)
+ return FALSE;
+
+ if (g_strcmp0 (transaction_result, "exit-success") != 0) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "apt transaction returned result %s", transaction_result);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+app_is_ours (GsApp *app)
+{
+ const gchar *management_plugin = gs_app_get_management_plugin (app);
+
+ // FIXME: Since appstream marks all packages as owned by PackageKit and
+ // we are replacing PackageKit we need to accept those packages
+ const gchar *our_management_plugins[] = { "PackageKit", "apt", NULL };
+
+ return g_strv_contains (our_management_plugins, management_plugin);
+}
+
+gboolean
+gs_plugin_app_install (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autofree gchar *filename = NULL;
+ gboolean success = FALSE;
+
+ if (!app_is_ours (app))
+ return TRUE;
+
+ if (gs_app_get_source_default (app) == NULL)
+ return TRUE;
+
+ switch (gs_app_get_state (app)) {
+ case AS_APP_STATE_AVAILABLE:
+ case AS_APP_STATE_UPDATABLE:
+ gs_app_set_state (app, AS_APP_STATE_INSTALLING);
+ success = aptd_transaction (plugin, "InstallPackages", app, NULL, NULL, error);
+ break;
+ case AS_APP_STATE_AVAILABLE_LOCAL:
+ filename = g_file_get_path (gs_app_get_local_file (app));
+ gs_app_set_state (app, AS_APP_STATE_INSTALLING);
+ success = aptd_transaction (plugin, "InstallFile", app, NULL,
+ g_variant_new_parsed ("(%s, true)", filename),
+ error);
+ break;
+ default:
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "do not know how to install app in state %s",
+ as_app_state_to_string (gs_app_get_state (app)));
+ return FALSE;
+ }
+
+
+ if (success)
+ gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+ else
+ gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+
+ return success;
+}
+
+gboolean
+gs_plugin_app_remove (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ if (!app_is_ours (app))
+ return TRUE;
+
+ if (gs_app_get_source_default (app) == NULL)
+ return TRUE;
+
+ gs_app_set_state (app, AS_APP_STATE_REMOVING);
+ if (aptd_transaction (plugin, "RemovePackages", app, NULL, NULL, error))
+ gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+ else {
+ gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+gs_plugin_refresh (GsPlugin *plugin,
+ guint cache_age,
+ GsPluginRefreshFlags flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ if ((flags & (GS_PLUGIN_REFRESH_FLAGS_METADATA | GS_PLUGIN_REFRESH_FLAGS_PAYLOAD)) == 0)
+ return TRUE;
+
+ if (!aptd_transaction (plugin, "UpdateCache", NULL, NULL, NULL, error))
+ return FALSE;
+
+ unload_apt_db (plugin);
+
+ gs_plugin_updates_changed (plugin);
+
+ return TRUE;
+}
+
+gboolean
+gs_plugin_add_updates (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ GList *link;
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ if (!load_apt_db (plugin, error))
+ return FALSE;
+
+ locker = g_mutex_locker_new (&priv->mutex);
+
+ for (link = priv->updatable_packages; link; link = link->next) {
+ PackageInfo *info = (PackageInfo *) link->data;
+ g_autoptr(GsApp) app = NULL;
+
+ if (!is_allowed_section (info))
+ continue;
+
+ app = gs_app_new (NULL);
+ gs_app_set_management_plugin (app, "apt");
+ gs_app_set_name (app, GS_APP_QUALITY_LOWEST, info->name);
+ gs_app_set_kind (app, AS_APP_KIND_GENERIC);
+ gs_app_add_source (app, info->name);
+ gs_app_list_add (list, app);
+ }
+
+ return TRUE;
+}
+
+static void
+set_list_state (GList *apps,
+ AsAppState state)
+{
+ GList *i;
+ guint j;
+ GsApp *app_i;
+ GsApp *app_j;
+ GPtrArray *related;
+
+ for (i = apps; i != NULL; i = i->next) {
+ app_i = GS_APP (i->data);
+ gs_app_set_state (app_i, state);
+
+ if (g_strcmp0 (gs_app_get_id (app_i), "os-update.virtual") == 0) {
+ related = gs_app_get_related (app_i);
+
+ for (j = 0; j < related->len; j++) {
+ app_j = GS_APP (g_ptr_array_index (related, j));
+ gs_app_set_state (app_j, state);
+ }
+ }
+ }
+}
+
+gboolean
+gs_plugin_update (GsPlugin *plugin,
+ GList *apps,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GList *i;
+ GsApp *app_i;
+
+ for (i = apps; i != NULL; i = i->next) {
+ app_i = GS_APP (i->data);
+
+ if (g_strcmp0 (gs_app_get_id (app_i), "os-update.virtual") == 0) {
+ set_list_state (apps, AS_APP_STATE_INSTALLING);
+
+ if (aptd_transaction (plugin, "UpgradeSystem", NULL, apps, g_variant_new_parsed
("(false,)"), error)) {
+ set_list_state (apps, AS_APP_STATE_INSTALLED);
+
+ unload_apt_db (plugin);
+
+ gs_plugin_updates_changed (plugin);
+
+ return TRUE;
+ } else {
+ set_list_state (apps, AS_APP_STATE_UPDATABLE_LIVE);
+
+ return FALSE;
+ }
+ }
+ }
+
+ return TRUE;
+}
+
+gboolean
+gs_plugin_update_app (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GPtrArray *apps;
+ GsApp *app_i;
+ guint i;
+ GVariantBuilder builder;
+
+ if (g_strcmp0 (gs_app_get_id (app), "os-update.virtual") == 0) {
+ apps = gs_app_get_related (app);
+
+ g_variant_builder_init (&builder, G_VARIANT_TYPE ("(as)"));
+ g_variant_builder_open (&builder, G_VARIANT_TYPE ("as"));
+
+ gs_app_set_state (app, AS_APP_STATE_INSTALLING);
+
+ for (i = 0; i < apps->len; i++) {
+ app_i = GS_APP (g_ptr_array_index (apps, i));
+ gs_app_set_state (app_i, AS_APP_STATE_INSTALLING);
+ g_variant_builder_add (&builder, "s", gs_app_get_source_default (app_i));
+ }
+
+ g_variant_builder_close (&builder);
+
+ if (aptd_transaction (plugin, "UpgradePackages", app, NULL, g_variant_builder_end (&builder),
error)) {
+ gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+
+ for (i = 0; i < apps->len; i++)
+ gs_app_set_state (GS_APP (g_ptr_array_index (apps, i)),
AS_APP_STATE_INSTALLED);
+
+ unload_apt_db (plugin);
+
+ gs_plugin_updates_changed (plugin);
+ } else {
+ gs_app_set_state (app, AS_APP_STATE_UPDATABLE_LIVE);
+
+ for (i = 0; i < apps->len; i++)
+ gs_app_set_state (GS_APP (g_ptr_array_index (apps, i)),
AS_APP_STATE_UPDATABLE_LIVE);
+
+ return FALSE;
+ }
+ } else if (app_is_ours (app)) {
+ gs_app_set_state (app, AS_APP_STATE_INSTALLING);
+
+ if (aptd_transaction (plugin, "UpgradePackages", app, NULL, NULL, error)) {
+ gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+
+ unload_apt_db (plugin);
+
+ gs_plugin_updates_changed (plugin);
+ } else {
+ gs_app_set_state (app, AS_APP_STATE_UPDATABLE_LIVE);
+
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+gboolean
+gs_plugin_launch (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ if (!app_is_ours (app))
+ return TRUE;
+
+ return gs_plugin_app_launch (plugin, app, error);
+}
+
+/* vim: set noexpandtab ts=8 sw=8: */
diff --git a/src/plugins/gs-plugin-packagekit-refine.c b/src/plugins/gs-plugin-packagekit-refine.c
index d96ecd8..6c02bd6 100644
--- a/src/plugins/gs-plugin-packagekit-refine.c
+++ b/src/plugins/gs-plugin-packagekit-refine.c
@@ -341,21 +341,6 @@ gs_plugin_packagekit_refine_from_desktop (GsPlugin *plugin,
static gchar *
gs_plugin_packagekit_fixup_update_description (const gchar *text)
{
- gchar *tmp;
- g_autoptr(GsMarkdown) markdown = NULL;
-
- /* nothing to do */
- if (text == NULL)
- return NULL;
-
- /* try to parse */
- markdown = gs_markdown_new (GS_MARKDOWN_OUTPUT_TEXT);
- gs_markdown_set_smart_quoting (markdown, FALSE);
- gs_markdown_set_autocode (markdown, FALSE);
- gs_markdown_set_autolinkify (markdown, FALSE);
- tmp = gs_markdown_parse (markdown, text);
- if (tmp != NULL)
- return tmp;
return g_strdup (text);
}
diff --git a/src/plugins/gs-plugin-packagekit-refresh.c b/src/plugins/gs-plugin-packagekit-refresh.c
index 114e21d..6cf77d7 100644
--- a/src/plugins/gs-plugin-packagekit-refresh.c
+++ b/src/plugins/gs-plugin-packagekit-refresh.c
@@ -45,9 +45,6 @@ gs_plugin_initialize (GsPlugin *plugin)
pk_task_set_only_download (priv->task, TRUE);
pk_client_set_background (PK_CLIENT (priv->task), TRUE);
pk_client_set_interactive (PK_CLIENT (priv->task), FALSE);
-
- /* we can return better results than dpkg directly */
- gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "dpkg");
}
void
diff --git a/src/plugins/gs-plugin-provenance.c b/src/plugins/gs-plugin-provenance.c
index e72779a..6cd7478 100644
--- a/src/plugins/gs-plugin-provenance.c
+++ b/src/plugins/gs-plugin-provenance.c
@@ -69,6 +69,7 @@ gs_plugin_initialize (GsPlugin *plugin)
priv->sources = gs_plugin_provenance_get_sources (plugin);
/* after the package source is set */
+ gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "apt");
gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "dummy");
gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "packagekit-refine");
}
diff --git a/src/plugins/gs-plugin-snappy.c b/src/plugins/gs-plugin-snappy.c
new file mode 100644
index 0000000..e029c8c
--- /dev/null
+++ b/src/plugins/gs-plugin-snappy.c
@@ -0,0 +1,476 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2015 Canonical Ltd
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <gs-plugin.h>
+#include <glib/gi18n.h>
+#include <json-glib/json-glib.h>
+#include <gnome-software.h>
+#include "gs-ubuntu-snapd.h"
+#include "gs-ubuntuone.h"
+
+// snapd API documentation is at https://github.com/ubuntu-core/snappy/blob/master/docs/rest.md
+
+#define SNAPD_SOCKET "/run/snapd.socket"
+
+typedef gboolean (*AppFilterFunc)(const gchar *id, JsonObject *object, gpointer data);
+
+void
+gs_plugin_initialize (GsPlugin *plugin)
+{
+ if (!g_file_test (SNAPD_SOCKET, G_FILE_TEST_EXISTS)) {
+ g_debug ("disabling '%s' as no %s available",
+ gs_plugin_get_name (plugin), SNAPD_SOCKET);
+ gs_plugin_set_enabled (plugin, FALSE);
+ }
+}
+
+static JsonParser *
+parse_result (const gchar *response, const gchar *response_type, GError **error)
+{
+ g_autoptr(JsonParser) parser = NULL;
+ g_autoptr(GError) sub_error = NULL;
+
+ if (response_type == NULL) {
+ g_set_error_literal (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd returned no content type");
+ return NULL;
+ }
+ if (g_strcmp0 (response_type, "application/json") != 0) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd returned unexpected content type %s", response_type);
+ return NULL;
+ }
+
+ parser = json_parser_new ();
+ if (!json_parser_load_from_data (parser, response, -1, &sub_error)) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Unable to parse snapd response: %s", sub_error->message);
+ return NULL;
+ }
+ if (!JSON_NODE_HOLDS_OBJECT (json_parser_get_root (parser))) {
+ g_set_error_literal (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd response does is not a valid JSON object");
+ return NULL;
+ }
+
+ return g_object_ref (parser);
+}
+
+static void
+refine_app (GsPlugin *plugin, GsApp *app, JsonObject *package)
+{
+ const gchar *status, *icon_url;
+ g_autoptr(GdkPixbuf) icon_pixbuf = NULL;
+ gint64 size = -1;
+
+ status = json_object_get_string_member (package, "status");
+ if (g_strcmp0 (status, "installed") == 0 || g_strcmp0 (status, "active") == 0) {
+ const gchar *update_available;
+
+ update_available = json_object_has_member (package, "update_available") ?
json_object_get_string_member (package, "update_available") : NULL;
+ if (update_available)
+ gs_app_set_state (app, AS_APP_STATE_UPDATABLE);
+ else
+ gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+ size = json_object_get_int_member (package, "installed-size");
+ }
+ else if (g_strcmp0 (status, "removed") == 0) {
+ // A removed app is only available if it can be downloaded (it might have been sideloaded)
+ size = json_object_get_int_member (package, "download-size");
+ if (size > 0)
+ gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+ } else if (g_strcmp0 (status, "not installed") == 0 || g_strcmp0 (status, "available") == 0) {
+ gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+ size = json_object_get_int_member (package, "download-size");
+ }
+ gs_app_set_name (app, GS_APP_QUALITY_HIGHEST, json_object_get_string_member (package, "summary"));
+ gs_app_set_summary (app, GS_APP_QUALITY_HIGHEST, json_object_get_string_member (package,
"description"));
+ gs_app_set_version (app, json_object_get_string_member (package, "version"));
+ gs_app_set_size_download (app, json_object_get_int_member (package, "download-size"));
+ gs_app_set_size_installed (app, json_object_get_int_member (package, "installed-size"));
+ gs_app_add_quirk (app, AS_APP_QUIRK_PROVENANCE);
+ icon_url = json_object_get_string_member (package, "icon");
+ if (g_str_has_prefix (icon_url, "/")) {
+ g_autofree gchar *icon_response = NULL;
+ gsize icon_response_length;
+
+ if (send_snapd_request ("GET", icon_url, NULL, TRUE, NULL, TRUE, NULL, NULL, NULL, NULL,
&icon_response, &icon_response_length, NULL)) {
+ g_autoptr(GdkPixbufLoader) loader = NULL;
+
+ loader = gdk_pixbuf_loader_new ();
+ gdk_pixbuf_loader_write (loader, (guchar *) icon_response, icon_response_length,
NULL);
+ gdk_pixbuf_loader_close (loader, NULL);
+ icon_pixbuf = g_object_ref (gdk_pixbuf_loader_get_pixbuf (loader));
+ }
+ else
+ g_printerr ("Failed to get icon\n");
+ }
+ else {
+ g_autoptr(SoupMessage) message = NULL;
+ g_autoptr(GdkPixbufLoader) loader = NULL;
+
+ message = soup_message_new (SOUP_METHOD_GET, icon_url);
+ if (message != NULL) {
+ soup_session_send_message (gs_plugin_get_soup_session (plugin), message);
+ loader = gdk_pixbuf_loader_new ();
+ gdk_pixbuf_loader_write (loader, (guint8 *) message->response_body->data,
message->response_body->length, NULL);
+ gdk_pixbuf_loader_close (loader, NULL);
+ icon_pixbuf = g_object_ref (gdk_pixbuf_loader_get_pixbuf (loader));
+ }
+ }
+
+ if (icon_pixbuf)
+ gs_app_set_pixbuf (app, icon_pixbuf);
+ else {
+ g_autoptr(AsIcon) icon = NULL;
+
+ icon = as_icon_new ();
+ as_icon_set_kind (icon, AS_ICON_KIND_STOCK);
+ as_icon_set_name (icon, "package-x-generic");
+ gs_app_add_icon (app, icon);
+ }
+}
+
+static gboolean
+get_apps (GsPlugin *plugin, const gchar *sources, gchar **search_terms, GsAppList *list, AppFilterFunc
filter_func, gpointer user_data, GError **error)
+{
+ guint status_code;
+ GPtrArray *query_fields;
+ g_autoptr (GString) path = NULL;
+ g_autofree gchar *reason_phrase = NULL, *response_type = NULL, *response = NULL;
+ g_autoptr(JsonParser) parser = NULL;
+ JsonObject *root;
+ JsonArray *result;
+ GList *snaps;
+ GList *i;
+
+ /* Get all the apps */
+ query_fields = g_ptr_array_new_with_free_func (g_free);
+ if (sources != NULL)
+ g_ptr_array_add (query_fields, g_strdup_printf ("sources=%s", sources));
+ if (search_terms != NULL) {
+ g_autofree gchar *query = NULL;
+ query = g_strjoinv ("+", search_terms);
+ g_ptr_array_add (query_fields, g_strdup_printf ("q=%s", query));
+ }
+ g_ptr_array_add (query_fields, NULL);
+ path = g_string_new ("/v2/snaps");
+ if (query_fields->len > 1) {
+ g_autofree gchar *fields = NULL;
+ g_string_append (path, "?");
+ fields = g_strjoinv ("&", (gchar **) query_fields->pdata);
+ g_string_append (path, fields);
+ }
+ g_ptr_array_free (query_fields, TRUE);
+ if (!send_snapd_request ("GET", path->str, NULL, TRUE, NULL, TRUE, NULL, &status_code,
&reason_phrase, &response_type, &response, NULL, error))
+ return FALSE;
+
+ if (status_code != SOUP_STATUS_OK) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd returned status code %d: %s", status_code, reason_phrase);
+ return FALSE;
+ }
+
+ parser = parse_result (response, response_type, error);
+ if (parser == NULL)
+ return FALSE;
+
+ root = json_node_get_object (json_parser_get_root (parser));
+ result = json_object_get_array_member (root, "result");
+ snaps = json_array_get_elements (result);
+
+ for (i = snaps; i != NULL; i = i->next) {
+ JsonObject *package = json_node_get_object (i->data);
+ g_autoptr(GsApp) app = NULL;
+ const gchar *id;
+
+ id = json_object_get_string_member (package, "name");
+
+ if (filter_func != NULL && !filter_func (id, package, user_data))
+ continue;
+
+ app = gs_app_new (id);
+ gs_app_set_management_plugin (app, "snappy");
+ gs_app_set_origin (app, _("Ubuntu Snappy Store"));
+ gs_app_set_kind (app, AS_APP_KIND_DESKTOP);
+ gs_app_add_quirk (app, AS_APP_QUIRK_NOT_REVIEWABLE);
+ gs_app_add_quirk (app, AS_APP_QUIRK_NOT_LAUNCHABLE);
+ refine_app (plugin, app, package);
+ gs_app_list_add (list, app);
+ }
+
+ g_list_free (snaps);
+
+ return TRUE;
+}
+
+static gboolean
+get_app (GsPlugin *plugin, GsApp *app, GError **error)
+{
+ guint status_code;
+ g_autofree gchar *path = NULL, *reason_phrase = NULL, *response_type = NULL, *response = NULL;
+ g_autoptr(JsonParser) parser = NULL;
+ JsonObject *root, *result;
+
+ path = g_strdup_printf ("/v2/snaps/%s", gs_app_get_id (app));
+ if (!send_snapd_request ("GET", path, NULL, TRUE, NULL, TRUE, NULL, &status_code, &reason_phrase,
&response_type, &response, NULL, error))
+ return FALSE;
+
+ if (status_code != SOUP_STATUS_OK) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd returned status code %d: %s", status_code, reason_phrase);
+ return FALSE;
+ }
+
+ parser = parse_result (response, response_type, error);
+ if (parser == NULL)
+ return FALSE;
+ root = json_node_get_object (json_parser_get_root (parser));
+ result = json_object_get_object_member (root, "result");
+ if (result == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd returned no results for %s", gs_app_get_id (app));
+ return FALSE;
+ }
+
+ refine_app (plugin, app, result);
+
+ return TRUE;
+}
+
+void
+gs_plugin_destroy (GsPlugin *plugin)
+{
+}
+
+static gboolean
+is_active (const gchar *id, JsonObject *object, gpointer data)
+{
+ const gchar *status = json_object_get_string_member (object, "status");
+ return g_strcmp0 (status, "active") == 0;
+}
+
+gboolean
+gs_plugin_add_installed (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ return get_apps (plugin, "local", NULL, list, is_active, NULL, error);
+}
+
+gboolean
+gs_plugin_add_search (GsPlugin *plugin,
+ gchar **values,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ return get_apps (plugin, NULL, values, list, NULL, values, error);
+}
+
+gboolean
+gs_plugin_refine (GsPlugin *plugin,
+ GsAppList *list,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ guint i;
+
+ for (i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+
+ if (g_strcmp0 (gs_app_get_management_plugin (app), "snappy") != 0)
+ continue;
+
+ // Get info from snapd
+ if (!get_app (plugin, app, error))
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+send_package_action (GsPlugin *plugin, GsApp *app, const char *id, const gchar *action, GError **error)
+{
+ g_autofree gchar *content = NULL, *path = NULL;
+ guint status_code;
+ g_autofree gchar *reason_phrase = NULL, *response_type = NULL, *response = NULL, *status = NULL;
+ g_autoptr(JsonParser) parser = NULL;
+ JsonObject *root, *result, *task, *progress;
+ JsonArray *tasks;
+ GList *task_list, *l;
+ gint64 done, total, task_done, task_total;
+ const gchar *resource_path;
+ const gchar *type;
+ const gchar *change_id;
+ g_autoptr(GVariant) macaroon = NULL;
+
+ content = g_strdup_printf ("{\"action\": \"%s\"}", action);
+ path = g_strdup_printf ("/v2/snaps/%s", id);
+ if (!send_snapd_request ("POST", path, content, TRUE, NULL, TRUE, &macaroon, &status_code,
&reason_phrase, &response_type, &response, NULL, error))
+ return FALSE;
+
+ if (status_code != SOUP_STATUS_ACCEPTED) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd returned status code %d: %s", status_code, reason_phrase);
+ return FALSE;
+ }
+
+ parser = parse_result (response, response_type, error);
+ if (parser == NULL)
+ return FALSE;
+
+ root = json_node_get_object (json_parser_get_root (parser));
+ type = json_object_get_string_member (root, "type");
+
+ if (g_strcmp0 (type, "async") == 0) {
+ change_id = json_object_get_string_member (root, "change");
+ resource_path = g_strdup_printf ("/v2/changes/%s", change_id);
+
+ while (TRUE) {
+ g_autofree gchar *status_reason_phrase = NULL, *status_response_type = NULL,
*status_response = NULL;
+ g_autoptr(JsonParser) status_parser = NULL;
+
+ /* Wait for a little bit before polling */
+ g_usleep (100 * 1000);
+
+ if (!send_snapd_request ("GET", resource_path, NULL, TRUE, macaroon, TRUE, NULL,
+ &status_code, &status_reason_phrase, &status_response_type,
+ &status_response, NULL, error)) {
+ return FALSE;
+ }
+
+ if (status_code != SOUP_STATUS_OK) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd returned status code %d: %s", status_code,
status_reason_phrase);
+ return FALSE;
+ }
+
+ status_parser = parse_result (status_response, status_response_type, error);
+ if (status_parser == NULL)
+ return FALSE;
+
+ root = json_node_get_object (json_parser_get_root (status_parser));
+ result = json_object_get_object_member (root, "result");
+
+ g_free (status);
+ status = g_strdup (json_object_get_string_member (result, "status"));
+
+ if (g_strcmp0 (status, "Done") == 0)
+ break;
+
+ tasks = json_object_get_array_member (result, "tasks");
+ task_list = json_array_get_elements (tasks);
+
+ done = 0;
+ total = 0;
+
+ for (l = task_list; l != NULL; l = l->next) {
+ task = json_node_get_object (l->data);
+ progress = json_object_get_object_member (task, "progress");
+ task_done = json_object_get_int_member (progress, "done");
+ task_total = json_object_get_int_member (progress, "total");
+
+ done += task_done;
+ total += task_total;
+ }
+
+ gs_app_set_progress (app, 100 * done / total);
+
+ g_list_free (task_list);
+ }
+ }
+
+ if (g_strcmp0 (status, "Done") != 0) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd operation finished with status %s", status);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+gs_plugin_app_install (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean result;
+
+ /* We can only install apps we know of */
+ if (g_strcmp0 (gs_app_get_management_plugin (app), "snappy") != 0)
+ return TRUE;
+
+ gs_app_set_state (app, AS_APP_STATE_INSTALLING);
+ result = send_package_action (plugin, app, gs_app_get_id (app), "install", error);
+ if (result)
+ gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+ else
+ gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+
+ return result;
+}
+
+gboolean
+gs_plugin_app_remove (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean result;
+
+ /* We can only remove apps we know of */
+ if (g_strcmp0 (gs_app_get_management_plugin (app), "snappy") != 0)
+ return TRUE;
+
+ gs_app_set_state (app, AS_APP_STATE_REMOVING);
+ result = send_package_action (plugin, app, gs_app_get_id (app), "remove", error);
+ if (result)
+ gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+ else
+ gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+
+ return result;
+}
diff --git a/src/plugins/gs-plugin-ubuntu-reviews.c b/src/plugins/gs-plugin-ubuntu-reviews.c
index 6ad884a..c0046b4 100644
--- a/src/plugins/gs-plugin-ubuntu-reviews.c
+++ b/src/plugins/gs-plugin-ubuntu-reviews.c
@@ -24,13 +24,19 @@
#include <string.h>
#include <math.h>
#include <json-glib/json-glib.h>
+#include <oauth.h>
#include <sqlite3.h>
#include <gnome-software.h>
+#include "gs-ubuntuone.h"
struct GsPluginData {
gchar *db_path;
sqlite3 *db;
gsize db_loaded;
+ gchar *consumer_key;
+ gchar *consumer_secret;
+ gchar *token_key;
+ gchar *token_secret;
};
typedef struct {
@@ -47,6 +53,9 @@ typedef struct {
// FIXME: Much shorter time?
#define REVIEW_STATS_AGE_MAX (60 * 60 * 24 * 7 * 4 * 3)
+/* Number of pages of reviews to download */
+#define N_PAGES 3
+
void
gs_plugin_initialize (GsPlugin *plugin)
{
@@ -75,6 +84,11 @@ void
gs_plugin_destroy (GsPlugin *plugin)
{
GsPluginData *priv = gs_plugin_get_data (plugin);
+
+ g_clear_pointer (&priv->token_secret, g_free);
+ g_clear_pointer (&priv->token_key, g_free);
+ g_clear_pointer (&priv->consumer_secret, g_free);
+ g_clear_pointer (&priv->consumer_key, g_free);
g_clear_pointer (&priv->db, sqlite3_close);
g_free (priv->db_path);
}
@@ -332,9 +346,34 @@ parse_review_entries (GsPlugin *plugin, JsonParser *parser, GError **error)
return TRUE;
}
+static void
+sign_message (SoupMessage *message, OAuthMethod method,
+ const gchar *consumer_key, const gchar *consumer_secret,
+ const gchar *token_key, const gchar *token_secret)
+{
+ g_autofree gchar *url = NULL, *oauth_authorization_parameters = NULL, *authorization_text = NULL;
+ gchar **url_parameters = NULL;
+ int url_parameters_length;
+
+ url = soup_uri_to_string (soup_message_get_uri (message), FALSE);
+
+ url_parameters_length = oauth_split_url_parameters(url, &url_parameters);
+ oauth_sign_array2_process (&url_parameters_length, &url_parameters,
+ NULL,
+ method,
+ message->method,
+ consumer_key, consumer_secret,
+ token_key, token_secret);
+ oauth_authorization_parameters = oauth_serialize_url_sep (url_parameters_length, 1, url_parameters,
", ", 6);
+ oauth_free_array (&url_parameters_length, &url_parameters);
+ authorization_text = g_strdup_printf ("OAuth realm=\"Ratings and Reviews\", %s",
oauth_authorization_parameters);
+ soup_message_headers_append (message->request_headers, "Authorization", authorization_text);
+}
+
static gboolean
-send_review_request (GsPlugin *plugin, const gchar *method, const gchar *path, JsonBuilder *request,
JsonParser **result, GError **error)
+send_review_request (GsPlugin *plugin, const gchar *method, const gchar *path, JsonBuilder *request,
gboolean do_sign, JsonParser **result, GError **error)
{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
g_autofree gchar *uri = NULL;
g_autoptr(SoupMessage) msg = NULL;
guint status_code;
@@ -354,6 +393,14 @@ send_review_request (GsPlugin *plugin, const gchar *method, const gchar *path, J
soup_message_set_request (msg, "application/json", SOUP_MEMORY_TAKE, data, length);
}
+ if (do_sign)
+ sign_message (msg,
+ OA_PLAINTEXT,
+ priv->consumer_key,
+ priv->consumer_secret,
+ priv->token_key,
+ priv->token_secret);
+
status_code = soup_session_send_message (gs_plugin_get_soup_session (plugin), msg);
if (status_code != SOUP_STATUS_OK) {
g_set_error (error,
@@ -395,7 +442,7 @@ download_review_stats (GsPlugin *plugin, GError **error)
g_autoptr(SoupMessage) msg = NULL;
g_autoptr(JsonParser) result = NULL;
- if (!send_review_request (plugin, SOUP_METHOD_GET, "/api/1.0/review-stats/any/any/", NULL, &result,
error))
+ if (!send_review_request (plugin, SOUP_METHOD_GET, "/api/1.0/review-stats/any/any/", NULL, FALSE,
&result, error))
return FALSE;
/* Extract the stats from the data */
@@ -543,8 +590,9 @@ parse_date_time (const gchar *text)
}
static GsReview *
-parse_review (JsonNode *node)
+parse_review (GsPlugin *plugin, JsonNode *node)
{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
GsReview *review;
JsonObject *object;
gint64 star_rating;
@@ -556,6 +604,8 @@ parse_review (JsonNode *node)
object = json_node_get_object (node);
review = gs_review_new ();
+ if (g_strcmp0 (priv->consumer_key, json_object_get_string_member (object, "reviewer_username")) == 0)
+ gs_review_add_flags (review, GS_REVIEW_FLAG_SELF);
gs_review_set_reviewer (review, json_object_get_string_member (object, "reviewer_displayname"));
gs_review_set_summary (review, json_object_get_string_member (object, "summary"));
gs_review_set_text (review, json_object_get_string_member (object, "review_text"));
@@ -583,7 +633,7 @@ parse_reviews (GsPlugin *plugin, JsonParser *parser, GsApp *app, GError **error)
g_autoptr(GsReview) review = NULL;
/* Read in from JSON... (skip bad entries) */
- review = parse_review (json_array_get_element (array, i));
+ review = parse_review (plugin, json_array_get_element (array, i));
if (review != NULL)
gs_app_add_review (app, review);
}
@@ -606,7 +656,7 @@ get_language (GsPlugin *plugin)
}
static gboolean
-download_reviews (GsPlugin *plugin, GsApp *app, const gchar *package_name, GError **error)
+download_reviews (GsPlugin *plugin, GsApp *app, const gchar *package_name, gint page_number, GError **error)
{
g_autofree gchar *language = NULL, *path = NULL;
g_autoptr(JsonParser) result = NULL;
@@ -614,8 +664,8 @@ download_reviews (GsPlugin *plugin, GsApp *app, const gchar *package_name, GErro
/* Get the review stats using HTTP */
// FIXME: This will only get the first page of reviews
language = get_language (plugin);
- path = g_strdup_printf ("/api/1.0/reviews/filter/%s/any/any/any/%s/", language, package_name);
- if (!send_review_request (plugin, SOUP_METHOD_GET, path, NULL, &result, error))
+ path = g_strdup_printf ("/api/1.0/reviews/filter/%s/any/any/any/%s/page/%d/", language, package_name,
page_number + 1);
+ if (!send_review_request (plugin, SOUP_METHOD_GET, path, NULL, FALSE, &result, error))
return FALSE;
/* Extract the stats from the data */
@@ -674,10 +724,44 @@ refine_rating (GsPlugin *plugin, GsApp *app, GError **error)
}
static gboolean
+get_ubuntuone_credentials (GsPlugin *plugin,
+ gboolean required,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+
+ /* Use current credentials if already available */
+ if (priv->consumer_key != NULL &&
+ priv->consumer_secret != NULL &&
+ priv->token_key != NULL &&
+ priv->token_secret != NULL)
+ return TRUE;
+
+ /* Otherwise start with a clean slate */
+ g_clear_pointer (&priv->token_secret, g_free);
+ g_clear_pointer (&priv->token_key, g_free);
+ g_clear_pointer (&priv->consumer_secret, g_free);
+ g_clear_pointer (&priv->consumer_key, g_free);
+
+ /* Use credentials if we have them */
+ if (gs_ubuntuone_get_credentials (&priv->consumer_key, &priv->consumer_secret, &priv->token_key,
&priv->token_secret))
+ return TRUE;
+
+ /* Otherwise log in to get them */
+ if (required)
+ return gs_ubuntuone_sign_in (&priv->consumer_key, &priv->consumer_secret, &priv->token_key,
&priv->token_secret, error);
+ else
+ return TRUE;
+}
+
+static gboolean
refine_reviews (GsPlugin *plugin, GsApp *app, GError **error)
{
GPtrArray *sources;
- guint i;
+ guint i, j;
+
+ if (!get_ubuntuone_credentials (plugin, FALSE, error))
+ return FALSE;
/* Skip if already has reviews */
if (gs_app_get_reviews (app)->len > 0)
@@ -686,12 +770,15 @@ refine_reviews (GsPlugin *plugin, GsApp *app, GError **error)
sources = gs_app_get_sources (app);
for (i = 0; i < sources->len; i++) {
const gchar *package_name;
- gboolean ret;
package_name = g_ptr_array_index (sources, i);
- ret = download_reviews (plugin, app, package_name, error);
- if (!ret)
- return FALSE;
+ for (j = 0; j < N_PAGES; j++) {
+ gboolean ret;
+
+ ret = download_reviews (plugin, app, package_name, j, error);
+ if (!ret)
+ return FALSE;
+ }
}
return TRUE;
@@ -716,3 +803,313 @@ gs_plugin_refine_app (GsPlugin *plugin,
return TRUE;
}
+static void
+add_string_member (JsonBuilder *builder, const gchar *name, const gchar *value)
+{
+ json_builder_set_member_name (builder, name);
+ json_builder_add_string_value (builder, value);
+}
+
+static void
+add_int_member (JsonBuilder *builder, const gchar *name, gint64 value)
+{
+ json_builder_set_member_name (builder, name);
+ json_builder_add_int_value (builder, value);
+}
+
+static gboolean
+set_package_review (GsPlugin *plugin,
+ GsReview *review,
+ const gchar *package_name,
+ GError **error)
+{
+ gint rating;
+ gint n_stars;
+ g_autoptr(GsOsRelease) os_release = NULL;
+ g_autofree gchar *os_id = NULL, *os_ubuntu_codename = NULL, *language = NULL, *architecture = NULL;
+ g_autoptr(JsonBuilder) request = NULL;
+
+ /* Ubuntu reviews require a summary and description - just make one up for now */
+ rating = gs_review_get_rating (review);
+ if (rating > 80)
+ n_stars = 5;
+ else if (rating > 60)
+ n_stars = 4;
+ else if (rating > 40)
+ n_stars = 3;
+ else if (rating > 20)
+ n_stars = 2;
+ else
+ n_stars = 1;
+
+ os_release = gs_os_release_new (error);
+ if (os_release == NULL)
+ return FALSE;
+ os_id = gs_os_release_get_id (os_release);
+ if (os_id == NULL)
+ return FALSE;
+ os_ubuntu_codename = gs_os_release_get_ubuntu_codename (os_release);
+ if (os_ubuntu_codename == NULL)
+ return FALSE;
+
+ language = get_language (plugin);
+
+ // FIXME: Need to get Apt::Architecture configuration value from APT
+ architecture = g_strdup ("amd64");
+
+ /* Create message for reviews.ubuntu.com */
+ request = json_builder_new ();
+ json_builder_begin_object (request);
+ add_string_member (request, "package_name", package_name);
+ add_string_member (request, "summary", gs_review_get_summary (review));
+ add_string_member (request, "review_text", gs_review_get_text (review));
+ add_string_member (request, "language", language);
+ add_string_member (request, "origin", os_id);
+ add_string_member (request, "distroseries", os_ubuntu_codename);
+ add_string_member (request, "version", gs_review_get_version (review));
+ add_int_member (request, "rating", n_stars);
+ add_string_member (request, "arch_tag", architecture);
+ json_builder_end_object (request);
+
+ return send_review_request (plugin, SOUP_METHOD_POST, "/api/1.0/reviews/", request, TRUE, NULL,
error);
+}
+
+static gboolean
+set_review_usefulness (GsPlugin *plugin,
+ const gchar *review_id,
+ gboolean is_useful,
+ GError **error)
+{
+ g_autofree gchar *path = NULL;
+
+ if (!get_ubuntuone_credentials (plugin, TRUE, error))
+ return FALSE;
+
+ /* Create message for reviews.ubuntu.com */
+ path = g_strdup_printf ("/api/1.0/reviews/%s/recommendations/?useful=%s", review_id, is_useful ?
"True" : "False");
+ return send_review_request (plugin, SOUP_METHOD_POST, path, NULL, TRUE, NULL, error);
+}
+
+static gboolean
+report_review (GsPlugin *plugin,
+ const gchar *review_id,
+ const gchar *reason,
+ const gchar *text,
+ GError **error)
+{
+ g_autofree gchar *path = NULL;
+
+ if (!get_ubuntuone_credentials (plugin, TRUE, error))
+ return FALSE;
+
+ /* Create message for reviews.ubuntu.com */
+ // FIXME: escape reason / text properly
+ path = g_strdup_printf ("/api/1.0/reviews/%s/recommendations/?reason=%s&text=%s", review_id, reason,
text);
+ return send_review_request (plugin, SOUP_METHOD_POST, path, NULL, TRUE, NULL, error);
+}
+
+static gboolean
+remove_review (GsPlugin *plugin,
+ const gchar *review_id,
+ GError **error)
+{
+ g_autofree gchar *path = NULL;
+
+ if (!get_ubuntuone_credentials (plugin, TRUE, error))
+ return FALSE;
+
+ /* Create message for reviews.ubuntu.com */
+ path = g_strdup_printf ("/api/1.0/reviews/delete/%s/", review_id);
+ return send_review_request (plugin, SOUP_METHOD_POST, path, NULL, TRUE, NULL, error);
+}
+
+gboolean
+gs_plugin_review_submit (GsPlugin *plugin,
+ GsApp *app,
+ GsReview *review,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+
+ /* Load database once */
+ if (g_once_init_enter (&priv->db_loaded)) {
+ gboolean ret = load_database (plugin, error);
+ g_once_init_leave (&priv->db_loaded, TRUE);
+ if (!ret)
+ return FALSE;
+ }
+
+ if (!get_ubuntuone_credentials (plugin, TRUE, error))
+ return FALSE;
+
+ return set_package_review (plugin,
+ review,
+ gs_app_get_source_default (app),
+ error);
+}
+
+gboolean
+gs_plugin_review_report (GsPlugin *plugin,
+ GsApp *app,
+ GsReview *review,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *review_id;
+
+ /* Can only modify Ubuntu reviews */
+ review_id = gs_review_get_metadata_item (review, "ubuntu-id");
+ if (review_id == NULL)
+ return TRUE;
+
+ if (!report_review (plugin, review_id, "FIXME: gnome-software", "FIXME: gnome-software", error))
+ return FALSE;
+ gs_review_add_flags (review, GS_REVIEW_FLAG_VOTED);
+ return TRUE;
+}
+
+gboolean
+gs_plugin_review_upvote (GsPlugin *plugin,
+ GsApp *app,
+ GsReview *review,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *review_id;
+
+ /* Can only modify Ubuntu reviews */
+ review_id = gs_review_get_metadata_item (review, "ubuntu-id");
+ if (review_id == NULL)
+ return TRUE;
+
+ if (!set_review_usefulness (plugin, review_id, TRUE, error))
+ return FALSE;
+ gs_review_add_flags (review, GS_REVIEW_FLAG_VOTED);
+ return TRUE;
+}
+
+gboolean
+gs_plugin_review_downvote (GsPlugin *plugin,
+ GsApp *app,
+ GsReview *review,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *review_id;
+
+ /* Can only modify Ubuntu reviews */
+ review_id = gs_review_get_metadata_item (review, "ubuntu-id");
+ if (review_id == NULL)
+ return TRUE;
+
+ if (!set_review_usefulness (plugin, review_id, FALSE, error))
+ return FALSE;
+ gs_review_add_flags (review, GS_REVIEW_FLAG_VOTED);
+ return TRUE;
+}
+
+gboolean
+gs_plugin_review_remove (GsPlugin *plugin,
+ GsApp *app,
+ GsReview *review,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *review_id;
+
+ /* Can only modify Ubuntu reviews */
+ review_id = gs_review_get_metadata_item (review, "ubuntu-id");
+ if (review_id == NULL)
+ return TRUE;
+
+ return remove_review (plugin, review_id, error);
+}
+
+typedef struct {
+ gchar *package_name;
+ gint rating;
+} PopularEntry;
+
+static gint
+popular_sqlite_cb (void *data,
+ gint argc,
+ gchar **argv,
+ gchar **col_name)
+{
+ GList **list = data;
+ PopularEntry *entry;
+
+ entry = g_slice_new (PopularEntry);
+ entry->package_name = g_strdup (argv[0]);
+ entry->rating = get_rating (g_ascii_strtoll (argv[1], NULL, 10), g_ascii_strtoll (argv[2], NULL, 10),
g_ascii_strtoll (argv[3], NULL, 10), g_ascii_strtoll (argv[4], NULL, 10), g_ascii_strtoll (argv[5], NULL,
10));
+ *list = g_list_prepend (*list, entry);
+
+ return 0;
+}
+
+static gint
+compare_popular_entry (gconstpointer a, gconstpointer b)
+{
+ PopularEntry *ea = a, *eb = b;
+ return eb->rating - ea->rating;
+}
+
+static void
+free_popular_entry (gpointer data)
+{
+ PopularEntry *entry = data;
+ g_free (entry->package_name);
+ g_slice_free (PopularEntry, entry);
+}
+
+gboolean
+gs_plugin_add_popular (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ gint result;
+ GList *entries = NULL, *link;
+ char *error_msg = NULL;
+
+ /* Load database once */
+ if (g_once_init_enter (&priv->db_loaded)) {
+ gboolean ret = load_database (plugin, error);
+ g_once_init_leave (&priv->db_loaded, TRUE);
+ if (!ret)
+ return FALSE;
+ }
+
+ result = sqlite3_exec (priv->db,
+ "SELECT package_name, one_star_count, two_star_count, three_star_count,
four_star_count, five_star_count FROM review_stats",
+ popular_sqlite_cb,
+ &entries,
+ &error_msg);
+ if (result != SQLITE_OK) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "SQL error: %s", error_msg);
+ sqlite3_free (error_msg);
+ return FALSE;
+ }
+
+ entries = g_list_sort (entries, compare_popular_entry);
+ for (link = entries; link; link = link->next) {
+ PopularEntry *entry = link->data;
+ g_autoptr(GsApp) app = NULL;
+
+ /* Need four stars to show */
+ if (entry->rating < 80)
+ break;
+
+ app = gs_app_new (NULL);
+ gs_app_add_source (app, entry->package_name);
+ gs_app_list_add (list, app);
+ }
+ g_list_free_full (entries, free_popular_entry);
+
+ return TRUE;
+}
diff --git a/src/plugins/gs-ubuntu-snapd.c b/src/plugins/gs-ubuntu-snapd.c
new file mode 100644
index 0000000..32fb2bc
--- /dev/null
+++ b/src/plugins/gs-ubuntu-snapd.c
@@ -0,0 +1,277 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <gs-plugin.h>
+#include <libsoup/soup.h>
+#include <gio/gunixsocketaddress.h>
+#include "gs-ubuntu-snapd.h"
+#include "gs-ubuntuone.h"
+
+#define SNAPD_SOCKET_PATH "/run/snapd.socket"
+
+// snapd API documentation is at https://github.com/ubuntu-core/snappy/blob/master/docs/rest.md
+
+static GSocket *
+open_snapd_socket (GError **error)
+{
+ GSocket *socket;
+ g_autoptr(GSocketAddress) address = NULL;
+ g_autoptr(GError) sub_error = NULL;
+
+ socket = g_socket_new (G_SOCKET_FAMILY_UNIX, G_SOCKET_TYPE_STREAM, G_SOCKET_PROTOCOL_DEFAULT,
&sub_error);
+ if (!socket) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Unable to open snapd socket: %s", sub_error->message);
+ return NULL;
+ }
+ address = g_unix_socket_address_new (SNAPD_SOCKET_PATH);
+ if (!g_socket_connect (socket, address, NULL, &sub_error)) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Unable to connect snapd socket: %s", sub_error->message);
+ g_object_unref (socket);
+ return NULL;
+ }
+
+ return socket;
+}
+
+static gboolean
+read_from_snapd (GSocket *socket, gchar *buffer, gsize buffer_length, gsize *read_offset, GError **error)
+{
+ gssize n_read;
+ n_read = g_socket_receive (socket, buffer + *read_offset, buffer_length - *read_offset, NULL, error);
+ if (n_read < 0)
+ return FALSE;
+ *read_offset += n_read;
+ buffer[*read_offset] = '\0';
+
+ return TRUE;
+}
+
+gboolean
+send_snapd_request (const gchar *method,
+ const gchar *path,
+ const gchar *content,
+ gboolean authenticate,
+ GVariant *macaroon,
+ gboolean retry_after_login,
+ GVariant **out_macaroon,
+ guint *status_code,
+ gchar **reason_phrase,
+ gchar **response_type,
+ gchar **response,
+ gsize *response_length,
+ GError **error)
+{
+ g_autoptr (GSocket) socket = NULL;
+ g_autoptr (GString) request = NULL;
+ gssize n_written;
+ gsize max_data_length = 65535, data_length = 0, header_length;
+ gchar data[max_data_length + 1], *body = NULL;
+ g_autoptr (SoupMessageHeaders) headers = NULL;
+ g_autoptr(GVariant) auto_macaroon = NULL;
+ gsize chunk_length, n_required;
+ gchar *chunk_start = NULL;
+ const gchar *root;
+ const gchar *discharge;
+ GVariantIter *iter;
+ guint code;
+ gboolean ret;
+
+ if (macaroon == NULL && authenticate) {
+ auto_macaroon = gs_ubuntuone_get_macaroon (TRUE, FALSE, NULL);
+ macaroon = auto_macaroon;
+ }
+
+ // NOTE: Would love to use libsoup but it doesn't support unix sockets
+ // https://bugzilla.gnome.org/show_bug.cgi?id=727563
+
+ socket = open_snapd_socket (error);
+
+ if (socket == NULL)
+ return FALSE;
+
+ request = g_string_new ("");
+ g_string_append_printf (request, "%s %s HTTP/1.1\r\n", method, path);
+ g_string_append (request, "Host:\r\n");
+ if (macaroon != NULL) {
+ g_variant_get (macaroon, "(&sas)", &root, &iter);
+ g_string_append_printf (request, "Authorization: Macaroon root=\"%s\"", root);
+
+ while (g_variant_iter_next (iter, "&s", &discharge))
+ g_string_append_printf (request, ",discharge=\"%s\"", discharge);
+
+ g_variant_iter_free (iter);
+ g_string_append (request, "\r\n");
+ }
+ if (content)
+ g_string_append_printf (request, "Content-Length: %zi\r\n", strlen (content));
+ g_string_append (request, "\r\n");
+ if (content)
+ g_string_append (request, content);
+
+ if (g_strcmp0 (g_getenv ("GNOME_SOFTWARE_SNAPPY"), "debug") == 0)
+ g_print ("===== begin snapd request =====\n%s\n===== end snapd request =====\n",
request->str);
+
+ /* Send HTTP request */
+ n_written = g_socket_send (socket, request->str, request->len, NULL, error);
+ if (n_written < 0)
+ return FALSE;
+
+ /* Read HTTP headers */
+ while (data_length < max_data_length && !body) {
+ if (!read_from_snapd (socket, data, max_data_length, &data_length, error))
+ return FALSE;
+ body = strstr (data, "\r\n\r\n");
+ }
+ if (!body) {
+ g_set_error_literal (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Unable to find header separator in snapd response");
+ return FALSE;
+ }
+
+ /* Body starts after header divider */
+ body += 4;
+ header_length = body - data;
+
+ /* Parse headers */
+ headers = soup_message_headers_new (SOUP_MESSAGE_HEADERS_RESPONSE);
+ if (!soup_headers_parse_response (data, header_length, headers, NULL, &code, reason_phrase)) {
+ g_set_error_literal (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "snapd response HTTP headers not parseable");
+ return FALSE;
+ }
+
+ if (status_code != NULL)
+ *status_code = code;
+
+ if ((code == 401 || code == 403) && retry_after_login) {
+ g_socket_close (socket, NULL);
+
+ gs_ubuntuone_clear_macaroon ();
+
+ macaroon = gs_ubuntuone_get_macaroon (FALSE, TRUE, NULL);
+
+ if (macaroon == NULL) {
+ g_set_error_literal (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED,
+ "failed to authenticate");
+ return FALSE;
+ }
+
+ ret = send_snapd_request (method,
+ path,
+ content,
+ TRUE,
+ macaroon,
+ FALSE,
+ NULL,
+ status_code,
+ reason_phrase,
+ response_type,
+ response,
+ response_length,
+ error);
+
+ if (ret && out_macaroon != NULL) {
+ *out_macaroon = macaroon;
+ } else {
+ g_variant_unref (macaroon);
+ }
+
+ return ret;
+ }
+
+ /* Work out how much data to follow */
+ if (g_strcmp0 (soup_message_headers_get_one (headers, "Transfer-Encoding"), "chunked") == 0) {
+ while (data_length < max_data_length) {
+ chunk_start = strstr (body, "\r\n");
+ if (chunk_start)
+ break;
+ if (!read_from_snapd (socket, data, max_data_length, &data_length, error))
+ return FALSE;
+ }
+ if (!chunk_start) {
+ g_set_error_literal (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Unable to find chunk header in snapd response");
+ return FALSE;
+ }
+ chunk_length = strtoul (body, NULL, 16);
+ chunk_start += 2;
+ // FIXME: Support multiple chunks
+ }
+ else {
+ const gchar *value;
+ value = soup_message_headers_get_one (headers, "Content-Length");
+ if (!value) {
+ g_set_error_literal (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Unable to determine content length of snapd response");
+ return FALSE;
+ }
+ chunk_length = strtoul (value, NULL, 10);
+ chunk_start = body;
+ }
+
+ /* Check if enough space to read chunk */
+ n_required = (chunk_start - data) + chunk_length;
+ if (n_required > max_data_length) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Not enough space for snapd response, require %zi octets, have %zi", n_required,
max_data_length);
+ return FALSE;
+ }
+
+ /* Read chunk content */
+ while (data_length < n_required)
+ if (!read_from_snapd (socket, data, n_required - data_length, &data_length, error))
+ return FALSE;
+
+ if (out_macaroon != NULL)
+ *out_macaroon = g_variant_ref (macaroon);
+ if (response_type)
+ *response_type = g_strdup (soup_message_headers_get_one (headers, "Content-Type"));
+ if (response) {
+ *response = g_malloc (chunk_length + 2);
+ memcpy (*response, chunk_start, chunk_length + 1);
+ (*response)[chunk_length + 1] = '\0';
+
+ if (g_strcmp0 (g_getenv ("GNOME_SOFTWARE_SNAPPY"), "debug") == 0)
+ g_print ("===== begin snapd response =====\nStatus %u\n%s\n===== end snapd response
=====\n", code, *response);
+ }
+ if (response_length)
+ *response_length = chunk_length;
+
+ return TRUE;
+}
diff --git a/src/plugins/gs-ubuntu-snapd.h b/src/plugins/gs-ubuntu-snapd.h
new file mode 100644
index 0000000..cbc1e1a
--- /dev/null
+++ b/src/plugins/gs-ubuntu-snapd.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef __GS_UBUNTU_SNAPD_H__
+#define __GS_UBUNTU_SNAPD_H__
+
+#include <gio/gio.h>
+
+gboolean send_snapd_request (const gchar *method,
+ const gchar *path,
+ const gchar *content,
+ gboolean authenticate,
+ GVariant *macaroon,
+ gboolean retry_after_login,
+ GVariant **out_macaroon,
+ guint *status_code,
+ gchar **reason_phrase,
+ gchar **response_type,
+ gchar **response,
+ gsize *response_length,
+ GError **error);
+
+#endif /* __GS_UBUNTU_SNAPD_H__ */
diff --git a/src/plugins/gs-ubuntuone-dialog.c b/src/plugins/gs-ubuntuone-dialog.c
new file mode 100644
index 0000000..7d982a7
--- /dev/null
+++ b/src/plugins/gs-ubuntuone-dialog.c
@@ -0,0 +1,666 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Canonical Ltd.
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "gs-ubuntuone-dialog.h"
+#include "gs-common.h"
+#include "gs-utils.h"
+
+#include <glib/gi18n.h>
+#include <json-glib/json-glib.h>
+#include <libsoup/soup.h>
+
+#include "gs-ubuntu-snapd.h"
+
+#define UBUNTU_LOGIN_HOST "https://login.ubuntu.com"
+
+struct _GsUbuntuoneDialog
+{
+ GtkDialog parent_instance;
+
+ GtkWidget *content_box;
+ GtkWidget *cancel_button;
+ GtkWidget *next_button;
+ GtkWidget *status_stack;
+ GtkWidget *status_image;
+ GtkWidget *status_label;
+ GtkWidget *page_stack;
+ GtkWidget *prompt_label;
+ GtkWidget *login_radio;
+ GtkWidget *register_radio;
+ GtkWidget *reset_radio;
+ GtkWidget *email_entry;
+ GtkWidget *password_entry;
+ GtkWidget *remember_check;
+ GtkWidget *passcode_entry;
+
+ SoupSession *session;
+
+ gboolean get_macaroon;
+
+ GVariant *macaroon;
+ gchar *consumer_key;
+ gchar *consumer_secret;
+ gchar *token_key;
+ gchar *token_secret;
+};
+
+G_DEFINE_TYPE (GsUbuntuoneDialog, gs_ubuntuone_dialog, GTK_TYPE_DIALOG)
+
+static gboolean
+is_email_address (const gchar *text)
+{
+ text = g_utf8_strchr (text, -1, '@');
+
+ if (!text)
+ return FALSE;
+
+ text = g_utf8_strchr (text + 1, -1, '.');
+
+ if (!text)
+ return FALSE;
+
+ return text[1];
+}
+
+static void
+update_widgets (GsUbuntuoneDialog *self)
+{
+ if (g_str_equal (gtk_stack_get_visible_child_name (GTK_STACK (self->page_stack)), "page-0")) {
+ gtk_widget_set_sensitive (self->next_button,
+ !gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON
(self->login_radio)) ||
+ (is_email_address (gtk_entry_get_text (GTK_ENTRY
(self->email_entry))) &&
+ gtk_entry_get_text_length (GTK_ENTRY (self->password_entry)) > 0));
+ gtk_widget_set_sensitive (self->password_entry,
+ gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON
(self->login_radio)));
+ gtk_widget_set_sensitive (self->remember_check,
+ gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON
(self->login_radio)));
+ } else if (g_str_equal (gtk_stack_get_visible_child_name (GTK_STACK (self->page_stack)), "page-1")) {
+ gtk_widget_set_sensitive (self->next_button, gtk_entry_get_text_length (GTK_ENTRY
(self->passcode_entry)) > 0);
+ } else if (g_str_equal (gtk_stack_get_visible_child_name (GTK_STACK (self->page_stack)), "page-2")) {
+ gtk_widget_set_visible (self->cancel_button, FALSE);
+ gtk_widget_set_sensitive (self->cancel_button, FALSE);
+ gtk_button_set_label (GTK_BUTTON (self->next_button), _("_Continue"));
+ }
+}
+
+typedef void (*ResponseCallback) (GsUbuntuoneDialog *self,
+ guint status,
+ GVariant *response,
+ gpointer user_data);
+
+typedef struct
+{
+ GsUbuntuoneDialog *dialog;
+ ResponseCallback callback;
+ gpointer user_data;
+} RequestInfo;
+
+static void
+response_received_cb (SoupSession *session,
+ SoupMessage *message,
+ gpointer user_data)
+{
+ RequestInfo *info = user_data;
+ g_autoptr(GVariant) response = NULL;
+ guint status;
+ GBytes *bytes;
+ g_autofree gchar *body = NULL;
+ gsize length;
+
+ g_object_get (message,
+ SOUP_MESSAGE_STATUS_CODE, &status,
+ SOUP_MESSAGE_RESPONSE_BODY_DATA, &bytes,
+ NULL);
+
+ body = g_bytes_unref_to_data (bytes, &length);
+
+ if (body)
+ response = json_gvariant_deserialize_data (body, length, NULL, NULL);
+
+ if (response)
+ g_variant_ref_sink (response);
+
+ if (info->callback)
+ info->callback (info->dialog, status, response, info->user_data);
+
+ g_free (info);
+}
+
+static void
+send_request (GsUbuntuoneDialog *self,
+ const gchar *method,
+ const gchar *uri,
+ GVariant *request,
+ ResponseCallback callback,
+ gpointer user_data)
+{
+ RequestInfo *info;
+ SoupMessage *message;
+ gchar *body;
+ gsize length;
+ g_autofree gchar *url = NULL;
+
+ if (self->session == NULL)
+ self->session = soup_session_new_with_options (SOUP_SESSION_USER_AGENT,
+ gs_user_agent (),
+ NULL);
+
+ body = json_gvariant_serialize_data (g_variant_ref_sink (request), &length);
+ g_variant_unref (request);
+
+ url = g_strdup_printf ("%s%s", UBUNTU_LOGIN_HOST, uri);
+ message = soup_message_new (method, url);
+
+ info = g_new0 (RequestInfo, 1);
+ info->dialog = self;
+ info->callback = callback;
+ info->user_data = user_data;
+
+ soup_message_set_request (message, "application/json", SOUP_MEMORY_TAKE, body, length);
+ soup_session_queue_message (self->session, message, response_received_cb, info);
+}
+
+static void
+show_status (GsUbuntuoneDialog *self,
+ const gchar *text,
+ gboolean is_error)
+{
+ PangoAttrList *attributes;
+
+ gtk_widget_set_visible (self->status_stack, TRUE);
+
+ if (is_error) {
+ gtk_stack_set_visible_child_name (GTK_STACK (self->status_stack), "status-image");
+ gtk_image_set_from_icon_name (GTK_IMAGE (self->status_image), "gtk-dialog-error",
GTK_ICON_SIZE_BUTTON);
+ } else {
+ gtk_stack_set_visible_child_name (GTK_STACK (self->status_stack), "status-spinner");
+ }
+
+ attributes = pango_attr_list_new ();
+ pango_attr_list_insert (attributes, pango_attr_weight_new (PANGO_WEIGHT_BOLD));
+ pango_attr_list_insert (attributes, pango_attr_foreground_new (is_error ? 65535 : 0, 0, 0));
+ gtk_label_set_attributes (GTK_LABEL (self->status_label), attributes);
+ pango_attr_list_unref (attributes);
+
+ gtk_label_set_text (GTK_LABEL (self->status_label), text);
+}
+
+static void
+reenable_widgets (GsUbuntuoneDialog *self)
+{
+ gtk_label_set_text (GTK_LABEL (self->status_label), NULL);
+ gtk_stack_set_visible_child_name (GTK_STACK (self->status_stack), "status-image");
+ gtk_widget_set_visible (self->status_stack, FALSE);
+
+ gtk_widget_set_sensitive (self->cancel_button, TRUE);
+ gtk_widget_set_sensitive (self->next_button, TRUE);
+ gtk_widget_set_sensitive (self->login_radio, TRUE);
+ gtk_widget_set_sensitive (self->register_radio, TRUE);
+ gtk_widget_set_sensitive (self->reset_radio, TRUE);
+ gtk_widget_set_sensitive (self->email_entry, TRUE);
+ gtk_widget_set_sensitive (self->password_entry, TRUE);
+ gtk_widget_set_sensitive (self->remember_check, TRUE);
+ gtk_widget_set_sensitive (self->passcode_entry, TRUE);
+}
+
+static void
+receive_login_response_cb (GsUbuntuoneDialog *self,
+ guint status,
+ GVariant *response,
+ gpointer user_data)
+{
+ const gchar *code;
+
+ reenable_widgets (self);
+
+ if (response) {
+ switch (status) {
+ case SOUP_STATUS_OK:
+ case SOUP_STATUS_CREATED:
+ g_clear_pointer (&self->token_secret, g_free);
+ g_clear_pointer (&self->token_key, g_free);
+ g_clear_pointer (&self->consumer_secret, g_free);
+ g_clear_pointer (&self->consumer_key, g_free);
+
+ g_variant_lookup (response, "consumer_key", "s", &self->consumer_key);
+ g_variant_lookup (response, "consumer_secret", "s", &self->consumer_secret);
+ g_variant_lookup (response, "token_key", "s", &self->token_key);
+ g_variant_lookup (response, "token_secret", "s", &self->token_secret);
+
+ gtk_stack_set_visible_child_name (GTK_STACK (self->page_stack), "page-2");
+ update_widgets (self);
+ break;
+
+ default:
+ g_variant_lookup (response, "code", "&s", &code);
+
+ if (!code)
+ code = "";
+
+ if (g_str_equal (code, "TWOFACTOR_REQUIRED")) {
+ gtk_stack_set_visible_child_name (GTK_STACK (self->page_stack), "page-1");
+ gtk_widget_grab_focus (self->passcode_entry);
+ update_widgets (self);
+ break;
+ }
+
+ update_widgets (self);
+
+ if (g_str_equal (code, "INVALID_CREDENTIALS")) {
+ show_status (self, _("Incorrect email or password"), TRUE);
+ gtk_widget_grab_focus (self->password_entry);
+ } else if (g_str_equal (code, "ACCOUNT_SUSPENDED")) {
+ show_status (self, _("Account suspended"), TRUE);
+ gtk_widget_grab_focus (self->email_entry);
+ } else if (g_str_equal (code, "ACCOUNT_DEACTIVATED")) {
+ show_status (self, _("Account deactivated"), TRUE);
+ gtk_widget_grab_focus (self->email_entry);
+ } else if (g_str_equal (code, "EMAIL_INVALIDATED")) {
+ show_status (self, _("Email invalidated"), TRUE);
+ gtk_widget_grab_focus (self->email_entry);
+ } else if (g_str_equal (code, "TWOFACTOR_FAILURE")) {
+ show_status (self, _("Two-factor authentication failed"), TRUE);
+ gtk_widget_grab_focus (self->passcode_entry);
+ } else if (g_str_equal (code, "PASSWORD_POLICY_ERROR")) {
+ show_status (self, _("Password reset required"), TRUE);
+ gtk_widget_grab_focus (self->reset_radio);
+ } else if (g_str_equal (code, "TOO_MANY_REQUESTS")) {
+ show_status (self, _("Too many requests"), TRUE);
+ gtk_widget_grab_focus (self->password_entry);
+ } else {
+ show_status (self, _("An error occurred"), TRUE);
+ gtk_widget_grab_focus (self->password_entry);
+ }
+
+ break;
+ }
+ } else {
+ update_widgets (self);
+ show_status (self, _("An error occurred"), TRUE);
+ gtk_widget_grab_focus (self->password_entry);
+ }
+}
+
+static void
+check_snapd_response (GsUbuntuoneDialog *self,
+ guint status_code,
+ gchar *reason_phrase,
+ gchar *response_type,
+ gchar *response,
+ gsize response_length)
+{
+ g_autoptr(GVariant) variant = NULL;
+ g_autoptr(GVariant) result = NULL;
+ g_autoptr(GVariant) discharges = NULL;
+ const gchar *type;
+ const gchar *kind;
+ const gchar *macaroon;
+ GVariantBuilder builder;
+ GVariantIter iter;
+ GVariant *discharge = NULL;
+
+ if (status_code == 401) {
+ /* snapd isn't giving us enough information to tell why the authentication failed... */
+ if (g_str_equal (gtk_stack_get_visible_child_name (GTK_STACK (self->page_stack)), "page-1")) {
+ show_status (self, _("Two-factor authentication failed"), TRUE);
+ gtk_widget_grab_focus (self->passcode_entry);
+ return;
+ }
+ }
+
+ variant = json_gvariant_deserialize_data (response, -1, NULL, NULL);
+
+ if (variant == NULL)
+ goto err;
+
+ g_variant_ref_sink (variant);
+
+ if (!g_variant_lookup (variant, "type", "&s", &type))
+ goto err;
+
+ result = g_variant_lookup_value (variant, "result", G_VARIANT_TYPE_DICTIONARY);
+
+ if (result == NULL)
+ goto err;
+
+ if (g_str_equal (type, "sync")) {
+ if (!g_variant_lookup (result, "macaroon", "&s", &macaroon))
+ goto err;
+
+ discharges = g_variant_lookup_value (result, "discharges", G_VARIANT_TYPE ("av"));
+
+ if (discharges == NULL)
+ goto err;
+
+ g_variant_builder_init (&builder, G_VARIANT_TYPE ("as"));
+ g_variant_iter_init (&iter, discharges);
+
+ while (g_variant_iter_loop (&iter, "v", &discharge))
+ g_variant_builder_add (&builder, "s", g_variant_get_string (discharge, NULL));
+
+ self->macaroon = g_variant_ref_sink (g_variant_new ("(s as)", macaroon, g_variant_builder_end
(&builder)));
+
+ gtk_stack_set_visible_child_name (GTK_STACK (self->page_stack), "page-2");
+ update_widgets (self);
+ } else if (g_variant_lookup (result, "kind", "&s", &kind)) {
+ if (g_str_equal (kind, "two-factor-required")) {
+ gtk_stack_set_visible_child_name (GTK_STACK (self->page_stack), "page-1");
+ gtk_widget_grab_focus (self->passcode_entry);
+ update_widgets (self);
+ } else
+ goto err;
+ } else
+ goto err;
+
+ return;
+
+err:
+ /* snapd isn't giving us enough information to tell why the authentication failed... */
+ show_status (self, status_code == 401 ? _("Incorrect email or password") : _("An error occurred"),
TRUE);
+ gtk_widget_grab_focus (self->password_entry);
+}
+
+static void
+send_login_request (GsUbuntuoneDialog *self)
+{
+ g_autofree gchar *content = NULL;
+ g_autofree gchar *username = NULL;
+ g_autofree gchar *password = NULL;
+ g_autofree gchar *otp = NULL;
+ GVariant *request;
+ guint status_code;
+ g_autofree gchar *reason_phrase = NULL;
+ g_autofree gchar *response_type = NULL;
+ g_autofree gchar *response = NULL;
+ gsize response_length;
+ g_autoptr(GError) error = NULL;
+
+ gtk_widget_set_sensitive (self->cancel_button, FALSE);
+ gtk_widget_set_sensitive (self->next_button, FALSE);
+ gtk_widget_set_sensitive (self->login_radio, FALSE);
+ gtk_widget_set_sensitive (self->register_radio, FALSE);
+ gtk_widget_set_sensitive (self->reset_radio, FALSE);
+ gtk_widget_set_sensitive (self->email_entry, FALSE);
+ gtk_widget_set_sensitive (self->password_entry, FALSE);
+ gtk_widget_set_sensitive (self->remember_check, FALSE);
+ gtk_widget_set_sensitive (self->passcode_entry, FALSE);
+
+ show_status (self, _("Signing in…"), FALSE);
+
+ if (self->get_macaroon) {
+ username = g_strescape (gtk_entry_get_text (GTK_ENTRY (self->email_entry)), NULL);
+ password = g_strescape (gtk_entry_get_text (GTK_ENTRY (self->password_entry)), NULL);
+
+ if (gtk_entry_get_text_length (GTK_ENTRY (self->passcode_entry)) > 0) {
+ otp = g_strescape (gtk_entry_get_text (GTK_ENTRY (self->passcode_entry)), NULL);
+
+ content = g_strdup_printf ("{"
+ " \"username\" : \"%s\","
+ " \"password\" : \"%s\","
+ " \"otp\" : \"%s\""
+ "}",
+ username,
+ password,
+ otp);
+ } else {
+ content = g_strdup_printf ("{"
+ " \"username\" : \"%s\","
+ " \"password\" : \"%s\""
+ "}",
+ username,
+ password);
+ }
+
+ if (send_snapd_request (SOUP_METHOD_POST,
+ "/v2/login",
+ content,
+ FALSE,
+ NULL,
+ FALSE,
+ NULL,
+ &status_code,
+ &reason_phrase,
+ &response_type,
+ &response,
+ &response_length,
+ &error)) {
+ reenable_widgets (self);
+
+ check_snapd_response (self,
+ status_code,
+ reason_phrase,
+ response_type,
+ response,
+ response_length);
+ } else {
+ g_warning ("could not send request: %s", error->message);
+
+ reenable_widgets (self);
+ show_status (self, _("An error occurred"), TRUE);
+ gtk_widget_grab_focus (self->password_entry);
+ }
+ } else {
+ if (gtk_entry_get_text_length (GTK_ENTRY (self->passcode_entry)) > 0) {
+ request = g_variant_new_parsed ("{"
+ " 'token_name' : <'GNOME Software'>,"
+ " 'email' : <%s>,"
+ " 'password' : <%s>,"
+ " 'otp' : <%s>"
+ "}",
+ gtk_entry_get_text (GTK_ENTRY (self->email_entry)),
+ gtk_entry_get_text (GTK_ENTRY (self->password_entry)),
+ gtk_entry_get_text (GTK_ENTRY
(self->passcode_entry)));
+ } else {
+ request = g_variant_new_parsed ("{"
+ " 'token_name' : <'GNOME Software'>,"
+ " 'email' : <%s>,"
+ " 'password' : <%s>"
+ "}",
+ gtk_entry_get_text (GTK_ENTRY (self->email_entry)),
+ gtk_entry_get_text (GTK_ENTRY
(self->password_entry)));
+ }
+
+ send_request (self,
+ SOUP_METHOD_POST,
+ "/api/v2/tokens/oauth",
+ request,
+ receive_login_response_cb,
+ NULL);
+ }
+}
+
+static void
+next_button_clicked_cb (GsUbuntuoneDialog *self,
+ GtkButton *button)
+{
+ if (g_str_equal (gtk_stack_get_visible_child_name (GTK_STACK (self->page_stack)), "page-0")) {
+ if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->login_radio))) {
+ send_login_request (self);
+ } else if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->register_radio))) {
+ g_app_info_launch_default_for_uri ("https://login.ubuntu.com/+new_account", NULL,
NULL);
+ } else if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->reset_radio))) {
+ g_app_info_launch_default_for_uri ("https://login.ubuntu.com/+forgot_password", NULL,
NULL);
+ }
+ } else if (g_str_equal (gtk_stack_get_visible_child_name (GTK_STACK (self->page_stack)), "page-1")) {
+ send_login_request (self);
+ } else if (g_str_equal (gtk_stack_get_visible_child_name (GTK_STACK (self->page_stack)), "page-2")) {
+ gtk_dialog_response (GTK_DIALOG (self), GTK_RESPONSE_OK);
+ }
+}
+
+static void
+radio_button_toggled_cb (GsUbuntuoneDialog *self,
+ GtkToggleButton *toggle)
+{
+ update_widgets (self);
+}
+
+static void
+entry_edited_cb (GsUbuntuoneDialog *self,
+ GParamSpec *pspec,
+ GObject *object)
+{
+ update_widgets (self);
+}
+
+static void
+gs_ubuntuone_dialog_init (GsUbuntuoneDialog *self)
+{
+ GList *focus_chain = NULL;
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ gtk_window_set_default (GTK_WINDOW (self), self->next_button);
+
+ focus_chain = g_list_append (focus_chain, self->email_entry);
+ focus_chain = g_list_append (focus_chain, self->password_entry);
+ focus_chain = g_list_append (focus_chain, self->remember_check);
+ focus_chain = g_list_append (focus_chain, self->login_radio);
+ focus_chain = g_list_append (focus_chain, self->register_radio);
+ focus_chain = g_list_append (focus_chain, self->reset_radio);
+ gtk_container_set_focus_chain (GTK_CONTAINER (gtk_widget_get_parent (self->email_entry)),
focus_chain);
+ g_list_free (focus_chain);
+
+ g_signal_connect_swapped (self->next_button, "clicked", G_CALLBACK (next_button_clicked_cb), self);
+ g_signal_connect_swapped (self->login_radio, "toggled", G_CALLBACK (radio_button_toggled_cb), self);
+ g_signal_connect_swapped (self->register_radio, "toggled", G_CALLBACK (radio_button_toggled_cb),
self);
+ g_signal_connect_swapped (self->reset_radio, "toggled", G_CALLBACK (radio_button_toggled_cb), self);
+ g_signal_connect_swapped (self->email_entry, "notify::text", G_CALLBACK (entry_edited_cb), self);
+ g_signal_connect_swapped (self->password_entry, "notify::text", G_CALLBACK (entry_edited_cb), self);
+ g_signal_connect_swapped (self->passcode_entry, "notify::text", G_CALLBACK (entry_edited_cb), self);
+
+ update_widgets (self);
+}
+
+static void
+gs_ubuntuone_dialog_dispose (GObject *object)
+{
+ GsUbuntuoneDialog *self = GS_UBUNTUONE_DIALOG (object);
+
+ g_clear_object (&self->session);
+
+ G_OBJECT_CLASS (gs_ubuntuone_dialog_parent_class)->dispose (object);
+}
+
+static void
+gs_ubuntuone_dialog_finalize (GObject *object)
+{
+ GsUbuntuoneDialog *self = GS_UBUNTUONE_DIALOG (object);
+
+ g_clear_pointer (&self->token_secret, g_free);
+ g_clear_pointer (&self->token_key, g_free);
+ g_clear_pointer (&self->consumer_secret, g_free);
+ g_clear_pointer (&self->consumer_key, g_free);
+ g_clear_pointer (&self->macaroon, g_variant_unref);
+
+ G_OBJECT_CLASS (gs_ubuntuone_dialog_parent_class)->finalize (object);
+}
+
+static void
+gs_ubuntuone_dialog_class_init (GsUbuntuoneDialogClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = gs_ubuntuone_dialog_dispose;
+ object_class->finalize = gs_ubuntuone_dialog_finalize;
+
+ gtk_widget_class_set_template_from_resource (widget_class,
"/org/gnome/Software/plugins/gs-ubuntuone-dialog.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, content_box);
+ gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, cancel_button);
+ gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, next_button);
+ gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, status_stack);
+ gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, status_image);
+ gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, status_label);
+ gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, page_stack);
+ gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, prompt_label);
+ gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, login_radio);
+ gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, register_radio);
+ gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, reset_radio);
+ gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, email_entry);
+ gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, password_entry);
+ gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, remember_check);
+ gtk_widget_class_bind_template_child (widget_class, GsUbuntuoneDialog, passcode_entry);
+}
+
+gboolean
+gs_ubuntuone_dialog_get_do_remember (GsUbuntuoneDialog *dialog)
+{
+ g_return_val_if_fail (GS_IS_UBUNTUONE_DIALOG (dialog), FALSE);
+ return gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (dialog->remember_check));
+}
+
+GVariant *
+gs_ubuntuone_dialog_get_macaroon (GsUbuntuoneDialog *dialog)
+{
+ g_return_val_if_fail (GS_IS_UBUNTUONE_DIALOG (dialog), NULL);
+ return dialog->macaroon;
+}
+
+const gchar *
+gs_ubuntuone_dialog_get_consumer_key (GsUbuntuoneDialog *dialog)
+{
+ g_return_val_if_fail (GS_IS_UBUNTUONE_DIALOG (dialog), NULL);
+ return dialog->consumer_key;
+}
+
+const gchar *
+gs_ubuntuone_dialog_get_consumer_secret (GsUbuntuoneDialog *dialog)
+{
+ g_return_val_if_fail (GS_IS_UBUNTUONE_DIALOG (dialog), NULL);
+ return dialog->consumer_secret;
+}
+
+const gchar *
+gs_ubuntuone_dialog_get_token_key (GsUbuntuoneDialog *dialog)
+{
+ g_return_val_if_fail (GS_IS_UBUNTUONE_DIALOG (dialog), NULL);
+ return dialog->token_key;
+}
+
+const gchar *
+gs_ubuntuone_dialog_get_token_secret (GsUbuntuoneDialog *dialog)
+{
+ g_return_val_if_fail (GS_IS_UBUNTUONE_DIALOG (dialog), NULL);
+ return dialog->token_secret;
+}
+
+GtkWidget *
+gs_ubuntuone_dialog_new (gboolean get_macaroon)
+{
+ GsUbuntuoneDialog *dialog = g_object_new (GS_TYPE_UBUNTUONE_DIALOG,
+ "use-header-bar", TRUE,
+ NULL);
+
+ dialog->get_macaroon = get_macaroon;
+
+ if (dialog->get_macaroon)
+ gtk_label_set_label (GTK_LABEL (dialog->prompt_label),
+ _("To install and remove snaps, you need an Ubuntu Single Sign-On account."));
+ else
+ gtk_label_set_label (GTK_LABEL (dialog->prompt_label),
+ _("To rate and review software, you need an Ubuntu Single Sign-On account."));
+
+ return GTK_WIDGET (dialog);
+}
+
+/* vim: set noexpandtab: */
diff --git a/src/plugins/gs-ubuntuone-dialog.h b/src/plugins/gs-ubuntuone-dialog.h
new file mode 100644
index 0000000..d98404e
--- /dev/null
+++ b/src/plugins/gs-ubuntuone-dialog.h
@@ -0,0 +1,45 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Canonical Ltd.
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef GS_UBUNTUONE_DIALOG_H
+#define GS_UBUNTUONE_DIALOG_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_UBUNTUONE_DIALOG gs_ubuntuone_dialog_get_type ()
+
+G_DECLARE_FINAL_TYPE (GsUbuntuoneDialog, gs_ubuntuone_dialog, GS, UBUNTUONE_DIALOG, GtkDialog)
+
+GtkWidget *gs_ubuntuone_dialog_new (gboolean get_macaroon);
+gboolean gs_ubuntuone_dialog_get_do_remember (GsUbuntuoneDialog *dialog);
+GVariant *gs_ubuntuone_dialog_get_macaroon (GsUbuntuoneDialog *dialog);
+const gchar *gs_ubuntuone_dialog_get_consumer_key (GsUbuntuoneDialog *dialog);
+const gchar *gs_ubuntuone_dialog_get_consumer_secret (GsUbuntuoneDialog *dialog);
+const gchar *gs_ubuntuone_dialog_get_token_key (GsUbuntuoneDialog *dialog);
+const gchar *gs_ubuntuone_dialog_get_token_secret (GsUbuntuoneDialog *dialog);
+
+G_END_DECLS
+
+#endif /* GS_UBUNTUONE_DIALOG_H */
+
+/* vim: set noexpandtab: */
diff --git a/src/plugins/gs-ubuntuone-dialog.ui b/src/plugins/gs-ubuntuone-dialog.ui
new file mode 100644
index 0000000..e61c09e
--- /dev/null
+++ b/src/plugins/gs-ubuntuone-dialog.ui
@@ -0,0 +1,386 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.19.0 -->
+<interface>
+ <requires lib="gtk+" version="3.16"/>
+ <template class="GsUbuntuoneDialog" parent="GtkDialog">
+ <action-widgets>
+ <action-widget response="cancel">cancel_button</action-widget>
+ </action-widgets>
+ <child internal-child="headerbar">
+ <object class="GtkHeaderBar">
+ <property name="show_close_button">False</property>
+ <child>
+ <object class="GtkButton" id="cancel_button">
+ <property name="label" translatable="yes">_Cancel</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_underline">True</property>
+ </object>
+ <packing>
+ <property name="pack-type">start</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="next_button">
+ <property name="label" translatable="yes">_Continue</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="can_default">True</property>
+ <property name="receives_default">True</property>
+ <property name="use_underline">True</property>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack-type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child internal-child="vbox">
+ <object class="GtkBox" id="content_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">20</property>
+ <property name="margin_right">20</property>
+ <property name="margin_top">20</property>
+ <property name="margin_bottom">20</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">40</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">20</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="yalign">0</property>
+ <property name="resource">/org/gnome/Software/plugins/ubuntu-one.png</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkStack" id="page_stack">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkGrid" id="page-0">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkLabel" id="prompt_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_bottom">20</property>
+ <property name="label" translatable="yes">To rate and review software, you need an
Ubuntu Single Sign-On account.</property>
+ <property name="wrap">True</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkAccelLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">end</property>
+ <property name="margin_right">10</property>
+ <property name="margin_bottom">20</property>
+ <property name="label" translatable="yes">_Email address:</property>
+ <property name="use_underline">True</property>
+ <property name="xalign">1</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="email_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="margin_bottom">20</property>
+ <property name="hexpand">True</property>
+ <property name="input_purpose">email</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioButton" id="login_radio">
+ <property name="label" translatable="yes">I have an Ubuntu Single Sign-On
account</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="margin_bottom">5</property>
+ <property name="xalign">0</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ <property name="width">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkAccelLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">end</property>
+ <property name="margin_left">25</property>
+ <property name="margin_right">10</property>
+ <property name="margin_bottom">5</property>
+ <property name="label" translatable="yes">_Password:</property>
+ <property name="use_underline">True</property>
+ <property name="xalign">1</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="password_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="margin_bottom">5</property>
+ <property name="hexpand">True</property>
+ <property name="visibility">False</property>
+ <property name="invisible_char">•</property>
+ <property name="input_purpose">password</property>
+ <property name="activates_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="remember_check">
+ <property name="label" translatable="yes">Sign in automatically next time</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="margin_bottom">20</property>
+ <property name="hexpand">True</property>
+ <property name="xalign">0</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioButton" id="register_radio">
+ <property name="label" translatable="yes">I want to register for an account
now</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="margin_bottom">20</property>
+ <property name="xalign">0</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ <property name="group">login_radio</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">5</property>
+ <property name="width">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioButton" id="reset_radio">
+ <property name="label" translatable="yes">I've forgotten my password</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="xalign">0</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ <property name="group">login_radio</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">6</property>
+ <property name="width">2</property>
+ </packing>
+ </child>
+ <child>
+ <placeholder/>
+ </child>
+ </object>
+ <packing>
+ <property name="name">page-0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkGrid" id="page-1">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_bottom">20</property>
+ <property name="label" translatable="yes">Enter your one-time password for
two-factor authentication.</property>
+ <property name="wrap">True</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ <property name="width">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkAccelLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_right">10</property>
+ <property name="label" translatable="yes">One-time password:</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="passcode_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hexpand">True</property>
+ <property name="input_purpose">pin</property>
+ <property name="activates_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="name">page-1</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkGrid" id="page-2">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">You are now signed into Ubuntu
One.</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="name">page-2</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">10</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkStack" id="status_stack">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_right">5</property>
+ <child>
+ <object class="GtkImage" id="status_image">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="name">status-image</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinner" id="status_spinner">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="active">True</property>
+ </object>
+ <packing>
+ <property name="name">status-spinner</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="status_label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/src/plugins/gs-ubuntuone.c b/src/plugins/gs-ubuntuone.c
new file mode 100644
index 0000000..3c7b582
--- /dev/null
+++ b/src/plugins/gs-ubuntuone.c
@@ -0,0 +1,410 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Canonical Ltd.
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <config.h>
+
+#include <libsecret/secret.h>
+
+#include <gs-plugin.h>
+
+#include "gs-ubuntuone.h"
+#include "gs-ubuntuone-dialog.h"
+
+#define SCHEMA_NAME "com.ubuntu.UbuntuOne.GnomeSoftware"
+#define MACAROON "macaroon"
+#define CONSUMER_KEY "consumer-key"
+#define CONSUMER_SECRET "consumer-secret"
+#define TOKEN_KEY "token-key"
+#define TOKEN_SECRET "token-secret"
+
+static SecretSchema schema = {
+ SCHEMA_NAME,
+ SECRET_SCHEMA_NONE,
+ { { "key", SECRET_SCHEMA_ATTRIBUTE_STRING } }
+};
+
+typedef struct
+{
+ GError **error;
+
+ GCond cond;
+ GMutex mutex;
+
+ gboolean get_macaroon;
+
+ gboolean done;
+ gboolean success;
+ gboolean remember;
+
+ GVariant *macaroon;
+ gchar *consumer_key;
+ gchar *consumer_secret;
+ gchar *token_key;
+ gchar *token_secret;
+} LoginContext;
+
+static gboolean
+show_login_dialog (gpointer user_data)
+{
+ LoginContext *context = user_data;
+ GtkWidget *dialog;
+
+ dialog = gs_ubuntuone_dialog_new (context->get_macaroon);
+
+ switch (gtk_dialog_run (GTK_DIALOG (dialog))) {
+ case GTK_RESPONSE_DELETE_EVENT:
+ case GTK_RESPONSE_CANCEL:
+ if (context->get_macaroon) {
+ g_set_error (context->error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Unable to obtain snapd macaroon");
+ } else {
+ g_set_error (context->error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Unable to sign into Ubuntu One");
+ }
+
+ context->success = FALSE;
+ break;
+
+ case GTK_RESPONSE_OK:
+ context->remember = gs_ubuntuone_dialog_get_do_remember (GS_UBUNTUONE_DIALOG (dialog));
+ context->macaroon = gs_ubuntuone_dialog_get_macaroon (GS_UBUNTUONE_DIALOG (dialog));
+ context->consumer_key = g_strdup (gs_ubuntuone_dialog_get_consumer_key (GS_UBUNTUONE_DIALOG
(dialog)));
+ context->consumer_secret = g_strdup (gs_ubuntuone_dialog_get_consumer_secret
(GS_UBUNTUONE_DIALOG (dialog)));
+ context->token_key = g_strdup (gs_ubuntuone_dialog_get_token_key (GS_UBUNTUONE_DIALOG
(dialog)));
+ context->token_secret = g_strdup (gs_ubuntuone_dialog_get_token_secret (GS_UBUNTUONE_DIALOG
(dialog)));
+ context->success = TRUE;
+
+ if (context->macaroon != NULL)
+ g_variant_ref (context->macaroon);
+
+ break;
+ }
+
+ gtk_widget_destroy (dialog);
+
+ g_mutex_lock (&context->mutex);
+ context->done = TRUE;
+ g_cond_signal (&context->cond);
+ g_mutex_unlock (&context->mutex);
+
+ return G_SOURCE_REMOVE;
+}
+
+GVariant *
+gs_ubuntuone_get_macaroon (gboolean use_cache,
+ gboolean show_dialog,
+ GError **error)
+{
+ LoginContext login_context = { 0 };
+ g_autofree gchar *password = NULL;
+ g_autofree gchar *printed = NULL;
+ GVariant *macaroon = NULL;
+ GError *error_local = NULL;
+
+ if (use_cache) {
+ password = secret_password_lookup_sync (&schema,
+ NULL,
+ &error_local,
+ "key", MACAROON,
+ NULL);
+
+ if (password) {
+ macaroon = g_variant_parse (G_VARIANT_TYPE ("(sas)"),
+ password,
+ NULL,
+ NULL,
+ &error_local);
+
+ if (macaroon)
+ return macaroon;
+
+ g_warning ("could not parse macaroon: %s", error_local->message);
+ g_clear_error (&error_local);
+ } else if (error_local != NULL) {
+ g_warning ("could not lookup cached macaroon: %s", error_local->message);
+ g_clear_error (&error_local);
+ }
+ }
+
+ if (show_dialog) {
+ /* Pop up a login dialog */
+ login_context.error = error;
+ login_context.get_macaroon = TRUE;
+ g_cond_init (&login_context.cond);
+ g_mutex_init (&login_context.mutex);
+ g_mutex_lock (&login_context.mutex);
+
+ gdk_threads_add_idle (show_login_dialog, &login_context);
+
+ while (!login_context.done)
+ g_cond_wait (&login_context.cond, &login_context.mutex);
+
+ g_mutex_unlock (&login_context.mutex);
+ g_mutex_clear (&login_context.mutex);
+ g_cond_clear (&login_context.cond);
+
+ if (login_context.macaroon != NULL && login_context.remember) {
+ printed = g_variant_print (login_context.macaroon, FALSE);
+
+ if (!secret_password_store_sync (&schema,
+ NULL,
+ SCHEMA_NAME,
+ printed,
+ NULL,
+ &error_local,
+ "key", MACAROON,
+ NULL)) {
+ g_warning ("could not store macaroon: %s", error_local->message);
+ g_clear_error (&error_local);
+ }
+ }
+
+ return login_context.macaroon;
+ }
+
+ return NULL;
+}
+
+void
+gs_ubuntuone_clear_macaroon (void)
+{
+ secret_password_clear_sync (&schema, NULL, NULL, "key", MACAROON, NULL);
+}
+
+typedef struct
+{
+ GCancellable *cancellable;
+ GCond cond;
+ GMutex mutex;
+
+ gint waiting;
+
+ gchar *consumer_key;
+ gchar *consumer_secret;
+ gchar *token_key;
+ gchar *token_secret;
+} SecretContext;
+
+static void
+lookup_consumer_key (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ SecretContext *context = user_data;
+
+ context->consumer_key = secret_password_lookup_finish (result, NULL);
+
+ g_mutex_lock (&context->mutex);
+
+ context->waiting--;
+
+ g_cond_signal (&context->cond);
+ g_mutex_unlock (&context->mutex);
+}
+
+static void
+lookup_consumer_secret (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ SecretContext *context = user_data;
+
+ context->consumer_secret = secret_password_lookup_finish (result, NULL);
+
+ g_mutex_lock (&context->mutex);
+
+ context->waiting--;
+
+ g_cond_signal (&context->cond);
+ g_mutex_unlock (&context->mutex);
+}
+
+static void
+lookup_token_key (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ SecretContext *context = user_data;
+
+ context->token_key = secret_password_lookup_finish (result, NULL);
+
+ g_mutex_lock (&context->mutex);
+
+ context->waiting--;
+
+ g_cond_signal (&context->cond);
+ g_mutex_unlock (&context->mutex);
+}
+
+static void
+lookup_token_secret (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ SecretContext *context = user_data;
+
+ context->token_secret = secret_password_lookup_finish (result, NULL);
+
+ g_mutex_lock (&context->mutex);
+
+ context->waiting--;
+
+ g_cond_signal (&context->cond);
+ g_mutex_unlock (&context->mutex);
+}
+
+gboolean
+gs_ubuntuone_get_credentials (gchar **consumer_key, gchar **consumer_secret, gchar **token_key, gchar
**token_secret)
+{
+ SecretContext secret_context = { 0 };
+
+ /* Use credentials from libsecret if available */
+ secret_context.waiting = 4;
+ secret_context.cancellable = g_cancellable_new ();
+ g_cond_init (&secret_context.cond);
+ g_mutex_init (&secret_context.mutex);
+ g_mutex_lock (&secret_context.mutex);
+
+ secret_password_lookup (&schema,
+ secret_context.cancellable,
+ lookup_consumer_key,
+ &secret_context,
+ "key", CONSUMER_KEY,
+ NULL);
+ secret_password_lookup (&schema,
+ secret_context.cancellable,
+ lookup_consumer_secret,
+ &secret_context,
+ "key", CONSUMER_SECRET,
+ NULL);
+ secret_password_lookup (&schema,
+ secret_context.cancellable,
+ lookup_token_key,
+ &secret_context,
+ "key", TOKEN_KEY,
+ NULL);
+ secret_password_lookup (&schema,
+ secret_context.cancellable,
+ lookup_token_secret,
+ &secret_context,
+ "key", TOKEN_SECRET,
+ NULL);
+
+ while (secret_context.waiting > 0)
+ g_cond_wait (&secret_context.cond, &secret_context.mutex);
+
+ g_mutex_unlock (&secret_context.mutex);
+ g_mutex_clear (&secret_context.mutex);
+ g_cond_clear (&secret_context.cond);
+ g_cancellable_cancel (secret_context.cancellable);
+ g_clear_object (&secret_context.cancellable);
+
+ if (secret_context.consumer_key != NULL &&
+ secret_context.consumer_secret != NULL &&
+ secret_context.token_key != NULL &&
+ secret_context.token_secret != NULL) {
+ *consumer_key = secret_context.consumer_key;
+ *consumer_secret = secret_context.consumer_secret;
+ *token_key = secret_context.token_key;
+ *token_secret = secret_context.token_secret;
+ return TRUE;
+ }
+
+ g_free (secret_context.token_secret);
+ g_free (secret_context.token_key);
+ g_free (secret_context.consumer_secret);
+ g_free (secret_context.consumer_key);
+ return FALSE;
+}
+
+gboolean
+gs_ubuntuone_sign_in (gchar **consumer_key, gchar **consumer_secret, gchar **token_key, gchar
**token_secret, GError **error)
+{
+ LoginContext login_context = { 0 };
+
+ /* Pop up a login dialog */
+ login_context.error = error;
+ login_context.get_macaroon = FALSE;
+ g_cond_init (&login_context.cond);
+ g_mutex_init (&login_context.mutex);
+ g_mutex_lock (&login_context.mutex);
+
+ gdk_threads_add_idle (show_login_dialog, &login_context);
+
+ while (!login_context.done)
+ g_cond_wait (&login_context.cond, &login_context.mutex);
+
+ g_mutex_unlock (&login_context.mutex);
+ g_mutex_clear (&login_context.mutex);
+ g_cond_clear (&login_context.cond);
+
+ if (login_context.remember) {
+ secret_password_store (&schema,
+ NULL,
+ SCHEMA_NAME,
+ login_context.consumer_key,
+ NULL,
+ NULL,
+ NULL,
+ "key", CONSUMER_KEY,
+ NULL);
+
+ secret_password_store (&schema,
+ NULL,
+ SCHEMA_NAME,
+ login_context.consumer_secret,
+ NULL,
+ NULL,
+ NULL,
+ "key", CONSUMER_SECRET,
+ NULL);
+
+ secret_password_store (&schema,
+ NULL,
+ SCHEMA_NAME,
+ login_context.token_key,
+ NULL,
+ NULL,
+ NULL,
+ "key", TOKEN_KEY,
+ NULL);
+
+ secret_password_store (&schema,
+ NULL,
+ SCHEMA_NAME,
+ login_context.token_secret,
+ NULL,
+ NULL,
+ NULL,
+ "key", TOKEN_SECRET,
+ NULL);
+ }
+
+ *consumer_key = login_context.consumer_key;
+ *consumer_secret = login_context.consumer_secret;
+ *token_key = login_context.token_key;
+ *token_secret = login_context.token_secret;
+ return login_context.success;
+}
diff --git a/src/plugins/gs-ubuntuone.h b/src/plugins/gs-ubuntuone.h
new file mode 100644
index 0000000..4e9fde8
--- /dev/null
+++ b/src/plugins/gs-ubuntuone.h
@@ -0,0 +1,49 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Canonical Ltd.
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef __GS_UBUNTUONE_H
+#define __GS_UBUNTUONE_H
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+GVariant *gs_ubuntuone_get_macaroon (gboolean use_cache,
+ gboolean show_dialog,
+ GError **error);
+
+void gs_ubuntuone_clear_macaroon (void);
+
+gboolean gs_ubuntuone_get_credentials (gchar **consumer_key,
+ gchar **consumer_secret,
+ gchar **token_key,
+ gchar **token_secret);
+
+gboolean gs_ubuntuone_sign_in (gchar **consumer_key,
+ gchar **consumer_secret,
+ gchar **token_key,
+ gchar **token_secret,
+ GError **error);
+
+G_END_DECLS
+
+#endif /* __GS_UBUNTUONE_H */
+
diff --git a/src/plugins/ubuntu-one.png b/src/plugins/ubuntu-one.png
new file mode 100644
index 0000000..a58248a
Binary files /dev/null and b/src/plugins/ubuntu-one.png differ
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]