[epiphany/wip/sync: 7/16] sync: Implement history sync



commit 224ec53421142b32362d77707af9fd2b0aac9603
Author: Gabriel Ivascu <ivascu gabriel59 gmail com>
Date:   Wed May 24 00:20:07 2017 +0300

    sync: Implement history sync

 data/org.gnome.epiphany.gschema.xml           |   15 +
 embed/ephy-embed-shell.c                      |    4 +-
 embed/ephy-web-view.c                         |    5 +-
 lib/ephy-prefs.h                              |    3 +
 lib/ephy-sync-utils.c                         |  185 ++++++++
 lib/ephy-sync-utils.h                         |   41 ++
 lib/history/ephy-history-service-urls-table.c |   17 +-
 lib/history/ephy-history-service.c            |   50 ++-
 lib/history/ephy-history-service.h            |    2 +-
 lib/history/ephy-history-types.c              |    7 +
 lib/history/ephy-history-types.h              |    3 +
 lib/meson.build                               |    1 +
 lib/sync/ephy-history-manager.c               |  563 +++++++++++++++++++++++++
 lib/sync/ephy-history-manager.h               |   36 ++
 lib/sync/ephy-history-record.c                |  404 ++++++++++++++++++
 lib/sync/ephy-history-record.h                |   44 ++
 lib/sync/ephy-password-manager.c              |    6 +-
 lib/sync/ephy-sync-crypto.c                   |  215 ++--------
 lib/sync/ephy-sync-crypto.h                   |   10 -
 lib/sync/ephy-sync-service.c                  |   54 ++-
 lib/sync/ephy-synchronizable-manager.c        |    5 +-
 lib/sync/ephy-synchronizable-manager.h        |    2 +-
 lib/sync/meson.build                          |    2 +
 src/bookmarks/ephy-add-bookmark-popover.c     |    3 +-
 src/bookmarks/ephy-bookmark-properties-grid.c |    2 +-
 src/bookmarks/ephy-bookmarks-manager.c        |    9 +-
 src/ephy-history-dialog.c                     |    5 +-
 src/ephy-shell.c                              |   30 ++
 src/ephy-shell.h                              |    3 +
 src/prefs-dialog.c                            |   17 +
 src/profile-migrator/ephy-profile-migrator.c  |    6 +-
 src/resources/gtk/history-dialog.ui           |    2 +
 src/resources/gtk/prefs-dialog.ui             |    7 +
 tests/ephy-web-view-test.c                    |    7 +-
 34 files changed, 1494 insertions(+), 271 deletions(-)
---
diff --git a/data/org.gnome.epiphany.gschema.xml b/data/org.gnome.epiphany.gschema.xml
index 3635eed..e354664 100644
--- a/data/org.gnome.epiphany.gschema.xml
+++ b/data/org.gnome.epiphany.gschema.xml
@@ -322,6 +322,21 @@
                        <summary>Initial sync or normal sync</summary>
                        <description>TRUE if passwords collection needs to be synced for the first time, 
FALSE otherwise.</description>
                </key>
+               <key type="b" name="sync-history-enabled">
+                       <default>false</default>
+                       <summary>Enable history sync</summary>
+                       <description>TRUE if history collection should be synced, FALSE 
otherwise.</description>
+               </key>
+               <key type="d" name="sync-history-time">
+                       <default>0</default>
+                       <summary>History sync timestamp</summary>
+                       <description>The timestamp at which last history sync was made.</description>
+               </key>
+               <key type="b" name="sync-history-initial">
+                       <default>true</default>
+                       <summary>Initial sync or normal sync</summary>
+                       <description>TRUE if history 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/ephy-embed-shell.c b/embed/ephy-embed-shell.c
index bb5572d..1e3292e 100644
--- a/embed/ephy-embed-shell.c
+++ b/embed/ephy-embed-shell.c
@@ -311,7 +311,7 @@ history_service_url_title_changed_cb (EphyHistoryService *service,
 
 static void
 history_service_url_deleted_cb (EphyHistoryService *service,
-                                const char         *url,
+                                EphyHistoryURL     *url,
                                 EphyEmbedShell     *shell)
 {
   EphyEmbedShellPrivate *priv = ephy_embed_shell_get_instance_private (shell);
@@ -320,7 +320,7 @@ history_service_url_deleted_cb (EphyHistoryService *service,
   for (l = priv->web_extensions; l; l = g_list_next (l)) {
     EphyWebExtensionProxy *web_extension = (EphyWebExtensionProxy *)l->data;
 
-    ephy_web_extension_proxy_history_delete_url (web_extension, url);
+    ephy_web_extension_proxy_history_delete_url (web_extension, url->url);
   }
 }
 
diff --git a/embed/ephy-web-view.c b/embed/ephy-web-view.c
index 4129100..13dbec6 100644
--- a/embed/ephy-web-view.c
+++ b/embed/ephy-web-view.c
@@ -1736,7 +1736,10 @@ load_changed_cb (WebKitWebView  *web_view,
 
         ephy_history_service_visit_url (view->history_service,
                                         history_uri,
-                                        view->visit_type);
+                                        NULL,
+                                        g_get_real_time (),
+                                        view->visit_type,
+                                        TRUE);
 
         g_free (history_uri);
       }
diff --git a/lib/ephy-prefs.h b/lib/ephy-prefs.h
index b6be084..fde68f9 100644
--- a/lib/ephy-prefs.h
+++ b/lib/ephy-prefs.h
@@ -162,6 +162,9 @@ static const char * const ephy_prefs_web_schema[] = {
 #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"
+#define EPHY_PREFS_SYNC_HISTORY_ENABLED   "sync-history-enabled"
+#define EPHY_PREFS_SYNC_HISTORY_TIME      "sync-history-time"
+#define EPHY_PREFS_SYNC_HISTORY_INITIAL   "sync-history-initial"
 
 static struct {
   const char *schema;
diff --git a/lib/ephy-sync-utils.c b/lib/ephy-sync-utils.c
new file mode 100644
index 0000000..b97fc54
--- /dev/null
+++ b/lib/ephy-sync-utils.c
@@ -0,0 +1,185 @@
+/* -*- 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-sync-utils.h"
+
+#include <stdio.h>
+#include <string.h>
+
+#define SYNC_ID_LEN 12
+
+static const char hex_digits[] = "0123456789abcdef";
+
+char *
+ephy_sync_utils_encode_hex (const guint8 *data,
+                            gsize         data_len)
+{
+  char *encoded;
+
+  g_return_val_if_fail (data, NULL);
+
+  encoded = g_malloc (data_len * 2 + 1);
+  for (gsize i = 0; i < data_len; i++) {
+    guint8 byte = data[i];
+
+    encoded[2 * i] = hex_digits[byte >> 4];
+    encoded[2 * i + 1] = hex_digits[byte & 0xf];
+  }
+  encoded[data_len * 2] = 0;
+
+  return encoded;
+}
+
+guint8 *
+ephy_sync_utils_decode_hex (const char *hex)
+{
+  guint8 *decoded;
+
+  g_return_val_if_fail (hex, NULL);
+
+  decoded = g_malloc (strlen (hex) / 2);
+  for (gsize i = 0, j = 0; i < strlen (hex); i += 2, j++)
+    sscanf (hex + i, "%2hhx", decoded + j);
+
+  return decoded;
+}
+
+static void
+base64_to_base64_urlsafe (char *text)
+{
+  g_assert (text);
+
+  /* Replace '+' with '-' and '/' with '_' */
+  g_strcanon (text, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789=/", '-');
+  g_strcanon (text, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789=-", '_');
+}
+
+char *
+ephy_sync_utils_base64_urlsafe_encode (const guint8 *data,
+                                       gsize         data_len,
+                                       gboolean      should_strip)
+{
+  char *base64;
+  char *out;
+  gsize start = 0;
+  gssize end;
+
+  g_return_val_if_fail (data, NULL);
+
+  base64 = g_base64_encode (data, data_len);
+  end = strlen (base64) - 1;
+
+  /* Strip the data of any leading or trailing '=' characters. */
+  if (should_strip) {
+    while (start < strlen (base64) && base64[start] == '=')
+      start++;
+
+    while (end >= 0 && base64[end] == '=')
+      end--;
+  }
+
+  out = g_strndup (base64 + start, end - start + 1);
+  base64_to_base64_urlsafe (out);
+
+  g_free (base64);
+
+  return out;
+}
+
+static void
+base64_urlsafe_to_base64 (char *text)
+{
+  g_assert (text);
+
+  /* Replace '-' with '+' and '_' with '/' */
+  g_strcanon (text, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789=_", '+');
+  g_strcanon (text, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789=+", '/');
+}
+
+guint8 *
+ephy_sync_utils_base64_urlsafe_decode (const char   *text,
+                                       gsize        *out_len,
+                                       gboolean      should_fill)
+{
+  guint8 *out;
+  char *to_decode;
+  char *suffix = NULL;
+
+  g_return_val_if_fail (text, NULL);
+  g_return_val_if_fail (out_len, NULL);
+
+  /* Fill the text with trailing '=' characters up to the proper length. */
+  if (should_fill)
+    suffix = g_strnfill ((4 - strlen (text) % 4) % 4, '=');
+
+  to_decode = g_strconcat (text, suffix, NULL);
+  base64_urlsafe_to_base64 (to_decode);
+  out = g_base64_decode (to_decode, out_len);
+
+  g_free (suffix);
+  g_free (to_decode);
+
+  return out;
+}
+
+/*
+ * This is mainly required by Nettle's RSA support.
+ * From Nettle's documentation: random_ctx and random is a randomness generator.
+ * random(random_ctx, length, dst) should generate length random octets and store them at dst.
+ * We don't really use random_ctx, since we have /dev/urandom available.
+ */
+void
+ephy_sync_utils_generate_random_bytes (void   *random_ctx,
+                                       gsize   num_bytes,
+                                       guint8 *out)
+{
+  FILE *fp;
+
+  g_assert (num_bytes > 0);
+  g_assert (out);
+
+  fp = fopen ("/dev/urandom", "r");
+  fread (out, sizeof (guint8), num_bytes, fp);
+  fclose (fp);
+}
+
+char *
+ephy_sync_utils_get_random_sync_id (void)
+{
+  char *id;
+  char *base64;
+  guint8 *bytes;
+  gsize bytes_len;
+
+  /* The sync id is a base64-urlsafe string. Base64 uses 4 chars to represent 3 bytes,
+   * therefore we need ceil(len * 3 / 4) bytes to cover the requested length. */
+  bytes_len = (SYNC_ID_LEN + 3) / 4 * 3;
+  bytes = g_malloc (bytes_len);
+
+  ephy_sync_utils_generate_random_bytes (NULL, bytes_len, bytes);
+  base64 = ephy_sync_utils_base64_urlsafe_encode (bytes, bytes_len, FALSE);
+  id = g_strndup (base64, SYNC_ID_LEN);
+
+  g_free (base64);
+  g_free (bytes);
+
+  return id;
+}
diff --git a/lib/ephy-sync-utils.h b/lib/ephy-sync-utils.h
new file mode 100644
index 0000000..d459535
--- /dev/null
+++ b/lib/ephy-sync-utils.h
@@ -0,0 +1,41 @@
+/* -*- 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.h>
+
+G_BEGIN_DECLS
+
+char   *ephy_sync_utils_encode_hex            (const guint8 *data,
+                                               gsize         data_len);
+guint8 *ephy_sync_utils_decode_hex            (const char   *hex);
+char   *ephy_sync_utils_base64_urlsafe_encode (const guint8 *data,
+                                               gsize         data_len,
+                                               gboolean      should_strip);
+guint8 *ephy_sync_utils_base64_urlsafe_decode (const char   *text,
+                                               gsize        *out_len,
+                                               gboolean      should_fill);
+void    ephy_sync_utils_generate_random_bytes (void         *random_ctx,
+                                               gsize         num_bytes,
+                                               guint8       *out);
+char   *ephy_sync_utils_get_random_sync_id    (void);
+
+G_END_DECLS
diff --git a/lib/history/ephy-history-service-urls-table.c b/lib/history/ephy-history-service-urls-table.c
index fd69bca..447e5b6 100644
--- a/lib/history/ephy-history-service-urls-table.c
+++ b/lib/history/ephy-history-service-urls-table.c
@@ -37,6 +37,7 @@ ephy_history_service_initialize_urls_table (EphyHistoryService *self)
                                   "host INTEGER NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,"
                                   "url LONGVARCAR,"
                                   "title LONGVARCAR,"
+                                  "sync_id LONGVARCAR,"
                                   "visit_count INTEGER DEFAULT 0 NOT NULL,"
                                   "typed_count INTEGER DEFAULT 0 NOT NULL,"
                                   "last_visit_time INTEGER,"
@@ -67,11 +68,11 @@ ephy_history_service_get_url_row (EphyHistoryService *self, const char *url_stri
 
   if (url != NULL && url->id != -1) {
     statement = ephy_sqlite_connection_create_statement (self->history_database,
-                                                         "SELECT id, url, title, visit_count, typed_count, 
last_visit_time, hidden_from_overview, thumbnail_update_time FROM urls "
+                                                         "SELECT id, url, title, visit_count, typed_count, 
last_visit_time, hidden_from_overview, thumbnail_update_time, sync_id FROM urls "
                                                          "WHERE id=?", &error);
   } else {
     statement = ephy_sqlite_connection_create_statement (self->history_database,
-                                                         "SELECT id, url, title, visit_count, typed_count, 
last_visit_time, hidden_from_overview, thumbnail_update_time FROM urls "
+                                                         "SELECT id, url, title, visit_count, typed_count, 
last_visit_time, hidden_from_overview, thumbnail_update_time, sync_id FROM urls "
                                                          "WHERE url=?", &error);
   }
 
@@ -116,6 +117,7 @@ ephy_history_service_get_url_row (EphyHistoryService *self, const char *url_stri
   url->last_visit_time = ephy_sqlite_statement_get_column_as_int64 (statement, 5);
   url->hidden = ephy_sqlite_statement_get_column_as_int (statement, 6);
   url->thumbnail_time = ephy_sqlite_statement_get_column_as_int64 (statement, 7);
+  url->sync_id = g_strdup (ephy_sqlite_statement_get_column_as_string (statement, 8));
 
   g_object_unref (statement);
   return url;
@@ -131,8 +133,8 @@ ephy_history_service_add_url_row (EphyHistoryService *self, EphyHistoryURL *url)
   g_assert (self->history_database != NULL);
 
   statement = ephy_sqlite_connection_create_statement (self->history_database,
-                                                       "INSERT INTO urls (url, title, visit_count, 
typed_count, last_visit_time, host) "
-                                                       " VALUES (?, ?, ?, ?, ?, ?)", &error);
+                                                       "INSERT INTO urls (url, title, visit_count, 
typed_count, last_visit_time, host, sync_id) "
+                                                       " VALUES (?, ?, ?, ?, ?, ?, ?)", &error);
   if (error) {
     g_warning ("Could not build urls table addition statement: %s", error->message);
     g_error_free (error);
@@ -144,7 +146,8 @@ ephy_history_service_add_url_row (EphyHistoryService *self, EphyHistoryURL *url)
       ephy_sqlite_statement_bind_int (statement, 2, url->visit_count, &error) == FALSE ||
       ephy_sqlite_statement_bind_int (statement, 3, url->typed_count, &error) == FALSE ||
       ephy_sqlite_statement_bind_int64 (statement, 4, url->last_visit_time, &error) == FALSE ||
-      ephy_sqlite_statement_bind_int (statement, 5, url->host->id, &error) == FALSE) {
+      ephy_sqlite_statement_bind_int (statement, 5, url->host->id, &error) == FALSE ||
+      ephy_sqlite_statement_bind_string (statement, 6, url->sync_id, &error) == FALSE) {
     g_warning ("Could not insert URL into urls table: %s", error->message);
     g_error_free (error);
     g_object_unref (statement);
@@ -215,6 +218,7 @@ create_url_from_statement (EphySQLiteStatement *statement)
   url->hidden = ephy_sqlite_statement_get_column_as_int (statement, 6);
   url->thumbnail_time = ephy_sqlite_statement_get_column_as_int64 (statement, 7);
   url->host->id = ephy_sqlite_statement_get_column_as_int (statement, 8);
+  url->sync_id = g_strdup (ephy_sqlite_statement_get_column_as_string (statement, 9));
 
   return url;
 }
@@ -237,7 +241,8 @@ ephy_history_service_find_url_rows (EphyHistoryService *self, EphyHistoryQuery *
                                "urls.last_visit_time, "
                                "urls.hidden_from_overview, "
                                "urls.thumbnail_update_time, "
-                               "urls.host "
+                               "urls.host, "
+                               "urls.sync_id "
                                "FROM "
                                "urls ";
 
diff --git a/lib/history/ephy-history-service.c b/lib/history/ephy-history-service.c
index 5c92776..7b95b37 100644
--- a/lib/history/ephy-history-service.c
+++ b/lib/history/ephy-history-service.c
@@ -25,6 +25,7 @@
 #include "ephy-history-types.h"
 #include "ephy-lib-type-builtins.h"
 #include "ephy-sqlite-connection.h"
+#include "ephy-sync-utils.h"
 
 #include <errno.h>
 #include <glib.h>
@@ -221,9 +222,8 @@ ephy_history_service_class_init (EphyHistoryServiceClass *klass)
                   G_SIGNAL_RUN_LAST,
                   0, NULL, NULL, NULL,
                   G_TYPE_NONE,
-                  2,
-                  G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE,
-                  EPHY_TYPE_HISTORY_PAGE_VISIT_TYPE);
+                  1,
+                  G_TYPE_POINTER | G_SIGNAL_TYPE_STATIC_SCOPE);
 
 /**
  * EphyHistoryService::urls-visited:
@@ -268,7 +268,7 @@ ephy_history_service_class_init (EphyHistoryServiceClass *klass)
                   0, NULL, NULL, NULL,
                   G_TYPE_NONE,
                   1,
-                  G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE);
+                  G_TYPE_POINTER | G_SIGNAL_TYPE_STATIC_SCOPE);
 
   signals[HOST_DELETED] =
     g_signal_new ("host-deleted",
@@ -579,6 +579,8 @@ ephy_history_service_execute_add_visit_helper (EphyHistoryService *self, EphyHis
   if (ephy_history_service_get_url_row (self, visit->url->url, visit->url) == NULL) {
     visit->url->last_visit_time = visit->visit_time;
     visit->url->visit_count = 1;
+    if (!visit->url->sync_id)
+      visit->url->sync_id = ephy_sync_utils_get_random_sync_id ();
 
     ephy_history_service_add_url_row (self, visit->url);
 
@@ -595,6 +597,9 @@ ephy_history_service_execute_add_visit_helper (EphyHistoryService *self, EphyHis
     ephy_history_service_update_url_row (self, visit->url);
   }
 
+  if (visit->url->notify_visit)
+    g_signal_emit (self, signals[VISIT_URL], 0, visit->url);
+
   ephy_history_service_add_visit_row (self, visit);
   return visit->id != -1;
 }
@@ -1058,7 +1063,7 @@ ephy_history_service_get_host_for_url (EphyHistoryService    *self,
 static gboolean
 delete_urls_signal_emit (SignalEmissionContext *ctx)
 {
-  char *url = (char *)ctx->user_data;
+  EphyHistoryURL *url = (EphyHistoryURL *)ctx->user_data;
 
   g_signal_emit (ctx->service, signals[URL_DELETED], 0, url);
 
@@ -1081,12 +1086,14 @@ ephy_history_service_execute_delete_urls (EphyHistoryService *self,
     url = l->data;
     ephy_history_service_delete_url (self, url);
 
-    ctx = signal_emission_context_new (self, g_strdup (url->url),
-                                       (GDestroyNotify)g_free);
-    g_idle_add_full (G_PRIORITY_DEFAULT_IDLE,
-                     (GSourceFunc)delete_urls_signal_emit,
-                     ctx,
-                     (GDestroyNotify)signal_emission_context_free);
+    if (url->notify_delete) {
+      ctx = signal_emission_context_new (self, ephy_history_url_copy (url),
+                                         (GDestroyNotify)ephy_history_url_free);
+      g_idle_add_full (G_PRIORITY_DEFAULT_IDLE,
+                       (GSourceFunc)delete_urls_signal_emit,
+                       ctx,
+                       (GDestroyNotify)signal_emission_context_free);
+    }
   }
 
   ephy_history_service_delete_orphan_hosts (self);
@@ -1305,22 +1312,23 @@ ephy_history_service_find_urls (EphyHistoryService *self,
 }
 
 void
-ephy_history_service_visit_url (EphyHistoryService      *self,
-                                const char              *url,
-                                EphyHistoryPageVisitType visit_type)
+ephy_history_service_visit_url (EphyHistoryService       *self,
+                                const char               *url,
+                                const char               *sync_id,
+                                gint64                    visit_time,
+                                EphyHistoryPageVisitType  visit_type,
+                                gboolean                  should_notify)
 {
   EphyHistoryPageVisit *visit;
 
   g_return_if_fail (EPHY_IS_HISTORY_SERVICE (self));
   g_return_if_fail (url != NULL);
+  g_return_if_fail (visit_time > 0);
 
-  g_signal_emit (self, signals[VISIT_URL], 0, url, visit_type);
-
-  visit = ephy_history_page_visit_new (url,
-                                       g_get_real_time (),
-                                       visit_type);
-  ephy_history_service_add_visit (self,
-                                  visit, NULL, NULL, NULL);
+  visit = ephy_history_page_visit_new (url, visit_time, visit_type);
+  visit->url->sync_id = g_strdup (sync_id);
+  visit->url->notify_visit = should_notify;
+  ephy_history_service_add_visit (self, visit, NULL, NULL, NULL);
   ephy_history_page_visit_free (visit);
 
   ephy_history_service_queue_urls_visited (self);
diff --git a/lib/history/ephy-history-service.h b/lib/history/ephy-history-service.h
index 61b0e89..82dbf59 100644
--- a/lib/history/ephy-history-service.h
+++ b/lib/history/ephy-history-service.h
@@ -52,7 +52,7 @@ void                     ephy_history_service_delete_host             (EphyHisto
 void                     ephy_history_service_get_url                 (EphyHistoryService *self, const char 
*url, GCancellable *cancellable, EphyHistoryJobCallback callback, gpointer user_data);
 void                     ephy_history_service_delete_urls             (EphyHistoryService *self, GList 
*urls, GCancellable *cancellable, EphyHistoryJobCallback callback, gpointer user_data);
 void                     ephy_history_service_find_urls               (EphyHistoryService *self, gint64 
from, gint64 to, guint limit, gint host, GList *substring_list, EphyHistorySortType sort_type, GCancellable 
*cancellable, EphyHistoryJobCallback callback, gpointer user_data);
-void                     ephy_history_service_visit_url               (EphyHistoryService *self, const char 
*orig_url, EphyHistoryPageVisitType visit_type);
+void                     ephy_history_service_visit_url               (EphyHistoryService *self, const char 
*url, const char *sync_id, gint64 visit_time, EphyHistoryPageVisitType visit_type, gboolean should_notify);
 void                     ephy_history_service_clear                   (EphyHistoryService *self, 
GCancellable *cancellable, EphyHistoryJobCallback callback, gpointer user_data);
 void                     ephy_history_service_find_hosts              (EphyHistoryService *self, gint64 
from, gint64 to, GCancellable *cancellable, EphyHistoryJobCallback callback, gpointer user_data);
 
diff --git a/lib/history/ephy-history-types.c b/lib/history/ephy-history-types.c
index b72a44b..088d1cd 100644
--- a/lib/history/ephy-history-types.c
+++ b/lib/history/ephy-history-types.c
@@ -127,10 +127,13 @@ ephy_history_url_new (const char *url, const char *title, int visit_count, int t
   history_url->id = -1;
   history_url->url = g_strdup (url);
   history_url->title = g_strdup (title);
+  history_url->sync_id = NULL;
   history_url->visit_count = visit_count;
   history_url->typed_count = typed_count;
   history_url->last_visit_time = last_visit_time;
   history_url->host = NULL;
+  history_url->notify_visit = TRUE;
+  history_url->notify_delete = TRUE;
   return history_url;
 }
 
@@ -147,9 +150,12 @@ ephy_history_url_copy (EphyHistoryURL *url)
                                url->typed_count,
                                url->last_visit_time);
   copy->id = url->id;
+  copy->sync_id = g_strdup (url->sync_id);
   copy->hidden = url->hidden;
   copy->host = ephy_history_host_copy (url->host);
   copy->thumbnail_time = url->thumbnail_time;
+  copy->notify_visit = url->notify_visit;
+  copy->notify_delete = url->notify_delete;
 
   return copy;
 }
@@ -162,6 +168,7 @@ ephy_history_url_free (EphyHistoryURL *url)
 
   g_free (url->url);
   g_free (url->title);
+  g_free (url->sync_id);
   ephy_history_host_free (url->host);
   g_slice_free1 (sizeof (EphyHistoryURL), url);
 }
diff --git a/lib/history/ephy-history-types.h b/lib/history/ephy-history-types.h
index 1a45e99..d9d1688 100644
--- a/lib/history/ephy-history-types.h
+++ b/lib/history/ephy-history-types.h
@@ -71,12 +71,15 @@ typedef struct _EphyHistoryURL
   int id;
   char* url;
   char* title;
+  char *sync_id;
   int visit_count;
   int typed_count;
   gint64 last_visit_time; /* Microseconds */
   gint64 thumbnail_time;  /* Seconds */
   gboolean hidden;
   EphyHistoryHost *host;
+  gboolean notify_visit;
+  gboolean notify_delete;
 } EphyHistoryURL;
 
 typedef struct _EphyHistoryPageVisit
diff --git a/lib/meson.build b/lib/meson.build
index b81c721..9b68094 100644
--- a/lib/meson.build
+++ b/lib/meson.build
@@ -33,6 +33,7 @@ libephymisc_sources = [
   'ephy-sqlite-connection.c',
   'ephy-sqlite-statement.c',
   'ephy-string.c',
+  'ephy-sync-utils.c',
   'ephy-time-helpers.c',
   'ephy-uri-helpers.c',
   'ephy-uri-tester-shared.c',
diff --git a/lib/sync/ephy-history-manager.c b/lib/sync/ephy-history-manager.c
new file mode 100644
index 0000000..1d21a63
--- /dev/null
+++ b/lib/sync/ephy-history-manager.c
@@ -0,0 +1,563 @@
+/* -*- 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-history-manager.h"
+
+#include "ephy-settings.h"
+#include "ephy-synchronizable-manager.h"
+
+struct _EphyHistoryManager {
+  GObject parent_instance;
+
+  EphyHistoryService *service;
+};
+
+static void ephy_synchronizable_manager_iface_init (EphySynchronizableManagerInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (EphyHistoryManager, ephy_history_manager, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (EPHY_TYPE_SYNCHRONIZABLE_MANAGER,
+                                                ephy_synchronizable_manager_iface_init))
+
+enum {
+  PROP_0,
+  PROP_HISTORY_SERVICE,
+  LAST_PROP
+};
+
+static GParamSpec *obj_properties[LAST_PROP];
+
+typedef struct {
+  EphyHistoryManager                     *manager;
+  gboolean                                is_initial;
+  GSList                                 *remotes_deleted;
+  GSList                                 *remotes_updated;
+  EphySynchronizableManagerMergeCallback  callback;
+  gpointer                                user_data;
+} MergeHistoryAsyncData;
+
+static MergeHistoryAsyncData *
+merge_history_async_data_new (EphyHistoryManager                     *manager,
+                              gboolean                                is_initial,
+                              GSList                                 *remotes_deleted,
+                              GSList                                 *remotes_updated,
+                              EphySynchronizableManagerMergeCallback  callback,
+                              gpointer                                user_data)
+{
+  MergeHistoryAsyncData *data;
+
+  data = g_slice_new (MergeHistoryAsyncData);
+  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_history_async_data_free (MergeHistoryAsyncData *data)
+{
+  g_assert (data);
+
+  g_object_unref (data->manager);
+  g_slice_free (MergeHistoryAsyncData, data);
+}
+
+static void
+url_visited_cb (EphyHistoryService *service,
+                EphyHistoryURL     *url,
+                EphyHistoryManager *self)
+{
+  EphyHistoryRecord *record;
+
+  record = ephy_history_record_new (url->sync_id, url->title, url->url, url->last_visit_time);
+  g_signal_emit_by_name (self, "synchronizable-modified", record, TRUE);
+  g_object_unref (record);
+}
+
+static void
+url_deleted_cb (EphyHistoryService *service,
+                EphyHistoryURL     *url,
+                EphyHistoryManager *self)
+{
+  EphyHistoryRecord *record;
+
+  record = ephy_history_record_new (url->sync_id, url->title, url->url, url->last_visit_time);
+  g_signal_emit_by_name (self, "synchronizable-deleted", record);
+  g_object_unref (record);
+}
+
+static void
+ephy_history_manager_set_property (GObject      *object,
+                                   guint         prop_id,
+                                   const GValue *value,
+                                   GParamSpec   *pspec)
+{
+  EphyHistoryManager *self = EPHY_HISTORY_MANAGER (object);
+
+  switch (prop_id) {
+    case PROP_HISTORY_SERVICE:
+      g_clear_object (&self->service);
+      self->service = g_object_ref (g_value_get_object (value));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+ephy_history_manager_get_property (GObject    *object,
+                                   guint       prop_id,
+                                   GValue     *value,
+                                   GParamSpec *pspec)
+{
+  EphyHistoryManager *self = EPHY_HISTORY_MANAGER (object);
+
+  switch (prop_id) {
+    case PROP_HISTORY_SERVICE:
+      g_value_set_object (value, self->service);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+ephy_history_manager_dispose (GObject *object)
+{
+  EphyHistoryManager *self = EPHY_HISTORY_MANAGER (object);
+
+  if (self->service) {
+    g_signal_handlers_disconnect_by_func (self->service, url_visited_cb, self);
+    g_signal_handlers_disconnect_by_func (self->service, url_deleted_cb, self);
+  }
+
+  g_clear_object (&self->service);
+
+  G_OBJECT_CLASS (ephy_history_manager_parent_class)->dispose (object);
+}
+
+static void
+ephy_history_manager_constructed (GObject *object)
+{
+  EphyHistoryManager *self = EPHY_HISTORY_MANAGER (object);
+
+  G_OBJECT_CLASS (ephy_history_manager_parent_class)->constructed (object);
+
+  g_signal_connect (self->service, "visit-url", G_CALLBACK (url_visited_cb), self);
+  g_signal_connect (self->service, "url-deleted", G_CALLBACK (url_deleted_cb), self);
+}
+
+static void
+ephy_history_manager_class_init (EphyHistoryManagerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->set_property = ephy_history_manager_set_property;
+  object_class->get_property = ephy_history_manager_get_property;
+  object_class->constructed = ephy_history_manager_constructed;
+  object_class->dispose = ephy_history_manager_dispose;
+
+  obj_properties[PROP_HISTORY_SERVICE] =
+    g_param_spec_object ("history-service",
+                         "History service",
+                         "History Service",
+                         EPHY_TYPE_HISTORY_SERVICE,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, LAST_PROP, obj_properties);
+}
+
+static void
+ephy_history_manager_init (EphyHistoryManager *self)
+{
+}
+
+EphyHistoryManager *
+ephy_history_manager_new (EphyHistoryService *service)
+{
+  return EPHY_HISTORY_MANAGER (g_object_new (EPHY_TYPE_HISTORY_MANAGER,
+                                             "history-service", service,
+                                             NULL));
+}
+
+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 ? "history" : "ephy-history";
+}
+
+static GType
+synchronizable_manager_get_synchronizable_type (EphySynchronizableManager *manager)
+{
+  return EPHY_TYPE_HISTORY_RECORD;
+}
+
+static gboolean
+synchronizable_manager_is_initial_sync (EphySynchronizableManager *manager)
+{
+  return g_settings_get_boolean (EPHY_SETTINGS_SYNC,
+                                 EPHY_PREFS_SYNC_HISTORY_INITIAL);
+}
+
+static void
+synchronizable_manager_set_is_initial_sync (EphySynchronizableManager *manager,
+                                            gboolean                   is_initial)
+{
+  g_settings_set_boolean (EPHY_SETTINGS_SYNC,
+                          EPHY_PREFS_SYNC_HISTORY_INITIAL,
+                          is_initial);
+}
+
+static double
+synchronizable_manager_get_sync_time (EphySynchronizableManager *manager)
+{
+  return g_settings_get_double (EPHY_SETTINGS_SYNC,
+                                EPHY_PREFS_SYNC_HISTORY_TIME);
+}
+
+static void
+synchronizable_manager_set_sync_time (EphySynchronizableManager *manager,
+                                      double                     sync_time)
+{
+  g_settings_set_double (EPHY_SETTINGS_SYNC,
+                         EPHY_PREFS_SYNC_HISTORY_TIME,
+                         sync_time);
+}
+
+static void
+synchronizable_manager_add (EphySynchronizableManager *manager,
+                            EphySynchronizable        *synchronizable)
+{
+  EphyHistoryManager *self = EPHY_HISTORY_MANAGER (manager);
+  EphyHistoryRecord *record = EPHY_HISTORY_RECORD (synchronizable);
+
+  if (ephy_history_record_get_last_visit_time (record) > 0)
+    ephy_history_service_visit_url (self->service,
+                                    ephy_history_record_get_uri (record),
+                                    ephy_history_record_get_id (record),
+                                    ephy_history_record_get_last_visit_time (record),
+                                    EPHY_PAGE_VISIT_LINK,
+                                    FALSE);
+}
+
+static void
+synchronizable_manager_remove (EphySynchronizableManager *manager,
+                               EphySynchronizable        *synchronizable)
+{
+  EphyHistoryManager *self = EPHY_HISTORY_MANAGER (manager);
+  EphyHistoryRecord *record = EPHY_HISTORY_RECORD (synchronizable);
+  EphyHistoryURL *url;
+  GList *to_delete = NULL;
+
+  url = ephy_history_url_new (ephy_history_record_get_uri (record),
+                              ephy_history_record_get_title (record),
+                              0, 0,
+                              ephy_history_record_get_last_visit_time (record));
+  url->notify_delete = FALSE;
+  to_delete = g_list_prepend (to_delete, url);
+  ephy_history_service_delete_urls (self->service, to_delete, NULL, NULL, NULL);
+
+  g_list_free_full (to_delete, (GDestroyNotify)ephy_history_url_free);
+}
+
+static void
+synchronizable_manager_save (EphySynchronizableManager *manager,
+                             EphySynchronizable        *synchronizable)
+{
+  /* No implementation.
+   * We don't care about the server time modified of history records.
+   */
+}
+
+static EphyHistoryRecord *
+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_history_record_get_id (l->data), id))
+      return l->data;
+  }
+
+  return NULL;
+}
+
+static EphyHistoryRecord *
+get_record_by_url (GSList     *records,
+                   const char *url)
+{
+  g_assert (url);
+
+  for (GSList *l = records; l && l->data; l = l->next) {
+    if (!g_strcmp0 (ephy_history_record_get_uri (l->data), url))
+      return l->data;
+  }
+
+  return NULL;
+}
+
+static void
+ephy_history_manager_handle_different_id_same_url (EphyHistoryManager *self,
+                                                   EphyHistoryRecord  *local,
+                                                   EphyHistoryRecord  *remote)
+{
+  gint64 local_last_visit_time;
+  gint64 remote_last_visit_time;
+
+  g_assert (EPHY_IS_HISTORY_MANAGER (self));
+  g_assert (EPHY_HISTORY_RECORD (local));
+  g_assert (EPHY_HISTORY_RECORD (remote));
+
+  local_last_visit_time = ephy_history_record_get_last_visit_time (local);
+  remote_last_visit_time = ephy_history_record_get_last_visit_time (remote);
+
+  if (remote_last_visit_time > local_last_visit_time)
+    ephy_history_service_visit_url (self->service,
+                                    ephy_history_record_get_uri (local),
+                                    ephy_history_record_get_id (local),
+                                    local_last_visit_time,
+                                    EPHY_PAGE_VISIT_LINK, FALSE);
+
+  ephy_history_record_set_id (remote, ephy_history_record_get_id (local));
+  ephy_history_record_add_visit_time (remote, local_last_visit_time);
+}
+
+static GSList *
+ephy_history_manager_handle_initial_merge (EphyHistoryManager *self,
+                                           GSList             *local_records,
+                                           GSList             *remote_records)
+{
+  EphyHistoryRecord *record;
+  GHashTable *dont_upload;
+  GSList *to_upload = NULL;
+  const char *remote_id;
+  const char *remote_url;
+  gint64 remote_last_visit_time;
+  gint64 local_last_visit_time;
+
+  g_assert (EPHY_IS_HISTORY_MANAGER (self));
+
+  /* A history record is uniquely identified by its sync ID or by its URL. When
+   * importing history records from server, we may encounter duplicates either
+   * by ID or by URL. We start from the assumption that same ID means same URL
+   * but same URL does not necessarily mean 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) {
+    remote_id = ephy_history_record_get_id (l->data);
+    remote_url = ephy_history_record_get_uri (l->data);
+    remote_last_visit_time = ephy_history_record_get_last_visit_time (l->data);
+
+    record = get_record_by_id (local_records, remote_id);
+    if (record) {
+      /* Same ID, same URL. Update last visit time for the local record and add
+       * the local last visit time to the remote one. */
+      local_last_visit_time = ephy_history_record_get_last_visit_time (record);
+      if (remote_last_visit_time > local_last_visit_time)
+        ephy_history_service_visit_url (self->service, remote_url,
+                                        remote_id, remote_last_visit_time,
+                                        EPHY_PAGE_VISIT_LINK, FALSE);
+
+      if (ephy_history_record_add_visit_time (l->data, local_last_visit_time))
+        to_upload = g_slist_prepend (to_upload, g_object_ref (l->data));
+
+      g_hash_table_add (dont_upload, (char *)remote_id);
+    } else {
+      record = get_record_by_url (local_records, remote_url);
+      if (record) {
+        /* Different ID, same URL. Keep local ID. */
+        g_signal_emit_by_name (self, "synchronizable-deleted", l->data);
+        ephy_history_manager_handle_different_id_same_url (self, record, l->data);
+        to_upload = g_slist_prepend (to_upload, g_object_ref (l->data));
+        g_hash_table_add (dont_upload, (char *)ephy_history_record_get_id (record));
+      } else {
+        /* Different ID, different URL. This is a new record. */
+        if (remote_last_visit_time > 0)
+          ephy_history_service_visit_url (self->service, remote_url,
+                                          remote_id, remote_last_visit_time,
+                                          EPHY_PAGE_VISIT_LINK, FALSE);
+      }
+    }
+  }
+
+  /* Set the remaining local records to be uploaded to server. */
+  for (GSList *l = local_records; l && l->data; l = l->next) {
+    record = EPHY_HISTORY_RECORD (l->data);
+    if (!g_hash_table_contains (dont_upload, ephy_history_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_history_manager_handle_regular_merge (EphyHistoryManager *self,
+                                           GSList             *local_records,
+                                           GSList             *deleted_records,
+                                           GSList             *updated_records)
+{
+  EphyHistoryRecord *record;
+  GSList *to_upload = NULL;
+  const char *remote_id;
+  const char *remote_url;
+  gint64 remote_last_visit_time;
+  gint64 local_last_visit_time;
+
+  g_assert (EPHY_IS_HISTORY_MANAGER (self));
+
+  for (GSList *l = deleted_records; l && l->data; l = l->next) {
+    record = get_record_by_id (local_records, ephy_history_record_get_id (l->data));
+    if (record)
+      ephy_synchronizable_manager_remove (EPHY_SYNCHRONIZABLE_MANAGER (self),
+                                          EPHY_SYNCHRONIZABLE (record));
+  }
+
+  /* See comment in ephy_history_manager_handle_initial_merge. */
+  for (GSList *l = updated_records; l && l->data; l = l->next) {
+    remote_id = ephy_history_record_get_id (l->data);
+    remote_url = ephy_history_record_get_uri (l->data);
+    remote_last_visit_time = ephy_history_record_get_last_visit_time (l->data);
+
+    record = get_record_by_id (local_records, remote_id);
+    if (record) {
+      /* Same ID, same URL. Update last visit time for the local record. */
+      local_last_visit_time = ephy_history_record_get_last_visit_time (record);
+
+      /* Firefox offers the option to "forget about this site" which means that
+       * the record is not deleted from server but only has its visit times
+       * deleted. Having no visit times translates to a negative last visit time
+       * in Epiphany. Since Epiphany does not support having a history record
+       * with no visit time, we delete it for good from the local database.
+       */
+      if (remote_last_visit_time <= 0)
+        ephy_synchronizable_manager_remove (EPHY_SYNCHRONIZABLE_MANAGER (self),
+                                            EPHY_SYNCHRONIZABLE (record));
+      else if (remote_last_visit_time > local_last_visit_time)
+        ephy_history_service_visit_url (self->service, remote_url,
+                                        remote_id, remote_last_visit_time,
+                                        EPHY_PAGE_VISIT_LINK, FALSE);
+    } else {
+      record = get_record_by_url (local_records, remote_url);
+      if (record) {
+        /* Different ID, same URL. Keep local ID. */
+        g_signal_emit_by_name (self, "synchronizable-deleted", l->data);
+        ephy_history_manager_handle_different_id_same_url (self, record, l->data);
+        to_upload = g_slist_prepend (to_upload, g_object_ref (l->data));
+      } else {
+        /* Different ID, different URL. This is a new record. */
+        if (remote_last_visit_time > 0)
+          ephy_history_service_visit_url (self->service, remote_url,
+                                          remote_id, remote_last_visit_time,
+                                          EPHY_PAGE_VISIT_LINK, FALSE);
+      }
+    }
+  }
+
+  return to_upload;
+}
+
+static void
+merge_history_cb (EphyHistoryService    *service,
+                  gboolean               success,
+                  GList                 *urls,
+                  MergeHistoryAsyncData *data)
+{
+  GSList *records = NULL;
+  GSList *to_upload = NULL;
+
+  if (!success) {
+    g_warning ("Failed to retrieve URLs in history");
+    goto out;
+  }
+
+  for (GList *l = urls; l && l->data; l = l->next) {
+    EphyHistoryURL *url = (EphyHistoryURL *)l->data;
+    records = g_slist_prepend (records, ephy_history_record_new (url->sync_id,
+                                                                 url->title,
+                                                                 url->url,
+                                                                 url->last_visit_time));
+  }
+
+  if (data->is_initial)
+    to_upload = ephy_history_manager_handle_initial_merge (data->manager,
+                                                           records,
+                                                           data->remotes_updated);
+  else
+    to_upload = ephy_history_manager_handle_regular_merge (data->manager,
+                                                           records,
+                                                           data->remotes_deleted,
+                                                           data->remotes_updated);
+
+out:
+  data->callback (to_upload, TRUE, data->user_data);
+
+  g_list_free_full (urls, (GDestroyNotify)ephy_history_url_free);
+  g_slist_free_full (records, g_object_unref);
+  merge_history_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)
+{
+  EphyHistoryManager *self = EPHY_HISTORY_MANAGER (manager);
+
+  ephy_history_service_find_urls (self->service, -1, -1, -1, 0, NULL,
+                                  EPHY_HISTORY_SORT_MOST_RECENTLY_VISITED, NULL,
+                                  (EphyHistoryJobCallback)merge_history_cb,
+                                  merge_history_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-history-manager.h b/lib/sync/ephy-history-manager.h
new file mode 100644
index 0000000..30f688c
--- /dev/null
+++ b/lib/sync/ephy-history-manager.h
@@ -0,0 +1,36 @@
+/* -*- 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-history-record.h"
+#include "ephy-history-service.h"
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define EPHY_TYPE_HISTORY_MANAGER (ephy_history_manager_get_type ())
+
+G_DECLARE_FINAL_TYPE (EphyHistoryManager, ephy_history_manager, EPHY, HISTORY_MANAGER, GObject)
+
+EphyHistoryManager *ephy_history_manager_new (EphyHistoryService *service);
+
+G_END_DECLS
diff --git a/lib/sync/ephy-history-record.c b/lib/sync/ephy-history-record.c
new file mode 100644
index 0000000..fc80392
--- /dev/null
+++ b/lib/sync/ephy-history-record.c
@@ -0,0 +1,404 @@
+/* -*- 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-history-record.h"
+
+#include "ephy-history-types.h"
+#include "ephy-synchronizable.h"
+
+struct _EphyHistoryRecord {
+  GObject parent_instance;
+
+  char      *id;
+  char      *title;
+  char      *uri;
+  GSequence *visits;
+};
+
+static void json_serializable_iface_init (JsonSerializableIface *iface);
+static void ephy_synchronizable_iface_init (EphySynchronizableInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (EphyHistoryRecord, ephy_history_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,
+  PROP_TITLE,
+  PROP_URI,
+  PROP_VISITS,
+  LAST_PROP
+};
+
+static GParamSpec *obj_properties[LAST_PROP];
+
+typedef struct {
+  gint64 timestamp; /* UNIX time in microseconds. */
+  guint type;       /* Transition type.*/
+} EphyHistoryRecordVisit;
+
+static EphyHistoryRecordVisit *
+ephy_history_record_visit_new (gint64 timestamp,
+                               guint  type)
+{
+  EphyHistoryRecordVisit *visit;
+
+  visit = g_slice_new (EphyHistoryRecordVisit);
+  visit->timestamp = timestamp;
+  visit->type = type;
+
+  return visit;
+}
+
+static void
+ephy_history_record_visit_free (EphyHistoryRecordVisit *visit)
+{
+  g_assert (visit);
+
+  g_slice_free (EphyHistoryRecordVisit, visit);
+}
+
+static int
+ephy_history_record_visit_compare (EphyHistoryRecordVisit *visit1,
+                                   EphyHistoryRecordVisit *visit2,
+                                   gpointer                user_data)
+{
+  g_assert (visit1);
+  g_assert (visit2);
+
+  /* We keep visits sorted in descending order by timestamp. */
+  return visit2->timestamp - visit1->timestamp;
+}
+
+static void
+ephy_history_record_set_property (GObject      *object,
+                                  guint         prop_id,
+                                  const GValue *value,
+                                  GParamSpec   *pspec)
+{
+  EphyHistoryRecord *self = EPHY_HISTORY_RECORD (object);
+
+  switch (prop_id) {
+    case PROP_ID:
+      g_free (self->id);
+      self->id = g_strdup (g_value_get_string (value));
+      break;
+    case PROP_TITLE:
+      g_free (self->title);
+      self->title = g_strdup (g_value_get_string (value));
+      break;
+    case PROP_URI:
+      g_free (self->uri);
+      self->uri = g_strdup (g_value_get_string (value));
+      break;
+    case PROP_VISITS:
+      if (self->visits)
+        g_sequence_free (self->visits);
+      self->visits = g_value_get_pointer (value);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+ephy_history_record_get_property (GObject    *object,
+                                  guint       prop_id,
+                                  GValue     *value,
+                                  GParamSpec *pspec)
+{
+  EphyHistoryRecord *self = EPHY_HISTORY_RECORD (object);
+
+  switch (prop_id) {
+    case PROP_ID:
+      g_value_set_string (value, self->id);
+      break;
+    case PROP_TITLE:
+      g_value_set_string (value, self->title);
+      break;
+    case PROP_URI:
+      g_value_set_string (value, self->uri);
+      break;
+    case PROP_VISITS:
+      g_value_set_pointer (value, self->visits);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+ephy_history_record_dispose (GObject *object)
+{
+  EphyHistoryRecord *self = EPHY_HISTORY_RECORD (object);
+
+  g_clear_pointer (&self->id, g_free);
+  g_clear_pointer (&self->title, g_free);
+  g_clear_pointer (&self->uri, g_free);
+  g_clear_pointer (&self->visits, g_sequence_free);
+
+  G_OBJECT_CLASS (ephy_history_record_parent_class)->dispose (object);
+}
+
+static void
+ephy_history_record_class_init (EphyHistoryRecordClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->set_property = ephy_history_record_set_property;
+  object_class->get_property = ephy_history_record_get_property;
+  object_class->dispose = ephy_history_record_dispose;
+
+  obj_properties[PROP_ID] =
+    g_param_spec_string ("id",
+                         "Id",
+                         "Id of the history record",
+                         "Default id",
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+  obj_properties[PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "Title of the history record",
+                         "Default title",
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+  obj_properties[PROP_URI] =
+    g_param_spec_string ("histUri",
+                         "History URI",
+                         "URI of the history record",
+                         "Default history uri",
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+  obj_properties[PROP_VISITS] =
+    g_param_spec_pointer ("visits",
+                          "Visits",
+                          "An array of how and when URI of the history record was visited",
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, LAST_PROP, obj_properties);
+}
+
+static void
+ephy_history_record_init (EphyHistoryRecord *self)
+{
+}
+
+EphyHistoryRecord *
+ephy_history_record_new (const char *id,
+                         const char *title,
+                         const char *uri,
+                         gint64      last_visit_time)
+{
+  EphyHistoryRecordVisit *visit;
+  GSequence *visits;
+
+  /* We only use link transition for now. */
+  visit = ephy_history_record_visit_new (last_visit_time, EPHY_PAGE_VISIT_LINK);
+  visits = g_sequence_new ((GDestroyNotify)ephy_history_record_visit_free);
+  g_sequence_prepend (visits, visit);
+
+  return EPHY_HISTORY_RECORD (g_object_new (EPHY_TYPE_HISTORY_RECORD,
+                                            "id", id,
+                                            "title", title,
+                                            "histUri", uri,
+                                            "visits", visits,
+                                            NULL));
+}
+
+void
+ephy_history_record_set_id (EphyHistoryRecord *self,
+                            const char        *id)
+{
+  g_return_if_fail (EPHY_IS_HISTORY_RECORD (self));
+  g_return_if_fail (id);
+
+  g_free (self->id);
+  self->id = g_strdup (id);
+}
+
+const char *
+ephy_history_record_get_id (EphyHistoryRecord *self)
+{
+  g_return_val_if_fail (EPHY_IS_HISTORY_RECORD (self), NULL);
+
+  return self->id;
+}
+
+const char *
+ephy_history_record_get_title (EphyHistoryRecord *self)
+{
+  g_return_val_if_fail (EPHY_IS_HISTORY_RECORD (self), NULL);
+
+  return self->title;
+}
+
+const char *
+ephy_history_record_get_uri (EphyHistoryRecord *self)
+{
+  g_return_val_if_fail (EPHY_IS_HISTORY_RECORD (self), NULL);
+
+  return self->uri;
+}
+
+gint64
+ephy_history_record_get_last_visit_time (EphyHistoryRecord *self)
+{
+  EphyHistoryRecordVisit *visit;
+
+  g_return_val_if_fail (EPHY_IS_HISTORY_RECORD (self), -1);
+  g_return_val_if_fail (self->visits, -1);
+
+  if (g_sequence_is_empty (self->visits))
+    return -1;
+
+  /* Visits are sorted in descending order by date. */
+  visit = (EphyHistoryRecordVisit *)g_sequence_get (g_sequence_get_begin_iter (self->visits));
+
+  return visit->timestamp;
+}
+
+gboolean
+ephy_history_record_add_visit_time (EphyHistoryRecord *self,
+                                    gint64             visit_time)
+{
+  EphyHistoryRecordVisit *visit;
+
+  g_return_val_if_fail (EPHY_IS_HISTORY_RECORD (self), FALSE);
+
+  visit = ephy_history_record_visit_new (visit_time, EPHY_PAGE_VISIT_LINK);
+  if (g_sequence_lookup (self->visits, visit,
+                         (GCompareDataFunc)ephy_history_record_visit_compare,
+                         NULL)) {
+    ephy_history_record_visit_free (visit);
+    return FALSE;
+  }
+
+  g_sequence_insert_sorted (self->visits, visit,
+                            (GCompareDataFunc)ephy_history_record_visit_compare,
+                            NULL);
+
+  return TRUE;
+}
+
+static JsonNode *
+serializable_serialize_property (JsonSerializable *serializable,
+                                 const char       *name,
+                                 const GValue     *value,
+                                 GParamSpec       *pspec)
+{
+  if (!g_strcmp0 (name, "visits")) {
+    JsonNode *node;
+    JsonArray *array;
+    GSequence *visits;
+    GSequenceIter *it;
+
+    node = json_node_new (JSON_NODE_ARRAY);
+    array = json_array_new ();
+    visits = g_value_get_pointer (value);
+
+    if (visits != NULL) {
+      for (it = g_sequence_get_begin_iter (visits); !g_sequence_iter_is_end (it); it = g_sequence_iter_next 
(it)) {
+        EphyHistoryRecordVisit *visit = g_sequence_get (it);
+        JsonObject *object = json_object_new ();
+        json_object_set_int_member (object, "date", visit->timestamp);
+        json_object_set_int_member (object, "type", visit->type);
+        json_array_add_object_element (array, object);
+      }
+    }
+
+    json_node_set_array (node, array);
+
+    return node;
+  }
+
+  return json_serializable_default_serialize_property (serializable, name, value, pspec);
+}
+
+static gboolean
+serializable_deserialize_property (JsonSerializable *serializable,
+                                   const char       *name,
+                                   GValue           *value,
+                                   GParamSpec       *pspec,
+                                   JsonNode         *node)
+{
+  if (!g_strcmp0 (name, "visits")) {
+    JsonArray *array;
+    GSequence *visits;
+
+    array = json_node_get_array (node);
+    visits = g_sequence_new ((GDestroyNotify)ephy_history_record_visit_free);
+
+    for (guint i = 0; i < json_array_get_length (array); i++) {
+      JsonObject *object = json_node_get_object (json_array_get_element (array, i));
+      gint64 timestamp = json_object_get_int_member (object, "date");
+      guint type = json_object_get_int_member (object, "type");
+      EphyHistoryRecordVisit *visit = ephy_history_record_visit_new (timestamp, type);
+      g_sequence_insert_sorted (visits, visit, (GCompareDataFunc)ephy_history_record_visit_compare, NULL);
+    }
+
+    g_value_set_pointer (value, visits);
+
+    return TRUE;
+  }
+
+  return json_serializable_default_deserialize_property (serializable, name, value, pspec, node);
+}
+
+static void
+json_serializable_iface_init (JsonSerializableIface *iface)
+{
+  iface->serialize_property = serializable_serialize_property;
+  iface->deserialize_property = serializable_deserialize_property;
+}
+
+static const char *
+synchronizable_get_id (EphySynchronizable *synchronizable)
+{
+  return ephy_history_record_get_id (EPHY_HISTORY_RECORD (synchronizable));
+}
+
+static double
+synchronizable_get_server_time_modified (EphySynchronizable *synchronizable)
+{
+  /* No implementation.
+   * We don't care about the server time modified of history records.
+   */
+  return 0;
+}
+
+static void
+synchronizable_set_server_time_modified (EphySynchronizable *synchronizable,
+                                         double              server_time_modified)
+{
+  /* No implementation.
+   * We don't care about the server time modified of history records.
+   */
+}
+
+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-history-record.h b/lib/sync/ephy-history-record.h
new file mode 100644
index 0000000..37e64a6
--- /dev/null
+++ b/lib/sync/ephy-history-record.h
@@ -0,0 +1,44 @@
+/* -*- 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>
+
+G_BEGIN_DECLS
+
+#define EPHY_TYPE_HISTORY_RECORD (ephy_history_record_get_type ())
+
+G_DECLARE_FINAL_TYPE (EphyHistoryRecord, ephy_history_record, EPHY, HISTORY_RECORD, GObject)
+
+EphyHistoryRecord *ephy_history_record_new                 (const char *id,
+                                                            const char *title,
+                                                            const char *history_uri,
+                                                            gint64      last_visit_time);
+void               ephy_history_record_set_id              (EphyHistoryRecord *self,
+                                                            const char        *id);
+const char        *ephy_history_record_get_id              (EphyHistoryRecord *self);
+const char        *ephy_history_record_get_title           (EphyHistoryRecord *self);
+const char        *ephy_history_record_get_uri             (EphyHistoryRecord *self);
+gint64             ephy_history_record_get_last_visit_time (EphyHistoryRecord *self);
+gboolean           ephy_history_record_add_visit_time      (EphyHistoryRecord *self,
+                                                            gint64             visit_time);
+
+G_END_DECLS
diff --git a/lib/sync/ephy-password-manager.c b/lib/sync/ephy-password-manager.c
index 10e1ae3..a84ddfd 100644
--- a/lib/sync/ephy-password-manager.c
+++ b/lib/sync/ephy-password-manager.c
@@ -446,7 +446,7 @@ update_password_cb (GSList   *records,
   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_signal_emit_by_name (data->manager, "synchronizable-modified", record, FALSE);
 
   g_slist_free_full (records, g_object_unref);
   update_password_async_data_free (data);
@@ -492,7 +492,7 @@ ephy_password_manager_save (EphyPasswordManager *self,
                                      username_field, password_field,
                                      timestamp, timestamp);
   ephy_password_manger_store_record (self, record);
-  g_signal_emit_by_name (self, "synchronizable-modified", record);
+  g_signal_emit_by_name (self, "synchronizable-modified", record, FALSE);
 
   g_free (hostname);
   g_free (uuid);
@@ -1063,7 +1063,7 @@ merge_cb (GSList   *records,
                                                             data->remotes_deleted,
                                                             data->remotes_updated);
 
-  data->callback (to_upload, data->user_data);
+  data->callback (to_upload, FALSE, data->user_data);
 
   g_slist_free_full (records, g_object_unref);
   merge_passwords_async_data_free (data);
diff --git a/lib/sync/ephy-sync-crypto.c b/lib/sync/ephy-sync-crypto.c
index 7607053..d7b6a33 100644
--- a/lib/sync/ephy-sync-crypto.c
+++ b/lib/sync/ephy-sync-crypto.c
@@ -21,6 +21,8 @@
 #include "config.h"
 #include "ephy-sync-crypto.h"
 
+#include "ephy-sync-utils.h"
+
 #include <glib/gstdio.h>
 #include <inttypes.h>
 #include <libsoup/soup.h>
@@ -31,9 +33,6 @@
 #define HAWK_VERSION  1
 #define NONCE_LEN     6
 #define IV_LEN        16
-#define SYNC_ID_LEN   12
-
-static const char hex_digits[] = "0123456789abcdef";
 
 SyncCryptoHawkOptions *
 ephy_sync_crypto_hawk_options_new (const char *app,
@@ -204,8 +203,8 @@ ephy_sync_crypto_key_bundle_from_array (JsonArray *array)
 
   aes_key = g_base64_decode (json_array_get_string_element (array, 0), &len);
   hmac_key = g_base64_decode (json_array_get_string_element (array, 1), &len);
-  aes_key_hex = ephy_sync_crypto_encode_hex (aes_key, 32);
-  hmac_key_hex = ephy_sync_crypto_encode_hex (hmac_key, 32);
+  aes_key_hex = ephy_sync_utils_encode_hex (aes_key, 32);
+  hmac_key_hex = ephy_sync_utils_encode_hex (hmac_key, 32);
   bundle = ephy_sync_crypto_key_bundle_new (aes_key_hex, hmac_key_hex);
 
   g_free (aes_key);
@@ -388,7 +387,7 @@ ephy_sync_crypto_calculate_payload_hash (const char *payload,
                             HAWK_VERSION, content, payload);
 
   digest_hex = g_compute_checksum_for_string (G_CHECKSUM_SHA256, update, -1);
-  digest = ephy_sync_crypto_decode_hex (digest_hex);
+  digest = ephy_sync_utils_decode_hex (digest_hex);
   hash = g_base64_encode (digest, g_checksum_type_get_length (G_CHECKSUM_SHA256));
 
   g_free (content);
@@ -419,7 +418,7 @@ ephy_sync_crypto_calculate_mac (const char              *type,
   digest_hex = g_compute_hmac_for_string (G_CHECKSUM_SHA256,
                                           key, key_len,
                                           normalized, -1);
-  digest = ephy_sync_crypto_decode_hex (digest_hex);
+  digest = ephy_sync_utils_decode_hex (digest_hex);
   mac = g_base64_encode (digest, g_checksum_type_get_length (G_CHECKSUM_SHA256));
 
   g_free (normalized);
@@ -517,7 +516,7 @@ ephy_sync_crypto_hkdf (const guint8 *in,
   prk_hex = g_compute_hmac_for_data (G_CHECKSUM_SHA256,
                                      salt, salt_len,
                                      in, in_len);
-  prk = ephy_sync_crypto_decode_hex (prk_hex);
+  prk = ephy_sync_utils_decode_hex (prk_hex);
 
   /* Step 2: Expand */
   counter = 1;
@@ -539,7 +538,7 @@ ephy_sync_crypto_hkdf (const guint8 *in,
     tmp_hex = g_compute_hmac_for_data (G_CHECKSUM_SHA256,
                                        prk, hash_len,
                                        data, data_len);
-    tmp = ephy_sync_crypto_decode_hex (tmp_hex);
+    tmp = ephy_sync_utils_decode_hex (tmp_hex);
     memcpy (out_full + i * hash_len, tmp, hash_len);
 
     g_free (data);
@@ -555,26 +554,6 @@ ephy_sync_crypto_hkdf (const guint8 *in,
   g_free (out_full);
 }
 
-static void
-ephy_sync_crypto_b64_to_b64_urlsafe (char *text)
-{
-  g_assert (text);
-
-  /* Replace '+' with '-' and '/' with '_' */
-  g_strcanon (text, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789=/", '-');
-  g_strcanon (text, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789=-", '_');
-}
-
-static void
-ephy_sync_crypto_b64_urlsafe_to_b64 (char *text)
-{
-  g_assert (text);
-
-  /* Replace '-' with '+' and '_' with '/' */
-  g_strcanon (text, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789=_", '+');
-  g_strcanon (text, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789=+", '/');
-}
-
 static guint8 *
 ephy_sync_crypto_pad (const char *text,
                       gsize       block_len,
@@ -698,27 +677,6 @@ ephy_sync_crypto_hmac_is_valid (const char   *text,
   return retval;
 }
 
-/*
- * This function is required by Nettle's RSA support.
- * From Nettle's documentation: random_ctx and random is a randomness generator.
- * random(random_ctx, length, dst) should generate length random octets and store them at dst.
- * We don't really use random_ctx, since we have /dev/urandom available.
- */
-static void
-ephy_sync_crypto_random_bytes_gen (void   *random_ctx,
-                                   gsize   length,
-                                   guint8 *dst)
-{
-  FILE *fp;
-
-  g_assert (length > 0);
-  g_assert (dst);
-
-  fp = fopen ("/dev/urandom", "r");
-  fread (dst, sizeof (guint8), length, fp);
-  fclose (fp);
-}
-
 void
 ephy_sync_crypto_process_key_fetch_token (const char  *key_fetch_token,
                                           guint8     **token_id,
@@ -740,7 +698,7 @@ ephy_sync_crypto_process_key_fetch_token (const char  *key_fetch_token,
   g_return_if_fail (resp_hmac_key);
   g_return_if_fail (resp_xor_key);
 
-  kft = ephy_sync_crypto_decode_hex (key_fetch_token);
+  kft = ephy_sync_utils_decode_hex (key_fetch_token);
   info_kft = ephy_sync_crypto_kw ("keyFetchToken");
   info_keys = ephy_sync_crypto_kw ("account/keys");
   out1 = g_malloc (3 * token_len);
@@ -794,7 +752,7 @@ ephy_sync_crypto_process_session_token (const char  *session_token,
   g_return_if_fail (req_hmac_key);
   g_return_if_fail (request_key);
 
-  st = ephy_sync_crypto_decode_hex (session_token);
+  st = ephy_sync_utils_decode_hex (session_token);
   info = ephy_sync_crypto_kw ("sessionToken");
   out = g_malloc (3 * token_len);
 
@@ -841,7 +799,7 @@ ephy_sync_crypto_compute_sync_keys (const char    *bundle_hex,
   g_return_val_if_fail (kA, FALSE);
   g_return_val_if_fail (kB, FALSE);
 
-  bundle = ephy_sync_crypto_decode_hex (bundle_hex);
+  bundle = ephy_sync_utils_decode_hex (bundle_hex);
   ciphertext = g_malloc (2 * key_len);
   resp_hmac = g_malloc (key_len);
 
@@ -851,7 +809,7 @@ ephy_sync_crypto_compute_sync_keys (const char    *bundle_hex,
   resp_hmac_2_hex = g_compute_hmac_for_data (G_CHECKSUM_SHA256,
                                              resp_hmac_key, key_len,
                                              ciphertext, 2 * key_len);
-  resp_hmac_2 = ephy_sync_crypto_decode_hex (resp_hmac_2_hex);
+  resp_hmac_2 = ephy_sync_utils_decode_hex (resp_hmac_2_hex);
   if (!ephy_sync_crypto_equals (resp_hmac, resp_hmac_2, key_len)) {
     g_warning ("HMAC values differs from the one expected");
     retval = FALSE;
@@ -904,14 +862,14 @@ ephy_sync_crypto_derive_key_bundle (const guint8 *key,
   prk_hex = g_compute_hmac_for_data (G_CHECKSUM_SHA256,
                                      salt, key_len,
                                      key, key_len);
-  prk = ephy_sync_crypto_decode_hex (prk_hex);
+  prk = ephy_sync_utils_decode_hex (prk_hex);
   tmp = ephy_sync_crypto_concat_bytes ((guint8 *)info, strlen (info),
                                        "\x01", 1,
                                        NULL);
   aes_key_hex = g_compute_hmac_for_data (G_CHECKSUM_SHA256,
                                          prk, key_len,
                                          tmp, strlen (info) + 1);
-  aes_key = ephy_sync_crypto_decode_hex (aes_key_hex);
+  aes_key = ephy_sync_utils_decode_hex (aes_key_hex);
   g_free (tmp);
   tmp = ephy_sync_crypto_concat_bytes (aes_key, key_len,
                                        (guint8 *)info, strlen (info),
@@ -945,10 +903,10 @@ ephy_sync_crypto_generate_crypto_keys (gsize key_len)
   char *payload;
 
   aes_key = g_malloc (key_len);
-  ephy_sync_crypto_random_bytes_gen (NULL, key_len, aes_key);
+  ephy_sync_utils_generate_random_bytes (NULL, key_len, aes_key);
   aes_key_b64 = g_base64_encode (aes_key, key_len);
   hmac_key = g_malloc (key_len);
-  ephy_sync_crypto_random_bytes_gen (NULL, key_len, hmac_key);
+  ephy_sync_utils_generate_random_bytes (NULL, key_len, hmac_key);
   hmac_key_b64 = g_base64_encode (hmac_key, key_len);
 
   node = json_node_new (JSON_NODE_OBJECT);
@@ -1014,8 +972,8 @@ ephy_sync_crypto_decrypt_record (const char          *payload,
   }
 
   /* Get the encryption key and the HMAC key. */
-  aes_key = ephy_sync_crypto_decode_hex (bundle->aes_key_hex);
-  hmac_key = ephy_sync_crypto_decode_hex (bundle->hmac_key_hex);
+  aes_key = ephy_sync_utils_decode_hex (bundle->aes_key_hex);
+  hmac_key = ephy_sync_utils_decode_hex (bundle->hmac_key_hex);
 
   /* Under no circumstances should a client try to decrypt a record
    * if the HMAC verification fails. */
@@ -1063,12 +1021,12 @@ ephy_sync_crypto_encrypt_record (const char          *cleartext,
   g_return_val_if_fail (bundle, NULL);
 
   /* Get the encryption key and the HMAC key. */
-  aes_key = ephy_sync_crypto_decode_hex (bundle->aes_key_hex);
-  hmac_key = ephy_sync_crypto_decode_hex (bundle->hmac_key_hex);
+  aes_key = ephy_sync_utils_decode_hex (bundle->aes_key_hex);
+  hmac_key = ephy_sync_utils_decode_hex (bundle->hmac_key_hex);
 
   /* Generate a random 16 bytes initialization vector. */
   iv = g_malloc (IV_LEN);
-  ephy_sync_crypto_random_bytes_gen (NULL, IV_LEN, iv);
+  ephy_sync_utils_generate_random_bytes (NULL, IV_LEN, iv);
 
   /* Encrypt the record using the AES key. */
   ciphertext = ephy_sync_crypto_aes_256_encrypt (cleartext, aes_key,
@@ -1142,8 +1100,8 @@ ephy_sync_crypto_compute_hawk_header (const char            *url,
     nonce = g_strdup (options->nonce);
   } else {
     bytes = g_malloc (NONCE_LEN / 2);
-    ephy_sync_crypto_random_bytes_gen (NULL, NONCE_LEN / 2, bytes);
-    nonce = ephy_sync_crypto_encode_hex (bytes, NONCE_LEN / 2);
+    ephy_sync_utils_generate_random_bytes (NULL, NONCE_LEN / 2, bytes);
+    nonce = ephy_sync_utils_encode_hex (bytes, NONCE_LEN / 2);
     g_free (bytes);
   }
 
@@ -1237,7 +1195,7 @@ ephy_sync_crypto_generate_rsa_key_pair (void)
 
   /* Key sizes below 2048 are considered breakable and should not be used. */
   success = rsa_generate_keypair (&public, &private,
-                                  NULL, ephy_sync_crypto_random_bytes_gen,
+                                  NULL, ephy_sync_utils_generate_random_bytes,
                                   NULL, NULL, 2048, 0);
   /* Given correct parameters, this never fails. */
   g_assert (success);
@@ -1274,18 +1232,18 @@ ephy_sync_crypto_create_assertion (const char           *certificate,
   /* Encode the header and body to base64 url safe and join them. */
   expires_at = g_get_real_time () / 1000 + seconds * 1000;
   body = g_strdup_printf ("{\"exp\": %lu, \"aud\": \"%s\"}", expires_at, audience);
-  body_b64 = ephy_sync_crypto_base64_urlsafe_encode ((guint8 *)body, strlen (body), TRUE);
-  header_b64 = ephy_sync_crypto_base64_urlsafe_encode ((guint8 *)header, strlen (header), TRUE);
+  body_b64 = ephy_sync_utils_base64_urlsafe_encode ((guint8 *)body, strlen (body), TRUE);
+  header_b64 = ephy_sync_utils_base64_urlsafe_encode ((guint8 *)header, strlen (header), TRUE);
   to_sign = g_strdup_printf ("%s.%s", header_b64, body_b64);
 
   /* Compute the SHA256 hash of the message to be signed. */
   digest_hex = g_compute_checksum_for_string (G_CHECKSUM_SHA256, to_sign, -1);
-  digest = ephy_sync_crypto_decode_hex (digest_hex);
+  digest = ephy_sync_utils_decode_hex (digest_hex);
 
   /* Use the provided key pair to RSA sign the message. */
   mpz_init (signature);
   success = rsa_sha256_sign_digest_tr (&rsa_key_pair->public, &rsa_key_pair->private,
-                                       NULL, ephy_sync_crypto_random_bytes_gen,
+                                       NULL, ephy_sync_utils_generate_random_bytes,
                                        digest, signature);
   /* Given correct parameters, this never fails. */
   g_assert (success);
@@ -1297,7 +1255,7 @@ ephy_sync_crypto_create_assertion (const char           *certificate,
   g_assert (count == expected_size);
 
   /* Join certificate, header, body and signed message to create the assertion. */
-  sig_b64 = ephy_sync_crypto_base64_urlsafe_encode (sig, count, TRUE);
+  sig_b64 = ephy_sync_utils_base64_urlsafe_encode (sig, count, TRUE);
   assertion = g_strdup_printf ("%s~%s.%s.%s", certificate, header_b64, body_b64, sig_b64);
 
   g_free (body);
@@ -1312,118 +1270,3 @@ ephy_sync_crypto_create_assertion (const char           *certificate,
 
   return assertion;
 }
-
-char *
-ephy_sync_crypto_base64_urlsafe_encode (const guint8 *data,
-                                        gsize         data_len,
-                                        gboolean      strip)
-{
-  char *base64;
-  char *out;
-  gsize start = 0;
-  gssize end;
-
-  g_return_val_if_fail (data, NULL);
-
-  base64 = g_base64_encode (data, data_len);
-  end = strlen (base64) - 1;
-
-  /* Strip the data of any leading or trailing '=' characters. */
-  if (strip) {
-    while (start < strlen (base64) && base64[start] == '=')
-      start++;
-
-    while (end >= 0 && base64[end] == '=')
-      end--;
-  }
-
-  out = g_strndup (base64 + start, end - start + 1);
-  ephy_sync_crypto_b64_to_b64_urlsafe (out);
-
-  g_free (base64);
-
-  return out;
-}
-
-guint8 *
-ephy_sync_crypto_base64_urlsafe_decode (const char  *text,
-                                        gsize       *out_len,
-                                        gboolean     fill)
-{
-  guint8 *out;
-  char *to_decode;
-  char *suffix = NULL;
-
-  g_return_val_if_fail (text, NULL);
-  g_return_val_if_fail (out_len, NULL);
-
-  /* Fill the text with trailing '=' characters up to the proper length. */
-  if (fill)
-    suffix = g_strnfill ((4 - strlen (text) % 4) % 4, '=');
-
-  to_decode = g_strconcat (text, suffix, NULL);
-  ephy_sync_crypto_b64_urlsafe_to_b64 (to_decode);
-  out = g_base64_decode (to_decode, out_len);
-
-  g_free (suffix);
-  g_free (to_decode);
-
-  return out;
-}
-
-char *
-ephy_sync_crypto_encode_hex (const guint8 *data,
-                             gsize         data_len)
-{
-  char *retval;
-
-  g_return_val_if_fail (data, NULL);
-
-  retval = g_malloc (data_len * 2 + 1);
-  for (gsize i = 0; i < data_len; i++) {
-    guint8 byte = data[i];
-
-    retval[2 * i] = hex_digits[byte >> 4];
-    retval[2 * i + 1] = hex_digits[byte & 0xf];
-  }
-  retval[data_len * 2] = 0;
-
-  return retval;
-}
-
-guint8 *
-ephy_sync_crypto_decode_hex (const char *hex)
-{
-  guint8 *retval;
-
-  g_return_val_if_fail (hex, NULL);
-
-  retval = g_malloc (strlen (hex) / 2);
-  for (gsize i = 0, j = 0; i < strlen (hex); i += 2, j++)
-    sscanf (hex + i, "%2hhx", retval + j);
-
-  return retval;
-}
-
-char *
-ephy_sync_crypto_get_random_sync_id (void)
-{
-  char *id;
-  char *base64;
-  guint8 *bytes;
-  gsize bytes_len;
-
-  /* The sync id is a base64-urlsafe string. Base64 uses 4 chars to represent 3 bytes,
-   * therefore we need ceil(len * 3 / 4) bytes to cover the requested length. */
-  bytes_len = (SYNC_ID_LEN + 3) / 4 * 3;
-  bytes = g_malloc (bytes_len);
-
-  ephy_sync_crypto_random_bytes_gen (NULL, bytes_len, bytes);
-  base64 = ephy_sync_crypto_base64_urlsafe_encode (bytes, bytes_len, FALSE);
-  id = g_strndup (base64, SYNC_ID_LEN);
-
-  g_free (base64);
-  g_free (bytes);
-
-  return id;
-}
diff --git a/lib/sync/ephy-sync-crypto.h b/lib/sync/ephy-sync-crypto.h
index f753e3d..3b275ce 100644
--- a/lib/sync/ephy-sync-crypto.h
+++ b/lib/sync/ephy-sync-crypto.h
@@ -116,15 +116,5 @@ char                   *ephy_sync_crypto_create_assertion         (const char
                                                                    const char           *audience,
                                                                    guint64               duration,
                                                                    SyncCryptoRSAKeyPair *rsa_key_pair);
-char                   *ephy_sync_crypto_base64_urlsafe_encode    (const guint8 *data,
-                                                                   gsize         data_len,
-                                                                   gboolean      strip);
-guint8                 *ephy_sync_crypto_base64_urlsafe_decode    (const char *text,
-                                                                   gsize      *out_len,
-                                                                   gboolean    fill);
-char                   *ephy_sync_crypto_encode_hex               (const guint8 *data,
-                                                                   gsize         data_len);
-guint8                 *ephy_sync_crypto_decode_hex               (const char *hex);
-char                   *ephy_sync_crypto_get_random_sync_id       (void);
 
 G_END_DECLS
diff --git a/lib/sync/ephy-sync-service.c b/lib/sync/ephy-sync-service.c
index 34b5703..bb56707 100644
--- a/lib/sync/ephy-sync-service.c
+++ b/lib/sync/ephy-sync-service.c
@@ -26,6 +26,7 @@
 #include "ephy-notification.h"
 #include "ephy-settings.h"
 #include "ephy-sync-crypto.h"
+#include "ephy-sync-utils.h"
 
 #include <glib/gi18n.h>
 #include <json-glib/json-glib.h>
@@ -564,8 +565,8 @@ ephy_sync_service_certificate_is_valid (EphySyncService *self,
   g_assert (certificate);
 
   pieces = g_strsplit (certificate, ".", 0);
-  header = (char *)ephy_sync_crypto_base64_urlsafe_decode (pieces[0], &len, TRUE);
-  payload = (char *)ephy_sync_crypto_base64_urlsafe_decode (pieces[1], &len, TRUE);
+  header = (char *)ephy_sync_utils_base64_urlsafe_decode (pieces[0], &len, TRUE);
+  payload = (char *)ephy_sync_utils_base64_urlsafe_decode (pieces[1], &len, TRUE);
   parser = json_parser_new ();
 
   json_parser_load_from_data (parser, header, -1, &error);
@@ -662,7 +663,7 @@ ephy_sync_service_destroy_session (EphySyncService *self,
   url = g_strdup_printf ("%ssession/destroy", FIREFOX_ACCOUNTS_SERVER_URL);
   ephy_sync_crypto_process_session_token (session_token, &token_id,
                                           &req_hmac_key, &request_key, 32);
-  token_id_hex = ephy_sync_crypto_encode_hex (token_id, 32);
+  token_id_hex = ephy_sync_utils_encode_hex (token_id, 32);
 
   msg = soup_message_new (SOUP_METHOD_POST, url);
   soup_message_set_request (msg, content_type, SOUP_MEMORY_STATIC,
@@ -820,7 +821,7 @@ ephy_sync_service_obtain_storage_credentials (EphySyncService *self)
   audience = get_audience (TOKEN_SERVER_URL);
   assertion = ephy_sync_crypto_create_assertion (self->certificate, audience,
                                                  300, self->rsa_key_pair);
-  key_b = ephy_sync_crypto_decode_hex (ephy_sync_service_get_secret (self, secrets[MASTER_KEY]));
+  key_b = ephy_sync_utils_decode_hex (ephy_sync_service_get_secret (self, secrets[MASTER_KEY]));
   hashed_key_b = g_compute_checksum_for_data (G_CHECKSUM_SHA256, key_b, 32);
   client_state = g_strndup (hashed_key_b, 32);
   authorization = g_strdup_printf ("BrowserID %s", assertion);
@@ -934,7 +935,7 @@ ephy_sync_service_obtain_signed_certificate (EphySyncService *self)
   session_token = ephy_sync_service_get_secret (self, secrets[SESSION_TOKEN]);
   ephy_sync_crypto_process_session_token (session_token, &token_id,
                                           &req_hmac_key, &request_key, 32);
-  token_id_hex = ephy_sync_crypto_encode_hex (token_id, 32);
+  token_id_hex = ephy_sync_utils_encode_hex (token_id, 32);
 
   n = mpz_get_str (NULL, 10, self->rsa_key_pair->public.n);
   e = mpz_get_str (NULL, 10, self->rsa_key_pair->public.e);
@@ -1050,7 +1051,6 @@ ephy_sync_service_delete_synchronizable (EphySyncService           *self,
   record = json_to_string (node, FALSE);
   bundle = ephy_sync_service_get_key_bundle (self, collection);
   payload = ephy_sync_crypto_encrypt_record (record,  bundle);
-  json_object_remove_member (object, "type");
   json_object_remove_member (object, "deleted");
   json_object_set_string_member (object, "payload", payload);
   body = json_to_string (node, FALSE);
@@ -1185,7 +1185,8 @@ upload_synchronizable_cb (SoupSession *session,
 static void
 ephy_sync_service_upload_synchronizable (EphySyncService           *self,
                                          EphySynchronizableManager *manager,
-                                         EphySynchronizable        *synchronizable)
+                                         EphySynchronizable        *synchronizable,
+                                         gboolean                   should_force)
 {
   SyncCryptoKeyBundle *bundle;
   SyncAsyncData *data;
@@ -1195,6 +1196,7 @@ ephy_sync_service_upload_synchronizable (EphySyncService           *self,
   char *id_safe;
   const char *collection;
   const char *id;
+  double time_modified;
 
   g_assert (EPHY_IS_SYNC_SERVICE (self));
   g_assert (EPHY_IS_SYNCHRONIZABLE_MANAGER (manager));
@@ -1213,8 +1215,9 @@ ephy_sync_service_upload_synchronizable (EphySyncService           *self,
   body = json_to_string (bso, FALSE);
 
   LOG ("Uploading object with id %s...", id);
-  ephy_sync_service_queue_storage_request (self, endpoint, SOUP_METHOD_PUT, body, -1,
-                                           ephy_synchronizable_get_server_time_modified (synchronizable),
+  time_modified = ephy_synchronizable_get_server_time_modified (synchronizable);
+  ephy_sync_service_queue_storage_request (self, endpoint, SOUP_METHOD_PUT, body,
+                                           -1, should_force ? -1 : time_modified,
                                            upload_synchronizable_cb, data);
 
   g_free (id_safe);
@@ -1225,13 +1228,15 @@ ephy_sync_service_upload_synchronizable (EphySyncService           *self,
 }
 
 static void
-merge_finished_cb (GSList   *to_upload,
-                   gpointer  user_data)
+merge_collection_finished_cb (GSList   *to_upload,
+                              gboolean  should_force,
+                              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);
+    ephy_sync_service_upload_synchronizable (data->service, data->manager,
+                                             l->data, should_force);
 
   if (data->is_last)
     g_signal_emit (data->service, signals[SYNC_FINISHED], 0);
@@ -1302,7 +1307,7 @@ sync_collection_cb (SoupSession *session,
 
   ephy_synchronizable_manager_merge (data->manager, data->is_initial,
                                      data->remotes_deleted, data->remotes_updated,
-                                     merge_finished_cb, data);
+                                     merge_collection_finished_cb, data);
   goto out_no_error;
 
 out_error:
@@ -1414,7 +1419,7 @@ ephy_sync_service_register_client_id (EphySyncService *self)
   protocol = g_strdup_printf ("1.%d", STORAGE_VERSION);
   json_array_add_string_element (array, protocol);
   json_object_set_array_member (payload, "protocols", array);
-  client_id = ephy_sync_crypto_get_random_sync_id ();
+  client_id = ephy_sync_utils_get_random_sync_id ();
   json_object_set_string_member (payload, "id", client_id);
   name = g_strdup_printf ("%s on Epiphany", client_id);
   json_object_set_string_member (payload, "name", name);
@@ -1803,7 +1808,7 @@ ephy_sync_service_upload_crypto_keys_record (EphySyncService *self)
   node = json_node_new (JSON_NODE_OBJECT);
   record = json_object_new ();
   payload_clear = ephy_sync_crypto_generate_crypto_keys (32);
-  master_key = ephy_sync_crypto_decode_hex (master_key_hex);
+  master_key = ephy_sync_utils_decode_hex (master_key_hex);
   bundle = ephy_sync_crypto_derive_key_bundle (master_key, 32);
   payload_cipher = ephy_sync_crypto_encrypt_record (payload_clear, bundle);
   json_object_set_string_member (record, "payload", payload_cipher);
@@ -1868,7 +1873,7 @@ obtain_crypto_keys_cb (SoupSession *session,
   /* Derive the Sync Key bundle from kB. The bundle consists of two 32 bytes keys:
    * the first one used as a symmetric encryption key (AES) and the second one
    * used as a HMAC key. */
-  key_b = ephy_sync_crypto_decode_hex (ephy_sync_service_get_secret (self, secrets[MASTER_KEY]));
+  key_b = ephy_sync_utils_decode_hex (ephy_sync_service_get_secret (self, secrets[MASTER_KEY]));
   bundle = ephy_sync_crypto_derive_key_bundle (key_b, 32);
   crypto_keys = ephy_sync_crypto_decrypt_record (payload, bundle);
   if (!crypto_keys) {
@@ -1911,7 +1916,7 @@ make_engine_object (int version)
   char *sync_id;
 
   object = json_object_new ();
-  sync_id = ephy_sync_crypto_get_random_sync_id ();
+  sync_id = ephy_sync_utils_get_random_sync_id ();
   json_object_set_int_member (object, "version", version);
   json_object_set_string_member (object, "syncID", sync_id);
 
@@ -1950,7 +1955,7 @@ ephy_sync_service_upload_meta_global_record (EphySyncService *self)
   json_object_set_object_member (engines, "forms", make_engine_object (1));
   json_object_set_object_member (payload, "engines", engines);
   json_object_set_int_member (payload, "storageVersion", STORAGE_VERSION);
-  sync_id = ephy_sync_crypto_get_random_sync_id ();
+  sync_id = ephy_sync_utils_get_random_sync_id ();
   json_object_set_string_member (payload, "syncID", sync_id);
   json_node_set_object (node, payload);
   payload_str = json_to_string (node, FALSE);
@@ -2075,7 +2080,7 @@ ephy_sync_service_conclude_sign_in (EphySyncService *self,
   g_assert (bundle);
 
   /* Derive the master sync keys form the key bundle. */
-  unwrap_key_b = ephy_sync_crypto_decode_hex (data->unwrap_b_key);
+  unwrap_key_b = ephy_sync_utils_decode_hex (data->unwrap_b_key);
   if (!ephy_sync_crypto_compute_sync_keys (bundle, data->resp_hmac_key,
                                            data->resp_xor_key, unwrap_key_b,
                                            &key_a, &key_b, 32)) {
@@ -2088,7 +2093,7 @@ ephy_sync_service_conclude_sign_in (EphySyncService *self,
   self->account = g_strdup (data->email);
   ephy_sync_service_set_secret (self, secrets[UID], data->uid);
   ephy_sync_service_set_secret (self, secrets[SESSION_TOKEN], data->session_token);
-  key_b_hex = ephy_sync_crypto_encode_hex (key_b, 32);
+  key_b_hex = ephy_sync_utils_encode_hex (key_b, 32);
   ephy_sync_service_set_secret (self, secrets[MASTER_KEY], key_b_hex);
 
   ephy_sync_service_check_storage_version (self);
@@ -2190,7 +2195,7 @@ ephy_sync_service_do_sign_in (EphySyncService *self,
    * See https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol#fetching-sync-keys */
   ephy_sync_crypto_process_key_fetch_token (key_fetch_token, &token_id, &req_hmac_key,
                                             &resp_hmac_key, &resp_xor_key, 32);
-  token_id_hex = ephy_sync_crypto_encode_hex (token_id, 32);
+  token_id_hex = ephy_sync_utils_encode_hex (token_id, 32);
 
   /* Get the master sync key bundle from the /account/keys endpoint. */
   data = sign_in_async_data_new (self, email, uid,
@@ -2226,6 +2231,7 @@ synchronizable_deleted_cb (EphySynchronizableManager *manager,
 static void
 synchronizable_modified_cb (EphySynchronizableManager *manager,
                             EphySynchronizable        *synchronizable,
+                            gboolean                   should_force,
                             EphySyncService           *self)
 {
   g_assert (EPHY_IS_SYNCHRONIZABLE_MANAGER (manager));
@@ -2235,7 +2241,7 @@ synchronizable_modified_cb (EphySynchronizableManager *manager,
   if (!ephy_sync_service_is_signed_in (self))
     return;
 
-  ephy_sync_service_upload_synchronizable (self, manager, synchronizable);
+  ephy_sync_service_upload_synchronizable (self, manager, synchronizable, should_force);
 }
 
 void
@@ -2290,12 +2296,12 @@ ephy_sync_service_do_sign_out (EphySyncService *self)
     g_signal_handlers_disconnect_by_func (l->data, synchronizable_deleted_cb, self);
     g_signal_handlers_disconnect_by_func (l->data, synchronizable_modified_cb, self);
   }
-  g_slist_free (self->managers);
-  self->managers = NULL;
+  g_clear_pointer (&self->managers, g_slist_free);
 
   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);
+  g_settings_set_boolean (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_HISTORY_INITIAL, TRUE);
 }
 
 void
diff --git a/lib/sync/ephy-synchronizable-manager.c b/lib/sync/ephy-synchronizable-manager.c
index 60212b7..5e2c70e 100644
--- a/lib/sync/ephy-synchronizable-manager.c
+++ b/lib/sync/ephy-synchronizable-manager.c
@@ -58,8 +58,9 @@ ephy_synchronizable_manager_default_init (EphySynchronizableManagerInterface *if
                   EPHY_TYPE_SYNCHRONIZABLE_MANAGER,
                   G_SIGNAL_RUN_LAST,
                   0, NULL, NULL, NULL,
-                  G_TYPE_NONE, 1,
-                  EPHY_TYPE_SYNCHRONIZABLE);
+                  G_TYPE_NONE, 2,
+                  EPHY_TYPE_SYNCHRONIZABLE,
+                  G_TYPE_BOOLEAN);
 }
 
 /**
diff --git a/lib/sync/ephy-synchronizable-manager.h b/lib/sync/ephy-synchronizable-manager.h
index 8c36332..046c340 100644
--- a/lib/sync/ephy-synchronizable-manager.h
+++ b/lib/sync/ephy-synchronizable-manager.h
@@ -30,7 +30,7 @@ G_BEGIN_DECLS
 
 G_DECLARE_INTERFACE (EphySynchronizableManager, ephy_synchronizable_manager, EPHY, SYNCHRONIZABLE_MANAGER, 
GObject)
 
-typedef void (*EphySynchronizableManagerMergeCallback) (GSList *to_upload, gpointer user_data);
+typedef void (*EphySynchronizableManagerMergeCallback) (GSList *to_upload, gboolean should_force, gpointer 
user_data);
 
 struct _EphySynchronizableManagerInterface {
   GTypeInterface parent_iface;
diff --git a/lib/sync/meson.build b/lib/sync/meson.build
index 658c17c..b728c39 100644
--- a/lib/sync/meson.build
+++ b/lib/sync/meson.build
@@ -1,4 +1,6 @@
 libephysync_sources = [
+  'ephy-history-manager.c',
+  'ephy-history-record.c',
   'ephy-password-manager.c',
   'ephy-password-record.c',
   'ephy-sync-crypto.c',
diff --git a/src/bookmarks/ephy-add-bookmark-popover.c b/src/bookmarks/ephy-add-bookmark-popover.c
index 76019ee..2bef611 100644
--- a/src/bookmarks/ephy-add-bookmark-popover.c
+++ b/src/bookmarks/ephy-add-bookmark-popover.c
@@ -27,6 +27,7 @@
 #include "ephy-embed-container.h"
 #include "ephy-location-entry.h"
 #include "ephy-shell.h"
+#include "ephy-sync-utils.h"
 
 struct _EphyAddBookmarkPopover {
   GtkPopover     parent_instance;
@@ -207,7 +208,7 @@ ephy_add_bookmark_popover_show (EphyAddBookmarkPopover *self)
 
   bookmark = ephy_bookmarks_manager_get_bookmark_by_url (manager, address);
   if (!bookmark) {
-    char *id = ephy_sync_crypto_get_random_sync_id ();
+    char *id = ephy_sync_utils_get_random_sync_id ();
     bookmark = ephy_bookmark_new (address,
                                   ephy_embed_get_title (embed),
                                   g_sequence_new (g_free),
diff --git a/src/bookmarks/ephy-bookmark-properties-grid.c b/src/bookmarks/ephy-bookmark-properties-grid.c
index 2a4d9ce..eedb1f1 100644
--- a/src/bookmarks/ephy-bookmark-properties-grid.c
+++ b/src/bookmarks/ephy-bookmark-properties-grid.c
@@ -419,7 +419,7 @@ ephy_bookmark_properties_grid_finalize (GObject *object)
   EphyBookmarkPropertiesGrid *self = EPHY_BOOKMARK_PROPERTIES_GRID (object);
 
   if (self->bookmark_is_modified && !self->bookmark_is_removed)
-    g_signal_emit_by_name (self->manager, "synchronizable-modified", self->bookmark);
+    g_signal_emit_by_name (self->manager, "synchronizable-modified", self->bookmark, FALSE);
 
   ephy_bookmarks_manager_save_to_file_async (self->manager, NULL,
                                              ephy_bookmarks_manager_save_to_file_warn_on_error_cb,
diff --git a/src/bookmarks/ephy-bookmarks-manager.c b/src/bookmarks/ephy-bookmarks-manager.c
index 5eb20b3..cabd623 100644
--- a/src/bookmarks/ephy-bookmarks-manager.c
+++ b/src/bookmarks/ephy-bookmarks-manager.c
@@ -26,6 +26,7 @@
 #include "ephy-debug.h"
 #include "ephy-file-helpers.h"
 #include "ephy-settings.h"
+#include "ephy-sync-utils.h"
 #include "ephy-synchronizable-manager.h"
 
 #include <string.h>
@@ -341,7 +342,7 @@ ephy_bookmarks_manager_add_bookmark (EphyBookmarksManager *self,
   g_return_if_fail (EPHY_IS_BOOKMARK (bookmark));
 
   ephy_bookmarks_manager_add_bookmark_internal (self, bookmark, TRUE);
-  g_signal_emit_by_name (self, "synchronizable-modified", bookmark);
+  g_signal_emit_by_name (self, "synchronizable-modified", bookmark, FALSE);
 }
 
 void
@@ -358,7 +359,7 @@ ephy_bookmarks_manager_add_bookmarks (EphyBookmarksManager *self,
     EphyBookmark *bookmark = g_sequence_get (iter);
 
     ephy_bookmarks_manager_add_bookmark_internal (self, bookmark, FALSE);
-    g_signal_emit_by_name (self, "synchronizable-modified", bookmark);
+    g_signal_emit_by_name (self, "synchronizable-modified", bookmark, FALSE);
   }
 
   ephy_bookmarks_manager_save_to_file_async (self, NULL,
@@ -781,7 +782,7 @@ ephy_bookmarks_manager_handle_initial_merge (EphyBookmarksManager *self,
         ephy_synchronizable_set_server_time_modified (EPHY_SYNCHRONIZABLE (bookmark), timestamp);
       } else {
         /* Same id, different url. Keep both and upload local one with new id. */
-        char *new_id = ephy_sync_crypto_get_random_sync_id ();
+        char *new_id = ephy_sync_utils_get_random_sync_id ();
         ephy_bookmark_set_id (bookmark, new_id);
         ephy_bookmarks_manager_add_bookmark_internal (self, l->data, FALSE);
         g_hash_table_add (dont_upload, (char *)id);
@@ -918,7 +919,7 @@ synchronizable_manager_merge (EphySynchronizableManager              *manager,
                                                              remotes_updated,
                                                              remotes_deleted);
 
-  callback (to_upload, user_data);
+  callback (to_upload, FALSE, user_data);
 }
 
 static void
diff --git a/src/ephy-history-dialog.c b/src/ephy-history-dialog.c
index 38f029b..e103f57 100644
--- a/src/ephy-history-dialog.c
+++ b/src/ephy-history-dialog.c
@@ -86,7 +86,8 @@ static GParamSpec *obj_properties[LAST_PROP];
 typedef enum {
   COLUMN_DATE,
   COLUMN_NAME,
-  COLUMN_LOCATION
+  COLUMN_LOCATION,
+  COLUMN_SYNC_ID
 } EphyHistoryDialogColumns;
 
 static gboolean
@@ -112,6 +113,7 @@ add_urls_source (EphyHistoryDialog *self)
                                        COLUMN_DATE, url->last_visit_time,
                                        COLUMN_NAME, url->title,
                                        COLUMN_LOCATION, url->url,
+                                       COLUMN_SYNC_ID, url->sync_id,
                                        -1);
     self->urls = g_list_remove_link (self->urls, element);
     ephy_history_url_free (url);
@@ -318,6 +320,7 @@ get_url_from_path (GtkTreeModel *model,
   gtk_tree_model_get (model, &iter,
                       COLUMN_NAME, &url->title,
                       COLUMN_LOCATION, &url->url,
+                      COLUMN_SYNC_ID, &url->sync_id,
                       -1);
   return url;
 }
diff --git a/src/ephy-shell.c b/src/ephy-shell.c
index 0310705..894a017 100644
--- a/src/ephy-shell.c
+++ b/src/ephy-shell.c
@@ -57,6 +57,7 @@ struct _EphyShell {
   GObject *lockdown;
   EphyBookmarksManager *bookmarks_manager;
   EphyPasswordManager *password_manager;
+  EphyHistoryManager *history_manager;
   GNetworkMonitor *network_monitor;
   GtkWidget *history_dialog;
   GObject *prefs_dialog;
@@ -354,6 +355,9 @@ ephy_shell_startup (GApplication *application)
     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)));
+    if (g_settings_get_boolean (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_HISTORY_ENABLED))
+      ephy_sync_service_register_manager (ephy_shell->sync_service,
+                                          EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_history_manager 
(ephy_shell)));
 
     gtk_application_set_app_menu (GTK_APPLICATION (application),
                                   G_MENU_MODEL (gtk_builder_get_object (builder, "app-menu")));
@@ -624,6 +628,7 @@ ephy_shell_dispose (GObject *object)
   g_clear_object (&shell->sync_service);
   g_clear_object (&shell->bookmarks_manager);
   g_clear_object (&shell->password_manager);
+  g_clear_object (&shell->history_manager);
 
   g_slist_free_full (shell->open_uris_idle_ids, remove_open_uris_idle_cb);
   shell->open_uris_idle_ids = NULL;
@@ -830,6 +835,31 @@ ephy_shell_get_password_manager (EphyShell *shell)
 }
 
 /**
+ * ephy_shell_get_history_manager:
+ * @shell: the #EphyShell
+ *
+ * Returns the history manager.
+ *
+ * Return value: (transfer none): An #EphyHistoryManager.
+ */
+EphyHistoryManager *
+ephy_shell_get_history_manager (EphyShell *shell)
+{
+  EphyEmbedShell *embed_shell;
+  EphyHistoryService *service;
+
+  g_return_val_if_fail (EPHY_IS_SHELL (shell), NULL);
+
+  if (shell->history_manager == NULL) {
+    embed_shell = ephy_embed_shell_get_default ();
+    service = ephy_embed_shell_get_global_history_service (embed_shell);
+    shell->history_manager = ephy_history_manager_new (service);
+  }
+
+  return shell->history_manager;
+}
+
+/**
  * ephy_shell_get_net_monitor:
  *
  * Return value: (transfer none):
diff --git a/src/ephy-shell.h b/src/ephy-shell.h
index 6734d82..b7d4800 100644
--- a/src/ephy-shell.h
+++ b/src/ephy-shell.h
@@ -25,6 +25,7 @@
 #include "ephy-bookmarks-manager.h"
 #include "ephy-embed-shell.h"
 #include "ephy-embed.h"
+#include "ephy-history-manager.h"
 #include "ephy-password-manager.h"
 #include "ephy-session.h"
 #include "ephy-sync-service.h"
@@ -105,6 +106,8 @@ EphyBookmarksManager *ephy_shell_get_bookmarks_manager   (EphyShell *shell);
 
 EphyPasswordManager *ephy_shell_get_password_manager     (EphyShell *shell);
 
+EphyHistoryManager *ephy_shell_get_history_manager       (EphyShell *shell);
+
 EphySyncService *ephy_shell_get_sync_service             (EphyShell *shell);
 
 GtkWidget       *ephy_shell_get_history_dialog           (EphyShell *shell);
diff --git a/src/prefs-dialog.c b/src/prefs-dialog.c
index 7c2c91a..f0cc739 100644
--- a/src/prefs-dialog.c
+++ b/src/prefs-dialog.c
@@ -122,6 +122,7 @@ struct _PrefsDialog {
   GtkWidget *sync_with_firefox_checkbutton;
   GtkWidget *sync_bookmarks_checkbutton;
   GtkWidget *sync_passwords_checkbutton;
+  GtkWidget *sync_history_checkbutton;
   GtkWidget *sync_frequency_5_min_radiobutton;
   GtkWidget *sync_frequency_15_min_radiobutton;
   GtkWidget *sync_frequency_30_min_radiobutton;
@@ -181,6 +182,8 @@ sync_collection_toggled_cb (GtkToggleButton *button,
     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 ()));
+  else if (GTK_WIDGET (button) == dialog->sync_history_checkbutton)
+    manager = EPHY_SYNCHRONIZABLE_MANAGER (ephy_shell_get_history_manager (ephy_shell_get_default ()));
 
   if (gtk_toggle_button_get_active (button))
     ephy_sync_service_register_manager (dialog->sync_service, manager);
@@ -233,6 +236,7 @@ sync_secrets_store_finished_cb (EphySyncService *service,
 {
   EphyBookmarksManager *bookmarks_manager;
   EphyPasswordManager *password_manager;
+  EphyHistoryManager *history_manager;
 
   g_assert (EPHY_IS_SYNC_SERVICE (service));
   g_assert (EPHY_IS_PREFS_DIALOG (dialog));
@@ -267,6 +271,10 @@ sync_secrets_store_finished_cb (EphySyncService *service,
       password_manager = ephy_shell_get_password_manager (ephy_shell_get_default ());
       ephy_sync_service_register_manager (service, EPHY_SYNCHRONIZABLE_MANAGER (password_manager));
     }
+    if (g_settings_get_boolean (EPHY_SETTINGS_SYNC, EPHY_PREFS_SYNC_HISTORY_ENABLED)) {
+      history_manager = ephy_shell_get_history_manager (ephy_shell_get_default ());
+      ephy_sync_service_register_manager (service, EPHY_SYNCHRONIZABLE_MANAGER (history_manager));
+    }
 
     g_free (text);
     g_free (user);
@@ -621,6 +629,7 @@ prefs_dialog_class_init (PrefsDialogClass *klass)
   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_history_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);
@@ -1680,6 +1689,9 @@ setup_sync_page (PrefsDialog *dialog)
   g_signal_connect_object (dialog->sync_passwords_checkbutton, "toggled",
                            G_CALLBACK (sync_collection_toggled_cb),
                            dialog, 0);
+  g_signal_connect_object (dialog->sync_history_checkbutton, "toggled",
+                           G_CALLBACK (sync_collection_toggled_cb),
+                           dialog, 0);
 
   g_settings_bind (sync_settings,
                    EPHY_PREFS_SYNC_WITH_FIREFOX,
@@ -1696,6 +1708,11 @@ setup_sync_page (PrefsDialog *dialog)
                    dialog->sync_passwords_checkbutton,
                    "active",
                    G_SETTINGS_BIND_DEFAULT);
+  g_settings_bind (sync_settings,
+                   EPHY_PREFS_SYNC_HISTORY_ENABLED,
+                   dialog->sync_history_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 effd7cc..d0591eb 100644
--- a/src/profile-migrator/ephy-profile-migrator.c
+++ b/src/profile-migrator/ephy-profile-migrator.c
@@ -41,7 +41,7 @@
 #include "ephy-search-engine-manager.h"
 #include "ephy-settings.h"
 #include "ephy-sqlite-connection.h"
-#include "ephy-sync-crypto.h"
+#include "ephy-sync-utils.h"
 #include "ephy-uri-tester-shared.h"
 #include "ephy-web-app-utils.h"
 
@@ -648,7 +648,7 @@ parse_rdf_item (EphyBookmarksManager *manager,
     char *id;
 
     g_sequence_sort (tags, (GCompareDataFunc)ephy_bookmark_tags_compare, NULL);
-    id = ephy_sync_crypto_get_random_sync_id ();
+    id = ephy_sync_utils_get_random_sync_id ();
     bookmark = ephy_bookmark_new ((const char *)link, (const char *)title, tags, id);
     ephy_bookmarks_manager_add_bookmark (manager, bookmark);
 
@@ -1102,7 +1102,7 @@ migrate_history_to_firefox_sync_history (void)
 
   /* Set sync_id for each row. */
   for (GSList *l = ids; l && l->data; l = l->next) {
-    char *sync_id = ephy_sync_crypto_get_random_sync_id ();
+    char *sync_id = ephy_sync_utils_get_random_sync_id ();
     char *sql = g_strdup_printf ("UPDATE urls SET sync_id = \"%s\" WHERE id=%d",
                                  sync_id, GPOINTER_TO_INT (l->data));
 
diff --git a/src/resources/gtk/history-dialog.ui b/src/resources/gtk/history-dialog.ui
index b8a4b15..d86534d 100644
--- a/src/resources/gtk/history-dialog.ui
+++ b/src/resources/gtk/history-dialog.ui
@@ -9,6 +9,8 @@
       <column type="gchararray"/>
       <!-- column-name LOCATION -->
       <column type="gchararray"/>
+      <!-- column-name SYNC_ID -->
+      <column type="gchararray"/>
     </columns>
   </object>
   <menu id="treeview_popup_menu_model">
diff --git a/src/resources/gtk/prefs-dialog.ui b/src/resources/gtk/prefs-dialog.ui
index a1f619e..db6911f 100644
--- a/src/resources/gtk/prefs-dialog.ui
+++ b/src/resources/gtk/prefs-dialog.ui
@@ -922,6 +922,13 @@
                                 <property name="use-underline">True</property>
                               </object>
                             </child>
+                            <child>
+                              <object class="GtkCheckButton" id="sync_history_checkbutton">
+                                <property name="label" translatable="yes">_History</property>
+                                <property name="visible">True</property>
+                                <property name="use-underline">True</property>
+                              </object>
+                            </child>
                           </object>
                         </child>
                         <child>
diff --git a/tests/ephy-web-view-test.c b/tests/ephy-web-view-test.c
index b6b53aa..f94ddfc 100644
--- a/tests/ephy-web-view-test.c
+++ b/tests/ephy-web-view-test.c
@@ -406,10 +406,9 @@ test_ephy_web_view_provisional_load_failure_updates_back_forward_list (void)
 }
 
 static void
-visit_url_cb (EphyHistoryService  *service,
-              const char          *url,
-              EphyHistoryPageVisit visit_type,
-              gpointer             user_data)
+visit_url_cb (EphyHistoryService *service,
+              EphyHistoryURL     *url,
+              gpointer            user_data)
 {
   /* We are only loading an error page, this code should never be
    * reached. */


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