[gnome-software/wip/rancell/ubuntu-3-20-rebase: 8/13] Allow logging into Ubuntu One for reviews



commit cd9c456ab52e90456c632a31448ae004b49e38d8
Author: Robert Ancell <robert ancell canonical com>
Date:   Sat Jun 17 12:50:45 2017 +1200

    Allow logging into Ubuntu One for reviews

 po/POTFILES.in                         |    2 +
 src/gnome-software.gresource.xml       |    2 +
 src/plugins/Makefile.am                |   18 +-
 src/plugins/gs-plugin-ubuntu-reviews.c |  501 +++++++++++++++++++++++++++--
 src/plugins/gs-ubuntuone-dialog.c      |  563 ++++++++++++++++++++++++++++++++
 src/plugins/gs-ubuntuone-dialog.h      |   45 +++
 src/plugins/gs-ubuntuone-dialog.ui     |  386 ++++++++++++++++++++++
 src/plugins/gs-ubuntuone.c             |  419 ++++++++++++++++++++++++
 src/plugins/gs-ubuntuone.h             |   51 +++
 src/plugins/ubuntu-one.png             |  Bin 0 -> 2540 bytes
 10 files changed, 1962 insertions(+), 25 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index b32efd6..afd5edb 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -62,5 +62,7 @@ src/plugins/gs-plugin-epiphany.c
 src/plugins/gs-plugin-moduleset.c
 src/plugins/gs-plugin-packagekit.c
 src/plugins/gs-plugin-packagekit-refine.c
+src/plugins/gs-ubuntuone-dialog.c
+[type: gettext/glade]src/plugins/gs-ubuntuone-dialog.ui
 src/plugins/menu-spec-common.c
 [type: gettext/glade]src/popular-tile.ui
diff --git a/src/gnome-software.gresource.xml b/src/gnome-software.gresource.xml
index 9c29e61..c567d48 100644
--- a/src/gnome-software.gresource.xml
+++ b/src/gnome-software.gresource.xml
@@ -32,5 +32,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/plugins/Makefile.am b/src/plugins/Makefile.am
index ba6da26..f02d101 100644
--- a/src/plugins/Makefile.am
+++ b/src/plugins/Makefile.am
@@ -174,8 +174,20 @@ libgs_plugin_hardcoded_blacklist_la_LDFLAGS = -module -avoid-version
 libgs_plugin_hardcoded_blacklist_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
 
 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-snapd.h                                      \
+       gs-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)
 
@@ -250,6 +262,6 @@ gs_self_test_CFLAGS = $(WARN_CFLAGS)
 
 TESTS = gs-self-test
 
-EXTRA_DIST = moduleset-test.xml
+EXTRA_DIST = moduleset-test.xml gs-ubuntuone-dialog.h gs-ubuntuone-dialog.ui ubuntu-one.png
 
 -include $(top_srcdir)/git.mk
diff --git a/src/plugins/gs-plugin-ubuntu-reviews.c b/src/plugins/gs-plugin-ubuntu-reviews.c
index 43c9ee5..79f3556 100644
--- a/src/plugins/gs-plugin-ubuntu-reviews.c
+++ b/src/plugins/gs-plugin-ubuntu-reviews.c
@@ -25,17 +25,23 @@
 #include <math.h>
 #include <libsoup/soup.h>
 #include <json-glib/json-glib.h>
+#include <oauth.h>
 #include <sqlite3.h>
 
 #include <gs-plugin.h>
 #include <gs-utils.h>
 
+#include "gs-ubuntuone.h"
 #include "gs-os-release.h"
 
 struct GsPluginPrivate {
        gchar           *db_path;
        sqlite3         *db;
        gsize            db_loaded;
+       gchar           *consumer_key;
+       gchar           *consumer_secret;
+       gchar           *token_key;
+       gchar           *token_secret;
 };
 
 typedef struct {
@@ -58,6 +64,9 @@ gs_plugin_get_name (void)
 // 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)
 {
@@ -80,7 +89,21 @@ gs_plugin_initialize (GsPlugin *plugin)
 const gchar **
 gs_plugin_order_after (GsPlugin *plugin)
 {
-       static const gchar *deps[] = { NULL };
+       static const gchar *deps[] = {
+               "appstream",
+               NULL };
+       return deps;
+}
+
+/**
+ * gs_plugin_get_conflicts:
+ */
+const gchar **
+gs_plugin_get_conflicts (GsPlugin *plugin)
+{
+       static const gchar *deps[] = {
+               "odrs",
+               NULL };
        return deps;
 }
 
@@ -89,7 +112,12 @@ gs_plugin_destroy (GsPlugin *plugin)
 {
        GsPluginPrivate *priv = plugin->priv;
 
+       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);
 }
 
 static gint
@@ -170,6 +198,65 @@ get_review_stats_sqlite_cb (void *data,
        return 0;
 }
 
+static gdouble
+pnormaldist (gdouble qn)
+{
+       static gdouble b[11] = { 1.570796288,      0.03706987906,   -0.8364353589e-3,
+                               -0.2250947176e-3,  0.6841218299e-5,  0.5824238515e-5,
+                               -0.104527497e-5,   0.8360937017e-7, -0.3231081277e-8,
+                                0.3657763036e-10, 0.6936233982e-12 };
+       gdouble w1, w3;
+       int i;
+
+       if (qn < 0 || qn > 1)
+               return 0; // This is an error case
+       if (qn == 0.5)
+               return 0;
+
+       w1 = qn;
+       if (qn > 0.5)
+               w1 = 1.0 - w1;
+       w3 = -log (4.0 * w1 * (1.0 - w1));
+       w1 = b[0];
+       for (i = 1; i < 11; i++)
+               w1 = w1 + (b[i] * pow (w3, i));
+
+       if (qn > 0.5)
+               return sqrt (w1 * w3);
+       else
+               return -sqrt (w1 * w3);
+}
+
+static gdouble
+wilson_score (gdouble value, gint n, gdouble power)
+{
+       gdouble z, phat;
+
+       if (value == 0)
+               return 0;
+
+       z = pnormaldist (1 - power / 2);
+       phat = value / n;
+       return (phat + z * z / (2 * n) - z * sqrt ((phat * (1 - phat) + z * z / (4 * n)) / n)) / (1 + z * z / 
n);
+}
+
+static gint
+get_rating (gint64 one_star_count, gint64 two_star_count, gint64 three_star_count, gint64 four_star_count, 
gint64 five_star_count)
+{
+       gint n_ratings;
+
+       n_ratings = one_star_count + two_star_count + three_star_count + four_star_count + five_star_count;
+       if (n_ratings == 0)
+               return -1;
+
+       // Use a Wilson score which is a method of ensuring small numbers of ratings don't give high scores
+       // https://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval
+       return (((wilson_score (one_star_count, n_ratings, 0.1) * -2) +
+                (wilson_score (two_star_count, n_ratings, 0.1) * -1) +
+                (wilson_score (four_star_count, n_ratings, 0.1) * 1) +
+                (wilson_score (five_star_count, n_ratings, 0.1) * 2)) + 3) * 20 - 10;
+}
+
 static gboolean
 get_review_stats (GsPlugin *plugin,
                  const gchar *package_name,
@@ -179,7 +266,7 @@ get_review_stats (GsPlugin *plugin,
 {
        Histogram histogram = { 0, 0, 0, 0, 0 };
        gchar *error_msg = NULL;
-       gint result, n_ratings;
+       gint result;
        g_autofree gchar *statement = NULL;
 
        /* Get histogram from the database */
@@ -199,13 +286,7 @@ get_review_stats (GsPlugin *plugin,
                return FALSE;
        }
 
-       /* Convert to a rating */
-       // FIXME: Convert to a Wilson score
-       n_ratings = histogram.one_star_count + histogram.two_star_count + histogram.three_star_count + 
histogram.four_star_count + histogram.five_star_count;
-       if (n_ratings == 0)
-               *rating = -1;
-       else
-               *rating = ((histogram.one_star_count * 20) + (histogram.two_star_count * 40) + 
(histogram.three_star_count * 60) + (histogram.four_star_count * 80) + (histogram.five_star_count * 100)) / 
n_ratings;
+       *rating = get_rating (histogram.one_star_count, histogram.two_star_count, histogram.three_star_count, 
histogram.four_star_count, histogram.five_star_count);
        review_ratings[0] = 0;
        review_ratings[1] = histogram.one_star_count;
        review_ratings[2] = histogram.two_star_count;
@@ -288,9 +369,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)
 {
+       GsPluginPrivate *priv = plugin->priv;
        g_autofree gchar *uri = NULL;
        g_autoptr(SoupMessage) msg = NULL;
        guint status_code;
@@ -310,6 +416,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 (plugin->soup_session, msg);
        if (status_code != SOUP_STATUS_OK) {
                g_set_error (error,
@@ -351,7 +465,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 */
@@ -498,8 +612,9 @@ parse_date_time (const gchar *text)
 }
 
 static GsReview *
-parse_review (JsonNode *node)
+parse_review (GsPlugin *plugin, JsonNode *node)
 {
+       GsPluginPrivate *priv = plugin->priv;
        GsReview *review;
        JsonObject *object;
        gint64 star_rating;
@@ -511,13 +626,15 @@ 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"));
        gs_review_set_version (review, json_object_get_string_member (object, "version"));
        star_rating = json_object_get_int_member (object, "rating");
        if (star_rating > 0)
-               gs_review_set_rating (review, star_rating * 20);
+               gs_review_set_rating (review, star_rating * 20 - 10);
        gs_review_set_date (review, parse_date_time (json_object_get_string_member (object, "date_created")));
        id_string = g_strdup_printf ("%" G_GINT64_FORMAT, json_object_get_int_member (object, "id"));
        gs_review_add_metadata (review, "ubuntu-id", id_string);
@@ -538,7 +655,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);
        }
@@ -561,7 +678,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;
@@ -569,8 +686,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 */
@@ -628,10 +745,44 @@ refine_rating (GsPlugin *plugin, GsApp *app, GError **error)
 }
 
 static gboolean
+get_ubuntuone_credentials (GsPlugin  *plugin,
+                          gboolean   required,
+                          GError   **error)
+{
+       GsPluginPrivate *priv = plugin->priv;
+
+       /* 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)
@@ -640,12 +791,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;
@@ -677,3 +831,306 @@ gs_plugin_refine (GsPlugin *plugin,
        return ret;
 }
 
+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_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_id = gs_os_release_get_id (error);
+       if (os_id == NULL)
+               return FALSE;
+       os_ubuntu_codename = gs_os_release_get ("UBUNTU_CODENAME", error);
+       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)
+{
+       /* Load database once */
+       if (g_once_init_enter (&plugin->priv->db_loaded)) {
+               gboolean ret = load_database (plugin, error);
+               g_once_init_leave (&plugin->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,
+                      GList **list,
+                      GCancellable *cancellable,
+                      GError **error)
+{
+       gint result;
+       GList *entries = NULL, *link;
+       char *error_msg = NULL;
+
+       /* Load database once */
+       if (g_once_init_enter (&plugin->priv->db_loaded)) {
+               gboolean ret = load_database (plugin, error);
+               g_once_init_leave (&plugin->priv->db_loaded, TRUE);
+               if (!ret)
+                       return FALSE;
+       }
+
+       result = sqlite3_exec (plugin->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_plugin_add_app (list, app);
+       }
+       g_list_free_full (entries, free_popular_entry);
+
+       return TRUE;
+}
diff --git a/src/plugins/gs-ubuntuone-dialog.c b/src/plugins/gs-ubuntuone-dialog.c
new file mode 100644
index 0000000..3814753
--- /dev/null
+++ b/src/plugins/gs-ubuntuone-dialog.c
@@ -0,0 +1,563 @@
+/* -*- 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-utils.h"
+
+#include <glib/gi18n.h>
+#include <json-glib/json-glib.h>
+#include <libsoup/soup.h>
+
+#ifdef USE_SNAPD
+#include <snapd-glib/snapd-glib.h>
+#include "gs-snapd.h"
+#endif
+
+#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
+send_login_request (GsUbuntuoneDialog *self)
+{
+       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) {
+#ifdef USE_SNAPD
+               const gchar *username, *password, *otp;
+               g_autoptr(SnapdAuthData) auth_data = NULL;
+               g_autoptr(GError) error = NULL;
+
+               username = gtk_entry_get_text (GTK_ENTRY (self->email_entry));
+               password = gtk_entry_get_text (GTK_ENTRY (self->password_entry));
+               otp = gtk_entry_get_text (GTK_ENTRY (self->passcode_entry));
+               if (otp[0] == '\0')
+                       otp = NULL;
+
+               auth_data = snapd_login_sync (username, password, otp, NULL, &error);
+               reenable_widgets (self);
+               if (auth_data != NULL) {
+                       self->macaroon = g_variant_ref_sink (g_variant_new ("(s^as)", 
snapd_auth_data_get_macaroon (auth_data), snapd_auth_data_get_discharges (auth_data)));
+                       gtk_stack_set_visible_child_name (GTK_STACK (self->page_stack), "page-2");
+                       update_widgets (self);
+               } else {
+                       if (g_error_matches (error, SNAPD_ERROR, SNAPD_ERROR_AUTH_DATA_INVALID) ||
+                           g_error_matches (error, SNAPD_ERROR, SNAPD_ERROR_AUTH_DATA_REQUIRED)) {
+                               show_status (self, _("Incorrect email or password"), TRUE);
+                               gtk_widget_grab_focus (self->password_entry);
+                       } else if (g_error_matches (error, SNAPD_ERROR, SNAPD_ERROR_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 if (g_error_matches (error, SNAPD_ERROR, SNAPD_ERROR_TWO_FACTOR_INVALID)) {
+                               show_status (self, _("Two-factor authentication failed"), TRUE);
+                               gtk_widget_grab_focus (self->passcode_entry);
+                       } else {
+                               show_status (self, _("An error occurred"), TRUE);
+                               gtk_widget_grab_focus (self->password_entry);
+                       }
+               }
+#endif
+       } else {
+               GVariant *request;
+
+               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..f778eef
--- /dev/null
+++ b/src/plugins/gs-ubuntuone.c
@@ -0,0 +1,419 @@
+/* -*- 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;
+}
+
+gboolean
+gs_ubuntuone_get_macaroon (gboolean   use_cache,
+                          gboolean   show_dialog,
+                          gchar    **macaroon,
+                          gchar   ***discharges,
+                          GError   **error)
+{
+       LoginContext login_context = { 0 };
+       g_autofree gchar *password = NULL;
+       g_autofree gchar *printed = NULL;
+       GError *error_local = NULL;
+
+       if (use_cache) {
+               password = secret_password_lookup_sync (&schema,
+                                                       NULL,
+                                                       &error_local,
+                                                       "key", MACAROON,
+                                                       NULL);
+
+               if (password) {
+                       GVariant *value;
+
+                       value = g_variant_parse (G_VARIANT_TYPE ("(sas)"),
+                                                password,
+                                                NULL,
+                                                NULL,
+                                                &error_local);
+
+                       if (value != NULL) {
+                               g_variant_get (value, "(s^as)", macaroon, discharges);
+                               g_variant_unref (value);
+                               return TRUE;
+                       }
+
+                       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);
+                       }
+               }
+
+               g_variant_get (login_context.macaroon, "(s^as)", macaroon, discharges);
+               g_variant_unref (login_context.macaroon);
+
+               return TRUE;
+       }
+
+       return FALSE;
+}
+
+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..b3ca792
--- /dev/null
+++ b/src/plugins/gs-ubuntuone.h
@@ -0,0 +1,51 @@
+/* -*- 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
+
+gboolean        gs_ubuntuone_get_macaroon      (gboolean         use_cache,
+                                                gboolean         show_dialog,
+                                                gchar          **macaroon,
+                                                gchar         ***discharges,
+                                                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]