[epiphany/wip/sync: 7/13] sync: Implement saved passwords sync
- From: Gabriel Ivașcu <gabrielivascu src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [epiphany/wip/sync: 7/13] sync: Implement saved passwords sync
- Date: Tue, 30 May 2017 19:30:44 +0000 (UTC)
commit c6199b69587aa58e01100f799c0ae64029ee67ff
Author: Gabriel Ivascu <ivascu gabriel59 gmail com>
Date: Sun Apr 30 22:11:15 2017 +0300
sync: Implement saved passwords sync
Saved passwords are added from the web process in EphyWebExtension.
Because of this, EphyWebExtension needs to have its own instance of sync
service to upload newly added passwords. Because sign in and sign out
happen in the sync service from the UI process, the web process
needs to figure out on its own when the sync user has changed (i.e. the
user signed in or signed out) and create a new sync service that will
load the new user-specific secret data from disk (which was previously
stored by the sync service from the UI process at sign in). This way the
passwords added in the future are uploaded to the correct sync user and
not to an invalid user. Moreover, the web process needs to
register/unregister the password manager to the sync service whenever
EPHY_PREFS_SYNC_PASSWORDS_ENABLED changes.
data/org.gnome.epiphany.gschema.xml | 15 +
embed/meson.build | 1 +
embed/web-extension/ephy-embed-form-auth.c | 14 +
embed/web-extension/ephy-embed-form-auth.h | 23 +-
embed/web-extension/ephy-web-extension.c | 147 +++-
embed/web-extension/meson.build | 1 +
lib/ephy-password-manager.c | 483 ------------
lib/ephy-password-manager.h | 77 --
lib/ephy-password-record.c | 236 ------
lib/ephy-password-record.h | 45 --
lib/ephy-prefs.h | 3 +
lib/meson.build | 2 -
lib/sync/ephy-password-manager.c | 1077 ++++++++++++++++++++++++++
lib/sync/ephy-password-manager.h | 82 ++
lib/sync/ephy-password-record.c | 370 +++++++++
lib/sync/ephy-password-record.h | 50 ++
lib/sync/ephy-sync-service.c | 220 ++++--
lib/sync/ephy-sync-service.h | 2 +-
lib/sync/ephy-synchronizable-manager.c | 54 +-
lib/sync/ephy-synchronizable-manager.h | 26 +-
lib/sync/meson.build | 2 +
meson.build | 2 +-
src/bookmarks/ephy-bookmarks-manager.c | 34 +-
src/ephy-shell.c | 11 +-
src/passwords-dialog.c | 26 +-
src/prefs-dialog.c | 34 +-
src/profile-migrator/ephy-profile-migrator.c | 24 +-
src/resources/gtk/prefs-dialog.ui | 7 +
28 files changed, 2063 insertions(+), 1005 deletions(-)
---
diff --git a/data/org.gnome.epiphany.gschema.xml b/data/org.gnome.epiphany.gschema.xml
index 03a974d..3635eed 100644
--- a/data/org.gnome.epiphany.gschema.xml
+++ b/data/org.gnome.epiphany.gschema.xml
@@ -307,6 +307,21 @@
<summary>Initial sync or normal sync</summary>
<description>TRUE if bookmarks collection needs to be synced for the first time,
FALSE otherwise.</description>
</key>
+ <key type="b" name="sync-passwords-enabled">
+ <default>false</default>
+ <summary>Enable passwords sync</summary>
+ <description>TRUE if passwords collection should be synced, FALSE
otherwise.</description>
+ </key>
+ <key type="d" name="sync-passwords-time">
+ <default>0</default>
+ <summary>Passwords sync timestamp</summary>
+ <description>The timestamp at which last passwords sync was made.</description>
+ </key>
+ <key type="b" name="sync-passwords-initial">
+ <default>true</default>
+ <summary>Initial sync or normal sync</summary>
+ <description>TRUE if passwords collection needs to be synced for the first time,
FALSE otherwise.</description>
+ </key>
</schema>
<enum id="org.gnome.Epiphany.Permission">
<value nick="undecided" value="-1"/>
diff --git a/embed/meson.build b/embed/meson.build
index 99812c5..f6d2297 100644
--- a/embed/meson.build
+++ b/embed/meson.build
@@ -54,6 +54,7 @@ libephyembed_includes = include_directories(
'../lib/contrib',
'../lib/contrib/gvdb',
'../lib/history',
+ '../lib/sync',
'../lib/widgets',
'../lib/widgets/contrib'
)
diff --git a/embed/web-extension/ephy-embed-form-auth.c b/embed/web-extension/ephy-embed-form-auth.c
index 930cb3a..ae36027 100644
--- a/embed/web-extension/ephy-embed-form-auth.c
+++ b/embed/web-extension/ephy-embed-form-auth.c
@@ -29,6 +29,7 @@ struct _EphyEmbedFormAuth {
WebKitDOMNode *username_node;
WebKitDOMNode *password_node;
char *username;
+ gboolean password_updated;
};
G_DEFINE_TYPE (EphyEmbedFormAuth, ephy_embed_form_auth, G_TYPE_OBJECT)
@@ -110,6 +111,19 @@ ephy_embed_form_auth_get_username (EphyEmbedFormAuth *form_auth)
return form_auth->username;
}
+gboolean
+ephy_embed_form_auth_get_password_updated (EphyEmbedFormAuth *form_auth)
+{
+ return form_auth->password_updated;
+}
+
+void
+ephy_embed_form_auth_set_password_updated (EphyEmbedFormAuth *form_auth,
+ gboolean password_updated)
+{
+ form_auth->password_updated = password_updated;
+}
+
WebKitDOMDocument *
ephy_embed_form_auth_get_owner_document (EphyEmbedFormAuth *form_auth)
{
diff --git a/embed/web-extension/ephy-embed-form-auth.h b/embed/web-extension/ephy-embed-form-auth.h
index cb78e06..3266771 100644
--- a/embed/web-extension/ephy-embed-form-auth.h
+++ b/embed/web-extension/ephy-embed-form-auth.h
@@ -30,15 +30,18 @@ G_BEGIN_DECLS
G_DECLARE_FINAL_TYPE (EphyEmbedFormAuth, ephy_embed_form_auth, EPHY, EMBED_FORM_AUTH, GObject)
-EphyEmbedFormAuth *ephy_embed_form_auth_new (WebKitWebPage *web_page,
- WebKitDOMNode *username_node,
- WebKitDOMNode *password_node,
- const char *username);
-WebKitDOMNode *ephy_embed_form_auth_get_username_node (EphyEmbedFormAuth *form_auth);
-WebKitDOMNode *ephy_embed_form_auth_get_password_node (EphyEmbedFormAuth *form_auth);
-SoupURI *ephy_embed_form_auth_get_uri (EphyEmbedFormAuth *form_auth);
-guint64 ephy_embed_form_auth_get_page_id (EphyEmbedFormAuth *form_auth);
-const char *ephy_embed_form_auth_get_username (EphyEmbedFormAuth *form_auth);
-WebKitDOMDocument *ephy_embed_form_auth_get_owner_document(EphyEmbedFormAuth *form_auth);
+EphyEmbedFormAuth *ephy_embed_form_auth_new (WebKitWebPage *web_page,
+ WebKitDOMNode *username_node,
+ WebKitDOMNode *password_node,
+ const char *username);
+WebKitDOMNode *ephy_embed_form_auth_get_username_node (EphyEmbedFormAuth *form_auth);
+WebKitDOMNode *ephy_embed_form_auth_get_password_node (EphyEmbedFormAuth *form_auth);
+SoupURI *ephy_embed_form_auth_get_uri (EphyEmbedFormAuth *form_auth);
+guint64 ephy_embed_form_auth_get_page_id (EphyEmbedFormAuth *form_auth);
+const char *ephy_embed_form_auth_get_username (EphyEmbedFormAuth *form_auth);
+gboolean ephy_embed_form_auth_get_password_updated (EphyEmbedFormAuth *form_auth);
+void ephy_embed_form_auth_set_password_updated (EphyEmbedFormAuth *form_auth,
+ gboolean password_updated);
+WebKitDOMDocument *ephy_embed_form_auth_get_owner_document (EphyEmbedFormAuth *form_auth);
G_END_DECLS
diff --git a/embed/web-extension/ephy-web-extension.c b/embed/web-extension/ephy-web-extension.c
index b628358..74a271b 100644
--- a/embed/web-extension/ephy-web-extension.c
+++ b/embed/web-extension/ephy-web-extension.c
@@ -30,6 +30,7 @@
#include "ephy-permissions-manager.h"
#include "ephy-prefs.h"
#include "ephy-settings.h"
+#include "ephy-sync-service.h"
#include "ephy-uri-helpers.h"
#include "ephy-uri-tester.h"
#include "ephy-web-dom-utils.h"
@@ -51,6 +52,7 @@ struct _EphyWebExtension {
GDBusConnection *dbus_connection;
GArray *page_created_signals_pending;
+ EphySyncService *sync_service;
EphyPasswordManager *password_manager;
GHashTable *form_auth_data_save_requests;
EphyWebOverviewModel *overview_model;
@@ -267,6 +269,7 @@ store_password (EphyEmbedFormAuth *form_auth)
char *username_field_value = NULL;
char *password_field_name = NULL;
char *password_field_value = NULL;
+ gboolean password_updated;
WebKitDOMNode *username_node;
EphyWebExtension *extension = ephy_web_extension_get ();
@@ -283,12 +286,14 @@ store_password (EphyEmbedFormAuth *form_auth)
uri = ephy_embed_form_auth_get_uri (form_auth);
uri_str = soup_uri_to_string (uri, FALSE);
+ password_updated = ephy_embed_form_auth_get_password_updated (form_auth);
ephy_password_manager_save (extension->password_manager,
uri_str,
+ username_field_value,
+ password_field_value,
username_field_name,
password_field_name,
- username_field_value,
- password_field_value);
+ !password_updated);
g_free (uri_str);
g_free (username_field_name);
@@ -417,15 +422,18 @@ should_store_cb (GSList *records,
LOG ("User/password already stored. Not asking about storing.");
} else if (permission == EPHY_PERMISSION_PERMIT) {
LOG ("User/password not yet stored. Storing.");
+ ephy_embed_form_auth_set_password_updated (form_auth, TRUE);
store_password (form_auth);
} else {
LOG ("User/password not yet stored. Asking about storing.");
+ ephy_embed_form_auth_set_password_updated (form_auth, TRUE);
request_decision_on_storing (g_object_ref (form_auth));
}
g_free (username_field_value);
} else {
LOG ("No result on query; asking whether we should store.");
+ ephy_embed_form_auth_set_password_updated (form_auth, FALSE);
request_decision_on_storing (g_object_ref (form_auth));
}
@@ -475,9 +483,9 @@ form_submitted_cb (WebKitDOMHTMLFormElement *dom_form,
ephy_password_manager_query (extension->password_manager,
uri_str,
+ username_field_value,
username_field_name,
password_field_name,
- username_field_value,
should_store_cb,
form_auth);
@@ -555,9 +563,9 @@ pre_fill_form (EphyEmbedFormAuth *form_auth)
ephy_password_manager_query (extension->password_manager,
uri_str,
+ username,
username_field_name,
password_field_name,
- username,
fill_form_cb,
form_auth);
@@ -730,8 +738,7 @@ show_user_choices (WebKitDOMDocument *document,
WebKitDOMNode *body;
WebKitDOMElement *main_div;
WebKitDOMElement *ul;
- GSList *iter;
- GSList *password_records_list;
+ GSList *cached_users;
gboolean username_node_ever_edited;
double x, y;
double input_width;
@@ -777,32 +784,27 @@ show_user_choices (WebKitDOMDocument *document,
"padding: 0;",
NULL);
- password_records_list = (GSList *)g_object_get_data (G_OBJECT (username_node),
- "ephy-password-records-list");
+ cached_users = (GSList *)g_object_get_data (G_OBJECT (username_node), "ephy-cached-users");
username_node_ever_edited =
GPOINTER_TO_INT (g_object_get_data (G_OBJECT (username_node),
"ephy-user-ever-edited"));
- for (iter = password_records_list; iter; iter = iter->next) {
- EphyPasswordRecord *record;
+ for (GSList *l = cached_users; l && l->data; l = l->next) {
+ const char *user = l->data;
WebKitDOMElement *li;
WebKitDOMElement *anchor;
char *child_style;
- const char *record_username;
gboolean is_selected;
- record = EPHY_PASSWORD_RECORD (iter->data);
- record_username = ephy_password_record_get_username (record);
-
/* Filter out the available names that do not match, but show all options in
* case we have been triggered by something other than the user editing the
* input.
*/
- if (username_node_ever_edited && !g_str_has_prefix (record_username, username))
+ if (username_node_ever_edited && !g_str_has_prefix (user, username))
continue;
- is_selected = !g_strcmp0 (username, record_username);
+ is_selected = !g_strcmp0 (username, user);
li = webkit_dom_document_create_element (document, "li", NULL);
webkit_dom_element_set_attribute (li, "tabindex", "-1", NULL);
@@ -834,7 +836,7 @@ show_user_choices (WebKitDOMDocument *document,
username_node);
webkit_dom_node_set_text_content (WEBKIT_DOM_NODE (anchor),
- record_username,
+ user,
NULL);
}
@@ -1108,7 +1110,7 @@ web_page_form_controls_associated (WebKitWebPage *web_page,
/* We have a field that may be the user, and one for a password. */
if (ephy_web_dom_utils_find_form_auth_elements (form, &username_node, &password_node)) {
EphyEmbedFormAuth *form_auth;
- GSList *password_records_list;
+ GSList *cached_users;
const char *uri;
LOG ("Hooking and pre-filling a form");
@@ -1126,11 +1128,11 @@ web_page_form_controls_associated (WebKitWebPage *web_page,
/* Plug in the user autocomplete */
uri = webkit_web_page_get_uri (web_page);
- password_records_list = ephy_password_manager_get_cached_by_uri (extension->password_manager, uri);
+ cached_users = ephy_password_manager_get_cached_users_for_uri (extension->password_manager, uri);
- if (password_records_list && password_records_list->next && username_node) {
+ if (cached_users && cached_users->next && username_node) {
LOG ("More than 1 password saved, hooking menu for choosing which on focus");
- g_object_set_data (G_OBJECT (username_node), "ephy-password-records-list", password_records_list);
+ g_object_set_data (G_OBJECT (username_node), "ephy-cached-users", cached_users);
g_object_set_data (G_OBJECT (username_node), "ephy-form-auth", form_auth);
g_object_set_data (G_OBJECT (username_node), "ephy-document", document);
webkit_dom_event_target_add_event_listener (WEBKIT_DOM_EVENT_TARGET (username_node), "input",
@@ -1149,7 +1151,7 @@ web_page_form_controls_associated (WebKitWebPage *web_page,
G_CALLBACK (username_node_changed_cb), FALSE,
web_page);
} else
- LOG ("No items or a single item in password_records_list, not hooking menu for choosing.");
+ LOG ("No items or a single item in cached_users, not hooking menu for choosing.");
pre_fill_form (form_auth);
@@ -1445,6 +1447,87 @@ static const GDBusInterfaceVTable interface_vtable = {
};
static void
+ephy_prefs_sync_passwords_enabled_cb (GSettings *settings,
+ char *key,
+ gpointer user_data)
+{
+ EphyWebExtension *extension;
+ EphySynchronizableManager *manager;
+
+ extension = EPHY_WEB_EXTENSION (user_data);
+ manager = EPHY_SYNCHRONIZABLE_MANAGER (extension->password_manager);
+
+ if (g_settings_get_boolean (settings, key))
+ ephy_sync_service_register_manager (extension->sync_service, manager);
+ else
+ ephy_sync_service_unregister_manager (extension->sync_service, manager);
+}
+
+static void
+ephy_web_extension_create_sync_service (EphyWebExtension *extension)
+{
+ EphySynchronizableManager *manager;
+
+ g_assert (EPHY_IS_WEB_EXTENSION (extension));
+ g_assert (EPHY_IS_PASSWORD_MANAGER (extension->password_manager));
+ g_assert (!extension->sync_service);
+
+ extension->sync_service = ephy_sync_service_new (FALSE);
+ manager = EPHY_SYNCHRONIZABLE_MANAGER (extension->password_manager);
+
+ if (g_settings_get_boolean (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_PASSWORDS_ENABLED))
+ ephy_sync_service_register_manager (extension->sync_service, manager);
+
+ g_signal_connect (EPHY_SETTINGS_SYNC, "changed::"EPHY_PREFS_SYNC_PASSWORDS_ENABLED,
+ G_CALLBACK (ephy_prefs_sync_passwords_enabled_cb), extension);
+}
+
+static void
+ephy_web_extension_destroy_sync_service (EphyWebExtension *extension)
+{
+ EphySynchronizableManager *manager;
+
+ g_assert (EPHY_IS_WEB_EXTENSION (extension));
+ g_assert (EPHY_IS_PASSWORD_MANAGER (extension->password_manager));
+ g_assert (EPHY_IS_SYNC_SERVICE (extension->sync_service));
+
+ manager = EPHY_SYNCHRONIZABLE_MANAGER (extension->password_manager);
+ ephy_sync_service_unregister_manager (extension->sync_service, manager);
+ g_signal_handlers_disconnect_by_func (EPHY_SETTINGS_SYNC,
+ ephy_prefs_sync_passwords_enabled_cb,
+ extension);
+
+ g_clear_object (&extension->sync_service);
+}
+
+static void
+ephy_prefs_sync_user_cb (GSettings *settings,
+ char *key,
+ gpointer user_data)
+{
+ EphyWebExtension *extension;
+ char *sync_user;
+
+ /* If the sync user has changed we need to destroy the previous sync service
+ * (which is no longer valid because the user specific data has been cleared)
+ * and create a new one which will load the new user specific data. This way
+ * we will correctly upload new saved passwords in the future.
+ */
+ extension = EPHY_WEB_EXTENSION (user_data);
+ sync_user = g_settings_get_string (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_USER);
+
+ if (!g_strcmp0 (sync_user, "")) {
+ /* Signed out. */
+ ephy_web_extension_destroy_sync_service (extension);
+ } else {
+ /* Signed in. */
+ ephy_web_extension_create_sync_service (extension);
+ }
+
+ g_free (sync_user);
+}
+
+static void
ephy_web_extension_dispose (GObject *object)
{
EphyWebExtension *extension = EPHY_WEB_EXTENSION (object);
@@ -1452,7 +1535,12 @@ ephy_web_extension_dispose (GObject *object)
g_clear_object (&extension->uri_tester);
g_clear_object (&extension->overview_model);
g_clear_object (&extension->permissions_manager);
- g_clear_object (&extension->password_manager);
+
+ if (extension->password_manager) {
+ if (extension->sync_service)
+ ephy_web_extension_destroy_sync_service (extension);
+ g_clear_object (&extension->password_manager);
+ }
if (extension->form_auth_data_save_requests) {
g_hash_table_destroy (extension->form_auth_data_save_requests);
@@ -1562,9 +1650,20 @@ ephy_web_extension_initialize (EphyWebExtension *extension,
extension->initialized = TRUE;
extension->extension = g_object_ref (wk_extension);
- if (!is_private_profile)
+ if (!is_private_profile) {
+ char *sync_user = g_settings_get_string (EPHY_SETTINGS_SYNC,
+ EPHY_PREFS_SYNC_USER);
extension->password_manager = ephy_password_manager_new ();
+ if (g_strcmp0 (sync_user, ""))
+ ephy_web_extension_create_sync_service (extension);
+
+ g_signal_connect (EPHY_SETTINGS_SYNC, "changed::"EPHY_PREFS_SYNC_USER,
+ G_CALLBACK (ephy_prefs_sync_user_cb), extension);
+
+ g_free (sync_user);
+ }
+
extension->permissions_manager = ephy_permissions_manager_new ();
g_signal_connect_swapped (extension->extension, "page-created",
diff --git a/embed/web-extension/meson.build b/embed/web-extension/meson.build
index de6be46..af53a64 100644
--- a/embed/web-extension/meson.build
+++ b/embed/web-extension/meson.build
@@ -10,6 +10,7 @@ web_extension_sources = [
web_extension_deps = [
ephymisc_dep,
+ ephysync_dep,
webkit2gtk_web_extension_dep
]
diff --git a/lib/ephy-prefs.h b/lib/ephy-prefs.h
index 7c11392..b6be084 100644
--- a/lib/ephy-prefs.h
+++ b/lib/ephy-prefs.h
@@ -159,6 +159,9 @@ static const char * const ephy_prefs_web_schema[] = {
#define EPHY_PREFS_SYNC_BOOKMARKS_ENABLED "sync-bookmarks-enabled"
#define EPHY_PREFS_SYNC_BOOKMARKS_TIME "sync-bookmarks-time"
#define EPHY_PREFS_SYNC_BOOKMARKS_INITIAL "sync-bookmarks-initial"
+#define EPHY_PREFS_SYNC_PASSWORDS_ENABLED "sync-passwords-enabled"
+#define EPHY_PREFS_SYNC_PASSWORDS_TIME "sync-passwords-time"
+#define EPHY_PREFS_SYNC_PASSWORDS_INITIAL "sync-passwords-initial"
static struct {
const char *schema;
diff --git a/lib/meson.build b/lib/meson.build
index 6e88051..b81c721 100644
--- a/lib/meson.build
+++ b/lib/meson.build
@@ -22,8 +22,6 @@ libephymisc_sources = [
'ephy-filters-manager.c',
'ephy-gui.c',
'ephy-langs.c',
- 'ephy-password-manager.c',
- 'ephy-password-record.c',
'ephy-permissions-manager.c',
'ephy-profile-utils.c',
'ephy-search-engine-manager.c',
diff --git a/lib/sync/ephy-password-manager.c b/lib/sync/ephy-password-manager.c
new file mode 100644
index 0000000..cd2055a
--- /dev/null
+++ b/lib/sync/ephy-password-manager.c
@@ -0,0 +1,1077 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2017 Gabriel Ivascu <ivascu gabriel59 gmail com>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Epiphany 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 Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include "ephy-password-manager.h"
+
+#include "ephy-debug.h"
+#include "ephy-settings.h"
+#include "ephy-synchronizable-manager.h"
+#include "ephy-uri-helpers.h"
+
+#include <glib/gi18n.h>
+#include <stdio.h>
+
+const SecretSchema *
+ephy_password_manager_get_password_schema (void)
+{
+ static const SecretSchema schema = {
+ "org.epiphany.FormPassword", SECRET_SCHEMA_NONE,
+ {
+ { ID_KEY, SECRET_SCHEMA_ATTRIBUTE_STRING },
+ { HOSTNAME_KEY, SECRET_SCHEMA_ATTRIBUTE_STRING },
+ { USERNAME_FIELD_KEY, SECRET_SCHEMA_ATTRIBUTE_STRING },
+ { PASSWORD_FIELD_KEY, SECRET_SCHEMA_ATTRIBUTE_STRING },
+ { USERNAME_KEY, SECRET_SCHEMA_ATTRIBUTE_STRING },
+ { SERVER_TIME_MODIFIED_KEY, SECRET_SCHEMA_ATTRIBUTE_STRING},
+ { "NULL", 0 },
+ }
+ };
+ return &schema;
+}
+
+struct _EphyPasswordManager {
+ GObject parent_instance;
+
+ GHashTable *cache;
+};
+
+static void ephy_synchronizable_manager_iface_init (EphySynchronizableManagerInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (EphyPasswordManager, ephy_password_manager, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (EPHY_TYPE_SYNCHRONIZABLE_MANAGER,
+ ephy_synchronizable_manager_iface_init))
+
+typedef struct {
+ EphyPasswordManagerQueryCallback callback;
+ gpointer user_data;
+} QueryAsyncData;
+
+typedef struct {
+ EphyPasswordManager *manager;
+ char *password;
+} UpdatePasswordAsyncData;
+
+typedef struct {
+ EphyPasswordManager *manager;
+ EphyPasswordRecord *record;
+} ReplaceRecordAsyncData;
+
+typedef struct {
+ EphyPasswordManager *manager;
+ gboolean is_initial;
+ GSList *remotes_deleted;
+ GSList *remotes_updated;
+ EphySynchronizableManagerMergeCallback callback;
+ gpointer user_data;
+} MergeAsyncData;
+
+static QueryAsyncData *
+query_async_data_new (EphyPasswordManagerQueryCallback callback,
+ gpointer user_data)
+{
+ QueryAsyncData *data;
+
+ data = g_slice_new (QueryAsyncData);
+ data->callback = callback;
+ data->user_data = user_data;
+
+ return data;
+}
+
+static void
+query_async_data_free (QueryAsyncData *data)
+{
+ g_assert (data);
+
+ g_slice_free (QueryAsyncData, data);
+}
+
+static UpdatePasswordAsyncData *
+update_password_async_data_new (EphyPasswordManager *manager,
+ const char *password)
+{
+ UpdatePasswordAsyncData *data;
+
+ data = g_slice_new (UpdatePasswordAsyncData);
+ data->manager = g_object_ref (manager);
+ data->password = g_strdup (password);
+
+ return data;
+}
+
+static void
+update_password_async_data_free (UpdatePasswordAsyncData *data)
+{
+ g_assert (data);
+
+ g_object_unref (data->manager);
+ g_free (data->password);
+ g_slice_free (UpdatePasswordAsyncData, data);
+}
+
+static MergeAsyncData *
+merge_async_data_new (EphyPasswordManager *manager,
+ gboolean is_initial,
+ GSList *remotes_deleted,
+ GSList *remotes_updated,
+ EphySynchronizableManagerMergeCallback callback,
+ gpointer user_data)
+{
+ MergeAsyncData *data;
+
+ data = g_slice_new (MergeAsyncData);
+ data->manager = g_object_ref (manager);
+ data->is_initial = is_initial;
+ data->remotes_deleted = remotes_deleted;
+ data->remotes_updated = remotes_updated;
+ data->callback = callback;
+ data->user_data = user_data;
+
+ return data;
+}
+
+static void
+merge_async_data_free (MergeAsyncData *data)
+{
+ g_assert (data);
+
+ g_object_unref (data->manager);
+ g_slice_free (MergeAsyncData, data);
+}
+
+static ReplaceRecordAsyncData *
+replace_record_async_data_new (EphyPasswordManager *manager,
+ EphyPasswordRecord *record)
+{
+ ReplaceRecordAsyncData *data;
+
+ data = g_slice_new (ReplaceRecordAsyncData);
+ data->manager = g_object_ref (manager);
+ data->record = g_object_ref (record);
+
+ return data;
+}
+
+static void
+replace_record_async_data_free (ReplaceRecordAsyncData *data)
+{
+ g_assert (data);
+
+ g_object_unref (data->manager);
+ g_object_unref (data->record);
+ g_slice_free (ReplaceRecordAsyncData, data);
+}
+
+static GHashTable *
+get_attributes_table (const char *id,
+ const char *uri,
+ const char *username,
+ const char *username_field,
+ const char *password_field,
+ double server_time_modified)
+{
+ GHashTable *attributes = secret_attributes_build (EPHY_FORM_PASSWORD_SCHEMA, NULL);
+
+ if (id)
+ g_hash_table_insert (attributes,
+ g_strdup (ID_KEY),
+ g_strdup (id));
+ if (uri)
+ g_hash_table_insert (attributes,
+ g_strdup (HOSTNAME_KEY),
+ ephy_uri_to_security_origin (uri));
+ if (username)
+ g_hash_table_insert (attributes,
+ g_strdup (USERNAME_KEY),
+ g_strdup (username));
+ if (username_field)
+ g_hash_table_insert (attributes,
+ g_strdup (USERNAME_FIELD_KEY),
+ g_strdup (username_field));
+ if (password_field)
+ g_hash_table_insert (attributes,
+ g_strdup (PASSWORD_FIELD_KEY),
+ g_strdup (password_field));
+ if (server_time_modified >= 0)
+ g_hash_table_insert (attributes,
+ g_strdup (SERVER_TIME_MODIFIED_KEY),
+ g_strdup_printf ("%.2lf", server_time_modified));
+
+ return attributes;
+}
+
+static void
+ephy_password_manager_cache_clear (EphyPasswordManager *self)
+{
+ GHashTableIter iter;
+ gpointer key, value;
+
+ g_assert (EPHY_IS_PASSWORD_MANAGER (self));
+ g_assert (self->cache);
+
+ g_hash_table_iter_init (&iter, self->cache);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ g_slist_free_full (value, g_free);
+ g_hash_table_remove_all (self->cache);
+}
+
+static void
+ephy_password_manager_cache_remove (EphyPasswordManager *self,
+ const char *hostname,
+ const char *username)
+{
+ GSList *usernames;
+ GSList *new_usernames = NULL;
+
+ g_assert (EPHY_IS_PASSWORD_MANAGER (self));
+ g_assert (self->cache);
+ g_assert (hostname);
+ g_assert (username);
+
+ usernames = g_hash_table_lookup (self->cache, hostname);
+ if (usernames) {
+ for (GSList *l = usernames; l && l->data; l = l->next) {
+ if (g_strcmp0 (username, l->data))
+ new_usernames = g_slist_prepend (new_usernames, g_strdup (l->data));
+ }
+ g_hash_table_replace (self->cache, g_strdup (hostname), new_usernames);
+ g_slist_free_full (usernames, g_free);
+ }
+}
+
+static void
+ephy_password_manager_cache_add (EphyPasswordManager *self,
+ const char *hostname,
+ const char *username)
+{
+ GSList *usernames;
+
+ g_assert (EPHY_IS_PASSWORD_MANAGER (self));
+ g_assert (self->cache);
+ g_assert (hostname);
+ g_assert (username);
+
+ usernames = g_hash_table_lookup (self->cache, hostname);
+ for (GSList *l = usernames; l && l->data; l = l->next) {
+ if (!g_strcmp0 (username, l->data))
+ return;
+ }
+ usernames = g_slist_prepend (usernames, g_strdup (username));
+ g_hash_table_replace (self->cache, g_strdup (hostname), usernames);
+}
+
+static void
+populate_cache_cb (GSList *records,
+ gpointer user_data)
+{
+ EphyPasswordManager *self = EPHY_PASSWORD_MANAGER (user_data);
+
+ for (GSList *l = records; l && l->data; l = l->next) {
+ EphyPasswordRecord *record = EPHY_PASSWORD_RECORD (l->data);
+ const char *hostname = ephy_password_record_get_hostname (record);
+ const char *username = ephy_password_record_get_username (record);
+
+ ephy_password_manager_cache_add (self, hostname, username);
+ }
+
+ g_slist_free_full (records, g_object_unref);
+}
+
+static void
+ephy_password_manager_dispose (GObject *object)
+{
+ EphyPasswordManager *self = EPHY_PASSWORD_MANAGER (object);
+
+ if (self->cache) {
+ ephy_password_manager_cache_clear (self);
+ g_clear_pointer (&self->cache, g_hash_table_unref);
+ }
+
+ G_OBJECT_CLASS (ephy_password_manager_parent_class)->dispose (object);
+}
+
+static void
+ephy_password_manager_class_init (EphyPasswordManagerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = ephy_password_manager_dispose;
+}
+
+static void
+ephy_password_manager_init (EphyPasswordManager *self)
+{
+ LOG ("Loading usernames into internal cache...");
+ self->cache = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+ ephy_password_manager_query (self, NULL, NULL, NULL, NULL,
+ populate_cache_cb, self);
+}
+
+EphyPasswordManager *
+ephy_password_manager_new (void)
+{
+ return EPHY_PASSWORD_MANAGER (g_object_new (EPHY_TYPE_PASSWORD_MANAGER, NULL));
+}
+
+GSList *
+ephy_password_manager_get_cached_users_for_uri (EphyPasswordManager *self,
+ const char *uri)
+{
+ GSList *list;
+ char *hostname;
+
+ g_return_val_if_fail (EPHY_IS_PASSWORD_MANAGER (self), NULL);
+ g_return_val_if_fail (uri, NULL);
+
+ hostname = ephy_uri_to_security_origin (uri);
+ list = g_hash_table_lookup (self->cache, hostname);
+ g_free (hostname);
+
+ return list;
+}
+
+static void
+secret_service_store_cb (SecretService *service,
+ GAsyncResult *result,
+ GTask *task)
+{
+ GError *error = NULL;
+
+ secret_service_store_finish (service, result, &error);
+ if (error)
+ g_task_return_error (task, error);
+ else
+ g_task_return_boolean (task, TRUE);
+
+ g_object_unref (task);
+}
+
+static void
+store_internal (const char *password,
+ GHashTable *attributes,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ SecretValue *value;
+ GTask *task;
+ const char *hostname;
+ const char *username;
+ char *label;
+
+ g_assert (password);
+ g_assert (attributes);
+
+ task = g_task_new (NULL, NULL, callback, user_data);
+ value = secret_value_new (password, -1, "text/plain");
+ hostname = g_hash_table_lookup (attributes, HOSTNAME_KEY);
+ username = g_hash_table_lookup (attributes, USERNAME_KEY);
+
+ if (username) {
+ /* Translators: The first %s is the username and the second one is the
+ * security origin where this is happening. Example: gnome gmail com and
+ * https://mail.google.com. */
+ label = g_strdup_printf (_("Password for %s in a form in %s"), username, hostname);
+ } else {
+ /* Translators: The %s is the security origin where this is happening.
+ * Example: https://mail.google.com. */
+ label = g_strdup_printf (_("Password in a form in %s"), hostname);
+ }
+
+ LOG ("Storing password record for (%s, %s, %s, %s)",
+ hostname, username,
+ (char *)g_hash_table_lookup (attributes, USERNAME_FIELD_KEY),
+ (char *)g_hash_table_lookup (attributes, PASSWORD_FIELD_KEY));
+
+ secret_service_store (NULL, EPHY_FORM_PASSWORD_SCHEMA,
+ attributes, NULL, label, value, NULL,
+ (GAsyncReadyCallback)secret_service_store_cb,
+ g_object_ref (task));
+
+ g_free (label);
+ secret_value_unref (value);
+ g_object_unref (task);
+}
+
+static void
+ephy_password_manger_store_record (EphyPasswordManager *self,
+ EphyPasswordRecord *record)
+{
+ GHashTable *attributes;
+
+ g_assert (EPHY_IS_PASSWORD_MANAGER (self));
+ g_assert (EPHY_IS_PASSWORD_RECORD (record));
+
+ attributes = get_attributes_table (ephy_password_record_get_id (record),
+ ephy_password_record_get_hostname (record),
+ ephy_password_record_get_username (record),
+ ephy_password_record_get_username_field (record),
+ ephy_password_record_get_password_field (record),
+ ephy_synchronizable_get_server_time_modified (EPHY_SYNCHRONIZABLE
(record)));
+ store_internal (ephy_password_record_get_password (record), attributes, NULL, NULL);
+ ephy_password_manager_cache_add (self,
+ ephy_password_record_get_hostname (record),
+ ephy_password_record_get_username (record));
+
+ g_hash_table_unref (attributes);
+}
+
+static void
+update_password_cb (GSList *records,
+ gpointer user_data)
+{
+ UpdatePasswordAsyncData *data = (UpdatePasswordAsyncData *)user_data;
+ EphyPasswordRecord *record;
+
+ /* We only expect one matching record here. */
+ g_assert (g_slist_length (records) == 1);
+
+ record = EPHY_PASSWORD_RECORD (records->data);
+ ephy_password_record_set_password (record, data->password);
+ ephy_password_manger_store_record (data->manager, record);
+ g_signal_emit_by_name (data->manager, "synchronizable-modified", record);
+
+ g_slist_free_full (records, g_object_unref);
+ update_password_async_data_free (data);
+}
+
+void
+ephy_password_manager_save (EphyPasswordManager *self,
+ const char *uri,
+ const char *username,
+ const char *password,
+ const char *username_field,
+ const char *password_field,
+ gboolean is_new)
+{
+ EphyPasswordRecord *record;
+ char *hostname;
+ char *uuid;
+ char *id;
+ gint64 timestamp;
+
+ g_return_if_fail (EPHY_IS_PASSWORD_MANAGER (self));
+ g_return_if_fail (uri);
+ g_return_if_fail (password);
+ g_return_if_fail (!username_field || username);
+ g_return_if_fail (!password_field || password);
+
+ if (!is_new) {
+ LOG ("Updating password for (%s, %s, %s, %s)",
+ uri, username, username_field, password_field);
+ ephy_password_manager_query (self, uri, username,
+ username_field, password_field,
+ update_password_cb,
+ update_password_async_data_new (self, password));
+ return;
+ }
+
+ uuid = g_uuid_string_random ();
+ id = g_strdup_printf ("{%s}", uuid);
+ timestamp = g_get_real_time () / 1000;
+ hostname = ephy_uri_to_security_origin (uri);
+ record = ephy_password_record_new (id, hostname,
+ username, password,
+ username_field, password_field,
+ timestamp, timestamp);
+ ephy_password_manger_store_record (self, record);
+ g_signal_emit_by_name (self, "synchronizable-modified", record);
+
+ g_free (hostname);
+ g_free (uuid);
+ g_free (id);
+ g_object_unref (record);
+}
+
+static void
+secret_service_search_cb (SecretService *service,
+ GAsyncResult *result,
+ QueryAsyncData *data)
+{
+ GList *matches = NULL;
+ GSList *records = NULL;
+ GError *error = NULL;
+
+ matches = secret_service_search_finish (service, result, &error);
+ if (error) {
+ g_warning ("Failed to search secrets in password schema: %s", error->message);
+ g_error_free (error);
+ goto out;
+ }
+
+ for (GList *l = matches; l && l->data; l = l->next) {
+ SecretItem *item = (SecretItem *)l->data;
+ GHashTable *attributes = secret_item_get_attributes (item);
+ SecretValue *value = secret_item_get_secret (item);
+ const char *id = g_hash_table_lookup (attributes, ID_KEY);
+ const char *hostname = g_hash_table_lookup (attributes, HOSTNAME_KEY);
+ const char *username = g_hash_table_lookup (attributes, USERNAME_KEY);
+ const char *username_field = g_hash_table_lookup (attributes, USERNAME_FIELD_KEY);
+ const char *password_field = g_hash_table_lookup (attributes, PASSWORD_FIELD_KEY);
+ const char *timestamp = g_hash_table_lookup (attributes, SERVER_TIME_MODIFIED_KEY);
+ const char *password = secret_value_get (value, NULL);
+ double server_time_modified;
+ EphyPasswordRecord *record;
+
+ LOG ("Found password record for (%s, %s, %s, %s)",
+ hostname, username, username_field, password_field);
+
+ record = ephy_password_record_new (id, hostname,
+ username, password,
+ username_field, password_field,
+ secret_item_get_created (item) * 1000,
+ secret_item_get_modified (item) * 1000);
+ sscanf (timestamp, "%lf", &server_time_modified);
+ ephy_synchronizable_set_server_time_modified (EPHY_SYNCHRONIZABLE (record),
+ server_time_modified);
+ records = g_slist_prepend (records, record);
+
+ secret_value_unref (value);
+ g_hash_table_unref (attributes);
+ }
+
+out:
+ if (data->callback)
+ data->callback (records, data->user_data);
+ query_async_data_free (data);
+ g_list_free_full (matches, g_object_unref);
+}
+
+void
+ephy_password_manager_query (EphyPasswordManager *self,
+ const char *uri,
+ const char *username,
+ const char *username_field,
+ const char *password_field,
+ EphyPasswordManagerQueryCallback callback,
+ gpointer user_data)
+{
+ QueryAsyncData *data;
+ GHashTable *attributes;
+
+ g_return_if_fail (EPHY_IS_PASSWORD_MANAGER (self));
+
+ LOG ("Querying password records for (%s, %s, %s, %s)",
+ uri, username, username_field, password_field);
+
+ attributes = get_attributes_table (NULL, uri, username,
+ username_field, password_field, -1);
+ data = query_async_data_new (callback, user_data);
+
+ secret_service_search (NULL,
+ EPHY_FORM_PASSWORD_SCHEMA,
+ attributes,
+ SECRET_SEARCH_ALL | SECRET_SEARCH_UNLOCK | SECRET_SEARCH_LOAD_SECRETS,
+ NULL,
+ (GAsyncReadyCallback)secret_service_search_cb,
+ data);
+
+ g_hash_table_unref (attributes);
+}
+
+void
+ephy_password_manager_store_raw (const char *uri,
+ const char *username,
+ const char *password,
+ const char *username_field,
+ const char *password_field,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GHashTable *attributes;
+
+ g_return_if_fail (uri);
+ g_return_if_fail (password);
+ g_return_if_fail (!username_field || username);
+ g_return_if_fail (!password_field || password);
+
+ attributes = get_attributes_table (NULL, uri, username,
+ username_field, password_field, -1);
+ store_internal (password, attributes, callback, user_data);
+
+ g_hash_table_unref (attributes);
+}
+
+gboolean
+ephy_password_manager_store_finish (GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (!error || !(*error), FALSE);
+ g_return_val_if_fail (g_task_is_valid (result, NULL), FALSE);
+
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+secret_service_clear_cb (SecretService *service,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GError *error = NULL;
+
+ secret_service_clear_finish (service, result, &error);
+ if (error) {
+ g_warning ("Failed to clear secrets from password schema: %s", error->message);
+ g_error_free (error);
+ return;
+ }
+
+ if (user_data) {
+ ReplaceRecordAsyncData *data = (ReplaceRecordAsyncData *)user_data;
+ ephy_password_manger_store_record (data->manager, data->record);
+ replace_record_async_data_free (data);
+ }
+}
+
+static void
+ephy_password_manager_forget_record (EphyPasswordManager *self,
+ EphyPasswordRecord *record,
+ EphyPasswordRecord *replacement)
+{
+ GHashTable *attributes;
+
+ g_assert (EPHY_IS_PASSWORD_MANAGER (self));
+ g_assert (EPHY_IS_PASSWORD_RECORD (record));
+
+ attributes = get_attributes_table (ephy_password_record_get_id (record),
+ ephy_password_record_get_hostname (record),
+ ephy_password_record_get_username (record),
+ ephy_password_record_get_username_field (record),
+ ephy_password_record_get_password_field (record),
+ ephy_synchronizable_get_server_time_modified (EPHY_SYNCHRONIZABLE
(record)));
+
+ LOG ("Forgetting password record for (%s, %s",
+ ephy_password_record_get_hostname (record),
+ ephy_password_record_get_username (record));
+
+ secret_service_clear (NULL, EPHY_FORM_PASSWORD_SCHEMA, attributes, NULL,
+ (GAsyncReadyCallback)secret_service_clear_cb,
+ replacement ? replace_record_async_data_new (self, replacement) : NULL);
+
+ ephy_password_manager_cache_remove (self,
+ ephy_password_record_get_hostname (record),
+ ephy_password_record_get_username (record));
+ g_hash_table_unref (attributes);
+}
+
+static void
+forget_cb (GSList *records,
+ gpointer user_data)
+{
+ EphyPasswordManager *self = EPHY_PASSWORD_MANAGER (user_data);
+ EphyPasswordRecord *record;
+
+ /* We only expect one matching record here. */
+ g_assert (g_slist_length (records) == 1);
+
+ record = EPHY_PASSWORD_RECORD (records->data);
+ g_signal_emit_by_name (self, "synchronizable-deleted", record);
+ ephy_password_manager_forget_record (self, record, NULL);
+
+ g_slist_free_full (records, g_object_unref);
+}
+
+void
+ephy_password_manager_forget (EphyPasswordManager *self,
+ const char *hostname,
+ const char *username,
+ const char *username_field,
+ const char *password_field)
+{
+ g_return_if_fail (EPHY_IS_PASSWORD_MANAGER (self));
+ g_return_if_fail (hostname);
+ g_return_if_fail (password_field);
+ g_return_if_fail (!username_field || username);
+
+ /* synchronizable-deleted signal needs an EphySynchronizable object,
+ * therefore we need to obtain the password record first and then emit
+ * the signal before clearing the password from the secret schema. */
+ ephy_password_manager_query (self, hostname, username,
+ username_field, password_field,
+ forget_cb, self);
+}
+
+static void
+forget_all_cb (GSList *records,
+ gpointer user_data)
+{
+ EphyPasswordManager *self = EPHY_PASSWORD_MANAGER (user_data);
+ GHashTable *attributes;
+
+ attributes = secret_attributes_build (EPHY_FORM_PASSWORD_SCHEMA, NULL);
+ secret_service_clear (NULL, EPHY_FORM_PASSWORD_SCHEMA, attributes, NULL,
+ (GAsyncReadyCallback)secret_service_clear_cb, NULL);
+
+ for (GSList *l = records; l && l->data; l = l->next)
+ g_signal_emit_by_name (self, "synchronizable-deleted", l->data);
+
+ ephy_password_manager_cache_clear (self);
+
+ g_hash_table_unref (attributes);
+ g_slist_free_full (records, g_object_unref);
+}
+
+void
+ephy_password_manager_forget_all (EphyPasswordManager *self)
+{
+ g_return_if_fail (EPHY_IS_PASSWORD_MANAGER (self));
+
+ /* synchronizable-deleted signal needs an EphySynchronizable object, therefore
+ * we need to obtain the password records first and emit the signal for each
+ * one before clearing the secret schema. */
+ ephy_password_manager_query (self, NULL, NULL, NULL, NULL,
+ forget_all_cb, self);
+}
+
+static const char *
+synchronizable_manager_get_collection_name (EphySynchronizableManager *manager)
+{
+ gboolean sync_with_firefox = g_settings_get_boolean (EPHY_SETTINGS_SYNC,
+ EPHY_PREFS_SYNC_WITH_FIREFOX);
+
+ return sync_with_firefox ? "passwords" : "ephy-passwords";
+}
+
+static GType
+synchronizable_manager_get_synchronizable_type (EphySynchronizableManager *manager)
+{
+ return EPHY_TYPE_PASSWORD_RECORD;
+}
+
+static gboolean
+synchronizable_manager_is_initial_sync (EphySynchronizableManager *manager)
+{
+ return g_settings_get_boolean (EPHY_SETTINGS_SYNC,
+ EPHY_PREFS_SYNC_PASSWORDS_INITIAL);
+}
+
+static void
+synchronizable_manager_set_is_initial_sync (EphySynchronizableManager *manager,
+ gboolean is_initial)
+{
+ g_settings_set_boolean (EPHY_SETTINGS_SYNC,
+ EPHY_PREFS_SYNC_PASSWORDS_INITIAL,
+ is_initial);
+}
+
+static double
+synchronizable_manager_get_sync_time (EphySynchronizableManager *manager)
+{
+ return g_settings_get_double (EPHY_SETTINGS_SYNC,
+ EPHY_PREFS_SYNC_PASSWORDS_TIME);
+}
+
+static void
+synchronizable_manager_set_sync_time (EphySynchronizableManager *manager,
+ double sync_time)
+{
+ g_settings_set_double (EPHY_SETTINGS_SYNC,
+ EPHY_PREFS_SYNC_PASSWORDS_TIME,
+ sync_time);
+}
+
+static void
+synchronizable_manager_add (EphySynchronizableManager *manager,
+ EphySynchronizable *synchronizable)
+{
+ EphyPasswordManager *self = EPHY_PASSWORD_MANAGER (manager);
+ EphyPasswordRecord *record = EPHY_PASSWORD_RECORD (synchronizable);
+
+ ephy_password_manger_store_record (self, record);
+}
+
+static void
+synchronizable_manager_remove (EphySynchronizableManager *manager,
+ EphySynchronizable *synchronizable)
+{
+ EphyPasswordManager *self = EPHY_PASSWORD_MANAGER (manager);
+ EphyPasswordRecord *record = EPHY_PASSWORD_RECORD (synchronizable);
+
+ ephy_password_manager_forget_record (self, record, NULL);
+}
+
+static void
+replace_existing_cb (GSList *records,
+ gpointer user_data)
+{
+ ReplaceRecordAsyncData *data = (ReplaceRecordAsyncData *)user_data;
+
+ /* We only expect one matching record here. */
+ g_assert (g_slist_length (records) == 1);
+
+ ephy_password_manager_forget_record (data->manager, records->data, data->record);
+
+ replace_record_async_data_free (data);
+}
+
+static void
+ephy_password_manager_replace_existing (EphyPasswordManager *self,
+ EphyPasswordRecord *record)
+{
+ g_assert (EPHY_IS_PASSWORD_MANAGER (self));
+ g_assert (EPHY_IS_PASSWORD_RECORD (record));
+
+ ephy_password_manager_query (self,
+ ephy_password_record_get_hostname (record),
+ ephy_password_record_get_username (record),
+ ephy_password_record_get_username_field (record),
+ ephy_password_record_get_password_field (record),
+ replace_existing_cb,
+ replace_record_async_data_new (self, record));
+}
+
+static void
+synchronizable_manager_save (EphySynchronizableManager *manager,
+ EphySynchronizable *synchronizable)
+{
+ EphyPasswordManager *self = EPHY_PASSWORD_MANAGER (manager);
+ EphyPasswordRecord *record = EPHY_PASSWORD_RECORD (synchronizable);
+
+ ephy_password_manager_replace_existing (self, record);
+}
+
+static EphyPasswordRecord *
+get_record_by_id (GSList *records,
+ const char *id)
+{
+ g_assert (id);
+
+ for (GSList *l = records; l && l->data; l = l->next) {
+ if (!g_strcmp0 (ephy_password_record_get_id (l->data), id))
+ return l->data;
+ }
+
+ return NULL;
+}
+
+static EphyPasswordRecord *
+get_record_by_parameters (GSList *records,
+ const char *hostname,
+ const char *username,
+ const char *username_field,
+ const char *password_field)
+{
+ for (GSList *l = records; l && l->data; l = l->next) {
+ if (!g_strcmp0 (ephy_password_record_get_username (l->data), username) &&
+ !g_strcmp0 (ephy_password_record_get_hostname (l->data), hostname) &&
+ !g_strcmp0 (ephy_password_record_get_username_field (l->data), username_field) &&
+ !g_strcmp0 (ephy_password_record_get_password_field (l->data), password_field))
+ return l->data;
+ }
+
+ return NULL;
+}
+
+static GSList *
+ephy_password_manager_handle_initial_merge (EphyPasswordManager *self,
+ GSList *local_records,
+ GSList *remote_records)
+{
+ EphyPasswordRecord *record;
+ GHashTable *dont_upload;
+ GSList *to_upload = NULL;
+
+ g_assert (EPHY_IS_PASSWORD_MANAGER (self));
+
+ /* A saved password record is uniquely identified by its id or by its tuple of
+ * (hostname, username, username field, password field). When it comes to
+ * importing passwords from server, we may encounter duplicates either by id
+ * or by mentioned tuple. Moreover, we assume that same id means same tuple
+ * but same tuple does not necessarily means same id. This is what our merge
+ * logic is based on. */
+
+ dont_upload = g_hash_table_new (g_str_hash, g_str_equal);
+
+ for (GSList *l = remote_records; l && l->data; l = l->next) {
+ const char *id = ephy_password_record_get_id (l->data);
+ const char *hostname = ephy_password_record_get_hostname (l->data);
+ const char *username = ephy_password_record_get_username (l->data);
+ const char *password = ephy_password_record_get_password (l->data);
+ const char *username_field = ephy_password_record_get_username_field (l->data);
+ const char *password_field = ephy_password_record_get_password_field (l->data);
+ guint64 timestamp = ephy_password_record_get_time_password_changed (l->data);
+ double server_time_modified = ephy_synchronizable_get_server_time_modified (l->data);
+
+ record = get_record_by_id (local_records, id);
+ if (record) {
+ if (!g_strcmp0 (ephy_password_record_get_password (record), password)) {
+ /* Same id, same password. Nothing to do. */
+ g_hash_table_add (dont_upload, (char *)id);
+ } else {
+ /* Same id, different password. Keep the most recent modified. */
+ if (ephy_password_record_get_time_password_changed (record) > timestamp) {
+ /* Local record is newer. Keep it and upload it to server.
+ * Also, must keep the most recent server time modified. */
+ if (ephy_synchronizable_get_server_time_modified (EPHY_SYNCHRONIZABLE (record)) <
server_time_modified) {
+ ephy_synchronizable_set_server_time_modified (EPHY_SYNCHRONIZABLE (record),
server_time_modified);
+ ephy_password_manager_replace_existing (self, record);
+ }
+ } else {
+ /* Remote record is newer. Forget local record and store remote record. */
+ ephy_password_manager_forget_record (self, record, l->data);
+ g_hash_table_add (dont_upload, (char *)id);
+ }
+ }
+ } else {
+ record = get_record_by_parameters (local_records,
+ hostname, username,
+ username_field, password_field);
+ if (record) {
+ /* Different id, same tuple. Keep the most recent modified. */
+ if (ephy_password_record_get_time_password_changed (record) > timestamp) {
+ /* Local record is newer. Keep it, upload it and delete remote record from server. */
+ g_signal_emit_by_name (self, "synchronizable-deleted", l->data);
+ } else {
+ /* Remote record is newer. Forget local record and store remote record. */
+ ephy_password_manager_forget_record (self, record, l->data);
+ g_hash_table_add (dont_upload, (char *)id);
+ }
+ } else {
+ /* Different id, different tuple. This is a new record, add it. */
+ ephy_password_manger_store_record (self, l->data);
+ g_hash_table_add (dont_upload, (char *)id);
+ }
+ }
+ }
+
+ /* Set the remaining local records to be uploaded to server. */
+ for (GSList *l = local_records; l && l->data; l = l->next) {
+ record = EPHY_PASSWORD_RECORD (l->data);
+ if (!g_hash_table_contains (dont_upload, ephy_password_record_get_id (record)))
+ to_upload = g_slist_prepend (to_upload, g_object_ref (record));
+ }
+
+ g_hash_table_unref (dont_upload);
+
+ return to_upload;
+}
+
+static GSList *
+ephy_password_manager_handle_regular_merge (EphyPasswordManager *self,
+ GSList *local_records,
+ GSList *deleted_records,
+ GSList *updated_records)
+{
+ EphyPasswordRecord *record;
+ GSList *to_upload = NULL;
+
+ g_assert (EPHY_IS_PASSWORD_MANAGER (self));
+
+ for (GSList *l = deleted_records; l && l->data; l = l->next) {
+ record = get_record_by_id (local_records, ephy_password_record_get_id (l->data));
+ if (record)
+ ephy_password_manager_forget_record (self, record, NULL);
+ }
+
+ /* See comment in ephy_password_manager_handle_initial_merge.
+ * Same logic applies here too. */
+ for (GSList *l = updated_records; l && l->data; l = l->next) {
+ const char *id = ephy_password_record_get_id (l->data);
+ const char *hostname = ephy_password_record_get_hostname (l->data);
+ const char *username = ephy_password_record_get_username (l->data);
+ const char *username_field = ephy_password_record_get_username_field (l->data);
+ const char *password_field = ephy_password_record_get_password_field (l->data);
+ guint64 timestamp = ephy_password_record_get_time_password_changed (l->data);
+
+ record = get_record_by_id (local_records, id);
+ if (record) {
+ /* Same id. Overwrite local record. */
+ ephy_password_manager_forget_record (self, record, l->data);
+ } else {
+ record = get_record_by_parameters (local_records,
+ hostname, username,
+ username_field, password_field);
+ if (record) {
+ /* Different id, same tuple. Keep the most recent modified. */
+ if (ephy_password_record_get_time_password_changed (record) > timestamp) {
+ /* Local record is newer. Keep it, upload it and delete remote record from server. */
+ to_upload = g_slist_prepend (to_upload, g_object_ref (record));
+ g_signal_emit_by_name (self, "synchronizable-deleted", l->data);
+ } else {
+ /* Remote record is newer. Forget local record and store remote record. */
+ ephy_password_manager_forget_record (self, record, l->data);
+ }
+ } else {
+ /* Different id, different tuple. This is a new record, add it. */
+ ephy_password_manger_store_record (self, l->data);
+ }
+ }
+ }
+
+ return to_upload;
+}
+
+static void
+merge_cb (GSList *records,
+ gpointer user_data)
+{
+ MergeAsyncData *data = (MergeAsyncData *)user_data;
+ GSList *to_upload = NULL;
+
+ if (data->is_initial)
+ to_upload = ephy_password_manager_handle_initial_merge (data->manager,
+ records,
+ data->remotes_updated);
+ else
+ to_upload = ephy_password_manager_handle_regular_merge (data->manager,
+ records,
+ data->remotes_deleted,
+ data->remotes_updated);
+
+ data->callback (to_upload, data->user_data);
+
+ g_slist_free_full (records, g_object_unref);
+ merge_async_data_free (data);
+}
+
+static void
+synchronizable_manager_merge (EphySynchronizableManager *manager,
+ gboolean is_initial,
+ GSList *remotes_deleted,
+ GSList *remotes_updated,
+ EphySynchronizableManagerMergeCallback callback,
+ gpointer user_data)
+{
+ EphyPasswordManager *self = EPHY_PASSWORD_MANAGER (manager);
+
+ ephy_password_manager_query (self, NULL, NULL, NULL, NULL,
+ merge_cb,
+ merge_async_data_new (self,
+ is_initial,
+ remotes_deleted,
+ remotes_updated,
+ callback,
+ user_data));
+}
+
+static void
+ephy_synchronizable_manager_iface_init (EphySynchronizableManagerInterface *iface)
+{
+ iface->get_collection_name = synchronizable_manager_get_collection_name;
+ iface->get_synchronizable_type = synchronizable_manager_get_synchronizable_type;
+ iface->is_initial_sync = synchronizable_manager_is_initial_sync;
+ iface->set_is_initial_sync = synchronizable_manager_set_is_initial_sync;
+ iface->get_sync_time = synchronizable_manager_get_sync_time;
+ iface->set_sync_time = synchronizable_manager_set_sync_time;
+ iface->add = synchronizable_manager_add;
+ iface->remove = synchronizable_manager_remove;
+ iface->save = synchronizable_manager_save;
+ iface->merge = synchronizable_manager_merge;
+}
diff --git a/lib/sync/ephy-password-manager.h b/lib/sync/ephy-password-manager.h
new file mode 100644
index 0000000..70c08cd
--- /dev/null
+++ b/lib/sync/ephy-password-manager.h
@@ -0,0 +1,82 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2017 Gabriel Ivascu <ivascu gabriel59 gmail com>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Epiphany 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 Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ephy-password-record.h"
+
+#include <glib-object.h>
+#include <libsecret/secret.h>
+
+G_BEGIN_DECLS
+
+const SecretSchema *ephy_password_manager_get_password_schema (void) G_GNUC_CONST;
+
+#define ID_KEY "id"
+#define HOSTNAME_KEY "uri"
+#define USERNAME_FIELD_KEY "form_username"
+#define PASSWORD_FIELD_KEY "form_password"
+#define USERNAME_KEY "username"
+#define SERVER_TIME_MODIFIED_KEY "server_time_modified"
+
+#define EPHY_FORM_PASSWORD_SCHEMA ephy_password_manager_get_password_schema ()
+
+#define EPHY_TYPE_PASSWORD_MANAGER (ephy_password_manager_get_type ())
+
+G_DECLARE_FINAL_TYPE (EphyPasswordManager, ephy_password_manager, EPHY, PASSWORD_MANAGER, GObject)
+
+typedef void (*EphyPasswordManagerQueryCallback) (GSList *records, gpointer user_data);
+
+EphyPasswordManager *ephy_password_manager_new (void);
+GSList *ephy_password_manager_get_cached_users_for_uri (EphyPasswordManager *self,
+ const char *uri);
+void ephy_password_manager_save (EphyPasswordManager *self,
+ const char *uri,
+ const char *username,
+ const char *password,
+ const char *username_field,
+ const char *password_field,
+ gboolean is_new);
+void ephy_password_manager_query (EphyPasswordManager *self,
+ const char *uri,
+ const char
*username,
+ const char
*username_field,
+ const char
*password_field,
+ EphyPasswordManagerQueryCallback
callback,
+ gpointer
user_data);
+void ephy_password_manager_forget (EphyPasswordManager *self,
+ const char *hostname,
+ const char *username,
+ const char *username_field,
+ const char *password_field);
+void ephy_password_manager_forget_all (EphyPasswordManager *self);
+/* Note: Below functions are deprecated and should not be used in newly written code.
+ * The only reason they still exist is that the profile migrator expects them. */
+void ephy_password_manager_store_raw (const char *uri,
+ const char *username,
+ const char *password,
+ const char *username_field,
+ const char *password_field,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+gboolean ephy_password_manager_store_finish (GAsyncResult *result,
+ GError **error);
+
+G_END_DECLS
diff --git a/lib/sync/ephy-password-record.c b/lib/sync/ephy-password-record.c
new file mode 100644
index 0000000..63ffc6b
--- /dev/null
+++ b/lib/sync/ephy-password-record.c
@@ -0,0 +1,370 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2017 Gabriel Ivascu <ivascu gabriel59 gmail com>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Epiphany 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 Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include "ephy-password-record.h"
+
+#include "ephy-synchronizable.h"
+
+struct _EphyPasswordRecord {
+ GObject parent_instance;
+
+ char *id;
+ char *hostname;
+ char *form_submit_url;
+ char *username;
+ char *password;
+ char *username_field;
+ char *password_field;
+ guint64 time_created;
+ guint64 time_password_changed;
+
+ double server_time_modified;
+};
+
+static void json_serializable_iface_init (JsonSerializableIface *iface);
+static void ephy_synchronizable_iface_init (EphySynchronizableInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (EphyPasswordRecord, ephy_password_record, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (JSON_TYPE_SERIALIZABLE,
+ json_serializable_iface_init)
+ G_IMPLEMENT_INTERFACE (EPHY_TYPE_SYNCHRONIZABLE,
+ ephy_synchronizable_iface_init))
+
+enum {
+ PROP_0,
+ PROP_ID, /* Firefox Sync */
+ PROP_HOSTNAME, /* Epiphany && Firefox Sync */
+ PROP_FORM_SUBMIT_URL, /* Firefox Sync */
+ PROP_USERNAME, /* Epiphany && Firefox Sync */
+ PROP_PASSWORD, /* Epiphany && Firefox Sync */
+ PROP_USERNAME_FIELD, /* Epiphany && Firefox Sync */
+ PROP_PASSWORD_FIELD, /* Epiphany && Firefox Sync */
+ PROP_TIME_CREATED, /* Firefox Sync */
+ PROP_TIME_PASSWORD_CHANGED, /* Firefox Sync */
+ LAST_PROP
+};
+
+static GParamSpec *obj_properties[LAST_PROP];
+
+static void
+ephy_password_record_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ EphyPasswordRecord *self = EPHY_PASSWORD_RECORD (object);
+
+ switch (prop_id) {
+ case PROP_ID:
+ g_free (self->id);
+ self->id = g_strdup (g_value_get_string (value));
+ break;
+ case PROP_HOSTNAME:
+ g_free (self->hostname);
+ self->hostname = g_strdup (g_value_get_string (value));
+ break;
+ case PROP_FORM_SUBMIT_URL:
+ g_free (self->form_submit_url);
+ self->form_submit_url = g_strdup (g_value_get_string (value));
+ break;
+ case PROP_USERNAME:
+ g_free (self->username);
+ self->username = g_strdup (g_value_get_string (value));
+ break;
+ case PROP_PASSWORD:
+ g_free (self->password);
+ self->password = g_strdup (g_value_get_string (value));
+ break;
+ case PROP_USERNAME_FIELD:
+ g_free (self->username_field);
+ self->username_field = g_strdup (g_value_get_string (value));
+ break;
+ case PROP_PASSWORD_FIELD:
+ g_free (self->password_field);
+ self->password_field = g_strdup (g_value_get_string (value));
+ break;
+ case PROP_TIME_CREATED:
+ self->time_created = g_value_get_uint64 (value);
+ break;
+ case PROP_TIME_PASSWORD_CHANGED:
+ self->time_password_changed = g_value_get_uint64 (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ephy_password_record_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ EphyPasswordRecord *self = EPHY_PASSWORD_RECORD (object);
+
+ switch (prop_id) {
+ case PROP_ID:
+ g_value_set_string (value, self->id);
+ break;
+ case PROP_HOSTNAME:
+ g_value_set_string (value, self->hostname);
+ break;
+ case PROP_FORM_SUBMIT_URL:
+ g_value_set_string (value, self->form_submit_url);
+ break;
+ case PROP_USERNAME:
+ g_value_set_string (value, self->username);
+ break;
+ case PROP_PASSWORD:
+ g_value_set_string (value, self->password);
+ break;
+ case PROP_USERNAME_FIELD:
+ g_value_set_string (value, self->username_field);
+ break;
+ case PROP_PASSWORD_FIELD:
+ g_value_set_string (value, self->password_field);
+ break;
+ case PROP_TIME_CREATED:
+ g_value_set_uint64 (value, self->time_created);
+ break;
+ case PROP_TIME_PASSWORD_CHANGED:
+ g_value_set_uint64 (value, self->time_password_changed);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ephy_password_record_dispose (GObject *object)
+{
+ EphyPasswordRecord *self = EPHY_PASSWORD_RECORD (object);
+
+ g_clear_pointer (&self->id, g_free);
+ g_clear_pointer (&self->hostname, g_free);
+ g_clear_pointer (&self->form_submit_url, g_free);
+ g_clear_pointer (&self->username, g_free);
+ g_clear_pointer (&self->password, g_free);
+ g_clear_pointer (&self->username_field, g_free);
+ g_clear_pointer (&self->password_field, g_free);
+
+ G_OBJECT_CLASS (ephy_password_record_parent_class)->dispose (object);
+}
+
+static void
+ephy_password_record_class_init (EphyPasswordRecordClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->set_property = ephy_password_record_set_property;
+ object_class->get_property = ephy_password_record_get_property;
+ object_class->dispose = ephy_password_record_dispose;
+
+ obj_properties[PROP_ID] =
+ g_param_spec_string ("id",
+ "Id",
+ "Id of the password record",
+ "Default id",
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+ obj_properties[PROP_HOSTNAME] =
+ g_param_spec_string ("hostname",
+ "Hostname",
+ "Hostname url that password is applicable at",
+ "Default hostname",
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+ obj_properties[PROP_FORM_SUBMIT_URL] =
+ g_param_spec_string ("formSubmitURL",
+ "Form submit URL",
+ "Submission URL set by form",
+ "Default form submit URL",
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+ obj_properties[PROP_USERNAME] =
+ g_param_spec_string ("username",
+ "Username",
+ "Username to log in as",
+ "Default username",
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+ obj_properties[PROP_PASSWORD] =
+ g_param_spec_string ("password",
+ "Password",
+ "Password for the username",
+ "Default password",
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+ obj_properties[PROP_USERNAME_FIELD] =
+ g_param_spec_string ("usernameField",
+ "Username field",
+ "HTML field name of the username",
+ "Default username field",
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+ obj_properties[PROP_PASSWORD_FIELD] =
+ g_param_spec_string ("passwordField",
+ "Password field",
+ "HTML field name of the password",
+ "Default password field",
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+ obj_properties[PROP_TIME_CREATED] =
+ g_param_spec_uint64 ("timeCreated",
+ "Time created",
+ "Unix timestamp in milliseconds at which the password was created",
+ 0,
+ G_MAXUINT64,
+ 0,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+ obj_properties[PROP_TIME_PASSWORD_CHANGED] =
+ g_param_spec_uint64 ("timePasswordChanged",
+ "Time password changed",
+ "Unix timestamp in milliseconds at which the password was changed",
+ 0,
+ G_MAXUINT64,
+ 0,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, LAST_PROP, obj_properties);
+}
+
+static void
+ephy_password_record_init (EphyPasswordRecord *self)
+{
+}
+
+EphyPasswordRecord *
+ephy_password_record_new (const char *id,
+ const char *hostname,
+ const char *username,
+ const char *password,
+ const char *username_field,
+ const char *password_field,
+ guint64 time_created,
+ guint64 time_password_changed)
+{
+ return EPHY_PASSWORD_RECORD (g_object_new (EPHY_TYPE_PASSWORD_RECORD,
+ "id", id,
+ "hostname", hostname,
+ "formSubmitURL", hostname,
+ "username", username,
+ "password", password,
+ "usernameField", username_field,
+ "passwordField", password_field,
+ "timeCreated", time_created,
+ "timePasswordChanged", time_password_changed,
+ NULL));
+}
+
+const char *
+ephy_password_record_get_id (EphyPasswordRecord *self)
+{
+ g_return_val_if_fail (EPHY_IS_PASSWORD_RECORD (self), NULL);
+
+ return self->id;
+}
+
+const char *
+ephy_password_record_get_hostname (EphyPasswordRecord *self)
+{
+ g_return_val_if_fail (EPHY_IS_PASSWORD_RECORD (self), NULL);
+
+ return self->hostname;
+}
+
+const char *
+ephy_password_record_get_username (EphyPasswordRecord *self)
+{
+ g_return_val_if_fail (EPHY_IS_PASSWORD_RECORD (self), NULL);
+
+ return self->username;
+}
+
+const char *
+ephy_password_record_get_password (EphyPasswordRecord *self)
+{
+ g_return_val_if_fail (EPHY_IS_PASSWORD_RECORD (self), NULL);
+
+ return self->password;
+}
+
+void
+ephy_password_record_set_password (EphyPasswordRecord *self,
+ const char *password)
+{
+ g_return_if_fail (EPHY_IS_PASSWORD_RECORD (self));
+
+ g_free (self->password);
+ self->password = g_strdup (password);
+}
+
+const char *
+ephy_password_record_get_username_field (EphyPasswordRecord *self)
+{
+ g_return_val_if_fail (EPHY_IS_PASSWORD_RECORD (self), NULL);
+
+ return self->username_field;
+}
+
+const char *
+ephy_password_record_get_password_field (EphyPasswordRecord *self)
+{
+ g_return_val_if_fail (EPHY_IS_PASSWORD_RECORD (self), NULL);
+
+ return self->password_field;
+}
+
+guint64
+ephy_password_record_get_time_password_changed (EphyPasswordRecord *self)
+{
+ g_return_val_if_fail (EPHY_IS_PASSWORD_RECORD (self), 0);
+
+ return self->time_password_changed;
+}
+
+static void
+json_serializable_iface_init (JsonSerializableIface *iface)
+{
+ iface->serialize_property = json_serializable_default_serialize_property;
+ iface->deserialize_property = json_serializable_default_deserialize_property;
+}
+
+static const char *
+synchronizable_get_id (EphySynchronizable *synchronizable)
+{
+ return ephy_password_record_get_id (EPHY_PASSWORD_RECORD (synchronizable));
+}
+
+static double
+synchronizable_get_server_time_modified (EphySynchronizable *synchronizable)
+{
+ return EPHY_PASSWORD_RECORD (synchronizable)->server_time_modified;
+}
+
+static void
+synchronizable_set_server_time_modified (EphySynchronizable *synchronizable,
+ double server_time_modified)
+{
+ EPHY_PASSWORD_RECORD (synchronizable)->server_time_modified = server_time_modified;
+}
+
+static void
+ephy_synchronizable_iface_init (EphySynchronizableInterface *iface)
+{
+ iface->get_id = synchronizable_get_id;
+ iface->get_server_time_modified = synchronizable_get_server_time_modified;
+ iface->set_server_time_modified = synchronizable_set_server_time_modified;
+ iface->to_bso = ephy_synchronizable_default_to_bso;
+}
diff --git a/lib/sync/ephy-password-record.h b/lib/sync/ephy-password-record.h
new file mode 100644
index 0000000..e4d1106
--- /dev/null
+++ b/lib/sync/ephy-password-record.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Copyright © 2017 Gabriel Ivascu <ivascu gabriel59 gmail com>
+ *
+ * This file is part of Epiphany.
+ *
+ * Epiphany 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Epiphany 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 Epiphany. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <glib-object.h>
+#include <libsecret/secret.h>
+
+G_BEGIN_DECLS
+
+#define EPHY_TYPE_PASSWORD_RECORD (ephy_password_record_get_type ())
+
+G_DECLARE_FINAL_TYPE (EphyPasswordRecord, ephy_password_record, EPHY, PASSWORD_RECORD, GObject)
+
+EphyPasswordRecord *ephy_password_record_new (const char *id,
+ const char *hostname,
+ const char *username,
+ const char *password,
+ const char *username_field,
+ const char *password_field,
+ guint64 time_created,
+ guint64 time_password_changed);
+const char *ephy_password_record_get_id (EphyPasswordRecord *self);
+const char *ephy_password_record_get_hostname (EphyPasswordRecord *self);
+const char *ephy_password_record_get_username (EphyPasswordRecord *self);
+const char *ephy_password_record_get_password (EphyPasswordRecord *self);
+void ephy_password_record_set_password (EphyPasswordRecord *self,
+ const char *password);
+const char *ephy_password_record_get_username_field (EphyPasswordRecord *self);
+const char *ephy_password_record_get_password_field (EphyPasswordRecord *self);
+guint64 ephy_password_record_get_time_password_changed (EphyPasswordRecord *self);
+
+G_END_DECLS
diff --git a/lib/sync/ephy-sync-service.c b/lib/sync/ephy-sync-service.c
index 782c6c7..e21123b 100644
--- a/lib/sync/ephy-sync-service.c
+++ b/lib/sync/ephy-sync-service.c
@@ -75,6 +75,8 @@ struct _EphySyncService {
char *certificate;
SyncCryptoRSAKeyPair *rsa_key_pair;
+
+ gboolean sync_periodically;
};
G_DEFINE_TYPE (EphySyncService, ephy_sync_service, G_TYPE_OBJECT);
@@ -95,6 +97,14 @@ static const char * const secrets[LAST_SECRET] = {
};
enum {
+ PROP_0,
+ PROP_SYNC_PERIODICALLY,
+ LAST_PROP
+};
+
+static GParamSpec *obj_properties[LAST_PROP];
+
+enum {
STORE_FINISHED,
SIGN_IN_ERROR,
SYNC_FREQUENCY_CHANGED,
@@ -131,6 +141,8 @@ typedef struct {
EphySynchronizableManager *manager;
gboolean is_initial;
gboolean is_last;
+ GSList *remotes_deleted;
+ GSList *remotes_updated;
} SyncCollectionAsyncData;
typedef struct {
@@ -233,6 +245,8 @@ sync_collection_async_data_new (EphySyncService *service,
data->manager = g_object_ref (manager);
data->is_initial = is_initial;
data->is_last = is_last;
+ data->remotes_deleted = NULL;
+ data->remotes_updated = NULL;
return data;
}
@@ -244,6 +258,8 @@ sync_collection_async_data_free (SyncCollectionAsyncData *data)
g_object_unref (data->service);
g_object_unref (data->manager);
+ g_slist_free_full (data->remotes_deleted, g_object_unref);
+ g_slist_free_full (data->remotes_updated, g_object_unref);
g_slice_free (SyncCollectionAsyncData, data);
}
@@ -273,6 +289,40 @@ sync_async_data_free (SyncAsyncData *data)
g_slice_free (SyncAsyncData, data);
}
+static void
+ephy_sync_service_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ EphySyncService *self = EPHY_SYNC_SERVICE (object);
+
+ switch (prop_id) {
+ case PROP_SYNC_PERIODICALLY:
+ self->sync_periodically = g_value_get_boolean (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+ephy_sync_service_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ EphySyncService *self = EPHY_SYNC_SERVICE (object);
+
+ switch (prop_id) {
+ case PROP_SYNC_PERIODICALLY:
+ g_value_set_boolean (value, self->sync_periodically);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
static const char *
ephy_sync_service_get_secret (EphySyncService *self,
const char *name)
@@ -950,6 +1000,7 @@ ephy_sync_service_delete_synchronizable (EphySyncService *self,
char *record;
char *payload;
char *body;
+ char *id_safe;
const char *collection;
const char *id;
@@ -958,9 +1009,12 @@ ephy_sync_service_delete_synchronizable (EphySyncService *self,
g_assert (EPHY_IS_SYNCHRONIZABLE (synchronizable));
g_assert (ephy_sync_service_is_signed_in (self));
- id = ephy_synchronizable_get_id (synchronizable);
collection = ephy_synchronizable_manager_get_collection_name (manager);
- endpoint = g_strdup_printf ("storage/%s/%s", collection, id);
+ id = ephy_synchronizable_get_id (synchronizable);
+ /* Firefox uses UUIDs with curly braces as IDs for saved passwords records.
+ * Curly braces are unsafe characters in URLs so they must be encoded. */
+ id_safe = soup_uri_encode (id, NULL);
+ endpoint = g_strdup_printf ("storage/%s/%s", collection, id_safe);
node = json_node_new (JSON_NODE_OBJECT);
object = json_object_new ();
@@ -980,6 +1034,7 @@ ephy_sync_service_delete_synchronizable (EphySyncService *self,
SOUP_METHOD_PUT, body, -1, -1,
delete_synchronizable_cb, NULL);
+ g_free (id_safe);
g_free (endpoint);
g_free (record);
g_free (payload);
@@ -1049,6 +1104,7 @@ ephy_sync_service_download_synchronizable (EphySyncService *self,
{
SyncAsyncData *data;
char *endpoint;
+ char *id_safe;
const char *collection;
const char *id;
@@ -1059,7 +1115,10 @@ ephy_sync_service_download_synchronizable (EphySyncService *self,
id = ephy_synchronizable_get_id (synchronizable);
collection = ephy_synchronizable_manager_get_collection_name (manager);
- endpoint = g_strdup_printf ("storage/%s/%s", collection, id);
+ /* Firefox uses UUIDs with curly braces as IDs for saved passwords records.
+ * Curly braces are unsafe characters in URLs so they must be encoded. */
+ id_safe = soup_uri_encode (id, NULL);
+ endpoint = g_strdup_printf ("storage/%s/%s", collection, id_safe);
data = sync_async_data_new (self, manager, synchronizable);
LOG ("Downloading object with id %s...", id);
@@ -1068,6 +1127,7 @@ ephy_sync_service_download_synchronizable (EphySyncService *self,
download_synchronizable_cb, data);
g_free (endpoint);
+ g_free (id_safe);
}
static void
@@ -1086,8 +1146,8 @@ upload_synchronizable_cb (SoupSession *session,
} else if (msg->status_code == 200) {
LOG ("Successfully uploaded to server");
time_modified = g_ascii_strtod (msg->response_body->data, NULL);
- /* FIXME: Make sure the synchronizable manager commits this change to file/database. */
ephy_synchronizable_set_server_time_modified (data->synchronizable, time_modified);
+ ephy_synchronizable_manager_save (data->manager, data->synchronizable);
} else {
g_warning ("Failed to upload object. Status code: %u, response: %s",
msg->status_code, msg->response_body->data);
@@ -1106,6 +1166,7 @@ ephy_sync_service_upload_synchronizable (EphySyncService *self,
JsonNode *bso;
char *endpoint;
char *body;
+ char *id_safe;
const char *collection;
const char *id;
@@ -1118,7 +1179,10 @@ ephy_sync_service_upload_synchronizable (EphySyncService *self,
bundle = ephy_sync_service_get_key_bundle (self, collection);
bso = ephy_synchronizable_to_bso (synchronizable, bundle);
id = ephy_synchronizable_get_id (synchronizable);
- endpoint = g_strdup_printf ("storage/%s/%s", collection, id);
+ /* Firefox uses UUIDs with curly braces as IDs for saved passwords records.
+ * Curly braces are unsafe characters in URLs so they must be encoded. */
+ id_safe = soup_uri_encode (id, NULL);
+ endpoint = g_strdup_printf ("storage/%s/%s", collection, id_safe);
data = sync_async_data_new (self, manager, synchronizable);
body = json_to_string (bso, FALSE);
@@ -1127,6 +1191,7 @@ ephy_sync_service_upload_synchronizable (EphySyncService *self,
ephy_synchronizable_get_server_time_modified (synchronizable),
upload_synchronizable_cb, data);
+ g_free (id_safe);
g_free (body);
g_free (endpoint);
json_node_unref (bso);
@@ -1134,6 +1199,23 @@ ephy_sync_service_upload_synchronizable (EphySyncService *self,
}
static void
+merge_finished_cb (GSList *to_upload,
+ gpointer user_data)
+{
+ SyncCollectionAsyncData *data = (SyncCollectionAsyncData *)user_data;
+
+ for (GSList *l = to_upload; l && l->data; l = l->next)
+ ephy_sync_service_upload_synchronizable (data->service, data->manager, l->data);
+
+ if (data->is_last)
+ g_signal_emit (data->service, signals[SYNC_FINISHED], 0);
+
+ if (to_upload)
+ g_slist_free_full (to_upload, g_object_unref);
+ sync_collection_async_data_free (data);
+}
+
+static void
sync_collection_cb (SoupSession *session,
SoupMessage *msg,
gpointer user_data)
@@ -1144,9 +1226,6 @@ sync_collection_cb (SoupSession *session,
JsonNode *node = NULL;
JsonArray *array = NULL;
GError *error = NULL;
- GSList *remotes_updated = NULL;
- GSList *remotes_deleted = NULL;
- GSList *to_upload = NULL;
GType type;
const char *collection;
const char *last_modified;
@@ -1157,21 +1236,19 @@ sync_collection_cb (SoupSession *session,
if (msg->status_code != 200) {
g_warning ("Failed to get records in collection %s. Status code: %u, response: %s",
collection, msg->status_code, msg->response_body->data);
- goto out;
+ goto out_error;
}
node = json_from_string (msg->response_body->data, &error);
if (error) {
g_warning ("Response is not a valid JSON: %s", error->message);
- goto out;
+ goto out_error;
}
array = json_node_get_array (node);
if (!array) {
g_warning ("JSON node does not hold an array");
- goto out;
+ goto out_error;
}
- LOG ("Found %u new remote objects...", json_array_get_length (array));
-
type = ephy_synchronizable_manager_get_synchronizable_type (data->manager);
bundle = ephy_sync_service_get_key_bundle (data->service, collection);
for (guint i = 0; i < json_array_get_length (array); i++) {
@@ -1182,36 +1259,35 @@ sync_collection_cb (SoupSession *session,
continue;
}
if (is_deleted)
- remotes_deleted = g_slist_prepend (remotes_deleted, remote);
+ data->remotes_deleted = g_slist_prepend (data->remotes_deleted, remote);
else
- remotes_updated = g_slist_prepend (remotes_updated, remote);
+ data->remotes_updated = g_slist_prepend (data->remotes_updated, remote);
}
- to_upload = ephy_synchronizable_manager_merge (data->manager, data->is_initial,
- remotes_deleted, remotes_updated);
- for (GSList *l = to_upload; l && l->data; l = l->next)
- ephy_sync_service_upload_synchronizable (data->service, data->manager, l->data);
+ LOG ("Found %u deleted objects and %u new/updated objects in %s collection",
+ g_slist_length (data->remotes_deleted),
+ g_slist_length (data->remotes_updated),
+ collection);
/* Update sync time. */
last_modified = soup_message_headers_get_one (msg->response_headers, "X-Last-Modified");
ephy_synchronizable_manager_set_sync_time (data->manager, g_ascii_strtod (last_modified, NULL));
ephy_synchronizable_manager_set_is_initial_sync (data->manager, FALSE);
-out:
+ ephy_synchronizable_manager_merge (data->manager, data->is_initial,
+ data->remotes_deleted, data->remotes_updated,
+ merge_finished_cb, data);
+ goto out_no_error;
+
+out_error:
if (data->is_last)
g_signal_emit (data->service, signals[SYNC_FINISHED], 0);
-
- if (to_upload)
- g_slist_free_full (to_upload, g_object_unref);
- if (remotes_updated)
- g_slist_free_full (remotes_updated, g_object_unref);
- if (remotes_deleted)
- g_slist_free_full (remotes_deleted, g_object_unref);
+ sync_collection_async_data_free (data);
+out_no_error:
if (node)
json_node_unref (node);
if (error)
g_error_free (error);
- sync_collection_async_data_free (data);
}
static void
@@ -1454,7 +1530,8 @@ load_secrets_cb (SecretService *service,
ephy_sync_service_set_secret (self, l->data,
json_object_get_string_member (object, l->data));
- ephy_sync_service_start_periodical_sync (self);
+ if (self->sync_periodically)
+ ephy_sync_service_start_periodical_sync (self);
goto out_no_error;
out_error:
@@ -1575,12 +1652,61 @@ ephy_sync_service_dispose (GObject *object)
}
static void
+ephy_sync_service_constructed (GObject *object)
+{
+ EphySyncService *self = EPHY_SYNC_SERVICE (object);
+ WebKitSettings *settings;
+ const char *user_agent;
+
+ G_OBJECT_CLASS (ephy_sync_service_parent_class)->constructed (object);
+
+ if (self->sync_periodically) {
+ settings = ephy_embed_prefs_get_settings ();
+ user_agent = webkit_settings_get_user_agent (settings);
+ g_object_set (self->session, "user-agent", user_agent, NULL);
+
+ g_signal_connect (self, "sync-frequency-changed",
+ G_CALLBACK (sync_frequency_changed_cb), NULL);
+ }
+}
+
+static void
+ephy_sync_service_init (EphySyncService *self)
+{
+ char *account;
+
+ self->session = soup_session_new ();
+ self->storage_queue = g_queue_new ();
+ self->secrets = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+
+ account = g_settings_get_string (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_USER);
+ if (g_strcmp0 (account, "")) {
+ self->account = g_strdup (account);
+ ephy_sync_service_load_secrets (self);
+ }
+
+ g_free (account);
+}
+
+static void
ephy_sync_service_class_init (EphySyncServiceClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->set_property = ephy_sync_service_set_property;
+ object_class->get_property = ephy_sync_service_get_property;
+ object_class->constructed = ephy_sync_service_constructed;
object_class->dispose = ephy_sync_service_dispose;
+ obj_properties[PROP_SYNC_PERIODICALLY] =
+ g_param_spec_boolean ("sync-periodically",
+ "Sync periodically",
+ "Whether should periodically sync data",
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, LAST_PROP, obj_properties);
+
signals[STORE_FINISHED] =
g_signal_new ("sync-secrets-store-finished",
EPHY_TYPE_SYNC_SERVICE,
@@ -1612,38 +1738,12 @@ ephy_sync_service_class_init (EphySyncServiceClass *klass)
G_TYPE_NONE, 0);
}
-static void
-ephy_sync_service_init (EphySyncService *self)
-{
- char *account;
- const char *user_agent;
- WebKitSettings *settings;
-
- self->session = soup_session_new ();
- self->storage_queue = g_queue_new ();
- self->secrets = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
-
- settings = ephy_embed_prefs_get_settings ();
- user_agent = webkit_settings_get_user_agent (settings);
- g_object_set (self->session, "user-agent", user_agent, NULL);
-
- account = g_settings_get_string (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_USER);
- if (g_strcmp0 (account, "")) {
- self->account = g_strdup (account);
- ephy_sync_service_load_secrets (self);
- }
-
- g_signal_connect (self, "sync-frequency-changed",
- G_CALLBACK (sync_frequency_changed_cb),
- NULL);
-
- g_free (account);
-}
-
EphySyncService *
-ephy_sync_service_new (void)
+ephy_sync_service_new (gboolean sync_periodically)
{
- return EPHY_SYNC_SERVICE (g_object_new (EPHY_TYPE_SYNC_SERVICE, NULL));
+ return EPHY_SYNC_SERVICE (g_object_new (EPHY_TYPE_SYNC_SERVICE,
+ "sync-periodically", sync_periodically,
+ NULL));
}
gboolean
@@ -2189,6 +2289,7 @@ ephy_sync_service_do_sign_out (EphySyncService *self)
g_settings_set_string (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_USER, "");
g_settings_set_boolean (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_BOOKMARKS_INITIAL, TRUE);
+ g_settings_set_boolean (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_PASSWORDS_INITIAL, TRUE);
}
void
@@ -2205,6 +2306,7 @@ ephy_sync_service_start_periodical_sync (EphySyncService *self)
{
g_return_if_fail (EPHY_IS_SYNC_SERVICE (self));
g_return_if_fail (ephy_sync_service_is_signed_in (self));
+ g_return_if_fail (self->sync_periodically);
ephy_sync_service_sync (self);
ephy_sync_service_schedule_periodical_sync (self);
diff --git a/lib/sync/ephy-sync-service.h b/lib/sync/ephy-sync-service.h
index b45892c..369b9a6 100644
--- a/lib/sync/ephy-sync-service.h
+++ b/lib/sync/ephy-sync-service.h
@@ -30,7 +30,7 @@ G_BEGIN_DECLS
G_DECLARE_FINAL_TYPE (EphySyncService, ephy_sync_service, EPHY, SYNC_SERVICE, GObject)
-EphySyncService *ephy_sync_service_new (void);
+EphySyncService *ephy_sync_service_new (gboolean sync_periodically);
gboolean ephy_sync_service_is_signed_in (EphySyncService *self);
const char *ephy_sync_service_get_sync_user (EphySyncService *self);
void ephy_sync_service_do_sign_in (EphySyncService *self,
diff --git a/lib/sync/ephy-synchronizable-manager.c b/lib/sync/ephy-synchronizable-manager.c
index c25a5cb..60212b7 100644
--- a/lib/sync/ephy-synchronizable-manager.c
+++ b/lib/sync/ephy-synchronizable-manager.c
@@ -42,6 +42,7 @@ ephy_synchronizable_manager_default_init (EphySynchronizableManagerInterface *if
iface->set_sync_time = ephy_synchronizable_manager_set_sync_time;
iface->add = ephy_synchronizable_manager_add;
iface->remove = ephy_synchronizable_manager_remove;
+ iface->save = ephy_synchronizable_manager_save;
iface->merge = ephy_synchronizable_manager_merge;
signals[SYNCHRONIZABLE_DELETED] =
@@ -219,6 +220,32 @@ ephy_synchronizable_manager_remove (EphySynchronizableManager *manager,
}
/**
+ * ephy_synchronizable_manager_save:
+ * @manager: an #EphySynchronizableManager
+ * @synchronizable: (transfer none): an #EphySynchronizable
+ *
+ * Saves @synchronizable in the local storage (e.g. file, database, secret schema).
+ * This should only be called on a synchronizable object that already exists in
+ * in @manager's collection (i.e. previously added with ephy_synchronizable_manager_add)
+ * as the @manager expects it to be present. Normally ephy_synchronizable_manager_add
+ * saves the object in the local storage, so the only use case of this function
+ * is to save an existing object which was modified outside the manager (i.e. modified
+ * by the sync service).
+ **/
+void
+ephy_synchronizable_manager_save (EphySynchronizableManager *manager,
+ EphySynchronizable *synchronizable)
+{
+ EphySynchronizableManagerInterface *iface;
+
+ g_return_if_fail (EPHY_IS_SYNCHRONIZABLE_MANAGER (manager));
+ g_return_if_fail (EPHY_IS_SYNCHRONIZABLE (synchronizable));
+
+ iface = EPHY_SYNCHRONIZABLE_MANAGER_GET_IFACE (manager);
+ iface->save (manager, synchronizable);
+}
+
+/**
* ephy_synchronizable_manager_merge:
* @manager: an #EphySynchronizableManager
* @is_initial: a boolean saying whether the collection managed by @manager
@@ -227,23 +254,28 @@ ephy_synchronizable_manager_remove (EphySynchronizableManager *manager,
* objects that were removed remotely from the server.
* @remotes_updated: (transfer none): a #GSList holding the #EphySynchronizable
* objects that were updated remotely on the server.
+ * @callback: an #EphySynchronizableManagerMergeCallback that will be called
+ * when the merge is complete.
+ * @user_data: user data to pass to the @callback.
*
* Merges a list of remote-deleted objects and a list of remote-updated objects
- * with the local objects in @manager's collection.
- *
- * Return value: (transfer full): a #GSList holding the #EphySynchronizable
- * objects that need to be re-uploaded to server.
+ * with the local objects in @manager's collection. When the merge is completed,
+ * @callback will be invoked with a list of objects that need to be (re)uploaded
+ * to server. Transfer full for the list of objects to be (re)uploaded.
**/
-GSList *
-ephy_synchronizable_manager_merge (EphySynchronizableManager *manager,
- gboolean is_initial,
- GSList *remotes_deleted,
- GSList *remotes_updated)
+void
+ephy_synchronizable_manager_merge (EphySynchronizableManager *manager,
+ gboolean is_initial,
+ GSList *remotes_deleted,
+ GSList *remotes_updated,
+ EphySynchronizableManagerMergeCallback callback,
+ gpointer user_data)
{
EphySynchronizableManagerInterface *iface;
- g_return_val_if_fail (EPHY_IS_SYNCHRONIZABLE_MANAGER (manager), NULL);
+ g_return_if_fail (EPHY_IS_SYNCHRONIZABLE_MANAGER (manager));
+ g_return_if_fail (callback);
iface = EPHY_SYNCHRONIZABLE_MANAGER_GET_IFACE (manager);
- return iface->merge (manager, is_initial, remotes_deleted, remotes_updated);
+ iface->merge (manager, is_initial, remotes_deleted, remotes_updated, callback, user_data);
}
diff --git a/lib/sync/ephy-synchronizable-manager.h b/lib/sync/ephy-synchronizable-manager.h
index fe791a3..8c36332 100644
--- a/lib/sync/ephy-synchronizable-manager.h
+++ b/lib/sync/ephy-synchronizable-manager.h
@@ -30,6 +30,8 @@ G_BEGIN_DECLS
G_DECLARE_INTERFACE (EphySynchronizableManager, ephy_synchronizable_manager, EPHY, SYNCHRONIZABLE_MANAGER,
GObject)
+typedef void (*EphySynchronizableManagerMergeCallback) (GSList *to_upload, gpointer user_data);
+
struct _EphySynchronizableManagerInterface {
GTypeInterface parent_iface;
@@ -45,10 +47,14 @@ struct _EphySynchronizableManagerInterface {
EphySynchronizable *synchronizable);
void (*remove) (EphySynchronizableManager *manager,
EphySynchronizable *synchronizable);
- GSList * (*merge) (EphySynchronizableManager *manager,
- gboolean is_initial,
- GSList *remotes_deleted,
- GSList *remotes_updated);
+ void (*save) (EphySynchronizableManager *manager,
+ EphySynchronizable *synchronizable);
+ void (*merge) (EphySynchronizableManager *manager,
+ gboolean is_initial,
+ GSList *remotes_deleted,
+ GSList *remotes_updated,
+ EphySynchronizableManagerMergeCallback callback,
+ gpointer user_data);
};
const char *ephy_synchronizable_manager_get_collection_name (EphySynchronizableManager *manager);
@@ -63,9 +69,13 @@ void ephy_synchronizable_manager_add (EphySyn
EphySynchronizable
*synchronizable);
void ephy_synchronizable_manager_remove (EphySynchronizableManager *manager,
EphySynchronizable
*synchronizable);
-GSList *ephy_synchronizable_manager_merge (EphySynchronizableManager *manager,
- gboolean
is_initial,
- GSList
*remotes_deleted,
- GSList
*remotes_updated);
+void ephy_synchronizable_manager_save (EphySynchronizableManager *manager,
+ EphySynchronizable
*synchronizable);
+void ephy_synchronizable_manager_merge (EphySynchronizableManager
*manager,
+ gboolean
is_initial,
+ GSList
*remotes_deleted,
+ GSList
*remotes_updated,
+
EphySynchronizableManagerMergeCallback callback,
+ gpointer
user_data);
G_END_DECLS
diff --git a/lib/sync/meson.build b/lib/sync/meson.build
index b4e6a22..658c17c 100644
--- a/lib/sync/meson.build
+++ b/lib/sync/meson.build
@@ -1,4 +1,6 @@
libephysync_sources = [
+ 'ephy-password-manager.c',
+ 'ephy-password-record.c',
'ephy-sync-crypto.c',
'ephy-sync-service.c',
'ephy-synchronizable-manager.c',
diff --git a/meson.build b/meson.build
index 874aaea..4a08813 100644
--- a/meson.build
+++ b/meson.build
@@ -41,7 +41,7 @@ configure_file(
configuration: conf
)
-glib_requirement = '>= 2.46.0'
+glib_requirement = '>= 2.52.0'
gtk_requirement = '>= 3.22.13'
nettle_requirement = '>= 3.2'
webkitgtk_requirement = '>= 2.17.3'
diff --git a/src/bookmarks/ephy-bookmarks-manager.c b/src/bookmarks/ephy-bookmarks-manager.c
index 8f7619a..5eb20b3 100644
--- a/src/bookmarks/ephy-bookmarks-manager.c
+++ b/src/bookmarks/ephy-bookmarks-manager.c
@@ -727,6 +727,17 @@ synchronizable_manager_remove (EphySynchronizableManager *manager,
ephy_bookmarks_manager_remove_bookmark_internal (self, bookmark);
}
+static void
+synchronizable_manager_save (EphySynchronizableManager *manager,
+ EphySynchronizable *synchronizable)
+{
+ EphyBookmarksManager *self = EPHY_BOOKMARKS_MANAGER (manager);
+
+ ephy_bookmarks_manager_save_to_file_async (self, NULL,
+
(GAsyncReadyCallback)ephy_bookmarks_manager_save_to_file_warn_on_error_cb,
+ NULL);
+}
+
static GSList *
ephy_bookmarks_manager_handle_initial_merge (EphyBookmarksManager *self,
GSList *remote_bookmarks)
@@ -888,18 +899,26 @@ next:
return to_upload;
}
-static GSList *
-synchronizable_manager_merge (EphySynchronizableManager *manager,
- gboolean is_initial,
- GSList *remotes_deleted,
- GSList *remotes_updated)
+static void
+synchronizable_manager_merge (EphySynchronizableManager *manager,
+ gboolean is_initial,
+ GSList *remotes_deleted,
+ GSList *remotes_updated,
+ EphySynchronizableManagerMergeCallback callback,
+ gpointer user_data)
{
EphyBookmarksManager *self = EPHY_BOOKMARKS_MANAGER (manager);
+ GSList *to_upload = NULL;
if (is_initial)
- return ephy_bookmarks_manager_handle_initial_merge (self, remotes_updated);
+ to_upload = ephy_bookmarks_manager_handle_initial_merge (self,
+ remotes_updated);
+ else
+ to_upload = ephy_bookmarks_manager_handle_regular_merge (self,
+ remotes_updated,
+ remotes_deleted);
- return ephy_bookmarks_manager_handle_regular_merge (self, remotes_updated, remotes_deleted);
+ callback (to_upload, user_data);
}
static void
@@ -913,5 +932,6 @@ ephy_synchronizable_manager_iface_init (EphySynchronizableManagerInterface *ifac
iface->set_sync_time = synchronizable_manager_set_sync_time;
iface->add = synchronizable_manager_add;
iface->remove = synchronizable_manager_remove;
+ iface->save = synchronizable_manager_save;
iface->merge = synchronizable_manager_merge;
}
diff --git a/src/ephy-shell.c b/src/ephy-shell.c
index 6f917a5..0310705 100644
--- a/src/ephy-shell.c
+++ b/src/ephy-shell.c
@@ -346,13 +346,15 @@ ephy_shell_startup (GApplication *application)
G_BINDING_SYNC_CREATE);
}
- ephy_shell->password_manager = ephy_password_manager_new ();
-
/* Create the sync service and register synchronizable managers. */
- ephy_shell->sync_service = ephy_sync_service_new ();
+ ephy_shell->sync_service = ephy_sync_service_new (TRUE);
if (g_settings_get_boolean (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_BOOKMARKS_ENABLED))
ephy_sync_service_register_manager (ephy_shell->sync_service,
EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_bookmarks_manager
(ephy_shell)));
+ if (g_settings_get_boolean (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_PASSWORDS_ENABLED))
+ ephy_sync_service_register_manager (ephy_shell->sync_service,
+ EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_password_manager
(ephy_shell)));
+
gtk_application_set_app_menu (GTK_APPLICATION (application),
G_MENU_MODEL (gtk_builder_get_object (builder, "app-menu")));
} else {
@@ -821,6 +823,9 @@ ephy_shell_get_password_manager (EphyShell *shell)
{
g_return_val_if_fail (EPHY_IS_SHELL (shell), NULL);
+ if (shell->password_manager == NULL)
+ shell->password_manager = ephy_password_manager_new ();
+
return shell->password_manager;
}
diff --git a/src/passwords-dialog.c b/src/passwords-dialog.c
index 2f0637f..3023dc2 100644
--- a/src/passwords-dialog.c
+++ b/src/passwords-dialog.c
@@ -62,16 +62,6 @@ struct _EphyPasswordsDialog {
G_DEFINE_TYPE (EphyPasswordsDialog, ephy_passwords_dialog, GTK_TYPE_DIALOG)
-static void populate_model (EphyPasswordsDialog *dialog);
-
-static void
-reload_model (EphyPasswordsDialog *dialog)
-{
- gtk_list_store_clear (GTK_LIST_STORE (dialog->liststore));
- dialog->filled = FALSE;
- populate_model (dialog);
-}
-
static void
ephy_passwords_dialog_dispose (GObject *object)
{
@@ -140,10 +130,10 @@ forget (GSimpleAction *action,
gtk_tree_model_get_value (model, &iter, COL_PASSWORDS_DATA, &val);
record = g_value_get_object (&val);
ephy_password_manager_forget (dialog->manager,
- ephy_password_record_get_origin (record),
- ephy_password_record_get_form_username (record),
- ephy_password_record_get_form_password (record),
- ephy_password_record_get_username (record));
+ ephy_password_record_get_hostname (record),
+ ephy_password_record_get_username (record),
+ ephy_password_record_get_username_field (record),
+ ephy_password_record_get_password_field (record));
dialog->records = g_slist_remove (dialog->records, record);
g_object_unref (record);
g_value_unset (&val);
@@ -351,10 +341,12 @@ forget_all (GSimpleAction *action,
EphyPasswordsDialog *dialog = EPHY_PASSWORDS_DIALOG (user_data);
ephy_password_manager_forget_all (dialog->manager);
+
+ gtk_list_store_clear (GTK_LIST_STORE (dialog->liststore));
+ dialog->filled = FALSE;
+
g_slist_free_full (dialog->records, g_object_unref);
dialog->records = NULL;
-
- reload_model (dialog);
}
static void
@@ -370,7 +362,7 @@ populate_model_cb (GSList *records,
gtk_list_store_insert_with_values (GTK_LIST_STORE (dialog->liststore),
&iter,
-1,
- COL_PASSWORDS_ORIGIN, ephy_password_record_get_origin (record),
+ COL_PASSWORDS_ORIGIN, ephy_password_record_get_hostname (record),
COL_PASSWORDS_USER, ephy_password_record_get_username (record),
COL_PASSWORDS_PASSWORD, ephy_password_record_get_password (record),
COL_PASSWORDS_INVISIBLE, "●●●●●●●●",
diff --git a/src/prefs-dialog.c b/src/prefs-dialog.c
index 1312d77..bd00d6d 100644
--- a/src/prefs-dialog.c
+++ b/src/prefs-dialog.c
@@ -121,6 +121,7 @@ struct _PrefsDialog {
GtkWidget *sync_options_box;
GtkWidget *sync_with_firefox_checkbutton;
GtkWidget *sync_bookmarks_checkbutton;
+ GtkWidget *sync_passwords_checkbutton;
GtkWidget *sync_frequency_5_min_radiobutton;
GtkWidget *sync_frequency_15_min_radiobutton;
GtkWidget *sync_frequency_30_min_radiobutton;
@@ -175,19 +176,20 @@ prefs_dialog_finalize (GObject *object)
}
static void
-sync_bookmarks_toggled_cb (GtkToggleButton *button,
- PrefsDialog *dialog)
+sync_collection_toggled_cb (GtkToggleButton *button,
+ PrefsDialog *dialog)
{
- EphyBookmarksManager *manager;
+ EphySynchronizableManager *manager = NULL;
- manager = ephy_shell_get_bookmarks_manager (ephy_shell_get_default ());
+ if (GTK_WIDGET (button) == dialog->sync_bookmarks_checkbutton)
+ manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_bookmarks_manager (ephy_shell_get_default ()));
+ else if (GTK_WIDGET (button) == dialog->sync_passwords_checkbutton)
+ manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_password_manager (ephy_shell_get_default ()));
if (gtk_toggle_button_get_active (button))
- ephy_sync_service_register_manager (dialog->sync_service,
- EPHY_SYNCHRONIZABLE_MANAGER (manager));
+ ephy_sync_service_register_manager (dialog->sync_service, manager);
else
- ephy_sync_service_unregister_manager (dialog->sync_service,
- EPHY_SYNCHRONIZABLE_MANAGER (manager));
+ ephy_sync_service_unregister_manager (dialog->sync_service, manager);
}
static void
@@ -234,6 +236,7 @@ sync_secrets_store_finished_cb (EphySyncService *service,
PrefsDialog *dialog)
{
EphyBookmarksManager *bookmarks_manager;
+ EphyPasswordManager *password_manager;
g_assert (EPHY_IS_SYNC_SERVICE (service));
g_assert (EPHY_IS_PREFS_DIALOG (dialog));
@@ -264,6 +267,10 @@ sync_secrets_store_finished_cb (EphySyncService *service,
bookmarks_manager = ephy_shell_get_bookmarks_manager (ephy_shell_get_default ());
ephy_sync_service_register_manager (service, EPHY_SYNCHRONIZABLE_MANAGER (bookmarks_manager));
}
+ if (g_settings_get_boolean (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_PASSWORDS_ENABLED)) {
+ password_manager = ephy_shell_get_password_manager (ephy_shell_get_default ());
+ ephy_sync_service_register_manager (service, EPHY_SYNCHRONIZABLE_MANAGER (password_manager));
+ }
g_free (text);
g_free (user);
@@ -617,6 +624,7 @@ prefs_dialog_class_init (PrefsDialogClass *klass)
gtk_widget_class_bind_template_child (widget_class, PrefsDialog, sync_options_box);
gtk_widget_class_bind_template_child (widget_class, PrefsDialog, sync_with_firefox_checkbutton);
gtk_widget_class_bind_template_child (widget_class, PrefsDialog, sync_bookmarks_checkbutton);
+ gtk_widget_class_bind_template_child (widget_class, PrefsDialog, sync_passwords_checkbutton);
gtk_widget_class_bind_template_child (widget_class, PrefsDialog, sync_frequency_5_min_radiobutton);
gtk_widget_class_bind_template_child (widget_class, PrefsDialog, sync_frequency_15_min_radiobutton);
gtk_widget_class_bind_template_child (widget_class, PrefsDialog, sync_frequency_30_min_radiobutton);
@@ -1671,7 +1679,10 @@ setup_sync_page (PrefsDialog *dialog)
G_CALLBACK (sync_finished_cb),
dialog, 0);
g_signal_connect_object (dialog->sync_bookmarks_checkbutton, "toggled",
- G_CALLBACK (sync_bookmarks_toggled_cb),
+ G_CALLBACK (sync_collection_toggled_cb),
+ dialog, 0);
+ g_signal_connect_object (dialog->sync_passwords_checkbutton, "toggled",
+ G_CALLBACK (sync_collection_toggled_cb),
dialog, 0);
g_settings_bind (sync_settings,
@@ -1684,6 +1695,11 @@ setup_sync_page (PrefsDialog *dialog)
dialog->sync_bookmarks_checkbutton,
"active",
G_SETTINGS_BIND_DEFAULT);
+ g_settings_bind (sync_settings,
+ EPHY_PREFS_SYNC_PASSWORDS_ENABLED,
+ dialog->sync_passwords_checkbutton,
+ "active",
+ G_SETTINGS_BIND_DEFAULT);
g_settings_bind_with_mapping (sync_settings,
EPHY_PREFS_SYNC_FREQUENCY,
dialog->sync_frequency_5_min_radiobutton,
diff --git a/src/profile-migrator/ephy-profile-migrator.c b/src/profile-migrator/ephy-profile-migrator.c
index 1f19ebf..bbd5fc8 100644
--- a/src/profile-migrator/ephy-profile-migrator.c
+++ b/src/profile-migrator/ephy-profile-migrator.c
@@ -312,7 +312,7 @@ load_collection_items_cb (SecretCollection *collection,
SecretValue *secret;
GList *l;
GHashTable *attributes, *t;
- const char *server, *username, *form_username, *form_password, *password;
+ const char *server, *username, *username_field, *password_field, *password;
char *actual_server;
SoupURI *uri;
GError *error = NULL;
@@ -341,20 +341,20 @@ load_collection_items_cb (SecretCollection *collection,
username = g_hash_table_lookup (attributes, "user");
uri = soup_uri_new (server);
t = soup_form_decode (uri->query);
- form_username = g_hash_table_lookup (t, FORM_USERNAME_KEY);
- form_password = g_hash_table_lookup (t, FORM_PASSWORD_KEY);
+ username_field = g_hash_table_lookup (t, USERNAME_FIELD_KEY);
+ password_field = g_hash_table_lookup (t, PASSWORD_FIELD_KEY);
soup_uri_set_query (uri, NULL);
actual_server = soup_uri_to_string (uri, FALSE);
secret_item_load_secret_sync (item, NULL, NULL);
secret = secret_item_get_secret (item);
password = secret_value_get (secret, NULL);
- ephy_password_manager_store (actual_server,
- form_username,
- form_password,
- username,
- password,
- (GAsyncReadyCallback)store_form_auth_data_cb,
- g_hash_table_ref (attributes));
+ ephy_password_manager_store_raw (actual_server,
+ username,
+ password,
+ username_field,
+ password_field,
+ (GAsyncReadyCallback)store_form_auth_data_cb,
+ g_hash_table_ref (attributes));
g_free (actual_server);
secret_value_unref (secret);
g_hash_table_unref (t);
@@ -443,7 +443,7 @@ migrate_insecure_password (SecretItem *item)
const char *original_uri;
attributes = secret_item_get_attributes (item);
- original_uri = g_hash_table_lookup (attributes, URI_KEY);
+ original_uri = g_hash_table_lookup (attributes, HOSTNAME_KEY);
original_origin = webkit_security_origin_new_for_uri (original_uri);
if (original_origin == NULL) {
g_warning ("Failed to convert URI %s to a security origin, insecure password will not be migrated",
original_uri);
@@ -462,7 +462,7 @@ migrate_insecure_password (SecretItem *item)
new_uri = webkit_security_origin_to_string (new_origin);
webkit_security_origin_unref (new_origin);
- g_hash_table_replace (attributes, g_strdup (URI_KEY), new_uri);
+ g_hash_table_replace (attributes, g_strdup (HOSTNAME_KEY), new_uri);
secret_item_set_attributes_sync (item, EPHY_FORM_PASSWORD_SCHEMA, attributes, NULL, &error);
if (error != NULL) {
g_warning ("Failed to convert URI %s to https://, insecure password will not be migrated: %s",
original_uri, error->message);
diff --git a/src/resources/gtk/prefs-dialog.ui b/src/resources/gtk/prefs-dialog.ui
index 09c5ac9..ac5bc0f 100644
--- a/src/resources/gtk/prefs-dialog.ui
+++ b/src/resources/gtk/prefs-dialog.ui
@@ -915,6 +915,13 @@
<property name="use-underline">True</property>
</object>
</child>
+ <child>
+ <object class="GtkCheckButton" id="sync_passwords_checkbutton">
+ <property name="label" translatable="yes">_Passwords</property>
+ <property name="visible">True</property>
+ <property name="use-underline">True</property>
+ </object>
+ </child>
</object>
</child>
<child>
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]