[gnome-software/wip/rancell/snap-auth-3-20: 57/57] Replace UbuntuOne dialog and use GsAuth
- From: Robert Ancell <rancell src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-software/wip/rancell/snap-auth-3-20: 57/57] Replace UbuntuOne dialog and use GsAuth
- Date: Thu, 14 Dec 2017 00:47:28 +0000 (UTC)
commit 30c795f4c1bf939495a43b87c802f91f7db2c22d
Author: Robert Ancell <robert ancell canonical com>
Date: Fri Sep 15 17:04:23 2017 +1200
Replace UbuntuOne dialog and use GsAuth
po/POTFILES.in | 2 -
src/gnome-software.gresource.xml | 2 -
src/plugins/Makefile.am | 26 +-
src/plugins/gs-plugin-snap.c | 150 ++++++++-
src/plugins/gs-plugin-ubuntu-reviews.c | 103 ++----
src/plugins/gs-plugin-ubuntuone.c | 266 +++++++++++++++
src/plugins/gs-snapd.c | 122 ++-----
src/plugins/gs-snapd.h | 32 ++-
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 2540 -> 0 bytes
14 files changed, 511 insertions(+), 1656 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 91c990a..313957f 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -59,8 +59,6 @@ src/gs-upgrade-banner.c
src/gs-utils.c
[type: gettext/glade]src/gs-menus.ui
src/org.gnome.Software.desktop.in
-src/plugins/gs-ubuntuone-dialog.c
-[type: gettext/glade]src/plugins/gs-ubuntuone-dialog.ui
src/plugins/gs-plugin-snap.c
src/plugins/menu-spec-common.c
[type: gettext/glade]src/gs-popular-tile.ui
diff --git a/src/gnome-software.gresource.xml b/src/gnome-software.gresource.xml
index dd289a2..73600ae 100644
--- a/src/gnome-software.gresource.xml
+++ b/src/gnome-software.gresource.xml
@@ -33,7 +33,5 @@
<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 2b71caa..b9a8861 100644
--- a/src/plugins/Makefile.am
+++ b/src/plugins/Makefile.am
@@ -41,7 +41,8 @@ plugin_LTLIBRARIES = \
libgs_plugin_provenance.la \
libgs_plugin_fedora_tagger_usage.la \
libgs_plugin_epiphany.la \
- libgs_plugin_icons.la
+ libgs_plugin_icons.la \
+ libgs_plugin_ubuntuone.la
if HAVE_APT
plugin_LTLIBRARIES += \
@@ -211,13 +212,7 @@ libgs_plugin_hardcoded_blacklist_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
if HAVE_UBUNTU_REVIEWS
libgs_plugin_ubuntu_reviews_la_SOURCES = \
- 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
+ gs-plugin-ubuntu-reviews.c
libgs_plugin_ubuntu_reviews_la_LIBADD = \
$(GS_PLUGIN_LIBS) \
$(SOUP_LIBS) \
@@ -301,10 +296,6 @@ libgs_plugin_packagekit_proxy_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
if HAVE_SNAP
libgs_plugin_snap_la_SOURCES = \
gs-plugin-snap.c \
- gs-ubuntuone.h \
- gs-ubuntuone.c \
- gs-ubuntuone-dialog.h \
- gs-ubuntuone-dialog.c \
gs-snapd.h \
gs-snapd.c
libgs_plugin_snap_la_LIBADD = \
@@ -317,6 +308,15 @@ libgs_plugin_snap_la_LDFLAGS = -module -avoid-version
libgs_plugin_snap_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS) -DUSE_SNAPD
endif
+libgs_plugin_ubuntuone_la_SOURCES = \
+ gs-plugin-ubuntuone.c
+libgs_plugin_ubuntuone_la_LIBADD = \
+ $(GS_PLUGIN_LIBS) \
+ $(SOUP_LIBS) \
+ $(JSON_GLIB_LIBS)
+libgs_plugin_ubuntuone_la_LDFLAGS = -module -avoid-version
+libgs_plugin_ubuntuone_la_CFLAGS = $(GS_PLUGIN_CFLAGS) $(WARN_CFLAGS)
+
check_PROGRAMS = \
gs-self-test
@@ -333,6 +333,6 @@ gs_self_test_CFLAGS = $(WARN_CFLAGS)
TESTS = gs-self-test
-EXTRA_DIST = moduleset-test.xml gs-ubuntuone-dialog.h gs-ubuntuone-dialog.ui ubuntu-one.png
com.canonical.Unity.Launcher.xml
+EXTRA_DIST = moduleset-test.xml com.canonical.Unity.Launcher.xml
-include $(top_srcdir)/git.mk
diff --git a/src/plugins/gs-plugin-snap.c b/src/plugins/gs-plugin-snap.c
index bf7bc29..4b4048b 100644
--- a/src/plugins/gs-plugin-snap.c
+++ b/src/plugins/gs-plugin-snap.c
@@ -24,11 +24,12 @@
#include <glib/gi18n.h>
#include <json-glib/json-glib.h>
+#include <snapd-glib/snapd-glib.h>
#include "gs-snapd.h"
-#include "gs-ubuntuone.h"
struct GsPluginPrivate {
gchar *store_name;
+ GsAuth *auth;
GHashTable *store_snaps;
};
@@ -74,6 +75,33 @@ gs_plugin_initialize (GsPlugin *plugin)
g_free, (GDestroyNotify) json_object_unref);
}
+static void
+get_macaroon (GsPlugin *plugin, gchar **macaroon, gchar ***discharges)
+{
+ GsAuth *auth;
+ const gchar *serialized_macaroon;
+ g_autoptr(GVariant) macaroon_variant = NULL;
+ g_autoptr (GError) error_local = NULL;
+
+ *macaroon = NULL;
+ *discharges = NULL;
+
+ auth = gs_plugin_get_auth_by_id (plugin, "snapd");
+ if (auth == NULL)
+ return;
+ serialized_macaroon = gs_auth_get_metadata_item (auth, "macaroon");
+ if (serialized_macaroon == NULL)
+ return;
+ macaroon_variant = g_variant_parse (G_VARIANT_TYPE ("(sas)"),
+ serialized_macaroon,
+ NULL,
+ NULL,
+ NULL);
+ if (macaroon_variant == NULL)
+ return;
+ g_variant_get (macaroon_variant, "(s^as)", macaroon, discharges);
+ }
+
static gboolean
gs_plugin_snap_set_app_pixbuf_from_data (GsApp *app, const gchar *buf, gsize count, GError **error)
{
@@ -105,10 +133,13 @@ gs_plugin_snap_set_app_pixbuf_from_data (GsApp *app, const gchar *buf, gsize cou
static JsonArray *
find_snaps (GsPlugin *plugin, const gchar *section, gboolean match_name, const gchar *query, GCancellable
*cancellable, GError **error)
{
+ g_autofree gchar *macaroon = NULL;
+ g_auto(GStrv) discharges = NULL;
g_autoptr(JsonArray) snaps = NULL;
guint i;
- snaps = gs_snapd_find (section, match_name, query, cancellable, error);
+ get_macaroon (plugin, &macaroon, &discharges);
+ snaps = gs_snapd_find (macaroon, discharges, section, match_name, query, cancellable, error);
if (snaps == NULL)
return NULL;
@@ -192,6 +223,7 @@ void
gs_plugin_destroy (GsPlugin *plugin)
{
g_free (plugin->priv->store_name);
+ g_object_unref (plugin->priv->auth);
g_hash_table_unref (plugin->priv->store_snaps);
}
@@ -326,10 +358,13 @@ gs_plugin_add_installed (GsPlugin *plugin,
GCancellable *cancellable,
GError **error)
{
+ g_autofree gchar *macaroon = NULL;
+ g_auto(GStrv) discharges = NULL;
g_autoptr(JsonArray) snaps = NULL;
guint i;
- snaps = gs_snapd_list (cancellable, error);
+ get_macaroon (plugin, &macaroon, &discharges);
+ snaps = gs_snapd_list (macaroon, discharges, cancellable, error);
if (snaps == NULL)
return FALSE;
@@ -384,10 +419,13 @@ load_icon (GsPlugin *plugin, GsApp *app, const gchar *icon_url, GCancellable *ca
/* icon is optional, either loaded from snapd or from a URL */
if (g_str_has_prefix (icon_url, "/")) {
+ g_autofree gchar *macaroon = NULL;
+ g_auto(GStrv) discharges = NULL;
g_autofree gchar *icon_data = NULL;
gsize icon_data_length;
- icon_data = gs_snapd_get_resource (icon_url, &icon_data_length, cancellable, error);
+ get_macaroon (plugin, &macaroon, &discharges);
+ icon_data = gs_snapd_get_resource (macaroon, discharges, icon_url, &icon_data_length,
cancellable, error);
if (icon_data == NULL)
return FALSE;
@@ -458,6 +496,8 @@ gs_plugin_refine_app (GsPlugin *plugin,
GCancellable *cancellable,
GError **error)
{
+ g_autofree gchar *macaroon = NULL;
+ g_auto(GStrv) discharges = NULL;
const gchar *id, *icon_url = NULL;
g_autoptr(JsonObject) local_snap = NULL;
g_autoptr(JsonObject) store_snap = NULL;
@@ -466,12 +506,14 @@ gs_plugin_refine_app (GsPlugin *plugin,
if (g_strcmp0 (gs_app_get_management_plugin (app), "snap") != 0)
return TRUE;
+ get_macaroon (plugin, &macaroon, &discharges);
+
id = gs_app_get_id (app);
if (id == NULL)
id = gs_app_get_source_default (app);
/* get information from installed snaps */
- local_snap = gs_snapd_list_one (id, cancellable, NULL);
+ local_snap = gs_snapd_list_one (macaroon, discharges, id, cancellable, NULL);
if (local_snap != NULL) {
JsonArray *apps;
g_autoptr(GDateTime) install_date = NULL;
@@ -615,16 +657,20 @@ gs_plugin_app_install (GsPlugin *plugin,
GCancellable *cancellable,
GError **error)
{
+ g_autofree gchar *macaroon = NULL;
+ g_auto(GStrv) discharges = NULL;
ProgressData data;
/* We can only install apps we know of */
if (g_strcmp0 (gs_app_get_management_plugin (app), "snap") != 0)
return TRUE;
+ get_macaroon (plugin, &macaroon, &discharges);
+
gs_app_set_state (app, AS_APP_STATE_INSTALLING);
data.plugin = plugin;
data.app = app;
- if (!gs_snapd_install (gs_app_get_id (app), progress_cb, &data, cancellable, error)) {
+ if (!gs_snapd_install (macaroon, discharges, gs_app_get_id (app), progress_cb, &data, cancellable,
error)) {
gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
return FALSE;
}
@@ -643,7 +689,7 @@ is_graphical (GsApp *app, GCancellable *cancellable)
guint i;
g_autoptr(GError) error = NULL;
- result = gs_snapd_get_interfaces (cancellable, &error);
+ result = gs_snapd_get_interfaces (NULL, NULL, cancellable, &error);
if (result == NULL) {
g_warning ("Failed to check interfaces: %s", error->message);
return FALSE;
@@ -708,19 +754,107 @@ gs_plugin_app_remove (GsPlugin *plugin,
GCancellable *cancellable,
GError **error)
{
+ g_autofree gchar *macaroon = NULL;
+ g_auto(GStrv) discharges = NULL;
ProgressData data;
/* We can only remove apps we know of */
if (g_strcmp0 (gs_app_get_management_plugin (app), "snap") != 0)
return TRUE;
+ get_macaroon (plugin, &macaroon, &discharges);
+
gs_app_set_state (app, AS_APP_STATE_REMOVING);
data.plugin = plugin;
data.app = app;
- if (!gs_snapd_remove (gs_app_get_id (app), progress_cb, &data, cancellable, error)) {
+ if (!gs_snapd_remove (macaroon, discharges, gs_app_get_id (app), progress_cb, &data, cancellable,
error)) {
gs_app_set_state (app, AS_APP_STATE_INSTALLED);
return FALSE;
}
gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
return TRUE;
}
+
+gboolean
+gs_plugin_auth_login (GsPlugin *plugin, GsAuth *auth,
+ GCancellable *cancellable, GError **error)
+{
+ g_autoptr(SnapdAuthData) auth_data = NULL;
+ g_autoptr(GVariant) macaroon_variant = NULL;
+ g_autofree gchar *serialized_macaroon = NULL;
+ g_autoptr(GError) local_error = NULL;
+
+ if (auth != plugin->priv->auth)
+ return TRUE;
+
+ auth_data = snapd_login_sync (gs_auth_get_username (auth), gs_auth_get_password (auth),
gs_auth_get_pin (auth), NULL, &local_error);
+ if (auth_data == NULL) {
+ if (g_error_matches (local_error, SNAPD_ERROR, SNAPD_ERROR_TWO_FACTOR_REQUIRED)) {
+ g_set_error_literal (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_PIN_REQUIRED,
+ local_error->message);
+ } else if (g_error_matches (local_error, SNAPD_ERROR, SNAPD_ERROR_AUTH_DATA_INVALID) ||
+ g_error_matches (local_error, SNAPD_ERROR, SNAPD_ERROR_TWO_FACTOR_INVALID)) {
+ g_set_error_literal (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_AUTH_INVALID,
+ local_error->message);
+ } else {
+ g_set_error_literal (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ local_error->message);
+ }
+ return FALSE;
+ }
+
+ macaroon_variant = g_variant_new ("(s^as)",
+ snapd_auth_data_get_macaroon (auth_data),
+ snapd_auth_data_get_discharges (auth_data));
+ serialized_macaroon = g_variant_print (macaroon_variant, FALSE);
+ gs_auth_add_metadata (auth, "macaroon", serialized_macaroon);
+
+ /* store */
+ if (!gs_auth_store_save (auth,
+ GS_AUTH_STORE_FLAG_USERNAME |
+ GS_AUTH_STORE_FLAG_METADATA,
+ cancellable, error))
+ return FALSE;
+
+ gs_auth_add_flags (plugin->priv->auth, GS_AUTH_FLAG_VALID);
+
+ return TRUE;
+}
+
+gboolean
+gs_plugin_auth_lost_password (GsPlugin *plugin, GsAuth *auth,
+ GCancellable *cancellable, GError **error)
+{
+ if (auth != plugin->priv->auth)
+ return TRUE;
+
+ // FIXME: snapd might not be using Ubuntu One accounts
+ // https://bugs.launchpad.net/bugs/1598667
+ g_set_error_literal (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_AUTH_INVALID,
+ "do online using @https://login.ubuntu.com/+forgot_password");
+ return FALSE;
+}
+
+gboolean
+gs_plugin_auth_register (GsPlugin *plugin, GsAuth *auth,
+ GCancellable *cancellable, GError **error)
+{
+ if (auth != plugin->priv->auth)
+ return TRUE;
+
+ // FIXME: snapd might not be using Ubuntu One accounts
+ // https://bugs.launchpad.net/bugs/1598667
+ g_set_error_literal (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_AUTH_INVALID,
+ "do online using @https://login.ubuntu.com/+login");
+ return FALSE;
+}
diff --git a/src/plugins/gs-plugin-ubuntu-reviews.c b/src/plugins/gs-plugin-ubuntu-reviews.c
index c3aee59..884c4c4 100644
--- a/src/plugins/gs-plugin-ubuntu-reviews.c
+++ b/src/plugins/gs-plugin-ubuntu-reviews.c
@@ -31,17 +31,12 @@
#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 {
@@ -112,10 +107,6 @@ 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);
}
@@ -380,12 +371,25 @@ parse_review_entries (GsPlugin *plugin, JsonParser *parser, GError **error)
static void
sign_message (SoupMessage *message, OAuthMethod method,
- const gchar *consumer_key, const gchar *consumer_secret,
- const gchar *token_key, const gchar *token_secret)
+ GsAuth *auth)
{
g_autofree gchar *url = NULL, *oauth_authorization_parameters = NULL, *authorization_text = NULL;
gchar **url_parameters = NULL;
int url_parameters_length;
+ const gchar *consumer_key;
+ const gchar *consumer_secret;
+ const gchar *token_key;
+ const gchar *token_secret;
+
+ if (auth == NULL)
+ return;
+
+ consumer_key = gs_auth_get_metadata_item (auth, "consumer-key");
+ consumer_secret = gs_auth_get_metadata_item (auth, "consumer-secret");
+ token_key = gs_auth_get_metadata_item (auth, "token-key");
+ token_secret = gs_auth_get_metadata_item (auth, "token-secret");
+ if (consumer_key == NULL || consumer_secret == NULL || token_key == NULL || token_secret == NULL)
+ return;
url = soup_uri_to_string (soup_message_get_uri (message), FALSE);
@@ -405,7 +409,6 @@ sign_message (SoupMessage *message, OAuthMethod method,
static gboolean
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;
@@ -428,18 +431,15 @@ send_review_request (GsPlugin *plugin, const gchar *method, const gchar *path, J
if (do_sign)
sign_message (msg,
OA_PLAINTEXT,
- priv->consumer_key,
- priv->consumer_secret,
- priv->token_key,
- priv->token_secret);
+ gs_plugin_get_auth_by_id (plugin, "ubuntuone"));
status_code = soup_session_send_message (plugin->soup_session, 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));
+
+ if (status_code == SOUP_STATUS_UNAUTHORIZED) {
+ g_set_error_literal (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_AUTH_REQUIRED,
+ "Requires authentication with @ubuntuone");
return FALSE;
}
@@ -621,9 +621,8 @@ parse_date_time (const gchar *text)
}
static GsReview *
-parse_review (GsPlugin *plugin, JsonNode *node)
+parse_review (GsPlugin *plugin, const gchar *our_username, JsonNode *node)
{
- GsPluginPrivate *priv = plugin->priv;
GsReview *review;
JsonObject *object;
gint64 star_rating;
@@ -635,7 +634,7 @@ parse_review (GsPlugin *plugin, 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)
+ 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"));
@@ -654,9 +653,15 @@ parse_review (GsPlugin *plugin, JsonNode *node)
static gboolean
parse_reviews (GsPlugin *plugin, JsonParser *parser, GsApp *app, GError **error)
{
+ GsAuth *auth;
JsonArray *array;
+ const gchar *consumer_key = NULL;
guint i;
+ auth = gs_plugin_get_auth_by_id (plugin, "ubuntuone");
+ if (auth != NULL)
+ consumer_key = gs_auth_get_metadata_item (auth, "consumer-key");
+
if (!JSON_NODE_HOLDS_ARRAY (json_parser_get_root (parser)))
return FALSE;
array = json_node_get_array (json_parser_get_root (parser));
@@ -664,7 +669,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 (plugin, json_array_get_element (array, i));
+ review = parse_review (plugin, consumer_key, json_array_get_element (array, i));
if (review != NULL)
gs_app_add_review (app, review);
}
@@ -754,45 +759,11 @@ 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, j;
- if (!get_ubuntuone_credentials (plugin, FALSE, error))
- return FALSE;
-
/* Skip if already has reviews */
if (gs_app_get_reviews (app)->len > 0)
return TRUE;
@@ -908,9 +879,6 @@ set_review_usefulness (GsPlugin *plugin,
{
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);
@@ -925,9 +893,6 @@ report_review (GsPlugin *plugin,
{
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);
@@ -941,9 +906,6 @@ remove_review (GsPlugin *plugin,
{
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);
@@ -964,9 +926,6 @@ gs_plugin_review_submit (GsPlugin *plugin,
return FALSE;
}
- if (!get_ubuntuone_credentials (plugin, TRUE, error))
- return FALSE;
-
return set_package_review (plugin,
review,
gs_app_get_source_default (app),
diff --git a/src/plugins/gs-plugin-ubuntuone.c b/src/plugins/gs-plugin-ubuntuone.c
new file mode 100644
index 0000000..5eeca99
--- /dev/null
+++ b/src/plugins/gs-plugin-ubuntuone.c
@@ -0,0 +1,266 @@
+/* -*- 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-utils.h>
+#include <gs-plugin.h>
+
+#include <string.h>
+#include <json-glib/json-glib.h>
+
+// Documented in http://canonical-identity-provider.readthedocs.io
+#define UBUNTU_LOGIN_HOST "https://login.ubuntu.com"
+
+struct GsPluginPrivate {
+ GsAuth *auth;
+};
+
+const gchar *
+gs_plugin_get_name (void)
+{
+ return "ubuntuone";
+}
+
+void
+gs_plugin_initialize (GsPlugin *plugin)
+{
+ /* create private area */
+ plugin->priv = GS_PLUGIN_GET_PRIVATE (GsPluginPrivate);
+
+ /* check that we are running on Ubuntu */
+ if (!gs_plugin_check_distro_id (plugin, "ubuntu")) {
+ gs_plugin_set_enabled (plugin, FALSE);
+ g_debug ("disabling '%s' as we're not Ubuntu", plugin->name);
+ return;
+ }
+
+ plugin->priv->auth = gs_auth_new (plugin->name);
+ gs_auth_set_provider_name (plugin->priv->auth, "Ubuntu One");
+ gs_auth_set_provider_schema (plugin->priv->auth, "com.ubuntu.UbuntuOne.GnomeSoftware");
+ //gs_auth_set_provider_logo (plugin->priv->auth, "...");
+ gs_plugin_add_auth (plugin, plugin->priv->auth);
+}
+
+gboolean
+gs_plugin_setup (GsPlugin *plugin, GCancellable *cancellable, GError **error)
+{
+ /* load from disk */
+ gs_auth_add_metadata (plugin->priv->auth, "consumer-key", NULL);
+ gs_auth_add_metadata (plugin->priv->auth, "consumer-secret", NULL);
+ gs_auth_add_metadata (plugin->priv->auth, "token-key", NULL);
+ gs_auth_add_metadata (plugin->priv->auth, "token-secret", NULL);
+ if (!gs_auth_store_load (plugin->priv->auth,
+ GS_AUTH_STORE_FLAG_USERNAME |
+ GS_AUTH_STORE_FLAG_METADATA,
+ cancellable, error))
+ return FALSE;
+
+ /* success */
+ return TRUE;
+}
+
+void
+gs_plugin_destroy (GsPlugin *plugin)
+{
+ g_clear_object (&plugin->priv->auth);
+}
+
+gboolean
+gs_plugin_auth_login (GsPlugin *plugin, GsAuth *auth,
+ GCancellable *cancellable, GError **error)
+{
+ 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 *tmp;
+
+ if (auth != plugin->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 (plugin->soup_session, 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_AUTH_INVALID,
+ message);
+ }
+ return FALSE;
+ }
+
+ /* consumer-key */
+ tmp = json_object_get_string_member (json_node_get_object (response_root), "consumer_key");
+ if (tmp == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Response from %s missing required field consumer_key",
+ UBUNTU_LOGIN_HOST);
+ return FALSE;
+ }
+ gs_auth_add_metadata (auth, "consumer-key", tmp);
+
+ /* consumer-secret */
+ tmp = json_object_get_string_member (json_node_get_object (response_root), "consumer_secret");
+ if (tmp == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Response from %s missing required field consumer_secret",
+ UBUNTU_LOGIN_HOST);
+ return FALSE;
+ }
+ gs_auth_add_metadata (auth, "consumer-secret", tmp);
+
+ /* token-key */
+ tmp = json_object_get_string_member (json_node_get_object (response_root), "token_key");
+ if (tmp == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Response from %s missing required field token_key",
+ UBUNTU_LOGIN_HOST);
+ return FALSE;
+ }
+ gs_auth_add_metadata (auth, "token-key", tmp);
+
+ /* token-secret */
+ tmp = json_object_get_string_member (json_node_get_object (response_root), "token_secret");
+ if (tmp == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Response from %s missing required field token_secret",
+ UBUNTU_LOGIN_HOST);
+ return FALSE;
+ }
+ gs_auth_add_metadata (auth, "token-secret", tmp);
+
+ /* store */
+ if (!gs_auth_store_save (auth,
+ GS_AUTH_STORE_FLAG_USERNAME |
+ GS_AUTH_STORE_FLAG_METADATA,
+ cancellable, error))
+ return FALSE;
+
+ gs_auth_add_flags (plugin->priv->auth, GS_AUTH_FLAG_VALID);
+
+ return TRUE;
+}
+
+gboolean
+gs_plugin_auth_lost_password (GsPlugin *plugin, GsAuth *auth,
+ GCancellable *cancellable, GError **error)
+{
+ if (auth != plugin->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)
+{
+ if (auth != plugin->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;
+}
diff --git a/src/plugins/gs-snapd.c b/src/plugins/gs-snapd.c
index 261af42..4bc288e 100644
--- a/src/plugins/gs-snapd.c
+++ b/src/plugins/gs-snapd.c
@@ -25,7 +25,6 @@
#include <libsoup/soup.h>
#include <gio/gunixsocketaddress.h>
#include "gs-snapd.h"
-#include "gs-ubuntuone.h"
// snapd API documentation is at https://github.com/snapcore/snapd/blob/master/docs/rest.md
@@ -93,12 +92,8 @@ static gboolean
send_request (const gchar *method,
const gchar *path,
const gchar *content,
- gboolean authenticate,
- const gchar *macaroon_,
- gchar **discharges_,
- gboolean retry_after_login,
- gchar **out_macaroon,
- gchar ***out_discharges,
+ const gchar *macaroon,
+ gchar **discharges,
guint *status_code,
gchar **reason_phrase,
gchar **response_type,
@@ -113,17 +108,8 @@ send_request (const gchar *method,
g_autoptr (GByteArray) buffer = NULL;
gsize data_length = 0, body_offset = 0;
g_autoptr (SoupMessageHeaders) headers = NULL;
- g_autofree gchar *macaroon = NULL;
- g_auto(GStrv) discharges = NULL;
gsize chunk_length = 0, n_required, chunk_offset;
guint code;
- gboolean ret;
-
- macaroon = g_strdup (macaroon_);
- discharges = g_strdupv (discharges_);
- if (macaroon == NULL && authenticate) {
- gs_ubuntuone_get_macaroon (TRUE, FALSE, &macaroon, &discharges, NULL);
- }
// NOTE: Would love to use libsoup but it doesn't support unix sockets
// https://bugzilla.gnome.org/show_bug.cgi?id=727563
@@ -200,44 +186,6 @@ send_request (const gchar *method,
if (status_code != NULL)
*status_code = code;
- if ((code == 401 || code == 403) && retry_after_login) {
- g_socket_close (socket, NULL);
-
- gs_ubuntuone_clear_macaroon ();
-
- g_clear_pointer (&macaroon, g_free);
- g_clear_pointer (&discharges, g_strfreev);
- gs_ubuntuone_get_macaroon (FALSE, TRUE, &macaroon, &discharges, NULL);
-
- if (macaroon == NULL) {
- g_set_error_literal (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED,
- "failed to authenticate");
- return FALSE;
- }
-
- ret = send_request (method,
- path,
- content,
- TRUE,
- macaroon, discharges,
- FALSE,
- NULL, NULL,
- status_code,
- reason_phrase,
- response_type,
- response,
- response_length,
- cancellable,
- error);
-
- if (ret && out_macaroon != NULL) {
- *out_macaroon = g_steal_pointer (&macaroon);
- *out_discharges = g_steal_pointer (&discharges);
- }
-
- return ret;
- }
-
/* read content */
switch (soup_message_headers_get_encoding (headers)) {
case SOUP_ENCODING_EOF:
@@ -323,10 +271,6 @@ send_request (const gchar *method,
}
- if (out_macaroon != NULL) {
- *out_macaroon = g_steal_pointer (&macaroon);
- *out_discharges = g_steal_pointer (&discharges);
- }
if (response_type)
*response_type = g_strdup (soup_message_headers_get_content_type (headers, NULL));
if (response != NULL) {
@@ -392,8 +336,7 @@ gs_snapd_get_system_info (GCancellable *cancellable, GError **error)
JsonObject *root, *result;
if (!send_request ("GET", "/v2/system-info", NULL,
- TRUE, NULL, NULL,
- TRUE, NULL, NULL,
+ NULL, NULL,
&status_code, &reason_phrase,
&response_type, &response, NULL,
cancellable, error))
@@ -425,7 +368,8 @@ gs_snapd_get_system_info (GCancellable *cancellable, GError **error)
}
JsonObject *
-gs_snapd_list_one (const gchar *name,
+gs_snapd_list_one (const gchar *macaroon, gchar **discharges,
+ const gchar *name,
GCancellable *cancellable, GError **error)
{
g_autofree gchar *path = NULL;
@@ -438,8 +382,7 @@ gs_snapd_list_one (const gchar *name,
path = g_strdup_printf ("/v2/snaps/%s", name);
if (!send_request ("GET", path, NULL,
- TRUE, NULL, NULL,
- TRUE, NULL, NULL,
+ macaroon, discharges,
&status_code, &reason_phrase,
&response_type, &response, NULL,
cancellable, error))
@@ -471,7 +414,8 @@ gs_snapd_list_one (const gchar *name,
}
JsonArray *
-gs_snapd_list (GCancellable *cancellable, GError **error)
+gs_snapd_list (const gchar *macaroon, gchar **discharges,
+ GCancellable *cancellable, GError **error)
{
guint status_code;
g_autofree gchar *reason_phrase = NULL;
@@ -482,8 +426,7 @@ gs_snapd_list (GCancellable *cancellable, GError **error)
JsonArray *result;
if (!send_request ("GET", "/v2/snaps", NULL,
- TRUE, NULL, NULL,
- TRUE, NULL, NULL,
+ macaroon, discharges,
&status_code, &reason_phrase,
&response_type, &response, NULL,
cancellable, error))
@@ -515,7 +458,8 @@ gs_snapd_list (GCancellable *cancellable, GError **error)
}
JsonArray *
-gs_snapd_find (const gchar *section, gboolean match_name, const gchar *query,
+gs_snapd_find (const gchar *macaroon, gchar **discharges,
+ const gchar *section, gboolean match_name, const gchar *query,
GCancellable *cancellable, GError **error)
{
g_autoptr(GString) path = NULL;
@@ -544,8 +488,7 @@ gs_snapd_find (const gchar *section, gboolean match_name, const gchar *query,
g_string_append (path, escaped);
}
if (!send_request ("GET", path->str, NULL,
- TRUE, NULL, NULL,
- TRUE, NULL, NULL,
+ macaroon, discharges,
&status_code, &reason_phrase,
&response_type, &response, NULL,
cancellable, error))
@@ -577,7 +520,7 @@ gs_snapd_find (const gchar *section, gboolean match_name, const gchar *query,
}
JsonObject *
-gs_snapd_get_interfaces (GCancellable *cancellable, GError **error)
+gs_snapd_get_interfaces (const gchar *macaroon, gchar **discharges, GCancellable *cancellable, GError
**error)
{
guint status_code;
g_autofree gchar *reason_phrase = NULL;
@@ -588,8 +531,7 @@ gs_snapd_get_interfaces (GCancellable *cancellable, GError **error)
JsonObject *result;
if (!send_request ("GET", "/v2/interfaces", NULL,
- TRUE, NULL, NULL,
- TRUE, NULL, NULL,
+ macaroon, discharges,
&status_code, &reason_phrase,
&response_type, &response, NULL,
cancellable, error))
@@ -635,8 +577,7 @@ get_changes (const gchar *macaroon, gchar **discharges,
path = g_strdup_printf ("/v2/changes/%s", change_id);
if (!send_request ("GET", path, NULL,
- TRUE, macaroon, discharges,
- TRUE, NULL, NULL,
+ macaroon, discharges,
&status_code, &reason_phrase,
&response_type, &response, NULL,
cancellable, error))
@@ -668,7 +609,9 @@ get_changes (const gchar *macaroon, gchar **discharges,
}
static gboolean
-send_package_action (const gchar *name,
+send_package_action (const gchar *macaroon,
+ gchar **discharges,
+ const gchar *name,
const gchar *action,
GsSnapdProgressCallback callback,
gpointer user_data,
@@ -677,8 +620,6 @@ send_package_action (const gchar *name,
{
g_autofree gchar *content = NULL, *path = NULL;
guint status_code;
- g_autofree gchar *macaroon = NULL;
- g_auto(GStrv) discharges = NULL;
g_autofree gchar *reason_phrase = NULL;
g_autofree gchar *response_type = NULL;
g_autofree gchar *response = NULL;
@@ -690,13 +631,20 @@ send_package_action (const gchar *name,
content = g_strdup_printf ("{\"action\": \"%s\"}", action);
path = g_strdup_printf ("/v2/snaps/%s", name);
if (!send_request ("POST", path, content,
- TRUE, NULL, NULL,
- TRUE, &macaroon, &discharges,
+ macaroon, discharges,
&status_code, &reason_phrase,
&response_type, &response, NULL,
cancellable, error))
return FALSE;
+ if (status_code == SOUP_STATUS_UNAUTHORIZED) {
+ g_set_error_literal (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_AUTH_REQUIRED,
+ "Requires authentication with @snapd");
+ return FALSE;
+ }
+
if (status_code != SOUP_STATUS_ACCEPTED) {
g_set_error (error,
GS_PLUGIN_ERROR,
@@ -747,24 +695,27 @@ send_package_action (const gchar *name,
}
gboolean
-gs_snapd_install (const gchar *name,
+gs_snapd_install (const gchar *macaroon, gchar **discharges,
+ const gchar *name,
GsSnapdProgressCallback callback, gpointer user_data,
GCancellable *cancellable,
GError **error)
{
- return send_package_action (name, "install", callback, user_data, cancellable, error);
+ return send_package_action (macaroon, discharges, name, "install", callback, user_data, cancellable,
error);
}
gboolean
-gs_snapd_remove (const gchar *name,
+gs_snapd_remove (const gchar *macaroon, gchar **discharges,
+ const gchar *name,
GsSnapdProgressCallback callback, gpointer user_data,
GCancellable *cancellable, GError **error)
{
- return send_package_action (name, "remove", callback, user_data, cancellable, error);
+ return send_package_action (macaroon, discharges, name, "remove", callback, user_data, cancellable,
error);
}
gchar *
-gs_snapd_get_resource (const gchar *path,
+gs_snapd_get_resource (const gchar *macaroon, gchar **discharges,
+ const gchar *path,
gsize *data_length,
GCancellable *cancellable, GError **error)
{
@@ -774,8 +725,7 @@ gs_snapd_get_resource (const gchar *path,
g_autofree gchar *data = NULL;
if (!send_request ("GET", path, NULL,
- TRUE, NULL, NULL,
- TRUE, NULL, NULL,
+ macaroon, discharges,
&status_code, &reason_phrase,
NULL, &data, data_length,
cancellable, error))
diff --git a/src/plugins/gs-snapd.h b/src/plugins/gs-snapd.h
index c75cdf2..c9a0ae5 100644
--- a/src/plugins/gs-snapd.h
+++ b/src/plugins/gs-snapd.h
@@ -27,38 +27,52 @@
typedef void (*GsSnapdProgressCallback) (JsonObject *object, gpointer user_data);
-JsonObject *gs_snapd_get_system_info (GCancellable *cancellable,
- GError **error);
+JsonObject *gs_snapd_get_system_info (GCancellable *cancellable,
+ GError **error);
-JsonObject *gs_snapd_list_one (const gchar *name,
+JsonObject *gs_snapd_list_one (const gchar *macaroon,
+ gchar **discharges,
+ const gchar *name,
GCancellable *cancellable,
GError **error);
-JsonArray *gs_snapd_list (GCancellable *cancellable,
+JsonArray *gs_snapd_list (const gchar *macaroon,
+ gchar **discharges,
+ GCancellable *cancellable,
GError **error);
-JsonArray *gs_snapd_find (const gchar *section,
+JsonArray *gs_snapd_find (const gchar *macaroon,
+ gchar **discharges,
+ const gchar *section,
gboolean match_name,
const gchar *query,
GCancellable *cancellable,
GError **error);
-JsonObject *gs_snapd_get_interfaces (GCancellable *cancellable,
+JsonObject *gs_snapd_get_interfaces (const gchar *macaroon,
+ gchar **discharges,
+ GCancellable *cancellable,
GError **error);
-gboolean gs_snapd_install (const gchar *name,
+gboolean gs_snapd_install (const gchar *macaroon,
+ gchar **discharges,
+ const gchar *name,
GsSnapdProgressCallback callback,
gpointer user_data,
GCancellable *cancellable,
GError **error);
-gboolean gs_snapd_remove (const gchar *name,
+gboolean gs_snapd_remove (const gchar *macaroon,
+ gchar **discharges,
+ const gchar *name,
GsSnapdProgressCallback callback,
gpointer user_data,
GCancellable *cancellable,
GError **error);
-gchar *gs_snapd_get_resource (const gchar *path,
+gchar *gs_snapd_get_resource (const gchar *macaroon,
+ gchar **discharges,
+ const gchar *path,
gsize *data_length,
GCancellable *cancellable,
GError **error);
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]