diff options
-rw-r--r-- | src/Makefile.am | 2 | ||||
-rw-r--r-- | src/mcd-account-manager-gvariant.c | 439 | ||||
-rw-r--r-- | src/mcd-account-manager-gvariant.h | 83 | ||||
-rw-r--r-- | src/plugin-account.c | 2 | ||||
-rw-r--r-- | tests/twisted/Makefile.am | 1 | ||||
-rw-r--r-- | tests/twisted/account-manager/gvariant-accounts.py | 130 |
6 files changed, 657 insertions, 0 deletions
diff --git a/src/Makefile.am b/src/Makefile.am index 0c78fc48..114a1077 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -20,6 +20,7 @@ mc_headers = \ mcd-account-conditions.h \ mcd-account-manager.h \ mcd-account-manager-default.h \ + mcd-account-manager-gvariant.h \ mcd-debug.h \ mcd-mission.h \ mcd-operation.h \ @@ -120,6 +121,7 @@ libmcd_convenience_la_SOURCES = \ mcd-account-manager.c \ mcd-account-manager-priv.h \ mcd-account-manager-default.c \ + mcd-account-manager-gvariant.c \ mcd-account-priv.h \ mcd-client.c \ mcd-client-priv.h \ diff --git a/src/mcd-account-manager-gvariant.c b/src/mcd-account-manager-gvariant.c new file mode 100644 index 00000000..2aa9b5c5 --- /dev/null +++ b/src/mcd-account-manager-gvariant.c @@ -0,0 +1,439 @@ +/* + * The gvariant account manager keyfile storage pseudo-plugin + * + * Copyright © 2012 Collabora Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "config.h" + +#include <string.h> + +#include <glib/gstdio.h> + +#include "mcd-account-manager-gvariant.h" +#include "mcd-debug.h" + +#define PLUGIN_NAME "gvariant" +#define PLUGIN_PRIORITY MCP_ACCOUNT_STORAGE_PLUGIN_PRIO_DEFAULT +#define PLUGIN_DESCRIPTION "GVariant account storage backend" + +static void account_storage_iface_init (McpAccountStorageIface *, + gpointer); + +G_DEFINE_TYPE_WITH_CODE (McdAccountManagerGVariant, mcd_account_manager_gvariant, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (MCP_TYPE_ACCOUNT_STORAGE, + account_storage_iface_init)); + +static void +mcd_account_manager_gvariant_init (McdAccountManagerGVariant *self) +{ + DEBUG (""); + self->loaded = FALSE; + + /* TODO: use this instead of MC_ACCOUNT_DIR + self->directory = g_build_filename ( + g_get_user_config_dir (), "mission-control", "accounts", NULL); + */ + + /* TODO: this will only work in the test environment for now. */ + self->directory = g_build_filename (g_getenv ("MC_ACCOUNT_DIR"), NULL); + + self->accounts = g_hash_table_new_full (g_str_hash, g_str_equal, + (GDestroyNotify) g_free, (GDestroyNotify) g_hash_table_unref); + self->accounts_to_paths = g_hash_table_new_full (g_str_hash, g_str_equal, + (GDestroyNotify) g_free, (GDestroyNotify) g_free); + self->accounts_modified = g_hash_table_new_full (g_str_hash, g_str_equal, + (GDestroyNotify) g_free, NULL); +} + +static void +_dispose (GObject *object) +{ + McdAccountManagerGVariant *self = (McdAccountManagerGVariant *) object; + + tp_clear_pointer (&self->directory, g_free); + + tp_clear_pointer (&self->accounts, g_hash_table_unref); + tp_clear_pointer (&self->accounts_to_paths, g_hash_table_unref); + tp_clear_pointer (&self->accounts_modified, g_hash_table_unref); + + if (G_OBJECT_CLASS (mcd_account_manager_gvariant_parent_class)->dispose) + G_OBJECT_CLASS (mcd_account_manager_gvariant_parent_class)->dispose (object); +} + +static void +mcd_account_manager_gvariant_class_init (McdAccountManagerGVariantClass *cls) +{ + GObjectClass *oclass = (GObjectClass *) cls; + + DEBUG (""); + + oclass->dispose = _dispose; +} + +/* simply replaces '/' with '_' */ +static gchar * +escape_path (const gchar *str) +{ + guint len = strlen (str); + gchar *s = g_new0 (gchar, len + 1); + guint i; + + for (i = 0; str[i] != '\0'; i++) + { + if (str[i] == '/') + s[i] = '_'; + else + s[i] = str[i]; + } + + s[i] = '\0'; + + return s; +} + +/* We happen to know that the string MC gave us is "sufficiently escaped" to + * put it in the keyfile as-is. */ +static gboolean +_set (const McpAccountStorage *storage, + const McpAccountManager *am, + const gchar *account, + const gchar *key, + const gchar *val) +{ + McdAccountManagerGVariant *self = MCD_ACCOUNT_MANAGER_GVARIANT (storage); + GHashTable *props; + + props = g_hash_table_lookup (self->accounts, account); + if (props == NULL) + { + gchar *tmp, *path; + + props = g_hash_table_new_full (g_str_hash, g_str_equal, + (GDestroyNotify) g_free, (GDestroyNotify) tp_g_value_slice_free); + + g_hash_table_insert (self->accounts, + g_strdup (account), props); + + /* now the file path */ + tmp = escape_path (account); + path = g_build_filename (self->directory, tmp, NULL); + g_hash_table_insert (self->accounts_to_paths, + g_strdup (account), path); + g_free (tmp); + } + + if (val != NULL) + { + g_hash_table_insert (props, g_strdup (key), + tp_g_value_slice_new_string (val)); + } + else + { + g_hash_table_remove (props, key); + } + + g_hash_table_insert (self->accounts_modified, + g_strdup (account), GINT_TO_POINTER (TRUE)); + + return TRUE; +} + +static gboolean +_get (const McpAccountStorage *storage, + const McpAccountManager *am, + const gchar *account, + const gchar *key) +{ + McdAccountManagerGVariant *self = MCD_ACCOUNT_MANAGER_GVARIANT (storage); + GHashTable *asv; + + asv = g_hash_table_lookup (self->accounts, account); + if (asv == NULL) + return FALSE; + + if (key != NULL) + { + const gchar *val = tp_asv_get_string (asv, key); + + if (val == NULL) + return FALSE; + + mcp_account_manager_set_value (am, account, + key, val); + } + else + { + GHashTableIter iter; + gpointer k, v; + + g_hash_table_iter_init (&iter, asv); + while (g_hash_table_iter_next (&iter, &k, &v)) + { + mcp_account_manager_set_value (am, account, + (const gchar *) k, + g_value_get_string ((GValue *) v)); + } + } + + return TRUE; +} + +static gboolean +_delete (const McpAccountStorage *storage, + const McpAccountManager *am, + const gchar *account, + const gchar *key) +{ + McdAccountManagerGVariant *self = MCD_ACCOUNT_MANAGER_GVARIANT (storage); + const gchar *filename; + gint ret = 0; + + /* first get rid of its details (we don't care if this fails) */ + g_hash_table_remove (self->accounts, account); + g_hash_table_remove (self->accounts_modified, account); + + /* and now its file */ + filename = g_hash_table_lookup (self->accounts_to_paths, account); + if (filename != NULL) + { + ret = g_unlink (filename); + g_hash_table_remove (self->accounts_to_paths, account); + } + + return (ret == 0); +} + +static gboolean +_commit_one (McdAccountManagerGVariant *self, + const gchar *account) +{ + gboolean rval = TRUE; + gpointer p; + GHashTable *props; + + GValue v = G_VALUE_INIT; + GVariant *variant; + const gchar *filename; + gchar *str; + + /* don't bother doing anything if it hasn't been modified */ + p = g_hash_table_lookup (self->accounts_modified, account); + if (!GINT_TO_POINTER (p)) + return TRUE; + + props = g_hash_table_lookup (self->accounts, account); + filename = g_hash_table_lookup (self->accounts_to_paths, account); + if (props == NULL || filename == NULL) + return FALSE; + + /* re-add this value for serialization */ + g_hash_table_insert (props, g_strdup ("id"), + tp_g_value_slice_new_string (account)); + + /* make into a GVariant */ + g_value_init (&v, TP_HASH_TYPE_STRING_VARIANT_MAP); + g_value_set_boxed (&v, props); + variant = dbus_g_value_build_g_variant (&v); + + /* now save to disk */ + str = g_variant_print (variant, TRUE); + rval = g_file_set_contents (filename, str, -1, NULL); + + /* clean up */ + g_value_unset (&v); + g_free (str); + g_hash_table_remove (self->accounts_modified, account); + + /* and finally remove this again */ + g_hash_table_remove (props, "id"); + + return rval; +} + +static gboolean +_commit (const McpAccountStorage *storage, + const McpAccountManager *am, + const gchar *account) +{ + McdAccountManagerGVariant *self = MCD_ACCOUNT_MANAGER_GVARIANT (storage); + gboolean rval = TRUE; + + if (account != NULL) + { + rval = _commit_one (self, account); + } + else + { + GHashTableIter iter; + gpointer key; + + g_hash_table_iter_init (&iter, self->accounts); + while (rval && g_hash_table_iter_next (&iter, &key, NULL)) + { + rval = _commit_one (self, (const gchar *) key); + } + } + + return rval; +} + +static void +add_account (McdAccountManagerGVariant *self, + GVariant *variant, + const gchar *path) +{ + const gchar *account_id; + gboolean found; + GValue value = G_VALUE_INIT; + GHashTable *result; + + found = g_variant_lookup (variant, "id", "&s", &account_id); + if (!found) + return; + + dbus_g_value_parse_g_variant (variant, &value); + result = g_value_dup_boxed (&value); + g_value_unset (&value); + + /* remove this object path value now; we'll add it back just before + * we serialize back to disk. */ + g_hash_table_remove (result, "id"); + + g_hash_table_insert (self->accounts, + g_strdup (account_id), result); + g_hash_table_insert (self->accounts_to_paths, + g_strdup (account_id), g_strdup (path)); +} + +static gboolean +load_accounts (McdAccountManagerGVariant *self, + GError **error) +{ + gboolean ret = TRUE; + GFile *dir; + GFileEnumerator *enumerator; + GFileInfo *info; + + if (self->loaded) + return TRUE; + self->loaded = TRUE; + + dir = g_file_new_for_path (self->directory); + + enumerator = g_file_enumerate_children (dir, G_FILE_ATTRIBUTE_STANDARD_NAME, + G_FILE_QUERY_INFO_NONE, NULL, error); + + if (enumerator == NULL) + { + ret = FALSE; + goto out; + } + + while ((info = g_file_enumerator_next_file (enumerator, NULL, error)) != NULL) + { + const gchar *filename = g_file_info_get_name (info); + gchar *path; + gchar *contents; + + path = g_build_filename (self->directory, filename, NULL); + + /* ignore errors tbh */ + if (g_file_get_contents (path, &contents, NULL, NULL)) + { + GVariant *variant; + GError *variant_error = NULL; + + variant = g_variant_parse (G_VARIANT_TYPE_VARDICT, + contents, NULL, NULL, &variant_error); + + if (variant == NULL) + { + DEBUG ("failed to parse account <%s>: %s", filename, + variant_error->message); + g_clear_error (&variant_error); + } + else + { + add_account (self, variant, path); + g_variant_unref (variant); + } + } + else + { + DEBUG ("failed to read file: %s; ignoring", path); + } + + g_free (path); + g_object_unref (info); + } + +out: + g_clear_object (&dir); + g_clear_object (&enumerator); + + return ret; +} + +static GList * +_list (const McpAccountStorage *storage, + const McpAccountManager *am) +{ + McdAccountManagerGVariant *self = (McdAccountManagerGVariant *) storage; + GList *accounts = NULL; + GError *error = NULL; + + if (!load_accounts (self, &error)) + { + DEBUG ("failed to load accounts: %s", error->message); + g_clear_error (&error); + } + else + { + GHashTableIter iter; + gpointer key; + + g_hash_table_iter_init (&iter, self->accounts); + while (g_hash_table_iter_next (&iter, &key, NULL)) + accounts = g_list_append (accounts, g_strdup (key)); + } + + return accounts; +} + +static void +account_storage_iface_init (McpAccountStorageIface *iface, + gpointer unused G_GNUC_UNUSED) +{ + mcp_account_storage_iface_set_name (iface, PLUGIN_NAME); + mcp_account_storage_iface_set_desc (iface, PLUGIN_DESCRIPTION); + mcp_account_storage_iface_set_priority (iface, PLUGIN_PRIORITY); + + mcp_account_storage_iface_implement_get (iface, _get); + mcp_account_storage_iface_implement_set (iface, _set); + mcp_account_storage_iface_implement_delete (iface, _delete); + mcp_account_storage_iface_implement_commit_one (iface, _commit); + mcp_account_storage_iface_implement_list (iface, _list); + +} + +McdAccountManagerGVariant * +mcd_account_manager_gvariant_new (void) +{ + return g_object_new (MCD_TYPE_ACCOUNT_MANAGER_GVARIANT, NULL); +} diff --git a/src/mcd-account-manager-gvariant.h b/src/mcd-account-manager-gvariant.h new file mode 100644 index 00000000..20537c02 --- /dev/null +++ b/src/mcd-account-manager-gvariant.h @@ -0,0 +1,83 @@ +/* + * The gvariant account manager keyfile storage pseudo-plugin + * + * Copyright © 2012 Collabora Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include <mission-control-plugins/mission-control-plugins.h> + +#ifndef __MCD_ACCOUNT_MANAGER_GVARIANT_H__ +#define __MCD_ACCOUNT_MANAGER_GVARIANT_H__ + +G_BEGIN_DECLS + +#define MCD_TYPE_ACCOUNT_MANAGER_GVARIANT \ + (mcd_account_manager_gvariant_get_type ()) + +#define MCD_ACCOUNT_MANAGER_GVARIANT(o) \ + (G_TYPE_CHECK_INSTANCE_CAST ((o), MCD_TYPE_ACCOUNT_MANAGER_GVARIANT, \ + McdAccountManagerGVariant)) + +#define MCD_ACCOUNT_MANAGER_GVARIANT_CLASS(k) \ + (G_TYPE_CHECK_CLASS_CAST((k), MCD_TYPE_ACCOUNT_MANAGER_GVARIANT, \ + McdAccountManagerClass)) + +#define MCD_IS_ACCOUNT_MANAGER_GVARIANT(o) \ + (G_TYPE_CHECK_INSTANCE_TYPE ((o), MCD_TYPE_ACCOUNT_MANAGER_GVARIANT)) + +#define MCD_IS_ACCOUNT_MANAGER_GVARIANT_CLASS(k) \ + (G_TYPE_CHECK_CLASS_TYPE ((k), MCD_TYPE_ACCOUNT_MANAGER_GVARIANT)) + +#define MCD_ACCOUNT_MANAGER_GVARIANT_GET_CLASS(o) \ + (G_TYPE_INSTANCE_GET_CLASS ((o), MCD_TYPE_ACCOUNT_MANAGER_GVARIANT, \ + McdAccountManagerGVariantClass)) + +typedef struct { + GObject parent; + + gboolean loaded; + gchar *directory; + + /* owned (gchar *) of account => owned GHashTable<owned gchar*, + * slice allocated GValue> of properties */ + GHashTable *accounts; + + /* owned (gchar *) of account => owned (gchar *) of full path to + * account file */ + GHashTable *accounts_to_paths; + + /* owned (gchar *) of account => (gpointer) boolean whether account + * has changed and so needs to be written to disk. in reality the + * value is always GUINT_TO_POINTER(TRUE) or the account is simply + * absent. */ + GHashTable *accounts_modified; +} _McdAccountManagerGVariant; + +typedef struct { + GObjectClass parent_class; +} _McdAccountManagerGVariantClass; + +typedef _McdAccountManagerGVariant McdAccountManagerGVariant; +typedef _McdAccountManagerGVariantClass McdAccountManagerGVariantClass; + +GType mcd_account_manager_gvariant_get_type (void) G_GNUC_CONST; + +McdAccountManagerGVariant *mcd_account_manager_gvariant_new (void); + +G_END_DECLS + +#endif diff --git a/src/plugin-account.c b/src/plugin-account.c index 4f26b068..db7c538c 100644 --- a/src/plugin-account.c +++ b/src/plugin-account.c @@ -33,6 +33,7 @@ /* these pseudo-plugins take care of the actual account storage/retrieval */ #include "mcd-account-manager-default.h" +#include "mcd-account-manager-gvariant.h" #if ENABLE_LIBACCOUNTS_SSO #include "mcd-account-manager-sso.h" @@ -311,6 +312,7 @@ sort_and_cache_plugins () /* Add compiled-in plugins */ add_storage_plugin (MCP_ACCOUNT_STORAGE (mcd_account_manager_default_new ())); + add_storage_plugin (MCP_ACCOUNT_STORAGE (mcd_account_manager_gvariant_new ())); add_libaccounts_plugins_if_enabled (); for (p = mcp_list_objects(); p != NULL; p = g_list_next (p)) diff --git a/tests/twisted/Makefile.am b/tests/twisted/Makefile.am index 8cb78774..0cf469b1 100644 --- a/tests/twisted/Makefile.am +++ b/tests/twisted/Makefile.am @@ -84,6 +84,7 @@ TWISTED_SEPARATE_TESTS = \ account-manager/avatar-persist.py \ account-manager/avatar-refresh.py \ account-manager/device-idle.py \ + account-manager/gvariant-accounts.py \ account-manager/make-valid.py \ crash-recovery/crash-recovery.py \ dispatcher/create-at-startup.py diff --git a/tests/twisted/account-manager/gvariant-accounts.py b/tests/twisted/account-manager/gvariant-accounts.py new file mode 100644 index 00000000..3f271be6 --- /dev/null +++ b/tests/twisted/account-manager/gvariant-accounts.py @@ -0,0 +1,130 @@ +# Copyright (C) 2012 Collabora Ltd. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA + +import dbus +import dbus.service +import os +import sys + +from servicetest import EventPattern, call_async, assertEquals +from mctest import exec_test, AccountManager, Account, MC +import constants as cs + +account_prefix = cs.tp_path_prefix + '/Account/' +account1_id = 'fakecm/fakeprotocol/jc_2edenton_40unatco_2eint' +filename = None + +def preseed(): + global filename + + accounts_dir = os.environ['MC_ACCOUNT_DIR'] + + escaped = account1_id.replace('/', '_') + filename = accounts_dir + '/' + escaped + account = open(filename, 'w') + account.write("""{ +'id': <'%s'>, +'manager': <'fakecm'>, +'protocol': <'fakeprotocol'>, +'DisplayName': <'Work account'>, +'NormalizedName': <'jc.denton@unatco.int'>, +'param-account': <'jc.denton@unatco.int'>, +'param-password': <'ionstorm'>, +'Enabled': <'true'>, +'ConnectAutomatically': <'true'>, +'AutomaticPresenceType': <'2'>, +'AutomaticPresenceMessage': <'My vision is augmented'>, +'Nickname': <'JC'>, +'AvatarMime': <'image/jpeg'>, +'avatar_token': <'Deus Ex'> +}""" % account1_id) + account.close() + +def test(q, bus, unused): + global filename + + expected_params = { + 'account': 'jc.denton@unatco.int', + 'password': 'ionstorm', + } + + mc = MC(q, bus) + + am = AccountManager(bus) + + # make sure we have the right accounts + props = am.GetAll(cs.AM, dbus_interface=dbus.PROPERTIES_IFACE) + accounts = props['ValidAccounts'] + + assertEquals(1, len(accounts)) + path = accounts[0] + assertEquals(account_prefix + account1_id, path) + + account = Account(bus, path) + + # make sure the account has got the right properties + props = account.GetAll(cs.ACCOUNT, dbus_interface=dbus.PROPERTIES_IFACE) + + assertEquals('Work account', props['DisplayName']) + assertEquals('jc.denton@unatco.int', props['NormalizedName']) + assertEquals(expected_params, props['Parameters']) + assertEquals(True, props['Enabled']) + assertEquals(True, props['ConnectAutomatically']) + assertEquals((cs.PRESENCE_TYPE_AVAILABLE, '', 'My vision is augmented'), + props['AutomaticPresence']) + assertEquals('JC', props['Nickname']) + + props = account.GetAll(cs.ACCOUNT_IFACE_AVATAR, dbus_interface=dbus.PROPERTIES_IFACE) + avatar = props['Avatar'] + assertEquals('image/jpeg', avatar[1]) + + # now delete the account and make sure the file is removed + assert os.path.exists(filename) + + account.Remove() + + q.expect_many(EventPattern('dbus-signal', signal='Removed'), + EventPattern('dbus-signal', signal='AccountRemoved')) + + assert not os.path.exists(filename) + + accounts = am.Get(cs.AM, 'ValidAccounts', dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals(0, len(accounts)) + + # create an account and assert the file is present + parameters = { + 'account': 'dontdivert@bar.com', + 'password': 'le password' + } + + accounts_dir = os.environ['MC_ACCOUNT_DIR'] + name = 'fakecm/fakeprotocol/dontdivert_40bar_2ecom0' + second_filename = os.path.join(accounts_dir, name.replace('/', '_')) + assert not os.path.exists(second_filename) + + path = am.CreateAccount('fakecm', 'fakeprotocol', 'Display name', parameters, {}) + + e = q.expect('dbus-signal', signal='AccountValidityChanged') + object_path, valid = e.args + assertEquals(path, object_path) + assert valid + + assert os.path.exists(second_filename) + +if __name__ == '__main__': + preseed() + exec_test(test, {}, preload_mc=False) |