[gnome-software/wip/rancell/ubuntuone: 18/18] Add UbuntuOne plugin to authorize Ubuntu reviews



commit ff663d00b04b7a8b743ed9f2645a9c770ac5f208
Author: Robert Ancell <robert ancell canonical com>
Date:   Wed Jun 29 16:37:38 2016 +1200

    Add UbuntuOne plugin to authorize Ubuntu reviews

 configure.ac                           |    2 +
 src/gs-os-release.c                    |   21 ++
 src/gs-os-release.h                    |   13 +-
 src/gs-plugin.h                        |   22 +-
 src/plugins/Makefile.am                |   17 ++-
 src/plugins/gs-plugin-ubuntu-reviews.c |  408 +++++++++++++++++++++++++++++---
 src/plugins/gs-plugin-ubuntuone.c      |  212 +++++++++++++++++
 7 files changed, 649 insertions(+), 46 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 7f2133e..1417c1d 100644
--- a/configure.ac
+++ b/configure.ac
@@ -68,6 +68,8 @@ PKG_CHECK_MODULES(SQLITE, sqlite3)
 PKG_CHECK_MODULES(SOUP, libsoup-2.4 >= 2.51.92)
 PKG_CHECK_MODULES(GSETTINGS_DESKTOP_SCHEMAS, gsettings-desktop-schemas >= 3.11.5)
 PKG_CHECK_MODULES(GNOME_DESKTOP, gnome-desktop-3.0 >= 3.17.92)
+PKG_CHECK_MODULES(OAUTH, oauth)
+PKG_CHECK_MODULES(LIBSECRET, libsecret-1)
 AC_PATH_PROG(APPSTREAM_UTIL, [appstream-util], [unfound])
 AC_ARG_ENABLE(man,
               [AS_HELP_STRING([--enable-man],
diff --git a/src/gs-os-release.c b/src/gs-os-release.c
index c92d364..3559e5a 100644
--- a/src/gs-os-release.c
+++ b/src/gs-os-release.c
@@ -45,6 +45,7 @@ struct _GsOsRelease
        gchar                   *id;
        gchar                   *version_id;
        gchar                   *pretty_name;
+       gchar                   *ubuntu_codename;
 };
 
 static void gs_os_release_initable_iface_init (GInitableIface *iface);
@@ -116,6 +117,10 @@ gs_os_release_initable_init (GInitable *initable,
                        os_release->pretty_name = g_strdup (tmp);
                        continue;
                }
+               if (g_strcmp0 (lines[i], "UBUNTU_CODENAME") == 0) {
+                       os_release->ubuntu_codename = g_strdup (tmp);
+                       continue;
+               }
        }
        return TRUE;
 }
@@ -195,6 +200,21 @@ gs_os_release_get_pretty_name (GsOsRelease *os_release)
        return os_release->pretty_name;
 }
 
+/**
+ * gs_os_release_get_ubuntu_codename:
+ * @os_release: A #GsOsRelease
+ *
+ * Gets the Ubuntu codename from the os-release parser.
+ *
+ * Returns: a string, or %NULL
+ **/
+const gchar *
+gs_os_release_get_ubuntu_codename (GsOsRelease *os_release)
+{
+       g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL);
+       return os_release->ubuntu_codename;
+}
+
 static void
 gs_os_release_finalize (GObject *object)
 {
@@ -204,6 +224,7 @@ gs_os_release_finalize (GObject *object)
        g_free (os_release->id);
        g_free (os_release->version_id);
        g_free (os_release->pretty_name);
+       g_free (os_release->ubuntu_codename);
        G_OBJECT_CLASS (gs_os_release_parent_class)->finalize (object);
 }
 
diff --git a/src/gs-os-release.h b/src/gs-os-release.h
index ffd338e..d7e951b 100644
--- a/src/gs-os-release.h
+++ b/src/gs-os-release.h
@@ -33,12 +33,13 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (GsOsRelease, gs_os_release, GS, OS_RELEASE, GObject)
 
-GsOsRelease    *gs_os_release_new              (GError         **error);
-const gchar    *gs_os_release_get_name         (GsOsRelease    *os_release);
-const gchar    *gs_os_release_get_version      (GsOsRelease    *os_release);
-const gchar    *gs_os_release_get_id           (GsOsRelease    *os_release);
-const gchar    *gs_os_release_get_version_id   (GsOsRelease    *os_release);
-const gchar    *gs_os_release_get_pretty_name  (GsOsRelease    *os_release);
+GsOsRelease    *gs_os_release_new                      (GError         **error);
+const gchar    *gs_os_release_get_name                 (GsOsRelease    *os_release);
+const gchar    *gs_os_release_get_version              (GsOsRelease    *os_release);
+const gchar    *gs_os_release_get_id                   (GsOsRelease    *os_release);
+const gchar    *gs_os_release_get_version_id           (GsOsRelease    *os_release);
+const gchar    *gs_os_release_get_pretty_name          (GsOsRelease    *os_release);
+const gchar    *gs_os_release_get_ubuntu_codename      (GsOsRelease    *os_release);
 
 G_END_DECLS
 
diff --git a/src/gs-plugin.h b/src/gs-plugin.h
index 646157f..28347b9 100644
--- a/src/gs-plugin.h
+++ b/src/gs-plugin.h
@@ -100,15 +100,17 @@ typedef enum {
 
 /**
  * GsPluginError:
- * @GS_PLUGIN_ERROR_FAILED:            Generic failure
- * @GS_PLUGIN_ERROR_NOT_SUPPORTED:     Action not supported
- * @GS_PLUGIN_ERROR_CANCELLED:         Action was cancelled
- * @GS_PLUGIN_ERROR_NO_NETWORK:                No network connection available
- * @GS_PLUGIN_ERROR_NO_SECURITY:       Security policy forbid action
- * @GS_PLUGIN_ERROR_NO_SPACE:          No disk space to allow action
- * @GS_PLUGIN_ERROR_AUTH_REQUIRED:     Authentication was required
- * @GS_PLUGIN_ERROR_AUTH_INVALID:      Provided authentication was invalid
- * @GS_PLUGIN_ERROR_PIN_REQUIRED:      PIN required for authentication
+ * @GS_PLUGIN_ERROR_FAILED:                    Generic failure
+ * @GS_PLUGIN_ERROR_NOT_SUPPORTED:             Action not supported
+ * @GS_PLUGIN_ERROR_CANCELLED:                 Action was cancelled
+ * @GS_PLUGIN_ERROR_NO_NETWORK:                        No network connection available
+ * @GS_PLUGIN_ERROR_NO_SECURITY:               Security policy forbid action
+ * @GS_PLUGIN_ERROR_NO_SPACE:                  No disk space to allow action
+ * @GS_PLUGIN_ERROR_AUTH_REQUIRED:             Authentication was required
+ * @GS_PLUGIN_ERROR_AUTH_INVALID:              Provided authentication was invalid
+ * @GS_PLUGIN_ERROR_PIN_REQUIRED:              PIN required for authentication
+ * @GS_PLUGIN_ERROR_ACCOUNT_SUSPENDED:         User account has been suspended
+ * @GS_PLUGIN_ERROR_ACCOUNT_DEACTIVATED:       User account has been deactivated
  *
  * The failure error types.
  **/
@@ -122,6 +124,8 @@ typedef enum {
        GS_PLUGIN_ERROR_AUTH_REQUIRED,
        GS_PLUGIN_ERROR_AUTH_INVALID,
        GS_PLUGIN_ERROR_PIN_REQUIRED,
+       GS_PLUGIN_ERROR_ACCOUNT_SUSPENDED,
+       GS_PLUGIN_ERROR_ACCOUNT_DEACTIVATED,
        /*< private >*/
        GS_PLUGIN_ERROR_LAST
 } GsPluginError;
diff --git a/src/plugins/Makefile.am b/src/plugins/Makefile.am
index c57b7d4..108a831 100644
--- a/src/plugins/Makefile.am
+++ b/src/plugins/Makefile.am
@@ -15,6 +15,8 @@ AM_CPPFLAGS =                                         \
        $(OSTREE_CFLAGS)                                \
        $(FLATPAK_CFLAGS)                               \
        $(RPM_CFLAGS)                                   \
+       $(LIBSECRET_CFLAGS)                             \
+       $(OAUTH_CFLAGS)                                 \
        -DI_KNOW_THE_GNOME_SOFTWARE_API_IS_SUBJECT_TO_CHANGE    \
        -DBINDIR=\"$(bindir)\"                          \
        -DDATADIR=\"$(datadir)\"                        \
@@ -43,7 +45,8 @@ plugin_LTLIBRARIES =                                  \
        libgs_plugin_fedora-tagger-usage.la             \
        libgs_plugin_epiphany.la                        \
        libgs_plugin_icons.la                           \
-       libgs_plugin_snap.la
+       libgs_plugin_snap.la                            \
+       libgs_plugin_ubuntuone.la
 
 if HAVE_PACKAGEKIT
 plugin_LTLIBRARIES +=                                  \
@@ -251,7 +254,7 @@ libgs_plugin_hardcoded_featured_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
 if HAVE_UBUNTU_REVIEWS
 libgs_plugin_ubuntu_reviews_la_SOURCES =               \
        gs-plugin-ubuntu-reviews.c
-libgs_plugin_ubuntu_reviews_la_LIBADD = $(GS_PLUGIN_LIBS) $(SOUP_LIBS) $(JSON_GLIB_LIBS) $(SQLITE_LIBS)
+libgs_plugin_ubuntu_reviews_la_LIBADD = $(GS_PLUGIN_LIBS) $(SOUP_LIBS) $(JSON_GLIB_LIBS) $(SQLITE_LIBS) 
$(LIBSECRET_LIBS) $(OAUTH_LIBS)
 libgs_plugin_ubuntu_reviews_la_LDFLAGS = -module -avoid-version
 libgs_plugin_ubuntu_reviews_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
 endif
@@ -344,6 +347,16 @@ libgs_plugin_snap_la_LIBADD =                              \
 libgs_plugin_snap_la_LDFLAGS = -module -avoid-version
 libgs_plugin_snap_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
 
+libgs_plugin_ubuntuone_la_SOURCES =                    \
+       gs-plugin-ubuntuone.c
+libgs_plugin_ubuntuone_la_LIBADD =                     \
+       $(GS_PLUGIN_LIBS)                               \
+       $(SOUP_LIBS)                                    \
+       $(JSON_GLIB_LIBS)                               \
+       $(LIBSECRET_LIBS)
+libgs_plugin_ubuntuone_la_LDFLAGS = -module -avoid-version
+libgs_plugin_ubuntuone_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
+
 if ENABLE_TESTS
 check_PROGRAMS =                                               \
        gs-self-test
diff --git a/src/plugins/gs-plugin-ubuntu-reviews.c b/src/plugins/gs-plugin-ubuntu-reviews.c
index ba664e0..012f465 100644
--- a/src/plugins/gs-plugin-ubuntu-reviews.c
+++ b/src/plugins/gs-plugin-ubuntu-reviews.c
@@ -25,12 +25,16 @@
 #include <math.h>
 #include <json-glib/json-glib.h>
 #include <sqlite3.h>
+#include <libsecret/secret.h>
+#include <oauth.h>
 #include <gnome-software.h>
 
 struct GsPluginData {
        gchar           *db_path;
        sqlite3         *db;
        gsize            db_loaded;
+       gchar           *origin;
+       gchar           *distroseries;
 };
 
 typedef struct {
@@ -50,10 +54,14 @@ typedef struct {
 /* Number of pages of reviews to download */
 #define N_PAGES                                3
 
+#define SECRET_SCHEMA_NAME             "com.ubuntu.UbuntuOne.GnomeSoftware"
+
 void
 gs_plugin_initialize (GsPlugin *plugin)
 {
        GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData));
+       g_autoptr(GsOsRelease) os_release = NULL;
+       g_autoptr(GError) error = NULL;
 
        /* check that we are running on Ubuntu */
        if (!gs_plugin_check_distro_id (plugin, "ubuntu")) {
@@ -67,6 +75,21 @@ gs_plugin_initialize (GsPlugin *plugin)
                                                  "ubuntu-reviews.db",
                                                  NULL);
 
+       os_release = gs_os_release_new (&error);
+       if (os_release == NULL) {
+               g_warning ("Failed to determine OS information: %s", error->message);
+               priv->origin = g_strdup ("unknown");
+               priv->distroseries = g_strdup ("unknown");
+       } else  {
+               priv->origin = g_strdup (gs_os_release_get_id (os_release));
+               if (priv->origin == NULL)
+                       priv->origin = g_strdup ("unknown");
+               if (strcmp (priv->origin, "ubuntu") == 0)
+                       priv->distroseries = g_strdup (gs_os_release_get_ubuntu_codename (os_release));
+               if (priv->distroseries == NULL)
+                       priv->distroseries = g_strdup ("unknown");
+       }
+
        /* we have more reviews than ORDS */
        gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_CONFLICTS, "odrs");
 
@@ -80,6 +103,8 @@ gs_plugin_destroy (GsPlugin *plugin)
        GsPluginData *priv = gs_plugin_get_data (plugin);
        g_clear_pointer (&priv->db, sqlite3_close);
        g_free (priv->db_path);
+       g_free (priv->origin);
+       g_free (priv->distroseries);
 }
 
 static gint
@@ -336,11 +361,84 @@ parse_review_entries (GsPlugin *plugin, JsonParser *parser, GError **error)
 }
 
 static gboolean
-send_review_request (GsPlugin *plugin, const gchar *method, const gchar *path, JsonBuilder *request, 
JsonParser **result, GCancellable *cancellable, GError **error)
+lookup_secret (const gchar *key, gchar **value, GCancellable *cancellable, GError **error)
+{
+       static SecretSchema schema = {
+               SECRET_SCHEMA_NAME,
+               SECRET_SCHEMA_NONE,
+               { { "key", SECRET_SCHEMA_ATTRIBUTE_STRING } }
+       };
+
+       *value = secret_password_lookup_sync (&schema, cancellable, error, "key", key, NULL);
+       return *value != NULL;
+}
+
+static gboolean
+get_ubuntuone_token (GsPlugin *plugin,
+                    gchar **consumer_key, gchar **consumer_secret,
+                    gchar **token_key, gchar **token_secret,
+                    GCancellable *cancellable, GError **error)
 {
+       if (!lookup_secret ("consumer-key", consumer_key, cancellable, error) ||
+           !lookup_secret ("consumer-secret", consumer_secret, cancellable, error) ||
+           !lookup_secret ("token-key", token_key, cancellable, error) ||
+           !lookup_secret ("token-secret", token_secret, cancellable, error))
+               return FALSE;
+
+       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,
+                    gboolean do_sign,
+                    guint *status_code,
+                    JsonParser **result,
+                    GCancellable *cancellable, GError **error)
+{
+       g_autofree gchar *consumer_key = NULL;
+       g_autofree gchar *consumer_secret = NULL;
+       g_autofree gchar *token_key = NULL;
+       g_autofree gchar *token_secret = NULL;
        g_autofree gchar *uri = NULL;
        g_autoptr(SoupMessage) msg = NULL;
-       guint status_code;
+
+       if (do_sign && !get_ubuntuone_token (plugin,
+                                            &consumer_key, &consumer_secret,
+                                            &token_key, &token_secret,
+                                            cancellable, NULL)) {
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_AUTH_REQUIRED,
+                                    "Requires authentication with @ubuntuone");
+               return FALSE;
+       }
 
        uri = g_strdup_printf ("%s%s",
                               UBUNTU_REVIEWS_SERVER, path);
@@ -357,15 +455,13 @@ send_review_request (GsPlugin *plugin, const gchar *method, const gchar *path, J
                soup_message_set_request (msg, "application/json", SOUP_MEMORY_TAKE, data, length);
        }
 
-       status_code = soup_session_send_message (gs_plugin_get_soup_session (plugin), msg);
-       if (status_code != SOUP_STATUS_OK) {
-               g_set_error (error,
-                            GS_PLUGIN_ERROR,
-                            GS_PLUGIN_ERROR_FAILED,
-                            "Got status code %s from reviews.ubuntu.com",
-                            soup_status_get_phrase (status_code));
-               return FALSE;
-       }
+       if (do_sign)
+               sign_message (msg,
+                             OA_PLAINTEXT,
+                             consumer_key, consumer_secret,
+                             token_key, token_secret);
+
+       *status_code = soup_session_send_message (gs_plugin_get_soup_session (plugin), msg);
 
        if (result != NULL) {
                g_autoptr(JsonParser) parser = NULL;
@@ -396,11 +492,23 @@ download_review_stats (GsPlugin *plugin, GCancellable *cancellable, GError **err
 {
        g_autofree gchar *uri = NULL;
        g_autoptr(SoupMessage) msg = NULL;
+       guint status_code;
        g_autoptr(JsonParser) result = NULL;
 
-       if (!send_review_request (plugin, SOUP_METHOD_GET, "/api/1.0/review-stats/any/any/", NULL, &result, 
cancellable, error))
+       if (!send_review_request (plugin, SOUP_METHOD_GET, "/api/1.0/review-stats/any/any/",
+                                 NULL, FALSE,
+                                 &status_code, &result, cancellable, error))
                return FALSE;
 
+       if (status_code != SOUP_STATUS_OK) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "Failed to download review stats, server returned status code %d",
+                            status_code);
+               return FALSE;
+       }
+
        /* Extract the stats from the data */
        if (!parse_review_entries (plugin, result, error))
                return FALSE;
@@ -545,39 +653,42 @@ parse_date_time (const gchar *text)
        return g_date_time_new_utc (values[0], values[1], values[2], values[3], values[4], values[5]);
 }
 
-static GsReview *
-parse_review (JsonNode *node)
+static gboolean
+parse_review (GsReview *review, const gchar *our_username, JsonNode *node)
 {
-       GsReview *review;
        JsonObject *object;
        gint64 star_rating;
        g_autofree gchar *id_string = NULL;
 
        if (!JSON_NODE_HOLDS_OBJECT (node))
-               return NULL;
+               return FALSE;
 
        object = json_node_get_object (node);
 
-       review = gs_review_new ();
+       if (g_strcmp0 (our_username, 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);
 
-       return review;
+       return TRUE;
 }
 
 static gboolean
-parse_reviews (GsPlugin *plugin, JsonParser *parser, GsApp *app, GError **error)
+parse_reviews (GsPlugin *plugin, JsonParser *parser, GsApp *app, GCancellable *cancellable, GError **error)
 {
        JsonArray *array;
        guint i;
+       g_autofree gchar *consumer_key = NULL;
+
+       lookup_secret ("consumer-key", &consumer_key, cancellable, NULL);
 
        if (!JSON_NODE_HOLDS_ARRAY (json_parser_get_root (parser)))
                return FALSE;
@@ -586,8 +697,8 @@ 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));
-               if (review != NULL)
+               review = gs_review_new ();
+               if (parse_review (review, consumer_key, json_array_get_element (array, i)))
                        gs_app_add_review (app, review);
        }
 
@@ -614,17 +725,29 @@ download_reviews (GsPlugin *plugin, GsApp *app,
                  GCancellable *cancellable, GError **error)
 {
        g_autofree gchar *language = NULL, *path = NULL;
+       guint status_code;
        g_autoptr(JsonParser) result = NULL;
 
        /* 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/page/%d/", language, package_name, 
page_number + 1);
-       if (!send_review_request (plugin, SOUP_METHOD_GET, path, NULL, FALSE, &result, cancellable, 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,
+                                 &status_code, &result, cancellable, error))
                return FALSE;
 
+       if (status_code != SOUP_STATUS_OK) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "Failed to download reviews, server returned status code %d",
+                            status_code);
+               return FALSE;
+       }
+
        /* Extract the stats from the data */
-       return parse_reviews (plugin, result, app, error);
+       return parse_reviews (plugin, result, app, cancellable, error);
 }
 
 static gboolean
@@ -653,9 +776,6 @@ refine_rating (GsPlugin *plugin, GsApp *app, GCancellable *cancellable, GError *
                gint review_ratings[6];
                gboolean ret;
 
-               /* If we have a local review, use that as the rating */
-               // FIXME
-
                /* Otherwise use the statistics */
                package_name = g_ptr_array_index (sources, i);
                ret = get_review_stats (plugin, package_name, &rating, review_ratings, cancellable, error);
@@ -724,3 +844,233 @@ gs_plugin_refine_app (GsPlugin *plugin,
        return TRUE;
 }
 
+static void
+add_string_member (JsonBuilder *builder, const gchar *name, const gchar *value)
+{
+       json_builder_set_member_name (builder, name);
+       json_builder_add_string_value (builder, value);
+}
+
+static void
+add_int_member (JsonBuilder *builder, const gchar *name, gint64 value)
+{
+       json_builder_set_member_name (builder, name);
+       json_builder_add_int_value (builder, value);
+}
+
+gboolean
+gs_plugin_review_submit (GsPlugin *plugin,
+                        GsApp *app,
+                        GsReview *review,
+                        GCancellable *cancellable,
+                        GError **error)
+{
+       GsPluginData *priv = gs_plugin_get_data (plugin);
+       gint rating;
+       gint n_stars;
+       g_autofree gchar *language = NULL, *architecture = NULL;
+       g_autoptr(JsonBuilder) request = NULL;
+       guint status_code;
+       g_autoptr(JsonParser) result = NULL;
+       g_autofree gchar *consumer_key = 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;
+
+       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", gs_app_get_source_default (app));
+       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", priv->origin);
+       add_string_member (request, "distroseries", priv->distroseries);
+       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);
+
+       if (!send_review_request (plugin, SOUP_METHOD_POST, "/api/1.0/reviews/",
+                                 request, TRUE,
+                                 &status_code, &result, cancellable, error))
+               return FALSE;
+
+       if (status_code != SOUP_STATUS_OK) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "Failed to submit review, server returned status code %d",
+                            status_code);
+               return FALSE;
+       }
+
+       // Extract new fields from posted review
+       lookup_secret ("consumer-key", &consumer_key, cancellable, NULL);
+       parse_review (review, consumer_key, json_parser_get_root (result));
+
+       return TRUE;
+}
+
+gboolean
+gs_plugin_review_report (GsPlugin *plugin,
+                        GsApp *app,
+                        GsReview *review,
+                        GCancellable *cancellable,
+                        GError **error)
+{
+       const gchar *review_id;
+       g_autofree gchar *reason = NULL;
+       g_autofree gchar *text = NULL;
+       g_autofree gchar *path = NULL;
+       guint status_code;
+
+       /* Can only modify Ubuntu reviews */
+       review_id = gs_review_get_metadata_item (review, "ubuntu-id");
+       if (review_id == NULL)
+               return TRUE;
+
+       /* Create message for reviews.ubuntu.com */
+       reason = soup_uri_encode ("FIXME: gnome-software", NULL);
+       text = soup_uri_encode ("FIXME: gnome-software", NULL);
+       path = g_strdup_printf ("/api/1.0/reviews/%s/recommendations/?reason=%s&text=%s",
+                               review_id, reason, text);
+       if (!send_review_request (plugin, SOUP_METHOD_POST, path,
+                                 NULL, TRUE,
+                                 &status_code, NULL, cancellable, error))
+               return FALSE;
+
+       if (status_code != SOUP_STATUS_CREATED) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "Failed to report review, server returned status code %d",
+                            status_code);
+               return FALSE;
+       }
+
+       gs_review_add_flags (review, GS_REVIEW_FLAG_VOTED);
+       return TRUE;
+}
+
+static gboolean
+set_review_usefulness (GsPlugin *plugin,
+                      const gchar *review_id,
+                      gboolean is_useful,
+                      GCancellable *cancellable,
+                      GError **error)
+{
+       g_autofree gchar *path = NULL;
+       guint status_code;
+
+       /* Create message for reviews.ubuntu.com */
+       path = g_strdup_printf ("/api/1.0/reviews/%s/recommendations/?useful=%s",
+                               review_id, is_useful ? "True" : "False");
+       if (!send_review_request (plugin, SOUP_METHOD_POST, path,
+                                 NULL, TRUE,
+                                 &status_code, NULL, cancellable, error))
+               return FALSE;
+
+       if (status_code != SOUP_STATUS_CREATED) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "Got status code %d from reviews.ubuntu.com",
+                            status_code);
+               return FALSE;
+       }
+
+       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, cancellable, 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, cancellable, 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;
+       g_autofree gchar *path = NULL;
+       guint status_code;
+
+       /* Can only modify Ubuntu reviews */
+       review_id = gs_review_get_metadata_item (review, "ubuntu-id");
+       if (review_id == NULL)
+               return TRUE;
+
+       /* Create message for reviews.ubuntu.com */
+       path = g_strdup_printf ("/api/1.0/reviews/delete/%s/", review_id);
+       if (!send_review_request (plugin, SOUP_METHOD_POST, path,
+                                 NULL, TRUE,
+                                 &status_code, NULL, cancellable, error))
+               return FALSE;
+
+       if (status_code != SOUP_STATUS_OK) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "Failed to remove review, server returned status code %d",
+                            status_code);
+               return FALSE;
+       }
+
+       return TRUE;
+}
diff --git a/src/plugins/gs-plugin-ubuntuone.c b/src/plugins/gs-plugin-ubuntuone.c
new file mode 100644
index 0000000..8131529
--- /dev/null
+++ b/src/plugins/gs-plugin-ubuntuone.c
@@ -0,0 +1,212 @@
+/* -*- 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 <string.h>
+#include <libsoup/soup.h>
+#include <json-glib/json-glib.h>
+#include <libsecret/secret.h>
+#include <gnome-software.h>
+
+// Documented in http://canonical-identity-provider.readthedocs.io
+#define UBUNTU_LOGIN_HOST "https://login.ubuntu.com";
+
+#define SECRET_SCHEMA_NAME "com.ubuntu.UbuntuOne.GnomeSoftware"
+
+struct GsPluginData {
+       GsAuth                  *auth;
+};
+
+void
+gs_plugin_initialize (GsPlugin *plugin)
+{
+       GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData));
+
+       /* set up a dummy authentication provider */
+       priv->auth = gs_auth_new (gs_plugin_get_name (plugin));
+       gs_auth_set_provider_name (priv->auth, "Ubuntu One");
+        //gs_auth_set_provider_logo (priv->auth, "...");
+       gs_plugin_add_auth (plugin, priv->auth);
+}
+
+static gboolean
+store_secret (const gchar *key, const gchar *value, GCancellable *cancellable, GError **error)
+{
+       static SecretSchema schema = {
+               SECRET_SCHEMA_NAME,
+               SECRET_SCHEMA_NONE,
+               { { "key", SECRET_SCHEMA_ATTRIBUTE_STRING } }
+       };
+
+       return secret_password_store_sync (&schema, NULL, SECRET_SCHEMA_NAME, value, cancellable, error, 
"key", key, NULL);
+}
+
+gboolean
+gs_plugin_auth_login (GsPlugin *plugin, GsAuth *auth,
+                     GCancellable *cancellable, GError **error)
+{
+       GsPluginData *priv = gs_plugin_get_data (plugin);
+       g_autoptr(JsonBuilder) builder = NULL;
+       g_autoptr(JsonNode) json_root = NULL;
+       g_autoptr(JsonGenerator) json_generator = NULL;
+       g_autofree gchar *data = NULL;
+       g_autofree gchar *uri = NULL;
+       g_autoptr(SoupMessage) msg = NULL;
+       guint status_code;
+       g_autoptr(JsonParser) parser = NULL;
+       JsonNode *response_root;
+       const gchar *consumer_key, *consumer_secret, *token_key, *token_secret;
+
+       if (auth != priv->auth)
+               return TRUE;
+
+       builder = json_builder_new ();
+       json_builder_begin_object (builder);
+       json_builder_set_member_name (builder, "token_name");
+       json_builder_add_string_value (builder, "GNOME Software");
+       json_builder_set_member_name (builder, "email");
+       json_builder_add_string_value (builder, gs_auth_get_username (auth));
+       json_builder_set_member_name (builder, "password");
+       json_builder_add_string_value (builder, gs_auth_get_password (auth));
+       if (gs_auth_get_pin (auth)) {
+               json_builder_set_member_name (builder, "otp");
+               json_builder_add_string_value (builder, gs_auth_get_pin (auth));
+       }
+       json_builder_end_object (builder);
+
+       json_root = json_builder_get_root (builder);
+       json_generator = json_generator_new ();
+       json_generator_set_pretty (json_generator, TRUE);
+       json_generator_set_root (json_generator, json_root);
+       data = json_generator_to_data (json_generator, NULL);
+       if (data == NULL) {
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "Failed to generate JSON request");
+               return FALSE;
+       }
+
+       uri = g_strdup_printf ("%s/api/v2/tokens/oauth", UBUNTU_LOGIN_HOST);
+       msg = soup_message_new (SOUP_METHOD_POST, uri);
+       soup_message_set_request (msg, "application/json", SOUP_MEMORY_COPY, data, strlen (data));
+       status_code = soup_session_send_message (gs_plugin_get_soup_session (plugin), msg);
+
+       parser = json_parser_new ();
+       if (!json_parser_load_from_data (parser, msg->response_body->data, -1, error))
+               return FALSE;
+       response_root = json_parser_get_root (parser);
+
+       if (status_code != SOUP_STATUS_OK) {
+               const gchar *message, *code;
+
+               message = json_object_get_string_member (json_node_get_object (response_root), "message");
+               code = json_object_get_string_member (json_node_get_object (response_root), "code");
+
+               if (g_strcmp0 (code, "INVALID_CREDENTIALS") == 0 || g_strcmp0 (code, "EMAIL_INVALIDATED") == 
0 || g_strcmp0 (code, "TWOFACTOR_FAILURE") == 0) {
+                       g_set_error_literal (error,
+                                            GS_PLUGIN_ERROR,
+                                            GS_PLUGIN_ERROR_AUTH_INVALID,
+                                            message);
+               }
+               else if (g_strcmp0 (code, "ACCOUNT_SUSPENDED") == 0) {
+                       g_set_error_literal (error,
+                                            GS_PLUGIN_ERROR,
+                                            GS_PLUGIN_ERROR_ACCOUNT_SUSPENDED,
+                                            message);
+               }
+               else if (g_strcmp0 (code, "ACCOUNT_DEACTIVATED") == 0) {
+                       g_set_error_literal (error,
+                                            GS_PLUGIN_ERROR,
+                                            GS_PLUGIN_ERROR_ACCOUNT_DEACTIVATED,
+                                            message);
+               }
+               else if (g_strcmp0 (code, "TWOFACTOR_REQUIRED") == 0) {
+                       g_set_error_literal (error,
+                                            GS_PLUGIN_ERROR,
+                                            GS_PLUGIN_ERROR_PIN_REQUIRED,
+                                            message);
+               }
+               else {
+                       g_set_error_literal (error,
+                                            GS_PLUGIN_ERROR,
+                                            GS_PLUGIN_ERROR_FAILED,
+                                            message);
+               }
+               return FALSE;
+       }
+
+       consumer_key = json_object_get_string_member (json_node_get_object (response_root), "consumer_key");
+       consumer_secret = json_object_get_string_member (json_node_get_object (response_root), 
"consumer_secret");
+       token_key = json_object_get_string_member (json_node_get_object (response_root), "token_key");
+       token_secret = json_object_get_string_member (json_node_get_object (response_root), "token_secret");
+       if (consumer_key == NULL || consumer_secret == NULL || token_key == NULL || token_secret == NULL) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "Response from %s missing required fields", UBUNTU_LOGIN_HOST);
+               return FALSE;
+       }
+
+       if (!store_secret ("consumer-key", consumer_key, cancellable, error) ||
+           !store_secret ("consumer-secret", consumer_secret, cancellable, error) ||
+           !store_secret ("token-key", token_key, cancellable, error) ||
+           !store_secret ("token-secret", token_secret, cancellable, error)) {
+               return FALSE;
+       }
+
+       return TRUE;
+}
+
+gboolean
+gs_plugin_auth_lost_password (GsPlugin *plugin, GsAuth *auth,
+                             GCancellable *cancellable, GError **error)
+{
+       GsPluginData *priv = gs_plugin_get_data (plugin);
+
+       if (auth != priv->auth)
+               return TRUE;
+
+       /* return with data */
+       g_set_error (error,
+                    GS_PLUGIN_ERROR,
+                    GS_PLUGIN_ERROR_AUTH_INVALID,
+                    "do online using @%s/+forgot_password", UBUNTU_LOGIN_HOST);
+       return FALSE;
+}
+
+gboolean
+gs_plugin_auth_register (GsPlugin *plugin, GsAuth *auth,
+                        GCancellable *cancellable, GError **error)
+{
+       GsPluginData *priv = gs_plugin_get_data (plugin);
+
+       if (auth != priv->auth)
+               return TRUE;
+
+       /* return with data */
+       g_set_error (error,
+                    GS_PLUGIN_ERROR,
+                    GS_PLUGIN_ERROR_AUTH_INVALID,
+                    "do online using @%s/+login", UBUNTU_LOGIN_HOST);
+       return FALSE;
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]