[gnome-software/wip/rancell/ubuntu-3-20-rebase: 4/37] Allow logging into Ubuntu One for reviews
- From: Robert Ancell <rancell src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-software/wip/rancell/ubuntu-3-20-rebase: 4/37] Allow logging into Ubuntu One for reviews
- Date: Sat, 17 Jun 2017 09:25:52 +0000 (UTC)
commit 5551f046bdc68d13b925f9e72992168e5bf39ed3
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 | 489 ++++++++++++++++++++++++++--
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, 1950 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 5ea32be..684375c 100644
--- a/src/plugins/Makefile.am
+++ b/src/plugins/Makefile.am
@@ -168,8 +168,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)
@@ -244,6 +256,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 16adcfd..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,9 @@ 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;
}
@@ -101,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
@@ -182,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,
@@ -191,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 */
@@ -211,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;
@@ -300,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;
@@ -322,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,
@@ -363,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 */
@@ -510,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;
@@ -523,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);
@@ -550,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);
}
@@ -573,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;
@@ -581,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 */
@@ -640,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)
@@ -652,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;
@@ -689,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]