[evolution-data-server] Merge offline-cache changes into master
- From: Milan Crha <mcrha src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [evolution-data-server] Merge offline-cache changes into master
- Date: Wed, 17 May 2017 13:36:34 +0000 (UTC)
commit 3abbcce2eadfef5a9de866b0aef43b8945b419b1
Author: Milan Crha <mcrha redhat com>
Date: Wed May 17 15:32:39 2017 +0200
Merge offline-cache changes into master
.../evolution-data-server-docs.sgml.in | 13 +
po/POTFILES.in | 8 +
.../backends/google/e-book-backend-google.c | 2398 ++------
.../backends/google/e-book-backend-google.h | 4 +-
.../backends/google/e-book-google-utils.c | 52 +-
.../backends/google/e-book-google-utils.h | 35 +-
.../backends/google/tests/phone-numbers.c | 9 +-
.../backends/webdav/e-book-backend-webdav.c | 2409 +++------
.../backends/webdav/e-book-backend-webdav.h | 23 +-
src/addressbook/libebook-contacts/e-vcard.c | 74 +
src/addressbook/libebook-contacts/e-vcard.h | 6 +
src/addressbook/libedata-book/CMakeLists.txt | 8 +-
src/addressbook/libedata-book/e-book-backend.c | 99 +-
src/addressbook/libedata-book/e-book-backend.h | 26 +-
src/addressbook/libedata-book/e-book-cache.c | 6113 +++++++++++++++++++
src/addressbook/libedata-book/e-book-cache.h | 323 +
.../libedata-book/e-book-meta-backend.c | 3767 ++++++++++++
.../libedata-book/e-book-meta-backend.h | 276 +
.../libedata-book/e-data-book-cursor-cache.c | 438 ++
.../libedata-book/e-data-book-cursor-cache.h | 81 +
src/addressbook/libedata-book/libedata-book.h | 3 +
.../backends/caldav/e-cal-backend-caldav.c | 6198 ++++----------------
.../backends/caldav/e-cal-backend-caldav.h | 4 +-
.../backends/gtasks/e-cal-backend-gtasks.c | 1655 ++----
.../backends/gtasks/e-cal-backend-gtasks.h | 4 +-
src/calendar/backends/http/e-cal-backend-http.c | 1715 ++-----
src/calendar/backends/http/e-cal-backend-http.h | 4 +-
src/calendar/libecal/e-cal-util.c | 204 +-
src/calendar/libecal/e-cal-util.h | 12 +
src/calendar/libedata-cal/CMakeLists.txt | 6 +-
src/calendar/libedata-cal/e-cal-backend-sexp.c | 85 +-
src/calendar/libedata-cal/e-cal-backend.c | 98 +-
src/calendar/libedata-cal/e-cal-backend.h | 26 +-
src/calendar/libedata-cal/e-cal-cache.c | 3651 ++++++++++++
src/calendar/libedata-cal/e-cal-cache.h | 335 ++
src/calendar/libedata-cal/e-cal-meta-backend.c | 4570 +++++++++++++++
src/calendar/libedata-cal/e-cal-meta-backend.h | 282 +
src/calendar/libedata-cal/libedata-cal.h | 2 +
src/libebackend/CMakeLists.txt | 2 +
src/libebackend/e-backend-enums.h | 43 +
src/libebackend/e-backend.c | 14 +-
src/libebackend/e-cache.c | 3068 ++++++++++
src/libebackend/e-cache.h | 524 ++
src/libebackend/libebackend.h | 1 +
src/libedataserver/CMakeLists.txt | 10 +-
src/libedataserver/e-data-server-util.c | 90 +
src/libedataserver/e-data-server-util.h | 4 +
src/libedataserver/e-soup-session.c | 1019 ++++
src/libedataserver/e-soup-session.h | 120 +
.../e-source-credentials-provider-impl-google.c | 8 +-
src/libedataserver/e-source-enums.h | 3 +
src/libedataserver/e-webdav-discover.c | 1927 +------
src/libedataserver/e-webdav-session.c | 4983 ++++++++++++++++
src/libedataserver/e-webdav-session.h | 582 ++
src/libedataserver/e-xml-document.c | 727 +++
src/libedataserver/e-xml-document.h | 134 +
src/libedataserver/e-xml-utils.c | 265 +
src/libedataserver/e-xml-utils.h | 24 +-
src/libedataserver/libedataserver.h | 3 +
.../client/test-book-client-view-operations.c | 27 +-
.../libebook/client/test-book-client-write-write.c | 16 +-
tests/libebook/data/vcards/.gitattributes | 1 +
tests/libebook/data/vcards/custom-1.vcf | 1 +
tests/libebook/data/vcards/custom-2.vcf | 1 +
tests/libebook/data/vcards/custom-3.vcf | 1 +
tests/libebook/data/vcards/custom-4.vcf | 1 +
tests/libebook/data/vcards/custom-5.vcf | 1 +
tests/libebook/data/vcards/custom-6.vcf | 1 +
tests/libebook/data/vcards/custom-7.vcf | 1 +
tests/libebook/data/vcards/custom-8.vcf | 1 +
tests/libebook/data/vcards/custom-9.vcf | 1 +
tests/libebook/data/vcards/logo-1.vcf | 21 +
tests/libebook/data/vcards/photo-1.vcf | 59 +
tests/libebook/vcard/.gitattributes | 1 +
tests/libebook/vcard/11.vcf | 16 +-
tests/libebook/vcard/12.vcf | 46 +-
tests/libebook/vcard/3.vcf | 26 +-
tests/libebook/vcard/4.vcf | 22 +-
tests/libebook/vcard/5.vcf | 64 +-
tests/libedata-book/CMakeLists.txt | 30 +-
tests/libedata-book/data-test-utils.h | 4 +
.../libedata-book/test-book-cache-create-cursor.c | 125 +
.../test-book-cache-cursor-calculate.c | 695 +++
.../test-book-cache-cursor-change-locale.c | 102 +
.../test-book-cache-cursor-move-by-de-DE.c | 82 +
.../test-book-cache-cursor-move-by-en-US.c | 100 +
.../test-book-cache-cursor-move-by-fr-CA.c | 82 +
.../test-book-cache-cursor-move-by-posix.c | 82 +
.../test-book-cache-cursor-set-sexp.c | 155 +
.../test-book-cache-cursor-set-target.c | 225 +
tests/libedata-book/test-book-cache-get-contact.c | 78 +
tests/libedata-book/test-book-cache-offline.c | 1138 ++++
tests/libedata-book/test-book-cache-utils.c | 695 +++
tests/libedata-book/test-book-cache-utils.h | 178 +
tests/libedata-book/test-book-meta-backend.c | 1718 ++++++
tests/libedata-book/test-sqlite-create-cursor.c | 8 +-
tests/libedata-cal/CMakeLists.txt | 71 +-
tests/libedata-cal/components/.gitattributes | 1 +
tests/libedata-cal/components/event-1.ics | 18 +
tests/libedata-cal/components/event-2.ics | 14 +
tests/libedata-cal/components/event-3.ics | 12 +
tests/libedata-cal/components/event-4.ics | 12 +
tests/libedata-cal/components/event-5.ics | 18 +
tests/libedata-cal/components/event-6-a.ics | 13 +
tests/libedata-cal/components/event-6.ics | 12 +
tests/libedata-cal/components/event-7.ics | 14 +
tests/libedata-cal/components/event-8.ics | 16 +
tests/libedata-cal/components/event-9.ics | 17 +
tests/libedata-cal/components/invite-1.ics | 19 +
tests/libedata-cal/components/invite-2.ics | 19 +
tests/libedata-cal/components/invite-3.ics | 21 +
tests/libedata-cal/components/invite-4.ics | 21 +
tests/libedata-cal/components/task-1.ics | 9 +
tests/libedata-cal/components/task-2.ics | 11 +
tests/libedata-cal/components/task-3.ics | 13 +
tests/libedata-cal/components/task-4.ics | 13 +
tests/libedata-cal/components/task-5.ics | 13 +
tests/libedata-cal/components/task-6.ics | 14 +
tests/libedata-cal/components/task-7.ics | 17 +
tests/libedata-cal/components/task-8.ics | 11 +
tests/libedata-cal/components/task-9.ics | 11 +
tests/libedata-cal/test-cal-cache-getters.c | 247 +
tests/libedata-cal/test-cal-cache-intervals.c | 344 ++
tests/libedata-cal/test-cal-cache-offline.c | 1043 ++++
tests/libedata-cal/test-cal-cache-search.c | 473 ++
tests/libedata-cal/test-cal-cache-utils.c | 180 +
tests/libedata-cal/test-cal-cache-utils.h | 52 +
tests/libedata-cal/test-cal-meta-backend.c | 2723 +++++++++
128 files changed, 47015 insertions(+), 12829 deletions(-)
---
diff --git a/docs/reference/evolution-data-server/evolution-data-server-docs.sgml.in
b/docs/reference/evolution-data-server/evolution-data-server-docs.sgml.in
index 3dfff75..0b981ba 100644
--- a/docs/reference/evolution-data-server/evolution-data-server-docs.sgml.in
+++ b/docs/reference/evolution-data-server/evolution-data-server-docs.sgml.in
@@ -157,6 +157,7 @@
<xi:include href="xml/e-backend.xml"/>
<xi:include href="xml/e-backend-enums.xml"/>
<xi:include href="xml/e-backend-factory.xml"/>
+ <xi:include href="xml/e-cache.xml"/>
<xi:include href="xml/e-data-factory.xml"/>
<xi:include href="xml/e-dbus-server.xml"/>
<xi:include href="xml/e-extensible.xml"/>
@@ -180,10 +181,13 @@
<xi:include href="xml/e-book-backend.xml"/>
<xi:include href="xml/e-book-backend-factory.xml"/>
<xi:include href="xml/e-book-backend-sexp.xml"/>
+ <xi:include href="xml/e-book-cache.xml"/>
+ <xi:include href="xml/e-book-meta-backend.xml"/>
<xi:include href="xml/e-book-sqlite.xml"/>
<xi:include href="xml/e-data-book.xml"/>
<xi:include href="xml/e-data-book-direct.xml"/>
<xi:include href="xml/e-data-book-cursor.xml"/>
+ <xi:include href="xml/e-data-book-cursor-cache.xml"/>
<xi:include href="xml/e-data-book-cursor-sqlite.xml"/>
<xi:include href="xml/e-data-book-factory.xml"/>
<xi:include href="xml/e-data-book-view.xml"/>
@@ -199,6 +203,8 @@
<xi:include href="xml/e-cal-backend-store.xml"/>
<xi:include href="xml/e-cal-backend-sync.xml"/>
<xi:include href="xml/e-cal-backend-intervaltree.xml"/>
+ <xi:include href="xml/e-cal-cache.xml"/>
+ <xi:include href="xml/e-cal-meta-backend.xml"/>
<xi:include href="xml/e-data-cal.xml"/>
<xi:include href="xml/e-data-cal-factory.xml"/>
<xi:include href="xml/e-data-cal-view.xml"/>
@@ -226,10 +232,13 @@
<xi:include href="xml/e-operation-pool.xml"/>
<xi:include href="xml/e-secret-store.xml"/>
<xi:include href="xml/e-sexp.xml"/>
+ <xi:include href="xml/e-soup-session.xml"/>
<xi:include href="xml/e-soup-ssl-trust.xml"/>
<xi:include href="xml/e-time-utils.xml"/>
<xi:include href="xml/e-uid.xml"/>
<xi:include href="xml/e-webdav-discover.xml"/>
+ <xi:include href="xml/e-webdav-session.xml"/>
+ <xi:include href="xml/e-xml-document.xml"/>
<xi:include href="xml/e-xml-hash-utils.xml"/>
<xi:include href="xml/e-xml-utils.xml"/>
<xi:include href="xml/eds-version.xml"/>
@@ -324,6 +333,10 @@
<title>Index of deprecated symbols</title>
<xi:include href="xml/api-index-deprecated.xml"><xi:fallback /></xi:include>
</index>
+ <index id="api-index-3.26" role="3.26">
+ <title>Index of new symbols in 3.26</title>
+ <xi:include href="xml/api-index-3.26.xml"><xi:fallback /></xi:include>
+ </index>
<index id="api-index-3.24" role="3.24">
<title>Index of new symbols in 3.24</title>
<xi:include href="xml/api-index-3.24.xml"><xi:fallback /></xi:include>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 4cdf92c..2a2d003 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -18,9 +18,12 @@ src/addressbook/libebook/e-destination.c
src/addressbook/libedata-book/e-book-backend.c
src/addressbook/libedata-book/e-book-backend-sexp.c
src/addressbook/libedata-book/e-book-backend-sqlitedb.c
+src/addressbook/libedata-book/e-book-cache.c
+src/addressbook/libedata-book/e-book-meta-backend.c
src/addressbook/libedata-book/e-book-sqlite.c
src/addressbook/libedata-book/e-data-book.c
src/addressbook/libedata-book/e-data-book-cursor.c
+src/addressbook/libedata-book/e-data-book-cursor-cache.c
src/addressbook/libedata-book/e-data-book-cursor-sqlite.c
src/addressbook/libedata-book/e-data-book-factory.c
src/calendar/backends/caldav/e-cal-backend-caldav.c
@@ -38,6 +41,8 @@ src/calendar/libedata-cal/e-cal-backend.c
src/calendar/libedata-cal/e-cal-backend-sexp.c
src/calendar/libedata-cal/e-cal-backend-sync.c
src/calendar/libedata-cal/e-cal-backend-util.c
+src/calendar/libedata-cal/e-cal-cache.c
+src/calendar/libedata-cal/e-cal-meta-backend.c
src/calendar/libedata-cal/e-data-cal.c
src/calendar/libedata-cal/e-data-cal-factory.c
src/camel/camel-address.c
@@ -176,6 +181,7 @@ data/org.gnome.evolution-data-server.calendar.gschema.xml.in
data/org.gnome.evolution-data-server.gschema.xml.in
data/org.gnome.evolution.shell.network-config.gschema.xml.in
src/libebackend/e-backend.c
+src/libebackend/e-cache.c
src/libebackend/e-collection-backend.c
src/libebackend/e-data-factory.c
src/libebackend/e-server-side-source.c
@@ -184,6 +190,7 @@ src/libebackend/e-subprocess-factory.c
src/libebackend/e-user-prompter-server.c
src/libedataserver/e-categories.c
src/libedataserver/e-client.c
+src/libedataserver/e-soup-session.c
src/libedataserver/e-source.c
src/libedataserver/e-source-credentials-provider-impl.c
src/libedataserver/e-source-credentials-provider-impl-google.c
@@ -194,6 +201,7 @@ src/libedataserver/e-source-registry.c
src/libedataserver/e-source-webdav.c
src/libedataserver/e-time-utils.c
src/libedataserver/e-webdav-discover.c
+src/libedataserver/e-webdav-session.c
src/libedataserverui/e-credentials-prompter.c
src/libedataserverui/e-credentials-prompter-impl-google.c
src/libedataserverui/e-credentials-prompter-impl-password.c
diff --git a/src/addressbook/backends/google/e-book-backend-google.c
b/src/addressbook/backends/google/e-book-backend-google.c
index 1284c0e..92e9ca6 100644
--- a/src/addressbook/backends/google/e-book-backend-google.c
+++ b/src/addressbook/backends/google/e-book-backend-google.c
@@ -2,6 +2,7 @@
*
* Copyright (C) 2008 Joergen Scheibengruber
* Copyright (C) 2010, 2011 Philip Withnall
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
*
* This library is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
@@ -31,26 +32,11 @@
#include "e-book-google-utils.h"
#include "e-gdata-oauth2-authorizer.h"
-#define E_BOOK_BACKEND_GOOGLE_GET_PRIVATE(obj) \
- (G_TYPE_INSTANCE_GET_PRIVATE \
- ((obj), E_TYPE_BOOK_BACKEND_GOOGLE, EBookBackendGooglePrivate))
-
-#define CLIENT_ID "evolution-client-0.1.0"
-
#define URI_GET_CONTACTS "https://www.google.com/m8/feeds/contacts/default/full"
-/* This macro was introduced in libgdata 0.11,
- * but we currently only require libgdata 0.10. */
-#ifndef GDATA_CHECK_VERSION
-#define GDATA_CHECK_VERSION(major,minor,micro) 0
-#endif
-
-G_DEFINE_TYPE (EBookBackendGoogle, e_book_backend_google, E_TYPE_BOOK_BACKEND)
+G_DEFINE_TYPE (EBookBackendGoogle, e_book_backend_google, E_TYPE_BOOK_META_BACKEND)
struct _EBookBackendGooglePrivate {
- EBookBackendCache *cache;
- GMutex cache_lock;
-
/* For all the group-related members */
GRecMutex groups_lock;
/* Mapping from group ID to (human readable) group name */
@@ -63,27 +49,25 @@ struct _EBookBackendGooglePrivate {
GHashTable *system_groups_by_entry_id;
/* Time when the groups were last queried */
GTimeVal groups_last_update;
+ /* Did the server-side groups change? If so, re-download the book */
+ gboolean groups_changed;
GDataAuthorizer *authorizer;
GDataService *service;
-
- guint refresh_id;
-
- /* Map of active opids to GCancellables */
- GHashTable *cancellables;
-
- /* Did the server-side groups change? If so, re-download the book */
- gboolean groups_changed;
+ GHashTable *preloaded; /* gchar *uid ~> EContact * */
};
static void
-data_book_error_from_gdata_error (GError **error,
- const GError *gdata_error)
+ebb_google_data_book_error_from_gdata_error (GError **error,
+ const GError *gdata_error)
{
gboolean use_fallback = FALSE;
g_return_if_fail (gdata_error != NULL);
+ if (!error)
+ return;
+
/* Authentication errors */
if (gdata_error->domain == GDATA_SERVICE_ERROR) {
switch (gdata_error->code) {
@@ -158,214 +142,62 @@ data_book_error_from_gdata_error (GError **error,
gdata_error->message);
}
-static void
-migrate_cache (EBookBackendCache *cache)
-{
- const gchar *version;
- const gchar *version_key = "book-cache-version";
-
- g_return_if_fail (cache != NULL);
-
- version = e_file_cache_get_object (E_FILE_CACHE (cache), version_key);
- if (!version || atoi (version) < 2) {
- /* not versioned yet or too old, dump the cache and reload it from the server */
- e_file_cache_clean (E_FILE_CACHE (cache));
- e_file_cache_add_object (E_FILE_CACHE (cache), version_key, "2");
- }
-}
-
-static void
-cache_init (EBookBackend *backend)
-{
- EBookBackendGooglePrivate *priv;
- const gchar *cache_dir;
- gchar *filename;
-
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
- g_mutex_lock (&priv->cache_lock);
-
- cache_dir = e_book_backend_get_cache_dir (backend);
- filename = g_build_filename (cache_dir, "cache.xml", NULL);
- priv->cache = e_book_backend_cache_new (filename);
- g_free (filename);
-
- migrate_cache (priv->cache);
-
- g_mutex_unlock (&priv->cache_lock);
-}
-
-static EContact *
-cache_add_contact (EBookBackend *backend,
- GDataEntry *entry)
-{
- EBookBackendGooglePrivate *priv;
- EContact *contact;
-
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
- g_rec_mutex_lock (&priv->groups_lock);
- contact = e_contact_new_from_gdata_entry (entry, priv->groups_by_id, priv->system_groups_by_entry_id);
- g_rec_mutex_unlock (&priv->groups_lock);
-
- if (!contact)
- return NULL;
-
- e_contact_add_gdata_entry_xml (contact, entry);
- g_mutex_lock (&priv->cache_lock);
- e_book_backend_cache_add_contact (priv->cache, contact);
- g_mutex_unlock (&priv->cache_lock);
- e_contact_remove_gdata_entry_xml (contact);
-
- return contact;
-}
-
static gboolean
-cache_remove_contact (EBookBackend *backend,
- const gchar *uid)
+ebb_google_is_authorized (EBookBackendGoogle *bbgoogle)
{
- EBookBackendGooglePrivate *priv;
- gboolean removed;
-
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_GOOGLE (bbgoogle), FALSE);
- g_mutex_lock (&priv->cache_lock);
- removed = e_book_backend_cache_remove_contact (priv->cache, uid);
- g_mutex_unlock (&priv->cache_lock);
+ if (!bbgoogle->priv->service)
+ return FALSE;
- return removed;
+ return gdata_service_is_authorized (GDATA_SERVICE (bbgoogle->priv->service));
}
static gboolean
-cache_has_contact (EBookBackend *backend,
- const gchar *uid)
-{
- EBookBackendGooglePrivate *priv;
- gboolean has_contact;
-
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
- g_mutex_lock (&priv->cache_lock);
- has_contact = e_book_backend_cache_check_contact (priv->cache, uid);
- g_mutex_unlock (&priv->cache_lock);
-
- return has_contact;
-}
-
-static EContact *
-cache_get_contact (EBookBackend *backend,
- const gchar *uid,
- GDataEntry **entry)
+ebb_google_request_authorization (EBookBackendGoogle *bbgoogle,
+ const ENamedParameters *credentials,
+ GCancellable *cancellable,
+ GError **error)
{
- EBookBackendGooglePrivate *priv;
- EContact *contact;
-
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
- g_mutex_lock (&priv->cache_lock);
- contact = e_book_backend_cache_get_contact (priv->cache, uid);
- g_mutex_unlock (&priv->cache_lock);
-
- if (contact) {
- if (entry) {
- const gchar *entry_xml, *edit_uri = NULL;
+ /* Make sure we have the GDataService configured
+ * before requesting authorization. */
- entry_xml = e_contact_get_gdata_entry_xml (contact, &edit_uri);
- *entry = GDATA_ENTRY (gdata_parsable_new_from_xml (GDATA_TYPE_CONTACTS_CONTACT,
entry_xml, -1, NULL));
+ if (!bbgoogle->priv->authorizer) {
+ ESource *source;
+ EGDataOAuth2Authorizer *authorizer;
- if (*entry) {
- GDataLink *edit_link = gdata_link_new (edit_uri, GDATA_LINK_EDIT);
- gdata_entry_add_link (*entry, edit_link);
- g_object_unref (edit_link);
- }
- }
+ source = e_backend_get_source (E_BACKEND (bbgoogle));
- e_contact_remove_gdata_entry_xml (contact);
+ /* Only OAuth2 is supported with Google Tasks */
+ authorizer = e_gdata_oauth2_authorizer_new (source);
+ bbgoogle->priv->authorizer = GDATA_AUTHORIZER (authorizer);
}
- return contact;
-}
-
-static void
-cache_get_contacts (EBookBackend *backend,
- GQueue *out_contacts)
-{
- EBookBackendGooglePrivate *priv;
- GList *list, *link;
-
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
- g_mutex_lock (&priv->cache_lock);
- list = e_book_backend_cache_get_contacts (
- priv->cache, "(contains \"x-evolution-any-field\" \"\")");
- g_mutex_unlock (&priv->cache_lock);
-
- for (link = list; link != NULL; link = g_list_next (link)) {
- EContact *contact = E_CONTACT (link->data);
-
- e_contact_remove_gdata_entry_xml (contact);
- g_queue_push_tail (out_contacts, g_object_ref (contact));
+ if (E_IS_GDATA_OAUTH2_AUTHORIZER (bbgoogle->priv->authorizer)) {
+ e_gdata_oauth2_authorizer_set_credentials (E_GDATA_OAUTH2_AUTHORIZER
(bbgoogle->priv->authorizer), credentials);
}
- g_list_free_full (list, (GDestroyNotify) g_object_unref);
-}
-
-static void
-cache_freeze (EBookBackend *backend)
-{
- EBookBackendGooglePrivate *priv;
-
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
- e_file_cache_freeze_changes (E_FILE_CACHE (priv->cache));
-}
-
-static void
-cache_thaw (EBookBackend *backend)
-{
- EBookBackendGooglePrivate *priv;
-
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
- e_file_cache_thaw_changes (E_FILE_CACHE (priv->cache));
-}
-
-static gchar *
-cache_get_last_update (EBookBackend *backend)
-{
- EBookBackendGooglePrivate *priv;
- gchar *last_update;
+ if (!bbgoogle->priv->service) {
+ GDataContactsService *contacts_service;
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
+ contacts_service = gdata_contacts_service_new (bbgoogle->priv->authorizer);
+ bbgoogle->priv->service = GDATA_SERVICE (contacts_service);
- g_mutex_lock (&priv->cache_lock);
- last_update = e_book_backend_cache_get_time (priv->cache);
- g_mutex_unlock (&priv->cache_lock);
+ e_binding_bind_property (
+ bbgoogle, "proxy-resolver",
+ bbgoogle->priv->service, "proxy-resolver",
+ G_BINDING_SYNC_CREATE);
+ }
- return last_update;
-}
+ /* If we're using OAuth tokens, then as far as the backend
+ * is concerned it's always authorized. The GDataAuthorizer
+ * will take care of everything in the background. */
+ if (!GDATA_IS_CLIENT_LOGIN_AUTHORIZER (bbgoogle->priv->authorizer))
+ return TRUE;
-static void
-cache_set_last_update (EBookBackend *backend,
- GTimeVal *tv)
-{
- EBookBackendGooglePrivate *priv;
- gchar *_time;
-
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
- if (tv)
- _time = g_time_val_to_iso8601 (tv);
- else
- _time = NULL;
-
- g_mutex_lock (&priv->cache_lock);
- if (tv)
- e_book_backend_cache_set_time (priv->cache, _time);
- else
- e_file_cache_remove_object (E_FILE_CACHE (priv->cache), "last_update_time");
- g_mutex_unlock (&priv->cache_lock);
- g_free (_time);
+ /* Otherwise it's up to us to obtain a login secret, but
+ there is currently no way to do it, thus simply fail. */
+ return FALSE;
}
/* returns whether group changed from the one stored in the cache;
@@ -374,661 +206,476 @@ cache_set_last_update (EBookBackend *backend,
* use group_name = NULL to remove it from the cache.
*/
static gboolean
-cache_update_group (EBookBackend *backend,
- const gchar *group_id,
- const gchar *group_name)
+ebb_google_cache_update_group (EBookBackendGoogle *bbgoogle,
+ const gchar *group_id,
+ const gchar *group_name)
{
- EBookBackendGooglePrivate *priv;
- EFileCache *file_cache;
+ EBookCache *book_cache;
gboolean changed;
- gchar *key;
+ gchar *key, *old_value;
- g_return_val_if_fail (E_IS_BOOK_BACKEND_GOOGLE (backend), FALSE);
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_GOOGLE (bbgoogle), FALSE);
g_return_val_if_fail (group_id != NULL, FALSE);
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
- key = g_strconcat ("google-group", ":", group_id, NULL);
+ book_cache = e_book_meta_backend_ref_cache (E_BOOK_META_BACKEND (bbgoogle));
+ g_return_val_if_fail (book_cache != NULL, FALSE);
- g_mutex_lock (&priv->cache_lock);
-
- file_cache = E_FILE_CACHE (priv->cache);
+ key = g_strconcat ("google-group", ":", group_id, NULL);
+ old_value = e_cache_dup_key (E_CACHE (book_cache), key, NULL);
if (group_name) {
- const gchar *old_value;
-
- old_value = e_file_cache_get_object (file_cache, key);
changed = old_value && g_strcmp0 (old_value, group_name) != 0;
- if (!e_file_cache_replace_object (file_cache, key, group_name))
- e_file_cache_add_object (file_cache, key, group_name);
+ e_cache_set_key (E_CACHE (book_cache), key, group_name, NULL);
/* Add the category to Evolution’s category list. */
e_categories_add (group_name, NULL, NULL, TRUE);
} else {
- const gchar *old_value;
+ changed = old_value != NULL;
- old_value = e_file_cache_get_object (file_cache, key);
- changed = e_file_cache_remove_object (file_cache, key);
+ e_cache_set_key (E_CACHE (book_cache), key, NULL, NULL);
/* Remove the category from Evolution’s category list. */
- if (old_value != NULL) {
+ if (changed)
e_categories_remove (old_value);
- }
}
- g_mutex_unlock (&priv->cache_lock);
-
+ g_object_unref (book_cache);
+ g_free (old_value);
g_free (key);
return changed;
}
-static gboolean
-backend_is_authorized (EBookBackend *backend)
+static void
+ebb_google_process_group (GDataEntry *entry,
+ guint entry_key,
+ guint entry_count,
+ gpointer user_data)
{
- EBookBackendGooglePrivate *priv;
-
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
- if (priv->service == NULL)
- return FALSE;
-
- return gdata_service_is_authorized (priv->service);
-}
+ EBookBackendGoogle *bbgoogle = user_data;
+ const gchar *uid, *system_group_id;
+ gchar *name;
+ gboolean is_deleted;
-static GCancellable *
-start_operation (EBookBackend *backend,
- guint32 opid,
- GCancellable *cancellable,
- const gchar *message)
-{
- EBookBackendGooglePrivate *priv;
- GList *list, *link;
+ g_return_if_fail (E_IS_BOOK_BACKEND_GOOGLE (bbgoogle));
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
+ uid = gdata_entry_get_id (entry);
+ name = e_contact_sanitise_google_group_name (entry);
- /* Insert the operation into the set of active cancellable operations */
- if (cancellable)
- g_object_ref (cancellable);
- else
- cancellable = g_cancellable_new ();
- g_hash_table_insert (priv->cancellables, GUINT_TO_POINTER (opid), g_object_ref (cancellable));
+ system_group_id = gdata_contacts_group_get_system_group_id (GDATA_CONTACTS_GROUP (entry));
+ is_deleted = gdata_contacts_group_is_deleted (GDATA_CONTACTS_GROUP (entry));
- /* Send out a status message to each view */
- list = e_book_backend_list_views (backend);
+ g_rec_mutex_lock (&bbgoogle->priv->groups_lock);
- for (link = list; link != NULL; link = g_list_next (link)) {
- EDataBookView *view = E_DATA_BOOK_VIEW (link->data);
- e_data_book_view_notify_progress (view, -1, message);
- }
+ if (system_group_id) {
+ if (is_deleted) {
+ gchar *entry_id = g_hash_table_lookup (bbgoogle->priv->system_groups_by_id,
system_group_id);
+ g_hash_table_remove (bbgoogle->priv->system_groups_by_entry_id, entry_id);
+ g_hash_table_remove (bbgoogle->priv->system_groups_by_id, system_group_id);
+ } else {
+ gchar *entry_id, *system_group_id_dup;
- g_list_free_full (list, (GDestroyNotify) g_object_unref);
+ entry_id = e_contact_sanitise_google_group_id (uid);
+ system_group_id_dup = g_strdup (system_group_id);
- return cancellable;
-}
+ g_hash_table_replace (bbgoogle->priv->system_groups_by_entry_id, entry_id,
system_group_id_dup);
+ g_hash_table_replace (bbgoogle->priv->system_groups_by_id, system_group_id_dup,
entry_id);
+ }
-static void
-finish_operation (EBookBackend *backend,
- guint32 opid,
- const GError *gdata_error)
-{
- EBookBackendGooglePrivate *priv;
- GError *book_error = NULL;
+ g_free (name);
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
+ /* use evolution's names for google's system groups */
+ name = g_strdup (e_contact_map_google_with_evo_group (system_group_id, TRUE));
- if (gdata_error != NULL) {
- data_book_error_from_gdata_error (&book_error, gdata_error);
- g_debug ("Book view query failed: %s", book_error->message);
+ g_warn_if_fail (name != NULL);
+ if (!name)
+ name = g_strdup (system_group_id);
}
- if (g_hash_table_remove (priv->cancellables, GUINT_TO_POINTER (opid))) {
- GList *list, *link;
-
- list = e_book_backend_list_views (backend);
+ if (is_deleted) {
+ g_hash_table_remove (bbgoogle->priv->groups_by_id, uid);
+ g_hash_table_remove (bbgoogle->priv->groups_by_name, name);
- for (link = list; link != NULL; link = g_list_next (link)) {
- EDataBookView *view = E_DATA_BOOK_VIEW (link->data);
- e_data_book_view_notify_complete (view, book_error);
- }
+ bbgoogle->priv->groups_changed = ebb_google_cache_update_group (bbgoogle, uid, NULL) ||
bbgoogle->priv->groups_changed;
+ } else {
+ g_hash_table_replace (bbgoogle->priv->groups_by_id, e_contact_sanitise_google_group_id (uid),
g_strdup (name));
+ g_hash_table_replace (bbgoogle->priv->groups_by_name, g_strdup (name),
e_contact_sanitise_google_group_id (uid));
- g_list_free_full (list, (GDestroyNotify) g_object_unref);
+ bbgoogle->priv->groups_changed = ebb_google_cache_update_group (bbgoogle, uid, name) ||
bbgoogle->priv->groups_changed;
}
- g_clear_error (&book_error);
-}
-
-static void
-process_contact_finish (EBookBackend *backend,
- GDataEntry *entry)
-{
- EContact *new_contact;
-
- g_debug (G_STRFUNC);
-
- new_contact = cache_add_contact (backend, entry);
+ g_rec_mutex_unlock (&bbgoogle->priv->groups_lock);
- if (!new_contact)
- return;
-
- e_book_backend_notify_update (backend, new_contact);
-
- g_object_unref (new_contact);
+ g_free (name);
}
-typedef struct {
- EBookBackend *backend;
- GCancellable *cancellable;
- GError *gdata_error;
+static gboolean
+ebb_google_get_groups_sync (EBookBackendGoogle *bbgoogle,
+ gboolean with_time_constraint,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GDataQuery *query;
+ GDataFeed *feed;
+ gboolean success;
- /* These two don't need locking; they're only accessed from the main thread. */
- gboolean update_complete;
- guint num_contacts_pending_photos;
-} GetContactsData;
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_GOOGLE (bbgoogle), FALSE);
+ g_return_val_if_fail (ebb_google_is_authorized (bbgoogle), FALSE);
-static void
-check_get_new_contacts_finished (GetContactsData *data)
-{
- g_debug (G_STRFUNC);
+ g_rec_mutex_lock (&bbgoogle->priv->groups_lock);
- /* Are we finished yet? */
- if (data->update_complete == FALSE || data->num_contacts_pending_photos > 0) {
- g_debug (
- "Bailing from check_get_new_contacts_finished(): update_complete: %u,
num_contacts_pending_photos: %u, data: %p",
- data->update_complete, data->num_contacts_pending_photos, data);
- return;
+ /* Build our query, always fetch all of them */
+ query = GDATA_QUERY (gdata_contacts_query_new_with_limits (NULL, 0, G_MAXINT));
+ if (with_time_constraint && bbgoogle->priv->groups_last_update.tv_sec != 0) {
+ gdata_query_set_updated_min (query, bbgoogle->priv->groups_last_update.tv_sec);
+ gdata_contacts_query_set_show_deleted (GDATA_CONTACTS_QUERY (query), TRUE);
}
- g_debug ("Proceeding with check_get_new_contacts_finished() for data: %p.", data);
-
- finish_operation (data->backend, -1, data->gdata_error);
+ bbgoogle->priv->groups_changed = FALSE;
- /* Tidy up */
- g_object_unref (data->cancellable);
- g_object_unref (data->backend);
- g_clear_error (&data->gdata_error);
+ /* Run the query synchronously */
+ feed = gdata_contacts_service_query_groups (
+ GDATA_CONTACTS_SERVICE (bbgoogle->priv->service),
+ query, cancellable, ebb_google_process_group, bbgoogle, error);
- g_slice_free (GetContactsData, data);
-}
+ success = feed != NULL;
-typedef struct {
- GetContactsData *parent_data;
+ if (success)
+ g_get_current_time (&bbgoogle->priv->groups_last_update);
- GCancellable *cancellable;
- gulong cancelled_handle;
-} PhotoData;
+ g_rec_mutex_unlock (&bbgoogle->priv->groups_lock);
-static void
-process_contact_photo_cancelled_cb (GCancellable *parent_cancellable,
- GCancellable *photo_cancellable)
-{
- g_debug (G_STRFUNC);
+ g_clear_object (&feed);
+ g_object_unref (query);
- g_cancellable_cancel (photo_cancellable);
+ return success;
}
-static void
-process_contact_photo_cb (GDataContactsContact *gdata_contact,
- GAsyncResult *async_result,
- PhotoData *data)
+static gboolean
+ebb_google_connect_sync (EBookMetaBackend *meta_backend,
+ const ENamedParameters *credentials,
+ ESourceAuthenticationResult *out_auth_result,
+ gchar **out_certificate_pem,
+ GTlsCertificateFlags *out_certificate_errors,
+ GCancellable *cancellable,
+ GError **error)
{
- EBookBackend *backend = data->parent_data->backend;
- guint8 *photo_data = NULL;
- gsize photo_length;
- gchar *photo_content_type = NULL;
- GError *error = NULL;
-
- g_debug (G_STRFUNC);
-
- /* Finish downloading the photo */
- photo_data = gdata_contacts_contact_get_photo_finish (gdata_contact, async_result, &photo_length,
&photo_content_type, &error);
+ EBookBackendGoogle *bbgoogle;
+ gboolean success;
+ GError *local_error = NULL;
- if (error == NULL) {
- EContactPhoto *photo;
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_GOOGLE (meta_backend), FALSE);
+ g_return_val_if_fail (out_auth_result != NULL, FALSE);
- /* Success! Create an EContactPhoto and store it on the final GDataContactsContact object so
it makes it into the cache. */
- photo = e_contact_photo_new ();
- photo->type = E_CONTACT_PHOTO_TYPE_INLINED;
- photo->data.inlined.data = (guchar *) photo_data;
- photo->data.inlined.length = photo_length;
- photo->data.inlined.mime_type = photo_content_type;
+ bbgoogle = E_BOOK_BACKEND_GOOGLE (meta_backend);
- g_object_set_data_full (G_OBJECT (gdata_contact), "photo", photo, (GDestroyNotify)
e_contact_photo_free);
+ *out_auth_result = E_SOURCE_AUTHENTICATION_ACCEPTED;
- photo_data = NULL;
- photo_content_type = NULL;
- } else {
- /* Error. */
- g_debug ("Downloading contact photo for '%s' failed: %s", gdata_entry_get_id (GDATA_ENTRY
(gdata_contact)), error->message);
- g_error_free (error);
- }
+ if (ebb_google_is_authorized (bbgoogle))
+ return TRUE;
- process_contact_finish (backend, GDATA_ENTRY (gdata_contact));
+ success = ebb_google_request_authorization (bbgoogle, credentials, cancellable, &local_error);
+ if (success)
+ success = gdata_authorizer_refresh_authorization (bbgoogle->priv->authorizer, cancellable,
&local_error);
- g_free (photo_data);
- g_free (photo_content_type);
+ if (success)
+ success = ebb_google_get_groups_sync (bbgoogle, FALSE, cancellable, &local_error);
- /* Disconnect from the cancellable. */
- g_cancellable_disconnect (data->parent_data->cancellable, data->cancelled_handle);
- g_object_unref (data->cancellable);
+ if (!success) {
+ if (g_error_matches (local_error, GDATA_SERVICE_ERROR,
GDATA_SERVICE_ERROR_AUTHENTICATION_REQUIRED)) {
+ if (!e_named_parameters_exists (credentials, E_SOURCE_CREDENTIAL_PASSWORD))
+ *out_auth_result = E_SOURCE_AUTHENTICATION_REQUIRED;
+ else
+ *out_auth_result = E_SOURCE_AUTHENTICATION_REJECTED;
+ } else {
+ *out_auth_result = E_SOURCE_AUTHENTICATION_ERROR;
+ ebb_google_data_book_error_from_gdata_error (error, local_error);
+ }
- data->parent_data->num_contacts_pending_photos--;
- check_get_new_contacts_finished (data->parent_data);
+ g_clear_error (&local_error);
+ }
- g_slice_free (PhotoData, data);
+ return success;
}
-static void
-process_contact_cb (GDataEntry *entry,
- guint entry_key,
- guint entry_count,
- GetContactsData *data)
+static gboolean
+ebb_google_disconnect_sync (EBookMetaBackend *meta_backend,
+ GCancellable *cancellable,
+ GError **error)
{
- EBookBackendGooglePrivate *priv;
- EBookBackend *backend = data->backend;
- gboolean is_deleted, is_cached;
- const gchar *uid;
-
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
- g_debug (G_STRFUNC);
- uid = gdata_entry_get_id (entry);
- is_deleted = gdata_contacts_contact_is_deleted (GDATA_CONTACTS_CONTACT (entry));
-
- is_cached = cache_has_contact (backend, uid);
- if (is_deleted) {
- /* Do we have this item in our cache? */
- if (is_cached) {
- cache_remove_contact (backend, uid);
- e_book_backend_notify_remove (backend, uid);
- }
- } else {
- gchar *old_photo_etag = NULL;
- const gchar *new_photo_etag;
-
- /* Download the contact's photo first, if the contact's uncached or if the photo's been
updated. */
- if (is_cached == TRUE) {
- EContact *old_contact;
- EContactPhoto *photo;
- EVCardAttribute *old_attr;
-
- old_contact = cache_get_contact (backend, uid, NULL);
-
- /* Get the old ETag. */
- old_attr = e_vcard_get_attribute (E_VCARD (old_contact), GDATA_PHOTO_ETAG_ATTR);
- old_photo_etag = (old_attr != NULL) ? e_vcard_attribute_get_value (old_attr) : NULL;
-
- /* Attach the old photo to the new contact. */
- photo = e_contact_get (old_contact, E_CONTACT_PHOTO);
-
- if (photo != NULL && photo->type == E_CONTACT_PHOTO_TYPE_INLINED) {
- g_object_set_data_full (G_OBJECT (entry), "photo", photo, (GDestroyNotify)
e_contact_photo_free);
- } else if (photo != NULL) {
- e_contact_photo_free (photo);
- }
-
- g_object_unref (old_contact);
- }
+ EBookBackendGoogle *bbgoogle;
- new_photo_etag = gdata_contacts_contact_get_photo_etag (GDATA_CONTACTS_CONTACT (entry));
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_GOOGLE (meta_backend), FALSE);
- if ((old_photo_etag == NULL && new_photo_etag != NULL) ||
- (old_photo_etag != NULL && new_photo_etag != NULL && strcmp (old_photo_etag,
new_photo_etag) != 0)) {
- GCancellable *cancellable;
- PhotoData *photo_data;
+ bbgoogle = E_BOOK_BACKEND_GOOGLE (meta_backend);
- photo_data = g_slice_new (PhotoData);
- photo_data->parent_data = data;
+ g_clear_object (&bbgoogle->priv->service);
+ g_clear_object (&bbgoogle->priv->authorizer);
- /* Increment the count of contacts whose photos we're waiting for. */
- data->num_contacts_pending_photos++;
+ return TRUE;
+}
- /* Cancel downloading if the get_new_contacts() operation is cancelled. */
- cancellable = g_cancellable_new ();
+static gboolean
+ebb_google_get_changes_sync (EBookMetaBackend *meta_backend,
+ const gchar *last_sync_tag,
+ gboolean is_repeat,
+ gchar **out_new_sync_tag,
+ gboolean *out_repeat,
+ GSList **out_created_objects, /* EBookMetaBackendInfo * */
+ GSList **out_modified_objects, /* EBookMetaBackendInfo * */
+ GSList **out_removed_objects, /* EBookMetaBackendInfo * */
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookBackendGoogle *bbgoogle;
+ EBookCache *book_cache;
+ gint64 updated_time = 0;
+ GTimeVal last_updated;
+ GDataFeed *feed;
+ GDataContactsQuery *contacts_query;
+ GError *local_error = NULL;
- photo_data->cancellable = g_object_ref (cancellable);
- photo_data->cancelled_handle = g_cancellable_connect (
- data->cancellable, (GCallback) process_contact_photo_cancelled_cb,
- g_object_ref (cancellable), (GDestroyNotify) g_object_unref);
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_GOOGLE (meta_backend), FALSE);
+ g_return_val_if_fail (out_new_sync_tag != NULL, FALSE);
+ g_return_val_if_fail (out_created_objects != NULL, FALSE);
+ g_return_val_if_fail (out_modified_objects != NULL, FALSE);
+ g_return_val_if_fail (out_removed_objects != NULL, FALSE);
- /* Download the photo. */
- gdata_contacts_contact_get_photo_async (
- GDATA_CONTACTS_CONTACT (entry),
- GDATA_CONTACTS_SERVICE (priv->service), cancellable,
- (GAsyncReadyCallback) process_contact_photo_cb, photo_data);
+ bbgoogle = E_BOOK_BACKEND_GOOGLE (meta_backend);
- g_object_unref (cancellable);
- g_free (old_photo_etag);
+ *out_created_objects = NULL;
+ *out_modified_objects = NULL;
+ *out_removed_objects = NULL;
- return;
- }
+ if (!ebb_google_get_groups_sync (bbgoogle, TRUE, cancellable, error))
+ return FALSE;
- g_free (old_photo_etag);
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
- /* Since we're not downloading a photo, add the contact to the cache now. */
- process_contact_finish (backend, entry);
+ if (!last_sync_tag ||
+ !g_time_val_from_iso8601 (last_sync_tag, &last_updated)) {
+ last_updated.tv_sec = 0;
}
-}
-
-static void
-get_new_contacts_cb (GDataService *service,
- GAsyncResult *result,
- GetContactsData *data)
-{
- EBookBackend *backend = data->backend;
- GDataFeed *feed;
- GError *gdata_error = NULL;
- g_debug (G_STRFUNC);
- feed = gdata_service_query_finish (service, result, &gdata_error);
- if (feed != NULL) {
- GList *entries = gdata_feed_get_entries (feed);
- g_debug ("Feed has %d entries", g_list_length (entries));
+ contacts_query = gdata_contacts_query_new_with_limits (NULL, 0, G_MAXINT);
+ if (last_updated.tv_sec > 0 && !bbgoogle->priv->groups_changed) {
+ gdata_query_set_updated_min (GDATA_QUERY (contacts_query), last_updated.tv_sec);
+ gdata_contacts_query_set_show_deleted (contacts_query, TRUE);
}
- if (feed != NULL)
- g_object_unref (feed);
+ feed = gdata_contacts_service_query_contacts (GDATA_CONTACTS_SERVICE (bbgoogle->priv->service),
GDATA_QUERY (contacts_query), cancellable, NULL, NULL, &local_error);
- if (!gdata_error) {
- /* Finish updating the cache */
- GTimeVal current_time;
- g_get_current_time (¤t_time);
- cache_set_last_update (backend, ¤t_time);
+ if (feed && !g_cancellable_is_cancelled (cancellable) && !local_error) {
+ GList *link;
- e_backend_ensure_source_status_connected (E_BACKEND (backend));
- }
+ if (gdata_feed_get_updated (feed) > updated_time)
+ updated_time = gdata_feed_get_updated (feed);
- /* Thaw the cache again */
- cache_thaw (backend);
+ for (link = gdata_feed_get_entries (feed); link && !g_cancellable_is_cancelled (cancellable);
link = g_list_next (link)) {
+ GDataContactsContact *gdata_contact = link->data;
+ EContact *cached_contact = NULL;
+ gchar *uid;
- /* Note: The operation's only marked as finished when all the
- * process_contact_photo_cb() callbacks have been called as well. */
- data->update_complete = TRUE;
- data->gdata_error = gdata_error;
- check_get_new_contacts_finished (data);
-}
-
-static void
-get_new_contacts (EBookBackend *backend)
-{
- EBookBackendGooglePrivate *priv;
- gchar *last_updated;
- GTimeVal updated;
- GDataQuery *query;
- GCancellable *cancellable;
- GetContactsData *data;
+ if (!GDATA_IS_CONTACTS_CONTACT (gdata_contact))
+ continue;
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
+ uid = g_strdup (gdata_entry_get_id (GDATA_ENTRY (gdata_contact)));
+ if (!uid || !*uid) {
+ g_free (uid);
+ continue;
+ }
- g_debug (G_STRFUNC);
- g_return_if_fail (backend_is_authorized (backend));
+ if (!e_book_cache_get_contact (book_cache, uid, FALSE, &cached_contact, cancellable,
NULL))
+ cached_contact = NULL;
- /* Sort out update times */
- last_updated = cache_get_last_update (backend);
- g_return_if_fail (last_updated == NULL || g_time_val_from_iso8601 (last_updated, &updated) == TRUE);
- g_free (last_updated);
+ if (gdata_contacts_contact_is_deleted (gdata_contact)) {
+ *out_removed_objects = g_slist_prepend (*out_removed_objects,
+ e_book_meta_backend_info_new (uid, NULL, NULL, NULL));
+ } else {
+ EContact *new_contact;
- /* Prevent the cache writing each change to disk individually (thawed in get_new_contacts_cb()) */
- cache_freeze (backend);
+ if (cached_contact) {
+ gchar *old_etag;
- /* Build our query */
- query = GDATA_QUERY (gdata_contacts_query_new_with_limits (NULL, 0, G_MAXINT));
- if (last_updated) {
- gdata_query_set_updated_min (query, updated.tv_sec);
- gdata_contacts_query_set_show_deleted (GDATA_CONTACTS_QUERY (query), TRUE);
- }
+ old_etag = e_contact_get (cached_contact, E_CONTACT_REV);
- /* Query for new contacts asynchronously */
- cancellable = start_operation (backend, -1, NULL, _("Querying for updated contacts…"));
-
- data = g_slice_new (GetContactsData);
- data->backend = g_object_ref (backend);
- data->cancellable = g_object_ref (cancellable);
- data->gdata_error = NULL;
- data->num_contacts_pending_photos = 0;
- data->update_complete = FALSE;
-
- gdata_contacts_service_query_contacts_async (
- GDATA_CONTACTS_SERVICE (priv->service),
- query,
- cancellable,
- (GDataQueryProgressCallback) process_contact_cb,
- data,
- (GDestroyNotify) NULL,
- (GAsyncReadyCallback) get_new_contacts_cb,
- data);
-
- g_object_unref (cancellable);
- g_object_unref (query);
-}
+ if (g_strcmp0 (gdata_entry_get_etag (GDATA_ENTRY (gdata_contact)),
old_etag) == 0) {
+ g_object_unref (cached_contact);
+ g_free (old_etag);
+ g_free (uid);
+ continue;
+ }
-static void
-process_group (GDataEntry *entry,
- guint entry_key,
- guint entry_count,
- EBookBackend *backend)
-{
- EBookBackendGooglePrivate *priv;
- const gchar *uid, *system_group_id;
- gchar *name;
- gboolean is_deleted;
+ g_free (old_etag);
+ }
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
+ g_rec_mutex_lock (&bbgoogle->priv->groups_lock);
+ new_contact = e_contact_new_from_gdata_entry (GDATA_ENTRY (gdata_contact),
+ bbgoogle->priv->groups_by_id,
bbgoogle->priv->system_groups_by_entry_id);
+ g_rec_mutex_unlock (&bbgoogle->priv->groups_lock);
- g_debug (G_STRFUNC);
- uid = gdata_entry_get_id (entry);
- name = e_contact_sanitise_google_group_name (entry);
+ if (new_contact) {
+ const gchar *revision, *photo_etag;
+ gchar *object, *extra;
- system_group_id = gdata_contacts_group_get_system_group_id (GDATA_CONTACTS_GROUP (entry));
- is_deleted = gdata_contacts_group_is_deleted (GDATA_CONTACTS_GROUP (entry));
+ photo_etag = gdata_contacts_contact_get_photo_etag (gdata_contact);
+ if (photo_etag && cached_contact) {
+ gchar *old_photo_etag;
- g_rec_mutex_lock (&priv->groups_lock);
+ old_photo_etag = e_vcard_util_dup_x_attribute (E_VCARD
(cached_contact), E_GOOGLE_X_PHOTO_ETAG);
+ if (g_strcmp0 (photo_etag, old_photo_etag) == 0) {
+ EContactPhoto *photo;
- if (system_group_id) {
- g_debug ("Processing %ssystem group %s, %s", is_deleted ? "(deleted) " : "", system_group_id,
uid);
+ /* To not download it again, when it's already
available locally */
+ photo_etag = NULL;
- if (is_deleted) {
- gchar *entry_id = g_hash_table_lookup (priv->system_groups_by_id, system_group_id);
- g_hash_table_remove (priv->system_groups_by_entry_id, entry_id);
- g_hash_table_remove (priv->system_groups_by_id, system_group_id);
- } else {
- gchar *entry_id, *system_group_id_dup;
+ /* Copy the photo attribute to the changed contact */
+ photo = e_contact_get (cached_contact,
E_CONTACT_PHOTO);
+ e_contact_set (new_contact, E_CONTACT_PHOTO, photo);
- entry_id = e_contact_sanitise_google_group_id (uid);
- system_group_id_dup = g_strdup (system_group_id);
+ e_contact_photo_free (photo);
+ }
- g_hash_table_replace (priv->system_groups_by_entry_id, entry_id, system_group_id_dup);
- g_hash_table_replace (priv->system_groups_by_id, system_group_id_dup, entry_id);
- }
+ g_free (old_photo_etag);
+ }
- g_free (name);
+ if (photo_etag) {
+ guint8 *photo_data;
+ gsize photo_length = 0;
+ gchar *photo_content_type = NULL;
+ GError *local_error2 = NULL;
- /* use evolution's names for google's system groups */
- name = g_strdup (e_contact_map_google_with_evo_group (system_group_id, TRUE));
+ photo_data = gdata_contacts_contact_get_photo (gdata_contact,
GDATA_CONTACTS_SERVICE (bbgoogle->priv->service),
+ &photo_length, &photo_content_type, cancellable,
&local_error2);
- g_warn_if_fail (name != NULL);
- if (!name)
- name = g_strdup (system_group_id);
- }
+ if (!local_error2) {
+ EContactPhoto *photo;
- if (is_deleted) {
- g_debug ("Processing (deleting) group %s, %s", uid, name);
- g_hash_table_remove (priv->groups_by_id, uid);
- g_hash_table_remove (priv->groups_by_name, name);
+ photo = e_contact_photo_new ();
+ photo->type = E_CONTACT_PHOTO_TYPE_INLINED;
+ photo->data.inlined.data = (guchar *) photo_data;
+ photo->data.inlined.length = photo_length;
+ photo->data.inlined.mime_type = photo_content_type;
- priv->groups_changed = cache_update_group (backend, uid, NULL) || priv->groups_changed;
- } else {
- g_debug ("Processing group %s, %s", uid, name);
- g_hash_table_replace (priv->groups_by_id, e_contact_sanitise_google_group_id (uid), g_strdup
(name));
- g_hash_table_replace (priv->groups_by_name, g_strdup (name),
e_contact_sanitise_google_group_id (uid));
+ e_contact_set (E_CONTACT (new_contact),
E_CONTACT_PHOTO, photo);
- priv->groups_changed = cache_update_group (backend, uid, name) || priv->groups_changed;
- }
+ e_contact_photo_free (photo);
- g_rec_mutex_unlock (&priv->groups_lock);
+ /* Read of the photo frees previously obtained
photo_etag */
+ photo_etag = gdata_contacts_contact_get_photo_etag
(gdata_contact);
- g_free (name);
-}
+ e_vcard_util_set_x_attribute (E_VCARD (new_contact),
E_GOOGLE_X_PHOTO_ETAG, photo_etag);
+ } else {
+ g_debug ("%s: Downloading contact photo for '%s'
failed: %s", G_STRFUNC,
+ gdata_entry_get_id (GDATA_ENTRY
(gdata_contact)), local_error2->message);
-static void
-get_groups_cb (GDataService *service,
- GAsyncResult *result,
- EBookBackend *backend)
-{
- EBookBackendGooglePrivate *priv;
- GDataFeed *feed;
- GError *gdata_error = NULL;
+ g_clear_error (&local_error2);
+ }
+ }
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
+ revision = gdata_entry_get_etag (GDATA_ENTRY (gdata_contact));
+ e_contact_set (new_contact, E_CONTACT_REV, revision);
+ object = e_vcard_to_string (E_VCARD (new_contact),
EVC_FORMAT_VCARD_30);
+ extra = gdata_parsable_get_xml (GDATA_PARSABLE (gdata_contact));
- g_debug (G_STRFUNC);
- feed = gdata_service_query_finish (service, result, &gdata_error);
- if (feed != NULL) {
- GList *entries = gdata_feed_get_entries (feed);
- g_debug ("Group feed has %d entries", g_list_length (entries));
- }
+ if (cached_contact) {
+ *out_modified_objects = g_slist_prepend
(*out_modified_objects,
+ e_book_meta_backend_info_new (uid, revision, object,
extra));
+ } else {
+ *out_created_objects = g_slist_prepend (*out_created_objects,
+ e_book_meta_backend_info_new (uid, revision, object,
extra));
+ }
- if (feed != NULL)
- g_object_unref (feed);
+ g_free (object);
+ g_free (extra);
+ }
- if (!gdata_error) {
- /* Update the update time */
- g_rec_mutex_lock (&priv->groups_lock);
- g_get_current_time (&(priv->groups_last_update));
- g_rec_mutex_unlock (&priv->groups_lock);
+ g_clear_object (&new_contact);
+ }
- e_backend_ensure_source_status_connected (E_BACKEND (backend));
+ g_clear_object (&cached_contact);
+ g_free (uid);
+ }
}
- finish_operation (backend, -2, gdata_error);
+ g_clear_object (&contacts_query);
+ g_clear_object (&feed);
- g_rec_mutex_lock (&priv->groups_lock);
+ if (!g_cancellable_is_cancelled (cancellable) && !local_error) {
+ last_updated.tv_sec = updated_time;
+ last_updated.tv_usec = 0;
- if (priv->groups_changed) {
- priv->groups_changed = FALSE;
+ *out_new_sync_tag = g_time_val_to_iso8601 (&last_updated);
+ }
- g_rec_mutex_unlock (&priv->groups_lock);
+ g_clear_object (&book_cache);
- /* do the update for all contacts, like with an empty cache */
- cache_set_last_update (backend, NULL);
- get_new_contacts (backend);
- } else {
- g_rec_mutex_unlock (&priv->groups_lock);
+ if (local_error) {
+ g_propagate_error (error, local_error);
+ return FALSE;
}
- g_object_unref (backend);
-
- g_clear_error (&gdata_error);
+ return TRUE;
}
-static void
-get_groups_and_update_cache_cb (GDataService *service,
- GAsyncResult *result,
- EBookBackend *backend)
+static gboolean
+ebb_google_load_contact_sync (EBookMetaBackend *meta_backend,
+ const gchar *uid,
+ const gchar *extra,
+ EContact **out_contact,
+ gchar **out_extra,
+ GCancellable *cancellable,
+ GError **error)
{
- g_object_ref (backend);
+ EBookBackendGoogle *bbgoogle;
- get_groups_cb (service, result, backend);
- get_new_contacts (backend);
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_GOOGLE (meta_backend), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (out_contact != NULL, FALSE);
+ g_return_val_if_fail (out_extra != NULL, FALSE);
- g_object_unref (backend);
-}
+ bbgoogle = E_BOOK_BACKEND_GOOGLE (meta_backend);
-static void
-get_groups (EBookBackend *backend,
- gboolean and_update_cache)
-{
- EBookBackendGooglePrivate *priv;
- GDataQuery *query;
- GCancellable *cancellable;
+ /* Only "load" preloaded during save, otherwise fail with an error,
+ because the backend provides objects within get_changes_sync() */
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
+ if (bbgoogle->priv->preloaded) {
+ EContact *contact;
- g_debug (G_STRFUNC);
- g_return_if_fail (backend_is_authorized (backend));
+ contact = g_hash_table_lookup (bbgoogle->priv->preloaded, uid);
+ if (contact) {
+ *out_contact = e_contact_duplicate (contact);
- g_rec_mutex_lock (&priv->groups_lock);
+ g_hash_table_remove (bbgoogle->priv->preloaded, uid);
- /* Build our query */
- query = GDATA_QUERY (gdata_contacts_query_new_with_limits (NULL, 0, G_MAXINT));
- if (priv->groups_last_update.tv_sec != 0 || priv->groups_last_update.tv_usec != 0) {
- gdata_query_set_updated_min (query, priv->groups_last_update.tv_sec);
- gdata_contacts_query_set_show_deleted (GDATA_CONTACTS_QUERY (query), TRUE);
+ return TRUE;
+ }
}
- priv->groups_changed = FALSE;
-
- g_rec_mutex_unlock (&priv->groups_lock);
-
- g_object_ref (backend);
-
- /* Run the query asynchronously */
- cancellable = start_operation (backend, -2, NULL, _("Querying for updated groups…"));
- gdata_contacts_service_query_groups_async (
- GDATA_CONTACTS_SERVICE (priv->service),
- query,
- cancellable,
- (GDataQueryProgressCallback) process_group,
- backend,
- (GDestroyNotify) NULL,
- (GAsyncReadyCallback) (and_update_cache ? get_groups_and_update_cache_cb : get_groups_cb),
- backend);
-
- g_object_unref (cancellable);
- g_object_unref (query);
-}
+ g_set_error_literal (error, E_BOOK_CLIENT_ERROR, E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND,
+ e_book_client_error_to_string (E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND));
-static void
-get_groups_sync (EBookBackend *backend,
- GCancellable *cancellable,
- GError **error)
-{
- EBookBackendGooglePrivate *priv;
- GDataQuery *query;
- GDataFeed *feed;
-
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
- g_debug (G_STRFUNC);
- g_return_if_fail (backend_is_authorized (backend));
-
- /* Build our query, always fetch all of them */
- query = GDATA_QUERY (gdata_contacts_query_new_with_limits (NULL, 0, G_MAXINT));
-
- /* Run the query synchronously */
- feed = gdata_contacts_service_query_groups (
- GDATA_CONTACTS_SERVICE (priv->service),
- query,
- cancellable,
- (GDataQueryProgressCallback) process_group,
- backend,
- error);
-
- if (feed)
- g_object_unref (feed);
-
- g_object_unref (query);
+ return FALSE;
}
static gchar *
-create_group (EBookBackend *backend,
- const gchar *category_name,
- GError **error)
+ebb_google_create_group_sync (EBookBackendGoogle *bbgoogle,
+ const gchar *category_name,
+ GCancellable *cancellable,
+ GError **error)
{
- EBookBackendGooglePrivate *priv;
GDataEntry *group, *new_group;
- gchar *uid;
const gchar *system_group_id;
-
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
+ gchar *uid;
system_group_id = e_contact_map_google_with_evo_group (category_name, FALSE);
if (system_group_id) {
gchar *group_entry_id;
- g_rec_mutex_lock (&priv->groups_lock);
- group_entry_id = g_strdup (g_hash_table_lookup (priv->system_groups_by_id, system_group_id));
- g_rec_mutex_unlock (&priv->groups_lock);
+ g_rec_mutex_lock (&bbgoogle->priv->groups_lock);
+ group_entry_id = g_strdup (g_hash_table_lookup (bbgoogle->priv->system_groups_by_id,
system_group_id));
+ g_rec_mutex_unlock (&bbgoogle->priv->groups_lock);
g_return_val_if_fail (group_entry_id != NULL, NULL);
@@ -1038,14 +685,12 @@ create_group (EBookBackend *backend,
group = GDATA_ENTRY (gdata_contacts_group_new (NULL));
gdata_entry_set_title (group, category_name);
- g_debug ("Creating group %s", category_name);
/* Insert the new group */
- new_group = GDATA_ENTRY (
- gdata_contacts_service_insert_group (
- GDATA_CONTACTS_SERVICE (priv->service),
+ new_group = GDATA_ENTRY (gdata_contacts_service_insert_group (
+ GDATA_CONTACTS_SERVICE (bbgoogle->priv->service),
GDATA_CONTACTS_GROUP (group),
- NULL, error));
+ cancellable, error));
g_object_unref (group);
if (new_group == NULL)
@@ -1054,217 +699,60 @@ create_group (EBookBackend *backend,
/* Add the new group to the group mappings */
uid = g_strdup (gdata_entry_get_id (new_group));
- g_rec_mutex_lock (&priv->groups_lock);
- g_hash_table_replace (priv->groups_by_id, e_contact_sanitise_google_group_id (uid), g_strdup
(category_name));
- g_hash_table_replace (priv->groups_by_name, g_strdup (category_name),
e_contact_sanitise_google_group_id (uid));
- g_rec_mutex_unlock (&priv->groups_lock);
+ g_rec_mutex_lock (&bbgoogle->priv->groups_lock);
+ g_hash_table_replace (bbgoogle->priv->groups_by_id, e_contact_sanitise_google_group_id (uid),
g_strdup (category_name));
+ g_hash_table_replace (bbgoogle->priv->groups_by_name, g_strdup (category_name),
e_contact_sanitise_google_group_id (uid));
+ g_rec_mutex_unlock (&bbgoogle->priv->groups_lock);
g_object_unref (new_group);
/* Update the cache. */
- cache_update_group (backend, uid, category_name);
-
- g_debug ("...got UID %s", uid);
+ ebb_google_cache_update_group (bbgoogle, uid, category_name);
return uid;
}
-static gchar *
-_create_group (const gchar *category_name,
- gpointer user_data,
- GError **error)
-{
- return create_group (E_BOOK_BACKEND (user_data), category_name, error);
-}
-
-static void
-refresh_local_cache_cb (ESource *source,
- gpointer user_data)
-{
- EBookBackend *backend = user_data;
-
- g_debug ("Invoking cache refresh");
-
- /* The TRUE means the cache update will be run immediately
- after groups are updated */
- get_groups (backend, TRUE);
-}
-
-static void
-cache_refresh_if_needed (EBookBackend *backend)
-{
- EBookBackendGooglePrivate *priv;
- gboolean is_online;
-
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
- g_debug (G_STRFUNC);
-
- is_online = e_backend_get_online (E_BACKEND (backend));
-
- if (!is_online || !backend_is_authorized (backend)) {
- g_debug ("We are not connected to Google%s.", (!is_online) ? " (offline mode)" : "");
- return;
- }
-
- if (!priv->refresh_id) {
- /* Update the cache asynchronously */
- refresh_local_cache_cb (NULL, backend);
-
- priv->refresh_id = e_source_refresh_add_timeout (
- e_backend_get_source (E_BACKEND (backend)),
- NULL,
- refresh_local_cache_cb,
- backend,
- NULL);
- } else {
- g_rec_mutex_lock (&priv->groups_lock);
- if (g_hash_table_size (priv->system_groups_by_id) == 0) {
- g_rec_mutex_unlock (&priv->groups_lock);
- get_groups (backend, FALSE);
- } else {
- g_rec_mutex_unlock (&priv->groups_lock);
- }
- }
-
- return;
-}
-
-#if !GDATA_CHECK_VERSION(0,15,0)
-static void
-fallback_set_proxy_uri (EBookBackend *backend)
-{
- EBookBackendGooglePrivate *priv;
- GProxyResolver *proxy_resolver;
-
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
- proxy_resolver = e_book_backend_ref_proxy_resolver (backend);
-
- if (proxy_resolver != NULL) {
- SoupURI *proxy_uri = NULL;
- gchar **proxies;
-
- /* Don't worry about errors since this is a
- * fallback function. It works if it works. */
- proxies = g_proxy_resolver_lookup (
- proxy_resolver, URI_GET_CONTACTS, NULL, NULL);
-
- if (proxies != NULL && strcmp (proxies[0], "direct://") != 0) {
- proxy_uri = soup_uri_new (proxies[0]);
- g_strfreev (proxies);
- }
-
- if (proxy_uri != NULL) {
- gdata_service_set_proxy_uri (priv->service, proxy_uri);
- soup_uri_free (proxy_uri);
- }
-
- g_object_unref (proxy_resolver);
- }
-}
-#endif
-
static gboolean
-connect_without_password (EBookBackend *backend,
- GCancellable *cancellable,
- GError **error)
-{
- ESource *source;
- ESourceAuthentication *extension;
- EGDataOAuth2Authorizer *authorizer;
- gchar *method;
- gboolean is_oauth2_method;
- EBookBackendGooglePrivate *priv;
-
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
- source = e_backend_get_source (E_BACKEND (backend));
- extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
- method = e_source_authentication_dup_method (extension);
- is_oauth2_method = g_strcmp0 (method, "OAuth2") == 0;
- g_free (method);
-
- /* Make sure we have the GDataService configured
- * before requesting authorization. */
-
- if (priv->authorizer == NULL) {
- authorizer = e_gdata_oauth2_authorizer_new (source);
- priv->authorizer = GDATA_AUTHORIZER (authorizer);
- }
-
- if (priv->service == NULL) {
- GDataContactsService *contacts_service;
-
- contacts_service =
- gdata_contacts_service_new (priv->authorizer);
- priv->service = GDATA_SERVICE (contacts_service);
-
-#if GDATA_CHECK_VERSION(0,15,0)
- /* proxy-resolver was added in 0.15.0.
- * (https://bugzilla.gnome.org/709758) */
- e_binding_bind_property (
- backend, "proxy-resolver",
- priv->service, "proxy-resolver",
- G_BINDING_SYNC_CREATE);
-#else
- /* XXX The fallback approach doesn't listen for proxy
- * setting changes, but really how often do proxy
- * settings change? */
- fallback_set_proxy_uri (backend);
-#endif
- }
-
- /* If we're using OAuth tokens, then as far as the backend
- * is concerned it's always authorized. The GDataAuthorizer
- * will take care of everything in the background. */
- if (is_oauth2_method)
- return TRUE;
-
- /* Otherwise it's up to us to obtain an OAuth 2 token. */
- return FALSE;
-}
-
-typedef enum {
- LEAVE_PHOTO,
- ADD_PHOTO,
- REMOVE_PHOTO,
- UPDATE_PHOTO,
-} PhotoOperation;
-
-static PhotoOperation
-pick_photo_operation (EContact *old_contact,
- EContact *new_contact)
+ebb_google_photo_changed (EBookMetaBackend *meta_backend,
+ EContact *old_contact,
+ EContact *new_contact,
+ GCancellable *cancellable)
{
+ EContact *old_contact_copy = NULL;
EContactPhoto *old_photo;
EContactPhoto *new_photo;
- gboolean have_old_photo;
- gboolean have_new_photo;
- PhotoOperation photo_operation = LEAVE_PHOTO;
+ gboolean changed = FALSE;
old_photo = e_contact_get (old_contact, E_CONTACT_PHOTO);
new_photo = e_contact_get (new_contact, E_CONTACT_PHOTO);
- have_old_photo =
- (old_photo != NULL) &&
- (old_photo->type == E_CONTACT_PHOTO_TYPE_INLINED);
+ if (!old_photo && new_photo)
+ changed = TRUE;
+
+ if (old_photo && !new_photo)
+ changed = TRUE;
- have_new_photo =
- (new_photo != NULL) &&
- (new_photo->type == E_CONTACT_PHOTO_TYPE_INLINED);
+ /* old_photo comes from cache, thus it's always URI (to local file or elsewhere),
+ while the new_photo is to be saved, which is always inlined. */
+ if (!changed && old_photo && new_photo &&
+ old_photo->type == E_CONTACT_PHOTO_TYPE_URI &&
+ new_photo->type == E_CONTACT_PHOTO_TYPE_INLINED) {
+ e_contact_photo_free (old_photo);
+ old_photo = NULL;
- if (!have_old_photo && have_new_photo)
- photo_operation = ADD_PHOTO;
+ old_contact_copy = e_contact_duplicate (old_contact);
- if (have_old_photo && !have_new_photo)
- photo_operation = REMOVE_PHOTO;
+ if (e_book_meta_backend_inline_local_photos_sync (meta_backend, old_contact_copy,
cancellable, NULL))
+ old_photo = e_contact_get (old_contact_copy, E_CONTACT_PHOTO);
+ }
- if (have_old_photo && have_new_photo) {
+ if (old_photo && new_photo &&
+ old_photo->type == E_CONTACT_PHOTO_TYPE_INLINED &&
+ new_photo->type == E_CONTACT_PHOTO_TYPE_INLINED) {
guchar *old_data;
guchar *new_data;
gsize old_length;
gsize new_length;
- gboolean changed;
old_data = old_photo->data.inlined.data;
new_data = new_photo->data.inlined.data;
@@ -1275,36 +763,30 @@ pick_photo_operation (EContact *old_contact,
changed =
(old_length != new_length) ||
(memcmp (old_data, new_data, old_length) != 0);
-
- if (changed)
- photo_operation = UPDATE_PHOTO;
}
- if (old_photo != NULL)
- e_contact_photo_free (old_photo);
-
- if (new_photo != NULL)
- e_contact_photo_free (new_photo);
+ e_contact_photo_free (old_photo);
+ e_contact_photo_free (new_photo);
+ g_clear_object (&old_contact_copy);
- return photo_operation;
+ return changed;
}
static GDataEntry *
-update_contact_photo (GDataContactsContact *contact,
- GDataContactsService *service,
- EContactPhoto *photo,
- GCancellable *cancellable,
- GError **error)
+ebb_google_update_contact_photo_sync (GDataContactsContact *contact,
+ GDataContactsService *service,
+ EContactPhoto *photo,
+ GCancellable *cancellable,
+ GError **error)
{
GDataAuthorizationDomain *authorization_domain;
- GDataEntry *new_contact = NULL;
+ GDataEntry *gdata_contact = NULL;
const gchar *content_type;
const guint8 *photo_data;
gsize photo_length;
gboolean success;
- authorization_domain =
- gdata_contacts_service_get_primary_authorization_domain ();
+ authorization_domain = gdata_contacts_service_get_primary_authorization_domain ();
if (photo != NULL) {
photo_data = (guint8 *) photo->data.inlined.data;
@@ -1325,7 +807,7 @@ update_contact_photo (GDataContactsContact *contact,
if (success) {
/* Setting the photo changes the contact's ETag,
* so query for the contact to obtain its new ETag. */
- new_contact = gdata_service_query_single_entry (
+ gdata_contact = gdata_service_query_single_entry (
GDATA_SERVICE (service),
authorization_domain,
gdata_entry_get_id (GDATA_ENTRY (contact)),
@@ -1333,146 +815,231 @@ update_contact_photo (GDataContactsContact *contact,
cancellable, error);
}
- return new_contact;
+ return gdata_contact;
}
-static void
-google_cancel_all_operations (EBookBackend *backend)
+static gboolean
+ebb_google_save_contact_sync (EBookMetaBackend *meta_backend,
+ gboolean overwrite_existing,
+ EConflictResolution conflict_resolution,
+ /* const */ EContact *contact,
+ const gchar *extra,
+ gchar **out_new_uid,
+ gchar **out_new_extra,
+ GCancellable *cancellable,
+ GError **error)
{
- EBookBackendGooglePrivate *priv;
- GHashTableIter iter;
- gpointer opid_ptr;
- GCancellable *cancellable;
+ EBookBackendGoogle *bbgoogle;
+ EBookCache *book_cache;
+ GDataEntry *entry = NULL;
+ GDataContactsContact *gdata_contact;
+ EContact *cached_contact = NULL;
+ EContact *new_contact;
+ const gchar *uid;
+ EContactPhoto *photo;
+ gboolean photo_changed;
+ GError *local_error = NULL;
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_GOOGLE (meta_backend), FALSE);
+ g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+ g_return_val_if_fail (out_new_uid != NULL, FALSE);
+ g_return_val_if_fail (out_new_extra != NULL, FALSE);
- g_debug (G_STRFUNC);
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
+ g_return_val_if_fail (book_cache != NULL, FALSE);
- if (!priv->cancellables)
- return;
+ bbgoogle = E_BOOK_BACKEND_GOOGLE (meta_backend);
- /* Cancel all active operations */
- g_hash_table_iter_init (&iter, priv->cancellables);
- while (g_hash_table_iter_next (&iter, &opid_ptr, (gpointer *) &cancellable)) {
- g_cancellable_cancel (cancellable);
+ if (!overwrite_existing || !e_book_cache_get_contact (book_cache, e_contact_get_const (contact,
E_CONTACT_UID),
+ FALSE, &cached_contact, cancellable, NULL)) {
+ cached_contact = NULL;
}
-}
-static void
-e_book_backend_google_notify_online_cb (EBookBackend *backend,
- GParamSpec *pspec)
-{
- EBookBackendGooglePrivate *priv;
- ESource *source;
- gboolean is_online;
+ if (extra && *extra)
+ entry = GDATA_ENTRY (gdata_parsable_new_from_xml (GDATA_TYPE_CONTACTS_CONTACT, extra, -1,
NULL));
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
+ g_rec_mutex_lock (&bbgoogle->priv->groups_lock);
- g_debug (G_STRFUNC);
+ /* Ensure the system groups have been fetched. */
+ if (g_hash_table_size (bbgoogle->priv->system_groups_by_id) == 0)
+ ebb_google_get_groups_sync (bbgoogle, FALSE, cancellable, NULL);
+
+ if (overwrite_existing || entry) {
+ if (gdata_entry_update_from_e_contact (entry, contact, FALSE,
+ bbgoogle->priv->groups_by_name,
+ bbgoogle->priv->system_groups_by_id,
+ ebb_google_create_group_sync,
+ bbgoogle,
+ cancellable)) {
+ overwrite_existing = TRUE;
+ } else {
+ g_clear_object (&entry);
+ }
+ } else {
+ /* Build the GDataEntry from the vCard */
+ entry = gdata_entry_new_from_e_contact (
+ contact,
+ bbgoogle->priv->groups_by_name,
+ bbgoogle->priv->system_groups_by_id,
+ ebb_google_create_group_sync,
+ bbgoogle,
+ cancellable);
+ }
- is_online = e_backend_get_online (E_BACKEND (backend));
- source = e_backend_get_source (E_BACKEND (backend));
+ g_rec_mutex_unlock (&bbgoogle->priv->groups_lock);
- if (is_online && e_book_backend_is_opened (backend)) {
- e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTING);
+ photo_changed = cached_contact && ebb_google_photo_changed (meta_backend, cached_contact, contact,
cancellable);
- if (connect_without_password (backend, NULL, NULL)) {
- e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTED);
+ g_clear_object (&cached_contact);
+ g_clear_object (&book_cache);
- e_book_backend_set_writable (backend, TRUE);
- cache_refresh_if_needed (backend);
- } else {
- e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
+ if (!entry) {
+ g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_OTHER_ERROR, _("Object
to save is not a valid vCard")));
+ return FALSE;
+ }
- e_backend_schedule_credentials_required (E_BACKEND (backend),
E_SOURCE_CREDENTIALS_REASON_REQUIRED,
- NULL, 0, NULL, NULL, G_STRFUNC);
- }
+ if (overwrite_existing) {
+ gdata_contact = GDATA_CONTACTS_CONTACT (gdata_service_update_entry (
+ bbgoogle->priv->service,
+ gdata_contacts_service_get_primary_authorization_domain (),
+ entry, cancellable, &local_error));
} else {
- /* Going offline, so cancel all running operations */
- google_cancel_all_operations (backend);
+ gdata_contact = gdata_contacts_service_insert_contact (
+ GDATA_CONTACTS_SERVICE (bbgoogle->priv->service),
+ GDATA_CONTACTS_CONTACT (entry),
+ cancellable, &local_error);
+ }
+
+ photo = g_object_steal_data (G_OBJECT (entry), "photo");
- /* Mark the book as unwriteable if we're going offline,
- * but don't do the inverse when we go online;
- * e_book_backend_google_authenticate_user() will mark us
- * as writeable again once the user's authenticated again. */
- e_book_backend_set_writable (backend, FALSE);
+ g_object_unref (entry);
- if (e_source_get_connection_status (source) != E_SOURCE_CONNECTION_STATUS_DISCONNECTED)
- e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
+ if (!gdata_contact) {
+ ebb_google_data_book_error_from_gdata_error (error, local_error);
+ g_clear_error (&local_error);
+ e_contact_photo_free (photo);
- /* We can free our service. */
- g_clear_object (&priv->service);
+ return FALSE;
}
-}
-static void
-book_backend_google_dispose (GObject *object)
-{
- EBookBackendGooglePrivate *priv;
+ if (photo_changed) {
+ entry = ebb_google_update_contact_photo_sync (gdata_contact, GDATA_CONTACTS_SERVICE
(bbgoogle->priv->service), photo, cancellable, &local_error);
+ if (!entry) {
+ ebb_google_data_book_error_from_gdata_error (error, local_error);
+ g_clear_error (&local_error);
+ e_contact_photo_free (photo);
+ g_clear_object (&gdata_contact);
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (object);
+ return FALSE;
+ }
- g_debug (G_STRFUNC);
+ g_object_unref (gdata_contact);
+ gdata_contact = GDATA_CONTACTS_CONTACT (entry);
+ }
- /* Cancel all outstanding operations */
- google_cancel_all_operations (E_BOOK_BACKEND (object));
+ g_rec_mutex_lock (&bbgoogle->priv->groups_lock);
+ new_contact = e_contact_new_from_gdata_entry (GDATA_ENTRY (gdata_contact),
+ bbgoogle->priv->groups_by_id,
+ bbgoogle->priv->system_groups_by_entry_id);
+ g_rec_mutex_unlock (&bbgoogle->priv->groups_lock);
- if (priv->refresh_id > 0) {
- e_source_refresh_remove_timeout (
- e_backend_get_source (E_BACKEND (object)),
- priv->refresh_id);
- priv->refresh_id = 0;
+ if (!new_contact) {
+ g_object_unref (gdata_contact);
+ e_contact_photo_free (photo);
+ g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_OTHER_ERROR, _("Failed
to create contact from returned server data")));
+ return FALSE;
}
- g_clear_object (&priv->service);
- g_clear_object (&priv->authorizer);
- g_clear_object (&priv->cache);
+ e_contact_set (new_contact, E_CONTACT_PHOTO, photo);
+ e_vcard_util_set_x_attribute (E_VCARD (new_contact), E_GOOGLE_X_PHOTO_ETAG,
gdata_contacts_contact_get_photo_etag (gdata_contact));
- /* Chain up to parent's dispose() method. */
- G_OBJECT_CLASS (e_book_backend_google_parent_class)->dispose (object);
+ *out_new_extra = gdata_parsable_get_xml (GDATA_PARSABLE (gdata_contact));
+
+ g_object_unref (gdata_contact);
+
+ e_contact_photo_free (photo);
+
+ uid = e_contact_get_const (new_contact, E_CONTACT_UID);
+
+ if (!uid) {
+ g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_OTHER_ERROR, _("Server
returned contact without UID")));
+
+ g_object_unref (new_contact);
+ g_free (*out_new_extra);
+ *out_new_extra = NULL;
+
+ return FALSE;
+ }
+
+ if (bbgoogle->priv->preloaded) {
+ *out_new_uid = g_strdup (uid);
+ g_hash_table_insert (bbgoogle->priv->preloaded, g_strdup (uid), new_contact);
+ } else {
+ g_object_unref (new_contact);
+ }
+
+ return TRUE;
}
-static void
-book_backend_google_finalize (GObject *object)
+static gboolean
+ebb_google_remove_contact_sync (EBookMetaBackend *meta_backend,
+ EConflictResolution conflict_resolution,
+ const gchar *uid,
+ const gchar *extra,
+ const gchar *object,
+ GCancellable *cancellable,
+ GError **error)
{
- EBookBackendGooglePrivate *priv;
+ EBookBackendGoogle *bbgoogle;
+ GDataEntry *entry;
+ GError *local_error = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_GOOGLE (meta_backend), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (extra != NULL, FALSE);
+
+ entry = GDATA_ENTRY (gdata_parsable_new_from_xml (GDATA_TYPE_CONTACTS_CONTACT, extra, -1, NULL));
+ if (!entry) {
+ g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_INVALID_ARG, NULL));
+ return FALSE;
+ }
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (object);
+ bbgoogle = E_BOOK_BACKEND_GOOGLE (meta_backend);
- g_debug (G_STRFUNC);
+ if (!gdata_service_delete_entry (bbgoogle->priv->service,
+ gdata_contacts_service_get_primary_authorization_domain (), entry,
+ cancellable, &local_error)) {
+ ebb_google_data_book_error_from_gdata_error (error, local_error);
+ g_error_free (local_error);
+ g_object_unref (entry);
- if (priv->cancellables) {
- g_hash_table_destroy (priv->groups_by_id);
- g_hash_table_destroy (priv->groups_by_name);
- g_hash_table_destroy (priv->system_groups_by_entry_id);
- g_hash_table_destroy (priv->system_groups_by_id);
- g_hash_table_destroy (priv->cancellables);
+ return FALSE;
}
- g_mutex_clear (&priv->cache_lock);
- g_rec_mutex_clear (&priv->groups_lock);
+ g_object_unref (entry);
- /* Chain up to parent's finalize() method. */
- G_OBJECT_CLASS (e_book_backend_google_parent_class)->finalize (object);
+ return TRUE;
}
static gchar *
-book_backend_google_get_backend_property (EBookBackend *backend,
- const gchar *prop_name)
+ebb_google_get_backend_property (EBookBackend *book_backend,
+ const gchar *prop_name)
{
- g_debug (G_STRFUNC);
-
g_return_val_if_fail (prop_name != NULL, NULL);
if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
- return g_strdup ("net,do-initial-query,contact-lists,refresh-supported");
+ return g_strjoin (",",
+ "net",
+ "do-initial-query",
+ "contact-lists",
+ e_book_meta_backend_get_capabilities (E_BOOK_META_BACKEND (book_backend)),
+ NULL);
} else if (g_str_equal (prop_name, BOOK_BACKEND_PROPERTY_REQUIRED_FIELDS)) {
return g_strdup ("");
} else if (g_str_equal (prop_name, BOOK_BACKEND_PROPERTY_SUPPORTED_FIELDS)) {
- return g_strjoin (
- ",",
+ return g_strjoin (",",
e_contact_field_name (E_CONTACT_UID),
e_contact_field_name (E_CONTACT_REV),
e_contact_field_name (E_CONTACT_FULL_NAME),
@@ -1600,774 +1167,99 @@ book_backend_google_get_backend_property (EBookBackend *backend,
e_contact_field_name (E_CONTACT_NOTE),
e_contact_field_name (E_CONTACT_PHOTO),
e_contact_field_name (E_CONTACT_CATEGORIES),
-#if defined(GDATA_CHECK_VERSION)
-#if GDATA_CHECK_VERSION(0, 11, 0)
e_contact_field_name (E_CONTACT_CATEGORY_LIST),
e_contact_field_name (E_CONTACT_FILE_AS),
-#else
- e_contact_field_name (E_CONTACT_CATEGORY_LIST),
-#endif
-#else
- e_contact_field_name (E_CONTACT_CATEGORY_LIST),
-#endif
e_contact_field_name (E_CONTACT_NICKNAME),
NULL);
}
- /* Chain up to parent's get_backend_property() method. */
- return E_BOOK_BACKEND_CLASS (e_book_backend_google_parent_class)->
- get_backend_property (backend, prop_name);
+ /* Chain up to parent's method. */
+ return E_BOOK_BACKEND_CLASS (e_book_backend_google_parent_class)->get_backend_property (book_backend,
prop_name);
}
-static gboolean
-book_backend_google_open_sync (EBookBackend *backend,
- GCancellable *cancellable,
- GError **error)
-{
- EBookBackendGooglePrivate *priv;
- gboolean is_online;
- gboolean success = TRUE;
-
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
- g_debug (G_STRFUNC);
-
- if (priv->cancellables && backend_is_authorized (backend))
- return TRUE;
-
- /* Set up our object */
- if (priv->cancellables == NULL) {
- priv->groups_by_id = g_hash_table_new_full (
- (GHashFunc) g_str_hash,
- (GEqualFunc) g_str_equal,
- (GDestroyNotify) g_free,
- (GDestroyNotify) g_free);
- priv->groups_by_name = g_hash_table_new_full (
- (GHashFunc) g_str_hash,
- (GEqualFunc) g_str_equal,
- (GDestroyNotify) g_free,
- (GDestroyNotify) g_free);
- priv->system_groups_by_id = g_hash_table_new_full (
- (GHashFunc) g_str_hash,
- (GEqualFunc) g_str_equal,
- (GDestroyNotify) g_free,
- (GDestroyNotify) g_free);
- /* shares keys and values with system_groups_by_id */
- priv->system_groups_by_entry_id = g_hash_table_new (
- (GHashFunc) g_str_hash,
- (GEqualFunc) g_str_equal);
- priv->cancellables = g_hash_table_new_full (
- (GHashFunc) g_direct_hash,
- (GEqualFunc) g_direct_equal,
- (GDestroyNotify) NULL,
- (GDestroyNotify) g_object_unref);
- }
-
- cache_init (backend);
-
- /* Set up ready to be interacted with */
- is_online = e_backend_get_online (E_BACKEND (backend));
- e_book_backend_set_writable (backend, FALSE);
-
- if (is_online) {
- ESource *source = e_backend_get_source (E_BACKEND (backend));
-
- e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTING);
-
- success = connect_without_password (backend, cancellable, error);
- if (success) {
- GError *local_error = NULL;
-
- /* Refresh the authorizer. This may block. */
- success = gdata_authorizer_refresh_authorization (
- priv->authorizer, cancellable, &local_error);
-
- if (success) {
- e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTED);
- } else {
- GError *local_error2 = NULL;
-
- e_source_set_connection_status (source,
E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
-
- if (local_error && !e_backend_credentials_required_sync (E_BACKEND (backend),
E_SOURCE_CREDENTIALS_REASON_ERROR,
- NULL, 0, local_error, cancellable, &local_error2)) {
- g_warning ("%s: Failed to call credentials required: %s", G_STRFUNC,
local_error2 ? local_error2->message : "Unknown error");
- }
-
- g_clear_error (&local_error2);
-
- if (local_error)
- g_propagate_error (error, local_error);
- }
- } else {
- GError *local_error = NULL;
-
- e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
-
- if (!e_backend_credentials_required_sync (E_BACKEND (backend),
E_SOURCE_CREDENTIALS_REASON_REQUIRED,
- NULL, 0, NULL, cancellable, &local_error)) {
- g_warning ("%s: Failed to call credentials required: %s", G_STRFUNC,
local_error ? local_error->message : "Unknown error");
- }
-
- g_clear_error (&local_error);
- }
- }
-
- if (is_online && backend_is_authorized (backend)) {
- e_book_backend_set_writable (backend, TRUE);
- cache_refresh_if_needed (backend);
- }
-
- return success;
-}
-
-static gboolean
-book_backend_google_create_contacts_sync (EBookBackend *backend,
- const gchar * const *vcards,
- GQueue *out_contacts,
- GCancellable *cancellable,
- GError **error)
-{
- EBookBackendGooglePrivate *priv;
- EContactPhoto *photo = NULL;
- EContact *contact;
- GDataEntry *entry;
- GDataContactsContact *new_contact;
- gchar *xml;
- gboolean success = TRUE;
- GError *gdata_error = NULL;
-
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
- /* We make the assumption that the vCard list we're passed is always
- * exactly one element long, since we haven't specified "bulk-adds"
- * in our static capability list. This simplifies the logic. */
- if (g_strv_length ((gchar **) vcards) > 1) {
- g_set_error_literal (
- error, E_CLIENT_ERROR,
- E_CLIENT_ERROR_NOT_SUPPORTED,
- _("The backend does not support bulk additions"));
- return FALSE;
- }
-
- g_debug (G_STRFUNC);
-
- g_debug ("Creating: %s", vcards[0]);
-
- if (!e_backend_get_online (E_BACKEND (backend))) {
- g_set_error_literal (
- error, E_CLIENT_ERROR,
- E_CLIENT_ERROR_OFFLINE_UNAVAILABLE,
- e_client_error_to_string (
- E_CLIENT_ERROR_OFFLINE_UNAVAILABLE));
- return FALSE;
- }
-
- g_warn_if_fail (backend_is_authorized (backend));
-
- g_rec_mutex_lock (&priv->groups_lock);
-
- /* Ensure the system groups have been fetched. */
- if (g_hash_table_size (priv->system_groups_by_id) == 0)
- get_groups_sync (backend, cancellable, NULL);
-
- /* Build the GDataEntry from the vCard */
- contact = e_contact_new_from_vcard (vcards[0]);
- entry = gdata_entry_new_from_e_contact (
- contact,
- priv->groups_by_name,
- priv->system_groups_by_id,
- _create_group, backend);
- g_object_unref (contact);
-
- g_rec_mutex_unlock (&priv->groups_lock);
-
- /* Debug XML output */
- xml = gdata_parsable_get_xml (GDATA_PARSABLE (entry));
- g_debug ("new entry with xml: %s", xml);
- g_free (xml);
-
- new_contact = gdata_contacts_service_insert_contact (
- GDATA_CONTACTS_SERVICE (priv->service),
- GDATA_CONTACTS_CONTACT (entry),
- cancellable, &gdata_error);
-
- if (new_contact == NULL) {
- success = FALSE;
- goto exit;
- }
-
- /* Add a photo for the new contact, if appropriate. This has to
- * be done before we finish the contact creation operation so we
- * can update the EContact with the photo data and ETag. */
- photo = g_object_steal_data (G_OBJECT (entry), "photo");
- if (photo != NULL) {
- GDataEntry *updated_entry;
- gchar *xml;
-
- updated_entry = update_contact_photo (
- new_contact,
- GDATA_CONTACTS_SERVICE (priv->service),
- photo, cancellable, &gdata_error);
-
- /* Sanity check. */
- g_return_val_if_fail (
- ((updated_entry != NULL) && (gdata_error == NULL)) ||
- ((updated_entry == NULL) && (gdata_error != NULL)),
- FALSE);
-
- if (gdata_error != NULL) {
- g_debug (
- "Uploading contact photo "
- "for '%s' failed: %s",
- gdata_entry_get_id (GDATA_ENTRY (new_contact)),
- gdata_error->message);
- success = FALSE;
- goto exit;
- }
-
- /* Output debug XML */
- xml = gdata_parsable_get_xml (
- GDATA_PARSABLE (updated_entry));
- g_debug ("After re-querying:\n%s", xml);
- g_free (xml);
-
- g_object_unref (new_contact);
- new_contact = GDATA_CONTACTS_CONTACT (updated_entry);
-
- /* Store the photo on the final GDataContactsContact
- * object so it makes it into the cache. */
- g_object_set_data_full (
- G_OBJECT (new_contact), "photo", photo,
- (GDestroyNotify) e_contact_photo_free);
- photo = NULL;
- }
-
- contact = cache_add_contact (backend, GDATA_ENTRY (new_contact));
- if (contact) {
- g_queue_push_tail (out_contacts, g_object_ref (contact));
- g_object_unref (contact);
- }
-
-exit:
- g_clear_object (&entry);
- g_clear_object (&new_contact);
-
- if (photo != NULL)
- e_contact_photo_free (photo);
-
- if (gdata_error != NULL) {
- g_warn_if_fail (success == FALSE);
- data_book_error_from_gdata_error (error, gdata_error);
- g_error_free (gdata_error);
- } else {
- e_backend_ensure_source_status_connected (E_BACKEND (backend));
- }
-
- return success;
-}
-
-static gboolean
-book_backend_google_modify_contacts_sync (EBookBackend *backend,
- const gchar * const *vcards,
- GQueue *out_contacts,
- GCancellable *cancellable,
- GError **error)
-{
- EBookBackendGooglePrivate *priv;
- GDataAuthorizationDomain *authorization_domain;
- EContact *contact, *cached_contact;
- PhotoOperation photo_operation;
- EContactPhoto *photo;
- GDataEntry *entry = NULL;
- GDataEntry *new_contact;
- const gchar *uid;
- gboolean success = TRUE;
- gchar *xml;
- GError *gdata_error = NULL;
-
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
- authorization_domain =
- gdata_contacts_service_get_primary_authorization_domain ();
-
- g_debug (G_STRFUNC);
-
- g_debug ("Updating: %s", vcards[0]);
-
- /* We make the assumption that the vCard list we're passed is
- * always exactly one element long, since we haven't specified
- * "bulk-modifies" in our static capability list. This is because
- * there is no clean way to roll back changes in case of an error. */
- if (g_strv_length ((gchar **) vcards) > 1) {
- g_set_error_literal (
- error, E_CLIENT_ERROR,
- E_CLIENT_ERROR_NOT_SUPPORTED,
- _("The backend does not support bulk modifications"));
- return FALSE;
- }
-
- if (!e_backend_get_online (E_BACKEND (backend))) {
- g_set_error_literal (
- error, E_CLIENT_ERROR,
- E_CLIENT_ERROR_OFFLINE_UNAVAILABLE,
- e_client_error_to_string (
- E_CLIENT_ERROR_OFFLINE_UNAVAILABLE));
- return FALSE;
- }
-
- g_warn_if_fail (backend_is_authorized (backend));
-
- /* Get the new contact and its UID. */
- contact = e_contact_new_from_vcard (vcards[0]);
- uid = e_contact_get (contact, E_CONTACT_UID);
-
- /* Get the old cached contact with the same UID,
- * and its associated GDataEntry. */
- cached_contact = cache_get_contact (backend, uid, &entry);
-
- if (cached_contact == NULL) {
- g_debug (
- "Modifying contact failed: "
- "Contact with uid %s not found in cache.", uid);
- g_object_unref (contact);
-
- g_set_error_literal (
- error, E_BOOK_CLIENT_ERROR,
- E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND,
- e_book_client_error_to_string (
- E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND));
- return FALSE;
- }
-
- g_rec_mutex_lock (&priv->groups_lock);
-
- /* Ensure the system groups have been fetched. */
- if (g_hash_table_size (priv->system_groups_by_id) == 0)
- get_groups_sync (backend, cancellable, NULL);
-
- /* Update the old GDataEntry from the new contact. */
- gdata_entry_update_from_e_contact (
- entry, contact, FALSE,
- priv->groups_by_name,
- priv->system_groups_by_id,
- _create_group, backend);
-
- g_rec_mutex_unlock (&priv->groups_lock);
-
- /* Output debug XML */
- xml = gdata_parsable_get_xml (GDATA_PARSABLE (entry));
- g_debug ("Before:\n%s", xml);
- g_free (xml);
-
- photo = g_object_steal_data (G_OBJECT (entry), "photo");
-
- /* Update the contact's photo. We can't rely on the ETags at this
- * point, as the ETag in @contact may be out of sync with the photo
- * in the EContact (since the photo may have been updated).
- * Consequently, after updating @entry its ETag may also be out of
- * sync with its attached photo data. This means that we have to
- * detect whether the photo has changed by comparing the photo data
- * itself, which is guaranteed to be in sync between @contact and
- * @entry. */
- photo_operation = pick_photo_operation (cached_contact, contact);
-
- /* Sanity check the photo operation. */
- switch (photo_operation) {
- case LEAVE_PHOTO:
- break;
-
- case ADD_PHOTO:
- case UPDATE_PHOTO:
- g_return_val_if_fail (photo != NULL, FALSE);
- break;
-
- case REMOVE_PHOTO:
- g_return_val_if_fail (photo == NULL, FALSE);
- break;
-
- default:
- g_return_val_if_reached (FALSE);
- }
-
- g_clear_object (&cached_contact);
- g_clear_object (&contact);
-
- new_contact = gdata_service_update_entry (
- priv->service,
- authorization_domain,
- entry,
- cancellable, &gdata_error);
-
- if (new_contact == NULL) {
- g_debug (
- "Modifying contact failed: %s",
- gdata_error->message);
- success = FALSE;
- goto exit;
- }
-
- /* Output debug XML */
- xml = gdata_parsable_get_xml (GDATA_PARSABLE (new_contact));
- g_debug ("After:\n%s", xml);
- g_free (xml);
-
- /* Add a photo for the new contact, if appropriate. This has to be
- * done before we respond to the contact creation operation so that
- * we can update the EContact with the photo data and ETag. */
- if (photo_operation != LEAVE_PHOTO) {
- GDataEntry *updated_entry;
-
- updated_entry = update_contact_photo (
- GDATA_CONTACTS_CONTACT (new_contact),
- GDATA_CONTACTS_SERVICE (priv->service),
- photo, cancellable, &gdata_error);
-
- /* Sanity check. */
- g_return_val_if_fail (
- ((updated_entry != NULL) && (gdata_error == NULL)) ||
- ((updated_entry == NULL) && (gdata_error != NULL)),
- FALSE);
-
- if (gdata_error != NULL) {
- g_debug (
- "Uploading contact photo "
- "for '%s' failed: %s",
- gdata_entry_get_id (new_contact),
- gdata_error->message);
- success = FALSE;
- goto exit;
- }
-
- /* Output debug XML */
- xml = gdata_parsable_get_xml (
- GDATA_PARSABLE (updated_entry));
- g_debug ("After re-querying:\n%s", xml);
- g_free (xml);
-
- g_object_unref (new_contact);
- new_contact = updated_entry;
- }
-
- /* Store the photo on the final GDataEntry
- * object so it makes it to the cache. */
- if (photo != NULL) {
- g_object_set_data_full (
- G_OBJECT (new_contact), "photo", photo,
- (GDestroyNotify) e_contact_photo_free);
- photo = NULL;
- } else {
- g_object_set_data (
- G_OBJECT (new_contact), "photo", NULL);
- }
-
- contact = cache_add_contact (backend, new_contact);
- if (contact) {
- g_queue_push_tail (out_contacts, g_object_ref (contact));
- g_object_unref (contact);
- }
-
-exit:
- g_clear_object (&entry);
- g_clear_object (&new_contact);
-
- if (photo != NULL)
- e_contact_photo_free (photo);
-
- if (gdata_error != NULL) {
- g_warn_if_fail (success == FALSE);
- data_book_error_from_gdata_error (error, gdata_error);
- g_error_free (gdata_error);
- } else {
- e_backend_ensure_source_status_connected (E_BACKEND (backend));
- }
-
- return success;
-}
-
-static gboolean
-book_backend_google_remove_contacts_sync (EBookBackend *backend,
- const gchar *const *uids,
- GCancellable *cancellable,
- GError **error)
-{
- EBookBackendGooglePrivate *priv;
- GDataAuthorizationDomain *authorization_domain;
- GDataEntry *entry = NULL;
- EContact *cached_contact;
- gboolean success;
- GError *gdata_error = NULL;
-
- priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
- authorization_domain =
- gdata_contacts_service_get_primary_authorization_domain ();
-
- g_debug (G_STRFUNC);
-
- /* We make the assumption that the ID list we're passed is always
- * exactly one element long, since we haven't specified "bulk-removes"
- * in our static capability list. This simplifies the logic. */
- if (g_strv_length ((gchar **) uids) > 1) {
- g_set_error_literal (
- error, E_CLIENT_ERROR,
- E_CLIENT_ERROR_NOT_SUPPORTED,
- _("The backend does not support bulk removals"));
- return FALSE;
- }
-
- if (!e_backend_get_online (E_BACKEND (backend))) {
- g_set_error_literal (
- error, E_CLIENT_ERROR,
- E_CLIENT_ERROR_OFFLINE_UNAVAILABLE,
- e_client_error_to_string (
- E_CLIENT_ERROR_OFFLINE_UNAVAILABLE));
- return FALSE;
- }
-
- g_warn_if_fail (backend_is_authorized (backend));
-
- /* Get the contact and associated GDataEntry from the cache */
- cached_contact = cache_get_contact (backend, uids[0], &entry);
-
- if (cached_contact == NULL) {
- g_set_error_literal (
- error, E_BOOK_CLIENT_ERROR,
- E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND,
- e_book_client_error_to_string (
- E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND));
- return FALSE;
- }
-
- g_object_unref (cached_contact);
-
- /* Remove the contact from the cache */
- cache_remove_contact (backend, uids[0]);
-
- success = gdata_service_delete_entry (
- priv->service,
- authorization_domain, entry,
- cancellable, &gdata_error);
-
- g_object_unref (entry);
-
- if (gdata_error != NULL) {
- g_warn_if_fail (success == FALSE);
- data_book_error_from_gdata_error (error, gdata_error);
- g_error_free (gdata_error);
- } else {
- e_backend_ensure_source_status_connected (E_BACKEND (backend));
- }
-
- return success;
-}
-
-static EContact *
-book_backend_google_get_contact_sync (EBookBackend *backend,
- const gchar *uid,
- GCancellable *cancellable,
- GError **error)
-{
- EContact *contact;
-
- g_debug (G_STRFUNC);
-
- /* Get the contact */
- contact = cache_get_contact (backend, uid, NULL);
- if (contact == NULL) {
- g_set_error_literal (
- error, E_BOOK_CLIENT_ERROR,
- E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND,
- e_book_client_error_to_string (
- E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND));
- }
-
- return contact;
-}
-
-static gboolean
-book_backend_google_get_contact_list_sync (EBookBackend *backend,
- const gchar *query,
- GQueue *out_contacts,
- GCancellable *cancellable,
- GError **error)
+static void
+ebb_google_constructed (GObject *object)
{
- EBookBackendSExp *sexp;
- GQueue queue = G_QUEUE_INIT;
-
- g_debug (G_STRFUNC);
-
- sexp = e_book_backend_sexp_new (query);
-
- /* Get all contacts */
- cache_get_contacts (backend, &queue);
-
- while (!g_queue_is_empty (&queue)) {
- EContact *contact;
-
- contact = g_queue_pop_head (&queue);
+ EBookBackendGoogle *bbgoogle = E_BOOK_BACKEND_GOOGLE (object);
- /* If the search expression matches the contact,
- * include it in the search results. */
- if (e_book_backend_sexp_match_contact (sexp, contact)) {
- g_object_ref (contact);
- g_queue_push_tail (out_contacts, contact);
- }
-
- g_object_unref (contact);
- }
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_book_backend_google_parent_class)->constructed (object);
- g_object_unref (sexp);
-
- return TRUE;
+ /* Set it as always writable, regardless online/offline state */
+ e_book_backend_set_writable (E_BOOK_BACKEND (bbgoogle), TRUE);
}
static void
-book_backend_google_start_view (EBookBackend *backend,
- EDataBookView *bookview)
+ebb_google_dispose (GObject *object)
{
- GQueue queue = G_QUEUE_INIT;
- GError *error = NULL;
-
- g_return_if_fail (E_IS_BOOK_BACKEND_GOOGLE (backend));
- g_return_if_fail (E_IS_DATA_BOOK_VIEW (bookview));
+ EBookBackendGoogle *bbgoogle = E_BOOK_BACKEND_GOOGLE (object);
- g_debug (G_STRFUNC);
-
- e_data_book_view_notify_progress (bookview, -1, _("Loading…"));
-
- /* Ensure that we're ready to support a view */
- cache_refresh_if_needed (backend);
-
- /* Get the contacts */
- cache_get_contacts (backend, &queue);
- g_debug (
- "%d contacts found in cache",
- g_queue_get_length (&queue));
-
- /* Notify the view that all the contacts have changed (i.e. been added) */
- while (!g_queue_is_empty (&queue)) {
- EContact *contact;
+ g_clear_object (&bbgoogle->priv->service);
+ g_clear_object (&bbgoogle->priv->authorizer);
- contact = g_queue_pop_head (&queue);
- e_data_book_view_notify_update (bookview, contact);
- g_object_unref (contact);
- }
+ g_hash_table_destroy (bbgoogle->priv->preloaded);
+ bbgoogle->priv->preloaded = NULL;
- /* This function frees the GError passed to it. */
- e_data_book_view_notify_complete (bookview, error);
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_book_backend_google_parent_class)->dispose (object);
}
static void
-book_backend_google_stop_view (EBookBackend *backend,
- EDataBookView *bookview)
+ebb_google_finalize (GObject *object)
{
- g_debug (G_STRFUNC);
-}
+ EBookBackendGoogle *bbgoogle = E_BOOK_BACKEND_GOOGLE (object);
-static gboolean
-book_backend_google_refresh_sync (EBookBackend *backend,
- GCancellable *cancellable,
- GError **error)
-{
- g_return_val_if_fail (E_IS_BOOK_BACKEND_GOOGLE (backend), FALSE);
+ g_clear_pointer (&bbgoogle->priv->groups_by_id, (GDestroyNotify) g_hash_table_destroy);
+ g_clear_pointer (&bbgoogle->priv->groups_by_id, (GDestroyNotify) g_hash_table_destroy);
+ g_clear_pointer (&bbgoogle->priv->groups_by_name, (GDestroyNotify) g_hash_table_destroy);
+ g_clear_pointer (&bbgoogle->priv->system_groups_by_entry_id, (GDestroyNotify) g_hash_table_destroy);
+ g_clear_pointer (&bbgoogle->priv->system_groups_by_id, (GDestroyNotify) g_hash_table_destroy);
- /* get only changes, it's not needed to redownload whole cache */
- get_new_contacts (backend);
+ g_rec_mutex_clear (&bbgoogle->priv->groups_lock);
- return TRUE;
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_book_backend_google_parent_class)->finalize (object);
}
-static ESourceAuthenticationResult
-book_backend_google_authenticate_sync (EBackend *backend,
- const ENamedParameters *credentials,
- gchar **out_certificate_pem,
- GTlsCertificateFlags *out_certificate_errors,
- GCancellable *cancellable,
- GError **error)
+static void
+e_book_backend_google_init (EBookBackendGoogle *bbgoogle)
{
- EBookBackend *book_backend = E_BOOK_BACKEND (backend);
- EBookBackendGooglePrivate *priv;
- ESourceAuthenticationResult result;
- EGDataOAuth2Authorizer *authorizer;
- GError *local_error = NULL;
+ bbgoogle->priv = G_TYPE_INSTANCE_GET_PRIVATE (bbgoogle, E_TYPE_BOOK_BACKEND_GOOGLE,
EBookBackendGooglePrivate);
+ bbgoogle->priv->preloaded = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
- g_debug (G_STRFUNC);
+ g_rec_mutex_init (&bbgoogle->priv->groups_lock);
- /* We should not have gotten here if we're offline. */
- g_return_val_if_fail (e_backend_get_online (backend), E_SOURCE_AUTHENTICATION_ERROR);
-
- priv = E_BOOK_BACKEND_GOOGLE (backend)->priv;
-
- g_return_val_if_fail (E_IS_GDATA_OAUTH2_AUTHORIZER (priv->authorizer), E_SOURCE_AUTHENTICATION_ERROR);
-
- authorizer = E_GDATA_OAUTH2_AUTHORIZER (priv->authorizer);
- e_gdata_oauth2_authorizer_set_credentials (authorizer, credentials);
-
- get_groups_sync (E_BOOK_BACKEND (backend), cancellable, &local_error);
-
- if (local_error == NULL) {
- result = E_SOURCE_AUTHENTICATION_ACCEPTED;
-
- if (backend_is_authorized (book_backend)) {
- e_book_backend_set_writable (book_backend, TRUE);
- cache_refresh_if_needed (book_backend);
- }
- } else if (g_error_matches (local_error, GDATA_SERVICE_ERROR,
GDATA_SERVICE_ERROR_AUTHENTICATION_REQUIRED)) {
- if (!e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_PASSWORD))
- result = E_SOURCE_AUTHENTICATION_REQUIRED;
- else
- result = E_SOURCE_AUTHENTICATION_REJECTED;
- g_clear_error (&local_error);
- } else {
- g_propagate_error (error, local_error);
- result = E_SOURCE_AUTHENTICATION_ERROR;
- }
-
- return result;
+ bbgoogle->priv->groups_by_id = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+ bbgoogle->priv->groups_by_name = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+ bbgoogle->priv->system_groups_by_id = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+ /* shares keys and values with system_groups_by_id */
+ bbgoogle->priv->system_groups_by_entry_id = g_hash_table_new (g_str_hash, g_str_equal);
}
static void
-e_book_backend_google_class_init (EBookBackendGoogleClass *class)
+e_book_backend_google_class_init (EBookBackendGoogleClass *klass)
{
GObjectClass *object_class;
- EBackendClass *backend_class;
EBookBackendClass *book_backend_class;
-
- g_type_class_add_private (class, sizeof (EBookBackendGooglePrivate));
-
- object_class = G_OBJECT_CLASS (class);
- object_class->dispose = book_backend_google_dispose;
- object_class->finalize = book_backend_google_finalize;
-
- backend_class = E_BACKEND_CLASS (class);
- backend_class->authenticate_sync = book_backend_google_authenticate_sync;
-
- book_backend_class = E_BOOK_BACKEND_CLASS (class);
- book_backend_class->get_backend_property = book_backend_google_get_backend_property;
- book_backend_class->open_sync = book_backend_google_open_sync;
- book_backend_class->create_contacts_sync = book_backend_google_create_contacts_sync;
- book_backend_class->modify_contacts_sync = book_backend_google_modify_contacts_sync;
- book_backend_class->remove_contacts_sync = book_backend_google_remove_contacts_sync;
- book_backend_class->get_contact_sync = book_backend_google_get_contact_sync;
- book_backend_class->get_contact_list_sync = book_backend_google_get_contact_list_sync;
- book_backend_class->start_view = book_backend_google_start_view;
- book_backend_class->stop_view = book_backend_google_stop_view;
- book_backend_class->refresh_sync = book_backend_google_refresh_sync;
-}
-
-static void
-e_book_backend_google_init (EBookBackendGoogle *backend)
-{
- g_debug (G_STRFUNC);
-
- backend->priv = E_BOOK_BACKEND_GOOGLE_GET_PRIVATE (backend);
-
- g_mutex_init (&backend->priv->cache_lock);
- g_rec_mutex_init (&backend->priv->groups_lock);
-
- g_signal_connect (
- backend, "notify::online",
- G_CALLBACK (e_book_backend_google_notify_online_cb), NULL);
+ EBookMetaBackendClass *book_meta_backend_class;
+
+ g_type_class_add_private (klass, sizeof (EBookBackendGooglePrivate));
+
+ book_meta_backend_class = E_BOOK_META_BACKEND_CLASS (klass);
+ book_meta_backend_class->backend_module_filename = "libebookbackendgoogle.so";
+ book_meta_backend_class->backend_factory_type_name = "EBookBackendGoogleFactory";
+ book_meta_backend_class->connect_sync = ebb_google_connect_sync;
+ book_meta_backend_class->disconnect_sync = ebb_google_disconnect_sync;
+ book_meta_backend_class->get_changes_sync = ebb_google_get_changes_sync;
+ book_meta_backend_class->load_contact_sync = ebb_google_load_contact_sync;
+ book_meta_backend_class->save_contact_sync = ebb_google_save_contact_sync;
+ book_meta_backend_class->remove_contact_sync = ebb_google_remove_contact_sync;
+
+ book_backend_class = E_BOOK_BACKEND_CLASS (klass);
+ book_backend_class->get_backend_property = ebb_google_get_backend_property;
+
+ object_class = G_OBJECT_CLASS (klass);
+ object_class->constructed = ebb_google_constructed;
+ object_class->dispose = ebb_google_dispose;
+ object_class->finalize = ebb_google_finalize;
}
-
diff --git a/src/addressbook/backends/google/e-book-backend-google.h
b/src/addressbook/backends/google/e-book-backend-google.h
index c910efd..fcbf1de 100644
--- a/src/addressbook/backends/google/e-book-backend-google.h
+++ b/src/addressbook/backends/google/e-book-backend-google.h
@@ -48,12 +48,12 @@ typedef struct _EBookBackendGoogleClass EBookBackendGoogleClass;
typedef struct _EBookBackendGooglePrivate EBookBackendGooglePrivate;
struct _EBookBackendGoogle {
- EBookBackend parent_object;
+ EBookMetaBackend parent_object;
EBookBackendGooglePrivate *priv;
};
struct _EBookBackendGoogleClass {
- EBookBackendClass parent_class;
+ EBookMetaBackendClass parent_class;
};
GType e_book_backend_google_get_type (void);
diff --git a/src/addressbook/backends/google/e-book-google-utils.c
b/src/addressbook/backends/google/e-book-google-utils.c
index 4157894..027eb25 100644
--- a/src/addressbook/backends/google/e-book-google-utils.c
+++ b/src/addressbook/backends/google/e-book-google-utils.c
@@ -68,10 +68,11 @@ static gboolean is_known_google_im_protocol (const gchar *protocol);
GDataEntry *
gdata_entry_new_from_e_contact (EContact *contact,
- GHashTable *groups_by_name,
- GHashTable *system_groups_by_id,
- EContactGoogleCreateGroupFunc create_group,
- gpointer create_group_user_data)
+ GHashTable *groups_by_name,
+ GHashTable *system_groups_by_id,
+ EContactGoogleCreateGroupFunc create_group,
+ EBookBackendGoogle *bbgoogle,
+ GCancellable *cancellable)
{
GDataEntry *entry;
@@ -83,7 +84,7 @@ gdata_entry_new_from_e_contact (EContact *contact,
entry = GDATA_ENTRY (gdata_contacts_contact_new (NULL));
- if (gdata_entry_update_from_e_contact (entry, contact, TRUE, groups_by_name, system_groups_by_id,
create_group, create_group_user_data))
+ if (gdata_entry_update_from_e_contact (entry, contact, TRUE, groups_by_name, system_groups_by_id,
create_group, bbgoogle, cancellable))
return entry;
g_object_unref (entry);
@@ -117,12 +118,13 @@ remove_anniversary (GDataContactsContact *contact)
gboolean
gdata_entry_update_from_e_contact (GDataEntry *entry,
- EContact *contact,
- gboolean ensure_personal_group,
- GHashTable *groups_by_name,
- GHashTable *system_groups_by_id,
- EContactGoogleCreateGroupFunc create_group,
- gpointer create_group_user_data)
+ EContact *contact,
+ gboolean ensure_personal_group,
+ GHashTable *groups_by_name,
+ GHashTable *system_groups_by_id,
+ EContactGoogleCreateGroupFunc create_group,
+ EBookBackendGoogle *bbgoogle,
+ GCancellable *cancellable)
{
GList *attributes, *iter, *category_names, *extended_property_names;
EContactName *name_struct = NULL;
@@ -228,6 +230,7 @@ gdata_entry_update_from_e_contact (GDataEntry *entry,
name = e_vcard_attribute_get_name (attr);
if (0 == g_ascii_strcasecmp (name, EVC_UID) ||
+ 0 == g_ascii_strcasecmp (name, EVC_REV) ||
0 == g_ascii_strcasecmp (name, EVC_N) ||
0 == g_ascii_strcasecmp (name, EVC_FN) ||
0 == g_ascii_strcasecmp (name, EVC_LABEL) ||
@@ -239,7 +242,8 @@ gdata_entry_update_from_e_contact (GDataEntry *entry,
0 == g_ascii_strcasecmp (name, EVC_CATEGORIES) ||
0 == g_ascii_strcasecmp (name, EVC_PHOTO) ||
0 == g_ascii_strcasecmp (name, GOOGLE_SYSTEM_GROUP_ATTR) ||
- 0 == g_ascii_strcasecmp (name, e_contact_field_name (E_CONTACT_NICKNAME))) {
+ 0 == g_ascii_strcasecmp (name, e_contact_field_name (E_CONTACT_NICKNAME)) ||
+ 0 == g_ascii_strcasecmp (name, E_GOOGLE_X_PHOTO_ETAG)) {
/* Ignore attributes which are treated separately */
} else if (0 == g_ascii_strcasecmp (name, EVC_EMAIL)) {
/* EMAIL */
@@ -450,12 +454,12 @@ gdata_entry_update_from_e_contact (GDataEntry *entry,
if (category_id == NULL)
category_id = g_strdup (g_hash_table_lookup (groups_by_name, category_name));
if (category_id == NULL) {
- GError *error = NULL;
+ GError *local_error = NULL;
- category_id = create_group (category_name, create_group_user_data, &error);
+ category_id = create_group (bbgoogle, category_name, cancellable, &local_error);
if (category_id == NULL) {
- g_warning ("Error creating group '%s': %s", category_name, error->message);
- g_error_free (error);
+ g_warning ("Error creating group '%s': %s", category_name, local_error ?
local_error->message : "Unknown error");
+ g_clear_error (&local_error);
continue;
}
}
@@ -565,8 +569,6 @@ e_contact_new_from_gdata_entry (GDataEntry *entry,
{
EVCard *vcard;
EVCardAttribute *attr, *system_group_ids_attr;
- EContactPhoto *photo;
- const gchar *photo_etag;
GList *email_addresses, *im_addresses, *phone_numbers, *postal_addresses, *orgs, *category_names,
*category_ids;
const gchar *uid, *note, *nickname;
GList *itr;
@@ -858,20 +860,6 @@ e_contact_new_from_gdata_entry (GDataEntry *entry,
break;
}
- /* PHOTO */
- photo = g_object_get_data (G_OBJECT (entry), "photo");
- photo_etag = gdata_contacts_contact_get_photo_etag (GDATA_CONTACTS_CONTACT (entry));
-
- if (photo != NULL) {
- /* Photo */
- e_contact_set (E_CONTACT (vcard), E_CONTACT_PHOTO, photo);
-
- /* ETag */
- attr = e_vcard_attribute_new ("", GDATA_PHOTO_ETAG_ATTR);
- e_vcard_attribute_add_value (attr, photo_etag);
- e_vcard_add_attribute (vcard, attr);
- }
-
return E_CONTACT (vcard);
}
diff --git a/src/addressbook/backends/google/e-book-google-utils.h
b/src/addressbook/backends/google/e-book-google-utils.h
index fdeb24d..6d7e133 100644
--- a/src/addressbook/backends/google/e-book-google-utils.h
+++ b/src/addressbook/backends/google/e-book-google-utils.h
@@ -20,19 +20,34 @@
#ifndef E_BOOK_GOOGLE_UTILS_H
#define E_BOOK_GOOGLE_UTILS_H
-G_BEGIN_DECLS
+#include <gdata/gdata.h>
+
+#include "e-book-backend-google.h"
-/* Custom attribute names. */
-#define GDATA_PHOTO_ETAG_ATTR "X-GDATA-PHOTO-ETAG"
+#define E_GOOGLE_X_PHOTO_ETAG "X-EVOLUTION-GOOGLE-PHOTO-ETAG"
+
+G_BEGIN_DECLS
-typedef gchar *(*EContactGoogleCreateGroupFunc) (const gchar *category_name, gpointer user_data, GError
**error);
+typedef gchar *(*EContactGoogleCreateGroupFunc) (EBookBackendGoogle *bbgoogle,
+ const gchar *category_name,
+ GCancellable *cancellable,
+ GError **error);
-GDataEntry *gdata_entry_new_from_e_contact (EContact *contact, GHashTable *groups_by_name, GHashTable
*system_groups_by_id,
- EContactGoogleCreateGroupFunc create_group,
- gpointer create_group_user_data) G_GNUC_MALLOC
G_GNUC_WARN_UNUSED_RESULT;
-gboolean gdata_entry_update_from_e_contact (GDataEntry *entry, EContact *contact, gboolean
ensure_personal_group, GHashTable *groups_by_name,
- GHashTable *system_groups_by_id,
- EContactGoogleCreateGroupFunc create_group, gpointer
create_group_user_data);
+GDataEntry * gdata_entry_new_from_e_contact (EContact *contact,
+ GHashTable *groups_by_name,
+ GHashTable *system_groups_by_id,
+ EContactGoogleCreateGroupFunc create_group,
+ EBookBackendGoogle *bbgoogle,
+ GCancellable *cancellable) G_GNUC_MALLOC
G_GNUC_WARN_UNUSED_RESULT;
+gboolean gdata_entry_update_from_e_contact
+ (GDataEntry *entry,
+ EContact *contact,
+ gboolean ensure_personal_group,
+ GHashTable *groups_by_name,
+ GHashTable *system_groups_by_id,
+ EContactGoogleCreateGroupFunc create_group,
+ EBookBackendGoogle *bbgoogle,
+ GCancellable *cancellable);
EContact *e_contact_new_from_gdata_entry (GDataEntry *entry, GHashTable *groups_by_id,
GHashTable *system_groups_by_entry_id) G_GNUC_MALLOC
G_GNUC_WARN_UNUSED_RESULT;
diff --git a/src/addressbook/backends/google/tests/phone-numbers.c
b/src/addressbook/backends/google/tests/phone-numbers.c
index 67fd609..f2ca12f 100644
--- a/src/addressbook/backends/google/tests/phone-numbers.c
+++ b/src/addressbook/backends/google/tests/phone-numbers.c
@@ -37,9 +37,10 @@ build_system_groups_by_id (void)
}
static gchar *
-create_group_null (const gchar *category_name,
- gpointer user_data,
- GError **error)
+create_group_null (EBookBackendGoogle *bbgoogle,
+ const gchar *category_name,
+ GCancellable *cancellable,
+ GError **error)
{
/* Must never be reached. */
g_assert_not_reached ();
@@ -61,7 +62,7 @@ create_group_null (const gchar *category_name,
"END:VCARD" \
); \
\
- entry = gdata_entry_new_from_e_contact (contact, groups_by_name, system_groups_by_id,
create_group_null, NULL); \
+ entry = gdata_entry_new_from_e_contact (contact, groups_by_name, system_groups_by_id,
create_group_null, NULL, NULL); \
g_assert (entry != NULL); \
\
g_hash_table_unref (system_groups_by_id); \
diff --git a/src/addressbook/backends/webdav/e-book-backend-webdav.c
b/src/addressbook/backends/webdav/e-book-backend-webdav.c
index 7deda66..12d4989 100644
--- a/src/addressbook/backends/webdav/e-book-backend-webdav.c
+++ b/src/addressbook/backends/webdav/e-book-backend-webdav.c
@@ -1,6 +1,6 @@
-/* e-book-backend-webdav.c - Webdav contact backend.
- *
+/*
* Copyright (C) 2008 Matthias Braun <matze braunis de>
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
*
* This library is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
@@ -17,12 +17,6 @@
* Authors: Matthias Braun <matze braunis de>
*/
-/*
- * Implementation notes:
- * We use the DavResource URIs as UID in the evolution contact
- * ETags are saved in the WEBDAV_CONTACT_ETAG field so we know which cached contacts
- * are outdated.
- */
#include "evolution-data-server-config.h"
#include <stdio.h>
@@ -30,1960 +24,1073 @@
#include <string.h>
#include <glib/gi18n-lib.h>
+#include "libedataserver/libedataserver.h"
+
#include "e-book-backend-webdav.h"
-#include <libsoup/soup.h>
-
-#include <libxml/parser.h>
-#include <libxml/xmlreader.h>
-#include <libxml/xpath.h>
-#include <libxml/xpathInternals.h>
-
-#define E_BOOK_BACKEND_WEBDAV_GET_PRIVATE(obj) \
- (G_TYPE_INSTANCE_GET_PRIVATE \
- ((obj), E_TYPE_BOOK_BACKEND_WEBDAV, EBookBackendWebdavPrivate))
-
-#define USERAGENT "Evolution/" VERSION
-#define WEBDAV_CLOSURE_NAME "EBookBackendWebdav.BookView::closure"
-#define WEBDAV_CTAG_KEY "WEBDAV_CTAG"
-#define WEBDAV_CACHE_VERSION_KEY "WEBDAV_CACHE_VERSION"
-#define WEBDAV_CACHE_VERSION "2"
-#define WEBDAV_CONTACT_ETAG "X-EVOLUTION-WEBDAV-ETAG"
-#define WEBDAV_CONTACT_HREF "X-EVOLUTION-WEBDAV-HREF"
-
-G_DEFINE_TYPE (EBookBackendWebdav, e_book_backend_webdav, E_TYPE_BOOK_BACKEND)
-
-struct _EBookBackendWebdavPrivate {
- gboolean marked_for_offline;
- SoupSession *session;
- gchar *uri;
- gchar *username;
- gchar *password;
- gboolean supports_getctag;
- gint64 last_server_test_us; /* real-time, in microseconds, when the last server test
- for changes had been made, when the server doesn't support ctag */
-
- GMutex cache_lock;
- GMutex update_lock;
- EBookBackendCache *cache;
-};
+#define E_WEBDAV_MAX_MULTIGET_AMOUNT 100 /* what's the maximum count of items to fetch within a multiget
request */
-typedef struct {
- EBookBackendWebdav *webdav;
- GThread *thread;
- EFlag *running;
-} WebdavBackendSearchClosure;
+#define E_WEBDAV_X_ETAG "X-EVOLUTION-WEBDAV-ETAG"
-static void
-webdav_debug_setup (SoupSession *session)
-{
- const gchar *debug_str;
- SoupLogger *logger;
- SoupLoggerLogLevel level;
-
- g_return_if_fail (session != NULL);
-
- debug_str = g_getenv ("WEBDAV_DEBUG");
- if (!debug_str || !*debug_str)
- return;
-
- if (g_ascii_strcasecmp (debug_str, "all") == 0)
- level = SOUP_LOGGER_LOG_BODY;
- else if (g_ascii_strcasecmp (debug_str, "headers") == 0)
- level = SOUP_LOGGER_LOG_HEADERS;
- else
- level = SOUP_LOGGER_LOG_MINIMAL;
-
- logger = soup_logger_new (level, 100 * 1024 * 1024);
- soup_session_add_feature (session, SOUP_SESSION_FEATURE (logger));
- g_object_unref (logger);
-}
+#define EDB_ERROR(_code) e_data_book_create_error (_code, NULL)
+#define EDB_ERROR_EX(_code, _msg) e_data_book_create_error (_code, _msg)
-static void
-webdav_contact_set_etag (EContact *contact,
- const gchar *etag)
-{
- EVCardAttribute *attr;
+struct _EBookBackendWebDAVPrivate {
+ /* The main WebDAV session */
+ EWebDAVSession *webdav;
- g_return_if_fail (E_IS_CONTACT (contact));
+ /* support for 'getctag' extension */
+ gboolean ctag_supported;
+};
- attr = e_vcard_get_attribute (E_VCARD (contact), WEBDAV_CONTACT_ETAG);
+G_DEFINE_TYPE (EBookBackendWebDAV, e_book_backend_webdav, E_TYPE_BOOK_META_BACKEND)
- if (attr) {
- e_vcard_attribute_remove_values (attr);
- if (etag) {
- e_vcard_attribute_add_value (attr, etag);
- } else {
- e_vcard_remove_attribute (E_VCARD (contact), attr);
- }
- } else if (etag) {
- e_vcard_append_attribute_with_value (
- E_VCARD (contact),
- e_vcard_attribute_new (NULL, WEBDAV_CONTACT_ETAG),
- etag);
- }
-}
-
-static gchar *
-webdav_contact_get_etag (EContact *contact)
+static gboolean
+ebb_webdav_connect_sync (EBookMetaBackend *meta_backend,
+ const ENamedParameters *credentials,
+ ESourceAuthenticationResult *out_auth_result,
+ gchar **out_certificate_pem,
+ GTlsCertificateFlags *out_certificate_errors,
+ GCancellable *cancellable,
+ GError **error)
{
- EVCardAttribute *attr;
- GList *v = NULL;
-
- g_return_val_if_fail (E_IS_CONTACT (contact), NULL);
-
- attr = e_vcard_get_attribute (E_VCARD (contact), WEBDAV_CONTACT_ETAG);
+ EBookBackendWebDAV *bbdav;
+ GHashTable *capabilities = NULL, *allows = NULL;
+ ESource *source;
+ gboolean success;
+ GError *local_error = NULL;
- if (attr)
- v = e_vcard_attribute_get_values (attr);
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_WEBDAV (meta_backend), FALSE);
+ g_return_val_if_fail (out_auth_result != NULL, FALSE);
- return ((v && v->data) ? g_strstrip (g_strdup (v->data)) : NULL);
-}
-
-static void
-webdav_contact_set_href (EContact *contact,
- const gchar *href)
-{
- EVCardAttribute *attr;
+ bbdav = E_BOOK_BACKEND_WEBDAV (meta_backend);
- g_return_if_fail (E_IS_CONTACT (contact));
+ if (bbdav->priv->webdav)
+ return TRUE;
- attr = e_vcard_get_attribute (E_VCARD (contact), WEBDAV_CONTACT_HREF);
+ source = e_backend_get_source (E_BACKEND (meta_backend));
- if (attr) {
- e_vcard_attribute_remove_values (attr);
- if (href) {
- e_vcard_attribute_add_value (attr, href);
- } else {
- e_vcard_remove_attribute (E_VCARD (contact), attr);
- }
- } else if (href) {
- e_vcard_append_attribute_with_value (
- E_VCARD (contact),
- e_vcard_attribute_new (NULL, WEBDAV_CONTACT_HREF),
- href);
- }
-}
+ bbdav->priv->webdav = e_webdav_session_new (source);
-static gchar *
-webdav_contact_get_href (EContact *contact)
-{
- EVCardAttribute *attr;
- GList *v = NULL;
+ e_soup_session_setup_logging (E_SOUP_SESSION (bbdav->priv->webdav), g_getenv ("WEBDAV_DEBUG"));
- g_return_val_if_fail (E_IS_CONTACT (contact), NULL);
+ e_binding_bind_property (
+ bbdav, "proxy-resolver",
+ bbdav->priv->webdav, "proxy-resolver",
+ G_BINDING_SYNC_CREATE);
- attr = e_vcard_get_attribute (E_VCARD (contact), WEBDAV_CONTACT_HREF);
+ /* Thinks the 'getctag' extension is available the first time, but unset it when realizes it isn't. */
+ bbdav->priv->ctag_supported = TRUE;
- if (attr)
- v = e_vcard_attribute_get_values (attr);
+ e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTING);
- return ((v && v->data) ? g_strstrip (g_strdup (v->data)) : NULL);
-}
+ e_soup_session_set_credentials (E_SOUP_SESSION (bbdav->priv->webdav), credentials);
-static void
-closure_destroy (WebdavBackendSearchClosure *closure)
-{
- e_flag_free (closure->running);
- if (closure->thread)
- g_thread_unref (closure->thread);
- g_free (closure);
-}
+ success = e_webdav_session_options_sync (bbdav->priv->webdav, NULL,
+ &capabilities, &allows, cancellable, &local_error);
-static WebdavBackendSearchClosure *
-init_closure (EDataBookView *book_view,
- EBookBackendWebdav *webdav)
-{
- WebdavBackendSearchClosure *closure = g_new (WebdavBackendSearchClosure, 1);
+ /* iCloud and Google servers can return "404 Not Found" when issued OPTIONS on the addressbook
collection */
+ if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_NOT_FOUND)) {
+ ESourceWebdav *webdav_extension;
+ SoupURI *soup_uri;
- closure->webdav = webdav;
- closure->thread = NULL;
- closure->running = e_flag_new ();
+ webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+ soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
+ if (soup_uri) {
+ if (soup_uri->host && soup_uri->path && *soup_uri->path &&
+ e_util_utf8_strstrcase (soup_uri->host, ".icloud.com")) {
+ /* Try parent directory */
+ gchar *path;
+ gint len = strlen (soup_uri->path);
- g_object_set_data_full (
- G_OBJECT (book_view), WEBDAV_CLOSURE_NAME,
- closure, (GDestroyNotify) closure_destroy);
+ if (soup_uri->path[len - 1] == '/')
+ soup_uri->path[len - 1] = '\0';
- return closure;
-}
+ path = g_path_get_dirname (soup_uri->path);
+ if (path && g_str_has_prefix (soup_uri->path, path)) {
+ gchar *uri;
-static WebdavBackendSearchClosure *
-get_closure (EDataBookView *book_view)
-{
- return g_object_get_data (G_OBJECT (book_view), WEBDAV_CLOSURE_NAME);
-}
+ soup_uri_set_path (soup_uri, path);
-static guint
-send_and_handle_ssl (EBookBackendWebdav *webdav,
- SoupMessage *message,
- GCancellable *cancellable)
-{
- guint status_code;
+ uri = soup_uri_to_string (soup_uri, FALSE);
+ if (uri) {
+ g_clear_error (&local_error);
- e_soup_ssl_trust_connect (message, e_backend_get_source (E_BACKEND (webdav)));
+ success = e_webdav_session_options_sync (bbdav->priv->webdav,
uri,
+ &capabilities, &allows, cancellable, &local_error);
+ }
- status_code = soup_session_send_message (webdav->priv->session, message);
+ g_free (uri);
+ }
- if (SOUP_STATUS_IS_SUCCESSFUL (status_code))
- e_backend_ensure_source_status_connected (E_BACKEND (webdav));
+ g_free (path);
+ } else if (soup_uri->host && e_util_utf8_strstrcase (soup_uri->host,
".googleusercontent.com")) {
+ g_clear_error (&local_error);
+ success = TRUE;
- return status_code;
-}
+ /* Google's WebDAV doesn't like OPTIONS, hard-code it */
+ capabilities = g_hash_table_new_full (camel_strcase_hash,
camel_strcase_equal, g_free, NULL);
+ g_hash_table_insert (capabilities, g_strdup
(E_WEBDAV_CAPABILITY_ADDRESSBOOK), GINT_TO_POINTER (1));
-static EContact *
-download_contact (EBookBackendWebdav *webdav,
- const gchar *uri,
- GCancellable *cancellable)
-{
- SoupMessage *message;
- const gchar *etag;
- EContact *contact;
- guint status;
-
- message = soup_message_new (SOUP_METHOD_GET, uri);
- soup_message_headers_append (message->request_headers, "User-Agent", USERAGENT);
- soup_message_headers_append (message->request_headers, "Connection", "close");
-
- status = send_and_handle_ssl (webdav, message, cancellable);
- if (status != 200) {
- g_warning ("Couldn't load '%s' (http status %d)", uri, status);
- g_object_unref (message);
- return NULL;
- }
+ allows = g_hash_table_new_full (camel_strcase_hash, camel_strcase_equal,
g_free, NULL);
+ g_hash_table_insert (allows, g_strdup (SOUP_METHOD_PUT), GINT_TO_POINTER (1));
+ }
- if (message->response_body == NULL) {
- g_message ("no response body after requesting '%s'", uri);
- g_object_unref (message);
- return NULL;
+ soup_uri_free (soup_uri);
+ }
}
- if (message->response_body->length <= 11 || 0 != g_ascii_strncasecmp ((const gchar *)
message->response_body->data, "BEGIN:VCARD", 11)) {
- g_object_unref (message);
- return NULL;
- }
+ if (success) {
+ ESourceWebdav *webdav_extension;
+ EBookCache *book_cache;
+ SoupURI *soup_uri;
+ gboolean is_writable;
+ gboolean addressbook;
- etag = soup_message_headers_get_list (message->response_headers, "ETag");
+ webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+ soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
- /* we use our URI as UID */
- contact = e_contact_new_from_vcard (message->response_body->data);
- if (contact == NULL) {
- g_warning ("Invalid vcard at '%s'", uri);
- g_object_unref (message);
- return NULL;
- }
+ /* The POST added for FastMail servers, which doesn't advertise PUT on collections. */
+ is_writable = allows && (
+ g_hash_table_contains (allows, SOUP_METHOD_PUT) ||
+ g_hash_table_contains (allows, SOUP_METHOD_POST) ||
+ g_hash_table_contains (allows, SOUP_METHOD_DELETE));
- webdav_contact_set_href (contact, uri);
- /* the etag is remembered in the WEBDAV_CONTACT_ETAG field */
- if (etag != NULL) {
- webdav_contact_set_etag (contact, etag);
- }
+ addressbook = capabilities && g_hash_table_contains (capabilities,
E_WEBDAV_CAPABILITY_ADDRESSBOOK);
- g_object_unref (message);
- return contact;
-}
+ if (addressbook) {
+ e_book_backend_set_writable (E_BOOK_BACKEND (bbdav), is_writable);
-static guint
-upload_contact (EBookBackendWebdav *webdav,
- const gchar *uri,
- EContact *contact,
- gchar **reason,
- GCancellable *cancellable)
-{
- ESource *source;
- ESourceWebdav *webdav_extension;
- SoupMessage *message;
- gchar *etag;
- const gchar *new_etag, *redir_uri;
- gchar *request;
- guint status;
- gboolean avoid_ifmatch;
- const gchar *extension_name;
-
- g_return_val_if_fail (uri != NULL, SOUP_STATUS_BAD_REQUEST);
-
- source = e_backend_get_source (E_BACKEND (webdav));
-
- extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
- webdav_extension = e_source_get_extension (source, extension_name);
-
- message = soup_message_new (SOUP_METHOD_PUT, uri);
- soup_message_headers_append (message->request_headers, "User-Agent", USERAGENT);
- soup_message_headers_append (message->request_headers, "Connection", "close");
-
- avoid_ifmatch = e_source_webdav_get_avoid_ifmatch (webdav_extension);
-
- /* some servers (like apache < 2.2.8) don't handle If-Match, correctly so
- * we can leave it out */
- if (!avoid_ifmatch) {
- /* only override if etag is still the same on the server */
- etag = webdav_contact_get_etag (contact);
- if (etag == NULL) {
- soup_message_headers_append (
- message->request_headers,
- "If-None-Match", "*");
- } else if (etag[0] == 'W' && etag[1] == '/') {
- g_warning ("we only have a weak ETag, don't use If-Match synchronisation");
+ e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTED);
} else {
- soup_message_headers_append (
- message->request_headers,
- "If-Match", etag);
- }
+ gchar *uri;
- g_free (etag);
- }
+ uri = soup_uri_to_string (soup_uri, FALSE);
- /* Remove the stored ETag, before saving to the server */
- webdav_contact_set_etag (contact, NULL);
+ success = FALSE;
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
+ _("Given URL “%s” doesn't reference WebDAV address book"), uri);
- request = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
- soup_message_set_request (
- message, "text/vcard", SOUP_MEMORY_TEMPORARY,
- request, strlen (request));
+ g_free (uri);
- status = send_and_handle_ssl (webdav, message, cancellable);
- new_etag = soup_message_headers_get_list (message->response_headers, "ETag");
-
- redir_uri = soup_message_headers_get_list (message->response_headers, "Location");
-
- /* set UID and WEBDAV_CONTACT_ETAG fields */
- webdav_contact_set_etag (contact, new_etag);
- if (redir_uri && *redir_uri) {
- if (!strstr (redir_uri, "://")) {
- /* it's a relative URI */
- SoupURI *suri = soup_uri_new (uri);
- gchar *full_uri;
+ e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
+ }
- if (*redir_uri != '/' && *redir_uri != '\\') {
- gchar *slashed_path = g_strconcat ("/", redir_uri, NULL);
+ g_clear_object (&book_cache);
+ soup_uri_free (soup_uri);
+ }
- soup_uri_set_path (suri, slashed_path);
- g_free (slashed_path);
- } else {
- soup_uri_set_path (suri, redir_uri);
- }
- full_uri = soup_uri_to_string (suri, FALSE);
+ if (success) {
+ gchar *ctag = NULL;
- webdav_contact_set_href (contact, full_uri);
+ /* Some servers, notably Google, allow OPTIONS when not
+ authorized (aka without credentials), thus try something
+ more aggressive, just in case.
- g_free (full_uri);
- soup_uri_free (suri);
+ The 'getctag' extension is not required, thuch check
+ for unauthorized error only. */
+ if (!e_webdav_session_getctag_sync (bbdav->priv->webdav, NULL, &ctag, cancellable,
&local_error) &&
+ g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED)) {
+ success = FALSE;
} else {
- webdav_contact_set_href (contact, redir_uri);
+ g_clear_error (&local_error);
}
- } else {
- webdav_contact_set_href (contact, uri);
- }
-
- if (reason != NULL) {
- const gchar *phrase;
- phrase = message->reason_phrase;
- if (phrase == NULL)
- phrase = soup_status_get_phrase (message->status_code);
- if (phrase == NULL)
- phrase = _("Unknown error");
-
- *reason = g_strdup (phrase);
+ g_free (ctag);
}
- g_object_unref (message);
- g_free (request);
-
- return status;
-}
-
-static gboolean
-webdav_handle_auth_request (EBookBackendWebdav *webdav,
- GError **error)
-{
- EBookBackendWebdavPrivate *priv = webdav->priv;
-
- if (priv->username != NULL) {
- g_free (priv->username);
- priv->username = NULL;
- g_free (priv->password);
- priv->password = NULL;
-
- g_set_error_literal (
- error, E_CLIENT_ERROR,
- E_CLIENT_ERROR_AUTHENTICATION_FAILED,
- e_client_error_to_string (
- E_CLIENT_ERROR_AUTHENTICATION_FAILED));
+ if (success) {
+ *out_auth_result = E_SOURCE_AUTHENTICATION_ACCEPTED;
} else {
- g_set_error_literal (
- error, E_CLIENT_ERROR,
- E_CLIENT_ERROR_AUTHENTICATION_REQUIRED,
- e_client_error_to_string (
- E_CLIENT_ERROR_AUTHENTICATION_REQUIRED));
- }
-
- return FALSE;
-}
-
-static guint
-delete_contact (EBookBackendWebdav *webdav,
- const gchar *uri,
- GCancellable *cancellable)
-{
- SoupMessage *message;
- guint status;
-
- message = soup_message_new (SOUP_METHOD_DELETE, uri);
- soup_message_headers_append (message->request_headers, "User-Agent", USERAGENT);
- soup_message_headers_append (message->request_headers, "Connection", "close");
-
- status = send_and_handle_ssl (webdav, message, cancellable);
- g_object_unref (message);
-
- return status;
-}
-
-typedef struct parser_strings_t {
- const xmlChar *multistatus;
- const xmlChar *dav;
- const xmlChar *href;
- const xmlChar *response;
- const xmlChar *propstat;
- const xmlChar *prop;
- const xmlChar *getetag;
-} parser_strings_t;
-
-typedef struct response_element_t response_element_t;
-struct response_element_t {
- xmlChar *href;
- xmlChar *etag;
- response_element_t *next;
-};
-
-static response_element_t *
-parse_response_tag (const parser_strings_t *strings,
- xmlTextReaderPtr reader)
-{
- xmlChar *href = NULL;
- xmlChar *etag = NULL;
- gint depth = xmlTextReaderDepth (reader);
- response_element_t *element;
-
- while (xmlTextReaderRead (reader) == 1 && xmlTextReaderDepth (reader) > depth) {
- const xmlChar *tag_name;
- if (xmlTextReaderNodeType (reader) != XML_READER_TYPE_ELEMENT)
- continue;
+ gboolean credentials_empty;
+ gboolean is_ssl_error;
+
+ credentials_empty = !credentials || !e_named_parameters_count (credentials);
+ is_ssl_error = g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED);
+
+ *out_auth_result = E_SOURCE_AUTHENTICATION_ERROR;
+
+ /* because evolution knows only G_IO_ERROR_CANCELLED */
+ if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_CANCELLED)) {
+ local_error->domain = G_IO_ERROR;
+ local_error->code = G_IO_ERROR_CANCELLED;
+ } else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_FORBIDDEN) &&
credentials_empty) {
+ *out_auth_result = E_SOURCE_AUTHENTICATION_REQUIRED;
+ } else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED)) {
+ if (credentials_empty)
+ *out_auth_result = E_SOURCE_AUTHENTICATION_REQUIRED;
+ else
+ *out_auth_result = E_SOURCE_AUTHENTICATION_REJECTED;
+ } else if (local_error) {
+ g_propagate_error (error, local_error);
+ local_error = NULL;
+ } else {
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+ _("Unknown error"));
+ }
- if (xmlTextReaderConstNamespaceUri (reader) != strings->dav)
- continue;
+ if (is_ssl_error) {
+ *out_auth_result = E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED;
- tag_name = xmlTextReaderConstLocalName (reader);
- if (tag_name == strings->href) {
- if (href != NULL) {
- /* multiple href elements?!? */
- xmlFree (href);
- }
- href = xmlTextReaderReadString (reader);
- } else if (tag_name == strings->propstat) {
- /* find <propstat><prop><getetag> hierarchy */
- gint depth2 = xmlTextReaderDepth (reader);
- while (xmlTextReaderRead (reader) == 1 && xmlTextReaderDepth (reader) > depth2) {
- gint depth3;
- if (xmlTextReaderNodeType (reader) != XML_READER_TYPE_ELEMENT)
- continue;
-
- if (xmlTextReaderConstNamespaceUri (reader) != strings->dav
- || xmlTextReaderConstLocalName (reader) != strings->prop)
- continue;
-
- depth3 = xmlTextReaderDepth (reader);
- while (xmlTextReaderRead (reader) == 1 && xmlTextReaderDepth (reader) >
depth3) {
- if (xmlTextReaderNodeType (reader) != XML_READER_TYPE_ELEMENT)
- continue;
-
- if (xmlTextReaderConstNamespaceUri (reader) != strings->dav
- || xmlTextReaderConstLocalName (reader)
- != strings->getetag)
- continue;
-
- if (etag != NULL) {
- /* multiple etag elements?!? */
- xmlFree (etag);
- }
- etag = xmlTextReaderReadString (reader);
- }
- }
+ e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_SSL_FAILED);
+ e_soup_session_get_ssl_error_details (E_SOUP_SESSION (bbdav->priv->webdav),
out_certificate_pem, out_certificate_errors);
+ } else {
+ e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
}
}
- if (href == NULL) {
- g_warning ("webdav returned response element without href");
- return NULL;
- }
+ if (capabilities)
+ g_hash_table_destroy (capabilities);
+ if (allows)
+ g_hash_table_destroy (allows);
- /* append element to list */
- element = g_malloc (sizeof (element[0]));
- element->href = href;
- element->etag = etag;
- return element;
+ if (!success)
+ g_clear_object (&bbdav->priv->webdav);
+
+ return success;
}
-static response_element_t *
-parse_propfind_response (xmlTextReaderPtr reader)
+static gboolean
+ebb_webdav_disconnect_sync (EBookMetaBackend *meta_backend,
+ GCancellable *cancellable,
+ GError **error)
{
- parser_strings_t strings;
- response_element_t *elements;
-
- /* get internalized versions of some strings to avoid strcmp while
- * parsing */
- strings.multistatus = xmlTextReaderConstString (reader, BAD_CAST "multistatus");
- strings.dav = xmlTextReaderConstString (reader, BAD_CAST "DAV:");
- strings.href = xmlTextReaderConstString (reader, BAD_CAST "href");
- strings.response = xmlTextReaderConstString (reader, BAD_CAST "response");
- strings.propstat = xmlTextReaderConstString (reader, BAD_CAST "propstat");
- strings.prop = xmlTextReaderConstString (reader, BAD_CAST "prop");
- strings.getetag = xmlTextReaderConstString (reader, BAD_CAST "getetag");
-
- while (xmlTextReaderRead (reader) == 1 && xmlTextReaderNodeType (reader) != XML_READER_TYPE_ELEMENT) {
- }
-
- if (xmlTextReaderConstLocalName (reader) != strings.multistatus
- || xmlTextReaderConstNamespaceUri (reader) != strings.dav) {
- g_warning ("webdav PROPFIND result is not <DAV:multistatus>");
- return NULL;
- }
-
- elements = NULL;
+ EBookBackendWebDAV *bbdav;
+ ESource *source;
- /* parse all DAV:response tags */
- while (xmlTextReaderRead (reader) == 1 && xmlTextReaderDepth (reader) > 0) {
- response_element_t *element;
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_WEBDAV (meta_backend), FALSE);
- if (xmlTextReaderNodeType (reader) != XML_READER_TYPE_ELEMENT)
- continue;
+ bbdav = E_BOOK_BACKEND_WEBDAV (meta_backend);
- if (xmlTextReaderConstLocalName (reader) != strings.response
- || xmlTextReaderConstNamespaceUri (reader) != strings.dav)
- continue;
+ if (bbdav->priv->webdav)
+ soup_session_abort (SOUP_SESSION (bbdav->priv->webdav));
- element = parse_response_tag (&strings, reader);
- if (element == NULL)
- continue;
+ g_clear_object (&bbdav->priv->webdav);
- element->next = elements;
- elements = element;
- }
+ source = e_backend_get_source (E_BACKEND (meta_backend));
+ e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
- return elements;
+ return TRUE;
}
-static SoupMessage *
-send_propfind (EBookBackendWebdav *webdav,
- GCancellable *cancellable,
- GError **error)
+static void
+ebb_webdav_update_nfo_with_contact (EBookMetaBackendInfo *nfo,
+ EContact *contact,
+ const gchar *etag)
{
- SoupMessage *message;
- EBookBackendWebdavPrivate *priv = webdav->priv;
- const gchar *request =
- "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
- "<propfind xmlns=\"DAV:\"><prop><getetag/></prop></propfind>";
-
- message = soup_message_new (SOUP_METHOD_PROPFIND, priv->uri);
- if (!message) {
- g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, _("Malformed URI: %s"),
priv->uri);
- return NULL;
- }
-
- soup_message_headers_append (message->request_headers, "User-Agent", USERAGENT);
- soup_message_headers_append (message->request_headers, "Connection", "close");
- soup_message_headers_append (message->request_headers, "Depth", "1");
- soup_message_set_request (
- message, "text/xml", SOUP_MEMORY_TEMPORARY,
- (gchar *) request, strlen (request));
-
- send_and_handle_ssl (webdav, message, cancellable);
-
- return message;
-}
+ const gchar *uid;
-static xmlXPathObjectPtr
-xpath_eval (xmlXPathContextPtr ctx,
- const gchar *format,
- ...)
-{
- xmlXPathObjectPtr result;
- va_list args;
- gchar *expr;
+ g_return_if_fail (nfo != NULL);
+ g_return_if_fail (E_IS_CONTACT (contact));
- if (ctx == NULL) {
- return NULL;
- }
+ uid = e_contact_get_const (contact, E_CONTACT_UID);
- va_start (args, format);
- expr = g_strdup_vprintf (format, args);
- va_end (args);
+ if (!etag || !*etag)
+ etag = nfo->revision;
- result = xmlXPathEvalExpression ((xmlChar *) expr, ctx);
- g_free (expr);
+ e_vcard_util_set_x_attribute (E_VCARD (contact), E_WEBDAV_X_ETAG, etag);
- if (result == NULL) {
- return NULL;
- }
+ g_warn_if_fail (nfo->object == NULL);
+ nfo->object = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
- if (result->type == XPATH_NODESET &&
- xmlXPathNodeSetIsEmpty (result->nodesetval)) {
- xmlXPathFreeObject (result);
- return NULL;
+ if (!nfo->uid || !*(nfo->uid)) {
+ g_free (nfo->uid);
+ nfo->uid = g_strdup (uid);
}
- return result;
-}
-
-static gchar *
-xp_object_get_string (xmlXPathObjectPtr result)
-{
- gchar *ret = NULL;
+ if (g_strcmp0 (etag, nfo->revision) != 0) {
+ gchar *copy = g_strdup (etag);
- if (result == NULL)
- return ret;
-
- if (result->type == XPATH_STRING) {
- ret = g_strdup ((gchar *) result->stringval);
+ g_free (nfo->revision);
+ nfo->revision = copy;
}
-
- xmlXPathFreeObject (result);
- return ret;
-}
-
-static guint
-xp_object_get_status (xmlXPathObjectPtr result)
-{
- gboolean res;
- guint ret = 0;
-
- if (result == NULL)
- return ret;
-
- if (result->type == XPATH_STRING) {
- res = soup_headers_parse_status_line ((gchar *) result->stringval, NULL, &ret, NULL);
- if (!res) {
- ret = 0;
- }
- }
-
- xmlXPathFreeObject (result);
- return ret;
}
static gboolean
-check_addressbook_changed (EBookBackendWebdav *webdav,
- gchar **new_ctag,
- GCancellable *cancellable)
+ebb_webdav_multiget_response_cb (EWebDAVSession *webdav,
+ xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prop_prefix,
+ const SoupURI *request_uri,
+ const gchar *href,
+ guint status_code,
+ gpointer user_data)
{
- gboolean res = TRUE;
- const gchar *request = "<?xml version=\"1.0\" encoding=\"utf-8\"?><propfind
xmlns=\"DAV:\"><prop><getctag/></prop></propfind>";
- EBookBackendWebdavPrivate *priv;
- SoupMessage *message;
+ GSList **from_link = user_data;
- g_return_val_if_fail (webdav != NULL, TRUE);
- g_return_val_if_fail (new_ctag != NULL, TRUE);
+ g_return_val_if_fail (from_link != NULL, FALSE);
- *new_ctag = NULL;
- priv = webdav->priv;
+ if (!xpath_prop_prefix) {
+ e_xml_xpath_context_register_namespaces (xpath_ctx, "C", E_WEBDAV_NS_CARDDAV, NULL);
+ } else if (status_code == SOUP_STATUS_OK) {
+ gchar *address_data, *etag;
- if (!priv->supports_getctag) {
- gint64 real_time_us = g_get_real_time ();
+ g_return_val_if_fail (href != NULL, FALSE);
- /* Fifteen minutes in microseconds */
- if (real_time_us - priv->last_server_test_us < 15 * 60 * 1000 * 1000)
- return FALSE;
+ address_data = e_xml_xpath_eval_as_string (xpath_ctx, "%s/C:address-data", xpath_prop_prefix);
+ etag = e_webdav_session_util_maybe_dequote (e_xml_xpath_eval_as_string (xpath_ctx,
"%s/D:getetag", xpath_prop_prefix));
- priv->last_server_test_us = real_time_us;
+ if (address_data) {
+ EContact *contact;
- return TRUE;
- }
+ contact = e_contact_new_from_vcard (address_data);
+ if (contact) {
+ const gchar *uid;
- priv->supports_getctag = FALSE;
+ uid = e_contact_get_const (contact, E_CONTACT_UID);
+ if (uid) {
+ GSList *link;
- message = soup_message_new (SOUP_METHOD_PROPFIND, priv->uri);
- if (!message)
- return TRUE;
+ for (link = *from_link; link; link = g_slist_next (link)) {
+ EBookMetaBackendInfo *nfo = link->data;
- soup_message_headers_append (message->request_headers, "User-Agent", USERAGENT);
- soup_message_headers_append (message->request_headers, "Connection", "close");
- soup_message_headers_append (message->request_headers, "Depth", "0");
- soup_message_set_request (message, "text/xml", SOUP_MEMORY_TEMPORARY, (gchar *) request, strlen
(request));
- send_and_handle_ssl (webdav, message, cancellable);
-
- if (message->status_code == 207 && message->response_body) {
- xmlDocPtr xml;
-
- xml = xmlReadMemory (message->response_body->data, message->response_body->length, NULL,
NULL, XML_PARSE_NOWARNING);
- if (xml) {
- const gchar *GETCTAG_XPATH_STATUS =
"string(/D:multistatus/D:response/D:propstat/D:prop/D:getctag/../../D:status)";
- const gchar *GETCTAG_XPATH_VALUE =
"string(/D:multistatus/D:response/D:propstat/D:prop/D:getctag)";
- xmlXPathContextPtr xpctx;
-
- xpctx = xmlXPathNewContext (xml);
- xmlXPathRegisterNs (xpctx, (xmlChar *) "D", (xmlChar *) "DAV:");
-
- if (xp_object_get_status (xpath_eval (xpctx, GETCTAG_XPATH_STATUS)) == 200) {
- gchar *txt = xp_object_get_string (xpath_eval (xpctx, GETCTAG_XPATH_VALUE));
- const gchar *stored_version;
- gboolean old_version;
-
- g_mutex_lock (&priv->cache_lock);
- stored_version = e_file_cache_get_object (E_FILE_CACHE (priv->cache),
WEBDAV_CACHE_VERSION_KEY);
-
- /* The ETag was moved from REV to its own attribute, thus
- * if the cache version is too low, update it. */
- old_version = !stored_version || atoi (stored_version) < atoi
(WEBDAV_CACHE_VERSION);
- g_mutex_unlock (&priv->cache_lock);
-
- if (txt && *txt) {
- gint len = strlen (txt);
-
- if (*txt == '\"' && len > 2 && txt[len - 1] == '\"') {
- /* dequote */
- *new_ctag = g_strndup (txt + 1, len - 2);
- } else {
- *new_ctag = txt;
- txt = NULL;
- }
+ if (!nfo)
+ continue;
- if (*new_ctag) {
- const gchar *my_ctag;
+ if (g_strcmp0 (nfo->extra, href) == 0) {
+ /* If the server returns data in the same order as it
had been requested,
+ then this speeds up lookup for the matching
object. */
+ if (link == *from_link)
+ *from_link = g_slist_next (*from_link);
- g_mutex_lock (&priv->cache_lock);
- my_ctag = e_file_cache_get_object (E_FILE_CACHE
(priv->cache), WEBDAV_CTAG_KEY);
- res = old_version || !my_ctag || !g_str_equal (my_ctag,
*new_ctag);
+ ebb_webdav_update_nfo_with_contact (nfo, contact,
etag);
- priv->supports_getctag = TRUE;
- g_mutex_unlock (&priv->cache_lock);
+ break;
+ }
}
}
- g_free (txt);
-
- if (old_version) {
- g_mutex_lock (&priv->cache_lock);
-
- if (!e_file_cache_replace_object (E_FILE_CACHE (priv->cache),
- WEBDAV_CACHE_VERSION_KEY,
- WEBDAV_CACHE_VERSION))
- e_file_cache_add_object (
- E_FILE_CACHE (priv->cache),
- WEBDAV_CACHE_VERSION_KEY,
- WEBDAV_CACHE_VERSION);
-
- g_mutex_unlock (&priv->cache_lock);
- }
+ g_object_unref (contact);
}
-
- xmlXPathFreeContext (xpctx);
- xmlFreeDoc (xml);
}
- }
-
- g_object_unref (message);
-
- return res;
-}
-static void
-remove_unknown_contacts_cb (gpointer href,
- gpointer pcontact,
- gpointer pwebdav)
-{
- EContact *contact = pcontact;
- EBookBackendWebdav *webdav = pwebdav;
- const gchar *uid;
+ g_free (address_data);
+ g_free (etag);
+ }
- uid = e_contact_get_const (contact, E_CONTACT_UID);
- if (uid && e_book_backend_cache_remove_contact (webdav->priv->cache, uid))
- e_book_backend_notify_remove ((EBookBackend *) webdav, uid);
+ return TRUE;
}
static gboolean
-download_contacts (EBookBackendWebdav *webdav,
- EFlag *running,
- EDataBookView *book_view,
- gboolean force,
- GCancellable *cancellable,
- GError **error)
+ebb_webdav_multiget_from_sets_sync (EBookBackendWebDAV *bbdav,
+ GSList **in_link,
+ GSList **set2,
+ GCancellable *cancellable,
+ GError **error)
{
- EBookBackendWebdavPrivate *priv = webdav->priv;
- EBookBackend *book_backend;
- SoupMessage *message;
- guint status;
- xmlTextReaderPtr reader;
- response_element_t *elements;
- response_element_t *element;
- response_element_t *next;
- gint count;
- gint i;
- gchar *new_ctag = NULL;
- GHashTable *href_to_contact;
- GList *cached_contacts, *iter;
-
- g_mutex_lock (&priv->update_lock);
-
- if (!force && !check_addressbook_changed (webdav, &new_ctag, cancellable)) {
- g_free (new_ctag);
- g_mutex_unlock (&priv->update_lock);
- return TRUE;
- }
+ EXmlDocument *xml;
+ gint left_to_go = E_WEBDAV_MAX_MULTIGET_AMOUNT;
+ GSList *link;
+ gboolean success = TRUE;
- book_backend = E_BOOK_BACKEND (webdav);
+ g_return_val_if_fail (in_link != NULL, FALSE);
+ g_return_val_if_fail (*in_link != NULL, FALSE);
+ g_return_val_if_fail (set2 != NULL, FALSE);
- if (book_view != NULL) {
- e_data_book_view_notify_progress (book_view, -1,
- _("Loading Addressbook summary..."));
- }
+ xml = e_xml_document_new (E_WEBDAV_NS_CARDDAV, "addressbook-multiget");
+ g_return_val_if_fail (xml != NULL, FALSE);
- message = send_propfind (webdav, cancellable, error);
- if (!message) {
- g_free (new_ctag);
- if (book_view)
- e_data_book_view_notify_progress (book_view, -1, NULL);
- g_mutex_unlock (&priv->update_lock);
- return FALSE;
- }
+ e_xml_document_add_namespaces (xml, "D", E_WEBDAV_NS_DAV, NULL);
- status = message->status_code;
-
- if (status == SOUP_STATUS_UNAUTHORIZED ||
- status == SOUP_STATUS_PROXY_UNAUTHORIZED ||
- status == SOUP_STATUS_FORBIDDEN) {
- g_object_unref (message);
- g_free (new_ctag);
- if (book_view)
- e_data_book_view_notify_progress (book_view, -1, NULL);
- g_mutex_unlock (&priv->update_lock);
- return webdav_handle_auth_request (webdav, error);
- }
- if (status != 207) {
- g_set_error (
- error, E_CLIENT_ERROR,
- E_CLIENT_ERROR_OTHER_ERROR,
- _("PROPFIND on webdav failed with HTTP status %d (%s)"),
- status,
- message->reason_phrase && *message->reason_phrase ? message->reason_phrase :
- (soup_status_get_phrase (message->status_code) ? soup_status_get_phrase
(message->status_code) : _("Unknown error")));
+ e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "prop");
+ e_xml_document_add_empty_element (xml, E_WEBDAV_NS_DAV, "getetag");
+ e_xml_document_add_empty_element (xml, E_WEBDAV_NS_CARDDAV, "address-data");
+ e_xml_document_end_element (xml); /* prop */
- g_object_unref (message);
- g_free (new_ctag);
+ link = *in_link;
- if (book_view)
- e_data_book_view_notify_progress (book_view, -1, NULL);
+ while (link && left_to_go > 0) {
+ EBookMetaBackendInfo *nfo = link->data;
+ SoupURI *suri;
+ gchar *path = NULL;
- g_mutex_unlock (&priv->update_lock);
+ link = g_slist_next (link);
+ if (!link) {
+ link = *set2;
+ *set2 = NULL;
+ }
- return FALSE;
- }
- if (message->response_body == NULL) {
- g_set_error_literal (
- error, E_CLIENT_ERROR,
- E_CLIENT_ERROR_OTHER_ERROR,
- _("No response body in webdav PROPFIND result"));
+ if (!nfo)
+ continue;
- g_object_unref (message);
- g_free (new_ctag);
+ left_to_go--;
- if (book_view)
- e_data_book_view_notify_progress (book_view, -1, NULL);
+ suri = soup_uri_new (nfo->extra);
+ if (suri) {
+ path = soup_uri_to_string (suri, TRUE);
+ soup_uri_free (suri);
+ }
- g_mutex_unlock (&priv->update_lock);
+ e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "href");
+ e_xml_document_write_string (xml, path ? path : nfo->extra);
+ e_xml_document_end_element (xml); /* href */
- return FALSE;
+ g_free (path);
}
- /* parse response */
- reader = xmlReaderForMemory (
- message->response_body->data,
- message->response_body->length, NULL, NULL,
- XML_PARSE_NOWARNING);
+ if (left_to_go != E_WEBDAV_MAX_MULTIGET_AMOUNT && success) {
+ GSList *from_link = *in_link;
- elements = parse_propfind_response (reader);
-
- /* count contacts */
- count = 0;
- for (element = elements; element != NULL; element = element->next) {
- ++count;
+ success = e_webdav_session_report_sync (bbdav->priv->webdav, NULL, NULL, xml,
+ ebb_webdav_multiget_response_cb, &from_link, NULL, NULL, cancellable, error);
}
- href_to_contact = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
- g_mutex_lock (&priv->cache_lock);
- e_file_cache_freeze_changes (E_FILE_CACHE (priv->cache));
- cached_contacts = e_book_backend_cache_get_contacts (priv->cache, NULL);
- for (iter = cached_contacts; iter; iter = g_list_next (iter)) {
- EContact *contact = iter->data;
- gchar *href;
-
- if (!contact)
- continue;
+ g_object_unref (xml);
- href = webdav_contact_get_href (contact);
+ *in_link = link;
- if (href)
- g_hash_table_insert (href_to_contact, href, g_object_ref (contact));
- }
- g_list_free_full (cached_contacts, g_object_unref);
- g_mutex_unlock (&priv->cache_lock);
-
- /* download contacts */
- i = 0;
- for (element = elements; element != NULL; element = element->next, ++i) {
- const gchar *uri;
- const gchar *etag;
- EContact *contact;
- gchar *complete_uri, *stored_etag;
-
- /* stop downloading if search was aborted */
- if (running != NULL && !e_flag_is_set (running))
- break;
-
- if (book_view != NULL) {
- gfloat percent = 100.0 / count * i;
- gchar buf[100];
- snprintf (buf, sizeof (buf), _("Loading Contacts (%d%%)"), (gint) percent);
- e_data_book_view_notify_progress (book_view, -1, buf);
- }
+ return success;
+}
- /* skip collections */
- uri = (const gchar *) element->href;
- if (uri[strlen (uri) - 1] == '/')
- continue;
+static gboolean
+ebb_webdav_get_contact_items_cb (EWebDAVSession *webdav,
+ xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prop_prefix,
+ const SoupURI *request_uri,
+ const gchar *href,
+ guint status_code,
+ gpointer user_data)
+{
+ GHashTable *known_items = user_data; /* gchar *href ~> EBookMetaBackendInfo * */
- /* uri might be relative, construct complete one */
- if (uri[0] == '/') {
- SoupURI *soup_uri = soup_uri_new (priv->uri);
- g_free (soup_uri->path);
- soup_uri->path = g_strdup (uri);
+ g_return_val_if_fail (xpath_ctx != NULL, FALSE);
+ g_return_val_if_fail (known_items != NULL, FALSE);
- complete_uri = soup_uri_to_string (soup_uri, FALSE);
- soup_uri_free (soup_uri);
- } else {
- complete_uri = g_strdup (uri);
- }
+ if (xpath_prop_prefix &&
+ status_code == SOUP_STATUS_OK) {
+ EBookMetaBackendInfo *nfo;
+ gchar *etag;
- etag = (const gchar *) element->etag;
+ g_return_val_if_fail (href != NULL, FALSE);
- contact = g_hash_table_lookup (href_to_contact, complete_uri);
- if (contact) {
- g_object_ref (contact);
- g_hash_table_remove (href_to_contact, complete_uri);
- stored_etag = webdav_contact_get_etag (contact);
- } else {
- stored_etag = NULL;
+ /* Skip collection resource, if returned by the server (like iCloud.com does) */
+ if (g_str_has_suffix (href, "/") ||
+ (request_uri && request_uri->path && g_str_has_suffix (href, request_uri->path))) {
+ return TRUE;
}
+ etag = e_webdav_session_util_maybe_dequote (e_xml_xpath_eval_as_string (xpath_ctx,
"%s/D:getetag", xpath_prop_prefix));
+ /* Return 'TRUE' to not stop on faulty data from the server */
+ g_return_val_if_fail (etag != NULL, TRUE);
- /* download contact if it is not cached or its ETag changed */
- if (contact == NULL || etag == NULL || !stored_etag ||
- strcmp (stored_etag, etag) != 0) {
- if (contact != NULL)
- g_object_unref (contact);
- contact = download_contact (webdav, complete_uri, cancellable);
- if (contact != NULL) {
- const gchar *uid;
+ /* UID is unknown at this moment */
+ nfo = e_book_meta_backend_info_new ("", etag, NULL, href);
- uid = e_contact_get_const (contact, E_CONTACT_UID);
-
- g_mutex_lock (&priv->cache_lock);
- if (e_book_backend_cache_remove_contact (priv->cache, uid))
- e_book_backend_notify_remove (book_backend, uid);
- e_book_backend_cache_add_contact (priv->cache, contact);
- g_mutex_unlock (&priv->cache_lock);
- e_book_backend_notify_update (book_backend, contact);
- }
- }
+ g_free (etag);
+ g_return_val_if_fail (nfo != NULL, FALSE);
- if (contact != NULL)
- g_object_unref (contact);
- g_free (complete_uri);
- g_free (stored_etag);
+ g_hash_table_insert (known_items, g_strdup (href), nfo);
}
- /* free element list */
- for (element = elements; element != NULL; element = next) {
- next = element->next;
+ return TRUE;
+}
- xmlFree (element->href);
- xmlFree (element->etag);
- g_free (element);
- }
+typedef struct _WebDAVChangesData {
+ GSList **out_modified_objects;
+ GSList **out_removed_objects;
+ GHashTable *known_items; /* gchar *href ~> EBookMetaBackendInfo * */
+} WebDAVChangesData;
- xmlFreeTextReader (reader);
- g_object_unref (message);
+static gboolean
+ebb_webdav_search_changes_cb (EBookCache *book_cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ const gchar *extra,
+ EOfflineState offline_state,
+ gpointer user_data)
+{
+ WebDAVChangesData *ccd = user_data;
- if (new_ctag) {
- g_mutex_lock (&priv->cache_lock);
- if (!e_file_cache_replace_object (E_FILE_CACHE (priv->cache), WEBDAV_CTAG_KEY, new_ctag))
- e_file_cache_add_object (E_FILE_CACHE (priv->cache), WEBDAV_CTAG_KEY, new_ctag);
- g_mutex_unlock (&priv->cache_lock);
- }
- g_free (new_ctag);
+ g_return_val_if_fail (ccd != NULL, FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
- if (book_view)
- e_data_book_view_notify_progress (book_view, -1, NULL);
+ /* Can be NULL for added components in offline mode */
+ if (extra && *extra) {
+ EBookMetaBackendInfo *nfo;
- g_mutex_lock (&priv->cache_lock);
+ nfo = g_hash_table_lookup (ccd->known_items, extra);
+ if (nfo) {
+ if (g_strcmp0 (revision, nfo->revision) == 0) {
+ g_hash_table_remove (ccd->known_items, extra);
+ } else {
+ if (!nfo->uid || !*(nfo->uid)) {
+ g_free (nfo->uid);
+ nfo->uid = g_strdup (uid);
+ }
- if (!g_cancellable_is_cancelled (cancellable) &&
- (!running || e_flag_is_set (running))) {
- /* clean-up the cache only if it wasn't cancelled during the work */
- g_hash_table_foreach (href_to_contact, remove_unknown_contacts_cb, webdav);
- }
- e_file_cache_thaw_changes (E_FILE_CACHE (priv->cache));
- g_mutex_unlock (&priv->cache_lock);
- g_mutex_unlock (&priv->update_lock);
+ *(ccd->out_modified_objects) = g_slist_prepend (*(ccd->out_modified_objects),
+ e_book_meta_backend_info_copy (nfo));
- g_hash_table_destroy (href_to_contact);
+ g_hash_table_remove (ccd->known_items, extra);
+ }
+ } else {
+ *(ccd->out_removed_objects) = g_slist_prepend (*(ccd->out_removed_objects),
+ e_book_meta_backend_info_new (uid, revision, object, extra));
+ }
+ }
return TRUE;
}
-static gpointer
-book_view_thread (gpointer data)
+static gboolean
+ebb_webdav_get_changes_sync (EBookMetaBackend *meta_backend,
+ const gchar *last_sync_tag,
+ gboolean is_repeat,
+ gchar **out_new_sync_tag,
+ gboolean *out_repeat,
+ GSList **out_created_objects,
+ GSList **out_modified_objects,
+ GSList **out_removed_objects,
+ GCancellable *cancellable,
+ GError **error)
{
- EDataBookView *book_view = data;
- WebdavBackendSearchClosure *closure = get_closure (book_view);
- EBookBackendWebdav *webdav = closure->webdav;
-
- e_flag_set (closure->running);
-
- /* ref the book view because it'll be removed and unrefed when/if
- * it's stopped */
- g_object_ref (book_view);
-
- download_contacts (webdav, closure->running, book_view, FALSE, NULL, NULL);
-
- g_object_unref (book_view);
-
- return NULL;
-}
+ EBookBackendWebDAV *bbdav;
+ EXmlDocument *xml;
+ GHashTable *known_items; /* gchar *href ~> EBookMetaBackendInfo * */
+ GHashTableIter iter;
+ gpointer key = NULL, value = NULL;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_WEBDAV (meta_backend), FALSE);
+ g_return_val_if_fail (out_new_sync_tag, FALSE);
+ g_return_val_if_fail (out_created_objects, FALSE);
+ g_return_val_if_fail (out_modified_objects, FALSE);
+ g_return_val_if_fail (out_removed_objects, FALSE);
+
+ *out_new_sync_tag = NULL;
+ *out_created_objects = NULL;
+ *out_modified_objects = NULL;
+ *out_removed_objects = NULL;
+
+ bbdav = E_BOOK_BACKEND_WEBDAV (meta_backend);
+
+ if (bbdav->priv->ctag_supported) {
+ gchar *new_sync_tag = NULL;
+
+ success = e_webdav_session_getctag_sync (bbdav->priv->webdav, NULL, &new_sync_tag,
cancellable, NULL);
+ if (!success) {
+ bbdav->priv->ctag_supported = g_cancellable_set_error_if_cancelled (cancellable,
error);
+ if (bbdav->priv->ctag_supported || !bbdav->priv->webdav)
+ return FALSE;
+ } else if (new_sync_tag && last_sync_tag && g_strcmp0 (last_sync_tag, new_sync_tag) == 0) {
+ *out_new_sync_tag = new_sync_tag;
+ return TRUE;
+ }
-static void
-e_book_backend_webdav_start_view (EBookBackend *backend,
- EDataBookView *book_view)
-{
- EBookBackendWebdav *webdav = E_BOOK_BACKEND_WEBDAV (backend);
- EBookBackendWebdavPrivate *priv = webdav->priv;
- EBookBackendSExp *sexp;
- const gchar *query;
- GList *contacts;
- GList *l;
-
- sexp = e_data_book_view_get_sexp (book_view);
- query = e_book_backend_sexp_text (sexp);
-
- g_mutex_lock (&priv->cache_lock);
- contacts = e_book_backend_cache_get_contacts (priv->cache, query);
- g_mutex_unlock (&priv->cache_lock);
-
- for (l = contacts; l != NULL; l = g_list_next (l)) {
- EContact *contact = l->data;
- e_data_book_view_notify_update (book_view, contact);
- g_object_unref (contact);
+ *out_new_sync_tag = new_sync_tag;
}
- g_list_free (contacts);
- /* this way the UI is notified about cached contacts immediately,
- * and the update thread notifies about possible changes only */
- e_data_book_view_notify_complete (book_view, NULL /* Success */);
+ xml = e_xml_document_new (E_WEBDAV_NS_DAV, "propfind");
+ g_return_val_if_fail (xml != NULL, FALSE);
- if (e_backend_get_online (E_BACKEND (backend))) {
- WebdavBackendSearchClosure *closure;
+ e_xml_document_start_element (xml, NULL, "prop");
+ e_xml_document_add_empty_element (xml, NULL, "getetag");
+ e_xml_document_end_element (xml); /* prop */
- closure = init_closure (
- book_view, E_BOOK_BACKEND_WEBDAV (backend));
+ known_items = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, e_book_meta_backend_info_free);
- closure->thread = g_thread_new (
- NULL, book_view_thread, book_view);
+ success = e_webdav_session_propfind_sync (bbdav->priv->webdav, NULL,
E_WEBDAV_DEPTH_THIS_AND_CHILDREN, xml,
+ ebb_webdav_get_contact_items_cb, known_items, cancellable, error);
- e_flag_wait (closure->running);
- }
-}
+ g_object_unref (xml);
-static void
-e_book_backend_webdav_stop_view (EBookBackend *backend,
- EDataBookView *book_view)
-{
- WebdavBackendSearchClosure *closure;
- gboolean need_join;
+ if (success) {
+ EBookCache *book_cache;
+ WebDAVChangesData ccd;
- if (!e_backend_get_online (E_BACKEND (backend)))
- return;
+ ccd.out_modified_objects = out_modified_objects;
+ ccd.out_removed_objects = out_removed_objects;
+ ccd.known_items = known_items;
- closure = get_closure (book_view);
- if (closure == NULL)
- return;
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
- need_join = e_flag_is_set (closure->running);
- e_flag_clear (closure->running);
+ success = e_book_cache_search_with_callback (book_cache, NULL, ebb_webdav_search_changes_cb,
&ccd, cancellable, error);
- if (need_join) {
- g_thread_join (closure->thread);
- closure->thread = NULL;
+ g_clear_object (&book_cache);
}
-}
-/** authentication callback for libsoup */
-static void
-soup_authenticate (SoupSession *session,
- SoupMessage *message,
- SoupAuth *auth,
- gboolean retrying,
- gpointer data)
-{
- EBookBackendWebdav *webdav = data;
- EBookBackendWebdavPrivate *priv = webdav->priv;
-
- if (retrying)
- return;
-
- if (!priv->username || !*priv->username || !priv->password)
- soup_message_set_status (message, SOUP_STATUS_FORBIDDEN);
- else
- soup_auth_authenticate (auth, priv->username, priv->password);
-}
-
-static void
-e_book_backend_webdav_notify_online_cb (EBookBackend *backend,
- GParamSpec *pspec)
-{
- gboolean online;
+ if (!success) {
+ g_hash_table_destroy (known_items);
+ return FALSE;
+ }
- /* set_mode is called before the backend is loaded */
- if (!e_book_backend_is_opened (backend))
- return;
+ g_hash_table_iter_init (&iter, known_items);
+ while (g_hash_table_iter_next (&iter, &key, &value)) {
+ *out_created_objects = g_slist_prepend (*out_created_objects, e_book_meta_backend_info_copy
(value));
+ }
- /* XXX Could just use a property binding for this.
- * EBackend:online --> EBookBackend:writable */
- online = e_backend_get_online (E_BACKEND (backend));
- e_book_backend_set_writable (backend, online);
-}
+ g_hash_table_destroy (known_items);
-static void
-book_backend_webdav_dispose (GObject *object)
-{
- EBookBackendWebdavPrivate *priv;
+ if (*out_created_objects || *out_modified_objects) {
+ GSList *link, *set2 = *out_modified_objects;
- priv = E_BOOK_BACKEND_WEBDAV_GET_PRIVATE (object);
+ if (*out_created_objects) {
+ link = *out_created_objects;
+ } else {
+ link = set2;
+ set2 = NULL;
+ }
- g_clear_object (&priv->session);
- g_clear_object (&priv->cache);
+ do {
+ success = ebb_webdav_multiget_from_sets_sync (bbdav, &link, &set2, cancellable,
error);
+ } while (success && link);
+ }
- /* Chain up to parent's dispose() method. */
- G_OBJECT_CLASS (e_book_backend_webdav_parent_class)->dispose (object);
+ return success;
}
-static void
-book_backend_webdav_finalize (GObject *object)
+static gboolean
+ebb_webdav_extract_existing_cb (EWebDAVSession *webdav,
+ xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prop_prefix,
+ const SoupURI *request_uri,
+ const gchar *href,
+ guint status_code,
+ gpointer user_data)
{
- EBookBackendWebdav *webdav = E_BOOK_BACKEND_WEBDAV (object);
- EBookBackendWebdavPrivate *priv = webdav->priv;
+ GSList **out_existing_objects = user_data;
- g_free (priv->uri);
- g_free (priv->username);
- g_free (priv->password);
+ g_return_val_if_fail (out_existing_objects != NULL, FALSE);
- g_mutex_clear (&priv->cache_lock);
- g_mutex_clear (&priv->update_lock);
+ if (!xpath_prop_prefix) {
+ e_xml_xpath_context_register_namespaces (xpath_ctx, "C", E_WEBDAV_NS_CARDDAV, NULL);
+ } else if (status_code == SOUP_STATUS_OK) {
+ gchar *etag;
+ gchar *address_data;
- /* Chain up to parent's finalize() method. */
- G_OBJECT_CLASS (e_book_backend_webdav_parent_class)->finalize (object);
-}
+ g_return_val_if_fail (href != NULL, FALSE);
-static gchar *
-book_backend_webdav_get_backend_property (EBookBackend *backend,
- const gchar *prop_name)
-{
- g_return_val_if_fail (prop_name != NULL, NULL);
+ etag = e_xml_xpath_eval_as_string (xpath_ctx, "%s/D:getetag", xpath_prop_prefix);
+ address_data = e_xml_xpath_eval_as_string (xpath_ctx, "%s/C:address-data", xpath_prop_prefix);
- if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
- return g_strdup ("net,do-initial-query,contact-lists,refresh-supported");
+ if (address_data) {
+ EContact *contact;
- } else if (g_str_equal (prop_name, BOOK_BACKEND_PROPERTY_REQUIRED_FIELDS)) {
- return g_strdup (e_contact_field_name (E_CONTACT_FILE_AS));
+ contact = e_contact_new_from_vcard (address_data);
+ if (contact) {
+ const gchar *uid;
- } else if (g_str_equal (prop_name, BOOK_BACKEND_PROPERTY_SUPPORTED_FIELDS)) {
- GString *fields;
- gint ii;
+ uid = e_contact_get_const (contact, E_CONTACT_UID);
- fields = g_string_sized_new (1024);
+ if (uid) {
+ etag = e_webdav_session_util_maybe_dequote (etag);
+ *out_existing_objects = g_slist_prepend (*out_existing_objects,
+ e_book_meta_backend_info_new (uid, etag, NULL, href));
+ }
- /* we support everything */
- for (ii = 1; ii < E_CONTACT_FIELD_LAST; ii++) {
- if (fields->len > 0)
- g_string_append_c (fields, ',');
- g_string_append (fields, e_contact_field_name (ii));
+ g_object_unref (contact);
+ }
}
- return g_string_free (fields, FALSE);
+ g_free (address_data);
+ g_free (etag);
}
- /* Chain up to parent's get_backend_property() method. */
- return E_BOOK_BACKEND_CLASS (e_book_backend_webdav_parent_class)->
- get_backend_property (backend, prop_name);
+ return TRUE;
}
static gboolean
-book_backend_webdav_test_can_connect (EBookBackendWebdav *webdav,
- gchar **out_certificate_pem,
- GTlsCertificateFlags *out_certificate_errors,
- GCancellable *cancellable,
- GError **error)
+ebb_webdav_list_existing_sync (EBookMetaBackend *meta_backend,
+ gchar **out_new_sync_tag,
+ GSList **out_existing_objects,
+ GCancellable *cancellable,
+ GError **error)
{
- SoupMessage *message;
- gboolean res = FALSE;
+ EBookBackendWebDAV *bbdav;
+ EXmlDocument *xml;
+ gboolean success;
- g_return_val_if_fail (E_IS_BOOK_BACKEND_WEBDAV (webdav), FALSE);
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_WEBDAV (meta_backend), FALSE);
+ g_return_val_if_fail (out_existing_objects != NULL, FALSE);
- /* Send a PROPFIND to test whether user/password is correct. */
- message = send_propfind (webdav, cancellable, error);
- if (!message)
- return FALSE;
+ *out_existing_objects = NULL;
- switch (message->status_code) {
- case SOUP_STATUS_OK:
- case SOUP_STATUS_MULTI_STATUS:
- res = TRUE;
- break;
-
- case SOUP_STATUS_UNAUTHORIZED:
- case SOUP_STATUS_PROXY_UNAUTHORIZED:
- g_free (webdav->priv->username);
- webdav->priv->username = NULL;
- g_free (webdav->priv->password);
- webdav->priv->password = NULL;
- g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_AUTHENTICATION_FAILED,
- e_client_error_to_string (E_CLIENT_ERROR_AUTHENTICATION_FAILED));
- break;
-
- case SOUP_STATUS_FORBIDDEN:
- g_free (webdav->priv->username);
- webdav->priv->username = NULL;
- g_free (webdav->priv->password);
- webdav->priv->password = NULL;
- g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_AUTHENTICATION_REQUIRED,
- e_client_error_to_string (E_CLIENT_ERROR_AUTHENTICATION_REQUIRED));
- break;
-
- case SOUP_STATUS_SSL_FAILED:
- if (out_certificate_pem && out_certificate_errors) {
- GTlsCertificate *certificate = NULL;
-
- g_object_get (G_OBJECT (message),
- "tls-certificate", &certificate,
- "tls-errors", out_certificate_errors,
- NULL);
-
- if (certificate) {
- g_object_get (certificate, "certificate-pem", out_certificate_pem,
NULL);
- g_object_unref (certificate);
- }
- }
-
- g_set_error_literal (
- error, SOUP_HTTP_ERROR,
- message->status_code,
- message->reason_phrase);
- break;
-
- default:
- g_set_error_literal (
- error, SOUP_HTTP_ERROR,
- message->status_code,
- message->reason_phrase);
- break;
- }
+ bbdav = E_BOOK_BACKEND_WEBDAV (meta_backend);
- g_object_unref (message);
+ xml = e_xml_document_new (E_WEBDAV_NS_CARDDAV, "addressbook-query");
+ g_return_val_if_fail (xml != NULL, FALSE);
- return res;
-}
-
-static gboolean
-book_backend_webdav_open_sync (EBookBackend *backend,
- GCancellable *cancellable,
- GError **error)
-{
- EBookBackendWebdav *webdav = E_BOOK_BACKEND_WEBDAV (backend);
- ESourceAuthentication *auth_extension;
- ESourceOffline *offline_extension;
- ESourceWebdav *webdav_extension;
- ESource *source;
- const gchar *extension_name;
- const gchar *cache_dir;
- gchar *filename;
- SoupSession *session;
- SoupURI *suri;
- gboolean success = TRUE;
-
- /* will try fetch ctag for the first time, if it fails then sets this to FALSE */
- webdav->priv->supports_getctag = TRUE;
-
- source = e_backend_get_source (E_BACKEND (backend));
- cache_dir = e_book_backend_get_cache_dir (backend);
-
- extension_name = E_SOURCE_EXTENSION_AUTHENTICATION;
- auth_extension = e_source_get_extension (source, extension_name);
-
- extension_name = E_SOURCE_EXTENSION_OFFLINE;
- offline_extension = e_source_get_extension (source, extension_name);
-
- extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
- webdav_extension = e_source_get_extension (source, extension_name);
-
- webdav->priv->marked_for_offline =
- e_source_offline_get_stay_synchronized (offline_extension);
-
- if (!e_backend_get_online (E_BACKEND (backend)) &&
- !webdav->priv->marked_for_offline ) {
- g_set_error_literal (
- error, E_CLIENT_ERROR,
- E_CLIENT_ERROR_OFFLINE_UNAVAILABLE,
- e_client_error_to_string (
- E_CLIENT_ERROR_OFFLINE_UNAVAILABLE));
- return FALSE;
- }
+ e_xml_document_add_namespaces (xml, "D", E_WEBDAV_NS_DAV, NULL);
- suri = e_source_webdav_dup_soup_uri (webdav_extension);
-
- webdav->priv->uri = soup_uri_to_string (suri, FALSE);
- if (!webdav->priv->uri || !*webdav->priv->uri) {
- g_free (webdav->priv->uri);
- webdav->priv->uri = NULL;
- soup_uri_free (suri);
- g_set_error_literal (
- error, E_CLIENT_ERROR,
- E_CLIENT_ERROR_OTHER_ERROR,
- _("Cannot transform SoupURI to string"));
- return FALSE;
- }
+ e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "prop");
+ e_xml_document_add_empty_element (xml, E_WEBDAV_NS_DAV, "getetag");
+ e_xml_document_start_element (xml, E_WEBDAV_NS_CARDDAV, "address-data");
+ e_xml_document_start_element (xml, E_WEBDAV_NS_CARDDAV, "prop");
+ e_xml_document_add_attribute (xml, NULL, "name", "VERSION");
+ e_xml_document_end_element (xml); /* prop / VERSION */
+ e_xml_document_start_element (xml, E_WEBDAV_NS_CARDDAV, "prop");
+ e_xml_document_add_attribute (xml, NULL, "name", "UID");
+ e_xml_document_end_element (xml); /* prop / UID */
+ e_xml_document_end_element (xml); /* address-data */
+ e_xml_document_end_element (xml); /* prop */
- g_mutex_lock (&webdav->priv->cache_lock);
+ success = e_webdav_session_report_sync (bbdav->priv->webdav, NULL, E_WEBDAV_DEPTH_THIS, xml,
+ ebb_webdav_extract_existing_cb, out_existing_objects, NULL, NULL, cancellable, error);
- /* make sure the uri ends with a forward slash */
- if (webdav->priv->uri[strlen (webdav->priv->uri) - 1] != '/') {
- gchar *tmp = webdav->priv->uri;
- webdav->priv->uri = g_strconcat (tmp, "/", NULL);
- g_free (tmp);
- }
+ g_object_unref (xml);
- if (!webdav->priv->cache) {
- filename = g_build_filename (cache_dir, "cache.xml", NULL);
- webdav->priv->cache = e_book_backend_cache_new (filename);
- g_free (filename);
- }
- g_mutex_unlock (&webdav->priv->cache_lock);
+ if (success)
+ *out_existing_objects = g_slist_reverse (*out_existing_objects);
- session = soup_session_sync_new ();
- g_object_set (
- session,
- SOUP_SESSION_TIMEOUT, 90,
- SOUP_SESSION_SSL_STRICT, TRUE,
- SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, TRUE,
- SOUP_SESSION_ACCEPT_LANGUAGE_AUTO, TRUE,
- NULL);
-
- e_binding_bind_property (
- backend, "proxy-resolver",
- session, "proxy-resolver",
- G_BINDING_SYNC_CREATE);
-
- e_source_webdav_unset_temporary_ssl_trust (webdav_extension);
-
- g_signal_connect (
- session, "authenticate",
- G_CALLBACK (soup_authenticate), webdav);
-
- webdav->priv->session = session;
- webdav_debug_setup (webdav->priv->session);
+ return success;
+}
- e_backend_set_online (E_BACKEND (backend), TRUE);
- e_book_backend_set_writable (backend, TRUE);
+static gchar *
+ebb_webdav_uid_to_uri (EBookBackendWebDAV *bbdav,
+ const gchar *uid,
+ const gchar *extension)
+{
+ ESourceWebdav *webdav_extension;
+ SoupURI *soup_uri;
+ gchar *uri, *tmp, *filename;
- e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTING);
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_WEBDAV (bbdav), NULL);
+ g_return_val_if_fail (uid != NULL, NULL);
- if (e_source_authentication_required (auth_extension)) {
- e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
+ webdav_extension = e_source_get_extension (e_backend_get_source (E_BACKEND (bbdav)),
E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+ soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
+ g_return_val_if_fail (soup_uri != NULL, NULL);
- success = e_backend_credentials_required_sync (E_BACKEND (backend),
- E_SOURCE_CREDENTIALS_REASON_REQUIRED, NULL, 0, NULL,
- cancellable, error);
+ if (extension) {
+ tmp = g_strconcat (uid, extension, NULL);
+ filename = soup_uri_encode (tmp, NULL);
+ g_free (tmp);
} else {
- gchar *certificate_pem = NULL;
- GTlsCertificateFlags certificate_errors = 0;
- GError *local_error = NULL;
-
- success = book_backend_webdav_test_can_connect (webdav, &certificate_pem,
&certificate_errors, cancellable, &local_error);
- if (!success && !g_cancellable_is_cancelled (cancellable)) {
- ESourceCredentialsReason reason;
- GError *local_error2 = NULL;
-
- if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
- reason = E_SOURCE_CREDENTIALS_REASON_SSL_FAILED;
- e_source_set_connection_status (source,
E_SOURCE_CONNECTION_STATUS_SSL_FAILED);
- } else if (g_error_matches (local_error, E_CLIENT_ERROR,
E_CLIENT_ERROR_AUTHENTICATION_FAILED) ||
- g_error_matches (local_error, E_CLIENT_ERROR,
E_CLIENT_ERROR_AUTHENTICATION_REQUIRED)) {
- reason = E_SOURCE_CREDENTIALS_REASON_REQUIRED;
- } else {
- reason = E_SOURCE_CREDENTIALS_REASON_ERROR;
- }
-
- if (!e_backend_credentials_required_sync (E_BACKEND (backend), reason,
certificate_pem, certificate_errors,
- local_error, cancellable, &local_error2)) {
- g_warning ("%s: Failed to call credentials required: %s", G_STRFUNC,
local_error2 ? local_error2->message : "Unknown error");
- }
-
- if (!local_error2 && g_error_matches (local_error, SOUP_HTTP_ERROR,
SOUP_STATUS_SSL_FAILED)) {
- /* These cerificate errors are treated through the authentication */
- g_clear_error (&local_error);
- } else {
- g_propagate_error (error, local_error);
- local_error = NULL;
- }
-
- g_clear_error (&local_error2);
- } else {
- e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTED);
- }
-
- g_free (certificate_pem);
-
- if (local_error)
- g_propagate_error (error, local_error);
+ filename = soup_uri_encode (uid, NULL);
}
- soup_uri_free (suri);
+ if (soup_uri->path) {
+ gchar *slash = strrchr (soup_uri->path, '/');
- return success;
-}
+ if (slash && !slash[1])
+ *slash = '\0';
+ }
-static gboolean
-webdav_can_use_uid (const gchar *uid)
-{
- const gchar *ptr;
+ soup_uri_set_user (soup_uri, NULL);
+ soup_uri_set_password (soup_uri, NULL);
- if (!uid || !*uid)
- return FALSE;
+ tmp = g_strconcat (soup_uri->path && *soup_uri->path ? soup_uri->path : "", "/", filename, NULL);
+ soup_uri_set_path (soup_uri, tmp);
+ g_free (tmp);
- for (ptr = uid; *ptr; ptr++) {
- if ((*ptr >= 'a' && *ptr <= 'z') ||
- (*ptr >= 'A' && *ptr <= 'Z') ||
- (*ptr >= '0' && *ptr <= '9') ||
- strchr (".-@", *ptr) != NULL)
- continue;
+ uri = soup_uri_to_string (soup_uri, FALSE);
- return FALSE;
- }
+ soup_uri_free (soup_uri);
+ g_free (filename);
- return TRUE;
+ return uri;
}
static gboolean
-book_backend_webdav_create_contacts_sync (EBookBackend *backend,
- const gchar * const *vcards,
- GQueue *out_contacts,
- GCancellable *cancellable,
- GError **error)
+ebb_webdav_load_contact_sync (EBookMetaBackend *meta_backend,
+ const gchar *uid,
+ const gchar *extra,
+ EContact **out_contact,
+ gchar **out_extra,
+ GCancellable *cancellable,
+ GError **error)
{
- EBookBackendWebdav *webdav = E_BOOK_BACKEND_WEBDAV (backend);
- EContact *contact;
- gchar *uid, *href;
- const gchar *orig_uid;
- guint status;
- gchar *status_reason = NULL, *stored_etag;
-
- /* We make the assumption that the vCard list we're passed is
- * always exactly one element long, since we haven't specified
- * "bulk-adds" in our static capability list. This is because
- * there is no way to roll back changes in case of an error. */
- if (g_strv_length ((gchar **) vcards) > 1) {
- g_set_error_literal (
- error, E_CLIENT_ERROR,
- E_CLIENT_ERROR_NOT_SUPPORTED,
- _("The backend does not support bulk additions"));
- return FALSE;
- }
-
- if (!e_backend_get_online (E_BACKEND (backend))) {
- g_set_error_literal (
- error, E_CLIENT_ERROR,
- E_CLIENT_ERROR_REPOSITORY_OFFLINE,
- e_client_error_to_string (
- E_CLIENT_ERROR_REPOSITORY_OFFLINE));
- return FALSE;
- }
+ EBookBackendWebDAV *bbdav;
+ gchar *uri = NULL, *href = NULL, *etag = NULL, *bytes = NULL;
+ gsize length = -1;
+ gboolean success = FALSE;
+ GError *local_error = NULL;
- contact = e_contact_new_from_vcard (vcards[0]);
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_WEBDAV (meta_backend), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (out_contact != NULL, FALSE);
- orig_uid = e_contact_get_const (contact, E_CONTACT_UID);
- if (orig_uid && *orig_uid && webdav_can_use_uid (orig_uid) && !e_book_backend_cache_check_contact
(webdav->priv->cache, orig_uid)) {
- uid = g_strdup (orig_uid);
- } else {
- uid = NULL;
+ bbdav = E_BOOK_BACKEND_WEBDAV (meta_backend);
- do {
- g_free (uid);
+ if (extra && *extra) {
+ uri = g_strdup (extra);
- /* do 3 random() calls to construct a unique ID... poor way but should be
- * good enough for us */
- uid = g_strdup_printf ("%08X-%08X-%08X", g_random_int (), g_random_int (),
g_random_int ());
+ success = e_webdav_session_get_data_sync (bbdav->priv->webdav, uri, &href, &etag, &bytes,
&length, cancellable, &local_error);
- } while (e_book_backend_cache_check_contact (webdav->priv->cache, uid) &&
- !g_cancellable_is_cancelled (cancellable));
-
- e_contact_set (contact, E_CONTACT_UID, uid);
+ if (!success) {
+ g_free (uri);
+ uri = NULL;
+ }
}
- href = g_strconcat (webdav->priv->uri, uid, ".vcf", NULL);
+ if (!success) {
+ uri = ebb_webdav_uid_to_uri (bbdav, uid, ".vcf");
+ g_return_val_if_fail (uri != NULL, FALSE);
+
+ g_clear_error (&local_error);
- /* kill WEBDAV_CONTACT_ETAG field (might have been set by some other backend) */
- webdav_contact_set_href (contact, NULL);
- webdav_contact_set_etag (contact, NULL);
+ success = e_webdav_session_get_data_sync (bbdav->priv->webdav, uri, &href, &etag, &bytes,
&length, cancellable, &local_error);
+ if (!success && !g_cancellable_is_cancelled (cancellable) &&
+ g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_NOT_FOUND)) {
+ g_free (uri);
+ uri = ebb_webdav_uid_to_uri (bbdav, uid, NULL);
- status = upload_contact (webdav, href, contact, &status_reason, cancellable);
- g_free (href);
+ if (uri) {
+ g_clear_error (&local_error);
- if (status != 201 && status != 204) {
- g_object_unref (contact);
- if (status == 401 || status == 407) {
- webdav_handle_auth_request (webdav, error);
- } else {
- g_set_error (
- error, E_CLIENT_ERROR,
- E_CLIENT_ERROR_OTHER_ERROR,
- _("Create resource “%s” failed with HTTP status %d (%s)"),
- uid, status, status_reason);
+ success = e_webdav_session_get_data_sync (bbdav->priv->webdav, uri, &href,
&etag, &bytes, &length, cancellable, &local_error);
+ }
}
- g_free (uid);
- g_free (status_reason);
- return FALSE;
}
- g_free (status_reason);
- g_free (uid);
+ if (success) {
+ *out_contact = NULL;
- /* PUT request didn't return an etag? try downloading to get one */
- stored_etag = webdav_contact_get_etag (contact);
- if (!stored_etag) {
- gchar *href;
- EContact *new_contact = NULL;
+ if (href && etag && bytes && length != ((gsize) -1)) {
+ EContact *contact;
- href = webdav_contact_get_href (contact);
- if (href) {
- new_contact = download_contact (webdav, href, cancellable);
- g_free (href);
+ contact = e_contact_new_from_vcard (bytes);
+ if (contact) {
+ e_vcard_util_set_x_attribute (E_VCARD (contact), E_WEBDAV_X_ETAG, etag);
+ *out_contact = contact;
+ }
}
- g_object_unref (contact);
-
- if (new_contact == NULL) {
- g_set_error_literal (
- error, E_CLIENT_ERROR,
- E_CLIENT_ERROR_OTHER_ERROR,
- e_client_error_to_string (
- E_CLIENT_ERROR_OTHER_ERROR));
- return FALSE;
+ if (!*out_contact) {
+ success = FALSE;
+ g_propagate_error (&local_error, EDB_ERROR_EX (E_DATA_BOOK_STATUS_OTHER_ERROR,
_("Received object is not a valid vCard")));
}
- contact = new_contact;
- } else {
- g_free (stored_etag);
}
- g_mutex_lock (&webdav->priv->cache_lock);
- e_book_backend_cache_add_contact (webdav->priv->cache, contact);
- g_mutex_unlock (&webdav->priv->cache_lock);
-
- g_queue_push_tail (out_contacts, g_object_ref (contact));
+ g_free (uri);
+ g_free (href);
+ g_free (etag);
+ g_free (bytes);
- g_object_unref (contact);
+ if (local_error)
+ g_propagate_error (error, local_error);
- return TRUE;
+ return success;
}
static gboolean
-book_backend_webdav_modify_contacts_sync (EBookBackend *backend,
- const gchar * const *vcards,
- GQueue *out_contacts,
- GCancellable *cancellable,
- GError **error)
+ebb_webdav_save_contact_sync (EBookMetaBackend *meta_backend,
+ gboolean overwrite_existing,
+ EConflictResolution conflict_resolution,
+ /* const */ EContact *contact,
+ const gchar *extra,
+ gchar **out_new_uid,
+ gchar **out_new_extra,
+ GCancellable *cancellable,
+ GError **error)
{
- EBookBackendWebdav *webdav = E_BOOK_BACKEND_WEBDAV (backend);
- EContact *contact;
- const gchar *uid;
- gchar *href, *etag;
- guint status;
- gchar *status_reason = NULL;
-
- /* We make the assumption that the vCard list we're passed is
- * always exactly one element long, since we haven't specified
- * "bulk-modifies" in our static capability list. This is because
- * there is no clean way to roll back changes in case of an error. */
- if (g_strv_length ((gchar **) vcards) > 1) {
- g_set_error_literal (
- error, E_CLIENT_ERROR,
- E_CLIENT_ERROR_NOT_SUPPORTED,
- _("The backend does not support bulk modifications"));
- return FALSE;
- }
+ EBookBackendWebDAV *bbdav;
+ gchar *href = NULL, *etag = NULL, *uid = NULL;
+ gchar *vcard_string = NULL;
+ gboolean success;
- if (!e_backend_get_online (E_BACKEND (backend))) {
- g_set_error_literal (
- error, E_CLIENT_ERROR,
- E_CLIENT_ERROR_REPOSITORY_OFFLINE,
- e_client_error_to_string (
- E_CLIENT_ERROR_REPOSITORY_OFFLINE));
- return FALSE;
- }
-
- /* modify contact */
- contact = e_contact_new_from_vcard (vcards[0]);
- href = webdav_contact_get_href (contact);
- status = upload_contact (webdav, href, contact, &status_reason, cancellable);
- g_free (href);
- if (status != 200 && status != 201 && status != 204) {
- g_object_unref (contact);
- if (status == 401 || status == 407) {
- webdav_handle_auth_request (webdav, error);
- g_free (status_reason);
- return FALSE;
- }
- /* data changed on server while we were editing */
- if (status == 412) {
- /* too bad no special error code in evolution for this... */
- g_set_error_literal (
- error, E_CLIENT_ERROR,
- E_CLIENT_ERROR_OTHER_ERROR,
- _("Contact on server changed -> not modifying"));
- g_free (status_reason);
- return FALSE;
- }
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_WEBDAV (meta_backend), FALSE);
+ g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+ g_return_val_if_fail (out_new_uid, FALSE);
+ g_return_val_if_fail (out_new_extra, FALSE);
- g_set_error (
- error, E_CLIENT_ERROR,
- E_CLIENT_ERROR_OTHER_ERROR,
- _("Modify contact failed with HTTP status %d (%s)"),
- status, status_reason);
+ bbdav = E_BOOK_BACKEND_WEBDAV (meta_backend);
- g_free (status_reason);
- return FALSE;
- }
+ uid = e_contact_get (contact, E_CONTACT_UID);
+ etag = e_vcard_util_dup_x_attribute (E_VCARD (contact), E_WEBDAV_X_ETAG);
- g_free (status_reason);
+ e_vcard_util_set_x_attribute (E_VCARD (contact), E_WEBDAV_X_ETAG, NULL);
- uid = e_contact_get_const (contact, E_CONTACT_UID);
- g_mutex_lock (&webdav->priv->cache_lock);
- e_book_backend_cache_remove_contact (webdav->priv->cache, uid);
+ vcard_string = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
- etag = webdav_contact_get_etag (contact);
+ if (uid && vcard_string && (!overwrite_existing || (extra && *extra))) {
+ gboolean force_write = FALSE;
- /* PUT request didn't return an etag? try downloading to get one */
- if (etag == NULL || (etag[0] == 'W' && etag[1] == '/')) {
- EContact *new_contact = NULL;
+ if (!extra || !*extra)
+ href = ebb_webdav_uid_to_uri (bbdav, uid, ".vcf");
- href = webdav_contact_get_href (contact);
- if (href) {
- new_contact = download_contact (webdav, href, cancellable);
- g_free (href);
+ if (overwrite_existing) {
+ switch (conflict_resolution) {
+ case E_CONFLICT_RESOLUTION_FAIL:
+ case E_CONFLICT_RESOLUTION_USE_NEWER:
+ case E_CONFLICT_RESOLUTION_KEEP_SERVER:
+ case E_CONFLICT_RESOLUTION_WRITE_COPY:
+ break;
+ case E_CONFLICT_RESOLUTION_KEEP_LOCAL:
+ force_write = TRUE;
+ break;
+ }
}
- if (new_contact != NULL) {
- g_object_unref (contact);
- contact = new_contact;
- }
+ success = e_webdav_session_put_data_sync (bbdav->priv->webdav, (extra && *extra) ? extra :
href,
+ force_write ? "" : overwrite_existing ? etag : NULL, E_WEBDAV_CONTENT_TYPE_VCARD,
+ vcard_string, -1, out_new_extra, NULL, cancellable, error);
+
+ /* To read the component back, because server can change it */
+ if (success)
+ *out_new_uid = g_strdup (uid);
+ } else {
+ success = FALSE;
+ g_propagate_error (error, EDB_ERROR_EX (E_DATA_BOOK_STATUS_OTHER_ERROR, _("Object to save is
not a valid vCard")));
}
+ g_free (vcard_string);
+ g_free (href);
g_free (etag);
+ g_free (uid);
- e_book_backend_cache_add_contact (webdav->priv->cache, contact);
- g_mutex_unlock (&webdav->priv->cache_lock);
-
- g_queue_push_tail (out_contacts, g_object_ref (contact));
-
- g_object_unref (contact);
-
- return TRUE;
+ return success;
}
static gboolean
-book_backend_webdav_remove_contacts_sync (EBookBackend *backend,
- const gchar * const *uids,
- GCancellable *cancellable,
- GError **error)
+ebb_webdav_remove_contact_sync (EBookMetaBackend *meta_backend,
+ EConflictResolution conflict_resolution,
+ const gchar *uid,
+ const gchar *extra,
+ const gchar *object,
+ GCancellable *cancellable,
+ GError **error)
{
- EBookBackendWebdav *webdav = E_BOOK_BACKEND_WEBDAV (backend);
+ EBookBackendWebDAV *bbdav;
EContact *contact;
- gchar *href;
- guint status;
-
- /* We make the assumption that the ID list we're passed is
- * always exactly one element long, since we haven't specified
- * "bulk-removes" in our static capability list. */
- if (g_strv_length ((gchar **) uids) > 1) {
- g_set_error_literal (
- error, E_CLIENT_ERROR,
- E_CLIENT_ERROR_NOT_SUPPORTED,
- _("The backend does not support bulk removals"));
- return FALSE;
- }
-
- if (!e_backend_get_online (E_BACKEND (backend))) {
- g_set_error_literal (
- error, E_CLIENT_ERROR,
- E_CLIENT_ERROR_REPOSITORY_OFFLINE,
- e_client_error_to_string (
- E_CLIENT_ERROR_REPOSITORY_OFFLINE));
- return FALSE;
- }
+ gchar *etag = NULL;
+ gboolean success;
+ GError *local_error = NULL;
- g_mutex_lock (&webdav->priv->cache_lock);
- contact = e_book_backend_cache_get_contact (webdav->priv->cache, uids[0]);
- g_mutex_unlock (&webdav->priv->cache_lock);
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_WEBDAV (meta_backend), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (object != NULL, FALSE);
- if (!contact) {
- g_set_error_literal (
- error, E_BOOK_CLIENT_ERROR,
- E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND,
- e_book_client_error_to_string (
- E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND));
- return FALSE;
- }
+ bbdav = E_BOOK_BACKEND_WEBDAV (meta_backend);
- href = webdav_contact_get_href (contact);
- if (!href) {
- g_object_unref (contact);
- g_set_error (
- error, E_CLIENT_ERROR,
- E_CLIENT_ERROR_OTHER_ERROR,
- _("DELETE failed with HTTP status %d"), SOUP_STATUS_MALFORMED);
+ if (!extra || !*extra) {
+ g_propagate_error (error, EDB_ERROR (E_DATA_BOOK_STATUS_INVALID_ARG));
return FALSE;
}
- status = delete_contact (webdav, href, cancellable);
-
- g_object_unref (contact);
- g_free (href);
-
- if (!SOUP_STATUS_IS_SUCCESSFUL (status)) {
- if (status == 401 || status == 407) {
- webdav_handle_auth_request (webdav, error);
- } else {
- g_set_error (
- error, E_CLIENT_ERROR,
- E_CLIENT_ERROR_OTHER_ERROR,
- _("DELETE failed with HTTP status %d"), status);
- }
+ contact = e_contact_new_from_vcard (object);
+ if (!contact) {
+ g_propagate_error (error, EDB_ERROR (E_DATA_BOOK_STATUS_INVALID_ARG));
return FALSE;
}
- g_mutex_lock (&webdav->priv->cache_lock);
- e_book_backend_cache_remove_contact (webdav->priv->cache, uids[0]);
- g_mutex_unlock (&webdav->priv->cache_lock);
-
- return TRUE;
-}
-
-static EContact *
-book_backend_webdav_get_contact_sync (EBookBackend *backend,
- const gchar *uid,
- GCancellable *cancellable,
- GError **error)
-{
- EBookBackendWebdav *webdav = E_BOOK_BACKEND_WEBDAV (backend);
- EContact *contact;
+ if (conflict_resolution == E_CONFLICT_RESOLUTION_FAIL)
+ etag = e_vcard_util_dup_x_attribute (E_VCARD (contact), E_WEBDAV_X_ETAG);
- g_mutex_lock (&webdav->priv->cache_lock);
- contact = e_book_backend_cache_get_contact (
- webdav->priv->cache, uid);
- g_mutex_unlock (&webdav->priv->cache_lock);
+ success = e_webdav_session_delete_sync (bbdav->priv->webdav, extra,
+ NULL, etag, cancellable, &local_error);
- if (contact && e_backend_get_online (E_BACKEND (backend))) {
+ if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_NOT_FOUND)) {
gchar *href;
- href = webdav_contact_get_href (contact);
- g_object_unref (contact);
-
+ href = ebb_webdav_uid_to_uri (bbdav, uid, ".vcf");
if (href) {
- contact = download_contact (webdav, href, cancellable);
+ g_clear_error (&local_error);
+ success = e_webdav_session_delete_sync (bbdav->priv->webdav, href,
+ NULL, etag, cancellable, &local_error);
+
g_free (href);
- } else {
- contact = NULL;
}
- /* update cache as we possibly have changes */
- if (contact != NULL) {
- g_mutex_lock (&webdav->priv->cache_lock);
- e_book_backend_cache_remove_contact (
- webdav->priv->cache, uid);
- e_book_backend_cache_add_contact (
- webdav->priv->cache, contact);
- g_mutex_unlock (&webdav->priv->cache_lock);
+ if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_NOT_FOUND)) {
+ href = ebb_webdav_uid_to_uri (bbdav, uid, NULL);
+ if (href) {
+ g_clear_error (&local_error);
+ success = e_webdav_session_delete_sync (bbdav->priv->webdav, href,
+ NULL, etag, cancellable, &local_error);
+
+ g_free (href);
+ }
}
}
- if (contact == NULL) {
- g_set_error_literal (
- error, E_BOOK_CLIENT_ERROR,
- E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND,
- e_book_client_error_to_string (
- E_BOOK_CLIENT_ERROR_CONTACT_NOT_FOUND));
- return FALSE;
- }
+ g_object_unref (contact);
+ g_free (etag);
- return contact;
+ if (local_error)
+ g_propagate_error (error, local_error);
+
+ return success;
}
-static gboolean
-book_backend_webdav_get_contact_list_sync (EBookBackend *backend,
- const gchar *query,
- GQueue *out_contacts,
- GCancellable *cancellable,
- GError **error)
+static gchar *
+ebb_webdav_get_backend_property (EBookBackend *book_backend,
+ const gchar *prop_name)
{
- EBookBackendWebdav *webdav = E_BOOK_BACKEND_WEBDAV (backend);
- GList *contact_list;
-
- if (e_backend_get_online (E_BACKEND (backend)) &&
- e_source_get_connection_status (e_backend_get_source (E_BACKEND (backend))) ==
E_SOURCE_CONNECTION_STATUS_CONNECTED) {
- /* make sure the cache is up to date */
- if (!download_contacts (webdav, NULL, NULL, FALSE, cancellable, error))
- return FALSE;
- }
+ g_return_val_if_fail (E_IS_BOOK_BACKEND_WEBDAV (book_backend), NULL);
+ g_return_val_if_fail (prop_name != NULL, NULL);
- /* answer query from cache */
- g_mutex_lock (&webdav->priv->cache_lock);
- contact_list = e_book_backend_cache_get_contacts (
- webdav->priv->cache, query);
- g_mutex_unlock (&webdav->priv->cache_lock);
-
- /* This appends contact_list to out_contacts, one element at a
- * time, since GLib lacks something like g_queue_append_list().
- *
- * XXX Would be better if e_book_backend_cache_get_contacts()
- * took an output GQueue instead of returning a GList. */
- while (contact_list != NULL) {
- GList *link = contact_list;
- contact_list = g_list_remove_link (contact_list, link);
- g_queue_push_tail_link (out_contacts, link);
+ if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
+ return g_strjoin (",",
+ "net",
+ "do-initial-query",
+ "contact-lists",
+ e_book_meta_backend_get_capabilities (E_BOOK_META_BACKEND (book_backend)),
+ NULL);
}
- return TRUE;
+ /* Chain up to parent's method. */
+ return E_BOOK_BACKEND_CLASS (e_book_backend_webdav_parent_class)->get_backend_property (book_backend,
prop_name);
}
-static ESourceAuthenticationResult
-book_backend_webdav_authenticate_sync (EBackend *backend,
- const ENamedParameters *credentials,
- gchar **out_certificate_pem,
- GTlsCertificateFlags *out_certificate_errors,
- GCancellable *cancellable,
- GError **error)
+static gchar *
+ebb_webdav_dup_contact_revision_cb (EBookCache *book_cache,
+ EContact *contact)
{
- EBookBackendWebdav *webdav = E_BOOK_BACKEND_WEBDAV (backend);
- ESourceAuthentication *auth_extension;
- ESourceAuthenticationResult result;
- ESource *source;
- const gchar *username;
- GError *local_error = NULL;
+ g_return_val_if_fail (E_IS_CONTACT (contact), NULL);
- source = e_backend_get_source (backend);
- auth_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
+ return e_vcard_util_dup_x_attribute (E_VCARD (contact), E_WEBDAV_X_ETAG);
+}
- g_free (webdav->priv->username);
- webdav->priv->username = NULL;
+static void
+e_book_backend_webdav_constructed (GObject *object)
+{
+ EBookBackendWebDAV *bbdav = E_BOOK_BACKEND_WEBDAV (object);
+ EBookCache *book_cache;
- g_free (webdav->priv->password);
- webdav->priv->password = g_strdup (e_named_parameters_get (credentials,
E_SOURCE_CREDENTIAL_PASSWORD));
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_book_backend_webdav_parent_class)->constructed (object);
- username = e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_USERNAME);
- if (username && *username) {
- webdav->priv->username = g_strdup (username);
- } else {
- webdav->priv->username = e_source_authentication_dup_user (auth_extension);
- }
+ book_cache = e_book_meta_backend_ref_cache (E_BOOK_META_BACKEND (bbdav));
- if (book_backend_webdav_test_can_connect (webdav, out_certificate_pem, out_certificate_errors,
cancellable, &local_error)) {
- result = E_SOURCE_AUTHENTICATION_ACCEPTED;
- } else if (g_error_matches (local_error, E_CLIENT_ERROR, E_CLIENT_ERROR_AUTHENTICATION_FAILED) ||
- g_error_matches (local_error, E_CLIENT_ERROR, E_CLIENT_ERROR_AUTHENTICATION_REQUIRED)) {
- if (!e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_PASSWORD) ||
- g_error_matches (local_error, E_CLIENT_ERROR, E_CLIENT_ERROR_AUTHENTICATION_REQUIRED))
- result = E_SOURCE_AUTHENTICATION_REQUIRED;
- else
- result = E_SOURCE_AUTHENTICATION_REJECTED;
- g_clear_error (&local_error);
- } else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
- result = E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED;
- g_propagate_error (error, local_error);
- } else {
- result = E_SOURCE_AUTHENTICATION_ERROR;
- g_propagate_error (error, local_error);
- }
+ g_signal_connect (book_cache, "dup-contact-revision",
+ G_CALLBACK (ebb_webdav_dup_contact_revision_cb), NULL);
- return result;
+ g_clear_object (&book_cache);
}
-static gboolean
-e_book_backend_webdav_refresh_sync (EBookBackend *book_backend,
- GCancellable *cancellable,
- GError **error)
+static void
+e_book_backend_webdav_dispose (GObject *object)
{
- EBackend *backend;
-
- g_return_val_if_fail (E_IS_BOOK_BACKEND_WEBDAV (book_backend), FALSE);
+ EBookBackendWebDAV *bbdav = E_BOOK_BACKEND_WEBDAV (object);
- backend = E_BACKEND (book_backend);
+ g_clear_object (&bbdav->priv->webdav);
- if (!e_backend_get_online (backend) &&
- e_backend_is_destination_reachable (backend, cancellable, NULL)) {
- e_backend_set_online (backend, TRUE);
- }
-
- if (e_backend_get_online (backend) && !g_cancellable_is_cancelled (cancellable)) {
- return download_contacts (E_BOOK_BACKEND_WEBDAV (book_backend), NULL, NULL, TRUE,
cancellable, error);
- }
-
- return TRUE;
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_book_backend_webdav_parent_class)->dispose (object);
}
static void
-e_book_backend_webdav_class_init (EBookBackendWebdavClass *class)
+e_book_backend_webdav_init (EBookBackendWebDAV *bbdav)
{
- GObjectClass *object_class;
- EBackendClass *backend_class;
- EBookBackendClass *book_backend_class;
-
- g_type_class_add_private (class, sizeof (EBookBackendWebdavPrivate));
-
- object_class = G_OBJECT_CLASS (class);
- object_class->dispose = book_backend_webdav_dispose;
- object_class->finalize = book_backend_webdav_finalize;
-
- backend_class = E_BACKEND_CLASS (class);
- backend_class->authenticate_sync = book_backend_webdav_authenticate_sync;
-
- book_backend_class = E_BOOK_BACKEND_CLASS (class);
- book_backend_class->get_backend_property = book_backend_webdav_get_backend_property;
- book_backend_class->open_sync = book_backend_webdav_open_sync;
- book_backend_class->create_contacts_sync = book_backend_webdav_create_contacts_sync;
- book_backend_class->modify_contacts_sync = book_backend_webdav_modify_contacts_sync;
- book_backend_class->remove_contacts_sync = book_backend_webdav_remove_contacts_sync;
- book_backend_class->get_contact_sync = book_backend_webdav_get_contact_sync;
- book_backend_class->get_contact_list_sync = book_backend_webdav_get_contact_list_sync;
- book_backend_class->start_view = e_book_backend_webdav_start_view;
- book_backend_class->stop_view = e_book_backend_webdav_stop_view;
- book_backend_class->refresh_sync = e_book_backend_webdav_refresh_sync;
+ bbdav->priv = G_TYPE_INSTANCE_GET_PRIVATE (bbdav, E_TYPE_BOOK_BACKEND_WEBDAV,
EBookBackendWebDAVPrivate);
}
static void
-e_book_backend_webdav_init (EBookBackendWebdav *backend)
+e_book_backend_webdav_class_init (EBookBackendWebDAVClass *klass)
{
- backend->priv = E_BOOK_BACKEND_WEBDAV_GET_PRIVATE (backend);
-
- g_mutex_init (&backend->priv->cache_lock);
- g_mutex_init (&backend->priv->update_lock);
-
- g_signal_connect (
- backend, "notify::online",
- G_CALLBACK (e_book_backend_webdav_notify_online_cb), NULL);
+ GObjectClass *object_class;
+ EBookBackendClass *book_backend_class;
+ EBookMetaBackendClass *book_meta_backend_class;
+
+ g_type_class_add_private (klass, sizeof (EBookBackendWebDAVPrivate));
+
+ book_meta_backend_class = E_BOOK_META_BACKEND_CLASS (klass);
+ book_meta_backend_class->backend_module_filename = "libebookbackendwebdav.so";
+ book_meta_backend_class->backend_factory_type_name = "EBookBackendWebdavFactory";
+ book_meta_backend_class->connect_sync = ebb_webdav_connect_sync;
+ book_meta_backend_class->disconnect_sync = ebb_webdav_disconnect_sync;
+ book_meta_backend_class->get_changes_sync = ebb_webdav_get_changes_sync;
+ book_meta_backend_class->list_existing_sync = ebb_webdav_list_existing_sync;
+ book_meta_backend_class->load_contact_sync = ebb_webdav_load_contact_sync;
+ book_meta_backend_class->save_contact_sync = ebb_webdav_save_contact_sync;
+ book_meta_backend_class->remove_contact_sync = ebb_webdav_remove_contact_sync;
+
+ book_backend_class = E_BOOK_BACKEND_CLASS (klass);
+ book_backend_class->get_backend_property = ebb_webdav_get_backend_property;
+
+ object_class = G_OBJECT_CLASS (klass);
+ object_class->constructed = e_book_backend_webdav_constructed;
+ object_class->dispose = e_book_backend_webdav_dispose;
}
-
diff --git a/src/addressbook/backends/webdav/e-book-backend-webdav.h
b/src/addressbook/backends/webdav/e-book-backend-webdav.h
index e7b1ed0..93162fb 100644
--- a/src/addressbook/backends/webdav/e-book-backend-webdav.h
+++ b/src/addressbook/backends/webdav/e-book-backend-webdav.h
@@ -27,10 +27,10 @@
(e_book_backend_webdav_get_type ())
#define E_BOOK_BACKEND_WEBDAV(obj) \
(G_TYPE_CHECK_INSTANCE_CAST \
- ((obj), E_TYPE_BOOK_BACKEND_WEBDAV, EBookBackendWebdav))
+ ((obj), E_TYPE_BOOK_BACKEND_WEBDAV, EBookBackendWebDAV))
#define E_BOOK_BACKEND_WEBDAV_CLASS(cls) \
(G_TYPE_CHECK_CLASS_CAST \
- ((cls), E_TYPE_BOOK_BACKEND_WEBDAV, EBookBackendWebdavClass))
+ ((cls), E_TYPE_BOOK_BACKEND_WEBDAV, EBookBackendWebDAVClass))
#define E_IS_BOOK_BACKEND_WEBDAV(obj) \
(G_TYPE_CHECK_INSTANCE_TYPE \
((obj), E_TYPE_BOOK_BACKEND_WEBDAV))
@@ -39,21 +39,21 @@
((cls), E_TYPE_BOOK_BACKEND_WEBDAV))
#define E_BOOK_BACKEND_WEBDAV_GET_CLASS(cls) \
(G_TYPE_INSTANCE_GET_CLASS \
- ((obj), E_TYPE_BOOK_BACKEND_WEBDAV, EBookBackendWebdavClass))
+ ((obj), E_TYPE_BOOK_BACKEND_WEBDAV, EBookBackendWebDAVClass))
G_BEGIN_DECLS
-typedef struct _EBookBackendWebdav EBookBackendWebdav;
-typedef struct _EBookBackendWebdavClass EBookBackendWebdavClass;
-typedef struct _EBookBackendWebdavPrivate EBookBackendWebdavPrivate;
+typedef struct _EBookBackendWebDAV EBookBackendWebDAV;
+typedef struct _EBookBackendWebDAVClass EBookBackendWebDAVClass;
+typedef struct _EBookBackendWebDAVPrivate EBookBackendWebDAVPrivate;
-struct _EBookBackendWebdav {
- EBookBackend parent;
- EBookBackendWebdavPrivate *priv;
+struct _EBookBackendWebDAV {
+ EBookMetaBackend parent;
+ EBookBackendWebDAVPrivate *priv;
};
-struct _EBookBackendWebdavClass {
- EBookBackendClass parent_class;
+struct _EBookBackendWebDAVClass {
+ EBookMetaBackendClass parent_class;
};
GType e_book_backend_webdav_get_type (void);
@@ -61,4 +61,3 @@ GType e_book_backend_webdav_get_type (void);
G_END_DECLS
#endif /* E_BOOK_BACKEND_WEBDAV_H */
-
diff --git a/src/addressbook/libebook-contacts/e-vcard.c b/src/addressbook/libebook-contacts/e-vcard.c
index b969e41..f9d897c 100644
--- a/src/addressbook/libebook-contacts/e-vcard.c
+++ b/src/addressbook/libebook-contacts/e-vcard.c
@@ -2034,6 +2034,11 @@ e_vcard_attribute_remove_param (EVCardAttribute *attr,
param = l->data;
if (g_ascii_strcasecmp (e_vcard_attribute_param_get_name (param),
param_name) == 0) {
+ if (g_ascii_strcasecmp (param_name, EVC_ENCODING) == 0) {
+ attr->encoding_set = FALSE;
+ attr->encoding = EVC_ENCODING_RAW;
+ }
+
attr->params = g_list_delete_link (attr->params, l);
e_vcard_attribute_param_free (param);
break;
@@ -2870,3 +2875,72 @@ e_vcard_attribute_param_get_values (EVCardAttributeParam *param)
return param->values;
}
+
+/**
+ * e_vcard_util_set_x_attribute:
+ * @vcard: an #EVCard
+ * @x_name: the attribute name, which starts with "X-"
+ * @value: (nullable): the value to set, or %NULL to unset
+ *
+ * Sets an "X-" attribute @x_name to value @value in @vcard, or
+ * removes it from @vcard, when @value is %NULL.
+ *
+ * Since: 3.26
+ **/
+void
+e_vcard_util_set_x_attribute (EVCard *vcard,
+ const gchar *x_name,
+ const gchar *value)
+{
+ EVCardAttribute *attr;
+
+ g_return_if_fail (E_IS_VCARD (vcard));
+ g_return_if_fail (x_name != NULL);
+ g_return_if_fail (g_str_has_prefix (x_name, "X-"));
+
+ attr = e_vcard_get_attribute (vcard, x_name);
+
+ if (attr) {
+ e_vcard_attribute_remove_values (attr);
+ if (value) {
+ e_vcard_attribute_add_value (attr, value);
+ } else {
+ e_vcard_remove_attribute (vcard, attr);
+ }
+ } else if (value) {
+ e_vcard_append_attribute_with_value (
+ vcard,
+ e_vcard_attribute_new (NULL, x_name),
+ value);
+ }
+}
+
+/**
+ * e_vcard_util_dup_x_attribute:
+ * @vcard: an #EVCard
+ * @x_name: the attribute name, which starts with "X-"
+ *
+ * Returns: (nullable) (transfer-full): Value of attribute @x_name, or %NULL,
+ * when there is no such attribute. Free the returned pointer with g_free(),
+ * when no longer needed.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_vcard_util_dup_x_attribute (EVCard *vcard,
+ const gchar *x_name)
+{
+ EVCardAttribute *attr;
+ GList *v = NULL;
+
+ g_return_val_if_fail (E_IS_VCARD (vcard), NULL);
+ g_return_val_if_fail (x_name != NULL, NULL);
+ g_return_val_if_fail (g_str_has_prefix (x_name, "X-"), NULL);
+
+ attr = e_vcard_get_attribute (vcard, x_name);
+
+ if (attr)
+ v = e_vcard_attribute_get_values (attr);
+
+ return ((v && v->data) ? g_strstrip (g_strdup (v->data)) : NULL);
+}
diff --git a/src/addressbook/libebook-contacts/e-vcard.h b/src/addressbook/libebook-contacts/e-vcard.h
index e3f9ed0..9efcb3d 100644
--- a/src/addressbook/libebook-contacts/e-vcard.h
+++ b/src/addressbook/libebook-contacts/e-vcard.h
@@ -316,6 +316,12 @@ gboolean e_vcard_attribute_has_type (EVCardAttribute *attr, cons
gchar * e_vcard_escape_string (const gchar *s);
gchar * e_vcard_unescape_string (const gchar *s);
+void e_vcard_util_set_x_attribute (EVCard *vcard,
+ const gchar *x_name,
+ const gchar *value);
+gchar * e_vcard_util_dup_x_attribute (EVCard *vcard,
+ const gchar *x_name);
+
G_END_DECLS
#endif /* _EVCARD_H */
diff --git a/src/addressbook/libedata-book/CMakeLists.txt b/src/addressbook/libedata-book/CMakeLists.txt
index a247c7b..2bac3a9 100644
--- a/src/addressbook/libedata-book/CMakeLists.txt
+++ b/src/addressbook/libedata-book/CMakeLists.txt
@@ -16,9 +16,12 @@ set(SOURCES
e-book-backend-cache.c
e-book-backend-sqlitedb.c
e-book-backend.c
+ e-book-cache.c
+ e-book-meta-backend.c
e-book-sqlite.c
e-data-book.c
e-data-book-cursor.c
+ e-data-book-cursor-cache.c
e-data-book-cursor-sqlite.c
e-data-book-direct.c
e-data-book-factory.c
@@ -33,15 +36,18 @@ set(HEADERS
e-book-backend-sexp.h
e-book-backend-summary.h
e-book-backend.h
+ e-book-cache.h
+ e-book-meta-backend.h
+ e-book-sqlite.h
e-data-book-factory.h
e-data-book-view.h
e-data-book.h
e-data-book-cursor.h
+ e-data-book-cursor-cache.h
e-data-book-cursor-sqlite.h
e-data-book-direct.h
e-book-backend-cache.h
e-book-backend-sqlitedb.h
- e-book-sqlite.h
e-subprocess-book-factory.h
)
diff --git a/src/addressbook/libedata-book/e-book-backend.c b/src/addressbook/libedata-book/e-book-backend.c
index 9efac67..45aa092 100644
--- a/src/addressbook/libedata-book/e-book-backend.c
+++ b/src/addressbook/libedata-book/e-book-backend.c
@@ -67,6 +67,7 @@ struct _EBookBackendPrivate {
GQueue pending_operations;
guint32 next_operation_id;
GSimpleAsyncResult *blocked;
+ gboolean blocked_by_custom_op;
};
struct _AsyncContext {
@@ -98,6 +99,11 @@ struct _DispatchNode {
GSimpleAsyncResult *simple;
GCancellable *cancellable;
+
+ GWeakRef *book_backend_weak_ref;
+ EBookBackendCustomOpFunc custom_func;
+ gpointer custom_func_user_data;
+ GDestroyNotify custom_func_user_data_free;
};
enum {
@@ -146,6 +152,12 @@ dispatch_node_free (DispatchNode *dispatch_node)
g_clear_object (&dispatch_node->simple);
g_clear_object (&dispatch_node->cancellable);
+ if (dispatch_node->custom_func_user_data_free)
+ dispatch_node->custom_func_user_data_free (dispatch_node->custom_func_user_data);
+
+ if (dispatch_node->book_backend_weak_ref)
+ e_weak_ref_free (dispatch_node->book_backend_weak_ref);
+
g_slice_free (DispatchNode, dispatch_node);
}
@@ -176,13 +188,35 @@ book_backend_push_operation (EBookBackend *backend,
g_mutex_unlock (&backend->priv->operation_lock);
}
+static void book_backend_unblock_operations (EBookBackend *backend, GSimpleAsyncResult *simple);
+
static void
book_backend_dispatch_thread (DispatchNode *node)
{
GCancellable *cancellable = node->cancellable;
GError *local_error = NULL;
- if (g_cancellable_set_error_if_cancelled (cancellable, &local_error)) {
+ if (node->custom_func) {
+ EBookBackend *book_backend;
+
+ book_backend = g_weak_ref_get (node->book_backend_weak_ref);
+ if (book_backend &&
+ !g_cancellable_is_cancelled (cancellable)) {
+ node->custom_func (book_backend, node->custom_func_user_data, cancellable,
&local_error);
+
+ if (local_error) {
+ if (!g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ e_book_backend_notify_error (book_backend, local_error->message);
+
+ g_clear_error (&local_error);
+ }
+ }
+
+ if (book_backend) {
+ book_backend_unblock_operations (book_backend, NULL);
+ e_util_unref_in_thread (book_backend);
+ }
+ } else if (g_cancellable_set_error_if_cancelled (cancellable, &local_error)) {
g_simple_async_result_take_error (node->simple, local_error);
g_simple_async_result_complete_in_idle (node->simple);
} else {
@@ -207,7 +241,8 @@ book_backend_dispatch_next_operation (EBookBackend *backend)
/* We can't dispatch additional operations
* while a blocking operation is in progress. */
- if (backend->priv->blocked != NULL) {
+ if (backend->priv->blocked != NULL ||
+ backend->priv->blocked_by_custom_op) {
g_mutex_unlock (&backend->priv->operation_lock);
return FALSE;
}
@@ -221,8 +256,12 @@ book_backend_dispatch_next_operation (EBookBackend *backend)
/* If this a blocking operation, block any
* further dispatching until this finishes. */
- if (node->blocking_operation)
- backend->priv->blocked = g_object_ref (node->simple);
+ if (node->blocking_operation) {
+ if (node->simple)
+ backend->priv->blocked = g_object_ref (node->simple);
+ else
+ backend->priv->blocked_by_custom_op = TRUE;
+ }
g_mutex_unlock (&backend->priv->operation_lock);
@@ -244,6 +283,7 @@ book_backend_unblock_operations (EBookBackend *backend,
g_mutex_lock (&backend->priv->operation_lock);
if (backend->priv->blocked == simple)
g_clear_object (&backend->priv->blocked);
+ backend->priv->blocked_by_custom_op = FALSE;
g_mutex_unlock (&backend->priv->operation_lock);
while (book_backend_dispatch_next_operation (backend))
@@ -705,6 +745,7 @@ e_book_backend_class_init (EBookBackendClass *class)
backend_class = E_BACKEND_CLASS (class);
backend_class->prepare_shutdown = book_backend_prepare_shutdown;
+ class->use_serial_dispatch_queue = TRUE;
class->get_backend_property = book_backend_get_backend_property;
class->get_contact_list_uids_sync = book_backend_get_contact_list_uids_sync;
class->notify_update = book_backend_notify_update;
@@ -3640,3 +3681,53 @@ e_book_backend_delete_cursor (EBookBackend *backend,
return success;
}
+
+/**
+ * e_book_backend_schedule_custom_operation:
+ * @book_backend: an #EBookBackend
+ * @use_cancellable: (nullable): an optional #GCancellable to use for @func
+ * @func: a function to call in a dedicated thread
+ * @user_data: user data being passed to @func
+ * @user_data_free: (nullable): optional destroy call back for @user_data
+ *
+ * Schedules user function @func to be run in a dedicated thread as
+ * a blocking operation.
+ *
+ * The function adds its own reference to @use_cancellable, if not %NULL.
+ *
+ * The error returned from @func is propagated to client using
+ * e_book_backend_notify_error() function. If it's not desired,
+ * then left the error unchanged and notify about errors manually.
+ *
+ * Since: 3.26
+ **/
+void
+e_book_backend_schedule_custom_operation (EBookBackend *book_backend,
+ GCancellable *use_cancellable,
+ EBookBackendCustomOpFunc func,
+ gpointer user_data,
+ GDestroyNotify user_data_free)
+{
+ DispatchNode *node;
+
+ g_return_if_fail (E_IS_BOOK_BACKEND (book_backend));
+ g_return_if_fail (func != NULL);
+
+ g_mutex_lock (&book_backend->priv->operation_lock);
+
+ node = g_slice_new0 (DispatchNode);
+ node->blocking_operation = TRUE;
+ node->book_backend_weak_ref = e_weak_ref_new (book_backend);
+ node->custom_func = func;
+ node->custom_func_user_data = user_data;
+ node->custom_func_user_data_free = user_data_free;
+
+ if (G_IS_CANCELLABLE (use_cancellable))
+ node->cancellable = g_object_ref (use_cancellable);
+
+ g_queue_push_tail (&book_backend->priv->pending_operations, node);
+
+ g_mutex_unlock (&book_backend->priv->operation_lock);
+
+ book_backend_dispatch_next_operation (book_backend);
+}
diff --git a/src/addressbook/libedata-book/e-book-backend.h b/src/addressbook/libedata-book/e-book-backend.h
index a9b7bd5..cffaa40 100644
--- a/src/addressbook/libedata-book/e-book-backend.h
+++ b/src/addressbook/libedata-book/e-book-backend.h
@@ -113,7 +113,7 @@ struct _EBookBackend {
/**
* EBookBackendClass:
* @use_serial_dispatch_queue: Whether a serial dispatch queue should
- * be used for this backend or not.
+ * be used for this backend or not. The default is %TRUE.
* @get_backend_property: Fetch a property value by name from the backend
* @open_sync: Open the backend
* @refresh_sync: Refresh the backend
@@ -475,6 +475,30 @@ GSimpleAsyncResult *
(EBookBackend *backend,
guint32 opid,
GQueue **result_queue);
+/**
+ * EBookBackendCustomOpFunc:
+ * @book_backend: an #EBookBackend
+ * @user_data: a function user data, as provided to e_book_backend_schedule_custom_operation()
+ * @cancellable: an optional #GCancellable, as provided to e_book_backend_schedule_custom_operation()
+ * @error: return location for a #GError, or %NULL
+ *
+ * A callback prototype being called in a dedicated thread, scheduled
+ * by e_book_backend_schedule_custom_operation().
+ *
+ * Since: 3.26
+ **/
+typedef void (* EBookBackendCustomOpFunc) (EBookBackend *book_backend,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error);
+
+void e_book_backend_schedule_custom_operation
+ (EBookBackend *book_backend,
+ GCancellable *use_cancellable,
+ EBookBackendCustomOpFunc func,
+ gpointer user_data,
+ GDestroyNotify user_data_free);
+
G_END_DECLS
diff --git a/src/addressbook/libedata-book/e-book-cache.c b/src/addressbook/libedata-book/e-book-cache.c
new file mode 100644
index 0000000..cb1fda2
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-cache.c
@@ -0,0 +1,6113 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2013 Intel Corporation
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Tristan Van Berkom <tristanvb openismus com>
+ */
+
+/**
+ * SECTION: e-book-cache
+ * @include: libedata-book/libedata-book.h
+ * @short_description: An #ECache descendant for addressbooks
+ *
+ * The #EBookCache is an API for storing and looking up #EContacts
+ * in an #ECache. It also supports cursors.
+ *
+ * The API is thread safe, in the similar way as the #ECache is.
+ *
+ * Any operations which can take a lot of time to complete (depending
+ * on the size of your addressbook) can be cancelled using a #GCancellable.
+ *
+ * Depending on your summary configuration, your mileage will vary. Refer
+ * to the #ESourceBackendSummarySetup for configuring your addressbook
+ * for the type of usage you mean to make of it.
+ **/
+
+#include "evolution-data-server-config.h"
+
+#include <locale.h>
+#include <string.h>
+#include <errno.h>
+#include <sqlite3.h>
+
+#include <glib/gi18n-lib.h>
+#include <glib/gstdio.h>
+
+#include "e-book-backend-sexp.h"
+
+#include "e-book-cache.h"
+
+#define E_BOOK_CACHE_VERSION 1
+#define INSERT_MULTI_STMT_BYTES 128
+#define COLUMN_DEFINITION_BYTES 32
+#define GENERATED_QUERY_BYTES 1024
+
+/* We use a 64 bitmask to track which auxiliary tables
+ * are needed to satisfy a query, it's doubtful that
+ * anyone will need an addressbook with 64 fields configured
+ * in the summary.
+ */
+#define EBC_MAX_SUMMARY_FIELDS 64
+
+/* The number of SQLite virtual machine instructions that are
+ * evaluated at a time, the user passed GCancellable is
+ * checked between each batch of evaluated instructions.
+ */
+#define EBC_CANCEL_BATCH_SIZE 200
+
+#define EBC_ESCAPE_SEQUENCE "ESCAPE '^'"
+
+/* Names for custom functions */
+#define EBC_FUNC_COMPARE_VCARD "compare_vcard"
+#define EBC_FUNC_EQPHONE_EXACT "eqphone_exact"
+#define EBC_FUNC_EQPHONE_NATIONAL "eqphone_national"
+#define EBC_FUNC_EQPHONE_SHORT "eqphone_short"
+
+/* Fallback collations are generated as with a prefix and an EContactField name */
+#define EBC_COLLATE_PREFIX "book_cache_"
+
+/* A special vcard attribute that we use only for private vcards */
+#define EBC_VCARD_SORT_KEY "X-EVOLUTION-SORT-KEY"
+
+/* Key names for e_cache_dup/set_key{_int} functions */
+#define EBC_KEY_MULTIVALUES "multivalues"
+#define EBC_KEY_LC_COLLATE "lc_collate"
+#define EBC_KEY_COUNTRYCODE "countrycode"
+
+/* Suffixes for column names used to store specialized data */
+#define EBC_SUFFIX_REVERSE "reverse"
+#define EBC_SUFFIX_SORT_KEY "localized"
+#define EBC_SUFFIX_PHONE "phone"
+#define EBC_SUFFIX_COUNTRY "country"
+
+/* Track EBookIndexType's in a bit mask */
+#define INDEX_FLAG(type) (1 << E_BOOK_INDEX_##type)
+
+#define EBC_COLUMN_EXTRA "bdata"
+
+typedef struct {
+ EContactField field_id; /* The EContact field */
+ GType type; /* The GType (only support string or gboolean) */
+ const gchar *dbname; /* The key for this field in the sqlite3 table */
+ gint index; /* Types of searches this field should support (see EBookIndexType) */
+ gchar *dbname_idx_suffix; /* dbnames for various indexes; can be NULL */
+ gchar *dbname_idx_phone;
+ gchar *dbname_idx_country;
+ gchar *dbname_idx_sort_key;
+ gchar *aux_table; /* Name of auxiliary table for this field, for multivalued fields
only */
+ gchar *aux_table_symbolic; /* Symbolic name of auxiliary table used in queries */
+} SummaryField;
+
+struct _EBookCachePrivate {
+ ESource *source; /* Optional, can be %NULL */
+
+ /* Parameters and settings */
+ gchar *locale; /* The current locale */
+ gchar *region_code; /* Region code (for phone number parsing) */
+
+ /* Summary configuration */
+ SummaryField *summary_fields;
+ gint n_summary_fields;
+
+ ECollator *collator; /* The ECollator to create sort keys for any sortable fields */
+};
+
+enum {
+ PROP_0,
+ PROP_LOCALE
+};
+
+enum {
+ E164_CHANGED,
+ DUP_CONTACT_REVISION,
+ LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL];
+
+G_DEFINE_TYPE_WITH_CODE (EBookCache, e_book_cache, E_TYPE_CACHE,
+ G_IMPLEMENT_INTERFACE (E_TYPE_EXTENSIBLE, NULL))
+
+G_DEFINE_BOXED_TYPE (EBookCacheSearchData, e_book_cache_search_data, e_book_cache_search_data_copy,
e_book_cache_search_data_free)
+
+/**
+ * e_book_cache_search_data_new:
+ * @uid: a contact UID; cannot be %NULL
+ * @vcard: the contact as a vCard string; cannot be %NULL
+ * @extra: (nullable): any extra data stored with the contact, or %NULL
+ *
+ * Creates a new EBookCacheSearchData prefilled with the given values.
+ *
+ * Returns: (transfer full): A new #EBookCacheSearchData. Free it with
+ * e_book_cache_search_data_free() when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EBookCacheSearchData *
+e_book_cache_search_data_new (const gchar *uid,
+ const gchar *vcard,
+ const gchar *extra)
+{
+ EBookCacheSearchData *data;
+
+ g_return_val_if_fail (uid != NULL, NULL);
+ g_return_val_if_fail (vcard != NULL, NULL);
+
+ data = g_new0 (EBookCacheSearchData, 1);
+ data->uid = g_strdup (uid);
+ data->vcard = g_strdup (vcard);
+ data->extra = g_strdup (extra);
+
+ return data;
+}
+
+/**
+ * e_book_cache_search_data_copy:
+ * @data: (nullable): a source #EBookCacheSearchData to copy, or %NULL
+ *
+ * Returns: (transfer full): Copy of the given @data. Free it with
+ * e_book_cache_search_data_free() when no longer needed.
+ * If the @data is %NULL, then returns %NULL as well.
+ *
+ * Since: 3.26
+ **/
+EBookCacheSearchData *
+e_book_cache_search_data_copy (const EBookCacheSearchData *data)
+{
+ if (!data)
+ return NULL;
+
+ return e_book_cache_search_data_new (data->uid, data->vcard, data->extra);
+}
+
+/**
+ * e_book_cache_search_data_free:
+ * @data: (nullable): an #EBookCacheSearchData
+ *
+ * Frees the @data structure, previously allocated with e_book_cache_search_data_new()
+ * or e_book_cache_search_data_copy().
+ *
+ * Since: 3.26
+ **/
+void
+e_book_cache_search_data_free (gpointer ptr)
+{
+ EBookCacheSearchData *data = ptr;
+
+ if (data) {
+ g_free (data->uid);
+ g_free (data->vcard);
+ g_free (data->extra);
+ g_free (data);
+ }
+}
+
+/* Default summary configuration */
+static EContactField default_summary_fields[] = {
+ E_CONTACT_UID,
+ E_CONTACT_REV,
+ E_CONTACT_FILE_AS,
+ E_CONTACT_NICKNAME,
+ E_CONTACT_FULL_NAME,
+ E_CONTACT_GIVEN_NAME,
+ E_CONTACT_FAMILY_NAME,
+ E_CONTACT_EMAIL,
+ E_CONTACT_TEL,
+ E_CONTACT_IS_LIST,
+ E_CONTACT_LIST_SHOW_ADDRESSES,
+ E_CONTACT_WANTS_HTML,
+ E_CONTACT_X509_CERT
+};
+
+/* Create indexes on full_name and email fields as autocompletion
+ * queries would mainly rely on this.
+ *
+ * Add sort keys for name fields as those are likely targets for
+ * cursor usage.
+ */
+static EContactField default_indexed_fields[] = {
+ E_CONTACT_FULL_NAME,
+ E_CONTACT_NICKNAME,
+ E_CONTACT_FILE_AS,
+ E_CONTACT_GIVEN_NAME,
+ E_CONTACT_FAMILY_NAME,
+ E_CONTACT_EMAIL,
+ E_CONTACT_FILE_AS,
+ E_CONTACT_FAMILY_NAME,
+ E_CONTACT_GIVEN_NAME
+};
+
+static EBookIndexType default_index_types[] = {
+ E_BOOK_INDEX_PREFIX,
+ E_BOOK_INDEX_PREFIX,
+ E_BOOK_INDEX_PREFIX,
+ E_BOOK_INDEX_PREFIX,
+ E_BOOK_INDEX_PREFIX,
+ E_BOOK_INDEX_PREFIX,
+ E_BOOK_INDEX_SORT_KEY,
+ E_BOOK_INDEX_SORT_KEY,
+ E_BOOK_INDEX_SORT_KEY
+};
+
+/******************************************************
+ * Summary Fields *
+ ******************************************************/
+
+static ECacheColumnInfo *
+column_info_new (SummaryField *field,
+ const gchar *column_name,
+ const gchar *column_type,
+ const gchar *idx_prefix)
+{
+ ECacheColumnInfo *info;
+ gchar *index = NULL;
+
+ g_return_val_if_fail (column_name != NULL, NULL);
+
+ if (field->type == E_TYPE_CONTACT_ATTR_LIST)
+ column_name = "value";
+
+ if (!column_type) {
+ if (field->type == G_TYPE_STRING)
+ column_type = "TEXT";
+ else if (field->type == G_TYPE_BOOLEAN || field->type == E_TYPE_CONTACT_CERT)
+ column_type = "INTEGER";
+ else if (field->type == E_TYPE_CONTACT_ATTR_LIST)
+ column_type = "TEXT";
+ else
+ g_warn_if_reached ();
+ }
+
+ if (idx_prefix)
+ index = g_strconcat (idx_prefix, "_", field->dbname, NULL);
+
+ info = e_cache_column_info_new (column_name, column_type, index);
+
+ g_free (index);
+
+ return info;
+}
+
+static gint
+summary_field_array_index (GArray *array,
+ EContactField field)
+{
+ gint ii;
+
+ for (ii = 0; ii < array->len; ii++) {
+ SummaryField *iter = &g_array_index (array, SummaryField, ii);
+ if (field == iter->field_id)
+ return ii;
+ }
+
+ return -1;
+}
+
+static SummaryField *
+summary_field_append (GArray *array,
+ EContactField field_id,
+ GError **error)
+{
+ const gchar *dbname = NULL;
+ GType type = G_TYPE_INVALID;
+ gint idx;
+ SummaryField new_field = { 0, };
+
+ if (field_id < 1 || field_id >= E_CONTACT_FIELD_LAST) {
+ g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_UNSUPPORTED_FIELD,
+ _("Unsupported contact field “%d” specified in summary"),
+ field_id);
+
+ return NULL;
+ }
+
+ /* Avoid including the same field twice in the summary */
+ idx = summary_field_array_index (array, field_id);
+ if (idx >= 0)
+ return &g_array_index (array, SummaryField, idx);
+
+ /* Resolve some exceptions, we store these
+ * specific contact fields with different names
+ * than those found in the EContactField table
+ */
+ switch (field_id) {
+ case E_CONTACT_UID:
+ case E_CONTACT_REV:
+ /* Skip these, it's already in the ECache */
+ return NULL;
+ case E_CONTACT_IS_LIST:
+ dbname = "is_list";
+ break;
+ default:
+ dbname = e_contact_field_name (field_id);
+ break;
+ }
+
+ type = e_contact_field_type (field_id);
+
+ if (type != G_TYPE_STRING &&
+ type != G_TYPE_BOOLEAN &&
+ type != E_TYPE_CONTACT_CERT &&
+ type != E_TYPE_CONTACT_ATTR_LIST) {
+ g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_UNSUPPORTED_FIELD,
+ _("Contact field “%s” of type “%s” specified in summary, "
+ "but only boolean, string and string list field types are supported"),
+ e_contact_pretty_name (field_id), g_type_name (type));
+
+ return NULL;
+ }
+
+ if (type == E_TYPE_CONTACT_ATTR_LIST) {
+ new_field.aux_table = g_strconcat ("attrlist", "_", dbname, "_list", NULL);
+ new_field.aux_table_symbolic = g_strconcat (dbname, "_list", NULL);
+ }
+
+ new_field.field_id = field_id;
+ new_field.dbname = dbname;
+ new_field.type = type;
+ new_field.index = 0;
+
+ g_array_append_val (array, new_field);
+
+ return &g_array_index (array, SummaryField, array->len - 1);
+}
+
+static void
+summary_fields_add_indexes (GArray *array,
+ EContactField *indexes,
+ EBookIndexType *index_types,
+ gint n_indexes)
+{
+ gint ii, jj;
+
+ for (ii = 0; ii < array->len; ii++) {
+ SummaryField *sfield = &g_array_index (array, SummaryField, ii);
+
+ for (jj = 0; jj < n_indexes; jj++) {
+ if (sfield->field_id == indexes[jj])
+ sfield->index |= (1 << index_types[jj]);
+
+ }
+ }
+}
+
+static inline gint
+summary_field_get_index (EBookCache *book_cache,
+ EContactField field_id)
+{
+ gint ii;
+
+ for (ii = 0; ii < book_cache->priv->n_summary_fields; ii++) {
+ if (book_cache->priv->summary_fields[ii].field_id == field_id)
+ return ii;
+ }
+
+ return -1;
+}
+
+static inline SummaryField *
+summary_field_get (EBookCache *book_cache,
+ EContactField field_id)
+{
+ gint index;
+
+ index = summary_field_get_index (book_cache, field_id);
+ if (index >= 0)
+ return &(book_cache->priv->summary_fields[index]);
+
+ return NULL;
+}
+
+static void
+summary_field_init_dbnames (SummaryField *field)
+{
+ if (field->type == G_TYPE_STRING && (field->index & INDEX_FLAG (SORT_KEY))) {
+ field->dbname_idx_sort_key = g_strconcat (field->dbname, "_", EBC_SUFFIX_SORT_KEY, NULL);
+ }
+
+ if (field->type != G_TYPE_BOOLEAN && field->type != E_TYPE_CONTACT_CERT &&
+ (field->index & INDEX_FLAG (SUFFIX)) != 0) {
+ field->dbname_idx_suffix = g_strconcat (field->dbname, "_", EBC_SUFFIX_REVERSE, NULL);
+ }
+
+ if (field->type != G_TYPE_BOOLEAN && field->type != E_TYPE_CONTACT_CERT &&
+ (field->index & INDEX_FLAG (PHONE)) != 0) {
+ field->dbname_idx_phone = g_strconcat (field->dbname, "_", EBC_SUFFIX_PHONE, NULL);
+ field->dbname_idx_country = g_strconcat (field->dbname, "_", EBC_SUFFIX_COUNTRY, NULL);
+ }
+}
+
+static void
+summary_field_prepend_columns (SummaryField *field,
+ GSList **out_columns)
+{
+ ECacheColumnInfo *info;
+
+ /* Doesn't hurt to verify a bit more here, this shouldn't happen though */
+ g_return_if_fail (
+ field->type == G_TYPE_STRING ||
+ field->type == G_TYPE_BOOLEAN ||
+ field->type == E_TYPE_CONTACT_CERT ||
+ field->type == E_TYPE_CONTACT_ATTR_LIST);
+
+ /* Normal / default column */
+ info = column_info_new (field, field->dbname, NULL,
+ (field->index & INDEX_FLAG (PREFIX)) != 0 ? "INDEX" : NULL);
+ *out_columns = g_slist_prepend (*out_columns, info);
+
+ /* Localized column, for storing sort keys */
+ if (field->type == G_TYPE_STRING && (field->index & INDEX_FLAG (SORT_KEY))) {
+ info = column_info_new (field, field->dbname_idx_sort_key, "TEXT", "SINDEX");
+ *out_columns = g_slist_prepend (*out_columns, info);
+ }
+
+ /* Suffix match column */
+ if (field->type != G_TYPE_BOOLEAN && field->type != E_TYPE_CONTACT_CERT &&
+ (field->index & INDEX_FLAG (SUFFIX)) != 0) {
+ info = column_info_new (field, field->dbname_idx_suffix, "TEXT", "RINDEX");
+ *out_columns = g_slist_prepend (*out_columns, info);
+ }
+
+ /* Phone match columns */
+ if (field->type != G_TYPE_BOOLEAN && field->type != E_TYPE_CONTACT_CERT &&
+ (field->index & INDEX_FLAG (PHONE)) != 0) {
+
+ /* One indexed column for storing the national number */
+ info = column_info_new (field, field->dbname_idx_phone, "TEXT", "PINDEX");
+ *out_columns = g_slist_prepend (*out_columns, info);
+
+ /* One integer column for storing the country code */
+ info = column_info_new (field, field->dbname_idx_country, "INTEGER DEFAULT 0", NULL);
+ *out_columns = g_slist_prepend (*out_columns, info);
+ }
+}
+
+static void
+summary_fields_array_free (SummaryField *fields,
+ gint n_fields)
+{
+ gint ii;
+
+ for (ii = 0; ii < n_fields; ii++) {
+ g_free (fields[ii].dbname_idx_suffix);
+ g_free (fields[ii].dbname_idx_phone);
+ g_free (fields[ii].dbname_idx_country);
+ g_free (fields[ii].dbname_idx_sort_key);
+ g_free (fields[ii].aux_table);
+ g_free (fields[ii].aux_table_symbolic);
+ }
+
+ g_free (fields);
+}
+
+/******************************************************
+ * Functions installed into the SQLite *
+ ******************************************************/
+
+/* Implementation for REGEXP keyword */
+static void
+ebc_regexp (sqlite3_context *context,
+ gint argc,
+ sqlite3_value **argv)
+{
+ GRegex *regex;
+ const gchar *expression;
+ const gchar *text;
+
+ /* Reuse the same GRegex for all REGEXP queries with the same expression */
+ regex = sqlite3_get_auxdata (context, 0);
+ if (!regex) {
+ GError *error = NULL;
+
+ expression = (const gchar *) sqlite3_value_text (argv[0]);
+
+ regex = g_regex_new (expression, 0, 0, &error);
+
+ if (!regex) {
+ sqlite3_result_error (
+ context,
+ error ? error->message :
+ _("Error parsing regular expression"),
+ -1);
+ g_clear_error (&error);
+ return;
+ }
+
+ /* SQLite will take care of freeing the GRegex when we're done with the query */
+ sqlite3_set_auxdata (context, 0, regex, (GDestroyNotify) g_regex_unref);
+ }
+
+ /* Now perform the comparison */
+ text = (const gchar *) sqlite3_value_text (argv[1]);
+ if (text != NULL) {
+ gboolean match;
+
+ match = g_regex_match (regex, text, 0, NULL);
+ sqlite3_result_int (context, match ? 1 : 0);
+ }
+}
+
+/* Implementation of EBC_FUNC_COMPARE_VCARD (fallback for non-summary queries) */
+static void
+ebc_compare_vcard (sqlite3_context *context,
+ gint argc,
+ sqlite3_value **argv)
+{
+ EBookBackendSExp *sexp = NULL;
+ const gchar *text;
+ const gchar *vcard;
+
+ /* Reuse the same sexp for all queries with the same search expression */
+ sexp = sqlite3_get_auxdata (context, 0);
+ if (!sexp) {
+
+ /* The first argument will be reused for many rows */
+ text = (const gchar *) sqlite3_value_text (argv[0]);
+ if (text) {
+ sexp = e_book_backend_sexp_new (text);
+ sqlite3_set_auxdata (
+ context, 0,
+ sexp,
+ g_object_unref);
+ }
+
+ /* This shouldn't happen, catch invalid sexp in preflight */
+ if (!sexp) {
+ sqlite3_result_int (context, 0);
+ return;
+ }
+
+ }
+
+ /* Reuse the same vcard as much as possible (it can be referred to more than
+ * once in the query, so it can be reused for multiple comparisons on the same row)
+ */
+ vcard = sqlite3_get_auxdata (context, 1);
+ if (!vcard) {
+ vcard = (const gchar *) sqlite3_value_text (argv[1]);
+
+ if (vcard)
+ sqlite3_set_auxdata (context, 1, g_strdup (vcard), g_free);
+ }
+
+ /* A NULL vcard can never match */
+ if (!vcard || !*vcard) {
+ sqlite3_result_int (context, 0);
+ return;
+ }
+
+ /* Compare this vcard */
+ if (e_book_backend_sexp_match_vcard (sexp, vcard))
+ sqlite3_result_int (context, 1);
+ else
+ sqlite3_result_int (context, 0);
+}
+
+static void
+ebc_eqphone (sqlite3_context *context,
+ gint argc,
+ sqlite3_value **argv,
+ EPhoneNumberMatch requested_match)
+{
+ EBookCache *ebc = sqlite3_user_data (context);
+ EPhoneNumber *input_phone = NULL, *row_phone = NULL;
+ EPhoneNumberMatch match = E_PHONE_NUMBER_MATCH_NONE;
+ const gchar *text;
+
+ /* Reuse the same phone number for all queries with the same phone number argument */
+ input_phone = sqlite3_get_auxdata (context, 0);
+ if (!input_phone) {
+
+ /* The first argument will be reused for many rows */
+ text = (const gchar *) sqlite3_value_text (argv[0]);
+ if (text) {
+
+ /* Ignore errors, they are fine for phone numbers */
+ input_phone = e_phone_number_from_string (text, ebc->priv->region_code, NULL);
+
+ /* SQLite will take care of freeing the EPhoneNumber when we're done with the
expression */
+ if (input_phone)
+ sqlite3_set_auxdata (
+ context, 0,
+ input_phone,
+ (GDestroyNotify) e_phone_number_free);
+ }
+ }
+
+ /* This shouldn't happen, as we catch invalid phone number queries in preflight
+ */
+ if (!input_phone) {
+ sqlite3_result_int (context, 0);
+ return;
+ }
+
+ /* Parse the phone number for this row */
+ text = (const gchar *) sqlite3_value_text (argv[1]);
+ if (text != NULL) {
+ row_phone = e_phone_number_from_string (text, ebc->priv->region_code, NULL);
+
+ /* And perform the comparison */
+ if (row_phone) {
+ match = e_phone_number_compare (input_phone, row_phone);
+
+ e_phone_number_free (row_phone);
+ }
+ }
+
+ /* Now report the result */
+ if (match != E_PHONE_NUMBER_MATCH_NONE &&
+ match <= requested_match)
+ sqlite3_result_int (context, 1);
+ else
+ sqlite3_result_int (context, 0);
+}
+
+/* Exact phone number match function: EBC_FUNC_EQPHONE_EXACT */
+static void
+ebc_eqphone_exact (sqlite3_context *context,
+ gint argc,
+ sqlite3_value **argv)
+{
+ ebc_eqphone (context, argc, argv, E_PHONE_NUMBER_MATCH_EXACT);
+}
+
+/* National phone number match function: EBC_FUNC_EQPHONE_NATIONAL */
+static void
+ebc_eqphone_national (sqlite3_context *context,
+ gint argc,
+ sqlite3_value **argv)
+{
+ ebc_eqphone (context, argc, argv, E_PHONE_NUMBER_MATCH_NATIONAL);
+}
+
+/* Short phone number match function: EBC_FUNC_EQPHONE_SHORT */
+static void
+ebc_eqphone_short (sqlite3_context *context,
+ gint argc,
+ sqlite3_value **argv)
+{
+ ebc_eqphone (context, argc, argv, E_PHONE_NUMBER_MATCH_SHORT);
+}
+
+typedef void (*EBCCustomFunc) (sqlite3_context *context,
+ gint argc,
+ sqlite3_value **argv);
+
+typedef struct {
+ const gchar *name;
+ EBCCustomFunc func;
+ gint arguments;
+} EBCCustomFuncTab;
+
+static EBCCustomFuncTab ebc_custom_functions[] = {
+ { "regexp", ebc_regexp, 2 }, /* regexp (expression, column_data) */
+ { EBC_FUNC_COMPARE_VCARD, ebc_compare_vcard, 2 }, /* compare_vcard (sexp, vcard) */
+ { EBC_FUNC_EQPHONE_EXACT, ebc_eqphone_exact, 2 }, /* eqphone_exact (search_input, column_data)
*/
+ { EBC_FUNC_EQPHONE_NATIONAL, ebc_eqphone_national, 2 }, /* eqphone_national (search_input,
column_data) */
+ { EBC_FUNC_EQPHONE_SHORT, ebc_eqphone_short, 2 }, /* eqphone_national (search_input,
column_data) */
+};
+
+/******************************************************
+ * Fallback Collation Sequences *
+ ******************************************************
+ *
+ * The fallback simply compares vcards, vcards which have been
+ * stored on the cursor will have a preencoded key (these
+ * utilities encode & decode that key).
+ */
+static gchar *
+ebc_encode_vcard_sort_key (const gchar *sort_key)
+{
+ EVCard *vcard = e_vcard_new ();
+ gchar *base64;
+ gchar *encoded;
+
+ /* Encode this otherwise e-vcard messes it up */
+ base64 = g_base64_encode ((const guchar *) sort_key, strlen (sort_key));
+ e_vcard_append_attribute_with_value (
+ vcard,
+ e_vcard_attribute_new (NULL, EBC_VCARD_SORT_KEY),
+ base64);
+ encoded = e_vcard_to_string (vcard, EVC_FORMAT_VCARD_30);
+
+ g_free (base64);
+ g_object_unref (vcard);
+
+ return encoded;
+}
+
+static gchar *
+ebc_decode_vcard_sort_key_from_vcard (EVCard *vcard)
+{
+ EVCardAttribute *attr;
+ GList *values = NULL;
+ gchar *sort_key = NULL;
+ gchar *base64 = NULL;
+
+ attr = e_vcard_get_attribute (vcard, EBC_VCARD_SORT_KEY);
+ if (attr)
+ values = e_vcard_attribute_get_values (attr);
+
+ if (values && values->data) {
+ gsize len;
+
+ base64 = g_strdup (values->data);
+
+ sort_key = (gchar *) g_base64_decode (base64, &len);
+ g_free (base64);
+ }
+
+ return sort_key;
+}
+
+static gchar *
+ebc_decode_vcard_sort_key (const gchar *encoded)
+{
+ EVCard *vcard;
+ gchar *sort_key;
+
+ vcard = e_vcard_new_from_string (encoded);
+ sort_key = ebc_decode_vcard_sort_key_from_vcard (vcard);
+ g_object_unref (vcard);
+
+ return sort_key;
+}
+
+static gchar *
+convert_phone (const gchar *normal,
+ const gchar *region_code,
+ gint *out_country_code)
+{
+ EPhoneNumber *number = NULL;
+ gchar *national_number = NULL;
+ gint country_code = 0;
+
+ /* Don't warn about erronous phone number strings, it's a perfectly normal
+ * use case for users to enter notes instead of phone numbers in the phone
+ * number contact fields, such as "Ask Jenny for Lisa's phone number"
+ */
+ if (normal && e_phone_number_is_supported ())
+ number = e_phone_number_from_string (normal, region_code, NULL);
+
+ if (number) {
+ EPhoneNumberCountrySource source;
+
+ national_number = e_phone_number_get_national_number (number);
+ country_code = e_phone_number_get_country_code (number, &source);
+ e_phone_number_free (number);
+
+ if (source == E_PHONE_NUMBER_COUNTRY_FROM_DEFAULT)
+ country_code = 0;
+ }
+
+ if (out_country_code)
+ *out_country_code = country_code;
+
+ return national_number;
+}
+
+static gchar *
+remove_leading_zeros (gchar *number)
+{
+ gchar *trimmed = NULL;
+ gchar *tmp = number;
+
+ g_return_val_if_fail (NULL != number, NULL);
+
+ while ('0' == *tmp)
+ tmp++;
+ trimmed = g_strdup (tmp);
+ g_free (number);
+
+ return trimmed;
+}
+
+static void
+ebc_fill_other_columns (EBookCache *book_cache,
+ EContact *contact,
+ ECacheColumnValues *other_columns)
+{
+ gint ii;
+
+ g_return_if_fail (E_IS_BOOK_CACHE (book_cache));
+ g_return_if_fail (E_IS_CONTACT (contact));
+ g_return_if_fail (other_columns != NULL);
+
+ for (ii = 0; ii < book_cache->priv->n_summary_fields; ii++) {
+ SummaryField *field = &(book_cache->priv->summary_fields[ii]);
+
+ if (field->field_id == E_CONTACT_UID ||
+ field->field_id == E_CONTACT_REV) {
+ continue;
+ }
+
+ if (field->type == G_TYPE_STRING) {
+ gchar *val;
+ gchar *normal;
+ gchar *str;
+
+ val = e_contact_get (contact, field->field_id);
+ normal = e_util_utf8_normalize (val);
+
+ e_cache_column_values_take_value (other_columns, field->dbname, normal);
+
+ if ((field->index & INDEX_FLAG (SORT_KEY)) != 0) {
+ if (val)
+ str = e_collator_generate_key (book_cache->priv->collator, val, NULL);
+ else
+ str = g_strdup ("");
+
+ e_cache_column_values_take_value (other_columns, field->dbname_idx_sort_key,
str);
+ }
+
+ if ((field->index & INDEX_FLAG (SUFFIX)) != 0) {
+ if (normal)
+ str = g_utf8_strreverse (normal, -1);
+ else
+ str = NULL;
+
+ e_cache_column_values_take_value (other_columns, field->dbname_idx_suffix,
str);
+ }
+
+ if ((field->index & INDEX_FLAG (PHONE)) != 0) {
+ gint country_code;
+
+ str = convert_phone (normal, book_cache->priv->region_code, &country_code);
+ str = remove_leading_zeros (str);
+
+ e_cache_column_values_take_value (other_columns, field->dbname_idx_phone,
str);
+
+ str = g_strdup_printf ("%d", country_code);
+
+ e_cache_column_values_take_value (other_columns, field->dbname_idx_country,
str);
+ }
+
+ g_free (val);
+ } else if (field->type == G_TYPE_BOOLEAN) {
+ gboolean val;
+
+ val = e_contact_get (contact, field->field_id) ? TRUE : FALSE;
+
+ e_cache_column_values_take_value (other_columns, field->dbname, g_strdup_printf
("%d", val ? 1 : 0));
+ } else if (field->type == E_TYPE_CONTACT_CERT) {
+ EContactCert *cert = NULL;
+
+ cert = e_contact_get (contact, field->field_id);
+
+ /* We don't actually store the cert; only a boolean to indicate
+ * that is *has* a cert. */
+ e_cache_column_values_take_value (other_columns, field->dbname, g_strdup_printf
("%d", cert ? 1 : 0));
+ e_contact_cert_free (cert);
+ } else if (field->type != E_TYPE_CONTACT_ATTR_LIST) {
+ g_warn_if_reached ();
+ }
+ }
+}
+
+static inline void
+format_column_declaration (GString *string,
+ ECacheColumnInfo *info)
+{
+ g_string_append (string, info->name);
+ g_string_append_c (string, ' ');
+
+ g_string_append (string, info->type);
+
+}
+
+static gboolean
+ebc_init_aux_tables (EBookCache *book_cache,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GString *string;
+ gboolean success = TRUE;
+ gchar *tmp;
+ gint ii;
+
+ for (ii = 0; success && ii < book_cache->priv->n_summary_fields; ii++) {
+ SummaryField *field = &(book_cache->priv->summary_fields[ii]);
+ GSList *aux_columns = NULL, *link;
+
+ if (field->type != E_TYPE_CONTACT_ATTR_LIST)
+ continue;
+
+ summary_field_prepend_columns (field, &aux_columns);
+ if (!aux_columns)
+ continue;
+
+ /* Create the auxiliary table for this multi valued field */
+ string = g_string_sized_new (
+ COLUMN_DEFINITION_BYTES * 3 +
+ COLUMN_DEFINITION_BYTES * g_slist_length (aux_columns));
+
+ e_cache_sqlite_stmt_append_printf (string, "CREATE TABLE IF NOT EXISTS %Q (uid TEXT NOT NULL
REFERENCES " E_CACHE_TABLE_OBJECTS
+ " (" E_CACHE_COLUMN_UID ")",
+ field->aux_table);
+ for (link = aux_columns; link; link = g_slist_next (link)) {
+ ECacheColumnInfo *info = link->data;
+
+ g_string_append (string, ", ");
+ format_column_declaration (string, info);
+ }
+ g_string_append_c (string, ')');
+
+ success = e_cache_sqlite_exec (E_CACHE (book_cache), string->str, cancellable, error);
+ g_string_free (string, TRUE);
+
+ if (success) {
+ /* Create an index on the implied 'uid' column, this is important
+ * when replacing (modifying) contacts, since we need to remove
+ * all rows in an auxiliary table which matches a given UID.
+ *
+ * This index speeds up the constraint in a statement such as:
+ *
+ * DELETE from email_list WHERE email_list.uid = 'contact uid'
+ */
+ tmp = e_cache_sqlite_stmt_printf ("CREATE INDEX IF NOT EXISTS UID_INDEX_%s_%s ON %Q
(uid)",
+ field->dbname, field->aux_table, field->aux_table);
+ success = e_cache_sqlite_exec (E_CACHE (book_cache), tmp, cancellable, error);
+ e_cache_sqlite_stmt_free (tmp);
+ }
+
+ /* Add indexes to columns in this auxiliary table
+ */
+ for (link = aux_columns; success && link; link = g_slist_next (link)) {
+ ECacheColumnInfo *info = link->data;
+
+ if (info->index_name) {
+ tmp = e_cache_sqlite_stmt_printf ("CREATE INDEX IF NOT EXISTS %Q ON %Q (%s)",
+ info->index_name, field->aux_table, info->name);
+ success = e_cache_sqlite_exec (E_CACHE (book_cache), tmp, cancellable, error);
+ e_cache_sqlite_stmt_free (tmp);
+ }
+ }
+
+ g_slist_free_full (aux_columns, e_cache_column_info_free);
+ }
+
+ return success;
+}
+
+static gboolean
+ebc_run_multi_insert_one (ECache *cache,
+ SummaryField *field,
+ const gchar *uid,
+ const gchar *value,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GString *stmt, *values;
+ gchar *normal;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+ g_return_val_if_fail (field != NULL, FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ stmt = g_string_sized_new (INSERT_MULTI_STMT_BYTES);
+ values = g_string_sized_new (INSERT_MULTI_STMT_BYTES);
+
+ normal = e_util_utf8_normalize (value);
+
+ e_cache_sqlite_stmt_append_printf (stmt, "INSERT INTO %Q (uid, value", field->aux_table);
+
+ if ((field->index & INDEX_FLAG (SUFFIX)) != 0) {
+ g_string_append (stmt, ", value_" EBC_SUFFIX_REVERSE);
+
+ if (normal) {
+ gchar *str;
+
+ str = g_utf8_strreverse (normal, -1);
+
+ e_cache_sqlite_stmt_append_printf (values, ", %Q", str);
+
+ g_free (str);
+ } else {
+ g_string_append (values, ", NULL");
+ }
+ }
+
+ if ((field->index & INDEX_FLAG (PHONE)) != 0) {
+ EBookCache *book_cache;
+ gint country_code = 0;
+ gchar *str;
+
+ g_string_append (stmt, ", value_" EBC_SUFFIX_PHONE);
+ g_string_append (stmt, ", value_" EBC_SUFFIX_COUNTRY);
+
+ book_cache = E_BOOK_CACHE (cache);
+ str = convert_phone (normal, book_cache->priv->region_code, &country_code);
+ str = remove_leading_zeros (str);
+
+ if (str) {
+ e_cache_sqlite_stmt_append_printf (values, ", %Q", str);
+ } else {
+ g_string_append (values, ",NULL");
+ }
+
+ g_string_append_printf (values, ",%d", country_code);
+ }
+
+ e_cache_sqlite_stmt_append_printf (stmt, ") VALUES (%Q, %Q", uid, normal);
+ g_free (normal);
+
+ g_string_append (stmt, values->str);
+ g_string_append_c (stmt, ')');
+
+ success = e_cache_sqlite_exec (cache, stmt->str, cancellable, error);
+
+ g_string_free (stmt, TRUE);
+ g_string_free (values, TRUE);
+
+ return success;
+}
+
+static gboolean
+ebc_run_multi_insert (ECache *cache,
+ SummaryField *field,
+ const gchar *uid,
+ EContact *contact,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GList *values, *link;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+ g_return_val_if_fail (field != NULL, FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+
+ values = e_contact_get (contact, field->field_id);
+
+ for (link = values; success && link; link = g_list_next (link)) {
+ const gchar *value = link->data;
+
+ success = ebc_run_multi_insert_one (cache, field, uid, value, cancellable, error);
+ }
+
+ /* Free the list of allocated strings */
+ e_contact_attr_list_free (values);
+
+ return success;
+}
+
+static gboolean
+ebc_run_multi_delete (ECache *cache,
+ SummaryField *field,
+ const gchar *uid,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gchar *stmt;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+ g_return_val_if_fail (field != NULL, FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ stmt = e_cache_sqlite_stmt_printf ("DELETE FROM %Q WHERE uid=%Q", field->aux_table, uid);
+ success = e_cache_sqlite_exec (cache, stmt, cancellable, error);
+ e_cache_sqlite_stmt_free (stmt);
+
+ return success;
+}
+
+static gboolean
+ebc_update_aux_tables (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookCache *book_cache;
+ EContact *contact = NULL;
+ gint ii;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+
+ book_cache = E_BOOK_CACHE (cache);
+
+ for (ii = 0; ii < book_cache->priv->n_summary_fields && success; ii++) {
+ SummaryField *field = &(book_cache->priv->summary_fields[ii]);
+
+ if (field->type != E_TYPE_CONTACT_ATTR_LIST)
+ continue;
+
+ if (!contact) {
+ contact = e_contact_new_from_vcard_with_uid (object, uid);
+ success = contact != NULL;
+ }
+
+ success = success && ebc_run_multi_delete (cache, field, uid, cancellable, error);
+ success = success && ebc_run_multi_insert (cache, field, uid, contact, cancellable, error);
+ }
+
+ g_clear_object (&contact);
+
+ return success;
+}
+
+static gboolean
+ebc_delete_from_aux_tables (ECache *cache,
+ const gchar *uid,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookCache *book_cache;
+ gint ii;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ book_cache = E_BOOK_CACHE (cache);
+
+ for (ii = 0; ii < book_cache->priv->n_summary_fields && success; ii++) {
+ SummaryField *field = &(book_cache->priv->summary_fields[ii]);
+
+ if (field->type != E_TYPE_CONTACT_ATTR_LIST)
+ continue;
+
+ success = success && ebc_run_multi_delete (cache, field, uid, cancellable, error);
+ }
+
+ return success;
+}
+
+static gboolean
+ebc_delete_from_aux_tables_offline_deleted (ECache *cache,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookCache *book_cache;
+ gint ii;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+
+ book_cache = E_BOOK_CACHE (cache);
+
+ for (ii = 0; ii < book_cache->priv->n_summary_fields && success; ii++) {
+ SummaryField *field = &(book_cache->priv->summary_fields[ii]);
+ gchar *stmt;
+
+ if (field->type != E_TYPE_CONTACT_ATTR_LIST)
+ continue;
+
+ stmt = e_cache_sqlite_stmt_printf ("DELETE FROM %Q WHERE uid IN ("
+ "SELECT " E_CACHE_COLUMN_UID " FROM " E_CACHE_TABLE_OBJECTS
+ " WHERE " E_CACHE_COLUMN_STATE "=%d)",
+ field->aux_table, E_OFFLINE_STATE_LOCALLY_DELETED);
+
+ success = e_cache_sqlite_exec (cache, stmt, cancellable, error);
+
+ e_cache_sqlite_stmt_free (stmt);
+ }
+
+ return success;
+}
+
+static gboolean
+ebc_empty_aux_tables (ECache *cache,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookCache *book_cache;
+ gint ii;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+
+ book_cache = E_BOOK_CACHE (cache);
+
+ for (ii = 0; ii < book_cache->priv->n_summary_fields && success; ii++) {
+ SummaryField *field = &(book_cache->priv->summary_fields[ii]);
+ gchar *stmt;
+
+ if (field->type != E_TYPE_CONTACT_ATTR_LIST)
+ continue;
+
+ stmt = e_cache_sqlite_stmt_printf ("DELETE FROM %Q", field->aux_table);
+ success = e_cache_sqlite_exec (cache, stmt, cancellable, error);
+ e_cache_sqlite_stmt_free (stmt);
+ }
+
+ return success;
+}
+
+static gboolean
+ebc_upgrade_cb (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ EOfflineState offline_state,
+ gint ncols,
+ const gchar *column_names[],
+ const gchar *column_values[],
+ gchar **out_revision,
+ gchar **out_object,
+ EOfflineState *out_offline_state,
+ ECacheColumnValues **out_other_columns,
+ gpointer user_data)
+{
+ EContact *contact;
+ ECacheColumnValues *other_columns;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+
+ contact = e_contact_new_from_vcard_with_uid (object, uid);
+
+ /* Ignore broken rows? */
+ if (!contact)
+ return TRUE;
+
+ other_columns = e_cache_column_values_new ();
+
+ ebc_fill_other_columns (E_BOOK_CACHE (cache), contact, other_columns);
+
+ g_clear_object (&contact);
+
+ /* This will cause rewrite even when no values changed, but it's
+ necessary, because the locale changed, which can influence
+ other tables, not only the other columns. */
+ *out_other_columns = other_columns;
+
+ return TRUE;
+}
+
+/* Called with the lock held and inside a transaction */
+static gboolean
+ebc_upgrade (EBookCache *book_cache,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean success;
+
+ success = e_cache_foreach_update (E_CACHE (book_cache), E_CACHE_EXCLUDE_DELETED, NULL,
+ ebc_upgrade_cb, NULL, cancellable, error);
+
+ /* Store the new locale & country code */
+ success = success && e_cache_set_key (E_CACHE (book_cache), EBC_KEY_LC_COLLATE,
book_cache->priv->locale, error);
+ success = success && e_cache_set_key (E_CACHE (book_cache), EBC_KEY_COUNTRYCODE,
book_cache->priv->region_code, error);
+
+ return success;
+}
+
+static gboolean
+ebc_set_locale_internal (EBookCache *book_cache,
+ const gchar *locale,
+ GError **error)
+{
+ ECollator *collator;
+
+ g_return_val_if_fail (locale && locale[0], FALSE);
+
+ if (g_strcmp0 (book_cache->priv->locale, locale) != 0) {
+ gchar *country_code = NULL;
+
+ collator = e_collator_new_interpret_country (locale, &country_code, error);
+ if (collator == NULL)
+ return FALSE;
+
+ /* Assign region code parsed from the locale by ICU */
+ g_free (book_cache->priv->region_code);
+ book_cache->priv->region_code = country_code;
+
+ /* Assign locale */
+ g_free (book_cache->priv->locale);
+ book_cache->priv->locale = g_strdup (locale);
+
+ /* Assign collator */
+ if (book_cache->priv->collator)
+ e_collator_unref (book_cache->priv->collator);
+ book_cache->priv->collator = collator;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+ebc_init_locale (EBookCache *book_cache,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gchar *stored_lc_collate;
+ gchar *stored_region_code;
+ const gchar *lc_collate;
+ gboolean success = TRUE;
+ gboolean relocalize_needed = FALSE;
+
+ /* Get the locale setting for this addressbook */
+ stored_lc_collate = e_cache_dup_key (E_CACHE (book_cache), EBC_KEY_LC_COLLATE, NULL);
+ stored_region_code = e_cache_dup_key (E_CACHE (book_cache), EBC_KEY_COUNTRYCODE, NULL);
+
+ lc_collate = stored_lc_collate;
+
+ /* When creating a new addressbook, or upgrading from a version
+ * where we did not have any locale setting; default to system locale,
+ * we must absolutely always have a locale set.
+ */
+ if (!lc_collate || !lc_collate[0])
+ lc_collate = setlocale (LC_COLLATE, NULL);
+ if (!lc_collate || !lc_collate[0])
+ lc_collate = setlocale (LC_ALL, NULL);
+ if (!lc_collate || !lc_collate[0])
+ lc_collate = "en_US.utf8";
+
+ /* Before touching any data, make sure we have a valid ECollator,
+ * this will also resolve our region code
+ */
+ if (success)
+ success = ebc_set_locale_internal (book_cache, lc_collate, error);
+
+ /* Check if we need to relocalize */
+ if (success) {
+ /* We may need to relocalize for a country code change */
+ if (g_strcmp0 (book_cache->priv->region_code, stored_region_code) != 0)
+ relocalize_needed = TRUE;
+ }
+
+ /* Reinsert all contacts with new locale & country code */
+ if (success && relocalize_needed)
+ success = ebc_upgrade (book_cache, cancellable, error);
+
+ g_free (stored_region_code);
+ g_free (stored_lc_collate);
+
+ return success;
+}
+
+typedef struct {
+ EBookCache *book_cache;
+ EContactField field;
+} EBCCollData;
+
+static gint
+ebc_fallback_collator (gpointer ref,
+ gint len1,
+ gconstpointer data1,
+ gint len2,
+ gconstpointer data2)
+{
+ EBCCollData *data = ref;
+ EBookCache *book_cache;
+ EContact *contact1, *contact2;
+ const gchar *str1, *str2;
+ gchar *key1, *key2;
+ gchar *tmp;
+ gint result = 0;
+
+ book_cache = data->book_cache;
+
+ str1 = (const gchar *) data1;
+ str2 = (const gchar *) data2;
+
+ /* Construct 2 contacts (we're comparing vcards) */
+ contact1 = e_contact_new ();
+ contact2 = e_contact_new ();
+ e_vcard_construct_full (E_VCARD (contact1), str1, len1, NULL);
+ e_vcard_construct_full (E_VCARD (contact2), str2, len2, NULL);
+
+ /* Extract first key */
+ key1 = ebc_decode_vcard_sort_key_from_vcard (E_VCARD (contact1));
+ if (!key1) {
+ tmp = e_contact_get (contact1, data->field);
+ if (tmp)
+ key1 = e_collator_generate_key (book_cache->priv->collator, tmp, NULL);
+ g_free (tmp);
+ }
+ if (!key1)
+ key1 = g_strdup ("");
+
+ /* Extract second key */
+ key2 = ebc_decode_vcard_sort_key_from_vcard (E_VCARD (contact2));
+ if (!key2) {
+ tmp = e_contact_get (contact2, data->field);
+ if (tmp)
+ key2 = e_collator_generate_key (book_cache->priv->collator, tmp, NULL);
+ g_free (tmp);
+ }
+ if (!key2)
+ key2 = g_strdup ("");
+
+ result = strcmp (key1, key2);
+
+ g_free (key1);
+ g_free (key2);
+ g_object_unref (contact1);
+ g_object_unref (contact2);
+
+ return result;
+}
+
+static EBCCollData *
+ebc_coll_data_new (EBookCache *book_cache,
+ EContactField field)
+{
+ EBCCollData *data = g_slice_new (EBCCollData);
+
+ data->book_cache = book_cache;
+ data->field = field;
+
+ return data;
+}
+
+static void
+ebc_coll_data_free (EBCCollData *data)
+{
+ if (data)
+ g_slice_free (EBCCollData, data);
+}
+
+/* COLLATE functions are generated on demand only */
+static void
+ebc_generate_collator (gpointer ref,
+ sqlite3 *db,
+ gint eTextRep,
+ const gchar *coll_name)
+{
+ EBookCache *book_cache = ref;
+ EBCCollData *data;
+ EContactField field;
+ const gchar *field_name;
+
+ field_name = coll_name + strlen (EBC_COLLATE_PREFIX);
+ field = e_contact_field_id (field_name);
+
+ /* This should be caught before reaching here, just an extra check */
+ if (field == 0 || field >= E_CONTACT_FIELD_LAST ||
+ e_contact_field_type (field) != G_TYPE_STRING) {
+ g_warning ("Specified collation on invalid contact field");
+ return;
+ }
+
+ data = ebc_coll_data_new (book_cache, field);
+ sqlite3_create_collation_v2 (
+ db, coll_name, SQLITE_UTF8,
+ data, ebc_fallback_collator,
+ (GDestroyNotify) ebc_coll_data_free);
+}
+
+/***************************************************************
+ * Structures and utilities for preflight and query generation *
+ ***************************************************************/
+
+/* This enumeration is ordered by severity, higher values
+ * of PreflightStatus take precedence in error reporting.
+ */
+typedef enum {
+ PREFLIGHT_OK = 0,
+ PREFLIGHT_LIST_ALL,
+ PREFLIGHT_NOT_SUMMARIZED,
+ PREFLIGHT_INVALID,
+ PREFLIGHT_UNSUPPORTED,
+} PreflightStatus;
+
+/* Whether we can satisfy the constraints or whether we
+ * need to do a fallback, we still need to call
+ * ebc_generate_constraints()
+ */
+#define EBC_STATUS_GEN_CONSTRAINTS(status) \
+ ((status) == PREFLIGHT_OK || \
+ (status) == PREFLIGHT_NOT_SUMMARIZED)
+
+/* Internal extension of the EBookQueryTest enumeration */
+enum {
+ /* 'exists' is a supported query on a field, but not part of EBookQueryTest */
+ BOOK_QUERY_EXISTS = E_BOOK_QUERY_LAST,
+ BOOK_QUERY_EXISTS_VCARD,
+
+ /* From here the compound types start */
+ BOOK_QUERY_SUB_AND,
+ BOOK_QUERY_SUB_OR,
+ BOOK_QUERY_SUB_NOT,
+ BOOK_QUERY_SUB_END,
+
+ BOOK_QUERY_SUB_FIRST = BOOK_QUERY_SUB_AND,
+};
+
+#define EBC_QUERY_TYPE_STR(query) \
+ ((query) == BOOK_QUERY_EXISTS ? "exists" : \
+ (query) == BOOK_QUERY_EXISTS_VCARD ? "exists_vcard" : \
+ (query) == BOOK_QUERY_SUB_AND ? "AND" : \
+ (query) == BOOK_QUERY_SUB_OR ? "OR" : \
+ (query) == BOOK_QUERY_SUB_NOT ? "NOT" : \
+ (query) == BOOK_QUERY_SUB_END ? "END" : \
+ (query) == E_BOOK_QUERY_IS ? "is" : \
+ (query) == E_BOOK_QUERY_CONTAINS ? "contains" : \
+ (query) == E_BOOK_QUERY_BEGINS_WITH ? "begins-with" : \
+ (query) == E_BOOK_QUERY_ENDS_WITH ? "ends-with" : \
+ (query) == E_BOOK_QUERY_EQUALS_PHONE_NUMBER ? "eqphone" : \
+ (query) == E_BOOK_QUERY_EQUALS_NATIONAL_PHONE_NUMBER ? "eqphone-national" : \
+ (query) == E_BOOK_QUERY_EQUALS_SHORT_PHONE_NUMBER ? "eqphone-short" : \
+ (query) == E_BOOK_QUERY_REGEX_NORMAL ? "regex-normal" : \
+ (query) == E_BOOK_QUERY_REGEX_NORMAL ? "regex-raw" : "(unknown)")
+
+#define EBC_FIELD_ID_STR(field_id) \
+ ((field_id) == E_CONTACT_FIELD_LAST ? "x-evolution-any-field" : \
+ (field_id) == 0 ? "(not an EContactField)" : \
+ e_contact_field_name (field_id))
+
+#define IS_QUERY_PHONE(query) \
+ ((query) == E_BOOK_QUERY_EQUALS_PHONE_NUMBER || \
+ (query) == E_BOOK_QUERY_EQUALS_NATIONAL_PHONE_NUMBER || \
+ (query) == E_BOOK_QUERY_EQUALS_SHORT_PHONE_NUMBER)
+
+typedef struct {
+ guint query; /* EBookQueryTest (extended) */
+} QueryElement;
+
+typedef struct {
+ guint query; /* EBookQueryTest (extended) */
+} QueryDelimiter;
+
+typedef struct {
+ guint query; /* EBookQueryTest (extended) */
+
+ EContactField field_id; /* The EContactField to compare */
+ SummaryField *field; /* The summary field for 'field' */
+ gchar *value; /* The value to compare with */
+
+} QueryFieldTest;
+
+typedef struct {
+ guint query; /* EBookQueryTest (extended) */
+
+ /* Common fields from QueryFieldTest */
+ EContactField field_id; /* The EContactField to compare */
+ SummaryField *field; /* The summary field for 'field' */
+ gchar *value; /* The value to compare with */
+
+ /* Extension */
+ gchar *region; /* Region code from the query input */
+ gchar *national; /* Parsed national number */
+ gint country; /* Parsed country code */
+} QueryPhoneTest;
+
+/* Stack initializer for the PreflightContext struct below */
+#define PREFLIGHT_CONTEXT_INIT { PREFLIGHT_OK, NULL, 0, FALSE }
+
+typedef struct {
+ PreflightStatus status; /* result status */
+ GPtrArray *constraints; /* main query; may be NULL */
+ guint64 aux_mask; /* Bitmask of which auxiliary tables are needed in the query */
+ guint64 left_join_mask; /* Do we need to use a LEFT JOIN */
+} PreflightContext;
+
+static QueryElement *
+query_delimiter_new (guint query)
+{
+ QueryDelimiter *delim;
+
+ g_return_val_if_fail (query >= BOOK_QUERY_SUB_FIRST, NULL);
+
+ delim = g_slice_new (QueryDelimiter);
+ delim->query = query;
+
+ return (QueryElement *) delim;
+}
+
+static QueryFieldTest *
+query_field_test_new (guint query,
+ EContactField field)
+{
+ QueryFieldTest *test;
+
+ g_return_val_if_fail (query < BOOK_QUERY_SUB_FIRST, NULL);
+ g_return_val_if_fail (IS_QUERY_PHONE (query) == FALSE, NULL);
+
+ test = g_slice_new (QueryFieldTest);
+ test->query = query;
+ test->field_id = field;
+
+ /* Instead of g_slice_new0, NULL them out manually */
+ test->field = NULL;
+ test->value = NULL;
+
+ return test;
+}
+
+static QueryPhoneTest *
+query_phone_test_new (guint query,
+ EContactField field)
+{
+ QueryPhoneTest *test;
+
+ g_return_val_if_fail (IS_QUERY_PHONE (query), NULL);
+
+ test = g_slice_new (QueryPhoneTest);
+ test->query = query;
+ test->field_id = field;
+
+ /* Instead of g_slice_new0, NULL them out manually */
+ test->field = NULL;
+ test->value = NULL;
+
+ /* Extra QueryPhoneTest fields */
+ test->region = NULL;
+ test->national = NULL;
+ test->country = 0;
+
+ return test;
+}
+
+static void
+query_element_free (QueryElement *element)
+{
+ if (element) {
+
+ if (element->query >= BOOK_QUERY_SUB_FIRST) {
+ QueryDelimiter *delim = (QueryDelimiter *) element;
+
+ g_slice_free (QueryDelimiter, delim);
+ } else if (IS_QUERY_PHONE (element->query)) {
+ QueryPhoneTest *test = (QueryPhoneTest *) element;
+
+ g_free (test->value);
+ g_free (test->region);
+ g_free (test->national);
+ g_slice_free (QueryPhoneTest, test);
+ } else {
+ QueryFieldTest *test = (QueryFieldTest *) element;
+
+ g_free (test->value);
+ g_slice_free (QueryFieldTest, test);
+ }
+ }
+}
+
+/* We use ptr arrays for the QueryElement vectors */
+static inline void
+constraints_insert (GPtrArray *array,
+ gint idx,
+ gpointer data)
+{
+ g_return_if_fail ((idx >= -1) && (idx < (gint) array->len + 1));
+
+ if (idx < 0)
+ idx = array->len;
+
+ g_ptr_array_add (array, NULL);
+
+ if (idx != (array->len - 1))
+ memmove (
+ &(array->pdata[idx + 1]),
+ &(array->pdata[idx]),
+ ((array->len - 1) - idx) * sizeof (gpointer));
+
+ array->pdata[idx] = data;
+}
+
+static inline void
+constraints_insert_delimiter (GPtrArray *array,
+ gint idx,
+ guint query)
+{
+ QueryElement *delim;
+
+ delim = query_delimiter_new (query);
+ constraints_insert (array, idx, delim);
+}
+
+static inline void
+constraints_insert_field_test (GPtrArray *array,
+ gint idx,
+ SummaryField *field,
+ guint query,
+ const gchar *value)
+{
+ QueryFieldTest *test;
+
+ test = query_field_test_new (query, field->field_id);
+ test->field = field;
+ test->value = g_strdup (value);
+
+ constraints_insert (array, idx, test);
+}
+
+static void
+preflight_context_clear (PreflightContext *context)
+{
+ if (context) {
+ /* Free any allocated data, but leave the context values in place */
+ if (context->constraints)
+ g_ptr_array_free (context->constraints, TRUE);
+ context->constraints = NULL;
+ }
+}
+
+/* A small API to track the current sub-query context.
+ *
+ * I.e. sub contexts can be OR, AND, or NOT, in which
+ * field tests or other sub contexts are nested.
+ *
+ * The 'count' field is a simple counter of how deep the contexts are nested.
+ *
+ * The 'cond_count' field is to be used by the caller for its own purposes;
+ * it is incremented in sub_query_context_push() only if the inc_cond_count
+ * parameter is TRUE. This is used by query_preflight_check() in a complex
+ * fashion which is described there.
+ */
+typedef GQueue SubQueryContext;
+
+typedef struct {
+ guint sub_type; /* The type of this sub context */
+ guint count; /* The number of field tests so far in this context */
+ guint cond_count; /* User-specific conditional counter */
+} SubQueryData;
+
+#define sub_query_context_new g_queue_new
+#define sub_query_context_free(ctx) g_queue_free (ctx)
+
+static inline void
+sub_query_context_push (SubQueryContext *ctx,
+ guint sub_type,
+ gboolean inc_cond_count)
+{
+ SubQueryData *data, *prev;
+
+ prev = g_queue_peek_tail (ctx);
+
+ data = g_slice_new (SubQueryData);
+ data->sub_type = sub_type;
+ data->count = 0;
+ data->cond_count = prev ? prev->cond_count : 0;
+ if (inc_cond_count)
+ data->cond_count++;
+
+ g_queue_push_tail (ctx, data);
+}
+
+static inline void
+sub_query_context_pop (SubQueryContext *ctx)
+{
+ SubQueryData *data;
+
+ data = g_queue_pop_tail (ctx);
+ g_slice_free (SubQueryData, data);
+}
+
+static inline guint
+sub_query_context_peek_type (SubQueryContext *ctx)
+{
+ SubQueryData *data;
+
+ data = g_queue_peek_tail (ctx);
+
+ return data->sub_type;
+}
+
+static inline guint
+sub_query_context_peek_cond_counter (SubQueryContext *ctx)
+{
+ SubQueryData *data;
+
+ data = g_queue_peek_tail (ctx);
+
+ if (data)
+ return data->cond_count;
+ else
+ return 0;
+}
+
+/* Returns the context field test count before incrementing */
+static inline guint
+sub_query_context_increment (SubQueryContext *ctx)
+{
+ SubQueryData *data;
+
+ data = g_queue_peek_tail (ctx);
+
+ if (data) {
+ data->count++;
+
+ return (data->count - 1);
+ }
+
+ /* If we're not in a sub context, just return 0 */
+ return 0;
+}
+
+/**********************************************************
+ * Querying preflighting *
+ **********************************************************
+ *
+ * The preflight checks are performed before a query might
+ * take place in order to evaluate whether the given query
+ * can be performed with the current summary configuration.
+ *
+ * After preflighting, all relevant data has been extracted
+ * from the search expression and the search expression need
+ * not be parsed again.
+ */
+
+/* The PreflightSubCallback is expected to return TRUE
+ * to keep iterating and FALSE to abort iteration.
+ *
+ * The sub_level is the counter of how deep the 'element'
+ * is nested in sub elements, the offset is the real offset
+ * of 'element' in the array passed to query_preflight_foreach_sub().
+ */
+typedef gboolean (* PreflightSubCallback) (QueryElement *element,
+ gint sub_level,
+ gint offset,
+ gpointer user_data);
+
+static void
+query_preflight_foreach_sub (QueryElement **elements,
+ gint n_elements,
+ gint offset,
+ gboolean include_delim,
+ PreflightSubCallback callback,
+ gpointer user_data)
+{
+ gint sub_counter = 1, ii;
+
+ g_return_if_fail (offset >= 0 && offset < n_elements);
+ g_return_if_fail (elements[offset]->query >= BOOK_QUERY_SUB_FIRST);
+ g_return_if_fail (callback != NULL);
+
+ if (include_delim && !callback (elements[offset], 0, offset, user_data))
+ return;
+
+ for (ii = (offset + 1); sub_counter > 0 && ii < n_elements; ii++) {
+
+ if (elements[ii]->query >= BOOK_QUERY_SUB_FIRST) {
+
+ if (elements[ii]->query == BOOK_QUERY_SUB_END)
+ sub_counter--;
+ else
+ sub_counter++;
+
+ if (include_delim &&
+ !callback (elements[ii], sub_counter, ii, user_data))
+ break;
+ } else {
+
+ if (!callback (elements[ii], sub_counter, ii, user_data))
+ break;
+ }
+ }
+}
+
+/* Table used in ESExp parsing below */
+static const struct {
+ const gchar *name; /* Name of the symbol to match for this parse phase */
+ gboolean subset; /* TRUE for the subset ESExpIFunc, otherwise the field check ESExpFunc */
+ guint test; /* Extended EBookQueryTest value */
+} check_symbols[] = {
+ { "and", TRUE, BOOK_QUERY_SUB_AND },
+ { "or", TRUE, BOOK_QUERY_SUB_OR },
+ { "not", TRUE, BOOK_QUERY_SUB_NOT },
+
+ { "contains", FALSE, E_BOOK_QUERY_CONTAINS },
+ { "is", FALSE, E_BOOK_QUERY_IS },
+ { "beginswith", FALSE, E_BOOK_QUERY_BEGINS_WITH },
+ { "endswith", FALSE, E_BOOK_QUERY_ENDS_WITH },
+ { "eqphone", FALSE, E_BOOK_QUERY_EQUALS_PHONE_NUMBER },
+ { "eqphone_national", FALSE, E_BOOK_QUERY_EQUALS_NATIONAL_PHONE_NUMBER },
+ { "eqphone_short", FALSE, E_BOOK_QUERY_EQUALS_SHORT_PHONE_NUMBER },
+ { "regex_normal", FALSE, E_BOOK_QUERY_REGEX_NORMAL },
+ { "regex_raw", FALSE, E_BOOK_QUERY_REGEX_RAW },
+ { "exists", FALSE, BOOK_QUERY_EXISTS },
+ { "exists_vcard", FALSE, BOOK_QUERY_EXISTS_VCARD }
+};
+
+/* Cheat our way into passing mode data to these funcs */
+static ESExpResult *
+func_check_subset (ESExp *f,
+ gint argc,
+ struct _ESExpTerm **argv,
+ gpointer data)
+{
+ ESExpResult *result, *sub_result;
+ GPtrArray *result_array;
+ QueryElement *element, **sub_elements;
+ gint ii, jj, len;
+ guint query_type;
+
+ query_type = GPOINTER_TO_UINT (data);
+
+ /* The compound query delimiter is the first element in this return array */
+ result_array = g_ptr_array_new_with_free_func ((GDestroyNotify) query_element_free);
+ element = query_delimiter_new (query_type);
+ g_ptr_array_add (result_array, element);
+
+ for (ii = 0; ii < argc; ii++) {
+ sub_result = e_sexp_term_eval (f, argv[ii]);
+
+ if (sub_result->type == ESEXP_RES_ARRAY_PTR) {
+ /* Steal the elements directly from the sub result */
+ sub_elements = (QueryElement **) sub_result->value.ptrarray->pdata;
+ len = sub_result->value.ptrarray->len;
+
+ for (jj = 0; jj < len; jj++) {
+ element = sub_elements[jj];
+ sub_elements[jj] = NULL;
+
+ g_ptr_array_add (result_array, element);
+ }
+ }
+ e_sexp_result_free (f, sub_result);
+ }
+
+ /* The last element in this return array is the sub end delimiter */
+ element = query_delimiter_new (BOOK_QUERY_SUB_END);
+ g_ptr_array_add (result_array, element);
+
+ result = e_sexp_result_new (f, ESEXP_RES_ARRAY_PTR);
+ result->value.ptrarray = result_array;
+
+ return result;
+}
+
+static ESExpResult *
+func_check (struct _ESExp *f,
+ gint argc,
+ struct _ESExpResult **argv,
+ gpointer data)
+{
+ ESExpResult *result;
+ GPtrArray *result_array;
+ QueryElement *element = NULL;
+ EContactField field_id = 0;
+ const gchar *query_name = NULL;
+ const gchar *query_value = NULL;
+ const gchar *query_extra = NULL;
+ guint query_type;
+
+ query_type = GPOINTER_TO_UINT (data);
+
+ if (argc == 1 && query_type == BOOK_QUERY_EXISTS &&
+ argv[0]->type == ESEXP_RES_STRING) {
+ query_name = argv[0]->value.string;
+
+ field_id = e_contact_field_id (query_name);
+ } else if (argc == 2 &&
+ argv[0]->type == ESEXP_RES_STRING &&
+ argv[1]->type == ESEXP_RES_STRING) {
+ query_name = argv[0]->value.string;
+ query_value = argv[1]->value.string;
+
+ /* We use E_CONTACT_FIELD_LAST to hold the special case of "x-evolution-any-field" */
+ if (g_strcmp0 (query_name, "x-evolution-any-field") == 0)
+ field_id = E_CONTACT_FIELD_LAST;
+ else
+ field_id = e_contact_field_id (query_name);
+
+ } else if (argc == 3 &&
+ argv[0]->type == ESEXP_RES_STRING &&
+ argv[1]->type == ESEXP_RES_STRING &&
+ argv[2]->type == ESEXP_RES_STRING) {
+ query_name = argv[0]->value.string;
+ query_value = argv[1]->value.string;
+ query_extra = argv[2]->value.string;
+
+ field_id = e_contact_field_id (query_name);
+ }
+
+ if (IS_QUERY_PHONE (query_type)) {
+ QueryPhoneTest *test;
+
+ /* Collect data from this field test */
+ test = query_phone_test_new (query_type, field_id);
+ test->value = g_strdup (query_value);
+ test->region = g_strdup (query_extra);
+
+ element = (QueryElement *) test;
+ } else {
+ QueryFieldTest *test;
+
+ /* Collect data from this field test */
+ test = query_field_test_new (query_type, field_id);
+ test->value = g_strdup (query_value);
+
+ element = (QueryElement *) test;
+ }
+
+ /* Return an array with only one element, for lack of a pointer type ESExpResult */
+ result_array = g_ptr_array_new_with_free_func ((GDestroyNotify) query_element_free);
+ g_ptr_array_add (result_array, element);
+
+ result = e_sexp_result_new (f, ESEXP_RES_ARRAY_PTR);
+ result->value.ptrarray = result_array;
+
+ return result;
+}
+
+/* Initial stage of preflighting:
+ *
+ * o Parse the search expression and generate our array of QueryElements
+ * o Collect lengths of query terms
+ */
+static void
+query_preflight_initialize (PreflightContext *context,
+ const gchar *sexp)
+{
+ ESExp *sexp_parser;
+ ESExpResult *result;
+ gint esexp_error, ii;
+
+ if (!sexp || !*sexp) {
+ context->status = PREFLIGHT_LIST_ALL;
+ return;
+ }
+
+ sexp_parser = e_sexp_new ();
+
+ for (ii = 0; ii < G_N_ELEMENTS (check_symbols); ii++) {
+ if (check_symbols[ii].subset) {
+ e_sexp_add_ifunction (
+ sexp_parser, 0, check_symbols[ii].name,
+ func_check_subset,
+ GUINT_TO_POINTER (check_symbols[ii].test));
+ } else {
+ e_sexp_add_function (
+ sexp_parser, 0, check_symbols[ii].name,
+ func_check,
+ GUINT_TO_POINTER (check_symbols[ii].test));
+ }
+ }
+
+ e_sexp_input_text (sexp_parser, sexp, strlen (sexp));
+ esexp_error = e_sexp_parse (sexp_parser);
+
+ if (esexp_error == -1) {
+ context->status = PREFLIGHT_INVALID;
+ } else {
+ result = e_sexp_eval (sexp_parser);
+ if (result) {
+ if (result->type == ESEXP_RES_ARRAY_PTR) {
+ /* Just steal the array away from the ESexpResult */
+ context->constraints = result->value.ptrarray;
+ result->value.ptrarray = NULL;
+ } else {
+ context->status = PREFLIGHT_INVALID;
+ }
+ }
+
+ e_sexp_result_free (sexp_parser, result);
+ }
+
+ g_object_unref (sexp_parser);
+}
+
+typedef struct {
+ EBookCache *book_cache;
+ SummaryField *field;
+ gboolean condition;
+} AttrListCheckData;
+
+static gboolean
+check_has_attr_list_cb (QueryElement *element,
+ gint sub_level,
+ gint offset,
+ gpointer user_data)
+{
+ QueryFieldTest *test = (QueryFieldTest *) element;
+ AttrListCheckData *data = (AttrListCheckData *) user_data;
+
+ /* We havent resolved all the fields at this stage yet */
+ if (!test->field)
+ test->field = summary_field_get (data->book_cache, test->field_id);
+
+ if (test->field && test->field->type == E_TYPE_CONTACT_ATTR_LIST)
+ data->condition = TRUE;
+
+ /* Keep looping until we find one */
+ return !data->condition;
+}
+
+static gboolean
+check_different_fields_cb (QueryElement *element,
+ gint sub_level,
+ gint offset,
+ gpointer user_data)
+{
+ QueryFieldTest *test = (QueryFieldTest *) element;
+ AttrListCheckData *data = (AttrListCheckData *) user_data;
+
+ /* We havent resolved all the fields at this stage yet */
+ if (!test->field)
+ test->field = summary_field_get (data->book_cache, test->field_id);
+
+ if (test->field && data->field && test->field != data->field)
+ data->condition = TRUE;
+ else
+ data->field = test->field;
+
+ /* Keep looping until we find one */
+ return !data->condition;
+}
+
+/* What is done in this pass:
+ * o Viability of the query is analyzed, i.e. can it be done with the summary columns.
+ * o Phone numbers are parsed and loaded onto QueryPhoneTests
+ * o Bitmask of auxiliary tables is collected
+ */
+static void
+query_preflight_check (PreflightContext *context,
+ EBookCache *book_cache)
+{
+ gint ii, n_elements;
+ QueryElement **elements;
+ SubQueryContext *ctx;
+
+ context->status = PREFLIGHT_OK;
+
+ if (context->constraints != NULL) {
+ elements = (QueryElement **) context->constraints->pdata;
+ n_elements = context->constraints->len;
+ } else {
+ elements = NULL;
+ n_elements = 0;
+ }
+
+ ctx = sub_query_context_new ();
+
+ for (ii = 0; ii < n_elements; ii++) {
+ QueryFieldTest *test;
+ guint field_test;
+
+ if (elements[ii]->query >= BOOK_QUERY_SUB_FIRST) {
+ AttrListCheckData data = { book_cache, NULL, FALSE };
+
+ switch (elements[ii]->query) {
+ case BOOK_QUERY_SUB_OR:
+ /* An OR doesn't have to force us to use a LEFT JOIN, as long
+ as all its sub-conditions are on the same field. */
+ query_preflight_foreach_sub (elements,
+ n_elements,
+ ii, FALSE,
+ check_different_fields_cb,
+ &data);
+ case BOOK_QUERY_SUB_AND:
+ sub_query_context_push (ctx, elements[ii]->query, data.condition);
+ break;
+ case BOOK_QUERY_SUB_END:
+ sub_query_context_pop (ctx);
+ break;
+
+ /* It's too complicated to properly perform
+ * the unary NOT operator on a constraint which
+ * accesses attribute lists.
+ *
+ * Hint, if the contact has a "%.com" email address
+ * and a "%.org" email address, what do we return
+ * for (not (endswith "email" ".com") ?
+ *
+ * Currently we rely on DISTINCT to sort out
+ * muliple results from the attribute list tables,
+ * this breaks down with NOT.
+ */
+ case BOOK_QUERY_SUB_NOT:
+ query_preflight_foreach_sub (elements,
+ n_elements,
+ ii, FALSE,
+ check_has_attr_list_cb,
+ &data);
+
+ if (data.condition) {
+ context->status = MAX (
+ context->status,
+ PREFLIGHT_NOT_SUMMARIZED);
+ }
+ break;
+
+ default:
+ g_warn_if_reached ();
+ }
+
+ continue;
+ }
+
+ test = (QueryFieldTest *) elements[ii];
+ field_test = (EBookQueryTest) test->query;
+
+ if (!test->field)
+ test->field = summary_field_get (book_cache, test->field_id);
+
+ /* Even if the field is not in the summary, we need to
+ * retport unsupported errors if phone number queries are
+ * issued while libphonenumber is unavailable
+ */
+ if (!test->field) {
+ /* Special case for e_book_query_any_field_contains().
+ *
+ * We interpret 'x-evolution-any-field' as E_CONTACT_FIELD_LAST
+ */
+ if (test->field_id == E_CONTACT_FIELD_LAST) {
+ /* If we search for a NULL or zero length string, it
+ * means 'get all contacts', that is considered a summary
+ * query but is handled differently (i.e. we just drop the
+ * field tests and run a regular query).
+ *
+ * This is only true if the 'any field contains' query is
+ * the only test in the constraints, however.
+ */
+ if (n_elements == 1 && (!test->value || !test->value[0])) {
+
+ context->status = MAX (context->status, PREFLIGHT_LIST_ALL);
+ } else {
+
+ /* Searching for a value with 'x-evolution-any-field' is
+ * not a summary query.
+ */
+ context->status = MAX (context->status, PREFLIGHT_NOT_SUMMARIZED);
+ }
+ } else {
+ /* Couldnt resolve the field, it's not a summary query */
+ context->status = MAX (context->status, PREFLIGHT_NOT_SUMMARIZED);
+ }
+ }
+
+ if (test->field && test->field->type == E_TYPE_CONTACT_CERT) {
+ /* For certificates, and later potentially other fields,
+ * the only information in the summary is the fact that
+ * they exist, or not. So the only check we can do from
+ * the summary is BOOK_QUERY_EXISTS. */
+ if (field_test != BOOK_QUERY_EXISTS) {
+ context->status = MAX (context->status, PREFLIGHT_NOT_SUMMARIZED);
+ }
+ /* Bypass the other checks below which are not appropriate. */
+ continue;
+ }
+
+ switch (field_test) {
+ case E_BOOK_QUERY_IS:
+ break;
+
+ case BOOK_QUERY_EXISTS:
+ case E_BOOK_QUERY_CONTAINS:
+ case E_BOOK_QUERY_BEGINS_WITH:
+ case E_BOOK_QUERY_ENDS_WITH:
+ case E_BOOK_QUERY_REGEX_NORMAL:
+ /* All of these queries can only apply to string fields,
+ * or fields which hold multiple strings
+ */
+ if (test->field) {
+ if (test->field->type != G_TYPE_STRING &&
+ test->field->type != E_TYPE_CONTACT_ATTR_LIST) {
+ context->status = MAX (context->status, PREFLIGHT_INVALID);
+ }
+ }
+
+ break;
+
+ case BOOK_QUERY_EXISTS_VCARD:
+ /* Exists vCard queries only supported in the fallback */
+ context->status = MAX (context->status, PREFLIGHT_NOT_SUMMARIZED);
+ break;
+
+ case E_BOOK_QUERY_REGEX_RAW:
+ /* Raw regex queries only supported in the fallback */
+ context->status = MAX (context->status, PREFLIGHT_NOT_SUMMARIZED);
+ break;
+
+ case E_BOOK_QUERY_EQUALS_PHONE_NUMBER:
+ case E_BOOK_QUERY_EQUALS_NATIONAL_PHONE_NUMBER:
+ case E_BOOK_QUERY_EQUALS_SHORT_PHONE_NUMBER:
+ /* Phone number queries are supported so long as they are in the summary,
+ * libphonenumber is available, and the phone number string is a valid one
+ */
+ if (!e_phone_number_is_supported ()) {
+ context->status = MAX (context->status, PREFLIGHT_UNSUPPORTED);
+ } else {
+ QueryPhoneTest *phone_test = (QueryPhoneTest *) test;
+ EPhoneNumberCountrySource source;
+ EPhoneNumber *number;
+ const gchar *region_code;
+
+ if (phone_test->region)
+ region_code = phone_test->region;
+ else
+ region_code = book_cache->priv->region_code;
+
+ number = e_phone_number_from_string (
+ phone_test->value,
+ region_code, NULL);
+
+ if (number == NULL) {
+ context->status = MAX (context->status, PREFLIGHT_INVALID);
+ } else {
+ /* Collect values we'll need later while generating field
+ * tests, no need to parse the phone number more than once
+ */
+ phone_test->national = e_phone_number_get_national_number (number);
+ phone_test->country = e_phone_number_get_country_code (number,
&source);
+ phone_test->national = remove_leading_zeros (phone_test->national);
+
+ if (source == E_PHONE_NUMBER_COUNTRY_FROM_DEFAULT)
+ phone_test->country = 0;
+
+ e_phone_number_free (number);
+ }
+ }
+ break;
+ }
+
+ if (test->field &&
+ test->field->type == E_TYPE_CONTACT_ATTR_LIST) {
+ gint aux_index = summary_field_get_index (book_cache, test->field_id);
+
+ /* It's really improbable that we ever get 64 fields in the summary
+ * In any case we warn about this in e_book_sqlite_new_full().
+ */
+ g_warn_if_fail (aux_index >= 0 && aux_index < EBC_MAX_SUMMARY_FIELDS);
+ context->aux_mask |= (1 << aux_index);
+
+ /* If this condition is a *requirement* for the overall query to
+ match a given record (i.e. there's no surrounding 'OR' but
+ only 'AND'), then we can use an inner join for the query and
+ it will be a lot more efficient. If records without this
+ condition can also match the overall condition, then we must
+ use LEFT JOIN. */
+ if (sub_query_context_peek_cond_counter (ctx)) {
+ context->left_join_mask |= (1 << aux_index);
+ }
+ }
+ }
+
+ sub_query_context_free (ctx);
+}
+
+/* Handle special case of E_CONTACT_FULL_NAME
+ *
+ * For any query which accesses the full name field,
+ * we need to also OR it with any of the related name
+ * fields, IF those are found in the summary as well.
+ */
+static void
+query_preflight_substitute_full_name (PreflightContext *context,
+ EBookCache *book_cache)
+{
+ gint ii, jj;
+
+ for (ii = 0; context->constraints != NULL && ii < context->constraints->len; ii++) {
+ SummaryField *family_name, *given_name, *nickname;
+ QueryElement *element;
+ QueryFieldTest *test;
+
+ element = g_ptr_array_index (context->constraints, ii);
+
+ if (element->query >= BOOK_QUERY_SUB_FIRST)
+ continue;
+
+ test = (QueryFieldTest *) element;
+ if (test->field_id != E_CONTACT_FULL_NAME)
+ continue;
+
+ family_name = summary_field_get (book_cache, E_CONTACT_FAMILY_NAME);
+ given_name = summary_field_get (book_cache, E_CONTACT_GIVEN_NAME);
+ nickname = summary_field_get (book_cache, E_CONTACT_NICKNAME);
+
+ /* If any of these are in the summary, then we'll construct
+ * a grouped OR statment for this E_CONTACT_FULL_NAME test */
+ if (family_name || given_name || nickname) {
+ /* Add the OR directly before the E_CONTACT_FULL_NAME test */
+ constraints_insert_delimiter (context->constraints, ii, BOOK_QUERY_SUB_OR);
+
+ jj = ii + 2;
+
+ if (family_name)
+ constraints_insert_field_test (
+ context->constraints, jj++,
+ family_name, test->query,
+ test->value);
+
+ if (given_name)
+ constraints_insert_field_test (
+ context->constraints, jj++,
+ given_name, test->query,
+ test->value);
+
+ if (nickname)
+ constraints_insert_field_test (
+ context->constraints, jj++,
+ nickname, test->query,
+ test->value);
+
+ constraints_insert_delimiter (context->constraints, jj, BOOK_QUERY_SUB_END);
+
+ ii = jj;
+ }
+ }
+}
+
+static void
+query_preflight (PreflightContext *context,
+ EBookCache *book_cache,
+ const gchar *sexp)
+{
+ query_preflight_initialize (context, sexp);
+
+ if (context->status == PREFLIGHT_OK) {
+ query_preflight_check (context, book_cache);
+
+ /* No need to change the constraints if we're not
+ * going to generate statements with it
+ */
+ if (context->status == PREFLIGHT_OK) {
+ /* Handle E_CONTACT_FULL_NAME substitutions */
+ query_preflight_substitute_full_name (context, book_cache);
+ } else {
+ /* We might use this context to perform a fallback query,
+ * so let's clear out all the constraints now
+ */
+ preflight_context_clear (context);
+ }
+ }
+}
+
+/**********************************************************
+ * Field Test Generators *
+ **********************************************************
+ *
+ * This section contains the field test generators for
+ * various EBookQueryTest types. When implementing new
+ * query types, a new GenerateFieldTest needs to be created
+ * and added to the table below.
+ */
+
+typedef void (* GenerateFieldTest) (EBookCache *book_cache,
+ GString *string,
+ QueryFieldTest *test);
+
+/* Appends an identifier suitable to identify the
+ * column to test in the context of a query.
+ *
+ * The suffix is for special indexed columns (such as
+ * reverse values, sort keys, phone numbers, etc).
+ */
+static void
+ebc_string_append_column (GString *string,
+ SummaryField *field,
+ const gchar *suffix)
+{
+ if (field->aux_table) {
+ g_string_append (string, field->aux_table_symbolic);
+ g_string_append (string, ".value");
+ } else {
+ g_string_append (string, "summary.");
+ g_string_append (string, field->dbname);
+ }
+
+ if (suffix) {
+ g_string_append_c (string, '_');
+ g_string_append (string, suffix);
+ }
+}
+
+/* This function escapes characters which need escaping
+ * for LIKE statements as well as the single quotes.
+ *
+ * The return value is not suitable to be formatted
+ * with %Q or %q
+ */
+static gchar *
+ebc_normalize_for_like (QueryFieldTest *test,
+ gboolean reverse_string,
+ gboolean *escape_needed)
+{
+ GString *str;
+ size_t len;
+ gchar cc;
+ gboolean escape_modifier_needed = FALSE;
+ const gchar *normal = NULL;
+ const gchar *ptr;
+ const gchar *str_to_escape;
+ gchar *reverse = NULL;
+ gchar *freeme = NULL;
+
+ if (test->field_id == E_CONTACT_UID ||
+ test->field_id == E_CONTACT_REV) {
+ normal = test->value;
+ } else {
+ freeme = e_util_utf8_normalize (test->value);
+ normal = freeme;
+ }
+
+ if (reverse_string) {
+ reverse = g_utf8_strreverse (normal, -1);
+ str_to_escape = reverse;
+ } else
+ str_to_escape = normal;
+
+ /* Just assume each character must be escaped. The result of this function
+ * is discarded shortly after calling this function. Therefore it's
+ * acceptable to possibly allocate twice the memory needed.
+ */
+ len = strlen (str_to_escape);
+ str = g_string_sized_new (2 * len + 4 + strlen (EBC_ESCAPE_SEQUENCE) - 1);
+
+ ptr = str_to_escape;
+ while ((cc = *ptr++)) {
+ if (cc == '\'') {
+ g_string_append_c (str, '\'');
+ } else if (cc == '%' || cc == '_' || cc == '^') {
+ g_string_append_c (str, '^');
+ escape_modifier_needed = TRUE;
+ }
+
+ g_string_append_c (str, cc);
+ }
+
+ if (escape_needed)
+ *escape_needed = escape_modifier_needed;
+
+ g_free (freeme);
+ g_free (reverse);
+
+ return g_string_free (str, FALSE);
+}
+
+static void
+field_test_query_is (EBookCache *book_cache,
+ GString *string,
+ QueryFieldTest *test)
+{
+ SummaryField *field = test->field;
+ gchar *normal;
+
+ ebc_string_append_column (string, field, NULL);
+
+ if (test->field_id == E_CONTACT_UID ||
+ test->field_id == E_CONTACT_REV) {
+ /* UID & REV fields are not normalized in the summary */
+ e_cache_sqlite_stmt_append_printf (string, " = %Q", test->value);
+ } else {
+ normal = e_util_utf8_normalize (test->value);
+ e_cache_sqlite_stmt_append_printf (string, " = %Q", normal);
+ g_free (normal);
+ }
+}
+
+static void
+field_test_query_contains (EBookCache *book_cache,
+ GString *string,
+ QueryFieldTest *test)
+{
+ SummaryField *field = test->field;
+ gboolean need_escape;
+ gchar *escaped;
+
+ escaped = ebc_normalize_for_like (test, FALSE, &need_escape);
+
+ g_string_append_c (string, '(');
+
+ ebc_string_append_column (string, field, NULL);
+ g_string_append (string, " IS NOT NULL AND ");
+ ebc_string_append_column (string, field, NULL);
+ g_string_append (string, " LIKE '%");
+ g_string_append (string, escaped);
+ g_string_append (string, "%'");
+
+ if (need_escape)
+ g_string_append (string, EBC_ESCAPE_SEQUENCE);
+
+ g_string_append_c (string, ')');
+
+ g_free (escaped);
+}
+
+static void
+field_test_query_begins_with (EBookCache *book_cache,
+ GString *string,
+ QueryFieldTest *test)
+{
+ SummaryField *field = test->field;
+ gboolean need_escape;
+ gchar *escaped;
+
+ escaped = ebc_normalize_for_like (test, FALSE, &need_escape);
+
+ g_string_append_c (string, '(');
+ ebc_string_append_column (string, field, NULL);
+ g_string_append (string, " IS NOT NULL AND ");
+
+ ebc_string_append_column (string, field, NULL);
+ g_string_append (string, " LIKE \'");
+ g_string_append (string, escaped);
+ g_string_append (string, "%\'");
+
+ if (need_escape)
+ g_string_append (string, EBC_ESCAPE_SEQUENCE);
+ g_string_append_c (string, ')');
+
+ g_free (escaped);
+}
+
+static void
+field_test_query_ends_with (EBookCache *book_cache,
+ GString *string,
+ QueryFieldTest *test)
+{
+ SummaryField *field = test->field;
+ gboolean need_escape;
+ gchar *escaped;
+
+ if ((field->index & INDEX_FLAG (SUFFIX)) != 0) {
+ escaped = ebc_normalize_for_like (test, TRUE, &need_escape);
+
+ g_string_append_c (string, '(');
+ ebc_string_append_column (string, field, EBC_SUFFIX_REVERSE);
+ g_string_append (string, " IS NOT NULL AND ");
+
+ ebc_string_append_column (string, field, EBC_SUFFIX_REVERSE);
+ g_string_append (string, " LIKE \'");
+ g_string_append (string, escaped);
+ g_string_append (string, "%\'");
+ } else {
+ escaped = ebc_normalize_for_like (test, FALSE, &need_escape);
+ g_string_append_c (string, '(');
+
+ ebc_string_append_column (string, field, NULL);
+ g_string_append (string, " IS NOT NULL AND ");
+
+ ebc_string_append_column (string, field, NULL);
+ g_string_append (string, " LIKE \'%");
+ g_string_append (string, escaped);
+ g_string_append (string, "\'");
+ }
+
+ if (need_escape)
+ g_string_append (string, EBC_ESCAPE_SEQUENCE);
+
+ g_string_append_c (string, ')');
+ g_free (escaped);
+}
+
+static void
+field_test_query_eqphone (EBookCache *book_cache,
+ GString *string,
+ QueryFieldTest *test)
+{
+ SummaryField *field = test->field;
+ QueryPhoneTest *phone_test = (QueryPhoneTest *) test;
+
+ if ((field->index & INDEX_FLAG (PHONE)) != 0) {
+ g_string_append_c (string, '(');
+ ebc_string_append_column (string, field, EBC_SUFFIX_PHONE);
+ e_cache_sqlite_stmt_append_printf (string, " = %Q AND ", phone_test->national);
+
+ /* For exact matches, a country code qualifier is required by both
+ * query input and row input
+ */
+ ebc_string_append_column (string, field, EBC_SUFFIX_COUNTRY);
+ g_string_append (string, " != 0 AND ");
+
+ ebc_string_append_column (string, field, EBC_SUFFIX_COUNTRY);
+ e_cache_sqlite_stmt_append_printf (string, " = %d", phone_test->country);
+ g_string_append_c (string, ')');
+ } else {
+ /* No indexed columns available, perform the fallback */
+ g_string_append (string, EBC_FUNC_EQPHONE_EXACT " (");
+ ebc_string_append_column (string, field, NULL);
+ e_cache_sqlite_stmt_append_printf (string, ", %Q)", test->value);
+ }
+}
+
+static void
+field_test_query_eqphone_national (EBookCache *book_cache,
+ GString *string,
+ QueryFieldTest *test)
+{
+
+ SummaryField *field = test->field;
+ QueryPhoneTest *phone_test = (QueryPhoneTest *) test;
+
+ if ((field->index & INDEX_FLAG (PHONE)) != 0) {
+ /* Only a compound expression if there is a country code */
+ if (phone_test->country)
+ g_string_append_c (string, '(');
+
+ /* Generate: phone = %Q */
+ ebc_string_append_column (string, field, EBC_SUFFIX_PHONE);
+ e_cache_sqlite_stmt_append_printf (string, " = %Q", phone_test->national);
+
+ /* When doing a national search, no need to check country
+ * code unless the query number also has a country code
+ */
+ if (phone_test->country) {
+ /* Generate: (phone = %Q AND (country = 0 OR country = %d)) */
+ g_string_append (string, " AND (");
+ ebc_string_append_column (string, field, EBC_SUFFIX_COUNTRY);
+ g_string_append (string, " = 0 OR ");
+ ebc_string_append_column (string, field, EBC_SUFFIX_COUNTRY);
+ e_cache_sqlite_stmt_append_printf (string, " = %d))", phone_test->country);
+ }
+ } else {
+ /* No indexed columns available, perform the fallback */
+ g_string_append (string, EBC_FUNC_EQPHONE_NATIONAL " (");
+ ebc_string_append_column (string, field, NULL);
+ e_cache_sqlite_stmt_append_printf (string, ", %Q)", test->value);
+ }
+}
+
+static void
+field_test_query_eqphone_short (EBookCache *book_cache,
+ GString *string,
+ QueryFieldTest *test)
+{
+ SummaryField *field = test->field;
+
+ /* No quick way to do the short match */
+ g_string_append (string, EBC_FUNC_EQPHONE_SHORT " (");
+ ebc_string_append_column (string, field, NULL);
+ e_cache_sqlite_stmt_append_printf (string, ", %Q)", test->value);
+}
+
+static void
+field_test_query_regex_normal (EBookCache *book_cache,
+ GString *string,
+ QueryFieldTest *test)
+{
+ SummaryField *field = test->field;
+ gchar *normal;
+
+ normal = e_util_utf8_normalize (test->value);
+
+ if (field->aux_table) {
+ e_cache_sqlite_stmt_append_printf (
+ string, "%s.value REGEXP %Q",
+ field->aux_table_symbolic,
+ normal);
+ } else {
+ e_cache_sqlite_stmt_append_printf (
+ string, "summary.%s REGEXP %Q",
+ field->dbname,
+ normal);
+ }
+
+ g_free (normal);
+}
+
+static void
+field_test_query_exists (EBookCache *book_cache,
+ GString *string,
+ QueryFieldTest *test)
+{
+ SummaryField *field = test->field;
+
+ ebc_string_append_column (string, field, NULL);
+
+ if (test->field->type == E_TYPE_CONTACT_CERT)
+ e_cache_sqlite_stmt_append_printf (string, " IS NOT '0'");
+ else
+ e_cache_sqlite_stmt_append_printf (string, " IS NOT NULL");
+}
+
+/* Lookup table for field test generators per EBookQueryTest,
+ *
+ * WARNING: This must stay in line with the EBookQueryTest definition.
+ */
+static const GenerateFieldTest field_test_func_table[] = {
+ field_test_query_is, /* E_BOOK_QUERY_IS */
+ field_test_query_contains, /* E_BOOK_QUERY_CONTAINS */
+ field_test_query_begins_with, /* E_BOOK_QUERY_BEGINS_WITH */
+ field_test_query_ends_with, /* E_BOOK_QUERY_ENDS_WITH */
+ field_test_query_eqphone, /* E_BOOK_QUERY_EQUALS_PHONE_NUMBER */
+ field_test_query_eqphone_national, /* E_BOOK_QUERY_EQUALS_NATIONAL_PHONE_NUMBER */
+ field_test_query_eqphone_short, /* E_BOOK_QUERY_EQUALS_SHORT_PHONE_NUMBER */
+ field_test_query_regex_normal, /* E_BOOK_QUERY_REGEX_NORMAL */
+ NULL /* Requires fallback */, /* E_BOOK_QUERY_REGEX_RAW */
+ field_test_query_exists, /* BOOK_QUERY_EXISTS */
+ NULL /* Requires fallback */ /* BOOK_QUERY_EXISTS_VCARD */
+};
+
+/**********************************************************
+ * Querying Contacts *
+ **********************************************************/
+
+/* The various search types indicate what should be fetched
+ */
+typedef enum {
+ SEARCH_FULL, /* Get a list of EBookCacheSearchData*/
+ SEARCH_UID_AND_REV, /* Get a list of EBookCacheSearchData, with shallow vcards only containing UID
& REV */
+ SEARCH_UID, /* Get a list of UID strings */
+ SEARCH_COUNT, /* Get the number of matching rows */
+} SearchType;
+
+static void
+ebc_generate_constraints (EBookCache *book_cache,
+ GString *string,
+ GPtrArray *constraints,
+ const gchar *sexp)
+{
+ SubQueryContext *ctx;
+ QueryDelimiter *delim;
+ QueryFieldTest *test;
+ QueryElement **elements;
+ gint n_elements, ii;
+
+ /* If there are no constraints, we generate the fallback constraint for 'sexp' */
+ if (constraints == NULL) {
+ e_cache_sqlite_stmt_append_printf (
+ string,
+ EBC_FUNC_COMPARE_VCARD " (%Q,summary." E_CACHE_COLUMN_OBJECT ")",
+ sexp);
+ return;
+ }
+
+ elements = (QueryElement **) constraints->pdata;
+ n_elements = constraints->len;
+
+ ctx = sub_query_context_new ();
+
+ for (ii = 0; ii < n_elements; ii++) {
+ GenerateFieldTest generate_test_func = NULL;
+
+ /* Seperate field tests with the appropriate grouping */
+ if (elements[ii]->query != BOOK_QUERY_SUB_END &&
+ sub_query_context_increment (ctx) > 0) {
+ guint delim_type = sub_query_context_peek_type (ctx);
+
+ switch (delim_type) {
+ case BOOK_QUERY_SUB_AND:
+ g_string_append (string, " AND ");
+ break;
+
+ case BOOK_QUERY_SUB_OR:
+ g_string_append (string, " OR ");
+ break;
+
+ case BOOK_QUERY_SUB_NOT:
+ /* Nothing to do between children of NOT,
+ * there should only ever be one child of NOT anyway
+ */
+ break;
+
+ case BOOK_QUERY_SUB_END:
+ default:
+ g_warn_if_reached ();
+ }
+ }
+
+ if (elements[ii]->query >= BOOK_QUERY_SUB_FIRST) {
+ delim = (QueryDelimiter *) elements[ii];
+
+ switch (delim->query) {
+ case BOOK_QUERY_SUB_NOT:
+ /* NOT is a unary operator and as such
+ * comes before the opening parenthesis
+ */
+ g_string_append (string, "NOT ");
+
+ /* Fall through */
+
+ case BOOK_QUERY_SUB_AND:
+ case BOOK_QUERY_SUB_OR:
+ /* Open a grouped statement and push the context */
+ sub_query_context_push (ctx, delim->query, FALSE);
+ g_string_append_c (string, '(');
+ break;
+
+ case BOOK_QUERY_SUB_END:
+ /* Close a grouped statement and pop the context */
+ g_string_append_c (string, ')');
+ sub_query_context_pop (ctx);
+ break;
+ default:
+ g_warn_if_reached ();
+ }
+
+ continue;
+ }
+
+ /* Find the appropriate field test generator */
+ test = (QueryFieldTest *) elements[ii];
+ if (test->query < G_N_ELEMENTS (field_test_func_table))
+ generate_test_func = field_test_func_table[test->query];
+
+ /* These should never happen, if it does it should be
+ * fixed in the preflight checks
+ */
+ g_warn_if_fail (generate_test_func != NULL);
+ g_warn_if_fail (test->field != NULL);
+
+ /* Generate the field test */
+ /* coverity[var_deref_op] */
+ generate_test_func (book_cache, string, test);
+ }
+
+ sub_query_context_free (ctx);
+}
+
+static void
+ebc_search_meta_contacts_cb (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ const gchar *extra,
+ gpointer out_value)
+{
+ GSList **out_list = out_value;
+ EBookCacheSearchData *sd;
+ EContact *contact;
+ gchar *vcard;
+
+ g_return_if_fail (out_list != NULL);
+
+ contact = e_contact_new ();
+
+ e_contact_set (contact, E_CONTACT_UID, uid);
+ if (revision)
+ e_contact_set (contact, E_CONTACT_REV, revision);
+
+ vcard = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
+
+ g_object_unref (contact);
+
+ sd = e_book_cache_search_data_new (uid, vcard, extra);
+
+ *out_list = g_slist_prepend (*out_list, sd);
+
+ g_free (vcard);
+}
+
+static void
+ebc_search_full_contacts_cb (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ const gchar *extra,
+ gpointer out_value)
+{
+ GSList **out_list = out_value;
+ EBookCacheSearchData *sd;
+
+ g_return_if_fail (out_list != NULL);
+
+ sd = e_book_cache_search_data_new (uid, object, extra);
+
+ *out_list = g_slist_prepend (*out_list, sd);
+}
+
+static void
+ebc_search_uids_cb (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ const gchar *extra,
+ gpointer out_value)
+{
+ GSList **out_list = out_value;
+
+ g_return_if_fail (out_list != NULL);
+
+ *out_list = g_slist_prepend (*out_list, g_strdup (uid));
+}
+
+typedef void (* EBookCacheInternalSearchFunc) (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ const gchar *extra,
+ gpointer out_value);
+
+/* Generates the SELECT portion of the query, this will take care of
+ * preparing the context of the query, and add the needed JOIN statements
+ * based on which fields are referenced in the query expression.
+ *
+ * This also handles getting the correct callback and asking for the
+ * right data depending on the 'search_type'
+ */
+static EBookCacheInternalSearchFunc
+ebc_generate_select (EBookCache *book_cache,
+ GString *string,
+ SearchType search_type,
+ PreflightContext *context,
+ GError **error)
+{
+ EBookCacheInternalSearchFunc callback = NULL;
+ gboolean add_auxiliary_tables = FALSE;
+ gint ii;
+
+ if (context->status == PREFLIGHT_OK &&
+ context->aux_mask != 0)
+ add_auxiliary_tables = TRUE;
+
+ g_string_append (string, "SELECT ");
+ if (add_auxiliary_tables)
+ g_string_append (string, "DISTINCT ");
+
+ switch (search_type) {
+ case SEARCH_FULL:
+ callback = ebc_search_full_contacts_cb;
+ g_string_append (string, "summary." E_CACHE_COLUMN_UID ",");
+ g_string_append (string, "summary." E_CACHE_COLUMN_REVISION ",");
+ g_string_append (string, "summary." E_CACHE_COLUMN_OBJECT ",");
+ g_string_append (string, "summary." E_CACHE_COLUMN_STATE ",");
+ g_string_append (string, "summary." EBC_COLUMN_EXTRA " ");
+ break;
+ case SEARCH_UID_AND_REV:
+ callback = ebc_search_meta_contacts_cb;
+ g_string_append (string, "summary." E_CACHE_COLUMN_UID ", summary." E_CACHE_COLUMN_REVISION
", summary." EBC_COLUMN_EXTRA " ");
+ break;
+ case SEARCH_UID:
+ callback = ebc_search_uids_cb;
+ g_string_append (string, "summary." E_CACHE_COLUMN_UID ",");
+ g_string_append (string, "summary." E_CACHE_COLUMN_REVISION " ");
+ break;
+ case SEARCH_COUNT:
+ if (context->aux_mask != 0)
+ g_string_append (string, "count (DISTINCT summary." E_CACHE_COLUMN_UID ") ");
+ else
+ g_string_append (string, "count (*) ");
+ break;
+ }
+
+ e_cache_sqlite_stmt_append_printf (string, "FROM %Q AS summary", E_CACHE_TABLE_OBJECTS);
+
+ /* Add any required auxiliary tables into the query context */
+ if (add_auxiliary_tables) {
+ for (ii = 0; ii < book_cache->priv->n_summary_fields; ii++) {
+
+ /* We cap this at EBC_MAX_SUMMARY_FIELDS (64 bits) at creation time */
+ if ((context->aux_mask & (1 << ii)) != 0) {
+ SummaryField *field = &(book_cache->priv->summary_fields[ii]);
+ gboolean left_join = (context->left_join_mask >> ii) & 1;
+
+ /* Note the '+' in the JOIN statement.
+ *
+ * This plus makes the uid's index ineligable to participate
+ * in any indexing.
+ *
+ * Without this, the indexes which we prefer for prefix or
+ * suffix matching in the auxiliary tables are ignored and
+ * only considered on exact matches.
+ *
+ * This is crucial to ensure that the uid index does not
+ * compete with the value index in constraints such as:
+ *
+ * WHERE email_list.value LIKE "boogieman%"
+ */
+ e_cache_sqlite_stmt_append_printf (
+ string, " %sJOIN %Q AS %s ON %s%s.uid = summary." E_CACHE_COLUMN_UID,
+ left_join ? "LEFT " : "",
+ field->aux_table,
+ field->aux_table_symbolic,
+ left_join ? "" : "+",
+ field->aux_table_symbolic);
+ }
+ }
+ }
+
+ return callback;
+}
+
+static gboolean
+ebc_is_autocomplete_query (PreflightContext *context)
+{
+ QueryFieldTest *test;
+ QueryElement **elements;
+ gint n_elements, ii;
+ int non_aux_fields = 0;
+
+ if (context->status != PREFLIGHT_OK || context->aux_mask == 0)
+ return FALSE;
+
+ elements = (QueryElement **) context->constraints->pdata;
+ n_elements = context->constraints->len;
+
+ for (ii = 0; ii < n_elements; ii++) {
+ test = (QueryFieldTest *) elements[ii];
+
+ /* For these, check if the field being operated on is
+ an auxiliary field or not. */
+ if (elements[ii]->query == E_BOOK_QUERY_BEGINS_WITH ||
+ elements[ii]->query == E_BOOK_QUERY_ENDS_WITH ||
+ elements[ii]->query == E_BOOK_QUERY_IS ||
+ elements[ii]->query == BOOK_QUERY_EXISTS ||
+ elements[ii]->query == E_BOOK_QUERY_CONTAINS) {
+ if (test->field->type != E_TYPE_CONTACT_ATTR_LIST)
+ non_aux_fields++;
+ continue;
+ }
+
+ /* Nothing else is allowed other than "(or" ... ")" */
+ if (elements[ii]->query != BOOK_QUERY_SUB_OR &&
+ elements[ii]->query != BOOK_QUERY_SUB_END)
+ return FALSE;
+ }
+
+ /* If there were no non-aux fields being queried, don't bother */
+ return non_aux_fields != 0;
+}
+
+static EBookCacheInternalSearchFunc
+ebc_generate_autocomplete_query (EBookCache *book_cache,
+ GString *string,
+ SearchType search_type,
+ PreflightContext *context,
+ GError **error)
+{
+ QueryElement **elements;
+ gint n_elements, ii;
+ guint64 aux_mask = context->aux_mask;
+ guint64 left_join_mask = context->left_join_mask;
+ EBookCacheInternalSearchFunc callback;
+ gboolean first = TRUE;
+
+ elements = (QueryElement **) context->constraints->pdata;
+ n_elements = context->constraints->len;
+
+ /* First the queries which use aux tables. */
+ for (ii = 0; ii < n_elements; ii++) {
+ GenerateFieldTest generate_test_func = NULL;
+ QueryFieldTest *test;
+ gint aux_index;
+
+ if (elements[ii]->query == BOOK_QUERY_SUB_OR ||
+ elements[ii]->query == BOOK_QUERY_SUB_END)
+ continue;
+
+ test = (QueryFieldTest *) elements[ii];
+ if (test->field->type != E_TYPE_CONTACT_ATTR_LIST)
+ continue;
+
+ aux_index = summary_field_get_index (book_cache, test->field_id);
+ g_warn_if_fail (aux_index >= 0 && aux_index < EBC_MAX_SUMMARY_FIELDS);
+ context->aux_mask = (1 << aux_index);
+ context->left_join_mask = 0;
+
+ callback = ebc_generate_select (book_cache, string, search_type, context, error);
+ e_cache_sqlite_stmt_append_printf (string, " WHERE summary." E_CACHE_COLUMN_STATE "!=%d AND
(", E_OFFLINE_STATE_LOCALLY_DELETED);
+ context->aux_mask = aux_mask;
+ context->left_join_mask = left_join_mask;
+ if (!callback)
+ return NULL;
+
+ generate_test_func = field_test_func_table[test->query];
+ generate_test_func (book_cache, string, test);
+
+ g_string_append (string, ") UNION ");
+ }
+
+ /* Finally, generate the SELECT for the primary fields. */
+ context->aux_mask = 0;
+ callback = ebc_generate_select (book_cache, string, search_type, context, error);
+ context->aux_mask = aux_mask;
+ if (!callback)
+ return NULL;
+
+ e_cache_sqlite_stmt_append_printf (string, " WHERE summary." E_CACHE_COLUMN_STATE "!=%d AND (",
E_OFFLINE_STATE_LOCALLY_DELETED);
+
+ for (ii = 0; ii < n_elements; ii++) {
+ GenerateFieldTest generate_test_func = NULL;
+ QueryFieldTest *test;
+
+ if (elements[ii]->query == BOOK_QUERY_SUB_OR ||
+ elements[ii]->query == BOOK_QUERY_SUB_END)
+ continue;
+
+ test = (QueryFieldTest *) elements[ii];
+ if (test->field->type == E_TYPE_CONTACT_ATTR_LIST)
+ continue;
+
+ if (!first)
+ g_string_append (string, " OR ");
+ else
+ first = FALSE;
+
+ generate_test_func = field_test_func_table[test->query];
+ generate_test_func (book_cache, string, test);
+ }
+
+ g_string_append (string, ")");
+
+ return callback;
+}
+
+struct EBCSearchData {
+ gint uid_index;
+ gint revision_index;
+ gint object_index;
+ gint extra_index;
+ gint state_index;
+
+ EBookCacheInternalSearchFunc func;
+ gpointer out_value;
+
+ EBookCacheSearchFunc user_func;
+ gpointer user_func_user_data;
+};
+
+static gboolean
+ebc_search_select_cb (ECache *cache,
+ gint ncols,
+ const gchar *column_names[],
+ const gchar *column_values[],
+ gpointer user_data)
+{
+ struct EBCSearchData *sd = user_data;
+ const gchar *object = NULL, *extra = NULL;
+ EOfflineState offline_state = E_OFFLINE_STATE_UNKNOWN;
+
+ g_return_val_if_fail (sd != NULL, FALSE);
+ g_return_val_if_fail (sd->func != NULL || sd->user_func != NULL, FALSE);
+ g_return_val_if_fail (sd->out_value != NULL || sd->user_func != NULL, FALSE);
+
+ if (sd->uid_index == -1 ||
+ sd->revision_index == -1 ||
+ sd->object_index == -1 ||
+ sd->extra_index == -1 ||
+ sd->state_index == -1) {
+ gint ii;
+
+ for (ii = 0; ii < ncols && (sd->uid_index == -1 ||
+ sd->revision_index == -1 ||
+ sd->object_index == -1 ||
+ sd->extra_index == -1 ||
+ sd->state_index == -1); ii++) {
+ const gchar *cname = column_names[ii];
+
+ if (!cname)
+ continue;
+
+ if (g_str_has_prefix (cname, "summary."))
+ cname += 8;
+
+ if (sd->uid_index == -1 && g_ascii_strcasecmp (cname, E_CACHE_COLUMN_UID) == 0) {
+ sd->uid_index = ii;
+ } else if (sd->revision_index == -1 && g_ascii_strcasecmp (cname,
E_CACHE_COLUMN_REVISION) == 0) {
+ sd->revision_index = ii;
+ } else if (sd->object_index == -1 && g_ascii_strcasecmp (cname,
E_CACHE_COLUMN_OBJECT) == 0) {
+ sd->object_index = ii;
+ } else if (sd->extra_index == -1 && g_ascii_strcasecmp (cname, EBC_COLUMN_EXTRA) ==
0) {
+ sd->extra_index = ii;
+ } else if (sd->state_index == -1 && g_ascii_strcasecmp (cname, E_CACHE_COLUMN_STATE)
== 0) {
+ sd->state_index = ii;
+ }
+ }
+ }
+
+ g_return_val_if_fail (sd->uid_index >= 0 && sd->uid_index < ncols, FALSE);
+ g_return_val_if_fail (sd->revision_index >= 0 && sd->revision_index < ncols, FALSE);
+
+ if (sd->object_index != -2) {
+ g_return_val_if_fail (sd->object_index >= 0 && sd->object_index < ncols, FALSE);
+ object = column_values[sd->object_index];
+ }
+
+ if (sd->extra_index != -2) {
+ g_return_val_if_fail (sd->extra_index >= 0 && sd->extra_index < ncols, FALSE);
+ extra = column_values[sd->extra_index];
+ }
+
+ if (sd->state_index != -2) {
+ g_return_val_if_fail (sd->extra_index >= 0 && sd->extra_index < ncols, FALSE);
+
+ if (!column_values[sd->state_index])
+ offline_state = E_OFFLINE_STATE_UNKNOWN;
+ else
+ offline_state = g_ascii_strtoull (column_values[sd->state_index], NULL, 10);
+ }
+
+ if (sd->user_func) {
+ return sd->user_func (E_BOOK_CACHE (cache), column_values[sd->uid_index],
column_values[sd->revision_index],
+ object, extra, offline_state, sd->user_func_user_data);
+ }
+
+ sd->func (cache, column_values[sd->uid_index], column_values[sd->revision_index], object, extra,
sd->out_value);
+
+ return TRUE;
+}
+
+static gboolean
+ebc_do_search_query (EBookCache *book_cache,
+ PreflightContext *context,
+ const gchar *sexp,
+ SearchType search_type,
+ gpointer out_value,
+ EBookCacheSearchFunc func,
+ gpointer func_user_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ struct EBCSearchData sd;
+ GString *stmt;
+ gboolean success = FALSE;
+
+ /* We might calculate a reasonable estimation of bytes
+ * during the preflight checks */
+ stmt = g_string_sized_new (GENERATED_QUERY_BYTES);
+
+ /* Extra special case. For the common case of the email composer's
+ addressbook autocompletion, we really want the most optimal query.
+ So check for it and use a basically hand-crafted one. */
+ if (ebc_is_autocomplete_query (context)) {
+ sd.func = ebc_generate_autocomplete_query (book_cache, stmt, search_type, context, error);
+ } else {
+ /* Generate the leading SELECT statement */
+ sd.func = ebc_generate_select (book_cache, stmt, search_type, context, error);
+
+ if (sd.func) {
+ e_cache_sqlite_stmt_append_printf (stmt,
+ " WHERE summary." E_CACHE_COLUMN_STATE "!=%d",
+ E_OFFLINE_STATE_LOCALLY_DELETED);
+
+ if (EBC_STATUS_GEN_CONSTRAINTS (context->status)) {
+ GString *where_clause = g_string_new ("");
+
+ /*
+ * Now generate the search expression on the main contacts table
+ */
+ ebc_generate_constraints (book_cache, where_clause, context->constraints,
sexp);
+ if (where_clause->len)
+ e_cache_sqlite_stmt_append_printf (stmt, " AND (%s)",
where_clause->str);
+ g_string_free (where_clause, TRUE);
+ }
+ }
+ }
+
+ if (sd.func) {
+ sd.uid_index = -1;
+ sd.revision_index = -1;
+ sd.object_index = search_type == SEARCH_FULL ? -1 : -2;
+ sd.extra_index = search_type == SEARCH_UID ? -2 : -1;
+ sd.state_index = search_type == SEARCH_FULL ? -1 : -2;
+ sd.out_value = out_value;
+ sd.user_func = func;
+ sd.user_func_user_data = func_user_data;
+
+ success = e_cache_sqlite_select (E_CACHE (book_cache), stmt->str,
+ ebc_search_select_cb, &sd, cancellable, error);
+ }
+
+ g_string_free (stmt, TRUE);
+
+ return success;
+}
+
+static gboolean
+ebc_search_internal (EBookCache *book_cache,
+ const gchar *sexp,
+ SearchType search_type,
+ gpointer out_value,
+ EBookCacheSearchFunc func,
+ gpointer func_user_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ PreflightContext context = PREFLIGHT_CONTEXT_INIT;
+ gboolean success = FALSE;
+
+ /* Now start with the query preflighting */
+ query_preflight (&context, book_cache, sexp);
+
+ switch (context.status) {
+ case PREFLIGHT_OK:
+ case PREFLIGHT_LIST_ALL:
+ case PREFLIGHT_NOT_SUMMARIZED:
+ /* No errors, let's really search */
+ success = ebc_do_search_query (
+ book_cache, &context, sexp,
+ search_type, out_value, func, func_user_data,
+ cancellable, error);
+ break;
+
+ case PREFLIGHT_INVALID:
+ g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY,
+ _("Invalid query: %s"), sexp);
+ break;
+
+ case PREFLIGHT_UNSUPPORTED:
+ g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_UNSUPPORTED_QUERY,
+ _("Query contained unsupported elements"));
+ break;
+ }
+
+ preflight_context_clear (&context);
+
+ return success;
+}
+
+/******************************************************************
+ * EBookCacheCursor Implementation *
+ ******************************************************************/
+typedef struct _CursorState CursorState;
+
+struct _CursorState {
+ gchar **values; /* The current cursor position, results will be returned after this
position */
+ gchar *last_uid; /* The current cursor contact UID position, used as a tie breaker */
+ EBookCacheCursorOrigin position;/* The position is updated with the cursor state and is used to
distinguish
+ * between the beginning and the ending of the cursor's contact list.
+ * While the cursor is in a non-null state, the position will be
+ * E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT.
+ */
+};
+
+struct _EBookCacheCursor {
+ EBookBackendSExp *sexp; /* An EBookBackendSExp based on the query, used by
e_book_sqlite_cursor_compare () */
+ gchar *select_vcards; /* The first fragment when querying results */
+ gchar *select_count; /* The first fragment when querying contact counts */
+ gchar *query; /* The SQL query expression derived from the passed search expression */
+ gchar *order; /* The normal order SQL query fragment to append at the end, containing
ORDER BY etc */
+ gchar *reverse_order; /* The reverse order SQL query fragment to append at the end,
containing ORDER BY etc */
+
+ EContactField *sort_fields; /* The fields to sort in a query in the order or sort priority */
+ EBookCursorSortType *sort_types; /* The sort method to use for each field */
+ gint n_sort_fields; /* The amound of sort fields */
+
+ CursorState state;
+};
+
+static CursorState *cursor_state_copy (EBookCacheCursor *cursor,
+ CursorState *state);
+static void cursor_state_free (EBookCacheCursor *cursor,
+ CursorState *state);
+static void cursor_state_clear (EBookCacheCursor *cursor,
+ CursorState *state,
+ EBookCacheCursorOrigin position);
+static void cursor_state_set_from_contact (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ CursorState *state,
+ EContact *contact);
+static void cursor_state_set_from_vcard (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ CursorState *state,
+ const gchar *vcard);
+
+static CursorState *
+cursor_state_copy (EBookCacheCursor *cursor,
+ CursorState *state)
+{
+ CursorState *copy;
+ gint ii;
+
+ copy = g_slice_new0 (CursorState);
+ copy->values = g_new0 (gchar *, cursor->n_sort_fields);
+
+ for (ii = 0; ii < cursor->n_sort_fields; ii++) {
+ copy->values[ii] = g_strdup (state->values[ii]);
+ }
+
+ copy->last_uid = g_strdup (state->last_uid);
+ copy->position = state->position;
+
+ return copy;
+}
+
+static void
+cursor_state_free (EBookCacheCursor *cursor,
+ CursorState *state)
+{
+ if (state) {
+ cursor_state_clear (cursor, state, E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN);
+ g_free (state->values);
+ g_slice_free (CursorState, state);
+ }
+}
+
+static void
+cursor_state_clear (EBookCacheCursor *cursor,
+ CursorState *state,
+ EBookCacheCursorOrigin position)
+{
+ gint ii;
+
+ for (ii = 0; ii < cursor->n_sort_fields; ii++) {
+ g_free (state->values[ii]);
+ state->values[ii] = NULL;
+ }
+
+ g_free (state->last_uid);
+ state->last_uid = NULL;
+ state->position = position;
+}
+
+static void
+cursor_state_set_from_contact (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ CursorState *state,
+ EContact *contact)
+{
+ gint ii;
+
+ cursor_state_clear (cursor, state, E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN);
+
+ for (ii = 0; ii < cursor->n_sort_fields; ii++) {
+ const gchar *string = e_contact_get_const (contact, cursor->sort_fields[ii]);
+ SummaryField *field;
+ gchar *sort_key;
+
+ if (string)
+ sort_key = e_collator_generate_key (book_cache->priv->collator, string, NULL);
+ else
+ sort_key = g_strdup ("");
+
+ field = summary_field_get (book_cache, cursor->sort_fields[ii]);
+
+ if (field && (field->index & INDEX_FLAG (SORT_KEY)) != 0) {
+ state->values[ii] = sort_key;
+ } else {
+ state->values[ii] = ebc_encode_vcard_sort_key (sort_key);
+ g_free (sort_key);
+ }
+ }
+
+ state->last_uid = e_contact_get (contact, E_CONTACT_UID);
+ state->position = E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT;
+}
+
+static void
+cursor_state_set_from_vcard (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ CursorState *state,
+ const gchar *vcard)
+{
+ EContact *contact;
+
+ contact = e_contact_new_from_vcard (vcard);
+ cursor_state_set_from_contact (book_cache, cursor, state, contact);
+ g_object_unref (contact);
+}
+
+static gboolean
+ebc_cursor_setup_query (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ const gchar *sexp,
+ GError **error)
+{
+ PreflightContext context = PREFLIGHT_CONTEXT_INIT;
+ GString *string, *where_clause;
+
+ /* Preflighting and error checking */
+ if (sexp) {
+ query_preflight (&context, book_cache, sexp);
+
+ if (context.status > PREFLIGHT_NOT_SUMMARIZED) {
+ g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY,
+ _("Invalid query for a book cursor"));
+
+ preflight_context_clear (&context);
+ return FALSE;
+ }
+ }
+
+ /* Now we caught the errors, let's generate our queries and get out of here ... */
+ g_free (cursor->select_vcards);
+ g_free (cursor->select_count);
+ g_free (cursor->query);
+ g_clear_object (&(cursor->sexp));
+
+ /* Generate the leading SELECT portions that we need */
+ string = g_string_new ("");
+ ebc_generate_select (book_cache, string, SEARCH_FULL, &context, NULL);
+ cursor->select_vcards = g_string_free (string, FALSE);
+
+ string = g_string_new ("");
+ ebc_generate_select (book_cache, string, SEARCH_COUNT, &context, NULL);
+ cursor->select_count = g_string_free (string, FALSE);
+
+ where_clause = g_string_new ("");
+
+ e_cache_sqlite_stmt_append_printf (where_clause, "summary." E_CACHE_COLUMN_STATE "!=%d",
+ E_OFFLINE_STATE_LOCALLY_DELETED);
+
+ if (!sexp || context.status == PREFLIGHT_LIST_ALL) {
+ cursor->sexp = NULL;
+ } else {
+ cursor->sexp = e_book_backend_sexp_new (sexp);
+
+ string = g_string_new (NULL);
+ ebc_generate_constraints (book_cache, string, context.constraints, sexp);
+ if (string->len)
+ e_cache_sqlite_stmt_append_printf (where_clause, " AND (%s)", string->str);
+ g_string_free (string, TRUE);
+ }
+
+ cursor->query = g_string_free (where_clause, FALSE);
+
+ preflight_context_clear (&context);
+
+ return TRUE;
+}
+
+static gchar *
+ebc_cursor_order_by_fragment (EBookCache *book_cache,
+ const EContactField *sort_fields,
+ const EBookCursorSortType *sort_types,
+ guint n_sort_fields,
+ gboolean reverse)
+{
+ GString *string;
+ gint ii;
+
+ string = g_string_new ("ORDER BY ");
+
+ for (ii = 0; ii < n_sort_fields; ii++) {
+ SummaryField *field = summary_field_get (book_cache, sort_fields[ii]);
+
+ if (ii > 0)
+ g_string_append (string, ", ");
+
+ if (field &&
+ (field->index & INDEX_FLAG (SORT_KEY)) != 0) {
+ g_string_append (string, "summary.");
+ g_string_append (string, field->dbname);
+ g_string_append (string, "_" EBC_SUFFIX_SORT_KEY " ");
+ } else {
+ g_string_append (string, "summary." E_CACHE_COLUMN_OBJECT);
+ g_string_append (string, " COLLATE ");
+ g_string_append (string, EBC_COLLATE_PREFIX);
+ g_string_append (string, e_contact_field_name (sort_fields[ii]));
+ g_string_append_c (string, ' ');
+ }
+
+ if (reverse)
+ g_string_append (string, (sort_types[ii] == E_BOOK_CURSOR_SORT_ASCENDING ? "DESC" :
"ASC"));
+ else
+ g_string_append (string, (sort_types[ii] == E_BOOK_CURSOR_SORT_ASCENDING ? "ASC" :
"DESC"));
+ }
+
+ /* Also order the UID, since it's our tie breaker */
+ if (n_sort_fields > 0)
+ g_string_append (string, ", ");
+
+ g_string_append (string, "summary." E_CACHE_COLUMN_UID " ");
+ g_string_append (string, reverse ? "DESC" : "ASC");
+
+ return g_string_free (string, FALSE);
+}
+
+static EBookCacheCursor *
+ebc_cursor_new (EBookCache *book_cache,
+ const gchar *sexp,
+ const EContactField *sort_fields,
+ const EBookCursorSortType *sort_types,
+ guint n_sort_fields)
+{
+ EBookCacheCursor *cursor = g_slice_new0 (EBookCacheCursor);
+
+ cursor->order = ebc_cursor_order_by_fragment (book_cache, sort_fields, sort_types, n_sort_fields,
FALSE);
+ cursor->reverse_order = ebc_cursor_order_by_fragment (book_cache, sort_fields, sort_types,
n_sort_fields, TRUE);
+
+ /* Sort parameters */
+ cursor->n_sort_fields = n_sort_fields;
+ cursor->sort_fields = g_memdup (sort_fields, sizeof (EContactField) * n_sort_fields);
+ cursor->sort_types = g_memdup (sort_types, sizeof (EBookCursorSortType) * n_sort_fields);
+
+ /* Cursor state */
+ cursor->state.values = g_new0 (gchar *, n_sort_fields);
+ cursor->state.last_uid = NULL;
+ cursor->state.position = E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN;
+
+ return cursor;
+}
+
+static void
+ebc_cursor_free (EBookCacheCursor *cursor)
+{
+ if (cursor) {
+ cursor_state_clear (cursor, &(cursor->state), E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN);
+ g_free (cursor->state.values);
+
+ g_clear_object (&(cursor->sexp));
+ g_free (cursor->select_vcards);
+ g_free (cursor->select_count);
+ g_free (cursor->query);
+ g_free (cursor->order);
+ g_free (cursor->reverse_order);
+ g_free (cursor->sort_fields);
+ g_free (cursor->sort_types);
+
+ g_slice_free (EBookCacheCursor, cursor);
+ }
+}
+
+#define GREATER_OR_LESS(cursor, idx, reverse) \
+ (reverse ? \
+ (((EBookCacheCursor *) cursor)->sort_types[idx] == E_BOOK_CURSOR_SORT_ASCENDING ? '<' : '>') : \
+ (((EBookCacheCursor *) cursor)->sort_types[idx] == E_BOOK_CURSOR_SORT_ASCENDING ? '>' : '<'))
+
+static inline void
+ebc_cursor_format_equality (EBookCache *book_cache,
+ GString *string,
+ EContactField field_id,
+ const gchar *value,
+ gchar equality)
+{
+ SummaryField *field = summary_field_get (book_cache, field_id);
+
+ if (field &&
+ (field->index & INDEX_FLAG (SORT_KEY)) != 0) {
+ g_string_append (string, "summary.");
+ g_string_append (string, field->dbname);
+ g_string_append (string, "_" EBC_SUFFIX_SORT_KEY " ");
+
+ e_cache_sqlite_stmt_append_printf (string, "%c %Q", equality, value);
+ } else {
+ e_cache_sqlite_stmt_append_printf (string, "(summary." E_CACHE_COLUMN_OBJECT " %c %Q ",
equality, value);
+
+ g_string_append (string, "COLLATE " EBC_COLLATE_PREFIX);
+ g_string_append (string, e_contact_field_name (field_id));
+ g_string_append_c (string, ')');
+ }
+}
+
+static gchar *
+ebc_cursor_constraints (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ CursorState *state,
+ gboolean reverse,
+ gboolean include_current_uid)
+{
+ GString *string;
+ gint ii, jj;
+
+ /* Example for:
+ * ORDER BY family_name ASC, given_name DESC
+ *
+ * Where current cursor values are:
+ * family_name = Jackson
+ * given_name = Micheal
+ *
+ * With reverse = FALSE
+ *
+ * (summary.family_name > 'Jackson') OR
+ * (summary.family_name = 'Jackson' AND summary.given_name < 'Micheal') OR
+ * (summary.family_name = 'Jackson' AND summary.given_name = 'Micheal' AND summary.uid >
'last-uid')
+ *
+ * With reverse = TRUE (needed for moving the cursor backwards through results)
+ *
+ * (summary.family_name < 'Jackson') OR
+ * (summary.family_name = 'Jackson' AND summary.given_name > 'Micheal') OR
+ * (summary.family_name = 'Jackson' AND summary.given_name = 'Micheal' AND summary.uid <
'last-uid')
+ *
+ */
+ string = g_string_new (NULL);
+
+ for (ii = 0; ii <= cursor->n_sort_fields; ii++) {
+ /* Break once we hit a NULL value */
+ if ((ii < cursor->n_sort_fields && state->values[ii] == NULL) ||
+ (ii == cursor->n_sort_fields && state->last_uid == NULL))
+ break;
+
+ /* Between each qualifier, add an 'OR' */
+ if (ii > 0)
+ g_string_append (string, " OR ");
+
+ /* Begin qualifier */
+ g_string_append_c (string, '(');
+
+ /* Create the '=' statements leading up to the current tie breaker */
+ for (jj = 0; jj < ii; jj++) {
+ ebc_cursor_format_equality (book_cache, string,
+ cursor->sort_fields[jj],
+ state->values[jj], '=');
+ g_string_append (string, " AND ");
+ }
+
+ if (ii == cursor->n_sort_fields) {
+ /* The 'include_current_uid' clause is used for calculating
+ * the current position of the cursor, inclusive of the
+ * current position.
+ */
+ if (include_current_uid)
+ g_string_append_c (string, '(');
+
+ /* Append the UID tie breaker */
+ e_cache_sqlite_stmt_append_printf (
+ string,
+ "summary." E_CACHE_COLUMN_UID " %c %Q",
+ reverse ? '<' : '>',
+ state->last_uid);
+
+ if (include_current_uid)
+ e_cache_sqlite_stmt_append_printf (
+ string,
+ " OR summary." E_CACHE_COLUMN_UID " = %Q)",
+ state->last_uid);
+ } else {
+ /* SPECIAL CASE: If we have a parially set cursor state, then we must
+ * report next results that are inclusive of the final qualifier.
+ *
+ * This allows one to set the cursor with the family name set to 'J'
+ * and include the results for contact's Mr & Miss 'J'.
+ */
+ gboolean include_exact_match =
+ (reverse == FALSE &&
+ ((ii + 1 < cursor->n_sort_fields && state->values[ii + 1] == NULL) ||
+ (ii + 1 == cursor->n_sort_fields && state->last_uid == NULL)));
+
+ if (include_exact_match)
+ g_string_append_c (string, '(');
+
+ /* Append the final qualifier for this field */
+ ebc_cursor_format_equality (book_cache, string,
+ cursor->sort_fields[ii],
+ state->values[ii],
+ GREATER_OR_LESS (cursor, ii, reverse));
+
+ if (include_exact_match) {
+ g_string_append (string, " OR ");
+ ebc_cursor_format_equality (book_cache, string,
+ cursor->sort_fields[ii],
+ state->values[ii], '=');
+ g_string_append_c (string, ')');
+ }
+ }
+
+ /* End qualifier */
+ g_string_append_c (string, ')');
+ }
+
+ return g_string_free (string, FALSE);
+}
+
+static gboolean
+ebc_get_int_cb (ECache *cache,
+ gint ncols,
+ const gchar **column_names,
+ const gchar **column_values,
+ gpointer user_data)
+{
+ gint *pint = user_data;
+
+ g_return_val_if_fail (pint != NULL, FALSE);
+
+ if (ncols == 1) {
+ *pint = column_values[0] ? g_ascii_strtoll (column_values[0], NULL, 10) : 0;
+ } else {
+ *pint = 0;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+cursor_count_total_locked (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ gint *out_total,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GString *query;
+ gboolean success;
+
+ query = g_string_new (cursor->select_count);
+
+ /* Add the filter constraints (if any) */
+ if (cursor->query) {
+ g_string_append (query, " WHERE ");
+
+ g_string_append_c (query, '(');
+ g_string_append (query, cursor->query);
+ g_string_append_c (query, ')');
+ }
+
+ /* Execute the query */
+ success = e_cache_sqlite_select (E_CACHE (book_cache), query->str, ebc_get_int_cb, out_total,
cancellable, error);
+
+ g_string_free (query, TRUE);
+
+ return success;
+}
+
+static gboolean
+cursor_count_position_locked (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ gint *out_position,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GString *query;
+ gboolean success;
+
+ query = g_string_new (cursor->select_count);
+
+ /* Add the filter constraints (if any) */
+ if (cursor->query) {
+ g_string_append (query, " WHERE ");
+
+ g_string_append_c (query, '(');
+ g_string_append (query, cursor->query);
+ g_string_append_c (query, ')');
+ }
+
+ /* Add the cursor constraints (if any) */
+ if (cursor->state.values[0] != NULL) {
+ gchar *constraints = NULL;
+
+ if (!cursor->query)
+ g_string_append (query, " WHERE ");
+ else
+ g_string_append (query, " AND ");
+
+ /* Here we do a reverse query, we're looking for all the
+ * results leading up to the current cursor value, including
+ * the cursor value
+ */
+ constraints = ebc_cursor_constraints (book_cache, cursor, &(cursor->state), TRUE, TRUE);
+
+ g_string_append_c (query, '(');
+ g_string_append (query, constraints);
+ g_string_append_c (query, ')');
+
+ g_free (constraints);
+ }
+
+ /* Execute the query */
+ success = e_cache_sqlite_select (E_CACHE (book_cache), query->str, ebc_get_int_cb, out_position,
cancellable, error);
+
+ g_string_free (query, TRUE);
+
+ return success;
+}
+
+typedef struct {
+ gint country_code;
+ gchar *national;
+} E164Number;
+
+static E164Number *
+ebc_e164_number_new (gint country_code,
+ const gchar *national)
+{
+ E164Number *number = g_slice_new (E164Number);
+
+ number->country_code = country_code;
+ number->national = g_strdup (national);
+
+ return number;
+}
+
+static void
+ebc_e164_number_free (E164Number *number)
+{
+ if (number) {
+ g_free (number->national);
+ g_slice_free (E164Number, number);
+ }
+}
+
+static gint
+ebc_e164_number_find (E164Number *number_a,
+ E164Number *number_b)
+{
+ gint ret;
+
+ ret = number_a->country_code - number_b->country_code;
+
+ if (ret == 0) {
+ ret = g_strcmp0 (
+ number_a->national,
+ number_b->national);
+ }
+
+ return ret;
+}
+
+static GList *
+extract_e164_attribute_params (EContact *contact)
+{
+ EVCard *vcard = E_VCARD (contact);
+ GList *extracted = NULL;
+ GList *attr_list;
+
+ for (attr_list = e_vcard_get_attributes (vcard); attr_list; attr_list = attr_list->next) {
+ EVCardAttribute *const attr = attr_list->data;
+ EVCardAttributeParam *param = NULL;
+ GList *param_list, *values, *l;
+ gchar *this_national = NULL;
+ gint this_country = 0;
+
+ /* We only attach E164 parameters to TEL attributes. */
+ if (strcmp (e_vcard_attribute_get_name (attr), EVC_TEL) != 0)
+ continue;
+
+ /* Find already exisiting parameter, so that we can reuse it. */
+ for (param_list = e_vcard_attribute_get_params (attr); param_list; param_list =
param_list->next) {
+ if (strcmp (e_vcard_attribute_param_get_name (param_list->data), EVC_X_E164) == 0) {
+ param = param_list->data;
+ break;
+ }
+ }
+
+ if (!param)
+ continue;
+
+ values = e_vcard_attribute_param_get_values (param);
+ for (l = values; l; l = l->next) {
+ const gchar *value = l->data;
+
+ if (value[0] == '+')
+ this_country = g_ascii_strtoll (&value[1], NULL, 10);
+ else if (this_national == NULL)
+ this_national = g_strdup (value);
+ }
+
+ if (this_national) {
+ E164Number *number;
+
+ number = ebc_e164_number_new (this_country, this_national);
+ extracted = g_list_prepend (extracted, number);
+ }
+
+ g_free (this_national);
+
+ /* Clear the values, we'll insert new ones */
+ e_vcard_attribute_param_remove_values (param);
+ e_vcard_attribute_remove_param (attr, EVC_X_E164);
+ }
+
+ return extracted;
+}
+
+static gboolean
+update_e164_attribute_params (EBookCache *book_cache,
+ EContact *contact,
+ const gchar *default_region)
+{
+ GList *original_numbers = NULL;
+ GList *attr_list;
+ gboolean changed = FALSE;
+ gint n_numbers = 0;
+ EVCard *vcard = E_VCARD (contact);
+
+ original_numbers = extract_e164_attribute_params (contact);
+
+ for (attr_list = e_vcard_get_attributes (vcard); attr_list; attr_list = attr_list->next) {
+ EVCardAttribute *const attr = attr_list->data;
+ EVCardAttributeParam *param = NULL;
+ const gchar *original_number = NULL;
+ gchar *country_string;
+ GList *values;
+ E164Number number = { 0, NULL };
+
+ /* We only attach E164 parameters to TEL attributes. */
+ if (strcmp (e_vcard_attribute_get_name (attr), EVC_TEL) != 0)
+ continue;
+
+ /* Fetch the TEL value */
+ values = e_vcard_attribute_get_values (attr);
+
+ /* Compute E164 number based on the TEL value */
+ if (values && values->data) {
+ original_number = (const gchar *) values->data;
+ number.national = convert_phone (original_number, book_cache->priv->region_code,
&(number.country_code));
+ }
+
+ if (number.national == NULL)
+ continue;
+
+ /* Count how many we successfully parsed in this region code */
+ n_numbers++;
+
+ /* Check if we have a differing e164 number, if there is no match
+ * in the old existing values then the vcard changed
+ */
+ if (!g_list_find_custom (original_numbers, &number, (GCompareFunc) ebc_e164_number_find))
+ changed = TRUE;
+
+ if (number.country_code != 0)
+ country_string = g_strdup_printf ("+%d", number.country_code);
+ else
+ country_string = g_strdup ("");
+
+ param = e_vcard_attribute_param_new (EVC_X_E164);
+ e_vcard_attribute_add_param (attr, param);
+
+ /* Assign the parameter values. It seems odd that we revert
+ * the order of NN and CC, but at least EVCard's parser doesn't
+ * permit an empty first param value. Which of course could be
+ * fixed - in order to create a nice potential IOP problem with
+ ** other vCard parsers. */
+ e_vcard_attribute_param_add_values (param, number.national, country_string, NULL);
+
+ g_free (number.national);
+ g_free (country_string);
+ }
+
+ if (!changed && n_numbers != g_list_length (original_numbers))
+ changed = TRUE;
+
+ g_list_free_full (original_numbers, (GDestroyNotify) ebc_e164_number_free);
+
+ return changed;
+}
+
+static gboolean
+e_book_cache_get_string (ECache *cache,
+ gint ncols,
+ const gchar **column_names,
+ const gchar **column_values,
+ gpointer user_data)
+{
+ gchar **pvalue = user_data;
+
+ g_return_val_if_fail (ncols == 1, FALSE);
+ g_return_val_if_fail (column_names != NULL, FALSE);
+ g_return_val_if_fail (column_values != NULL, FALSE);
+ g_return_val_if_fail (pvalue != NULL, FALSE);
+
+ if (!*pvalue)
+ *pvalue = g_strdup (column_values[0]);
+
+ return TRUE;
+}
+
+static gboolean
+e_book_cache_get_old_contacts_cb (ECache *cache,
+ gint ncols,
+ const gchar *column_names[],
+ const gchar *column_values[],
+ gpointer user_data)
+{
+ GSList **pold_contacts = user_data;
+
+ g_return_val_if_fail (pold_contacts != NULL, FALSE);
+ g_return_val_if_fail (ncols == 3, FALSE);
+
+ if (column_values[0] && column_values[1]) {
+ *pold_contacts = g_slist_prepend (*pold_contacts,
+ e_book_cache_search_data_new (column_values[0], column_values[1], column_values[2]));
+ }
+
+ return TRUE;
+}
+
+static gboolean
+e_book_cache_gather_table_names_cb (ECache *cache,
+ gint ncols,
+ const gchar *column_names[],
+ const gchar *column_values[],
+ gpointer user_data)
+{
+ GSList **ptables = user_data;
+
+ g_return_val_if_fail (ptables != NULL, FALSE);
+ g_return_val_if_fail (ncols == 1, FALSE);
+
+ *ptables = g_slist_prepend (*ptables, g_strdup (column_values[0]));
+
+ return TRUE;
+}
+
+static gboolean
+e_book_cache_migrate (ECache *cache,
+ gint from_version,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookCache *book_cache = E_BOOK_CACHE (cache);
+ gboolean success = TRUE;
+
+ /* Migration from EBookSqlite database */
+ if (from_version <= 0) {
+ GSList *tables = NULL, *old_contacts = NULL, *link;
+
+ if (e_cache_sqlite_select (cache, "SELECT uid,vcard,bdata FROM folder_id ORDER BY uid",
+ e_book_cache_get_old_contacts_cb, &old_contacts, cancellable, NULL)) {
+
+ old_contacts = g_slist_reverse (old_contacts);
+
+ for (link = old_contacts; link && success; link = g_slist_next (link)) {
+ EBookCacheSearchData *data = link->data;
+ EContact *contact;
+
+ if (!data)
+ continue;
+
+ contact = e_contact_new_from_vcard_with_uid (data->vcard, data->uid);
+ if (!contact)
+ continue;
+
+ success = e_book_cache_put_contact (book_cache, contact, data->extra,
E_CACHE_IS_ONLINE, cancellable, error);
+ }
+ }
+
+ /* Delete obsolete tables */
+ success = success && e_cache_sqlite_select (cache,
+ "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'folder_id%'",
+ e_book_cache_gather_table_names_cb, &tables, cancellable, error);
+
+ for (link = tables; link && success; link = g_slist_next (link)) {
+ const gchar *name = link->data;
+ gchar *stmt;
+
+ if (!link)
+ continue;
+
+ stmt = e_cache_sqlite_stmt_printf ("DROP TABLE IF EXISTS %Q", name);
+ success = e_cache_sqlite_exec (cache, stmt, cancellable, error);
+ e_cache_sqlite_stmt_free (stmt);
+ }
+
+ g_slist_free_full (tables, g_free);
+
+ success = success && e_cache_sqlite_exec (cache, "DROP TABLE IF EXISTS keys", cancellable,
error);
+ success = success && e_cache_sqlite_exec (cache, "DROP TABLE IF EXISTS folders", cancellable,
error);
+ success = success && e_cache_sqlite_exec (cache, "DROP TABLE IF EXISTS folder_id",
cancellable, error);
+
+ if (success) {
+ /* Save the changes by finishing the transaction */
+ e_cache_unlock (cache, E_CACHE_UNLOCK_COMMIT);
+ e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+
+ /* Try to vacuum, but do not claim any error if failed */
+ e_cache_sqlite_maybe_vacuum (cache, cancellable, NULL);
+ }
+
+ g_slist_free_full (old_contacts, e_book_cache_search_data_free);
+ }
+
+ /* Add any version-related changes here */
+ /*if (from_version < E_BOOK_CACHE_VERSION) {
+ }*/
+
+ return success;
+}
+
+static gboolean
+e_book_cache_populate_other_columns (EBookCache *book_cache,
+ ESourceBackendSummarySetup *setup,
+ GSList **out_columns, /* ECacheColumnInfo * */
+ GError **error)
+{
+ GSList *columns = NULL;
+ gboolean use_default;
+ gboolean success = TRUE;
+ gint ii;
+
+ g_return_val_if_fail (out_columns != NULL, FALSE);
+
+ #define add_column(_name, _type, _index_name) G_STMT_START { \
+ columns = g_slist_prepend (columns, e_cache_column_info_new (_name, _type, _index_name)); \
+ } G_STMT_END
+
+ add_column (EBC_COLUMN_EXTRA, "TEXT", NULL);
+
+ use_default = !setup;
+
+ if (setup) {
+ EContactField *fields;
+ EContactField *indexed_fields;
+ EBookIndexType *index_types = NULL;
+ gint n_fields = 0, n_indexed_fields = 0, ii;
+
+ fields = e_source_backend_summary_setup_get_summary_fields (setup, &n_fields);
+ indexed_fields = e_source_backend_summary_setup_get_indexed_fields (setup, &index_types,
&n_indexed_fields);
+
+ if (n_fields <= 0 || n_fields >= EBC_MAX_SUMMARY_FIELDS) {
+ if (n_fields)
+ g_warning ("EBookCache refused to create cache with more than %d summary
fields", EBC_MAX_SUMMARY_FIELDS);
+ use_default = TRUE;
+ } else {
+ GArray *summary_fields;
+
+ summary_fields = g_array_new (FALSE, FALSE, sizeof (SummaryField));
+
+ /* Ensure the non-optional fields first */
+ summary_field_append (summary_fields, E_CONTACT_UID, error);
+ summary_field_append (summary_fields, E_CONTACT_REV, error);
+
+ for (ii = 0; ii < n_fields; ii++) {
+ if (!summary_field_append (summary_fields, fields[ii], error)) {
+ success = FALSE;
+ break;
+ }
+ }
+
+ if (!success) {
+ gint n_sfields;
+ SummaryField *sfields;
+
+ /* Properly free the array */
+ n_sfields = summary_fields->len;
+ sfields = (SummaryField *) g_array_free (summary_fields, FALSE);
+ summary_fields_array_free (sfields, n_sfields);
+
+ g_free (fields);
+ g_free (index_types);
+ g_free (indexed_fields);
+
+ g_slist_free_full (columns, e_cache_column_info_free);
+
+ return FALSE;
+ }
+
+ /* Add the 'indexed' flag to the SummaryField structs */
+ summary_fields_add_indexes (summary_fields, indexed_fields, index_types,
n_indexed_fields);
+
+ book_cache->priv->n_summary_fields = summary_fields->len;
+ book_cache->priv->summary_fields = (SummaryField *) g_array_free (summary_fields,
FALSE);
+ }
+
+ g_free (fields);
+ g_free (index_types);
+ g_free (indexed_fields);
+ }
+
+ if (use_default) {
+ GArray *summary_fields;
+
+ g_warn_if_fail (book_cache->priv->n_summary_fields == 0);
+
+ /* Create the default summary structs */
+ summary_fields = g_array_new (FALSE, FALSE, sizeof (SummaryField));
+ for (ii = 0; ii < G_N_ELEMENTS (default_summary_fields); ii++) {
+ summary_field_append (summary_fields, default_summary_fields[ii], NULL);
+ }
+
+ /* Add the default index flags */
+ summary_fields_add_indexes (
+ summary_fields,
+ default_indexed_fields,
+ default_index_types,
+ G_N_ELEMENTS (default_indexed_fields));
+
+ book_cache->priv->n_summary_fields = summary_fields->len;
+ book_cache->priv->summary_fields = (SummaryField *) g_array_free (summary_fields, FALSE);
+ }
+
+ #undef add_column
+
+ if (success) {
+ for (ii = 0; ii < book_cache->priv->n_summary_fields; ii++) {
+ SummaryField *fld = &(book_cache->priv->summary_fields[ii]);
+
+ summary_field_init_dbnames (fld);
+
+ if (fld->type != E_TYPE_CONTACT_ATTR_LIST)
+ summary_field_prepend_columns (fld, &columns);
+ }
+ }
+
+ *out_columns = columns;
+
+ return success;
+}
+
+static gboolean
+e_book_cache_initialize (EBookCache *book_cache,
+ const gchar *filename,
+ ESource *source,
+ ESourceBackendSummarySetup *setup,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECache *cache;
+ GSList *other_columns = NULL;
+ sqlite3 *db;
+ gint ii, sqret;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (filename != NULL, FALSE);
+ if (source)
+ g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
+ if (setup)
+ g_return_val_if_fail (E_IS_SOURCE_BACKEND_SUMMARY_SETUP (setup), FALSE);
+
+ if (source)
+ book_cache->priv->source = g_object_ref (source);
+
+ cache = E_CACHE (book_cache);
+
+ success = e_book_cache_populate_other_columns (book_cache, setup, &other_columns, error);
+ if (!success)
+ goto exit;
+
+ success = e_cache_initialize_sync (cache, filename, other_columns, cancellable, error);
+ if (!success)
+ goto exit;
+
+ e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+
+ db = e_cache_get_sqlitedb (cache);
+ sqret = SQLITE_OK;
+
+ /* Install our custom functions */
+ for (ii = 0; sqret == SQLITE_OK && ii < G_N_ELEMENTS (ebc_custom_functions); ii++) {
+ sqret = sqlite3_create_function (
+ db,
+ ebc_custom_functions[ii].name,
+ ebc_custom_functions[ii].arguments,
+ SQLITE_UTF8, book_cache,
+ ebc_custom_functions[ii].func,
+ NULL, NULL);
+ }
+
+ /* Fallback COLLATE implementations generated on demand */
+ if (sqret == SQLITE_OK)
+ sqret = sqlite3_collation_needed (db, book_cache, ebc_generate_collator);
+
+ if (sqret != SQLITE_OK) {
+ if (!db) {
+ g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_LOAD, _("Insufficient
memory"));
+ } else {
+ const gchar *errmsg = sqlite3_errmsg (db);
+
+ g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_ENGINE, _("Can't open database %s:
%s"), filename, errmsg);
+ }
+
+ success = FALSE;
+ }
+
+ success = success && ebc_init_locale (book_cache, cancellable, error);
+
+ success = success && ebc_init_aux_tables (book_cache, cancellable, error);
+
+ /* Check for data migration */
+ success = success && e_book_cache_migrate (cache, e_cache_get_version (cache), cancellable, error);
+
+ e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+ if (!success)
+ goto exit;
+
+ if (e_cache_get_version (cache) != E_BOOK_CACHE_VERSION)
+ e_cache_set_version (cache, E_BOOK_CACHE_VERSION);
+
+ exit:
+ g_slist_free_full (other_columns, e_cache_column_info_free);
+
+ return success;
+}
+
+/**
+ * e_book_cache_new:
+ * @filename: file name to load or create the new cache
+ * @source: (nullable): an optional #ESource, associated with the #EBookCache, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new #EBookCache with the default summary configuration.
+ *
+ * Aside from the mandatory fields %E_CONTACT_UID, %E_CONTACT_REV,
+ * the default configuration stores the following fields for quick
+ * performance of searches: %E_CONTACT_FILE_AS, %E_CONTACT_NICKNAME,
+ * %E_CONTACT_FULL_NAME, %E_CONTACT_GIVEN_NAME, %E_CONTACT_FAMILY_NAME,
+ * %E_CONTACT_EMAIL, %E_CONTACT_TEL, %E_CONTACT_IS_LIST, %E_CONTACT_LIST_SHOW_ADDRESSES,
+ * and %E_CONTACT_WANTS_HTML.
+ *
+ * The fields %E_CONTACT_FULL_NAME and %E_CONTACT_EMAIL are configured
+ * to respond extra quickly with the %E_BOOK_INDEX_PREFIX index flag.
+ *
+ * The fields %E_CONTACT_FILE_AS, %E_CONTACT_FAMILY_NAME and
+ * %E_CONTACT_GIVEN_NAME are configured to perform well with
+ * the #EBookCacheCursor, using the %E_BOOK_INDEX_SORT_KEY
+ * index flag.
+ *
+ * Returns: (transfer full) (nullable): A new #EBookCache or %NULL on error
+ *
+ * Since: 3.26
+ **/
+EBookCache *
+e_book_cache_new (const gchar *filename,
+ ESource *source,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_return_val_if_fail (filename != NULL, NULL);
+
+ return e_book_cache_new_full (filename, source, NULL, cancellable, error);
+}
+
+/**
+ * e_book_cache_new_full:
+ * @filename: file name to load or create the new cache
+ * @source: (nullable): an optional #ESource, associated with the #EBookCache, or %NULL
+ * @setup: (nullable): an #ESourceBackendSummarySetup describing how the summary should be setup, or %NULL
to use the default
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new #EBookCache with the given or the default summary configuration.
+ *
+ * Like e_book_sqlite_new(), but allows configuration of which contact fields
+ * will be stored for quick reference in the summary. The configuration indicated by
+ * @setup will only be taken into account when initially creating the underlying table,
+ * further configurations will be ignored.
+ *
+ * The fields %E_CONTACT_UID and %E_CONTACT_REV are not optional,
+ * they will be stored in the summary regardless of this function's parameters.
+ * Only #EContactFields with the type %G_TYPE_STRING, %G_TYPE_BOOLEAN or
+ * %E_TYPE_CONTACT_ATTR_LIST are currently supported.
+ *
+ * Returns: (transfer full) (nullable): A new #EBookCache or %NULL on error
+ *
+ * Since: 3.26
+ **/
+EBookCache *
+e_book_cache_new_full (const gchar *filename,
+ ESource *source,
+ ESourceBackendSummarySetup *setup,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookCache *book_cache;
+
+ g_return_val_if_fail (filename != NULL, NULL);
+
+ book_cache = g_object_new (E_TYPE_BOOK_CACHE, NULL);
+
+ if (!e_book_cache_initialize (book_cache, filename, source, setup, cancellable, error)) {
+ g_object_unref (book_cache);
+ book_cache = NULL;
+ }
+
+ return book_cache;
+}
+
+/**
+ * e_book_cache_ref_source:
+ * @book_cache: An #EBookCache
+ *
+ * References the #ESource to which @book_cache is paired,
+ * use g_object_unref() when no longer needed.
+ * It can be %NULL in some cases, like when running tests.
+ *
+ * Returns: (transfer full): A reference to the #ESource to which @book_cache
+ * is paired, or %NULL.
+ *
+ * Since: 3.26
+ **/
+ESource *
+e_book_cache_ref_source (EBookCache *book_cache)
+{
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), NULL);
+
+ if (book_cache->priv->source)
+ return g_object_ref (book_cache->priv->source);
+
+ return NULL;
+}
+
+/**
+ * e_book_cache_dup_contact_revision:
+ * @book_cache: an #EBookCache
+ * @contact: an #EContact
+ *
+ * Returns the @contact revision, used to detect changes.
+ * The returned string should be freed with g_free(), when
+ * no longer needed.
+ *
+ * Returns: (transfer full): A newly allocated string containing
+ * revision of the @contact.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_book_cache_dup_contact_revision (EBookCache *book_cache,
+ EContact *contact)
+{
+ gchar *revision = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), NULL);
+ g_return_val_if_fail (E_IS_CONTACT (contact), NULL);
+
+ g_signal_emit (book_cache, signals[DUP_CONTACT_REVISION], 0, contact, &revision);
+
+ return revision;
+}
+
+/**
+ * e_book_cache_set_locale:
+ * @book_cache: An #EBookCache
+ * @lc_collate: The new locale for the cache
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Relocalizes any locale specific data in the specified
+ * new @lc_collate locale.
+ *
+ * The @lc_collate locale setting is stored and remembered on
+ * subsequent accesses of the cache, changing the locale will
+ * store the new locale and will modify sort keys and any
+ * locale specific data in the cache.
+ *
+ * As a side effect, it's possible that changing the locale
+ * will cause stored vCard-s to change.
+ *
+ * Returns: Whether the new locale was successfully set.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_set_locale (EBookCache *book_cache,
+ const gchar *lc_collate,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECache *cache;
+ gboolean success, changed = FALSE;
+ gchar *stored_lc_collate = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+
+ cache = E_CACHE (book_cache);
+
+ e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+
+ success = ebc_set_locale_internal (book_cache, lc_collate, error);
+
+ if (success)
+ stored_lc_collate = e_cache_dup_key (cache, EBC_KEY_LC_COLLATE, NULL);
+
+ if (success && g_strcmp0 (stored_lc_collate, lc_collate) != 0)
+ success = ebc_upgrade (book_cache, cancellable, error);
+
+ /* If for some reason we failed, then reset the collator to use the old locale */
+ if (!success && stored_lc_collate && stored_lc_collate[0]) {
+ ebc_set_locale_internal (book_cache, stored_lc_collate, NULL);
+ changed = TRUE;
+ }
+
+ e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+ g_free (stored_lc_collate);
+
+ if (success && changed)
+ g_object_notify (G_OBJECT (book_cache), "locale");
+
+ return success;
+}
+
+/**
+ * e_book_cache_dup_locale:
+ * @book_cache: An #EBookCache
+ *
+ * Returns: (transfer full): A new string containing the current local
+ * being used by the @book_cache. Free it with g_free(), when no
+ * longer needed.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_book_cache_dup_locale (EBookCache *book_cache)
+{
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), NULL);
+
+ return g_strdup (book_cache->priv->locale);
+}
+
+/**
+ * e_book_cache_ref_collator:
+ * @book_cache: An #EBookCache
+ *
+ * References the currently active #ECollator for @book_cache,
+ * use e_collator_unref() when finished using the returned collator.
+ *
+ * Note that the active collator will change with the active locale setting.
+ *
+ * Returns: (transfer full): A reference to the active collator.
+ *
+ * Since: 3.26
+ **/
+ECollator *
+e_book_cache_ref_collator (EBookCache *book_cache)
+{
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), NULL);
+
+ return e_collator_ref (book_cache->priv->collator);
+}
+
+/**
+ * e_book_cache_put_contact:
+ * @book_cache: An #EBookCache
+ * @contact: an #EContact to be added
+ * @extra: extra data to store in association with this contact
+ * @offline_flag: one of #ECacheOfflineFlag, whether putting this contact in offline
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * This is a convenience wrapper for e_book_cache_put_contacts(),
+ * which is the preferred way to add or modify multiple contacts when possible.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_put_contact (EBookCache *book_cache,
+ EContact *contact,
+ const gchar *extra,
+ ECacheOfflineFlag offline_flag,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GSList *contacts, *extras;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+
+ contacts = g_slist_append (NULL, contact);
+ extras = g_slist_append (NULL, (gpointer) extra);
+
+ success = e_book_cache_put_contacts (book_cache, contacts, extras, offline_flag, cancellable, error);
+
+ g_slist_free (contacts);
+ g_slist_free (extras);
+
+ return success;
+}
+
+/**
+ * e_book_cache_put_contacts:
+ * @book_cache: An #EBookCache
+ * @contacts: (element-type EContact): A list of contacts to add to @book_cache
+ * @extras: (nullable) (element-type utf8): A list of extra data to store in association with the @contacts
+ * @offline_flag: one of #ECacheOfflineFlag, whether putting these contacts in offline
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Adds or replaces contacts in @book_cache.
+ *
+ * If @extras is specified, it must have an equal length as the @contacts list. Each element
+ * from the @extras list will be stored in association with its corresponding contact
+ * in the @contacts list.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_put_contacts (EBookCache *book_cache,
+ const GSList *contacts,
+ const GSList *extras,
+ ECacheOfflineFlag offline_flag,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const GSList *clink, *elink;
+ ECache *cache;
+ ECacheColumnValues *other_columns;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (contacts != NULL, FALSE);
+ g_return_val_if_fail (extras == NULL || g_slist_length ((GSList *) extras) == g_slist_length ((GSList
*) contacts), FALSE);
+
+ cache = E_CACHE (book_cache);
+ other_columns = e_cache_column_values_new ();
+
+ e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+ e_cache_freeze_revision_change (cache);
+
+ for (clink = contacts, elink = extras; clink; clink = g_slist_next (clink), elink = g_slist_next
(elink)) {
+ EContact *contact = clink->data;
+ const gchar *extra = elink ? elink->data : NULL;
+ gchar *uid, *rev, *vcard;
+
+ g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+
+ vcard = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
+ g_return_val_if_fail (vcard != NULL, FALSE);
+
+ e_cache_column_values_remove_all (other_columns);
+
+ if (extra)
+ e_cache_column_values_take_value (other_columns, EBC_COLUMN_EXTRA, g_strdup (extra));
+
+ uid = e_contact_get (contact, E_CONTACT_UID);
+ rev = e_book_cache_dup_contact_revision (book_cache, contact);
+
+ ebc_fill_other_columns (book_cache, contact, other_columns);
+
+ success = e_cache_put (cache, uid, rev, vcard, other_columns, offline_flag, cancellable,
error);
+
+ g_free (vcard);
+ g_free (rev);
+ g_free (uid);
+
+ if (!success)
+ break;
+ }
+
+ e_cache_thaw_revision_change (cache);
+ e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+ e_cache_column_values_free (other_columns);
+
+ return success;
+}
+
+/**
+ * e_book_cache_remove_contact:
+ * @book_cache: An #EBookCache
+ * @uid: the uid of the contact to remove
+ * @offline_flag: one of #ECacheOfflineFlag, whether removing this contact in offline
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Removes the contact identified by @uid from @book_cache.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_remove_contact (EBookCache *book_cache,
+ const gchar *uid,
+ ECacheOfflineFlag offline_flag,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GSList *uids;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ uids = g_slist_append (NULL, (gpointer) uid);
+
+ success = e_book_cache_remove_contacts (book_cache, uids, offline_flag, cancellable, error);
+
+ g_slist_free (uids);
+
+ return success;
+}
+
+/**
+ * e_book_cache_remove_contacts:
+ * @book_cache: An #EBookCache
+ * @uids: (element-type utf8): a #GSList of uids indicating which contacts to remove
+ * @offline_flag: one of #ECacheOfflineFlag, whether removing these contacts in offline
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Removes the contacts indicated by @uids from @book_cache.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_remove_contacts (EBookCache *book_cache,
+ const GSList *uids,
+ ECacheOfflineFlag offline_flag,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECache *cache;
+ const GSList *link;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (uids != NULL, FALSE);
+
+ cache = E_CACHE (book_cache);
+
+ e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+ e_cache_freeze_revision_change (cache);
+
+ for (link = uids; success && link; link = g_slist_next (link)) {
+ const gchar *uid = link->data;
+
+ success = e_cache_remove (cache, uid, offline_flag, cancellable, error);
+ }
+
+ e_cache_thaw_revision_change (cache);
+ e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+ return success;
+}
+
+/**
+ * e_book_cache_get_contact:
+ * @book_cache: An #EBookCache
+ * @uid: The uid of the contact to fetch
+ * @meta_contact: Whether an entire contact is desired, or only the metadata
+ * @out_contact: (out) (transfer full): Return location to store the fetched contact
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Fetch the #EContact specified by @uid in @book_cache.
+ *
+ * If @meta_contact is specified, then a shallow #EContact will be created
+ * holding only the %E_CONTACT_UID and %E_CONTACT_REV fields.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_get_contact (EBookCache *book_cache,
+ const gchar *uid,
+ gboolean meta_contact,
+ EContact **out_contact,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gchar *vcard = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (out_contact != NULL, FALSE);
+
+ *out_contact = NULL;
+
+ if (!e_book_cache_get_vcard (book_cache, uid, meta_contact, &vcard, cancellable, error) ||
+ !vcard) {
+ return FALSE;
+ }
+
+ *out_contact = e_contact_new_from_vcard_with_uid (vcard, uid);
+
+ g_free (vcard);
+
+ return TRUE;
+}
+
+/**
+ * e_book_cache_get_vcard:
+ * @book_cache: An #EBookCache
+ * @uid: The uid of the contact to fetch
+ * @meta_contact: Whether an entire contact is desired, or only the metadata
+ * @out_vcard: (out) (transfer full): Return location to store the fetched vCard string
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Fetch a vCard string for @uid in @book_cache.
+ *
+ * If @meta_contact is specified, then a shallow vCard representation will be
+ * created holding only the %E_CONTACT_UID and %E_CONTACT_REV fields.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_get_vcard (EBookCache *book_cache,
+ const gchar *uid,
+ gboolean meta_contact,
+ gchar **out_vcard,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gchar *full_vcard, *revision = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (out_vcard != NULL, FALSE);
+
+ *out_vcard = NULL;
+
+ full_vcard = e_cache_get (E_CACHE (book_cache), uid,
+ meta_contact ? &revision : NULL,
+ NULL, cancellable, error);
+
+ if (!full_vcard) {
+ g_warn_if_fail (revision == NULL);
+ return FALSE;
+ }
+
+ if (meta_contact) {
+ EContact *contact = e_contact_new ();
+
+ e_contact_set (contact, E_CONTACT_UID, uid);
+ if (revision)
+ e_contact_set (contact, E_CONTACT_REV, revision);
+
+ *out_vcard = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
+
+ g_object_unref (contact);
+ g_free (full_vcard);
+ } else {
+ *out_vcard = full_vcard;
+ }
+
+ g_free (revision);
+
+ return TRUE;
+}
+
+/**
+ * e_book_cache_set_contact_extra:
+ * @book_cache: An #EBookCache
+ * @uid: The uid of the contact to set the extra data for
+ * @extra: (nullable): The extra data to set
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Sets or replaces the extra data associated with @uid.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_set_contact_extra (EBookCache *book_cache,
+ const gchar *uid,
+ const gchar *extra,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gchar *stmt;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ if (!e_cache_contains (E_CACHE (book_cache), uid, E_CACHE_INCLUDE_DELETED)) {
+ g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s” not found"), uid);
+ return FALSE;
+ }
+
+ if (extra) {
+ stmt = e_cache_sqlite_stmt_printf (
+ "UPDATE " E_CACHE_TABLE_OBJECTS " SET " EBC_COLUMN_EXTRA "=%Q"
+ " WHERE " E_CACHE_COLUMN_UID "=%Q",
+ extra, uid);
+ } else {
+ stmt = e_cache_sqlite_stmt_printf (
+ "UPDATE " E_CACHE_TABLE_OBJECTS " SET " EBC_COLUMN_EXTRA "=NULL"
+ " WHERE " E_CACHE_COLUMN_UID "=%Q",
+ uid);
+ }
+
+ success = e_cache_sqlite_exec (E_CACHE (book_cache), stmt, cancellable, error);
+
+ e_cache_sqlite_stmt_free (stmt);
+
+ return success;
+}
+
+/**
+ * e_book_cache_get_contact_extra:
+ * @book_cache: An #EBookCache
+ * @uid: The uid of the contact to fetch the extra data for
+ * @out_extra: (out) (transfer full): Return location to store the extra data
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Fetches the extra data previously set for @uid, either with
+ * e_book_cache_set_contact_extra() or when adding contacts.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_get_contact_extra (EBookCache *book_cache,
+ const gchar *uid,
+ gchar **out_extra,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gchar *stmt;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ if (!e_cache_contains (E_CACHE (book_cache), uid, E_CACHE_INCLUDE_DELETED)) {
+ g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s” not found"), uid);
+ return FALSE;
+ }
+
+ stmt = e_cache_sqlite_stmt_printf (
+ "SELECT " EBC_COLUMN_EXTRA " FROM " E_CACHE_TABLE_OBJECTS
+ " WHERE " E_CACHE_COLUMN_UID "=%Q",
+ uid);
+
+ success = e_cache_sqlite_select (E_CACHE (book_cache), stmt, e_book_cache_get_string, out_extra,
cancellable, error);
+
+ e_cache_sqlite_stmt_free (stmt);
+
+ return success;
+}
+
+/**
+ * e_book_cache_search:
+ * @book_cache: An #EBookCache
+ * @sexp: (nullable): search expression; use %NULL or an empty string to list all stored contacts
+ * @meta_contacts: Whether entire contacts are desired, or only the metadata
+ * @out_list: (out) (transfer full) (element-type EBookCacheSearchData): Return location
+ * to store a #GSList of #EBookCacheSearchData structures
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Searches @book_cache for contacts matching the search expression @sexp.
+ *
+ * When @sexp refers only to #EContactFields configured in the summary of @book_cache,
+ * the search should always be quick, when searching for other #EContactFields
+ * a fallback will be used.
+ *
+ * The returned @out_list list should be freed with g_slist_free_full (list, e_book_cache_search_data_free)
+ * when no longer needed.
+ *
+ * If @meta_contact is specified, then shallow vCard representations will be
+ * created holding only the %E_CONTACT_UID and %E_CONTACT_REV fields.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_search (EBookCache *book_cache,
+ const gchar *sexp,
+ gboolean meta_contacts,
+ GSList **out_list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (out_list != NULL, FALSE);
+
+ *out_list = NULL;
+
+ return ebc_search_internal (book_cache, sexp,
+ meta_contacts ? SEARCH_UID_AND_REV : SEARCH_FULL,
+ out_list, NULL, NULL, cancellable, error);
+}
+
+/**
+ * e_book_cache_search_uids:
+ * @book_cache: An #EBookCache
+ * @sexp: (nullable): search expression; use %NULL or an empty string to get all stored contacts
+ * @out_list: (out) (transfer full) (element-type utf8): Return location to store a #GSList of contact uids
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Similar to e_book_cache_search(), but fetches only a list of contact UIDs.
+ *
+ * The returned @out_list list should be freed with g_slist_free_full(list, g_free)
+ * when no longer needed.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_search_uids (EBookCache *book_cache,
+ const gchar *sexp,
+ GSList **out_list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (out_list != NULL, FALSE);
+
+ *out_list = NULL;
+
+ return ebc_search_internal (book_cache, sexp, SEARCH_UID, out_list, NULL, NULL, cancellable, error);
+}
+
+/**
+ * e_book_cache_search_with_callback:
+ * @book_cache: An #EBookCache
+ * @sexp: (nullable): search expression; use %NULL or an empty string to get all stored contacts
+ * @func: an #EBookCacheSearchFunc callback to call for each found row
+ * @user_data: user data for @func
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Similar to e_book_cache_search(), but calls the @func for each found contact.
+ *
+ * Returns: %TRUE on success, otherwise %FALSE is returned and @error is set appropriately.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_search_with_callback (EBookCache *book_cache,
+ const gchar *sexp,
+ EBookCacheSearchFunc func,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (func != NULL, FALSE);
+
+ return ebc_search_internal (book_cache, sexp, SEARCH_FULL, NULL, func, user_data, cancellable, error);
+}
+
+/**
+ * e_book_cache_cursor_new:
+ * @book_cache: An #EBookCache
+ * @sexp: search expression; use %NULL or an empty string to get all stored contacts
+ * @sort_fields: (array length=n_sort_fields): An array of #EContactField-s as sort keys in order of priority
+ * @sort_types: (array length=n_sort_fields): An array of #EBookCursorSortTypes, one for each field in
@sort_fields
+ * @n_sort_fields: The number of fields to sort results by
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new #EBookCacheCursor.
+ *
+ * The cursor should be freed with e_book_cache_cursor_free() when
+ * no longer needed.
+ *
+ * Returns: (transfer full): A newly created #EBookCacheCursor
+ *
+ * Since: 3.26
+ **/
+EBookCacheCursor *
+e_book_cache_cursor_new (EBookCache *book_cache,
+ const gchar *sexp,
+ const EContactField *sort_fields,
+ const EBookCursorSortType *sort_types,
+ guint n_sort_fields,
+ GError **error)
+{
+ EBookCacheCursor *cursor;
+ gint ii;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), NULL);
+
+ /* We don't like '\0' sexps, prefer NULL */
+ if (sexp && !*sexp)
+ sexp = NULL;
+
+ e_cache_lock (E_CACHE (book_cache), E_CACHE_LOCK_READ);
+
+ /* Need one sort key ... */
+ if (n_sort_fields == 0) {
+ g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY,
+ _("At least one sort field must be specified to use a cursor"));
+ e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+ return NULL;
+ }
+
+ /* We only support string fields to sort the cursor */
+ for (ii = 0; ii < n_sort_fields; ii++) {
+ if (e_contact_field_type (sort_fields[ii]) != G_TYPE_STRING) {
+ g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY,
+ _("Cannot sort by a field that is not a string type"));
+
+ e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+ return NULL;
+ }
+ }
+
+ /* Now we need to create the cursor instance before setting up the query
+ * (not really true, but more convenient that way).
+ */
+ cursor = ebc_cursor_new (book_cache, sexp, sort_fields, sort_types, n_sort_fields);
+
+ /* Setup the cursor's query expression which might fail */
+ if (!ebc_cursor_setup_query (book_cache, cursor, sexp, error)) {
+ ebc_cursor_free (cursor);
+ cursor = NULL;
+ }
+
+ e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+
+ return cursor;
+}
+
+/**
+ * e_book_cache_cursor_free:
+ * @book_cache: An #EBookCache
+ * @cursor: The #EBookCacheCursor to free
+ *
+ * Frees the @cursor, previously allocated with e_book_cache_cursor_new().
+ *
+ * Since: 3.26
+ **/
+void
+e_book_cache_cursor_free (EBookCache *book_cache,
+ EBookCacheCursor *cursor)
+{
+ g_return_if_fail (E_IS_BOOK_CACHE (book_cache));
+ g_return_if_fail (cursor != NULL);
+
+ ebc_cursor_free (cursor);
+}
+
+typedef struct {
+ gint uid_index;
+ gint object_index;
+ gint extra_index;
+
+ GSList *results;
+ gchar *alloc_vcard;
+ const gchar *last_vcard;
+
+ gboolean collect_results;
+ gint n_results;
+} CursorCollectData;
+
+static gboolean
+ebc_collect_results_for_cursor_cb (ECache *cache,
+ gint ncols,
+ const gchar *column_names[],
+ const gchar *column_values[],
+ gpointer user_data)
+{
+ CursorCollectData *data = user_data;
+ const gchar *object = NULL, *extra = NULL;
+
+ if (data->uid_index == -1 ||
+ data->object_index == -1 ||
+ data->extra_index == -1) {
+ gint ii;
+
+ for (ii = 0; ii < ncols && (data->uid_index == -1 ||
+ data->object_index == -1 ||
+ data->extra_index == -1); ii++) {
+ const gchar *cname = column_names[ii];
+
+ if (!cname)
+ continue;
+
+ if (g_str_has_prefix (cname, "summary."))
+ cname += 8;
+
+ if (data->uid_index == -1 && g_ascii_strcasecmp (cname, E_CACHE_COLUMN_UID) == 0) {
+ data->uid_index = ii;
+ } else if (data->object_index == -1 && g_ascii_strcasecmp (cname,
E_CACHE_COLUMN_OBJECT) == 0) {
+ data->object_index = ii;
+ } else if (data->extra_index == -1 && g_ascii_strcasecmp (cname, EBC_COLUMN_EXTRA) ==
0) {
+ data->extra_index = ii;
+ }
+ }
+
+ if (data->object_index == -1)
+ data->object_index = -2;
+
+ if (data->extra_index == -1)
+ data->extra_index = -2;
+ }
+
+ g_return_val_if_fail (data->uid_index >= 0 && data->uid_index < ncols, FALSE);
+
+ if (data->object_index != -2) {
+ g_return_val_if_fail (data->object_index >= 0 && data->object_index < ncols, FALSE);
+ object = column_values[data->object_index];
+ }
+
+ if (data->extra_index != -2) {
+ g_return_val_if_fail (data->extra_index >= 0 && data->extra_index < ncols, FALSE);
+ extra = column_values[data->extra_index];
+ }
+
+ if (data->collect_results) {
+ EBookCacheSearchData *search_data;
+
+ search_data = e_book_cache_search_data_new (column_values[data->uid_index], object, extra);
+
+ data->results = g_slist_prepend (data->results, search_data);
+
+ data->last_vcard = search_data->vcard;
+ } else {
+ g_free (data->alloc_vcard);
+ data->alloc_vcard = g_strdup (object);
+
+ data->last_vcard = data->alloc_vcard;
+ }
+
+ data->n_results++;
+
+ return TRUE;
+}
+
+/**
+ * e_book_cache_cursor_step:
+ * @book_cache: An #EBookCache
+ * @cursor: The #EBookCacheCursor to use
+ * @flags: The #EBookCacheCursorStepFlags for this step
+ * @origin: The #EBookCacheCursorOrigin from whence to step
+ * @count: A positive or negative amount of contacts to try and fetch
+ * @out_results: (out) (nullable) (element-type EBookCacheSearchData) (transfer full):
+ * A return location to store the results, or %NULL if %E_BOOK_CACHE_CURSOR_STEP_FETCH is not specified in
@flags.
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Steps @cursor through its sorted query by a maximum of @count contacts
+ * starting from @origin.
+ *
+ * If @count is negative, then the cursor will move through the list in reverse.
+ *
+ * If @cursor reaches the beginning or end of the query results, then the
+ * returned list might not contain the amount of desired contacts, or might
+ * return no results if the cursor currently points to the last contact.
+ * Reaching the end of the list is not considered an error condition. Attempts
+ * to step beyond the end of the list after having reached the end of the list
+ * will however trigger an %E_CACHE_ERROR_END_OF_LIST error.
+ *
+ * If %E_BOOK_CACHE_CURSOR_STEP_FETCH is specified in @flags, a pointer to
+ * a %NULL #GSList pointer should be provided for the @out_results parameter.
+ *
+ * The result list will be stored to @out_results and should be freed
+ * with g_slist_free_full (results, e_book_cache_search_data_free);
+ * when no longer needed.
+ *
+ * Returns: The number of contacts traversed if successful, otherwise -1 is
+ * returned and the @error is set.
+ *
+ * Since: 3.26
+ **/
+gint
+e_book_cache_cursor_step (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ EBookCacheCursorStepFlags flags,
+ EBookCacheCursorOrigin origin,
+ gint count,
+ GSList **out_results,
+ GCancellable *cancellable,
+ GError **error)
+{
+ CursorCollectData data = { -1, -1, -1, NULL, NULL, NULL, FALSE, 0 };
+ CursorState *state;
+ GString *query;
+ gboolean success;
+ EBookCacheCursorOrigin try_position;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), -1);
+ g_return_val_if_fail (cursor != NULL, -1);
+ g_return_val_if_fail ((flags & E_BOOK_CACHE_CURSOR_STEP_FETCH) == 0 ||
+ (out_results != NULL), -1);
+
+ if (out_results)
+ *out_results = NULL;
+
+ e_cache_lock (E_CACHE (book_cache), E_CACHE_LOCK_READ);
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
+ e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+ return -1;
+ }
+
+ /* Check if this step should result in an end of list error first */
+ try_position = cursor->state.position;
+ if (origin != E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT)
+ try_position = origin;
+
+ /* Report errors for requests to run off the end of the list */
+ if (try_position == E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN && count < 0) {
+ g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_END_OF_LIST,
+ _("Tried to step a cursor in reverse, "
+ "but cursor is already at the beginning of the contact list"));
+
+ e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+ return -1;
+ } else if (try_position == E_BOOK_CACHE_CURSOR_ORIGIN_END && count > 0) {
+ g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_END_OF_LIST,
+ _("Tried to step a cursor forwards, "
+ "but cursor is already at the end of the contact list"));
+
+ e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+ return -1;
+ }
+
+ /* Nothing to do, silently return */
+ if (count == 0 && try_position == E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT) {
+ e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+ return 0;
+ }
+
+ /* If we're not going to modify the position, just use
+ * a copy of the current cursor state.
+ */
+ if ((flags & E_BOOK_CACHE_CURSOR_STEP_MOVE) != 0)
+ state = &(cursor->state);
+ else
+ state = cursor_state_copy (cursor, &(cursor->state));
+
+ /* Every query starts with the STATE_CURRENT position, first
+ * fix up the cursor state according to 'origin'
+ */
+ switch (origin) {
+ case E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT:
+ /* Do nothing, normal operation */
+ break;
+
+ case E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN:
+ case E_BOOK_CACHE_CURSOR_ORIGIN_END:
+
+ /* Prepare the state before executing the query */
+ cursor_state_clear (cursor, state, origin);
+ break;
+ }
+
+ /* If count is 0 then there is no need to run any
+ * query, however it can be useful if you just want
+ * to move the cursor to the beginning or ending of
+ * the list.
+ */
+ if (count == 0) {
+ /* Free the state copy if need be */
+ if ((flags & E_BOOK_CACHE_CURSOR_STEP_MOVE) == 0)
+ cursor_state_free (cursor, state);
+
+ e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+ return 0;
+ }
+
+ query = g_string_new (cursor->select_vcards);
+
+ /* Add the filter constraints (if any) */
+ if (cursor->query) {
+ g_string_append (query, " WHERE ");
+
+ g_string_append_c (query, '(');
+ g_string_append (query, cursor->query);
+ g_string_append_c (query, ')');
+ }
+
+ /* Add the cursor constraints (if any) */
+ if (state->values[0] != NULL) {
+ gchar *constraints = NULL;
+
+ if (!cursor->query)
+ g_string_append (query, " WHERE ");
+ else
+ g_string_append (query, " AND ");
+
+ constraints = ebc_cursor_constraints (book_cache, cursor, state, count < 0, FALSE);
+
+ g_string_append_c (query, '(');
+ g_string_append (query, constraints);
+ g_string_append_c (query, ')');
+
+ g_free (constraints);
+ }
+
+ /* Add the sort order */
+ g_string_append_c (query, ' ');
+ if (count > 0)
+ g_string_append (query, cursor->order);
+ else
+ g_string_append (query, cursor->reverse_order);
+
+ /* Add the limit */
+ g_string_append_printf (query, " LIMIT %d", ABS (count));
+
+ /* Specify whether we really want results or not */
+ data.collect_results = (flags & E_BOOK_CACHE_CURSOR_STEP_FETCH) != 0;
+
+ /* Execute the query */
+ success = e_cache_sqlite_select (E_CACHE (book_cache), query->str,
+ ebc_collect_results_for_cursor_cb, &data,
+ cancellable, error);
+
+ /* Lock was obtained above */
+ e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+
+ g_string_free (query, TRUE);
+
+ /* If there was no error, update the internal cursor state */
+ if (success) {
+ if (data.n_results < ABS (count)) {
+ /* We've reached the end, clear the current state */
+ if (count < 0)
+ cursor_state_clear (cursor, state, E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN);
+ else
+ cursor_state_clear (cursor, state, E_BOOK_CACHE_CURSOR_ORIGIN_END);
+
+ } else if (data.last_vcard) {
+ /* Set the cursor state to the last result */
+ cursor_state_set_from_vcard (book_cache, cursor, state, data.last_vcard);
+ } else {
+ /* Should never get here */
+ g_warn_if_reached ();
+ }
+
+ /* Assign the results to return (if any) */
+ if (out_results) {
+ /* Correct the order of results at the last minute */
+ *out_results = g_slist_reverse (data.results);
+ data.results = NULL;
+ }
+ }
+
+ /* Cleanup what was allocated by collect_results_for_cursor_cb() */
+ if (data.results)
+ g_slist_free_full (data.results, e_book_cache_search_data_free);
+ g_free (data.alloc_vcard);
+
+ /* Free the copy state if we were working with a copy */
+ if ((flags & E_BOOK_CACHE_CURSOR_STEP_MOVE) == 0)
+ cursor_state_free (cursor, state);
+
+ if (success)
+ return data.n_results;
+
+ return -1;
+}
+
+/**
+ * e_book_cache_cursor_set_target_alphabetic_index:
+ * @book_cache: An #EBookCache
+ * @cursor: The #EBookCacheCursor to modify
+ * @idx: The alphabetic index
+ *
+ * Sets the @cursor position to an
+ * <link linkend="cursor-alphabet">Alphabetic Index</link>
+ * into the alphabet active in @book_cache's locale.
+ *
+ * After setting the target to an alphabetic index, for example the
+ * index for letter 'E', then further calls to e_book_cache_cursor_step()
+ * will return results starting with the letter 'E' (or results starting
+ * with the last result in 'D', if moving in a negative direction).
+ *
+ * The passed index must be a valid index in the active locale, knowledge
+ * on the currently active alphabet index must be obtained using #ECollator
+ * APIs.
+ *
+ * Use e_book_cahce_ref_collator() to obtain the active collator for @book_cache.
+ *
+ * Since: 3.26
+ **/
+void
+e_book_cache_cursor_set_target_alphabetic_index (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ gint idx)
+{
+ gint n_labels = 0;
+
+ g_return_if_fail (E_IS_BOOK_CACHE (book_cache));
+ g_return_if_fail (cursor != NULL);
+ g_return_if_fail (idx >= 0);
+
+ e_collator_get_index_labels (book_cache->priv->collator, &n_labels, NULL, NULL, NULL);
+ g_return_if_fail (idx < n_labels);
+
+ cursor_state_clear (cursor, &(cursor->state), E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT);
+ if (cursor->n_sort_fields > 0) {
+ SummaryField *field;
+ gchar *index_key;
+
+ index_key = e_collator_generate_key_for_index (book_cache->priv->collator, idx);
+ field = summary_field_get (book_cache, cursor->sort_fields[0]);
+
+ if (field && (field->index & INDEX_FLAG (SORT_KEY)) != 0) {
+ cursor->state.values[0] = index_key;
+ } else {
+ cursor->state.values[0] = ebc_encode_vcard_sort_key (index_key);
+ g_free (index_key);
+ }
+ }
+}
+
+/**
+ * e_book_cache_cursor_set_sexp:
+ * @book_cache: An #EBookCache
+ * @cursor: The #EBookCacheCursor to modify
+ * @sexp: The new query expression for @cursor
+ * @error: return location for a #GError, or %NULL
+ *
+ * Modifies the current query expression for @cursor. This will not
+ * modify @cursor's state, but will change the outcome of any further
+ * calls to e_book_cache_cursor_step() or e_book_cache_cursor_calculate().
+ *
+ * Returns: %TRUE if the expression was valid and accepted by @cursor
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_cursor_set_sexp (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ const gchar *sexp,
+ GError **error)
+{
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (cursor != NULL, FALSE);
+
+ /* We don't like '\0' sexps, prefer NULL */
+ if (sexp && !*sexp)
+ sexp = NULL;
+
+ e_cache_lock (E_CACHE (book_cache), E_CACHE_LOCK_READ);
+
+ success = ebc_cursor_setup_query (book_cache, cursor, sexp, error);
+
+ e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+
+ return success;
+}
+
+/**
+ * e_book_cache_cursor_calculate:
+ * @book_cache: An #EBookCache
+ * @cursor: The #EBookCacheCursor
+ * @out_total: (out) (nullable): A return location to store the total result set for this cursor
+ * @out_position: (out) (nullable): A return location to store the cursor position
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Calculates the @out_total amount of results for the @cursor's query expression,
+ * as well as the current @out_position of @cursor in the results. The @out_position is
+ * represented as the amount of results which lead up to the current value
+ * of @cursor, if @cursor currently points to an exact contact, the position
+ * also includes the cursor contact.
+ *
+ * Returns: Whether @out_total and @out_position were successfully calculated.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_cache_cursor_calculate (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ gint *out_total,
+ gint *out_position,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean success = TRUE;
+ gint local_total = 0;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+ g_return_val_if_fail (cursor != NULL, FALSE);
+
+ /* If we're in a clear cursor state, then the position is 0 */
+ if (out_position && cursor->state.values[0] == NULL) {
+ if (cursor->state.position == E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN) {
+ /* Mark the local pointer NULL, no need to calculate this anymore */
+ *out_position = 0;
+ out_position = NULL;
+ } else if (cursor->state.position == E_BOOK_CACHE_CURSOR_ORIGIN_END) {
+ /* Make sure that we look up the total so we can
+ * set the position to 'total + 1'
+ */
+ if (!out_total)
+ out_total = &local_total;
+ }
+ }
+
+ /* Early return if there is nothing to do */
+ if (!out_total && !out_position)
+ return TRUE;
+
+ e_cache_lock (E_CACHE (book_cache), E_CACHE_LOCK_READ);
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
+ e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+ return FALSE;
+ }
+
+ if (out_total)
+ success = cursor_count_total_locked (book_cache, cursor, out_total, cancellable, error);
+
+ if (success && out_position)
+ success = cursor_count_position_locked (book_cache, cursor, out_position, cancellable, error);
+
+ e_cache_unlock (E_CACHE (book_cache), E_CACHE_UNLOCK_NONE);
+
+ /* In the case we're at the end, we just set the position
+ * to be the total + 1
+ */
+ if (success && out_position && out_total &&
+ cursor->state.position == E_BOOK_CACHE_CURSOR_ORIGIN_END)
+ *out_position = *out_total + 1;
+
+ return success;
+}
+
+/**
+ * e_book_cache_cursor_compare_contact:
+ * @book_cache: An #EBookCache
+ * @cursor: The #EBookCacheCursor
+ * @contact: The #EContact to compare
+ * @out_matches_sexp: (out) (nullable): Whether the contact matches the cursor's search expression
+ *
+ * Compares @contact with @cursor and returns whether @contact is less than, equal to, or greater
+ * than @cursor.
+ *
+ * Returns: A value that is less than, equal to, or greater than zero if @contact is found,
+ * respectively, to be less than, to match, or be greater than the current value of @cursor.
+ *
+ * Since: 3.26
+ **/
+gint
+e_book_cache_cursor_compare_contact (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ EContact *contact,
+ gboolean *out_matches_sexp)
+{
+ gint ii;
+ gint comparison = 0;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), -1);
+ g_return_val_if_fail (cursor != NULL, -1);
+ g_return_val_if_fail (E_IS_CONTACT (contact), -1);
+
+ if (out_matches_sexp) {
+ if (!cursor->sexp)
+ *out_matches_sexp = TRUE;
+ else
+ *out_matches_sexp = e_book_backend_sexp_match_contact (cursor->sexp, contact);
+ }
+
+ for (ii = 0; ii < cursor->n_sort_fields && comparison == 0; ii++) {
+ SummaryField *field;
+ gchar *contact_key = NULL;
+ const gchar *cursor_key = NULL;
+ const gchar *field_value;
+ gchar *freeme = NULL;
+
+ field_value = e_contact_get_const (contact, cursor->sort_fields[ii]);
+ if (field_value)
+ contact_key = e_collator_generate_key (book_cache->priv->collator, field_value, NULL);
+
+ field = summary_field_get (book_cache, cursor->sort_fields[ii]);
+
+ if (field && (field->index & INDEX_FLAG (SORT_KEY)) != 0) {
+ cursor_key = cursor->state.values[ii];
+ } else {
+
+ if (cursor->state.values[ii])
+ freeme = ebc_decode_vcard_sort_key (cursor->state.values[ii]);
+
+ cursor_key = freeme;
+ }
+
+ /* Empty state sorts below any contact value, which means the contact sorts above cursor */
+ if (cursor_key == NULL)
+ comparison = 1;
+ else
+ /* Check if contact sorts below, equal to, or above the cursor */
+ comparison = g_strcmp0 (contact_key, cursor_key);
+
+ g_free (contact_key);
+ g_free (freeme);
+ }
+
+ /* UID tie-breaker */
+ if (comparison == 0) {
+ const gchar *uid;
+
+ uid = e_contact_get_const (contact, E_CONTACT_UID);
+
+ if (cursor->state.last_uid == NULL)
+ comparison = 1;
+ else if (uid == NULL)
+ comparison = -1;
+ else
+ comparison = strcmp (uid, cursor->state.last_uid);
+ }
+
+ return comparison;
+}
+
+static gchar *
+ebc_dup_contact_revision (EBookCache *book_cache,
+ EContact *contact)
+{
+ g_return_val_if_fail (E_IS_CONTACT (contact), NULL);
+
+ return e_contact_get (contact, E_CONTACT_REV);
+}
+
+static gboolean
+e_book_cache_put_locked (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ ECacheColumnValues *other_columns,
+ EOfflineState offline_state,
+ gboolean is_replace,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookCache *book_cache;
+ EContact *contact;
+ gchar *updated_vcard = NULL;
+ gboolean e164_changed;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+ g_return_val_if_fail (E_CACHE_CLASS (e_book_cache_parent_class)->put_locked != NULL, FALSE);
+
+ book_cache = E_BOOK_CACHE (cache);
+
+ contact = e_contact_new_from_vcard_with_uid (object, uid);
+
+ /* Update E.164 parameters in vcard if needed */
+ e164_changed = update_e164_attribute_params (book_cache, contact, book_cache->priv->region_code);
+
+ if (e164_changed) {
+ updated_vcard = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
+ object = updated_vcard;
+ }
+
+ success = E_CACHE_CLASS (e_book_cache_parent_class)->put_locked (cache, uid, revision, object,
other_columns, offline_state,
+ is_replace, cancellable, error);
+
+ success = success && ebc_update_aux_tables (cache, uid, revision, object, cancellable, error);
+
+ if (success && e164_changed)
+ g_signal_emit (book_cache, signals[E164_CHANGED], 0, contact, is_replace);
+
+ g_clear_object (&contact);
+ g_free (updated_vcard);
+
+ return success;
+}
+
+static gboolean
+e_book_cache_remove_locked (ECache *cache,
+ const gchar *uid,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+ g_return_val_if_fail (E_CACHE_CLASS (e_book_cache_parent_class)->remove_locked != NULL, FALSE);
+
+ success = ebc_delete_from_aux_tables (cache, uid, cancellable, error);
+
+ success = success && E_CACHE_CLASS (e_book_cache_parent_class)->remove_locked (cache, uid,
cancellable, error);
+
+ return success;
+}
+
+static gboolean
+e_book_cache_remove_all_locked (ECache *cache,
+ const GSList *uids,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+ g_return_val_if_fail (E_CACHE_CLASS (e_book_cache_parent_class)->remove_all_locked != NULL, FALSE);
+
+ success = ebc_empty_aux_tables (cache, cancellable, error);
+
+ success = success && E_CACHE_CLASS (e_book_cache_parent_class)->remove_all_locked (cache, uids,
cancellable, error);
+
+ return success;
+}
+
+static gboolean
+e_book_cache_clear_offline_changes_locked (ECache *cache,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_CACHE (cache), FALSE);
+ g_return_val_if_fail (E_CACHE_CLASS (e_book_cache_parent_class)->clear_offline_changes_locked !=
NULL, FALSE);
+
+ /* First check whether there are any locally deleted objects at all */
+ if (e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, cancellable, error) >
+ e_cache_get_count (cache, E_CACHE_EXCLUDE_DELETED, cancellable, error))
+ success = ebc_delete_from_aux_tables_offline_deleted (cache, cancellable, error);
+ else
+ success = TRUE;
+
+ success = success && E_CACHE_CLASS (e_book_cache_parent_class)->clear_offline_changes_locked (cache,
cancellable, error);
+
+ return success;
+}
+
+static void
+e_book_cache_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case PROP_LOCALE:
+ g_value_take_string (
+ value,
+ e_book_cache_dup_locale (E_BOOK_CACHE (object)));
+ return;
+ }
+
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+e_book_cache_finalize (GObject *object)
+{
+ EBookCache *book_cache = E_BOOK_CACHE (object);
+
+ g_clear_object (&book_cache->priv->source);
+
+ if (book_cache->priv->collator) {
+ e_collator_unref (book_cache->priv->collator);
+ book_cache->priv->collator = NULL;
+ }
+
+ g_free (book_cache->priv->locale);
+ g_free (book_cache->priv->region_code);
+
+ if (book_cache->priv->summary_fields) {
+ summary_fields_array_free (book_cache->priv->summary_fields,
book_cache->priv->n_summary_fields);
+ book_cache->priv->summary_fields = NULL;
+ }
+
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_book_cache_parent_class)->finalize (object);
+}
+
+static void
+e_book_cache_class_init (EBookCacheClass *klass)
+{
+ GObjectClass *object_class;
+ ECacheClass *cache_class;
+
+ g_type_class_add_private (klass, sizeof (EBookCachePrivate));
+
+ object_class = G_OBJECT_CLASS (klass);
+ object_class->get_property = e_book_cache_get_property;
+ object_class->finalize = e_book_cache_finalize;
+
+ cache_class = E_CACHE_CLASS (klass);
+ cache_class->put_locked = e_book_cache_put_locked;
+ cache_class->remove_locked = e_book_cache_remove_locked;
+ cache_class->remove_all_locked = e_book_cache_remove_all_locked;
+ cache_class->clear_offline_changes_locked = e_book_cache_clear_offline_changes_locked;
+
+ klass->dup_contact_revision = ebc_dup_contact_revision;
+
+ g_object_class_install_property (
+ object_class,
+ PROP_LOCALE,
+ g_param_spec_string (
+ "locale",
+ "Locate",
+ "The locale currently being used",
+ NULL,
+ G_PARAM_READABLE |
+ G_PARAM_STATIC_STRINGS));
+
+ signals[E164_CHANGED] = g_signal_new (
+ "e164-changed",
+ G_OBJECT_CLASS_TYPE (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (EBookCacheClass, e164_changed),
+ NULL,
+ NULL,
+ g_cclosure_marshal_generic,
+ G_TYPE_NONE, 2,
+ E_TYPE_CONTACT,
+ G_TYPE_BOOLEAN);
+
+ /**
+ * @EBookCache:dup-contact-revision:
+ * A signal being called to get revision of an EContact.
+ * The default implementation returns E_CONTACT_REV field value.
+ **/
+ signals[DUP_CONTACT_REVISION] = g_signal_new (
+ "dup-contact-revision",
+ G_OBJECT_CLASS_TYPE (klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+ G_STRUCT_OFFSET (EBookCacheClass, dup_contact_revision),
+ g_signal_accumulator_first_wins,
+ NULL,
+ g_cclosure_marshal_generic,
+ G_TYPE_STRING, 1,
+ E_TYPE_CONTACT);
+}
+
+static void
+e_book_cache_init (EBookCache *book_cache)
+{
+ book_cache->priv = G_TYPE_INSTANCE_GET_PRIVATE (book_cache, E_TYPE_BOOK_CACHE, EBookCachePrivate);
+}
diff --git a/src/addressbook/libedata-book/e-book-cache.h b/src/addressbook/libedata-book/e-book-cache.h
new file mode 100644
index 0000000..5e13799
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-cache.h
@@ -0,0 +1,323 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2013 Intel Corporation
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Tristan Van Berkom <tristanvb openismus com>
+ */
+
+#if !defined (__LIBEDATA_BOOK_H_INSIDE__) && !defined (LIBEDATA_BOOK_COMPILATION)
+#error "Only <libedata-book/libedata-book.h> should be included directly."
+#endif
+
+#ifndef E_BOOK_CACHE_H
+#define E_BOOK_CACHE_H
+
+#include <libebackend/libebackend.h>
+#include <libebook-contacts/libebook-contacts.h>
+
+/* Standard GObject macros */
+#define E_TYPE_BOOK_CACHE \
+ (e_book_cache_get_type ())
+#define E_BOOK_CACHE(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST \
+ ((obj), E_TYPE_BOOK_CACHE, EBookCache))
+#define E_BOOK_CACHE_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_CAST \
+ ((cls), E_TYPE_BOOK_CACHE, EBookCacheClass))
+#define E_IS_BOOK_CACHE(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE \
+ ((obj), E_TYPE_BOOK_CACHE))
+#define E_IS_BOOK_CACHE_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_TYPE \
+ ((cls), E_TYPE_BOOK_CACHE))
+#define E_BOOK_CACHE_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS \
+ ((obj), E_TYPE_BOOK_CACHE, EBookCacheClass))
+
+G_BEGIN_DECLS
+
+typedef struct _EBookCache EBookCache;
+typedef struct _EBookCacheClass EBookCacheClass;
+typedef struct _EBookCachePrivate EBookCachePrivate;
+
+/**
+ * EBookCacheSearchData:
+ * @uid: The %E_CONTACT_UID field of this contact
+ * @vcard: The vcard string
+ * @extra: Any extra data associated with the vcard
+ *
+ * This structure is used to represent contacts returned
+ * by the #EBookCache from various functions
+ * such as e_book_cache_search().
+ *
+ * The @extra parameter will contain any data which was
+ * previously passed for this contact in e_book_cache_put_contact()
+ * or set with e_book_cache_set_contact_extra().
+ *
+ * These should be freed with e_book_cache_search_data_free().
+ *
+ * Since: 3.26
+ **/
+typedef struct {
+ gchar *uid;
+ gchar *vcard;
+ gchar *extra;
+} EBookCacheSearchData;
+
+#define E_TYPE_BOOK_CACHE_SEARCH_DATA (e_book_cache_search_data_get_type ())
+
+GType e_book_cache_search_data_get_type
+ (void) G_GNUC_CONST;
+EBookCacheSearchData *
+ e_book_cache_search_data_new (const gchar *uid,
+ const gchar *vcard,
+ const gchar *extra);
+EBookCacheSearchData *
+ e_book_cache_search_data_copy (const EBookCacheSearchData *data);
+void e_book_cache_search_data_free (/* EBookCacheSearchData * */ gpointer data);
+
+/**
+ * EBookCacheSearchFunc:
+ * @book_cache: an #EBookCache
+ * @uid: a unique object identifier
+ * @revision: the object revision
+ * @object: the object itself
+ * @extra: extra data stored with the object
+ * @offline_state: objects offline state, one of #EOfflineState
+ * @user_data: user data, as used in e_book_cache_search_with_callback()
+ *
+ * A callback called for each object row when using
+ * e_book_cache_search_with_callback() function.
+ *
+ * Returns: %TRUE to continue, %FALSE to stop walk through.
+ *
+ * Since: 3.26
+ **/
+typedef gboolean (* EBookCacheSearchFunc) (EBookCache *book_cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ const gchar *extra,
+ EOfflineState offline_state,
+ gpointer user_data);
+
+/**
+ * EBookCache:
+ *
+ * Contains only private data that should be read and manipulated using
+ * the functions below.
+ *
+ * Since: 3.26
+ **/
+struct _EBookCache {
+ /*< private >*/
+ ECache parent;
+ EBookCachePrivate *priv;
+};
+
+/**
+ * EBookCacheClass:
+ *
+ * Class structure for the #EBookCache class.
+ *
+ * Since: 3.26
+ */
+struct _EBookCacheClass {
+ /*< private >*/
+ ECacheClass parent_class;
+
+ /* Signals */
+ void (* e164_changed) (EBookCache *book_cache,
+ EContact *contact,
+ gboolean is_replace);
+
+ gchar * (* dup_contact_revision)
+ (EBookCache *book_cache,
+ EContact *contact);
+
+ /* Padding for future expansion */
+ gpointer reserved[10];
+};
+
+/**
+ * EBookCacheCursor:
+ *
+ * An opaque cursor pointer
+ *
+ * Since: 3.26
+ */
+typedef struct _EBookCacheCursor EBookCacheCursor;
+
+/**
+ * EBookCacheCursorOrigin:
+ * @E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT: The current cursor position.
+ * @E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN: The beginning of the cursor results.
+ * @E_BOOK_CACHE_CURSOR_ORIGIN_END: The end of the cursor results.
+ *
+ * Specifies the start position to in the list of traversed contacts
+ * in calls to e_book_cache_cursor_step().
+ *
+ * When an #EBookCacheCursor is created, the current position implied by %E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT
+ * is the same as %E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN.
+ *
+ * Since: 3.26
+ */
+typedef enum {
+ E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT = 0,
+ E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN,
+ E_BOOK_CACHE_CURSOR_ORIGIN_END
+} EBookCacheCursorOrigin;
+
+/**
+ * EBookCacheCursorStepFlags:
+ * @E_BOOK_CACHE_CURSOR_STEP_MOVE: The cursor position should be modified while stepping.
+ * @E_BOOK_CACHE_CURSOR_STEP_FETCH: Traversed contacts should be listed and returned while stepping.
+ *
+ * Defines the behaviour of e_book_cache_cursor_step().
+ *
+ * Since: 3.26
+ */
+typedef enum {
+ E_BOOK_CACHE_CURSOR_STEP_MOVE = (1 << 0),
+ E_BOOK_CACHE_CURSOR_STEP_FETCH = (1 << 1)
+} EBookCacheCursorStepFlags;
+
+GType e_book_cache_get_type (void) G_GNUC_CONST;
+
+EBookCache * e_book_cache_new (const gchar *filename,
+ ESource *source,
+ GCancellable *cancellable,
+ GError **error);
+EBookCache * e_book_cache_new_full (const gchar *filename,
+ ESource *source,
+ ESourceBackendSummarySetup *setup,
+ GCancellable *cancellable,
+ GError **error);
+ESource * e_book_cache_ref_source (EBookCache *book_cache);
+gchar * e_book_cache_dup_contact_revision
+ (EBookCache *book_cache,
+ EContact *contact);
+gboolean e_book_cache_set_locale (EBookCache *book_cache,
+ const gchar *lc_collate,
+ GCancellable *cancellable,
+ GError **error);
+gchar * e_book_cache_dup_locale (EBookCache *book_cache);
+
+ECollator * e_book_cache_ref_collator (EBookCache *book_cache);
+
+/* Adding / Removing / Searching contacts */
+gboolean e_book_cache_put_contact (EBookCache *book_cache,
+ EContact *contact,
+ const gchar *extra,
+ ECacheOfflineFlag offline_flag,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_cache_put_contacts (EBookCache *book_cache,
+ const GSList *contacts,
+ const GSList *extras,
+ ECacheOfflineFlag offline_flag,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_cache_remove_contact (EBookCache *book_cache,
+ const gchar *uid,
+ ECacheOfflineFlag offline_flag,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_cache_remove_contacts (EBookCache *book_cache,
+ const GSList *uids,
+ ECacheOfflineFlag offline_flag,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_cache_get_contact (EBookCache *book_cache,
+ const gchar *uid,
+ gboolean meta_contact,
+ EContact **out_contact,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_cache_get_vcard (EBookCache *book_cache,
+ const gchar *uid,
+ gboolean meta_contact,
+ gchar **out_vcard,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_cache_set_contact_extra (EBookCache *book_cache,
+ const gchar *uid,
+ const gchar *extra,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_cache_get_contact_extra (EBookCache *book_cache,
+ const gchar *uid,
+ gchar **out_extra,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_cache_search (EBookCache *book_cache,
+ const gchar *sexp,
+ gboolean meta_contacts,
+ GSList **out_list, /* EBookCacheSearchData * */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_cache_search_uids (EBookCache *book_cache,
+ const gchar *sexp,
+ GSList **out_list, /* gchar * */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_cache_search_with_callback
+ (EBookCache *book_cache,
+ const gchar *sexp,
+ EBookCacheSearchFunc func,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error);
+/* Cursor API */
+EBookCacheCursor *
+ e_book_cache_cursor_new (EBookCache *book_cache,
+ const gchar *sexp,
+ const EContactField *sort_fields,
+ const EBookCursorSortType *sort_types,
+ guint n_sort_fields,
+ GError **error);
+void e_book_cache_cursor_free (EBookCache *book_cache,
+ EBookCacheCursor *cursor);
+gint e_book_cache_cursor_step (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ EBookCacheCursorStepFlags flags,
+ EBookCacheCursorOrigin origin,
+ gint count,
+ GSList **out_results,
+ GCancellable *cancellable,
+ GError **error);
+void e_book_cache_cursor_set_target_alphabetic_index
+ (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ gint idx);
+gboolean e_book_cache_cursor_set_sexp (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ const gchar *sexp,
+ GError **error);
+gboolean e_book_cache_cursor_calculate (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ gint *out_total,
+ gint *out_position,
+ GCancellable *cancellable,
+ GError **error);
+gint e_book_cache_cursor_compare_contact
+ (EBookCache *book_cache,
+ EBookCacheCursor *cursor,
+ EContact *contact,
+ gboolean *out_matches_sexp);
+
+G_END_DECLS
+
+#endif /* E_BOOK_CACHE_H */
diff --git a/src/addressbook/libedata-book/e-book-meta-backend.c
b/src/addressbook/libedata-book/e-book-meta-backend.c
new file mode 100644
index 0000000..bde296b
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-meta-backend.c
@@ -0,0 +1,3767 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * SECTION: e-book-meta-backend
+ * @include: libedata-book/libedata-book.h
+ * @short_description: An #EBookBackend descendant for book backends
+ *
+ * The #EBookMetaBackend is an abstract #EBookBackend descendant which
+ * aims to implement all evolution-data-server internals for the backend
+ * itself and lefts the backend do as minimum work as possible, like
+ * loading and saving contacts, listing available contacts and so on,
+ * thus the backend implementation can focus on things like converting
+ * (possibly) remote data into vCard objects and back.
+ *
+ * As the #EBookMetaBackend uses an #EBookCache, the offline support
+ * is provided by default.
+ *
+ * The structure is thread safe.
+ **/
+
+#include "evolution-data-server-config.h"
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+#include <glib/gstdio.h>
+
+#include "e-book-backend-sexp.h"
+#include "e-book-backend.h"
+#include "e-data-book-cursor-cache.h"
+#include "e-data-book-factory.h"
+
+#include "e-book-meta-backend.h"
+
+#define EBMB_KEY_SYNC_TAG "ebmb::sync-tag"
+#define EBMB_KEY_EVER_CONNECTED "ebmb::ever-connected"
+#define EBMB_KEY_CONNECTED_WRITABLE "ebmb::connected-writable"
+
+#define LOCAL_PREFIX "file://"
+
+struct _EBookMetaBackendPrivate {
+ GMutex connect_lock;
+ GMutex property_lock;
+ GError *create_cache_error;
+ EBookCache *cache;
+ ENamedParameters *last_credentials;
+ GHashTable *view_cancellables;
+ GCancellable *refresh_cancellable; /* Set when refreshing the content */
+ GCancellable *source_changed_cancellable; /* Set when processing source changed signal */
+ GCancellable *go_offline_cancellable; /* Set when going offline */
+ gboolean current_online_state; /* The only state of the internal structures;
+ used to detect false notifications on EBackend::online */
+ gulong source_changed_id;
+ gulong notify_online_id;
+ gulong revision_changed_id;
+ guint refresh_timeout_id;
+
+ gboolean refresh_after_authenticate;
+ gint ever_connected;
+ gint connected_writable;
+
+ /* Last successful connect data, for some extensions */
+ guint16 authentication_port;
+ gchar *authentication_host;
+ gchar *authentication_user;
+ gchar *authentication_method;
+ gchar *authentication_proxy_uid;
+ gchar *authentication_credential_name;
+ SoupURI *webdav_soup_uri;
+
+ GSList *cursors;
+};
+
+enum {
+ PROP_0,
+ PROP_CACHE
+};
+
+enum {
+ REFRESH_COMPLETED,
+ SOURCE_CHANGED,
+ LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL];
+
+G_DEFINE_ABSTRACT_TYPE (EBookMetaBackend, e_book_meta_backend, E_TYPE_BOOK_BACKEND)
+
+G_DEFINE_BOXED_TYPE (EBookMetaBackendInfo, e_book_meta_backend_info, e_book_meta_backend_info_copy,
e_book_meta_backend_info_free)
+
+static void ebmb_schedule_refresh (EBookMetaBackend *meta_backend);
+static void ebmb_schedule_source_changed (EBookMetaBackend *meta_backend);
+static void ebmb_schedule_go_offline (EBookMetaBackend *meta_backend);
+static gboolean ebmb_load_contact_wrapper_sync (EBookMetaBackend *meta_backend,
+ EBookCache *book_cache,
+ const gchar *uid,
+ const gchar *preloaded_object,
+ const gchar *preloaded_extra,
+ gchar **out_new_uid,
+ GCancellable *cancellable,
+ GError **error);
+static gboolean ebmb_save_contact_wrapper_sync (EBookMetaBackend *meta_backend,
+ EBookCache *book_cache,
+ gboolean overwrite_existing,
+ EConflictResolution conflict_resolution,
+ /* const */ EContact *in_contact,
+ const gchar *extra,
+ const gchar *orig_uid,
+ gboolean *out_requires_put,
+ gchar **out_new_uid,
+ gchar **out_new_extra,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * e_book_meta_backend_info_new:
+ * @uid: a contact UID; cannot be %NULL
+ * @revision: (nullable): the contact revision; can be %NULL
+ * @object: (nullable): the contact object as a vCard string; can be %NULL
+ * @extra: (nullable): extra backend-specific data; can be %NULL
+ *
+ * Creates a new #EBookMetaBackendInfo prefilled with the given values.
+ *
+ * Returns: (transfer full): A new #EBookMetaBackendInfo. Free it with
+ * e_book_meta_backend_info_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EBookMetaBackendInfo *
+e_book_meta_backend_info_new (const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ const gchar *extra)
+{
+ EBookMetaBackendInfo *info;
+
+ g_return_val_if_fail (uid != NULL, NULL);
+
+ info = g_new0 (EBookMetaBackendInfo, 1);
+ info->uid = g_strdup (uid);
+ info->revision = g_strdup (revision);
+ info->object = g_strdup (object);
+ info->extra = g_strdup (extra);
+
+ return info;
+}
+
+/**
+ * e_book_meta_backend_info_copy:
+ * @src: (nullable): a source EBookMetaBackendInfo to copy, or %NULL
+ *
+ * Returns: (transfer full): Copy of the given @src. Free it with
+ * e_book_meta_backend_info_free() when no longer needed.
+ * If the @src is %NULL, then returns %NULL as well.
+ *
+ * Since: 3.26
+ **/
+EBookMetaBackendInfo *
+e_book_meta_backend_info_copy (const EBookMetaBackendInfo *src)
+{
+ if (!src)
+ return NULL;
+
+ return e_book_meta_backend_info_new (src->uid, src->revision, src->object, src->extra);
+}
+
+/**
+ * e_book_meta_backend_info_free:
+ * @ptr: (nullable): an #EBookMetaBackendInfo
+ *
+ * Frees the @ptr structure, previously allocated with e_book_meta_backend_info_new()
+ * or e_book_meta_backend_info_copy().
+ *
+ * Since: 3.26
+ **/
+void
+e_book_meta_backend_info_free (gpointer ptr)
+{
+ EBookMetaBackendInfo *info = ptr;
+
+ if (info) {
+ g_free (info->uid);
+ g_free (info->revision);
+ g_free (info->object);
+ g_free (info->extra);
+ g_free (info);
+ }
+}
+
+/* Unref returned cancellable with g_object_unref(), when done with it */
+static GCancellable *
+ebmb_create_view_cancellable (EBookMetaBackend *meta_backend,
+ EDataBookView *view)
+{
+ GCancellable *cancellable;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), NULL);
+ g_return_val_if_fail (E_IS_DATA_BOOK_VIEW (view), NULL);
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ cancellable = g_cancellable_new ();
+ g_hash_table_insert (meta_backend->priv->view_cancellables, view, g_object_ref (cancellable));
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ return cancellable;
+}
+
+static GCancellable *
+ebmb_steal_view_cancellable (EBookMetaBackend *meta_backend,
+ EDataBookView *view)
+{
+ GCancellable *cancellable;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), NULL);
+ g_return_val_if_fail (E_IS_DATA_BOOK_VIEW (view), NULL);
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ cancellable = g_hash_table_lookup (meta_backend->priv->view_cancellables, view);
+ if (cancellable) {
+ g_object_ref (cancellable);
+ g_hash_table_remove (meta_backend->priv->view_cancellables, view);
+ }
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ return cancellable;
+}
+
+static void
+ebmb_update_connection_values (EBookMetaBackend *meta_backend)
+{
+ ESource *source;
+
+ g_return_if_fail (E_IS_BOOK_META_BACKEND (meta_backend));
+
+ source = e_backend_get_source (E_BACKEND (meta_backend));
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ meta_backend->priv->authentication_port = 0;
+ g_clear_pointer (&meta_backend->priv->authentication_host, g_free);
+ g_clear_pointer (&meta_backend->priv->authentication_user, g_free);
+ g_clear_pointer (&meta_backend->priv->authentication_method, g_free);
+ g_clear_pointer (&meta_backend->priv->authentication_proxy_uid, g_free);
+ g_clear_pointer (&meta_backend->priv->authentication_credential_name, g_free);
+ g_clear_pointer (&meta_backend->priv->webdav_soup_uri, (GDestroyNotify) soup_uri_free);
+
+ if (source && e_source_has_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
+ ESourceAuthentication *auth_extension;
+
+ auth_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
+
+ meta_backend->priv->authentication_port = e_source_authentication_get_port (auth_extension);
+ meta_backend->priv->authentication_host = e_source_authentication_dup_host (auth_extension);
+ meta_backend->priv->authentication_user = e_source_authentication_dup_user (auth_extension);
+ meta_backend->priv->authentication_method = e_source_authentication_dup_method
(auth_extension);
+ meta_backend->priv->authentication_proxy_uid = e_source_authentication_dup_proxy_uid
(auth_extension);
+ meta_backend->priv->authentication_credential_name =
e_source_authentication_dup_credential_name (auth_extension);
+ }
+
+ if (source && e_source_has_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND)) {
+ ESourceWebdav *webdav_extension;
+
+ webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+
+ meta_backend->priv->webdav_soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
+ }
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ e_book_meta_backend_set_ever_connected (meta_backend, TRUE);
+ e_book_meta_backend_set_connected_writable (meta_backend, e_book_backend_get_writable (E_BOOK_BACKEND
(meta_backend)));
+}
+
+static gboolean
+ebmb_connect_wrapper_sync (EBookMetaBackend *meta_backend,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ENamedParameters *credentials;
+ ESourceAuthenticationResult auth_result = E_SOURCE_AUTHENTICATION_UNKNOWN;
+ ESourceCredentialsReason creds_reason = E_SOURCE_CREDENTIALS_REASON_ERROR;
+ gchar *certificate_pem = NULL;
+ GTlsCertificateFlags certificate_errors = 0;
+ GError *local_error = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+
+ if (!e_backend_get_online (E_BACKEND (meta_backend))) {
+ g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_REPOSITORY_OFFLINE,
+ e_client_error_to_string (E_CLIENT_ERROR_REPOSITORY_OFFLINE));
+
+ return FALSE;
+ }
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+ credentials = e_named_parameters_new_clone (meta_backend->priv->last_credentials);
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ g_mutex_lock (&meta_backend->priv->connect_lock);
+ if (e_book_meta_backend_connect_sync (meta_backend, credentials, &auth_result, &certificate_pem,
&certificate_errors,
+ cancellable, &local_error)) {
+ ebmb_update_connection_values (meta_backend);
+ g_mutex_unlock (&meta_backend->priv->connect_lock);
+ e_named_parameters_free (credentials);
+
+ return TRUE;
+ }
+
+ g_mutex_unlock (&meta_backend->priv->connect_lock);
+
+ e_named_parameters_free (credentials);
+
+ g_warn_if_fail (auth_result != E_SOURCE_AUTHENTICATION_ACCEPTED);
+
+ switch (auth_result) {
+ case E_SOURCE_AUTHENTICATION_UNKNOWN:
+ if (local_error)
+ g_propagate_error (error, local_error);
+ g_free (certificate_pem);
+ return FALSE;
+ case E_SOURCE_AUTHENTICATION_ERROR:
+ creds_reason = E_SOURCE_CREDENTIALS_REASON_ERROR;
+ break;
+ case E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED:
+ creds_reason = E_SOURCE_CREDENTIALS_REASON_SSL_FAILED;
+ break;
+ case E_SOURCE_AUTHENTICATION_ACCEPTED:
+ g_warn_if_reached ();
+ break;
+ case E_SOURCE_AUTHENTICATION_REJECTED:
+ creds_reason = E_SOURCE_CREDENTIALS_REASON_REJECTED;
+ break;
+ case E_SOURCE_AUTHENTICATION_REQUIRED:
+ creds_reason = E_SOURCE_CREDENTIALS_REASON_REQUIRED;
+ break;
+ }
+
+ e_backend_schedule_credentials_required (E_BACKEND (meta_backend), creds_reason, certificate_pem,
certificate_errors,
+ local_error, cancellable, G_STRFUNC);
+
+ g_clear_error (&local_error);
+ g_free (certificate_pem);
+
+ return FALSE;
+}
+
+static gboolean
+ebmb_gather_locally_cached_objects_cb (EBookCache *book_cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ const gchar *extra,
+ EOfflineState offline_state,
+ gpointer user_data)
+{
+ GHashTable *locally_cached = user_data;
+
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (locally_cached != NULL, FALSE);
+
+ if (offline_state == E_OFFLINE_STATE_SYNCED) {
+ g_hash_table_insert (locally_cached,
+ g_strdup (uid),
+ g_strdup (revision));
+ }
+
+ return TRUE;
+}
+
+static gboolean
+ebmb_get_changes_sync (EBookMetaBackend *meta_backend,
+ const gchar *last_sync_tag,
+ gboolean is_repeat,
+ gchar **out_new_sync_tag,
+ gboolean *out_repeat,
+ GSList **out_created_objects,
+ GSList **out_modified_objects,
+ GSList **out_removed_objects,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GHashTable *locally_cached; /* EContactId * ~> gchar *revision */
+ GHashTableIter iter;
+ GSList *existing_objects = NULL, *link;
+ EBookCache *book_cache;
+ gpointer key, value;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (out_created_objects, FALSE);
+ g_return_val_if_fail (out_modified_objects, FALSE);
+ g_return_val_if_fail (out_removed_objects, FALSE);
+
+ *out_created_objects = NULL;
+ *out_modified_objects = NULL;
+ *out_removed_objects = NULL;
+
+ if (!e_backend_get_online (E_BACKEND (meta_backend)))
+ return TRUE;
+
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
+ g_return_val_if_fail (book_cache != NULL, FALSE);
+
+ if (!ebmb_connect_wrapper_sync (meta_backend, cancellable, error) ||
+ !e_book_meta_backend_list_existing_sync (meta_backend, out_new_sync_tag, &existing_objects,
cancellable, error)) {
+ g_object_unref (book_cache);
+ return FALSE;
+ }
+
+ locally_cached = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+
+ g_warn_if_fail (e_book_cache_search_with_callback (book_cache, NULL,
+ ebmb_gather_locally_cached_objects_cb, locally_cached, cancellable, error));
+
+ for (link = existing_objects; link; link = g_slist_next (link)) {
+ EBookMetaBackendInfo *nfo = link->data;
+
+ if (!nfo)
+ continue;
+
+ if (!g_hash_table_contains (locally_cached, nfo->uid)) {
+ link->data = NULL;
+
+ *out_created_objects = g_slist_prepend (*out_created_objects, nfo);
+ } else {
+ const gchar *local_revision = g_hash_table_lookup (locally_cached, nfo->uid);
+
+ if (g_strcmp0 (local_revision, nfo->revision) != 0) {
+ link->data = NULL;
+
+ *out_modified_objects = g_slist_prepend (*out_modified_objects, nfo);
+ }
+
+ g_hash_table_remove (locally_cached, nfo->uid);
+ }
+ }
+
+ /* What left in the hash table is removed from the remote side */
+ g_hash_table_iter_init (&iter, locally_cached);
+ while (g_hash_table_iter_next (&iter, &key, &value)) {
+ const gchar *uid = key;
+ const gchar *revision = value;
+ EBookMetaBackendInfo *nfo;
+
+ if (!uid) {
+ g_warn_if_reached ();
+ continue;
+ }
+
+ nfo = e_book_meta_backend_info_new (uid, revision, NULL, NULL);
+ *out_removed_objects = g_slist_prepend (*out_removed_objects, nfo);
+ }
+
+ g_slist_free_full (existing_objects, e_book_meta_backend_info_free);
+ g_hash_table_destroy (locally_cached);
+ g_object_unref (book_cache);
+
+ *out_created_objects = g_slist_reverse (*out_created_objects);
+ *out_modified_objects = g_slist_reverse (*out_modified_objects);
+ *out_removed_objects = g_slist_reverse (*out_removed_objects);
+
+ return TRUE;
+}
+
+static gboolean
+ebmb_search_sync (EBookMetaBackend *meta_backend,
+ const gchar *expr,
+ gboolean meta_contact,
+ GSList **out_contacts,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookCache *book_cache;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (out_contacts != NULL, FALSE);
+
+ *out_contacts = NULL;
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
+
+ g_return_val_if_fail (book_cache != NULL, FALSE);
+
+ success = e_book_cache_search (book_cache, expr, meta_contact, out_contacts, cancellable, error);
+
+ if (success) {
+ GSList *link;
+
+ for (link = *out_contacts; link; link = g_slist_next (link)) {
+ EBookCacheSearchData *search_data = link->data;
+ EContact *contact = NULL;
+
+ if (search_data) {
+ contact = e_contact_new_from_vcard_with_uid (search_data->vcard,
search_data->uid);
+ e_book_cache_search_data_free (search_data);
+ }
+
+ link->data = contact;
+ }
+ }
+
+ g_object_unref (book_cache);
+
+ return success;
+}
+
+static gboolean
+ebmb_search_uids_sync (EBookMetaBackend *meta_backend,
+ const gchar *expr,
+ GSList **out_uids,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookCache *book_cache;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (out_uids != NULL, FALSE);
+
+ *out_uids = NULL;
+
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
+ g_return_val_if_fail (book_cache != NULL, FALSE);
+
+ success = e_book_cache_search_uids (book_cache, expr, out_uids, cancellable, error);
+
+ g_object_unref (book_cache);
+
+ return success;
+}
+
+static gboolean
+ebmb_requires_reconnect (EBookMetaBackend *meta_backend)
+{
+ ESource *source;
+ gboolean requires = FALSE;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+
+ source = e_backend_get_source (E_BACKEND (meta_backend));
+ g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ if (e_source_has_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
+ ESourceAuthentication *auth_extension;
+
+ auth_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
+
+ e_source_extension_property_lock (E_SOURCE_EXTENSION (auth_extension));
+
+ requires = meta_backend->priv->authentication_port != e_source_authentication_get_port
(auth_extension) ||
+ g_strcmp0 (meta_backend->priv->authentication_host, e_source_authentication_get_host
(auth_extension)) != 0 ||
+ g_strcmp0 (meta_backend->priv->authentication_user, e_source_authentication_get_user
(auth_extension)) != 0 ||
+ g_strcmp0 (meta_backend->priv->authentication_method,
e_source_authentication_get_method (auth_extension)) != 0 ||
+ g_strcmp0 (meta_backend->priv->authentication_proxy_uid,
e_source_authentication_get_proxy_uid (auth_extension)) != 0 ||
+ g_strcmp0 (meta_backend->priv->authentication_credential_name,
e_source_authentication_get_credential_name (auth_extension)) != 0;
+
+ e_source_extension_property_unlock (E_SOURCE_EXTENSION (auth_extension));
+ }
+
+ if (!requires && e_source_has_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND)) {
+ ESourceWebdav *webdav_extension;
+ SoupURI *soup_uri;
+
+ webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+ soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
+
+ requires = (!meta_backend->priv->webdav_soup_uri && soup_uri) ||
+ (soup_uri && meta_backend->priv->webdav_soup_uri &&
+ !soup_uri_equal (meta_backend->priv->webdav_soup_uri, soup_uri));
+
+ if (soup_uri)
+ soup_uri_free (soup_uri);
+ }
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ return requires;
+}
+
+static GSList * /* gchar * */
+ebmb_gather_photos_local_filenames (EBookMetaBackend *meta_backend,
+ EContact *contact)
+{
+ EBookCache *book_cache;
+ GList *attributes, *link;
+ GSList *filenames = NULL;
+ gchar *cache_path;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), NULL);
+ g_return_val_if_fail (E_IS_CONTACT (contact), NULL);
+
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
+ g_return_val_if_fail (book_cache != NULL, NULL);
+
+ cache_path = g_path_get_dirname (e_cache_get_filename (E_CACHE (book_cache)));
+
+ g_object_unref (book_cache);
+
+ attributes = e_vcard_get_attributes (E_VCARD (contact));
+
+ for (link = attributes; link; link = g_list_next (link)) {
+ EVCardAttribute *attr = link->data;
+ const gchar *attr_name;
+ GList *values;
+
+ attr_name = e_vcard_attribute_get_name (attr);
+ if (!attr_name || (
+ g_ascii_strcasecmp (attr_name, EVC_PHOTO) != 0 &&
+ g_ascii_strcasecmp (attr_name, EVC_LOGO) != 0)) {
+ continue;
+ }
+
+ values = e_vcard_attribute_get_param (attr, EVC_VALUE);
+ if (values && g_ascii_strcasecmp (values->data, "uri") == 0) {
+ const gchar *url;
+
+ url = e_vcard_attribute_get_value (attr);
+ if (url && g_str_has_prefix (url, LOCAL_PREFIX)) {
+ gchar *filename;
+
+ filename = g_filename_from_uri (url, NULL, NULL);
+ if (filename && g_str_has_prefix (filename, cache_path))
+ filenames = g_slist_prepend (filenames, filename);
+ else
+ g_free (filename);
+ }
+ }
+ }
+
+ g_free (cache_path);
+
+ return filenames;
+}
+
+static void
+ebmb_start_view_thread_func (EBookBackend *book_backend,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EDataBookView *view = user_data;
+ EBookBackendSExp *sexp;
+ GSList *contacts = NULL;
+ const gchar *expr = NULL;
+ gboolean meta_contact = FALSE;
+ GHashTable *fields_of_interest;
+ GError *local_error = NULL;
+
+ g_return_if_fail (E_IS_BOOK_META_BACKEND (book_backend));
+ g_return_if_fail (E_IS_DATA_BOOK_VIEW (view));
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error))
+ return;
+
+ /* Fill the view with known (locally stored) contacts satisfying the expression */
+ sexp = e_data_book_view_get_sexp (view);
+ if (sexp)
+ expr = e_book_backend_sexp_text (sexp);
+
+ fields_of_interest = e_data_book_view_get_fields_of_interest (view);
+ if (fields_of_interest && g_hash_table_size (fields_of_interest) == 2) {
+ GHashTableIter iter;
+ gpointer key, value;
+
+ meta_contact = TRUE;
+
+ g_hash_table_iter_init (&iter, fields_of_interest);
+ while (g_hash_table_iter_next (&iter, &key, &value)) {
+ const gchar *field_name = key;
+ EContactField field = e_contact_field_id (field_name);
+
+ if (field != E_CONTACT_UID &&
+ field != E_CONTACT_REV) {
+ meta_contact = FALSE;
+ break;
+ }
+ }
+ }
+
+ if (e_book_meta_backend_search_sync (E_BOOK_META_BACKEND (book_backend), expr, meta_contact,
&contacts, cancellable, &local_error) && contacts) {
+ if (!g_cancellable_is_cancelled (cancellable)) {
+ GSList *link;
+
+ for (link = contacts; link; link = g_slist_next (link)) {
+ EContact *contact = link->data;
+ gchar *vcard;
+
+ if (!contact)
+ continue;
+
+ vcard = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
+ e_data_book_view_notify_update_prefiltered_vcard (view,
+ e_contact_get_const (contact, E_CONTACT_UID),
+ vcard);
+ }
+ }
+
+ g_slist_free_full (contacts, g_object_unref);
+ }
+
+ e_data_book_view_notify_complete (view, local_error);
+
+ g_clear_error (&local_error);
+}
+
+static gboolean
+ebmb_upload_local_changes_sync (EBookMetaBackend *meta_backend,
+ EBookCache *book_cache,
+ EConflictResolution conflict_resolution,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GSList *offline_changes, *link;
+ GHashTable *covered_uids;
+ ECache *cache;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), FALSE);
+
+ cache = E_CACHE (book_cache);
+ covered_uids = g_hash_table_new (g_str_hash, g_str_equal);
+
+ offline_changes = e_cache_get_offline_changes (cache, cancellable, error);
+ for (link = offline_changes; link && success; link = g_slist_next (link)) {
+ ECacheOfflineChange *change = link->data;
+ gchar *extra = NULL;
+
+ success = !g_cancellable_set_error_if_cancelled (cancellable, error);
+ if (!success)
+ break;
+
+ if (!change || g_hash_table_contains (covered_uids, change->uid))
+ continue;
+
+ g_hash_table_insert (covered_uids, change->uid, NULL);
+
+ if (!e_book_cache_get_contact_extra (book_cache, change->uid, &extra, cancellable, NULL))
+ extra = NULL;
+
+ if (change->state == E_OFFLINE_STATE_LOCALLY_CREATED ||
+ change->state == E_OFFLINE_STATE_LOCALLY_MODIFIED) {
+ EContact *contact = NULL;
+
+ success = e_book_cache_get_contact (book_cache, change->uid, FALSE, &contact,
cancellable, error);
+ if (success) {
+ success = ebmb_save_contact_wrapper_sync (meta_backend, book_cache,
+ change->state == E_OFFLINE_STATE_LOCALLY_MODIFIED,
+ conflict_resolution, contact, extra, change->uid, NULL, NULL, NULL,
cancellable, error);
+ }
+
+ g_clear_object (&contact);
+ } else if (change->state == E_OFFLINE_STATE_LOCALLY_DELETED) {
+ GError *local_error = NULL;
+
+ success = e_book_meta_backend_remove_contact_sync (meta_backend, conflict_resolution,
+ change->uid, extra, change->object, cancellable, &local_error);
+
+ if (!success) {
+ if (g_error_matches (local_error, E_DATA_BOOK_ERROR,
E_DATA_BOOK_STATUS_CONTACT_NOT_FOUND)) {
+ g_clear_error (&local_error);
+ success = TRUE;
+ } else if (local_error) {
+ g_propagate_error (error, local_error);
+ }
+ }
+ } else {
+ g_warn_if_reached ();
+ }
+
+ g_free (extra);
+ }
+
+ g_slist_free_full (offline_changes, e_cache_offline_change_free);
+ g_hash_table_destroy (covered_uids);
+
+ if (success)
+ success = e_cache_clear_offline_changes (cache, cancellable, error);
+
+ return success;
+}
+
+static void
+ebmb_foreach_cursor (EBookMetaBackend *meta_backend,
+ EContact *contact,
+ void (* func) (EDataBookCursor *cursor, EContact *contact))
+{
+ GSList *link;
+
+ g_return_if_fail (E_IS_BOOK_META_BACKEND (meta_backend));
+ g_return_if_fail (func != NULL);
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ for (link = meta_backend->priv->cursors; link; link = g_slist_next (link)) {
+ EDataBookCursor *cursor = link->data;
+
+ func (cursor, contact);
+ }
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+}
+
+static gboolean
+ebmb_maybe_remove_from_cache (EBookMetaBackend *meta_backend,
+ EBookCache *book_cache,
+ ECacheOfflineFlag offline_flag,
+ const gchar *uid,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookBackend *book_backend;
+ EContact *contact = NULL;
+ GSList *local_photos, *link;
+ GError *local_error = NULL;
+
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ if (!e_book_cache_get_contact (book_cache, uid, FALSE, &contact, cancellable, &local_error)) {
+ if (g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND)) {
+ g_clear_error (&local_error);
+ return TRUE;
+ }
+
+ g_propagate_error (error, local_error);
+ return FALSE;
+ }
+
+ book_backend = E_BOOK_BACKEND (meta_backend);
+
+ if (!e_book_cache_remove_contact (book_cache, uid, offline_flag, cancellable, error)) {
+ g_object_unref (contact);
+ return FALSE;
+ }
+
+ local_photos = ebmb_gather_photos_local_filenames (meta_backend, contact);
+ for (link = local_photos; link; link = g_slist_next (link)) {
+ const gchar *filename = link->data;
+
+ if (filename && g_unlink (filename) == -1) {
+ /* Ignore these errors */
+ }
+ }
+
+ g_slist_free_full (local_photos, g_free);
+
+ e_book_backend_notify_remove (book_backend, uid);
+
+ ebmb_foreach_cursor (meta_backend, contact, e_data_book_cursor_contact_removed);
+
+ g_object_unref (contact);
+
+ return TRUE;
+}
+
+static void
+ebmb_refresh_thread_func (EBookBackend *book_backend,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackend *meta_backend;
+ EBookCache *book_cache;
+ gboolean success, repeat = TRUE, is_repeat = FALSE;
+ GString *invalid_objects = NULL;
+
+ g_return_if_fail (E_IS_BOOK_META_BACKEND (book_backend));
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error))
+ goto done;
+
+ meta_backend = E_BOOK_META_BACKEND (book_backend);
+
+ if (!e_backend_get_online (E_BACKEND (meta_backend)) ||
+ !ebmb_connect_wrapper_sync (meta_backend, cancellable, NULL)) {
+ /* Ignore connection errors here */
+ g_mutex_lock (&meta_backend->priv->property_lock);
+ meta_backend->priv->refresh_after_authenticate = TRUE;
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+ goto done;
+ }
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+ meta_backend->priv->refresh_after_authenticate = FALSE;
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
+ if (!book_cache) {
+ g_warn_if_reached ();
+ goto done;
+ }
+
+ success = ebmb_upload_local_changes_sync (meta_backend, book_cache, E_CONFLICT_RESOLUTION_FAIL,
cancellable, error);
+
+ while (repeat && success &&
+ !g_cancellable_set_error_if_cancelled (cancellable, error)) {
+ GSList *created_objects = NULL, *modified_objects = NULL, *removed_objects = NULL, *link;
+ gchar *last_sync_tag, *new_sync_tag = NULL;
+
+ repeat = FALSE;
+
+ last_sync_tag = e_cache_dup_key (E_CACHE (book_cache), EBMB_KEY_SYNC_TAG, NULL);
+ if (last_sync_tag && !*last_sync_tag) {
+ g_free (last_sync_tag);
+ last_sync_tag = NULL;
+ }
+
+ success = e_book_meta_backend_get_changes_sync (meta_backend, last_sync_tag, is_repeat,
&new_sync_tag, &repeat,
+ &created_objects, &modified_objects, &removed_objects, cancellable, error);
+
+ if (success) {
+ GHashTable *covered_uids;
+
+ covered_uids = g_hash_table_new (g_str_hash, g_str_equal);
+
+ /* Removed objects first */
+ for (link = removed_objects; link && success; link = g_slist_next (link)) {
+ EBookMetaBackendInfo *nfo = link->data;
+
+ if (!nfo) {
+ g_warn_if_reached ();
+ continue;
+ }
+
+ success = ebmb_maybe_remove_from_cache (meta_backend, book_cache,
E_CACHE_IS_ONLINE, nfo->uid, cancellable, error);
+ }
+
+ /* Then modified objects */
+ for (link = modified_objects; link && success; link = g_slist_next (link)) {
+ EBookMetaBackendInfo *nfo = link->data;
+ GError *local_error = NULL;
+
+ if (!nfo || !nfo->uid) {
+ g_warn_if_reached ();
+ continue;
+ }
+
+ if (!*nfo->uid ||
+ g_hash_table_contains (covered_uids, nfo->uid))
+ continue;
+
+ g_hash_table_insert (covered_uids, nfo->uid, NULL);
+
+ success = ebmb_load_contact_wrapper_sync (meta_backend, book_cache, nfo->uid,
nfo->object, nfo->extra, NULL, cancellable, &local_error);
+
+ /* Do not stop on invalid objects, just notify about them later, and load as
many as possible */
+ if (!success && g_error_matches (local_error, E_DATA_BOOK_ERROR,
E_DATA_BOOK_STATUS_INVALID_ARG)) {
+ if (!invalid_objects) {
+ invalid_objects = g_string_new (local_error->message);
+ } else {
+ g_string_append_c (invalid_objects, '\n');
+ g_string_append (invalid_objects, local_error->message);
+ }
+ g_clear_error (&local_error);
+ success = TRUE;
+ } else if (local_error) {
+ g_propagate_error (error, local_error);
+ }
+ }
+
+ g_hash_table_remove_all (covered_uids);
+
+ /* Finally created objects */
+ for (link = created_objects; link && success; link = g_slist_next (link)) {
+ EBookMetaBackendInfo *nfo = link->data;
+ GError *local_error = NULL;
+
+ if (!nfo || !nfo->uid) {
+ g_warn_if_reached ();
+ continue;
+ }
+
+ if (!*nfo->uid)
+ continue;
+
+ success = ebmb_load_contact_wrapper_sync (meta_backend, book_cache, nfo->uid,
nfo->object, nfo->extra, NULL, cancellable, &local_error);
+
+ /* Do not stop on invalid objects, just notify about them later, and load as
many as possible */
+ if (!success && g_error_matches (local_error, E_DATA_BOOK_ERROR,
E_DATA_BOOK_STATUS_INVALID_ARG)) {
+ if (!invalid_objects) {
+ invalid_objects = g_string_new (local_error->message);
+ } else {
+ g_string_append_c (invalid_objects, '\n');
+ g_string_append (invalid_objects, local_error->message);
+ }
+ g_clear_error (&local_error);
+ success = TRUE;
+ } else if (local_error) {
+ g_propagate_error (error, local_error);
+ }
+ }
+
+ g_hash_table_destroy (covered_uids);
+ }
+
+ if (success && new_sync_tag)
+ e_cache_set_key (E_CACHE (book_cache), EBMB_KEY_SYNC_TAG, new_sync_tag, NULL);
+
+ g_slist_free_full (created_objects, e_book_meta_backend_info_free);
+ g_slist_free_full (modified_objects, e_book_meta_backend_info_free);
+ g_slist_free_full (removed_objects, e_book_meta_backend_info_free);
+ g_free (last_sync_tag);
+ g_free (new_sync_tag);
+
+ is_repeat = TRUE;
+ }
+
+ g_object_unref (book_cache);
+
+ done:
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ if (meta_backend->priv->refresh_cancellable == cancellable)
+ g_clear_object (&meta_backend->priv->refresh_cancellable);
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ if (invalid_objects) {
+ e_book_backend_notify_error (E_BOOK_BACKEND (meta_backend), invalid_objects->str);
+
+ g_string_free (invalid_objects, TRUE);
+ }
+
+ g_signal_emit (meta_backend, signals[REFRESH_COMPLETED], 0, NULL);
+}
+
+static void
+ebmb_source_refresh_timeout_cb (ESource *source,
+ gpointer user_data)
+{
+ GWeakRef *weak_ref = user_data;
+ EBookMetaBackend *meta_backend;
+
+ g_return_if_fail (weak_ref != NULL);
+
+ meta_backend = g_weak_ref_get (weak_ref);
+ if (meta_backend) {
+ ebmb_schedule_refresh (meta_backend);
+ g_object_unref (meta_backend);
+ }
+}
+
+static void
+ebmb_source_changed_thread_func (EBookBackend *book_backend,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackend *meta_backend;
+
+ g_return_if_fail (E_IS_BOOK_META_BACKEND (book_backend));
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error))
+ return;
+
+ meta_backend = E_BOOK_META_BACKEND (book_backend);
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+ if (!meta_backend->priv->refresh_timeout_id) {
+ ESource *source = e_backend_get_source (E_BACKEND (meta_backend));
+
+ if (e_source_has_extension (source, E_SOURCE_EXTENSION_REFRESH)) {
+ meta_backend->priv->refresh_timeout_id = e_source_refresh_add_timeout (source, NULL,
+ ebmb_source_refresh_timeout_cb, e_weak_ref_new (meta_backend),
(GDestroyNotify) e_weak_ref_free);
+ }
+ }
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ g_signal_emit (meta_backend, signals[SOURCE_CHANGED], 0, NULL);
+
+ if (e_backend_get_online (E_BACKEND (meta_backend)) &&
+ e_book_meta_backend_requires_reconnect (meta_backend)) {
+ gboolean can_refresh;
+
+ g_mutex_lock (&meta_backend->priv->connect_lock);
+ can_refresh = e_book_meta_backend_disconnect_sync (meta_backend, cancellable, error);
+ g_mutex_unlock (&meta_backend->priv->connect_lock);
+
+ if (can_refresh)
+ ebmb_schedule_refresh (meta_backend);
+ }
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ if (meta_backend->priv->source_changed_cancellable == cancellable)
+ g_clear_object (&meta_backend->priv->source_changed_cancellable);
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+}
+
+static void
+ebmb_go_offline_thread_func (EBookBackend *book_backend,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackend *meta_backend;
+
+ g_return_if_fail (E_IS_BOOK_META_BACKEND (book_backend));
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error))
+ return;
+
+ meta_backend = E_BOOK_META_BACKEND (book_backend);
+
+ g_mutex_lock (&meta_backend->priv->connect_lock);
+ e_book_meta_backend_disconnect_sync (meta_backend, cancellable, error);
+ g_mutex_unlock (&meta_backend->priv->connect_lock);
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ if (meta_backend->priv->go_offline_cancellable == cancellable)
+ g_clear_object (&meta_backend->priv->go_offline_cancellable);
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+}
+
+static gboolean
+ebmb_put_contact (EBookMetaBackend *meta_backend,
+ EBookCache *book_cache,
+ ECacheOfflineFlag offline_flag,
+ EContact *contact,
+ const gchar *extra,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EContact *existing_contact = NULL;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+
+ success = e_book_meta_backend_store_inline_photos_sync (meta_backend, contact, cancellable, error);
+
+ if (success && e_book_cache_get_contact (book_cache,
+ e_contact_get_const (contact, E_CONTACT_UID), FALSE, &existing_contact, cancellable, NULL)) {
+ GSList *old_photos, *new_photos, *link;
+
+ old_photos = ebmb_gather_photos_local_filenames (meta_backend, existing_contact);
+ if (old_photos) {
+ GHashTable *photos_hash;
+
+ photos_hash = g_hash_table_new (g_str_hash, g_str_equal);
+
+ new_photos = ebmb_gather_photos_local_filenames (meta_backend, contact);
+
+ for (link = new_photos; link; link = g_slist_next (link)) {
+ const gchar *filename = link->data;
+
+ if (filename)
+ g_hash_table_insert (photos_hash, (gpointer) filename, NULL);
+ }
+
+ for (link = old_photos; link; link = g_slist_next (link)) {
+ const gchar *filename = link->data;
+
+ if (filename && !g_hash_table_contains (photos_hash, filename)) {
+ if (g_unlink (filename) == -1) {
+ /* Ignore these errors */
+ }
+ }
+ }
+
+ g_slist_free_full (old_photos, g_free);
+ g_slist_free_full (new_photos, g_free);
+ g_hash_table_destroy (photos_hash);
+ }
+ }
+
+ success = success && e_book_cache_put_contact (book_cache, contact, extra, offline_flag, cancellable,
error);
+
+ if (success)
+ e_book_backend_notify_update (E_BOOK_BACKEND (meta_backend), contact);
+
+ g_clear_object (&existing_contact);
+
+ return success;
+}
+
+static gboolean
+ebmb_load_contact_wrapper_sync (EBookMetaBackend *meta_backend,
+ EBookCache *book_cache,
+ const gchar *uid,
+ const gchar *preloaded_object,
+ const gchar *preloaded_extra,
+ gchar **out_new_uid,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECacheOfflineFlag offline_flag = E_CACHE_IS_ONLINE;
+ EContact *contact = NULL;
+ gchar *extra = NULL;
+ gboolean success = TRUE;
+
+ if (preloaded_object && *preloaded_object) {
+ contact = e_contact_new_from_vcard_with_uid (preloaded_object, uid);
+ if (!contact) {
+ g_propagate_error (error, e_data_book_create_error_fmt
(E_DATA_BOOK_STATUS_INVALID_ARG, _("Preloaded object for UID “%s” is invalid"), uid));
+ return FALSE;
+ }
+ } else if (!e_book_meta_backend_load_contact_sync (meta_backend, uid, preloaded_extra, &contact,
&extra, cancellable, error)) {
+ g_free (extra);
+ return FALSE;
+ } else if (!contact) {
+ g_propagate_error (error, e_data_book_create_error_fmt (E_DATA_BOOK_STATUS_INVALID_ARG,
_("Received object for UID “%s” is invalid"), uid));
+ g_free (extra);
+ return FALSE;
+ }
+
+ success = ebmb_put_contact (meta_backend, book_cache, offline_flag,
+ contact, extra ? extra : preloaded_extra, cancellable, error);
+
+ if (success && out_new_uid)
+ *out_new_uid = e_contact_get (contact, E_CONTACT_UID);
+
+ g_object_unref (contact);
+ g_free (extra);
+
+ return success;
+}
+
+static gboolean
+ebmb_save_contact_wrapper_sync (EBookMetaBackend *meta_backend,
+ EBookCache *book_cache,
+ gboolean overwrite_existing,
+ EConflictResolution conflict_resolution,
+ /* const */ EContact *in_contact,
+ const gchar *extra,
+ const gchar *orig_uid,
+ gboolean *out_requires_put,
+ gchar **out_new_uid,
+ gchar **out_new_extra,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EContact *contact;
+ gchar *new_uid = NULL, *new_extra = NULL;
+ gboolean success = TRUE;
+
+ if (out_requires_put)
+ *out_requires_put = TRUE;
+
+ if (out_new_uid)
+ *out_new_uid = NULL;
+
+ contact = e_contact_duplicate (in_contact);
+
+ success = e_book_meta_backend_inline_local_photos_sync (meta_backend, contact, cancellable, error);
+
+ success = success && e_book_meta_backend_save_contact_sync (meta_backend, overwrite_existing,
conflict_resolution,
+ contact, extra, &new_uid, &new_extra, cancellable, error);
+
+ if (success && new_uid && *new_uid) {
+ gchar *loaded_uid = NULL;
+
+ success = ebmb_load_contact_wrapper_sync (meta_backend, book_cache, new_uid, NULL,
+ new_extra ? new_extra : extra, &loaded_uid, cancellable, error);
+
+ if (success && g_strcmp0 (loaded_uid, orig_uid) != 0)
+ success = ebmb_maybe_remove_from_cache (meta_backend, book_cache, E_CACHE_IS_ONLINE,
orig_uid, cancellable, error);
+
+ if (success && out_new_uid)
+ *out_new_uid = loaded_uid;
+ else
+ g_free (loaded_uid);
+
+ if (out_requires_put)
+ *out_requires_put = FALSE;
+ }
+
+ g_free (new_uid);
+
+ if (success && out_new_extra)
+ *out_new_extra = new_extra;
+ else
+ g_free (new_extra);
+ g_object_unref (contact);
+
+ return success;
+}
+
+static gchar *
+ebmb_get_backend_property (EBookBackend *book_backend,
+ const gchar *prop_name)
+{
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), NULL);
+ g_return_val_if_fail (prop_name != NULL, NULL);
+
+ if (g_str_equal (prop_name, BOOK_BACKEND_PROPERTY_REVISION)) {
+ EBookCache *book_cache;
+ gchar *revision = NULL;
+
+ book_cache = e_book_meta_backend_ref_cache (E_BOOK_META_BACKEND (book_backend));
+ if (book_cache) {
+ revision = e_cache_dup_revision (E_CACHE (book_cache));
+ g_object_unref (book_cache);
+ } else {
+ g_warn_if_reached ();
+ }
+
+ return revision;
+ } else if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
+ return g_strdup (e_book_meta_backend_get_capabilities (E_BOOK_META_BACKEND (book_backend)));
+ } else if (g_str_equal (prop_name, BOOK_BACKEND_PROPERTY_REQUIRED_FIELDS)) {
+ return g_strdup (e_contact_field_name (E_CONTACT_FILE_AS));
+ } else if (g_str_equal (prop_name, BOOK_BACKEND_PROPERTY_SUPPORTED_FIELDS)) {
+ GString *fields;
+ gint ii;
+
+ fields = g_string_sized_new (1024);
+
+ /* Claim to support everything by default */
+ for (ii = 1; ii < E_CONTACT_FIELD_LAST; ii++) {
+ if (fields->len > 0)
+ g_string_append_c (fields, ',');
+ g_string_append (fields, e_contact_field_name (ii));
+ }
+
+ return g_string_free (fields, FALSE);
+ }
+
+ /* Chain up to parent's method. */
+ return E_BOOK_BACKEND_CLASS (e_book_meta_backend_parent_class)->get_backend_property (book_backend,
prop_name);
+}
+
+static gboolean
+ebmb_open_sync (EBookBackend *book_backend,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackend *meta_backend;
+ ESource *source;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), FALSE);
+
+ if (e_book_backend_is_opened (book_backend))
+ return TRUE;
+
+ meta_backend = E_BOOK_META_BACKEND (book_backend);
+ if (meta_backend->priv->create_cache_error) {
+ g_propagate_error (error, meta_backend->priv->create_cache_error);
+ meta_backend->priv->create_cache_error = NULL;
+ return FALSE;
+ }
+
+ source = e_backend_get_source (E_BACKEND (book_backend));
+
+ if (!meta_backend->priv->source_changed_id) {
+ meta_backend->priv->source_changed_id = g_signal_connect_swapped (source, "changed",
+ G_CALLBACK (ebmb_schedule_source_changed), meta_backend);
+ }
+
+ if (e_source_has_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND)) {
+ ESourceWebdav *webdav_extension;
+
+ webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+ e_source_webdav_unset_temporary_ssl_trust (webdav_extension);
+ }
+
+ if (e_book_meta_backend_get_ever_connected (meta_backend)) {
+ e_book_backend_set_writable (E_BOOK_BACKEND (meta_backend),
+ e_book_meta_backend_get_connected_writable (meta_backend));
+ } else {
+ if (!ebmb_connect_wrapper_sync (meta_backend, cancellable, error)) {
+ g_mutex_lock (&meta_backend->priv->property_lock);
+ meta_backend->priv->refresh_after_authenticate = TRUE;
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ return FALSE;
+ }
+ }
+
+ ebmb_schedule_refresh (E_BOOK_META_BACKEND (book_backend));
+
+ return TRUE;
+}
+
+static gboolean
+ebmb_refresh_sync (EBookBackend *book_backend,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackend *meta_backend;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), FALSE);
+
+ meta_backend = E_BOOK_META_BACKEND (book_backend);
+
+ if (!e_backend_get_online (E_BACKEND (book_backend)))
+ return TRUE;
+
+ success = ebmb_connect_wrapper_sync (meta_backend, cancellable, error);
+
+ if (success)
+ ebmb_schedule_refresh (meta_backend);
+
+ return success;
+}
+
+/* Copied from e_cal_component_gen_uid() */
+static gchar *
+e_book_meta_backend_gen_uid (void)
+{
+ gchar *iso, *ret;
+ static const gchar *hostname;
+ time_t t = time (NULL);
+ struct tm stm;
+ static gint serial;
+
+ if (!hostname) {
+#ifndef G_OS_WIN32
+ static gchar buffer[512];
+
+ if ((gethostname (buffer, sizeof (buffer) - 1) == 0) &&
+ (buffer[0] != 0))
+ hostname = buffer;
+ else
+ hostname = "localhost";
+#else
+ hostname = g_get_host_name ();
+#endif
+ }
+
+#ifdef G_OS_WIN32
+#ifdef gmtime_r
+#undef gmtime_r
+#endif
+
+/* The gmtime() in Microsoft's C library is MT-safe */
+#define gmtime_r(tp,tmp) (gmtime(tp)?(*(tmp)=*gmtime(tp),(tmp)):0)
+#endif
+
+ gmtime_r (&t, &stm);
+ iso = g_strdup_printf ("%04d%02d%02dT%02d%02d%02dZ",
+ (stm.tm_year + 1900),
+ (stm.tm_mon + 1),
+ stm.tm_mday,
+ stm.tm_hour,
+ stm.tm_min,
+ stm.tm_sec);
+
+ ret = g_strdup_printf (
+ "%s-%d-%d-%d-%d@%s",
+ iso,
+ getpid (),
+ getgid (),
+ getppid (),
+ serial++,
+ hostname);
+ g_free (iso);
+
+ return ret;
+}
+
+static gboolean
+ebmb_create_contact_sync (EBookMetaBackend *meta_backend,
+ EBookCache *book_cache,
+ ECacheOfflineFlag *offline_flag,
+ EConflictResolution conflict_resolution,
+ EContact *contact,
+ gchar **out_new_uid,
+ EContact **out_new_contact,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *uid;
+ gchar *new_uid = NULL, *new_extra = NULL;
+ gboolean success, requires_put = TRUE;
+
+ g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+
+ uid = e_contact_get_const (contact, E_CONTACT_UID);
+ if (!uid) {
+ gchar *new_uid;
+
+ new_uid = e_book_meta_backend_gen_uid ();
+ if (!new_uid) {
+ g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_INVALID_ARG,
NULL));
+ return FALSE;
+ }
+
+ e_contact_set (contact, E_CONTACT_UID, new_uid);
+ uid = e_contact_get_const (contact, E_CONTACT_UID);
+
+ g_free (new_uid);
+ }
+
+ if (e_cache_contains (E_CACHE (book_cache), uid, E_CACHE_EXCLUDE_DELETED)) {
+ g_propagate_error (error, e_data_book_create_error
(E_DATA_BOOK_STATUS_CONTACTID_ALREADY_EXISTS, NULL));
+ return FALSE;
+ }
+
+ if (*offline_flag == E_CACHE_OFFLINE_UNKNOWN) {
+ if (e_backend_get_online (E_BACKEND (meta_backend)) &&
+ ebmb_connect_wrapper_sync (meta_backend, cancellable, NULL)) {
+ *offline_flag = E_CACHE_IS_ONLINE;
+ } else {
+ *offline_flag = E_CACHE_IS_OFFLINE;
+ }
+ }
+
+ if (*offline_flag == E_CACHE_IS_ONLINE) {
+ if (!ebmb_save_contact_wrapper_sync (meta_backend, book_cache, FALSE, conflict_resolution,
contact, NULL, uid,
+ &requires_put, &new_uid, &new_extra, cancellable, error)) {
+ return FALSE;
+ }
+ }
+
+ if (requires_put) {
+ success = e_book_cache_put_contact (book_cache, contact, new_extra, *offline_flag,
cancellable, error);
+ if (success)
+ e_book_backend_notify_update (E_BOOK_BACKEND (meta_backend), contact);
+ } else {
+ success = TRUE;
+ }
+
+ if (success) {
+ if (out_new_uid)
+ *out_new_uid = g_strdup (new_uid ? new_uid : uid);
+ if (out_new_contact) {
+ if (new_uid) {
+ if (!e_book_cache_get_contact (book_cache, new_uid, FALSE, out_new_contact,
cancellable, NULL))
+ *out_new_contact = g_object_ref (contact);
+ } else {
+ *out_new_contact = g_object_ref (contact);
+ }
+ }
+ }
+
+ g_free (new_uid);
+ g_free (new_extra);
+
+ return success;
+}
+
+static gboolean
+ebmb_create_contacts_sync (EBookBackend *book_backend,
+ const gchar * const *vcards,
+ GQueue *out_contacts,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackend *meta_backend;
+ EBookCache *book_cache;
+ ECacheOfflineFlag offline_flag = E_CACHE_OFFLINE_UNKNOWN;
+ EConflictResolution conflict_resolution = E_CONFLICT_RESOLUTION_FAIL;
+ gint ii;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), FALSE);
+ g_return_val_if_fail (vcards != NULL, FALSE);
+ g_return_val_if_fail (out_contacts != NULL, FALSE);
+
+ if (!e_book_backend_get_writable (book_backend)) {
+ g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_PERMISSION_DENIED,
NULL));
+ return FALSE;
+ }
+
+ meta_backend = E_BOOK_META_BACKEND (book_backend);
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
+ g_return_val_if_fail (book_cache != NULL, FALSE);
+
+ for (ii = 0; vcards[ii] && success; ii++) {
+ EContact *contact, *new_contact = NULL;
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
+ success = FALSE;
+ break;
+ }
+
+ contact = e_contact_new_from_vcard (vcards[ii]);
+ if (!contact) {
+ g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_INVALID_ARG,
NULL));
+ success = FALSE;
+ break;
+ }
+
+ success = ebmb_create_contact_sync (meta_backend, book_cache, &offline_flag,
conflict_resolution,
+ contact, NULL, &new_contact, cancellable, error);
+
+ if (success) {
+ ebmb_foreach_cursor (meta_backend, new_contact, e_data_book_cursor_contact_added);
+
+ g_queue_push_tail (out_contacts, new_contact);
+ }
+
+ g_object_unref (contact);
+ }
+
+ g_object_unref (book_cache);
+
+ if (!success) {
+ g_queue_foreach (out_contacts, (GFunc) g_object_unref, NULL);
+ g_queue_clear (out_contacts);
+ }
+
+ return success;
+}
+
+static gboolean
+ebmb_modify_contact_sync (EBookMetaBackend *meta_backend,
+ EBookCache *book_cache,
+ ECacheOfflineFlag *offline_flag,
+ EConflictResolution conflict_resolution,
+ EContact *contact,
+ EContact **out_new_contact,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *uid;
+ EContact *existing_contact = NULL;
+ gchar *extra = NULL, *new_uid = NULL, *new_extra = NULL;
+ gboolean success = TRUE, requires_put = TRUE;
+ GError *local_error = NULL;
+
+ g_return_val_if_fail (contact != NULL, FALSE);
+
+ uid = e_contact_get_const (contact, E_CONTACT_UID);
+ if (!uid) {
+ g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_INVALID_ARG, NULL));
+ return FALSE;
+ }
+
+ if (!e_book_cache_get_contact (book_cache, uid, FALSE, &existing_contact, cancellable, &local_error))
{
+ if (g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND)) {
+ g_clear_error (&local_error);
+ local_error = e_data_book_create_error (E_DATA_BOOK_STATUS_CONTACT_NOT_FOUND, NULL);
+ }
+
+ g_propagate_error (error, local_error);
+
+ return FALSE;
+ }
+
+ if (!e_book_cache_get_contact_extra (book_cache, uid, &extra, cancellable, NULL))
+ extra = NULL;
+
+ if (success && *offline_flag == E_CACHE_OFFLINE_UNKNOWN) {
+ if (e_backend_get_online (E_BACKEND (meta_backend)) &&
+ ebmb_connect_wrapper_sync (meta_backend, cancellable, NULL)) {
+ *offline_flag = E_CACHE_IS_ONLINE;
+ } else {
+ *offline_flag = E_CACHE_IS_OFFLINE;
+ }
+ }
+
+ if (success && *offline_flag == E_CACHE_IS_ONLINE) {
+ success = ebmb_save_contact_wrapper_sync (meta_backend, book_cache, TRUE, conflict_resolution,
+ contact, extra, uid, &requires_put, &new_uid, &new_extra, cancellable, error);
+ }
+
+ if (success && requires_put)
+ success = ebmb_put_contact (meta_backend, book_cache, *offline_flag, contact, new_extra ?
new_extra : extra, cancellable, error);
+
+ if (success && out_new_contact) {
+ if (new_uid) {
+ if (!e_book_cache_get_contact (book_cache, new_uid, FALSE, out_new_contact,
cancellable, NULL))
+ *out_new_contact = NULL;
+ } else {
+ *out_new_contact = g_object_ref (contact);
+ }
+ }
+
+ g_clear_object (&existing_contact);
+ g_free (new_extra);
+ g_free (new_uid);
+ g_free (extra);
+
+ return success;
+}
+
+static gboolean
+ebmb_modify_contacts_sync (EBookBackend *book_backend,
+ const gchar * const *vcards,
+ GQueue *out_contacts,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackend *meta_backend;
+ EBookCache *book_cache;
+ ECacheOfflineFlag offline_flag = E_CACHE_OFFLINE_UNKNOWN;
+ EConflictResolution conflict_resolution = E_CONFLICT_RESOLUTION_FAIL;
+ gint ii;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), FALSE);
+ g_return_val_if_fail (vcards != NULL, FALSE);
+ g_return_val_if_fail (out_contacts != NULL, FALSE);
+
+ if (!e_book_backend_get_writable (book_backend)) {
+ g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_PERMISSION_DENIED,
NULL));
+ return FALSE;
+ }
+
+ meta_backend = E_BOOK_META_BACKEND (book_backend);
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
+ g_return_val_if_fail (book_cache != NULL, FALSE);
+
+ for (ii = 0; vcards[ii] && success; ii++) {
+ EContact *contact, *new_contact = NULL;
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
+ success = FALSE;
+ break;
+ }
+
+ contact = e_contact_new_from_vcard (vcards[ii]);
+ if (!contact) {
+ g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_INVALID_ARG,
NULL));
+ success = FALSE;
+ break;
+ }
+
+ success = ebmb_modify_contact_sync (meta_backend, book_cache, &offline_flag,
conflict_resolution,
+ contact, &new_contact, cancellable, error);
+
+ if (success && new_contact) {
+ ebmb_foreach_cursor (meta_backend, contact, e_data_book_cursor_contact_removed);
+ ebmb_foreach_cursor (meta_backend, new_contact, e_data_book_cursor_contact_added);
+
+ g_queue_push_tail (out_contacts, g_object_ref (new_contact));
+ }
+
+ g_clear_object (&new_contact);
+ g_object_unref (contact);
+ }
+
+ g_object_unref (book_cache);
+
+ if (!success) {
+ g_queue_foreach (out_contacts, (GFunc) g_object_unref, NULL);
+ g_queue_clear (out_contacts);
+ }
+
+ return success;
+}
+
+static gboolean
+ebmb_remove_contact_sync (EBookMetaBackend *meta_backend,
+ EBookCache *book_cache,
+ ECacheOfflineFlag *offline_flag,
+ EConflictResolution conflict_resolution,
+ const gchar *uid,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EContact *existing_contact = NULL;
+ gchar *extra = NULL;
+ gboolean success = TRUE;
+ GError *local_error = NULL;
+
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ if (!e_book_cache_get_contact (book_cache, uid, FALSE, &existing_contact, cancellable, &local_error))
{
+ if (g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND)) {
+ g_clear_error (&local_error);
+ local_error = e_data_book_create_error (E_DATA_BOOK_STATUS_CONTACT_NOT_FOUND, NULL);
+ }
+
+ g_propagate_error (error, local_error);
+
+ return FALSE;
+ }
+
+ if (*offline_flag == E_CACHE_OFFLINE_UNKNOWN) {
+ if (e_backend_get_online (E_BACKEND (meta_backend)) &&
+ ebmb_connect_wrapper_sync (meta_backend, cancellable, NULL)) {
+ *offline_flag = E_CACHE_IS_ONLINE;
+ } else {
+ *offline_flag = E_CACHE_IS_OFFLINE;
+ }
+ }
+
+ if (!e_book_cache_get_contact_extra (book_cache, uid, &extra, cancellable, NULL))
+ extra = NULL;
+
+ if (*offline_flag == E_CACHE_IS_ONLINE) {
+ gchar *vcard_string = NULL;
+
+ g_warn_if_fail (e_book_cache_get_vcard (book_cache, uid, FALSE, &vcard_string, cancellable,
NULL));
+
+ success = e_book_meta_backend_remove_contact_sync (meta_backend, conflict_resolution, uid,
extra, vcard_string, cancellable, error);
+
+ g_free (vcard_string);
+ }
+
+ success = success && ebmb_maybe_remove_from_cache (meta_backend, book_cache, *offline_flag, uid,
cancellable, error);
+
+ g_clear_object (&existing_contact);
+ g_free (extra);
+
+ return success;
+}
+
+static gboolean
+ebmb_remove_contacts_sync (EBookBackend *book_backend,
+ const gchar * const *uids,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackend *meta_backend;
+ EBookCache *book_cache;
+ ECacheOfflineFlag offline_flag = E_CACHE_OFFLINE_UNKNOWN;
+ EConflictResolution conflict_resolution = E_CONFLICT_RESOLUTION_FAIL;
+ gint ii;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), FALSE);
+ g_return_val_if_fail (uids != NULL, FALSE);
+
+ if (!e_book_backend_get_writable (book_backend)) {
+ g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_PERMISSION_DENIED,
NULL));
+ return FALSE;
+ }
+
+ meta_backend = E_BOOK_META_BACKEND (book_backend);
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
+ g_return_val_if_fail (book_cache != NULL, FALSE);
+
+ for (ii = 0; uids[ii] && success; ii++) {
+ const gchar *uid = uids[ii];
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
+ success = FALSE;
+ break;
+ }
+
+ if (!uid) {
+ g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_INVALID_ARG,
NULL));
+ success = FALSE;
+ break;
+ }
+
+ success = ebmb_remove_contact_sync (meta_backend, book_cache, &offline_flag,
conflict_resolution, uid, cancellable, error);
+ }
+
+ g_object_unref (book_cache);
+
+ return success;
+}
+
+static EContact *
+ebmb_get_contact_sync (EBookBackend *book_backend,
+ const gchar *uid,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackend *meta_backend;
+ EBookCache *book_cache;
+ EContact *contact = NULL;
+ GError *local_error = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), NULL);
+ g_return_val_if_fail (uid && *uid, NULL);
+
+ meta_backend = E_BOOK_META_BACKEND (book_backend);
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
+
+ g_return_val_if_fail (book_cache != NULL, NULL);
+
+ if (!e_book_cache_get_contact (book_cache, uid, FALSE, &contact, cancellable, &local_error) &&
+ g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND)) {
+ gchar *loaded_uid = NULL;
+ gboolean found = FALSE;
+
+ g_clear_error (&local_error);
+
+ /* Ignore errors here, just try whether it's on the remote side, but not in the local cache */
+ if (e_backend_get_online (E_BACKEND (meta_backend)) &&
+ ebmb_connect_wrapper_sync (meta_backend, cancellable, NULL) &&
+ ebmb_load_contact_wrapper_sync (meta_backend, book_cache, uid, NULL, NULL, &loaded_uid,
cancellable, NULL)) {
+ found = e_book_cache_get_contact (book_cache, loaded_uid, FALSE, &contact,
cancellable, NULL);
+ }
+
+ if (!found)
+ g_propagate_error (error, e_data_book_create_error
(E_DATA_BOOK_STATUS_CONTACT_NOT_FOUND, NULL));
+
+ g_free (loaded_uid);
+ } else if (local_error) {
+ g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_OTHER_ERROR,
local_error->message));
+ g_clear_error (&local_error);
+ }
+
+ g_object_unref (book_cache);
+
+ return contact;
+}
+
+static gboolean
+ebmb_get_contact_list_sync (EBookBackend *book_backend,
+ const gchar *query,
+ GQueue *out_contacts,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GSList *contacts = NULL, *link;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), FALSE);
+ g_return_val_if_fail (out_contacts != NULL, FALSE);
+
+ success = e_book_meta_backend_search_sync (E_BOOK_META_BACKEND (book_backend), query, FALSE,
&contacts, cancellable, error);
+ if (success) {
+ for (link = contacts; link; link = g_slist_next (link)) {
+ EContact *contact = link->data;
+
+ g_queue_push_tail (out_contacts, g_object_ref (contact));
+ }
+
+ g_slist_free_full (contacts, g_object_unref);
+ }
+
+ return success;
+}
+
+static gboolean
+ebmb_get_contact_list_uids_sync (EBookBackend *book_backend,
+ const gchar *query,
+ GQueue *out_uids,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GSList *uids = NULL, *link;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), FALSE);
+ g_return_val_if_fail (out_uids != NULL, FALSE);
+
+ success = e_book_meta_backend_search_uids_sync (E_BOOK_META_BACKEND (book_backend), query, &uids,
cancellable, error);
+ if (success) {
+ for (link = uids; link; link = g_slist_next (link)) {
+ gchar *uid = link->data;
+
+ g_queue_push_tail (out_uids, uid);
+ link->data = NULL;
+ }
+
+ g_slist_free_full (uids, g_free);
+ }
+
+ return success;
+}
+
+static void
+ebmb_start_view (EBookBackend *book_backend,
+ EDataBookView *view)
+{
+ GCancellable *cancellable;
+
+ g_return_if_fail (E_IS_BOOK_META_BACKEND (book_backend));
+
+ cancellable = ebmb_create_view_cancellable (E_BOOK_META_BACKEND (book_backend), view);
+
+ e_book_backend_schedule_custom_operation (book_backend, cancellable,
+ ebmb_start_view_thread_func, g_object_ref (view), g_object_unref);
+
+ g_object_unref (cancellable);
+}
+
+static void
+ebmb_stop_view (EBookBackend *book_backend,
+ EDataBookView *view)
+{
+ GCancellable *cancellable;
+
+ g_return_if_fail (E_IS_BOOK_META_BACKEND (book_backend));
+
+ cancellable = ebmb_steal_view_cancellable (E_BOOK_META_BACKEND (book_backend), view);
+ if (cancellable) {
+ g_cancellable_cancel (cancellable);
+ g_object_unref (cancellable);
+ }
+}
+
+static EDataBookDirect *
+ebmb_get_direct_book (EBookBackend *book_backend)
+{
+ EBookMetaBackendClass *klass;
+ EBookCache *book_cache;
+ EDataBookDirect *direct_book;
+ const gchar *cache_filename;
+ gchar *backend_path;
+ gchar *dirname;
+ const gchar *modules_env;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), NULL);
+
+ klass = E_BOOK_META_BACKEND_GET_CLASS (book_backend);
+ g_return_val_if_fail (klass != NULL, NULL);
+
+ if (!klass->backend_module_filename ||
+ !klass->backend_factory_type_name)
+ return NULL;
+
+ book_cache = e_book_meta_backend_ref_cache (E_BOOK_META_BACKEND (book_backend));
+ g_return_val_if_fail (book_cache != NULL, NULL);
+
+ cache_filename = e_cache_get_filename (E_CACHE (book_cache));
+ dirname = g_path_get_dirname (cache_filename);
+
+ modules_env = g_getenv (EDS_ADDRESS_BOOK_MODULES);
+
+ /* Support in-tree testing / relocated modules */
+ if (modules_env) {
+ backend_path = g_build_filename (modules_env, klass->backend_module_filename, NULL);
+ } else {
+ backend_path = g_build_filename (BACKENDDIR, klass->backend_module_filename, NULL);
+ }
+
+ direct_book = e_data_book_direct_new (backend_path, klass->backend_factory_type_name, dirname);
+
+ g_object_unref (book_cache);
+ g_free (backend_path);
+ g_free (dirname);
+
+ return direct_book;
+}
+
+static void
+ebmb_configure_direct (EBookBackend *book_backend,
+ const gchar *base_directory)
+{
+ EBookMetaBackend *meta_backend;
+ EBookCache *book_cache;
+ const gchar *cache_filename;
+ gchar *dirname;
+
+ g_return_if_fail (E_IS_BOOK_META_BACKEND (book_backend));
+
+ if (!base_directory)
+ return;
+
+ meta_backend = E_BOOK_META_BACKEND (book_backend);
+
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
+ g_return_if_fail (book_cache != NULL);
+
+ cache_filename = e_cache_get_filename (E_CACHE (book_cache));
+ dirname = g_path_get_dirname (cache_filename);
+
+ /* Did path for the cache change? Change the cache as well */
+ if (dirname && !g_str_equal (base_directory, dirname) &&
+ !g_str_has_prefix (dirname, base_directory)) {
+ gchar *filename = g_path_get_basename (cache_filename);
+ gchar *new_cache_filename;
+ EBookCache *new_cache;
+ ESource *source;
+
+ new_cache_filename = g_build_filename (base_directory, filename, NULL);
+ source = e_backend_get_source (E_BACKEND (book_backend));
+
+ g_clear_error (&meta_backend->priv->create_cache_error);
+
+ new_cache = e_book_cache_new (new_cache_filename, source, NULL,
&meta_backend->priv->create_cache_error);
+ g_prefix_error (&meta_backend->priv->create_cache_error, _("Failed to create cache ”%s”:"),
new_cache_filename);
+
+ if (new_cache) {
+ e_book_meta_backend_set_cache (meta_backend, new_cache);
+ g_clear_object (&new_cache);
+ }
+
+ g_free (new_cache_filename);
+ g_free (filename);
+ }
+
+ g_free (dirname);
+ g_object_unref (book_cache);
+}
+
+static gboolean
+ebmb_set_locale (EBookBackend *book_backend,
+ const gchar *locale,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackend *meta_backend;
+ EBookCache *book_cache;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), FALSE);
+
+ meta_backend = E_BOOK_META_BACKEND (book_backend);
+
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
+ g_return_val_if_fail (book_cache != NULL, FALSE);
+
+ success = e_book_cache_set_locale (book_cache, locale, cancellable, error);
+ if (success) {
+ GSList *link;
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ for (link = meta_backend->priv->cursors; success && link; link = g_slist_next (link)) {
+ EDataBookCursor *cursor = link->data;
+
+ success = e_data_book_cursor_load_locale (cursor, NULL, cancellable, error);
+ }
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+ }
+
+ g_object_unref (book_cache);
+
+ return success;
+}
+
+static gchar *
+ebmb_dup_locale (EBookBackend *book_backend)
+{
+ EBookMetaBackend *meta_backend;
+ EBookCache *book_cache;
+ gchar *locale;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), NULL);
+
+ meta_backend = E_BOOK_META_BACKEND (book_backend);
+
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
+ g_return_val_if_fail (book_cache != NULL, NULL);
+
+ locale = e_book_cache_dup_locale (book_cache);
+
+ g_object_unref (book_cache);
+
+ return locale;
+}
+
+static EDataBookCursor *
+ebmb_create_cursor (EBookBackend *book_backend,
+ EContactField *sort_fields,
+ EBookCursorSortType *sort_types,
+ guint n_fields,
+ GError **error)
+{
+ EBookMetaBackend *meta_backend;
+ EBookCache *book_cache;
+ EDataBookCursor *cursor;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), NULL);
+
+ meta_backend = E_BOOK_META_BACKEND (book_backend);
+
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
+ g_return_val_if_fail (book_cache != NULL, NULL);
+
+ cursor = e_data_book_cursor_cache_new (book_backend, book_cache, sort_fields, sort_types, n_fields,
error);
+
+ if (cursor) {
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ meta_backend->priv->cursors = g_slist_prepend (meta_backend->priv->cursors, cursor);
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+ }
+
+ g_object_unref (book_cache);
+
+ return cursor;
+}
+
+static gboolean
+ebmb_delete_cursor (EBookBackend *book_backend,
+ EDataBookCursor *cursor,
+ GError **error)
+{
+ EBookMetaBackend *meta_backend;
+ GSList *link;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (book_backend), FALSE);
+
+ meta_backend = E_BOOK_META_BACKEND (book_backend);
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ link = g_slist_find (meta_backend->priv->cursors, cursor);
+
+ if (link) {
+ meta_backend->priv->cursors = g_slist_remove (meta_backend->priv->cursors, cursor);
+ g_object_unref (cursor);
+ } else {
+ g_set_error_literal (
+ error,
+ E_CLIENT_ERROR,
+ E_CLIENT_ERROR_INVALID_ARG,
+ _("Requested to delete an unrelated cursor"));
+ }
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ return link != NULL;
+}
+
+static ESourceAuthenticationResult
+ebmb_authenticate_sync (EBackend *backend,
+ const ENamedParameters *credentials,
+ gchar **out_certificate_pem,
+ GTlsCertificateFlags *out_certificate_errors,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackend *meta_backend;
+ ESourceAuthenticationResult auth_result = E_SOURCE_AUTHENTICATION_UNKNOWN;
+ gboolean success, refresh_after_authenticate = FALSE;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (backend), E_SOURCE_AUTHENTICATION_ERROR);
+
+ meta_backend = E_BOOK_META_BACKEND (backend);
+
+ if (!e_backend_get_online (E_BACKEND (meta_backend))) {
+ g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_REPOSITORY_OFFLINE,
+ e_client_error_to_string (E_CLIENT_ERROR_REPOSITORY_OFFLINE));
+
+ return E_SOURCE_AUTHENTICATION_ERROR;
+ }
+
+ g_mutex_lock (&meta_backend->priv->connect_lock);
+ success = e_book_meta_backend_connect_sync (meta_backend, credentials, &auth_result,
+ out_certificate_pem, out_certificate_errors, cancellable, error);
+
+ if (success) {
+ ebmb_update_connection_values (meta_backend);
+ auth_result = E_SOURCE_AUTHENTICATION_ACCEPTED;
+ } else {
+ if (auth_result == E_SOURCE_AUTHENTICATION_UNKNOWN)
+ auth_result = E_SOURCE_AUTHENTICATION_ERROR;
+ }
+ g_mutex_unlock (&meta_backend->priv->connect_lock);
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ e_named_parameters_free (meta_backend->priv->last_credentials);
+ if (success) {
+ meta_backend->priv->last_credentials = e_named_parameters_new_clone (credentials);
+
+ refresh_after_authenticate = meta_backend->priv->refresh_after_authenticate;
+ meta_backend->priv->refresh_after_authenticate = FALSE;
+ } else {
+ meta_backend->priv->last_credentials = NULL;
+ }
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ if (refresh_after_authenticate)
+ ebmb_schedule_refresh (meta_backend);
+
+ return auth_result;
+}
+
+static void
+ebmb_schedule_refresh (EBookMetaBackend *meta_backend)
+{
+ GCancellable *cancellable;
+
+ g_return_if_fail (E_IS_BOOK_META_BACKEND (meta_backend));
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ if (meta_backend->priv->refresh_cancellable) {
+ /* Already refreshing the content */
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+ return;
+ }
+
+ cancellable = g_cancellable_new ();
+ meta_backend->priv->refresh_cancellable = g_object_ref (cancellable);
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ e_book_backend_schedule_custom_operation (E_BOOK_BACKEND (meta_backend), cancellable,
+ ebmb_refresh_thread_func, NULL, NULL);
+
+ g_object_unref (cancellable);
+}
+
+static void
+ebmb_schedule_source_changed (EBookMetaBackend *meta_backend)
+{
+ GCancellable *cancellable;
+
+ g_return_if_fail (E_IS_BOOK_META_BACKEND (meta_backend));
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ if (meta_backend->priv->source_changed_cancellable) {
+ /* Already updating */
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+ return;
+ }
+
+ cancellable = g_cancellable_new ();
+ meta_backend->priv->source_changed_cancellable = g_object_ref (cancellable);
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ e_book_backend_schedule_custom_operation (E_BOOK_BACKEND (meta_backend), cancellable,
+ ebmb_source_changed_thread_func, NULL, NULL);
+
+ g_object_unref (cancellable);
+}
+
+static void
+ebmb_schedule_go_offline (EBookMetaBackend *meta_backend)
+{
+ GCancellable *cancellable;
+
+ g_return_if_fail (E_IS_BOOK_META_BACKEND (meta_backend));
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ /* Cancel anything ongoing now, but disconnect in a dedicated thread */
+ if (meta_backend->priv->refresh_cancellable) {
+ g_cancellable_cancel (meta_backend->priv->refresh_cancellable);
+ g_clear_object (&meta_backend->priv->refresh_cancellable);
+ }
+
+ if (meta_backend->priv->source_changed_cancellable) {
+ g_cancellable_cancel (meta_backend->priv->source_changed_cancellable);
+ g_clear_object (&meta_backend->priv->source_changed_cancellable);
+ }
+
+ if (meta_backend->priv->go_offline_cancellable) {
+ /* Already going offline */
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+ return;
+ }
+
+ cancellable = g_cancellable_new ();
+ meta_backend->priv->go_offline_cancellable = g_object_ref (cancellable);
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ e_book_backend_schedule_custom_operation (E_BOOK_BACKEND (meta_backend), cancellable,
+ ebmb_go_offline_thread_func, NULL, NULL);
+
+ g_object_unref (cancellable);
+}
+
+static void
+ebmb_notify_online_cb (GObject *object,
+ GParamSpec *param,
+ gpointer user_data)
+{
+ EBookMetaBackend *meta_backend = user_data;
+ gboolean new_value;
+
+ g_return_if_fail (E_IS_BOOK_META_BACKEND (meta_backend));
+
+ new_value = e_backend_get_online (E_BACKEND (meta_backend));
+ if (!new_value == !meta_backend->priv->current_online_state)
+ return;
+
+ meta_backend->priv->current_online_state = new_value;
+
+ if (new_value)
+ ebmb_schedule_refresh (meta_backend);
+ else
+ ebmb_schedule_go_offline (meta_backend);
+}
+
+static void
+ebmb_cancel_view_cb (gpointer key,
+ gpointer value,
+ gpointer user_data)
+{
+ GCancellable *cancellable = value;
+
+ g_return_if_fail (G_IS_CANCELLABLE (cancellable));
+
+ g_cancellable_cancel (cancellable);
+}
+
+static void
+e_book_meta_backend_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case PROP_CACHE:
+ e_book_meta_backend_set_cache (
+ E_BOOK_META_BACKEND (object),
+ g_value_get_object (value));
+ return;
+ }
+
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+e_book_meta_backend_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case PROP_CACHE:
+ g_value_take_object (
+ value,
+ e_book_meta_backend_ref_cache (
+ E_BOOK_META_BACKEND (object)));
+ return;
+ }
+
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+e_book_meta_backend_constructed (GObject *object)
+{
+ EBookMetaBackend *meta_backend = E_BOOK_META_BACKEND (object);
+
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_book_meta_backend_parent_class)->constructed (object);
+
+ meta_backend->priv->current_online_state = e_backend_get_online (E_BACKEND (meta_backend));
+
+ meta_backend->priv->notify_online_id = g_signal_connect (meta_backend, "notify::online",
+ G_CALLBACK (ebmb_notify_online_cb), meta_backend);
+
+ if (!meta_backend->priv->cache) {
+ EBookCache *cache;
+ ESource *source;
+ gchar *filename;
+
+ source = e_backend_get_source (E_BACKEND (meta_backend));
+ filename = g_build_filename (e_book_backend_get_cache_dir (E_BOOK_BACKEND (meta_backend)),
"cache.db", NULL);
+ cache = e_book_cache_new (filename, source, NULL, &meta_backend->priv->create_cache_error);
+ g_prefix_error (&meta_backend->priv->create_cache_error, _("Failed to create cache ”%s”:"),
filename);
+
+ g_free (filename);
+
+ if (cache) {
+ e_book_meta_backend_set_cache (meta_backend, cache);
+ g_clear_object (&cache);
+ }
+ }
+}
+
+static void
+e_book_meta_backend_dispose (GObject *object)
+{
+ EBookMetaBackend *meta_backend = E_BOOK_META_BACKEND (object);
+ ESource *source = e_backend_get_source (E_BACKEND (meta_backend));
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ if (meta_backend->priv->cursors) {
+ g_slist_free_full (meta_backend->priv->cursors, g_object_unref);
+ meta_backend->priv->cursors = NULL;
+ }
+
+ if (meta_backend->priv->refresh_timeout_id) {
+ if (source)
+ e_source_refresh_remove_timeout (source, meta_backend->priv->refresh_timeout_id);
+ meta_backend->priv->refresh_timeout_id = 0;
+ }
+
+ if (meta_backend->priv->source_changed_id) {
+ if (source)
+ g_signal_handler_disconnect (source, meta_backend->priv->source_changed_id);
+ meta_backend->priv->source_changed_id = 0;
+ }
+
+ if (meta_backend->priv->notify_online_id) {
+ g_signal_handler_disconnect (meta_backend, meta_backend->priv->notify_online_id);
+ meta_backend->priv->notify_online_id = 0;
+ }
+
+ if (meta_backend->priv->revision_changed_id) {
+ if (meta_backend->priv->cache)
+ g_signal_handler_disconnect (meta_backend->priv->cache,
meta_backend->priv->revision_changed_id);
+ meta_backend->priv->revision_changed_id = 0;
+ }
+
+ g_hash_table_foreach (meta_backend->priv->view_cancellables, ebmb_cancel_view_cb, NULL);
+
+ if (meta_backend->priv->refresh_cancellable) {
+ g_cancellable_cancel (meta_backend->priv->refresh_cancellable);
+ g_clear_object (&meta_backend->priv->refresh_cancellable);
+ }
+
+ if (meta_backend->priv->source_changed_cancellable) {
+ g_cancellable_cancel (meta_backend->priv->source_changed_cancellable);
+ g_clear_object (&meta_backend->priv->source_changed_cancellable);
+ }
+
+ if (meta_backend->priv->go_offline_cancellable) {
+ g_cancellable_cancel (meta_backend->priv->go_offline_cancellable);
+ g_clear_object (&meta_backend->priv->go_offline_cancellable);
+ }
+
+ e_named_parameters_free (meta_backend->priv->last_credentials);
+ meta_backend->priv->last_credentials = NULL;
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_book_meta_backend_parent_class)->dispose (object);
+}
+
+static void
+e_book_meta_backend_finalize (GObject *object)
+{
+ EBookMetaBackend *meta_backend = E_BOOK_META_BACKEND (object);
+
+ g_clear_object (&meta_backend->priv->cache);
+ g_clear_object (&meta_backend->priv->refresh_cancellable);
+ g_clear_object (&meta_backend->priv->source_changed_cancellable);
+ g_clear_object (&meta_backend->priv->go_offline_cancellable);
+ g_clear_error (&meta_backend->priv->create_cache_error);
+ g_clear_pointer (&meta_backend->priv->authentication_host, g_free);
+ g_clear_pointer (&meta_backend->priv->authentication_user, g_free);
+ g_clear_pointer (&meta_backend->priv->authentication_method, g_free);
+ g_clear_pointer (&meta_backend->priv->authentication_proxy_uid, g_free);
+ g_clear_pointer (&meta_backend->priv->authentication_credential_name, g_free);
+ g_clear_pointer (&meta_backend->priv->webdav_soup_uri, (GDestroyNotify) soup_uri_free);
+
+ g_mutex_clear (&meta_backend->priv->connect_lock);
+ g_mutex_clear (&meta_backend->priv->property_lock);
+ g_hash_table_destroy (meta_backend->priv->view_cancellables);
+
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_book_meta_backend_parent_class)->finalize (object);
+}
+
+static void
+e_book_meta_backend_class_init (EBookMetaBackendClass *klass)
+{
+ GObjectClass *object_class;
+ EBackendClass *backend_class;
+ EBookBackendClass *book_backend_class;
+
+ g_type_class_add_private (klass, sizeof (EBookMetaBackendPrivate));
+
+ klass->backend_factory_type_name = NULL;
+ klass->backend_module_filename = NULL;
+ klass->get_changes_sync = ebmb_get_changes_sync;
+ klass->search_sync = ebmb_search_sync;
+ klass->search_uids_sync = ebmb_search_uids_sync;
+ klass->requires_reconnect = ebmb_requires_reconnect;
+
+ book_backend_class = E_BOOK_BACKEND_CLASS (klass);
+ book_backend_class->get_backend_property = ebmb_get_backend_property;
+ book_backend_class->open_sync = ebmb_open_sync;
+ book_backend_class->refresh_sync = ebmb_refresh_sync;
+ book_backend_class->create_contacts_sync = ebmb_create_contacts_sync;
+ book_backend_class->modify_contacts_sync = ebmb_modify_contacts_sync;
+ book_backend_class->remove_contacts_sync = ebmb_remove_contacts_sync;
+ book_backend_class->get_contact_sync = ebmb_get_contact_sync;
+ book_backend_class->get_contact_list_sync = ebmb_get_contact_list_sync;
+ book_backend_class->get_contact_list_uids_sync = ebmb_get_contact_list_uids_sync;
+ book_backend_class->start_view = ebmb_start_view;
+ book_backend_class->stop_view = ebmb_stop_view;
+ book_backend_class->get_direct_book = ebmb_get_direct_book;
+ book_backend_class->configure_direct = ebmb_configure_direct;
+ book_backend_class->set_locale = ebmb_set_locale;
+ book_backend_class->dup_locale = ebmb_dup_locale;
+ book_backend_class->create_cursor = ebmb_create_cursor;
+ book_backend_class->delete_cursor = ebmb_delete_cursor;
+
+ backend_class = E_BACKEND_CLASS (klass);
+ backend_class->authenticate_sync = ebmb_authenticate_sync;
+
+ object_class = G_OBJECT_CLASS (klass);
+ object_class->set_property = e_book_meta_backend_set_property;
+ object_class->get_property = e_book_meta_backend_get_property;
+ object_class->constructed = e_book_meta_backend_constructed;
+ object_class->dispose = e_book_meta_backend_dispose;
+ object_class->finalize = e_book_meta_backend_finalize;
+
+ /**
+ * EBookMetaBackend:cache:
+ *
+ * The #EBookCache being used for this meta backend.
+ **/
+ g_object_class_install_property (
+ object_class,
+ PROP_CACHE,
+ g_param_spec_object (
+ "cache",
+ "Cache",
+ "Book Cache",
+ E_TYPE_BOOK_CACHE,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ /* This signal is meant for testing purposes mainly */
+ signals[REFRESH_COMPLETED] = g_signal_new (
+ "refresh-completed",
+ G_OBJECT_CLASS_TYPE (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 0, G_TYPE_NONE);
+
+ /**
+ * EBookMetaBackend::source-changed
+ *
+ * This signal is emitted whenever the underlying backend #ESource
+ * changes. Unlike the #ESource's 'changed' signal this one is
+ * tight to the #EBookMetaBackend itself and is emitted from
+ * a dedicated thread, thus it doesn't block the main thread.
+ *
+ * Since: 3.26
+ **/
+ signals[SOURCE_CHANGED] = g_signal_new (
+ "source-changed",
+ G_OBJECT_CLASS_TYPE (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (EBookMetaBackendClass, source_changed),
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 0, G_TYPE_NONE);
+}
+
+static void
+e_book_meta_backend_init (EBookMetaBackend *meta_backend)
+{
+ meta_backend->priv = G_TYPE_INSTANCE_GET_PRIVATE (meta_backend, E_TYPE_BOOK_META_BACKEND,
EBookMetaBackendPrivate);
+
+ g_mutex_init (&meta_backend->priv->connect_lock);
+ g_mutex_init (&meta_backend->priv->property_lock);
+
+ meta_backend->priv->view_cancellables = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL,
g_object_unref);
+ meta_backend->priv->current_online_state = FALSE;
+ meta_backend->priv->refresh_after_authenticate = FALSE;
+ meta_backend->priv->ever_connected = -1;
+ meta_backend->priv->connected_writable = -1;
+}
+
+/**
+ * e_book_meta_backend_get_capabilities:
+ * @meta_backend: an #EBookMetaBackend
+ *
+ * Returns: an #EBookBackend::capabilities property to be used by
+ * the descendant in conjunction to the descendant's capabilities
+ * in the result of e_book_backend_get_backend_property() with
+ * #CLIENT_BACKEND_PROPERTY_CAPABILITIES.
+ *
+ * Since: 3.26
+ **/
+const gchar *
+e_book_meta_backend_get_capabilities (EBookMetaBackend *meta_backend)
+{
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), NULL);
+
+ return "refresh-supported" ","
+ "bulk-adds" ","
+ "bulk-modifies" ","
+ "bulk-removes";
+}
+
+/**
+ * e_book_meta_backend_set_ever_connected:
+ * @meta_backend: an #EBookMetaBackend
+ * @value: value to set
+ *
+ * Sets whether the @meta_backend ever made a successful connection
+ * to its destination.
+ *
+ * This is used by the @meta_backend itself, during the opening phase,
+ * when it had not been connected yet, then it does so immediately, to
+ * eventually report settings error easily.
+ *
+ * Since: 3.26
+ **/
+void
+e_book_meta_backend_set_ever_connected (EBookMetaBackend *meta_backend,
+ gboolean value)
+{
+ EBookCache *book_cache;
+
+ g_return_if_fail (E_IS_BOOK_META_BACKEND (meta_backend));
+
+ if ((value ? 1 : 0) == meta_backend->priv->ever_connected)
+ return;
+
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
+ meta_backend->priv->ever_connected = value ? 1 : 0;
+ e_cache_set_key_int (E_CACHE (book_cache), EBMB_KEY_EVER_CONNECTED,
meta_backend->priv->ever_connected, NULL);
+ g_clear_object (&book_cache);
+}
+
+/**
+ * e_book_meta_backend_get_ever_connected:
+ * @meta_backend: an #EBookMetaBackend
+ *
+ * Returns: Whether the @meta_backend ever made a successful connection
+ * to its destination.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_get_ever_connected (EBookMetaBackend *meta_backend)
+{
+ gboolean result;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+
+ if (meta_backend->priv->ever_connected == -1) {
+ EBookCache *book_cache;
+
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
+ result = e_cache_get_key_int (E_CACHE (book_cache), EBMB_KEY_EVER_CONNECTED, NULL) == 1;
+ g_clear_object (&book_cache);
+
+ meta_backend->priv->ever_connected = result ? 1 : 0;
+ } else {
+ result = meta_backend->priv->ever_connected == 1;
+ }
+
+ return result;
+}
+
+/**
+ * e_book_meta_backend_set_connected_writable:
+ * @meta_backend: an #EBookMetaBackend
+ * @value: value to set
+ *
+ * Sets whether the @meta_backend connected to a writable destination.
+ * This value has meaning only if e_book_meta_backend_get_ever_connected()
+ * is %TRUE.
+ *
+ * This is used by the @meta_backend itself, during the opening phase,
+ * to set the backend writable or not also in the offline mode.
+ *
+ * Since: 3.26
+ **/
+void
+e_book_meta_backend_set_connected_writable (EBookMetaBackend *meta_backend,
+ gboolean value)
+{
+ EBookCache *book_cache;
+
+ g_return_if_fail (E_IS_BOOK_META_BACKEND (meta_backend));
+
+ if ((value ? 1 : 0) == meta_backend->priv->connected_writable)
+ return;
+
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
+ meta_backend->priv->connected_writable = value ? 1 : 0;
+ e_cache_set_key_int (E_CACHE (book_cache), EBMB_KEY_CONNECTED_WRITABLE,
meta_backend->priv->connected_writable, NULL);
+ g_clear_object (&book_cache);
+}
+
+/**
+ * e_book_meta_backend_get_connected_writable:
+ * @meta_backend: an #EBookMetaBackend
+ *
+ * This value has meaning only if e_book_meta_backend_get_ever_connected()
+ * is %TRUE.
+ *
+ * Returns: Whether the @meta_backend connected to a writable destination.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_get_connected_writable (EBookMetaBackend *meta_backend)
+{
+ gboolean result;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+
+ if (meta_backend->priv->connected_writable == -1) {
+ EBookCache *book_cache;
+
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
+ result = e_cache_get_key_int (E_CACHE (book_cache), EBMB_KEY_CONNECTED_WRITABLE, NULL) == 1;
+ g_clear_object (&book_cache);
+
+ meta_backend->priv->connected_writable = result ? 1 : 0;
+ } else {
+ result = meta_backend->priv->connected_writable == 1;
+ }
+
+ return result;
+}
+
+static void
+ebmb_cache_revision_changed_cb (ECache *cache,
+ gpointer user_data)
+{
+ EBookMetaBackend *meta_backend = user_data;
+ gchar *revision;
+
+ g_return_if_fail (E_IS_CACHE (cache));
+ g_return_if_fail (E_IS_BOOK_META_BACKEND (meta_backend));
+
+ revision = e_cache_dup_revision (cache);
+ if (revision) {
+ e_book_backend_notify_property_changed (E_BOOK_BACKEND (meta_backend),
+ BOOK_BACKEND_PROPERTY_REVISION, revision);
+ g_free (revision);
+ }
+}
+
+/**
+ * e_book_meta_backend_set_cache:
+ * @meta_backend: an #EBookMetaBackend
+ * @cache: an #EBookCache to use
+ *
+ * Sets the @cache as the cache to be used by the @meta_backend.
+ * By default, a cache.db in EBookBackend::cache-dir is created
+ * in the constructed method. This function can be used to override
+ * the default.
+ *
+ * Note the @meta_backend adds its own reference to the @cache.
+ *
+ * Since: 3.26
+ **/
+void
+e_book_meta_backend_set_cache (EBookMetaBackend *meta_backend,
+ EBookCache *cache)
+{
+ g_return_if_fail (E_IS_BOOK_META_BACKEND (meta_backend));
+ g_return_if_fail (E_IS_BOOK_CACHE (cache));
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ if (meta_backend->priv->cache == cache) {
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+ return;
+ }
+
+ g_clear_error (&meta_backend->priv->create_cache_error);
+
+ if (meta_backend->priv->cache) {
+ g_signal_handler_disconnect (meta_backend->priv->cache,
+ meta_backend->priv->revision_changed_id);
+ }
+
+ g_clear_object (&meta_backend->priv->cache);
+ meta_backend->priv->cache = g_object_ref (cache);
+
+ meta_backend->priv->revision_changed_id = g_signal_connect_object (meta_backend->priv->cache,
+ "revision-changed", G_CALLBACK (ebmb_cache_revision_changed_cb), meta_backend, 0);
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ g_object_notify (G_OBJECT (meta_backend), "cache");
+}
+
+/**
+ * e_book_meta_backend_ref_cache:
+ * @meta_backend: an #EBookMetaBackend
+ *
+ * Returns: (transfer full): Referenced #EBookCache, which is used by @meta_backend.
+ * Unref it with g_object_unref(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EBookCache *
+e_book_meta_backend_ref_cache (EBookMetaBackend *meta_backend)
+{
+ EBookCache *cache;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), NULL);
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ if (meta_backend->priv->cache)
+ cache = g_object_ref (meta_backend->priv->cache);
+ else
+ cache = NULL;
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ return cache;
+}
+
+static gchar *
+ebmb_get_mime_type (const gchar *url,
+ const gchar *content,
+ gsize content_len)
+{
+ gchar *content_type, *filename = NULL, *mime_type = NULL;
+
+ if (url) {
+ filename = g_filename_from_uri (url, NULL, NULL);
+ if (filename) {
+ gchar *extension;
+
+ /* When storing inline attachments to the local file,
+ the file extension is the mime type as stored in the attribute */
+ extension = strrchr (filename, '.');
+ if (extension)
+ extension++;
+
+ if (extension) {
+ mime_type = g_uri_unescape_string (extension, NULL);
+ if (mime_type && !strchr (mime_type, '/')) {
+ gchar *tmp;
+
+ tmp = g_strconcat ("image/", mime_type, NULL);
+
+ g_free (mime_type);
+ mime_type = tmp;
+ }
+
+ content_type = g_content_type_from_mime_type (mime_type);
+
+ if (!content_type) {
+ g_free (mime_type);
+ mime_type = NULL;
+ }
+
+ g_free (content_type);
+ }
+ }
+ }
+
+ if (!mime_type) {
+ content_type = g_content_type_guess (filename, (const guchar *) content, content_len, NULL);
+
+ if (content_type)
+ mime_type = g_content_type_get_mime_type (content_type);
+
+ g_free (content_type);
+ }
+
+ g_free (filename);
+
+ return mime_type;
+}
+
+/**
+ * e_book_meta_backend_inline_local_photos_sync:
+ * @meta_backend: an #EBookMetaBackend
+ * @contact: an #EContact to work with
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Changes all URL photos and logos which point to a local file in @contact
+ * to inline type, aka adds the file content into the @contact.
+ * This is called automatically before e_book_meta_backend_save_contact_sync().
+ *
+ * The reverse operation is e_book_meta_backend_store_inline_photos_sync().
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_inline_local_photos_sync (EBookMetaBackend *meta_backend,
+ EContact *contact,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GList *attributes, *link;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+
+ attributes = e_vcard_get_attributes (E_VCARD (contact));
+
+ for (link = attributes; link; link = g_list_next (link)) {
+ EVCardAttribute *attr = link->data;
+ const gchar *attr_name;
+ GList *values;
+
+ attr_name = e_vcard_attribute_get_name (attr);
+ if (!attr_name || (
+ g_ascii_strcasecmp (attr_name, EVC_PHOTO) != 0 &&
+ g_ascii_strcasecmp (attr_name, EVC_LOGO) != 0)) {
+ continue;
+ }
+
+ values = e_vcard_attribute_get_param (attr, EVC_VALUE);
+ if (values && g_ascii_strcasecmp (values->data, "uri") == 0) {
+ gchar *url;
+
+ url = e_vcard_attribute_get_value (attr);
+ if (url && g_str_has_prefix (url, LOCAL_PREFIX)) {
+ GFile *file;
+ gchar *basename;
+ gchar *content;
+ gsize len;
+
+ file = g_file_new_for_uri (url);
+ basename = g_file_get_basename (file);
+ if (g_file_load_contents (file, cancellable, &content, &len, NULL, error)) {
+ gchar *mime_type;
+ const gchar *image_type, *pp;
+
+ mime_type = ebmb_get_mime_type (url, content, len);
+ if (mime_type && (pp = strchr (mime_type, '/'))) {
+ image_type = pp + 1;
+ } else {
+ image_type = "X-EVOLUTION-UNKNOWN";
+ }
+
+ e_vcard_attribute_remove_param (attr, EVC_TYPE);
+ e_vcard_attribute_remove_param (attr, EVC_ENCODING);
+ e_vcard_attribute_remove_param (attr, EVC_VALUE);
+ e_vcard_attribute_remove_values (attr);
+
+ e_vcard_attribute_add_param_with_value (attr,
e_vcard_attribute_param_new (EVC_TYPE), image_type);
+ e_vcard_attribute_add_param_with_value (attr,
e_vcard_attribute_param_new (EVC_ENCODING), "b");
+ e_vcard_attribute_add_value_decoded (attr, content, len);
+
+ g_free (mime_type);
+ g_free (content);
+ } else {
+ success = FALSE;
+ }
+
+ g_object_unref (file);
+ g_free (basename);
+ }
+
+ g_free (url);
+ }
+ }
+
+ return success;
+}
+
+static gchar *
+ebmb_create_photo_local_filename (EBookMetaBackend *meta_backend,
+ const gchar *uid,
+ const gchar *attr_name,
+ gint fileindex,
+ const gchar *type)
+{
+ EBookCache *book_cache;
+ gchar *local_filename, *cache_path, *checksum, *prefix, *extension, *filename;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), NULL);
+ g_return_val_if_fail (uid != NULL, NULL);
+ g_return_val_if_fail (attr_name != NULL, NULL);
+
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
+ g_return_val_if_fail (book_cache != NULL, NULL);
+
+ cache_path = g_path_get_dirname (e_cache_get_filename (E_CACHE (book_cache)));
+ checksum = g_compute_checksum_for_string (G_CHECKSUM_SHA1, uid, -1);
+ prefix = g_strdup_printf ("%s-%s-%d", attr_name, checksum, fileindex);
+
+ if (type && *type)
+ extension = g_uri_escape_string (type, NULL, TRUE);
+ else
+ extension = NULL;
+
+ filename = g_strconcat (prefix, extension ? "." : NULL, extension, NULL);
+
+ local_filename = g_build_filename (cache_path, filename, NULL);
+
+ g_object_unref (book_cache);
+ g_free (cache_path);
+ g_free (checksum);
+ g_free (prefix);
+ g_free (extension);
+ g_free (filename);
+
+ return local_filename;
+}
+
+/**
+ * e_book_meta_backend_store_inline_photos_sync:
+ * @meta_backend: an #EBookMetaBackend
+ * @contact: an #EContact to work with
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Changes all inline photos and logos to URL type in @contact, which
+ * will point to a local file instead, beside the cache file.
+ * This is called automatically after e_book_meta_backend_load_contact_sync().
+ *
+ * The reverse operation is e_book_meta_backend_inline_local_photos_sync().
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_store_inline_photos_sync (EBookMetaBackend *meta_backend,
+ EContact *contact,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gint fileindex;
+ GList *attributes, *link;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+
+ attributes = e_vcard_get_attributes (E_VCARD (contact));
+
+ for (link = attributes, fileindex = 0; link; link = g_list_next (link), fileindex++) {
+ EVCardAttribute *attr = link->data;
+ const gchar *attr_name;
+ GList *values;
+
+ attr_name = e_vcard_attribute_get_name (attr);
+ if (!attr_name || (
+ g_ascii_strcasecmp (attr_name, EVC_PHOTO) != 0 &&
+ g_ascii_strcasecmp (attr_name, EVC_LOGO) != 0)) {
+ continue;
+ }
+
+ values = e_vcard_attribute_get_param (attr, EVC_ENCODING);
+ if (values && (g_ascii_strcasecmp (values->data, "b") == 0 || g_ascii_strcasecmp
(values->data, "base64") == 0)) {
+ values = e_vcard_attribute_get_values_decoded (attr);
+ if (values && values->data) {
+ const GString *decoded = values->data;
+ gchar *local_filename;
+
+ if (!decoded->len)
+ continue;
+
+ values = e_vcard_attribute_get_param (attr, EVC_TYPE);
+
+ local_filename = ebmb_create_photo_local_filename (meta_backend,
e_contact_get_const (contact, E_CONTACT_UID),
+ attr_name, fileindex, values ? values->data : NULL);
+ if (local_filename &&
+ g_file_set_contents (local_filename, decoded->str, decoded->len, error)) {
+ gchar *url;
+
+ e_vcard_attribute_remove_param (attr, EVC_TYPE);
+ e_vcard_attribute_remove_param (attr, EVC_ENCODING);
+ e_vcard_attribute_remove_param (attr, EVC_VALUE);
+ e_vcard_attribute_remove_values (attr);
+
+ url = g_filename_to_uri (local_filename, NULL, NULL);
+
+ e_vcard_attribute_add_param_with_value (attr,
e_vcard_attribute_param_new (EVC_VALUE), "uri");
+ e_vcard_attribute_add_value (attr, url);
+
+ g_free (url);
+ } else {
+ success = FALSE;
+ }
+
+ g_free (local_filename);
+ }
+ }
+ }
+
+ return success;
+}
+
+/**
+ * e_book_meta_backend_empty_cache_sync:
+ * @meta_backend: an #EBookMetaBackend
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Empties the local cache by removing all known contacts from it
+ * and notifies about such removal any opened views.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_empty_cache_sync (EBookMetaBackend *meta_backend,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookBackend *book_backend;
+ EBookCache *book_cache;
+ GSList *uids = NULL, *link;
+ gchar *cache_path, *cache_filename;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
+ g_return_val_if_fail (book_cache != NULL, FALSE);
+
+ e_cache_lock (E_CACHE (book_cache), E_CACHE_LOCK_WRITE);
+
+ book_backend = E_BOOK_BACKEND (meta_backend);
+
+ success = e_book_cache_search_uids (book_cache, NULL, &uids, cancellable, error);
+ if (success)
+ success = e_cache_remove_all (E_CACHE (book_cache), cancellable, error);
+
+ e_cache_unlock (E_CACHE (book_cache), success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+ cache_path = g_path_get_dirname (e_cache_get_filename (E_CACHE (book_cache)));
+ cache_filename = g_path_get_basename (e_cache_get_filename (E_CACHE (book_cache)));
+
+ g_object_unref (book_cache);
+
+ if (success) {
+ GDir *dir;
+
+ for (link = uids; link; link = g_slist_next (link)) {
+ const gchar *uid = link->data;
+
+ if (!uid)
+ continue;
+
+ e_book_backend_notify_remove (book_backend, uid);
+ }
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ for (link = meta_backend->priv->cursors; link; link = g_slist_next (link)) {
+ EDataBookCursor *cursor = link->data;
+
+ e_data_book_cursor_recalculate (cursor, cancellable, NULL);
+ }
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ /* Remove also all photos and logos stored beside the cache */
+ dir = g_dir_open (cache_path, 0, NULL);
+ if (dir) {
+ const gchar *filename;
+
+ while (filename = g_dir_read_name (dir), filename) {
+ if ((g_str_has_prefix (filename, EVC_PHOTO) ||
+ g_str_has_prefix (filename, EVC_LOGO)) &&
+ g_strcmp0 (cache_filename, filename) != 0) {
+ if (g_unlink (filename) == -1) {
+ /* Something failed, ignore the error */
+ }
+ }
+ }
+
+ g_dir_close (dir);
+ }
+ }
+
+ g_slist_free_full (uids, g_free);
+ g_free (cache_filename);
+ g_free (cache_path);
+
+ return success;
+}
+
+/**
+ * e_book_meta_backend_connect_sync:
+ * @meta_backend: an #EBookMetaBackend
+ * @credentials: (nullable): an #ENamedParameters with previously used credentials, or %NULL
+ * @out_auth_result: (out): an #ESourceAuthenticationResult with an authentication result
+ * @out_certificate_pem: (out) (transfer full): a PEM encoded certificate on failure, or %NULL
+ * @out_certificate_errors: (out): a #GTlsCertificateFlags on failure, or 0
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * This is called always before any operation which requires a connection
+ * to the remote side. It can fail with an #E_CLIENT_ERROR_REPOSITORY_OFFLINE
+ * error to indicate that the remote side cannot be currently reached. Other
+ * errors are propagated to the caller/client side. This method is not called
+ * when the backend is offline.
+ *
+ * The descendant should also call e_book_backend_set_writable() after successful
+ * connect to the remote side. This value is stored for later use, when being
+ * opened offline.
+ *
+ * The @credentials parameter consists of the previously used credentials.
+ * It's always %NULL with the first connection attempt. To get the credentials,
+ * just set the @out_auth_result to %E_SOURCE_AUTHENTICATION_REQUIRED for
+ * the first time and the function will be called again once the credentials
+ * are available. See the documentation of #ESourceAuthenticationResult for
+ * other available results.
+ *
+ * The out parameters are passed to e_backend_schedule_credentials_required()
+ * and are ignored when the descendant returns %TRUE, aka they are used
+ * only if the connection fails. The @out_certificate_pem and @out_certificate_errors
+ * should be used together and they can be left untouched if the failure reason was
+ * not related to certificate. Use @out_auth_result %E_SOURCE_AUTHENTICATION_UNKNOWN
+ * to indicate other error than @credentials error, otherwise the @error is used
+ * according to @out_auth_result value.
+ *
+ * It is mandatory to implement this virtual method by the descendant.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_connect_sync (EBookMetaBackend *meta_backend,
+ const ENamedParameters *credentials,
+ ESourceAuthenticationResult *out_auth_result,
+ gchar **out_certificate_pem,
+ GTlsCertificateFlags *out_certificate_errors,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackendClass *klass;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+
+ klass = E_BOOK_META_BACKEND_GET_CLASS (meta_backend);
+ g_return_val_if_fail (klass != NULL, FALSE);
+ g_return_val_if_fail (klass->connect_sync != NULL, FALSE);
+
+ return klass->connect_sync (meta_backend, credentials, out_auth_result, out_certificate_pem,
out_certificate_errors, cancellable, error);
+}
+
+/**
+ * e_book_meta_backend_disconnect_sync:
+ * @meta_backend: an #EBookMetaBackend
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * This is called when the backend goes into offline mode or
+ * when the disconnect is required. The implementation should
+ * not report any error when it is called and the @meta_backend
+ * is not connected.
+ *
+ * It is mandatory to implement this virtual method by the descendant.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_disconnect_sync (EBookMetaBackend *meta_backend,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackendClass *klass;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+
+ klass = E_BOOK_META_BACKEND_GET_CLASS (meta_backend);
+ g_return_val_if_fail (klass != NULL, FALSE);
+ g_return_val_if_fail (klass->disconnect_sync != NULL, FALSE);
+
+ return klass->disconnect_sync (meta_backend, cancellable, error);
+}
+
+/**
+ * e_book_meta_backend_get_changes_sync:
+ * @meta_backend: an #EBookMetaBackend
+ * @last_sync_tag: (nullable): optional sync tag from the last check
+ * @is_repeat: set to %TRUE when this is the repeated call
+ * @out_new_sync_tag: (out) (transfer full): new sync tag to store on success
+ * @out_repeat: (out): whether to repeat this call again; default is %FALSE
+ * @out_created_objects: (out) (element-type EBookMetaBackendInfo) (transfer full):
+ * a #GSList of #EBookMetaBackendInfo object infos which had been created since
+ * the last check
+ * @out_modified_objects: (out) (element-type EBookMetaBackendInfo) (transfer full):
+ * a #GSList of #EBookMetaBackendInfo object infos which had been modified since
+ * the last check
+ * @out_removed_objects: (out) (element-type EBookMetaBackendInfo) (transfer full):
+ * a #GSList of #EBookMetaBackendInfo object infos which had been removed since
+ * the last check
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gathers the changes since the last check which had been done
+ * on the remote side.
+ *
+ * The @last_sync_tag can be used as a tag of the last check. This can be %NULL,
+ * when there was no previous call or when the descendant doesn't store any
+ * such tags. The @out_new_sync_tag can be populated with a value to be stored
+ * and used the next time.
+ *
+ * The @out_repeat can be set to %TRUE when the descendant didn't finish
+ * read of all the changes. In that case the @meta_backend calls this
+ * function again with the @out_new_sync_tag as the @last_sync_tag, but also
+ * notifies about the found changes immediately. The @is_repeat is set
+ * to %TRUE as well in this case, otherwise it's %FALSE.
+ *
+ * The descendant can populate also EBookMetaBackendInfo::object of
+ * the @out_created_objects and @out_modified_objects, if known, in which
+ * case this will be used instead of loading it with e_book_meta_backend_load_contact_sync().
+ *
+ * It is optional to implement this virtual method by the descendant.
+ * The default implementation calls e_book_meta_backend_list_existing_sync()
+ * and then compares the list with the current content of the local cache
+ * and populates the respective lists appropriately.
+ *
+ * Each output #GSList should be freed with
+ * g_slist_free_full (objects, e_book_meta_backend_info_free);
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_get_changes_sync (EBookMetaBackend *meta_backend,
+ const gchar *last_sync_tag,
+ gboolean is_repeat,
+ gchar **out_new_sync_tag,
+ gboolean *out_repeat,
+ GSList **out_created_objects,
+ GSList **out_modified_objects,
+ GSList **out_removed_objects,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackendClass *klass;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (out_new_sync_tag != NULL, FALSE);
+ g_return_val_if_fail (out_repeat != NULL, FALSE);
+ g_return_val_if_fail (out_created_objects != NULL, FALSE);
+ g_return_val_if_fail (out_created_objects != NULL, FALSE);
+ g_return_val_if_fail (out_modified_objects != NULL, FALSE);
+ g_return_val_if_fail (out_removed_objects != NULL, FALSE);
+
+ klass = E_BOOK_META_BACKEND_GET_CLASS (meta_backend);
+ g_return_val_if_fail (klass != NULL, FALSE);
+ g_return_val_if_fail (klass->get_changes_sync != NULL, FALSE);
+
+ return klass->get_changes_sync (meta_backend,
+ last_sync_tag,
+ is_repeat,
+ out_new_sync_tag,
+ out_repeat,
+ out_created_objects,
+ out_modified_objects,
+ out_removed_objects,
+ cancellable,
+ error);
+}
+
+/**
+ * e_book_meta_backend_list_existing_sync:
+ * @meta_backend: an #EBookMetaBackend
+ * @out_new_sync_tag: (out) (transfer full): optional return location for a new sync tag
+ * @out_existing_objects: (out) (element-type EBookMetaBackendInfo) (transfer full):
+ * a #GSList of #EBookMetaBackendInfo object infos which are stored on the remote side
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Used to get list of all existing objects on the remote side. The descendant
+ * can optionally provide @out_new_sync_tag, which will be stored on success, if
+ * not %NULL. The descendant can populate also EBookMetaBackendInfo::object of
+ * the @out_existing_objects, if known, in which case this will be used instead
+ * of loading it with e_book_meta_backend_load_contact_sync().
+ *
+ * It is mandatory to implement this virtual method by the descendant, unless
+ * it implements its own get_changes_sync().
+ *
+ * The @out_existing_objects #GSList should be freed with
+ * g_slist_free_full (objects, e_book_meta_backend_info_free);
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_list_existing_sync (EBookMetaBackend *meta_backend,
+ gchar **out_new_sync_tag,
+ GSList **out_existing_objects,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackendClass *klass;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (out_existing_objects != NULL, FALSE);
+
+ klass = E_BOOK_META_BACKEND_GET_CLASS (meta_backend);
+ g_return_val_if_fail (klass != NULL, FALSE);
+ g_return_val_if_fail (klass->list_existing_sync != NULL, FALSE);
+
+ return klass->list_existing_sync (meta_backend, out_new_sync_tag, out_existing_objects, cancellable,
error);
+}
+
+/**
+ * e_book_meta_backend_load_contact_sync:
+ * @meta_backend: an #EBookMetaBackend
+ * @uid: a contact UID
+ * @extra: (nullable): optional extra data stored with the contact, or %NULL
+ * @out_contact: (out) (transfer full): a loaded contact, as an #EContact
+ * @out_extra: (out) (transfer full): an extra data to store to #EBookCache with this contact
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Loads a contact from the remote side.
+ *
+ * It is mandatory to implement this virtual method by the descendant.
+ *
+ * The returned @out_contact should be freed with g_object_unref(),
+ * when no longer needed.
+ *
+ * The returned @out_extra should be freed with g_free(), when no longer
+ * needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_load_contact_sync (EBookMetaBackend *meta_backend,
+ const gchar *uid,
+ const gchar *extra,
+ EContact **out_contact,
+ gchar **out_extra,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackendClass *klass;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (out_contact != NULL, FALSE);
+ g_return_val_if_fail (out_extra != NULL, FALSE);
+
+ klass = E_BOOK_META_BACKEND_GET_CLASS (meta_backend);
+ g_return_val_if_fail (klass != NULL, FALSE);
+ g_return_val_if_fail (klass->load_contact_sync != NULL, FALSE);
+
+ return klass->load_contact_sync (meta_backend, uid, extra, out_contact, out_extra, cancellable,
error);
+}
+
+/**
+ * e_book_meta_backend_save_contact_sync:
+ * @meta_backend: an #EBookMetaBackend
+ * @overwrite_existing: %TRUE when can overwrite existing contacts, %FALSE otherwise
+ * @conflict_resolution: one of #EConflictResolution, what to do on conflicts
+ * @contact: an #EContact to save
+ * @extra: (nullable): extra data saved with the contacts in an #EBookCache
+ * @out_new_uid: (out) (transfer full): return location for the UID of the saved contact
+ * @out_new_extra: (out) (transfer full): return location for the extra data to store with the contact
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Saves one contact into the remote side. When the @overwrite_existing is %TRUE, then
+ * the descendant can overwrite an object with the same UID on the remote side
+ * (usually used for modify). The @conflict_resolution defines what to do when
+ * the remote side had made any changes to the object since the last update.
+ *
+ * The @contact has already converted locally stored photos and logos
+ * into inline variants, thus it's not needed to call
+ * e_book_meta_backend_inline_local_photos_sync() by the descendant.
+ *
+ * The @out_new_uid can be populated with a UID of the saved contact as the server
+ * assigned it to it. This UID, if set, is loaded from the remote side afterwards,
+ * also to see whether any changes had been made to the contact by the remote side.
+ *
+ * The @out_new_extra can be populated with a new extra data to save with the contact.
+ * Left it %NULL, to keep the same value as the @extra.
+ *
+ * The descendant can use an #E_CLIENT_ERROR_OUT_OF_SYNC error to indicate that
+ * the save failed due to made changes on the remote side, and let the @meta_backend
+ * resolve this conflict based on the @conflict_resolution on its own.
+ * The #E_CLIENT_ERROR_OUT_OF_SYNC error should not be used when the descendant
+ * is able to resolve the conflicts itself.
+ *
+ * It is mandatory to implement this virtual method by the writable descendant.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_save_contact_sync (EBookMetaBackend *meta_backend,
+ gboolean overwrite_existing,
+ EConflictResolution conflict_resolution,
+ /* const */ EContact *contact,
+ const gchar *extra,
+ gchar **out_new_uid,
+ gchar **out_new_extra,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackendClass *klass;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+ g_return_val_if_fail (out_new_uid != NULL, FALSE);
+ g_return_val_if_fail (out_new_extra != NULL, FALSE);
+
+ klass = E_BOOK_META_BACKEND_GET_CLASS (meta_backend);
+ g_return_val_if_fail (klass != NULL, FALSE);
+
+ if (!klass->save_contact_sync) {
+ g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_NOT_SUPPORTED, NULL));
+ return FALSE;
+ }
+
+ return klass->save_contact_sync (meta_backend,
+ overwrite_existing,
+ conflict_resolution,
+ contact,
+ extra,
+ out_new_uid,
+ out_new_extra,
+ cancellable,
+ error);
+}
+
+/**
+ * e_book_meta_backend_remove_contact_sync:
+ * @meta_backend: an #EBookMetaBackend
+ * @conflict_resolution: an #EConflictResolution to use
+ * @uid: a contact UID
+ * @extra: (nullable): extra data being saved with the contact in the local cache, or %NULL
+ * @object: (nullable): corresponding vCard object, as stored in the local cache, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Removes a contact from the remote side. The @object is not %NULL when
+ * it's removing locally deleted object in offline mode. Being it %NULL,
+ * the descendant can obtain the object from the #EBookCache.
+ *
+ * It is mandatory to implement this virtual method by the writable descendant.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_remove_contact_sync (EBookMetaBackend *meta_backend,
+ EConflictResolution conflict_resolution,
+ const gchar *uid,
+ const gchar *extra,
+ const gchar *object,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackendClass *klass;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ klass = E_BOOK_META_BACKEND_GET_CLASS (meta_backend);
+ g_return_val_if_fail (klass != NULL, FALSE);
+
+ if (!klass->remove_contact_sync) {
+ g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_NOT_SUPPORTED, NULL));
+ return FALSE;
+ }
+
+ return klass->remove_contact_sync (meta_backend, conflict_resolution, uid, extra, object,
cancellable, error);
+}
+
+/**
+ * e_book_meta_backend_search_sync:
+ * @meta_backend: an #EBookMetaBackend
+ * @expr: (nullable): a search expression, or %NULL
+ * @meta_contact: %TRUE, when return #EContact filled with UID and REV only, %FALSE to return full contacts
+ * @out_contacts: (out) (transfer full) (element-type EContact): return location for the found contacts as
#EContact
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Searches @meta_backend with given expression @expr and returns
+ * found contacts as a #GSList of #EContact @out_contacts.
+ * Free the returned @out_contacts with g_slist_free_full (contacts, g_object_unref);
+ * when no longer needed.
+ * When the @expr is %NULL, all objects are returned. To get
+ * UID-s instead, call e_book_meta_backend_search_uids_sync().
+ *
+ * It is optional to implement this virtual method by the descendant.
+ * The default implementation searches @meta_backend's cache. It's also
+ * not required to be online for searching, thus @meta_backend doesn't
+ * ensure it.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_search_sync (EBookMetaBackend *meta_backend,
+ const gchar *expr,
+ gboolean meta_contact,
+ GSList **out_contacts,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackendClass *klass;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (out_contacts != NULL, FALSE);
+
+ klass = E_BOOK_META_BACKEND_GET_CLASS (meta_backend);
+ g_return_val_if_fail (klass != NULL, FALSE);
+ g_return_val_if_fail (klass->search_sync != NULL, FALSE);
+
+ return klass->search_sync (meta_backend, expr, meta_contact, out_contacts, cancellable, error);
+}
+
+/**
+ * e_book_meta_backend_search_uids_sync:
+ * @meta_backend: an #EBookMetaBackend
+ * @expr: (nullable): a search expression, or %NULL
+ * @out_uids: (out) (transfer full) (element-type utf8): return location for the found contact UID-s
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Searches @meta_backend with given expression @expr and returns
+ * found contact UID-s as a #GSList @out_contacts.
+ * Free the returned @out_uids with g_slist_free_full (uids, g_free);
+ * when no longer needed.
+ * When the @expr is %NULL, all UID-s are returned. To get #EContact-s
+ * instead, call e_book_meta_backend_search_sync().
+ *
+ * It is optional to implement this virtual method by the descendant.
+ * The default implementation searches @meta_backend's cache. It's also
+ * not required to be online for searching, thus @meta_backend doesn't
+ * ensure it.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_search_uids_sync (EBookMetaBackend *meta_backend,
+ const gchar *expr,
+ GSList **out_uids,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackendClass *klass;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (out_uids != NULL, FALSE);
+
+ klass = E_BOOK_META_BACKEND_GET_CLASS (meta_backend);
+ g_return_val_if_fail (klass != NULL, FALSE);
+ g_return_val_if_fail (klass->search_uids_sync != NULL, FALSE);
+
+ return klass->search_uids_sync (meta_backend, expr, out_uids, cancellable, error);
+}
+
+/**
+ * e_book_meta_backend_requires_reconnect:
+ * @meta_backend: an #EBookMetaBackend
+ *
+ * Determines, whether current source content requires reconnect of the backend.
+ *
+ * It is optional to implement this virtual method by the descendant. The default
+ * implementation compares %E_SOURCE_EXTENSION_AUTHENTICATION and
+ * %E_SOURCE_EXTENSION_WEBDAV_BACKEND, if existing in the source,
+ * with the values after the last successful connect and returns
+ * %TRUE when they changed. It always return %TRUE when there was
+ * no successful connect done yet.
+ *
+ * Returns: %TRUE, when reconnect is required, %FALSE otherwise.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_book_meta_backend_requires_reconnect (EBookMetaBackend *meta_backend)
+{
+ EBookMetaBackendClass *klass;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND (meta_backend), FALSE);
+
+ klass = E_BOOK_META_BACKEND_GET_CLASS (meta_backend);
+ g_return_val_if_fail (klass != NULL, FALSE);
+ g_return_val_if_fail (klass->requires_reconnect != NULL, FALSE);
+
+ return klass->requires_reconnect (meta_backend);
+}
diff --git a/src/addressbook/libedata-book/e-book-meta-backend.h
b/src/addressbook/libedata-book/e-book-meta-backend.h
new file mode 100644
index 0000000..740f321
--- /dev/null
+++ b/src/addressbook/libedata-book/e-book-meta-backend.h
@@ -0,0 +1,276 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#if !defined (__LIBEDATA_BOOK_H_INSIDE__) && !defined (LIBEDATA_BOOK_COMPILATION)
+#error "Only <libedata-book/libedata-book.h> should be included directly."
+#endif
+
+#ifndef E_BOOK_META_BACKEND_H
+#define E_BOOK_META_BACKEND_H
+
+#include <libebackend/libebackend.h>
+#include <libedata-book/e-book-backend.h>
+#include <libedata-book/e-book-cache.h>
+#include <libebook-contacts/libebook-contacts.h>
+
+/* Standard GObject macros */
+#define E_TYPE_BOOK_META_BACKEND \
+ (e_book_meta_backend_get_type ())
+#define E_BOOK_META_BACKEND(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST \
+ ((obj), E_TYPE_BOOK_META_BACKEND, EBookMetaBackend))
+#define E_BOOK_META_BACKEND_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_CAST \
+ ((cls), E_TYPE_BOOK_META_BACKEND, EBookMetaBackendClass))
+#define E_IS_BOOK_META_BACKEND(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE \
+ ((obj), E_TYPE_BOOK_META_BACKEND))
+#define E_IS_BOOK_META_BACKEND_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_TYPE \
+ ((cls), E_TYPE_BOOK_META_BACKEND))
+#define E_BOOK_META_BACKEND_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS \
+ ((obj), E_TYPE_BOOK_META_BACKEND, EBookMetaBackendClass))
+
+G_BEGIN_DECLS
+
+typedef struct _EBookMetaBackendInfo {
+ gchar *uid;
+ gchar *revision;
+ gchar *object;
+ gchar *extra;
+} EBookMetaBackendInfo;
+
+#define E_TYPE_BOOK_META_BACKEND_INFO (e_book_meta_backend_info_get_type ())
+
+GType e_book_meta_backend_info_get_type
+ (void) G_GNUC_CONST;
+EBookMetaBackendInfo *
+ e_book_meta_backend_info_new (const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ const gchar *extra);
+EBookMetaBackendInfo *
+ e_book_meta_backend_info_copy (const EBookMetaBackendInfo *src);
+void e_book_meta_backend_info_free (gpointer ptr /* EBookMetaBackendInfo * */);
+
+typedef struct _EBookMetaBackend EBookMetaBackend;
+typedef struct _EBookMetaBackendClass EBookMetaBackendClass;
+typedef struct _EBookMetaBackendPrivate EBookMetaBackendPrivate;
+
+/**
+ * EBookMetaBackend:
+ *
+ * Contains only private data that should be read and manipulated using
+ * the functions below.
+ *
+ * Since: 3.26
+ **/
+struct _EBookMetaBackend {
+ /*< private >*/
+ EBookBackend parent;
+ EBookMetaBackendPrivate *priv;
+};
+
+/**
+ * EBookMetaBackendClass:
+ *
+ * Class structure for the #EBookMetaBackend class.
+ *
+ * Since: 3.26
+ */
+struct _EBookMetaBackendClass {
+ /*< private >*/
+ EBookBackendClass parent_class;
+
+ /* For Direct Read Access */
+ const gchar *backend_module_filename;
+ const gchar *backend_factory_type_name;
+
+ /* Virtual methods */
+ gboolean (* connect_sync) (EBookMetaBackend *meta_backend,
+ const ENamedParameters *credentials,
+ ESourceAuthenticationResult *out_auth_result,
+ gchar **out_certificate_pem,
+ GTlsCertificateFlags *out_certificate_errors,
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (* disconnect_sync) (EBookMetaBackend *meta_backend,
+ GCancellable *cancellable,
+ GError **error);
+
+ gboolean (* get_changes_sync) (EBookMetaBackend *meta_backend,
+ const gchar *last_sync_tag,
+ gboolean is_repeat,
+ gchar **out_new_sync_tag,
+ gboolean *out_repeat,
+ GSList **out_created_objects, /* EBookMetaBackendInfo * */
+ GSList **out_modified_objects, /* EBookMetaBackendInfo * */
+ GSList **out_removed_objects, /* EBookMetaBackendInfo * */
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (* list_existing_sync) (EBookMetaBackend *meta_backend,
+ gchar **out_new_sync_tag,
+ GSList **out_existing_objects, /* EBookMetaBackendInfo * */
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (* load_contact_sync) (EBookMetaBackend *meta_backend,
+ const gchar *uid,
+ const gchar *extra,
+ EContact **out_contact,
+ gchar **out_extra,
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (* save_contact_sync) (EBookMetaBackend *meta_backend,
+ gboolean overwrite_existing,
+ EConflictResolution conflict_resolution,
+ /* const */ EContact *contact,
+ const gchar *extra,
+ gchar **out_new_uid,
+ gchar **out_new_extra,
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (* remove_contact_sync) (EBookMetaBackend *meta_backend,
+ EConflictResolution conflict_resolution,
+ const gchar *uid,
+ const gchar *extra,
+ const gchar *object,
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (* search_sync) (EBookMetaBackend *meta_backend,
+ const gchar *expr,
+ gboolean meta_contact,
+ GSList **out_contacts, /* EContact * */
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (* search_uids_sync) (EBookMetaBackend *meta_backend,
+ const gchar *expr,
+ GSList **out_uids, /* gchar * */
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (* requires_reconnect) (EBookMetaBackend *meta_backend);
+
+ /* Signals */
+ void (* source_changed) (EBookMetaBackend *meta_backend);
+
+ /* Padding for future expansion */
+ gpointer reserved[10];
+};
+
+GType e_book_meta_backend_get_type (void) G_GNUC_CONST;
+
+const gchar * e_book_meta_backend_get_capabilities
+ (EBookMetaBackend *meta_backend);
+void e_book_meta_backend_set_ever_connected
+ (EBookMetaBackend *meta_backend,
+ gboolean value);
+gboolean e_book_meta_backend_get_ever_connected
+ (EBookMetaBackend *meta_backend);
+void e_book_meta_backend_set_connected_writable
+ (EBookMetaBackend *meta_backend,
+ gboolean value);
+gboolean e_book_meta_backend_get_connected_writable
+ (EBookMetaBackend *meta_backend);
+void e_book_meta_backend_set_cache (EBookMetaBackend *meta_backend,
+ EBookCache *cache);
+EBookCache * e_book_meta_backend_ref_cache (EBookMetaBackend *meta_backend);
+gboolean e_book_meta_backend_inline_local_photos_sync
+ (EBookMetaBackend *meta_backend,
+ EContact *contact,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_meta_backend_store_inline_photos_sync
+ (EBookMetaBackend *meta_backend,
+ EContact *contact,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_meta_backend_empty_cache_sync
+ (EBookMetaBackend *meta_backend,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_meta_backend_connect_sync(EBookMetaBackend *meta_backend,
+ const ENamedParameters *credentials,
+ ESourceAuthenticationResult *out_auth_result,
+ gchar **out_certificate_pem,
+ GTlsCertificateFlags *out_certificate_errors,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_meta_backend_disconnect_sync
+ (EBookMetaBackend *meta_backend,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_meta_backend_get_changes_sync
+ (EBookMetaBackend *meta_backend,
+ const gchar *last_sync_tag,
+ gboolean is_repeat,
+ gchar **out_new_sync_tag,
+ gboolean *out_repeat,
+ GSList **out_created_objects, /* EBookMetaBackendInfo * */
+ GSList **out_modified_objects, /* EBookMetaBackendInfo * */
+ GSList **out_removed_objects, /* EBookMetaBackendInfo * */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_meta_backend_list_existing_sync
+ (EBookMetaBackend *meta_backend,
+ gchar **out_new_sync_tag,
+ GSList **out_existing_objects, /* EBookMetaBackendInfo * */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_meta_backend_load_contact_sync
+ (EBookMetaBackend *meta_backend,
+ const gchar *uid,
+ const gchar *extra,
+ EContact **out_contact,
+ gchar **out_extra,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_meta_backend_save_contact_sync
+ (EBookMetaBackend *meta_backend,
+ gboolean overwrite_existing,
+ EConflictResolution conflict_resolution,
+ /* const */ EContact *contact,
+ const gchar *extra,
+ gchar **out_new_uid,
+ gchar **out_new_extra,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_meta_backend_remove_contact_sync
+ (EBookMetaBackend *meta_backend,
+ EConflictResolution conflict_resolution,
+ const gchar *uid,
+ const gchar *extra,
+ const gchar *object,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_meta_backend_search_sync (EBookMetaBackend *meta_backend,
+ const gchar *expr,
+ gboolean meta_contact,
+ GSList **out_contacts, /* EContact * */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_meta_backend_search_uids_sync
+ (EBookMetaBackend *meta_backend,
+ const gchar *expr,
+ GSList **out_uids, /* gchar * */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_book_meta_backend_requires_reconnect
+ (EBookMetaBackend *meta_backend);
+
+G_END_DECLS
+
+#endif /* E_BOOK_META_BACKEND_H */
diff --git a/src/addressbook/libedata-book/e-data-book-cursor-cache.c
b/src/addressbook/libedata-book/e-data-book-cursor-cache.c
new file mode 100644
index 0000000..e6c22ce
--- /dev/null
+++ b/src/addressbook/libedata-book/e-data-book-cursor-cache.c
@@ -0,0 +1,438 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2013 Intel Corporation
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Tristan Van Berkom <tristanvb openismus com>
+ */
+
+/**
+ * SECTION: e-data-book-cursor-cache
+ * @include: libedata-book/libedata-book.h
+ * @short_description: The SQLite cursor implementation
+ *
+ * This cursor implementation can be used with any backend which
+ * stores contacts using #EBookCache.
+ */
+
+#include "evolution-data-server-config.h"
+
+#include <glib/gi18n.h>
+
+#include "e-data-book-cursor-cache.h"
+
+struct _EDataBookCursorCachePrivate {
+ EBookCache *book_cache;
+ EBookCacheCursor *cursor;
+};
+
+enum {
+ PROP_0,
+ PROP_BOOK_CACHE,
+ PROP_CURSOR,
+};
+
+G_DEFINE_TYPE (EDataBookCursorCache, e_data_book_cursor_cache, E_TYPE_DATA_BOOK_CURSOR);
+
+static gboolean
+edbcc_set_sexp (EDataBookCursor *cursor,
+ const gchar *sexp,
+ GError **error)
+{
+ EDataBookCursorCache *cache_cursor;
+ gboolean success;
+ GError *local_error = NULL;
+
+ cache_cursor = E_DATA_BOOK_CURSOR_CACHE (cursor);
+
+ success = e_book_cache_cursor_set_sexp (cache_cursor->priv->book_cache, cache_cursor->priv->cursor,
sexp, &local_error);
+
+ if (!success) {
+ if (g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY)) {
+ g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_INVALID_QUERY,
local_error->message);
+ g_clear_error (&local_error);
+ } else {
+ g_propagate_error (error, local_error);
+ }
+ }
+
+ return success;
+}
+
+static gboolean
+convert_origin (EBookCursorOrigin src_origin,
+ EBookCacheCursorOrigin *dest_origin,
+ GError **error)
+{
+ gboolean success = TRUE;
+
+ switch (src_origin) {
+ case E_BOOK_CURSOR_ORIGIN_CURRENT:
+ *dest_origin = E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT;
+ break;
+ case E_BOOK_CURSOR_ORIGIN_BEGIN:
+ *dest_origin = E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN;
+ break;
+ case E_BOOK_CURSOR_ORIGIN_END:
+ *dest_origin = E_BOOK_CACHE_CURSOR_ORIGIN_END;
+ break;
+ default:
+ g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_INVALID_ARG, _("Unrecognized
cursor origin"));
+ success = FALSE;
+ break;
+ }
+
+ return success;
+}
+
+static void
+convert_flags (EBookCursorStepFlags src_flags,
+ EBookCacheCursorStepFlags *dest_flags)
+{
+ if (src_flags & E_BOOK_CURSOR_STEP_MOVE)
+ *dest_flags |= E_BOOK_CACHE_CURSOR_STEP_MOVE;
+
+ if (src_flags & E_BOOK_CURSOR_STEP_FETCH)
+ *dest_flags |= E_BOOK_CACHE_CURSOR_STEP_FETCH;
+}
+
+static gint
+edbcc_step (EDataBookCursor *cursor,
+ const gchar *revision_guard,
+ EBookCursorStepFlags flags,
+ EBookCursorOrigin origin,
+ gint count,
+ GSList **results,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EDataBookCursorCache *cache_cursor;
+ GSList *local_results = NULL, *local_converted_results = NULL, *link;
+ EBookCacheCursorOrigin cache_origin = E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT;
+ EBookCacheCursorStepFlags cache_flags = 0;
+ gchar *revision = NULL;
+ gboolean success = TRUE;
+ gint n_results = -1;
+
+ cache_cursor = E_DATA_BOOK_CURSOR_CACHE (cursor);
+
+ if (!convert_origin (origin, &cache_origin, error))
+ return FALSE;
+
+ convert_flags (flags, &cache_flags);
+
+ /* Here we check the EBookCache revision
+ * against the revision_guard with an atomic transaction
+ * with the cache.
+ *
+ * The addressbook modifications and revision changes
+ * are also atomically committed to the SQLite.
+ */
+ e_cache_lock (E_CACHE (cache_cursor->priv->book_cache), E_CACHE_LOCK_READ);
+
+ if (revision_guard)
+ revision = e_cache_dup_revision (E_CACHE (cache_cursor->priv->book_cache));
+
+ if (revision_guard &&
+ g_strcmp0 (revision, revision_guard) != 0) {
+ g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_OUT_OF_SYNC,
+ _("Out of sync revision while moving cursor"));
+ success = FALSE;
+ }
+
+ if (success) {
+ GError *local_error = NULL;
+
+ n_results = e_book_cache_cursor_step (
+ cache_cursor->priv->book_cache,
+ cache_cursor->priv->cursor,
+ cache_flags,
+ cache_origin,
+ count,
+ &local_results,
+ cancellable,
+ &local_error);
+
+ if (n_results < 0) {
+
+ /* Convert the SQLite backend error to an EClient error */
+ if (g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_END_OF_LIST)) {
+ g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_QUERY_REFUSED,
local_error->message);
+ g_clear_error (&local_error);
+ } else {
+ g_propagate_error (error, local_error);
+ }
+
+ success = FALSE;
+ }
+ }
+
+ e_cache_unlock (E_CACHE (cache_cursor->priv->book_cache), E_CACHE_UNLOCK_NONE);
+
+ for (link = local_results; link; link = link->next) {
+ EBookCacheSearchData *data = link->data;
+
+ local_converted_results = g_slist_prepend (local_converted_results, data->vcard);
+ data->vcard = NULL;
+ }
+
+ g_slist_free_full (local_results, e_book_cache_search_data_free);
+
+ if (results)
+ *results = g_slist_reverse (local_converted_results);
+ else
+ g_slist_free_full (local_converted_results, g_free);
+
+ g_free (revision);
+
+ if (success)
+ return n_results;
+
+ return -1;
+}
+
+static gboolean
+edbcc_set_alphabetic_index (EDataBookCursor *cursor,
+ gint index,
+ const gchar *locale,
+ GError **error)
+{
+ EDataBookCursorCache *cache_cursor;
+ gchar *current_locale;
+
+ cache_cursor = E_DATA_BOOK_CURSOR_CACHE (cursor);
+
+ current_locale = e_book_cache_dup_locale (cache_cursor->priv->book_cache);
+
+ /* Locale mismatch, need to report error */
+ if (g_strcmp0 (current_locale, locale) != 0) {
+ g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_OUT_OF_SYNC,
+ _("Alphabetic index was set for incorrect locale"));
+ g_free (current_locale);
+
+ return FALSE;
+ }
+
+ e_book_cache_cursor_set_target_alphabetic_index (
+ cache_cursor->priv->book_cache,
+ cache_cursor->priv->cursor,
+ index);
+
+ g_free (current_locale);
+
+ return TRUE;
+}
+
+static gboolean
+edbcc_get_position (EDataBookCursor *cursor,
+ gint *total,
+ gint *position,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EDataBookCursorCache *cache_cursor;
+
+ cache_cursor = E_DATA_BOOK_CURSOR_CACHE (cursor);
+
+ return e_book_cache_cursor_calculate (
+ cache_cursor->priv->book_cache,
+ cache_cursor->priv->cursor,
+ total,
+ position,
+ cancellable,
+ error);
+}
+
+static gint
+edbcc_compare_contact (EDataBookCursor *cursor,
+ EContact *contact,
+ gboolean *matches_sexp)
+{
+ EDataBookCursorCache *cache_cursor;
+
+ cache_cursor = E_DATA_BOOK_CURSOR_CACHE (cursor);
+
+ return e_book_cache_cursor_compare_contact (
+ cache_cursor->priv->book_cache,
+ cache_cursor->priv->cursor,
+ contact,
+ matches_sexp);
+}
+
+static gboolean
+edbcc_load_locale (EDataBookCursor *cursor,
+ gchar **out_locale,
+ GError **error)
+{
+ EDataBookCursorCache *cache_cursor;
+
+ g_return_val_if_fail (E_IS_DATA_BOOK_CURSOR_CACHE (cursor), FALSE);
+ g_return_val_if_fail (out_locale != NULL, FALSE);
+
+ cache_cursor = E_DATA_BOOK_CURSOR_CACHE (cursor);
+
+ *out_locale = e_book_cache_dup_locale (cache_cursor->priv->book_cache);
+
+ return TRUE;
+}
+
+static void
+e_data_book_cursor_cache_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ EDataBookCursorCache *cache_cursor = E_DATA_BOOK_CURSOR_CACHE (object);
+
+ switch (property_id) {
+
+ case PROP_BOOK_CACHE:
+ /* Construct-only, can only be set once */
+ cache_cursor->priv->book_cache = g_value_dup_object (value);
+ return;
+
+ case PROP_CURSOR:
+ /* Construct-only, can only be set once */
+ cache_cursor->priv->cursor = g_value_get_pointer (value);
+ return;
+ }
+
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+e_data_book_cursor_cache_dispose (GObject *object)
+{
+ EDataBookCursorCache *cache_cursor = E_DATA_BOOK_CURSOR_CACHE (object);
+
+ if (cache_cursor->priv->book_cache) {
+ if (cache_cursor->priv->cursor) {
+ e_book_cache_cursor_free (cache_cursor->priv->book_cache, cache_cursor->priv->cursor);
+ cache_cursor->priv->cursor = NULL;
+ }
+
+ g_clear_object (&cache_cursor->priv->book_cache);
+ }
+
+ /* Chain up to parent's method */
+ G_OBJECT_CLASS (e_data_book_cursor_cache_parent_class)->dispose (object);
+}
+
+static void
+e_data_book_cursor_cache_class_init (EDataBookCursorCacheClass *class)
+{
+ GObjectClass *object_class;
+ EDataBookCursorClass *cursor_class;
+
+ object_class = G_OBJECT_CLASS (class);
+ object_class->set_property = e_data_book_cursor_cache_set_property;
+ object_class->dispose = e_data_book_cursor_cache_dispose;
+
+ cursor_class = E_DATA_BOOK_CURSOR_CLASS (class);
+ cursor_class->set_sexp = edbcc_set_sexp;
+ cursor_class->step = edbcc_step;
+ cursor_class->set_alphabetic_index = edbcc_set_alphabetic_index;
+ cursor_class->get_position = edbcc_get_position;
+ cursor_class->compare_contact = edbcc_compare_contact;
+ cursor_class->load_locale = edbcc_load_locale;
+
+ g_object_class_install_property (
+ object_class,
+ PROP_BOOK_CACHE,
+ g_param_spec_object (
+ "book-cache",
+ "Book Cache",
+ "The EBookCache to use for queries",
+ E_TYPE_BOOK_CACHE,
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
+
+ g_object_class_install_property (
+ object_class,
+ PROP_CURSOR,
+ g_param_spec_pointer (
+ "cursor",
+ "Cursor",
+ "The EBookCacheCursor pointer",
+ G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
+
+ g_type_class_add_private (class, sizeof (EDataBookCursorCachePrivate));
+}
+
+static void
+e_data_book_cursor_cache_init (EDataBookCursorCache *cache_cursor)
+{
+ cache_cursor->priv = G_TYPE_INSTANCE_GET_PRIVATE (cache_cursor, E_TYPE_DATA_BOOK_CURSOR_CACHE,
EDataBookCursorCachePrivate);
+}
+
+/**
+ * e_data_book_cursor_cache_new:
+ * @book_backend: the #EBookBackend creating this cursor
+ * @book_cache: the #EBookCache object to base this cursor on
+ * @sort_fields: (array length=n_fields): an array of #EContactFields as sort keys in order of priority
+ * @sort_types: (array length=n_fields): an array of #EBookCursorSortTypes, one for each field in
@sort_fields
+ * @n_fields: the number of fields to sort results by.
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates an #EDataBookCursor and implements all of the cursor methods
+ * using the delegate @book_cache object.
+ *
+ * This is suitable cursor type for any backend which stores its contacts
+ * using the #EBookCache object. The #EBookMetaBackend does that transparently.
+ *
+ * Returns: (transfer full): A newly created #EDataBookCursor, or %NULL if cursor creation failed.
+ *
+ * Since: 3.26
+ */
+EDataBookCursor *
+e_data_book_cursor_cache_new (EBookBackend *book_backend,
+ EBookCache *book_cache,
+ const EContactField *sort_fields,
+ const EBookCursorSortType *sort_types,
+ guint n_fields,
+ GError **error)
+{
+ EDataBookCursor *cursor = NULL;
+ EBookCacheCursor *cache_cursor;
+ GError *local_error = NULL;
+
+ g_return_val_if_fail (E_IS_BOOK_BACKEND (book_backend), NULL);
+ g_return_val_if_fail (E_IS_BOOK_CACHE (book_cache), NULL);
+
+ cache_cursor = e_book_cache_cursor_new (
+ book_cache, NULL,
+ sort_fields,
+ sort_types,
+ n_fields,
+ &local_error);
+
+ if (cache_cursor) {
+ cursor = g_object_new (E_TYPE_DATA_BOOK_CURSOR_CACHE,
+ "book-cache", book_cache,
+ "cursor", cache_cursor,
+ NULL);
+
+ /* Initially created cursors should have a position & total */
+ if (!e_data_book_cursor_load_locale (E_DATA_BOOK_CURSOR (cursor), NULL, NULL, error))
+ g_clear_object (&cursor);
+
+ } else if (g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY)) {
+ g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_INVALID_QUERY,
local_error->message);
+ g_clear_error (&local_error);
+ } else {
+ g_propagate_error (error, local_error);
+ }
+
+ return cursor;
+}
diff --git a/src/addressbook/libedata-book/e-data-book-cursor-cache.h
b/src/addressbook/libedata-book/e-data-book-cursor-cache.h
new file mode 100644
index 0000000..a443d86
--- /dev/null
+++ b/src/addressbook/libedata-book/e-data-book-cursor-cache.h
@@ -0,0 +1,81 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2013 Intel Corporation
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Tristan Van Berkom <tristanvb openismus com>
+ */
+
+#if !defined (__LIBEDATA_BOOK_H_INSIDE__) && !defined (LIBEDATA_BOOK_COMPILATION)
+#error "Only <libedata-book/libedata-book.h> should be included directly."
+#endif
+
+#ifndef E_DATA_BOOK_CURSOR_CACHE_H
+#define E_DATA_BOOK_CURSOR_CACHE_H
+
+#include <libedata-book/e-data-book-cursor.h>
+#include <libedata-book/e-book-cache.h>
+#include <libedata-book/e-book-backend.h>
+
+#define E_TYPE_DATA_BOOK_CURSOR_CACHE (e_data_book_cursor_cache_get_type ())
+#define E_DATA_BOOK_CURSOR_CACHE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o),
E_TYPE_DATA_BOOK_CURSOR_CACHE, EDataBookCursorCache))
+#define E_DATA_BOOK_CURSOR_CACHE_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), E_TYPE_DATA_BOOK_CURSOR_CACHE,
EDataBookCursorCacheClass))
+#define E_IS_DATA_BOOK_CURSOR_CACHE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o),
E_TYPE_DATA_BOOK_CURSOR_CACHE))
+#define E_IS_DATA_BOOK_CURSOR_CACHE_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), E_TYPE_DATA_BOOK_CURSOR_CACHE))
+#define E_DATA_BOOK_CURSOR_CACHE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o),
E_TYPE_DATA_BOOK_CURSOR_CACHE, EDataBookCursorCacheClass))
+
+G_BEGIN_DECLS
+
+typedef struct _EDataBookCursorCache EDataBookCursorCache;
+typedef struct _EDataBookCursorCacheClass EDataBookCursorCacheClass;
+typedef struct _EDataBookCursorCachePrivate EDataBookCursorCachePrivate;
+
+/**
+ * EDataBookCursorCache:
+ *
+ * An opaque handle for the #EBookCache cursor instance.
+ *
+ * Since: 3.26
+ */
+struct _EDataBookCursorCache {
+ /*< private >*/
+ EDataBookCursor parent;
+ EDataBookCursorCachePrivate *priv;
+};
+
+/**
+ * EDataBookCursorCacheClass:
+ *
+ * The #EBookCache cursor class structure.
+ *
+ * Since: 3.26
+ */
+struct _EDataBookCursorCacheClass {
+ /*< private >*/
+ EDataBookCursorClass parent;
+};
+
+GType e_data_book_cursor_cache_get_type (void);
+EDataBookCursor *
+ e_data_book_cursor_cache_new (EBookBackend *book_backend,
+ EBookCache *book_cache,
+ const EContactField *sort_fields,
+ const EBookCursorSortType *sort_types,
+ guint n_fields,
+ GError **error);
+
+G_END_DECLS
+
+#endif /* E_DATA_BOOK_CURSOR_CACHE_H */
diff --git a/src/addressbook/libedata-book/libedata-book.h b/src/addressbook/libedata-book/libedata-book.h
index ff070ec..50ee041 100644
--- a/src/addressbook/libedata-book/libedata-book.h
+++ b/src/addressbook/libedata-book/libedata-book.h
@@ -29,8 +29,11 @@
#include <libedata-book/e-book-backend-sqlitedb.h>
#include <libedata-book/e-book-backend-summary.h>
#include <libedata-book/e-book-backend.h>
+#include <libedata-book/e-book-cache.h>
+#include <libedata-book/e-book-meta-backend.h>
#include <libedata-book/e-book-sqlite.h>
#include <libedata-book/e-data-book-cursor.h>
+#include <libedata-book/e-data-book-cursor-cache.h>
#include <libedata-book/e-data-book-cursor-sqlite.h>
#include <libedata-book/e-data-book-direct.h>
#include <libedata-book/e-data-book-factory.h>
diff --git a/src/calendar/backends/caldav/e-cal-backend-caldav.c
b/src/calendar/backends/caldav/e-cal-backend-caldav.c
index 8f0f601..d2ae19b 100644
--- a/src/calendar/backends/caldav/e-cal-backend-caldav.c
+++ b/src/calendar/backends/caldav/e-cal-backend-caldav.c
@@ -1,7 +1,6 @@
/*
- * Evolution calendar - caldav backend
- *
* Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
*
* This library is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
@@ -21,89 +20,25 @@
#include "evolution-data-server-config.h"
#include <string.h>
-#include <unistd.h>
-#include <glib/gstdio.h>
#include <glib/gi18n-lib.h>
-/* LibXML2 includes */
-#include <libxml/parser.h>
-#include <libxml/tree.h>
-#include <libxml/xpath.h>
-#include <libxml/xpathInternals.h>
-
-/* LibSoup includes */
-#include <libsoup/soup.h>
-
#include <libedataserver/libedataserver.h>
#include "e-cal-backend-caldav.h"
-#define d(x)
-
-#define E_CAL_BACKEND_CALDAV_GET_PRIVATE(obj) \
- (G_TYPE_INSTANCE_GET_PRIVATE \
- ((obj), E_TYPE_CAL_BACKEND_CALDAV, ECalBackendCalDAVPrivate))
+#define E_CALDAV_MAX_MULTIGET_AMOUNT 100 /* what's the maximum count of items to fetch within a multiget
request */
-#define CALDAV_CTAG_KEY "CALDAV_CTAG"
-#define CALDAV_MAX_MULTIGET_AMOUNT 100 /* what's the maximum count of items to fetch within a multiget
request */
-#define LOCAL_PREFIX "file://"
-
-/* in seconds */
-#define DEFAULT_REFRESH_TIME 60
+#define E_CALDAV_X_ETAG "X-EVOLUTION-CALDAV-ETAG"
#define EDC_ERROR(_code) e_data_cal_create_error (_code, NULL)
#define EDC_ERROR_EX(_code, _msg) e_data_cal_create_error (_code, _msg)
-typedef enum {
-
- SLAVE_SHOULD_SLEEP,
- SLAVE_SHOULD_WORK,
- SLAVE_SHOULD_WORK_NO_CTAG_CHECK,
- SLAVE_SHOULD_DIE
-
-} SlaveCommand;
-
-/* Private part of the ECalBackendHttp structure */
struct _ECalBackendCalDAVPrivate {
-
- /* The local disk cache */
- ECalBackendStore *store;
-
- /* should we sync for offline mode? */
- gboolean do_offline;
-
- /* TRUE after caldav_open */
- gboolean loaded;
- /* TRUE when server reachable */
- gboolean opened;
-
- /* lock to indicate a busy state */
- GMutex busy_lock;
-
- /* cond to synch threads */
- GCond cond;
-
- /* cond to know the slave gone */
- GCond slave_gone_cond;
-
- /* BG synch thread */
- const GThread *synch_slave; /* just for a reference, whether thread exists */
- SlaveCommand slave_cmd;
- gboolean slave_busy; /* whether is slave working */
-
- /* The main soup session */
- SoupSession *session;
-
- /* clandar uri */
- gchar *uri;
-
- /* Authentication info */
- ENamedParameters *credentials;
- gboolean auth_required;
+ /* The main WebDAV session */
+ EWebDAVSession *webdav;
/* support for 'getctag' extension */
gboolean ctag_supported;
- gchar *ctag_to_store;
/* TRUE when 'calendar-schedule' supported on the server */
gboolean calendar_schedule;
@@ -119,4515 +54,1238 @@ struct _ECalBackendCalDAVPrivate {
/* The iCloud.com requires timezone IDs as locations */
gboolean is_icloud;
-
- /* set to true if thread for ESource::changed is invoked */
- gboolean updating_source;
-
- guint refresh_id;
-
- /* If we fail to obtain an OAuth2 access token,
- * soup_authenticate_bearer() stashes an error
- * here to be claimed in caldav_credentials_required_sync().
- * This lets us propagate a more useful error
- * message than a generic SOUP_STATUS_UNAUTHORIZED description. */
- GError *bearer_auth_error;
- GMutex bearer_auth_error_lock;
- ESoupAuthBearer *using_bearer_auth;
};
-/* Forward Declarations */
-static void e_caldav_backend_initable_init
- (GInitableIface *interface);
-
-G_DEFINE_TYPE_WITH_CODE (
- ECalBackendCalDAV,
- e_cal_backend_caldav,
- E_TYPE_CAL_BACKEND_SYNC,
- G_IMPLEMENT_INTERFACE (
- G_TYPE_INITABLE,
- e_caldav_backend_initable_init))
-
-/* ************************************************************************* */
-/* Debugging */
-
-#define DEBUG_MESSAGE "message"
-#define DEBUG_MESSAGE_HEADER "message:header"
-#define DEBUG_MESSAGE_BODY "message:body"
-#define DEBUG_SERVER_ITEMS "items"
-#define DEBUG_ATTACHMENTS "attachments"
-
-static gboolean open_calendar_wrapper (ECalBackendCalDAV *cbdav,
- GCancellable *cancellable,
- GError **error,
- gboolean first_attempt,
- gboolean *know_unreachable,
- gchar **out_certificate_pem,
- GTlsCertificateFlags *out_certificate_errors);
-
-static void convert_to_inline_attachment (ECalBackendCalDAV *cbdav, icalcomponent *icalcomp);
-static void convert_to_url_attachment (ECalBackendCalDAV *cbdav, icalcomponent *icalcomp);
-static void remove_cached_attachment (ECalBackendCalDAV *cbdav, const gchar *uid);
-
-static gboolean caldav_debug_all = FALSE;
-static GHashTable *caldav_debug_table = NULL;
-
-static void
-add_debug_key (const gchar *start,
- const gchar *end)
-{
- gchar *debug_key;
- gchar *debug_value;
-
- if (start == end) {
- return;
- }
-
- debug_key = debug_value = g_strndup (start, end - start);
-
- debug_key = g_strchug (debug_key);
- debug_key = g_strchomp (debug_key);
-
- if (strlen (debug_key) == 0) {
- g_free (debug_value);
- return;
- }
-
- g_hash_table_insert (
- caldav_debug_table,
- debug_key,
- debug_value);
-
- d (g_debug ("Adding %s to enabled debugging keys", debug_key));
-}
-
-static gpointer
-caldav_debug_init_once (gpointer data)
-{
- const gchar *dbg;
-
- dbg = g_getenv ("CALDAV_DEBUG");
-
- if (dbg) {
- const gchar *ptr;
-
- d (g_debug ("Got debug env variable: [%s]", dbg));
-
- caldav_debug_table = g_hash_table_new (
- g_str_hash,
- g_str_equal);
-
- ptr = dbg;
-
- while (*ptr != '\0') {
- if (*ptr == ',' || *ptr == ':') {
-
- add_debug_key (dbg, ptr);
-
- if (*ptr == ',') {
- dbg = ptr + 1;
- }
- }
-
- ptr++;
- }
-
- if (ptr - dbg > 0) {
- add_debug_key (dbg, ptr);
- }
-
- if (g_hash_table_lookup (caldav_debug_table, "all")) {
- caldav_debug_all = TRUE;
- g_hash_table_destroy (caldav_debug_table);
- caldav_debug_table = NULL;
- }
- }
-
- return NULL;
-}
-
-static void
-caldav_debug_init (void)
-{
- static GOnce debug_once = G_ONCE_INIT;
-
- g_once (
- &debug_once,
- caldav_debug_init_once,
- NULL);
-}
-
-static gboolean
-caldav_debug_show (const gchar *component)
-{
- if (G_UNLIKELY (caldav_debug_all)) {
- return TRUE;
- } else if (G_UNLIKELY (caldav_debug_table != NULL) &&
- g_hash_table_lookup (caldav_debug_table, component)) {
- return TRUE;
- }
-
- return FALSE;
-}
-
-#define DEBUG_MAX_BODY_SIZE (100 * 1024 * 1024)
-
-static void
-caldav_debug_setup (SoupSession *session)
-{
- SoupLogger *logger;
- SoupLoggerLogLevel level;
-
- if (caldav_debug_show (DEBUG_MESSAGE_BODY))
- level = SOUP_LOGGER_LOG_BODY;
- else if (caldav_debug_show (DEBUG_MESSAGE_HEADER))
- level = SOUP_LOGGER_LOG_HEADERS;
- else
- level = SOUP_LOGGER_LOG_MINIMAL;
-
- logger = soup_logger_new (level, DEBUG_MAX_BODY_SIZE);
- soup_session_add_feature (session, SOUP_SESSION_FEATURE (logger));
- g_object_unref (logger);
-}
-
-/* TODO Do not replicate this in every backend */
-static icaltimezone *
-resolve_tzid (const gchar *tzid,
- gpointer user_data)
-{
- ETimezoneCache *timezone_cache;
-
- timezone_cache = E_TIMEZONE_CACHE (user_data);
-
- return e_timezone_cache_get_timezone (timezone_cache, tzid);
-}
-
-static gboolean
-put_component_to_store (ECalBackendCalDAV *cbdav,
- ECalComponent *comp)
-{
- time_t time_start, time_end;
-
- e_cal_util_get_component_occur_times (
- comp, &time_start, &time_end,
- resolve_tzid, cbdav, icaltimezone_get_utc_timezone (),
- e_cal_backend_get_kind (E_CAL_BACKEND (cbdav)));
-
- return e_cal_backend_store_put_component_with_time_range (
- cbdav->priv->store, comp, time_start, time_end);
-}
-
-static ECalBackendSyncClass *parent_class = NULL;
-
-static void caldav_source_changed_cb (ESource *source, ECalBackendCalDAV *cbdav);
-
-static gboolean remove_comp_from_cache (ECalBackendCalDAV *cbdav, const gchar *uid, const gchar *rid);
-static gboolean put_comp_to_cache (ECalBackendCalDAV *cbdav, icalcomponent *icalcomp, const gchar *href,
const gchar *etag);
-static void put_server_comp_to_cache (ECalBackendCalDAV *cbdav, icalcomponent *icomp, const gchar *href,
const gchar *etag, GTree *c_uid2complist);
-
-/* ************************************************************************* */
-/* Misc. utility functions */
-
-static void
-caldav_ensure_bearer_auth_usage (ECalBackendCalDAV *cbdav,
- ESoupAuthBearer *bearer)
-{
- SoupSessionFeature *feature;
- SoupURI *soup_uri;
- ESourceWebdav *extension;
- ESource *source;
-
- g_return_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav));
-
- source = e_backend_get_source (E_BACKEND (cbdav));
-
- /* Preload the SoupAuthManager with a valid "Bearer" token
- * when using OAuth 2.0. This avoids an extra unauthorized
- * HTTP round-trip, which apparently Google doesn't like. */
-
- feature = soup_session_get_feature (cbdav->priv->session, SOUP_TYPE_AUTH_MANAGER);
-
- if (!soup_session_feature_has_feature (feature, E_TYPE_SOUP_AUTH_BEARER)) {
- /* Add the "Bearer" auth type to support OAuth 2.0. */
- soup_session_feature_add_feature (feature, E_TYPE_SOUP_AUTH_BEARER);
- }
-
- extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
- soup_uri = e_source_webdav_dup_soup_uri (extension);
-
- soup_auth_manager_use_auth (
- SOUP_AUTH_MANAGER (feature),
- soup_uri, SOUP_AUTH (bearer));
-
- soup_uri_free (soup_uri);
-}
-
-static gboolean
-caldav_setup_bearer_auth (ECalBackendCalDAV *cbdav,
- gboolean is_in_authenticate,
- ESoupAuthBearer *bearer,
- GCancellable *cancellable,
- GError **error)
-{
- ESource *source;
- gchar *access_token = NULL;
- gint expires_in_seconds = -1;
- gboolean success = FALSE;
-
- g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav), FALSE);
- g_return_val_if_fail (E_IS_SOUP_AUTH_BEARER (bearer), FALSE);
-
- source = e_backend_get_source (E_BACKEND (cbdav));
-
- success = e_util_get_source_oauth2_access_token_sync (source, cbdav->priv->credentials,
- &access_token, &expires_in_seconds, cancellable, error);
-
- if (success) {
- e_soup_auth_bearer_set_access_token (bearer, access_token, expires_in_seconds);
- if (!is_in_authenticate)
- caldav_ensure_bearer_auth_usage (cbdav, bearer);
- }
-
- g_free (access_token);
-
- return success;
-}
+G_DEFINE_TYPE (ECalBackendCalDAV, e_cal_backend_caldav, E_TYPE_CAL_META_BACKEND)
static gboolean
-caldav_maybe_prepare_bearer_auth (ECalBackendCalDAV *cbdav,
- GCancellable *cancellable,
- GError **error)
+ecb_caldav_connect_sync (ECalMetaBackend *meta_backend,
+ const ENamedParameters *credentials,
+ ESourceAuthenticationResult *out_auth_result,
+ gchar **out_certificate_pem,
+ GTlsCertificateFlags *out_certificate_errors,
+ GCancellable *cancellable,
+ GError **error)
{
+ ECalBackendCalDAV *cbdav;
+ GHashTable *capabilities = NULL, *allows = NULL;
ESource *source;
- gchar *auth_method = NULL;
gboolean success;
+ GError *local_error = NULL;
- g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav), FALSE);
-
- source = e_backend_get_source (E_BACKEND (cbdav));
-
- if (e_source_has_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
- ESourceAuthentication *extension;
+ g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (meta_backend), FALSE);
+ g_return_val_if_fail (out_auth_result != NULL, FALSE);
- extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
- auth_method = e_source_authentication_dup_method (extension);
- } else {
- return TRUE;
- }
+ cbdav = E_CAL_BACKEND_CALDAV (meta_backend);
- if (g_strcmp0 (auth_method, "OAuth2") != 0 && g_strcmp0 (auth_method, "Google") != 0) {
- g_free (auth_method);
+ if (cbdav->priv->webdav)
return TRUE;
- }
-
- g_free (auth_method);
-
- if (cbdav->priv->using_bearer_auth) {
- success = caldav_setup_bearer_auth (cbdav, FALSE, cbdav->priv->using_bearer_auth,
cancellable, error);
- } else {
- ESourceWebdav *extension;
- SoupAuth *soup_auth;
- SoupURI *soup_uri;
-
- extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
- soup_uri = e_source_webdav_dup_soup_uri (extension);
- soup_auth = g_object_new (
- E_TYPE_SOUP_AUTH_BEARER,
- SOUP_AUTH_HOST, soup_uri->host, NULL);
+ source = e_backend_get_source (E_BACKEND (meta_backend));
- success = caldav_setup_bearer_auth (cbdav, FALSE, E_SOUP_AUTH_BEARER (soup_auth),
cancellable, error);
- if (success)
- cbdav->priv->using_bearer_auth = g_object_ref (soup_auth);
-
- g_object_unref (soup_auth);
- soup_uri_free (soup_uri);
- }
+ cbdav->priv->webdav = e_webdav_session_new (source);
- return success;
-}
+ e_soup_session_setup_logging (E_SOUP_SESSION (cbdav->priv->webdav), g_getenv ("CALDAV_DEBUG"));
-static void
-update_slave_cmd (ECalBackendCalDAVPrivate *priv,
- SlaveCommand slave_cmd)
-{
- g_return_if_fail (priv != NULL);
-
- if (priv->slave_cmd == SLAVE_SHOULD_DIE)
- return;
-
- priv->slave_cmd = slave_cmd;
-}
-
-#define X_E_CALDAV "X-EVOLUTION-CALDAV-"
-#define X_E_CALDAV_ATTACHMENT_NAME X_E_CALDAV "ATTACHMENT-NAME"
-
-static void
-icomp_x_prop_set (icalcomponent *comp,
- const gchar *key,
- const gchar *value)
-{
- icalproperty *xprop;
-
- /* Find the old one first */
- xprop = icalcomponent_get_first_property (comp, ICAL_X_PROPERTY);
-
- while (xprop) {
- const gchar *str = icalproperty_get_x_name (xprop);
-
- if (!strcmp (str, key)) {
- if (value) {
- icalproperty_set_value_from_string (xprop, value, "NO");
- } else {
- icalcomponent_remove_property (comp, xprop);
- icalproperty_free (xprop);
- }
- break;
- }
-
- xprop = icalcomponent_get_next_property (comp, ICAL_X_PROPERTY);
- }
-
- if (!xprop && value) {
- xprop = icalproperty_new_x (value);
- icalproperty_set_x_name (xprop, key);
- icalcomponent_add_property (comp, xprop);
- }
-}
-
-static gchar *
-icomp_x_prop_get (icalcomponent *comp,
- const gchar *key)
-{
- icalproperty *xprop;
-
- /* Find the old one first */
- xprop = icalcomponent_get_first_property (comp, ICAL_X_PROPERTY);
-
- while (xprop) {
- const gchar *str = icalproperty_get_x_name (xprop);
+ e_binding_bind_property (
+ cbdav, "proxy-resolver",
+ cbdav->priv->webdav, "proxy-resolver",
+ G_BINDING_SYNC_CREATE);
- if (!strcmp (str, key)) {
- break;
- }
+ /* Thinks the 'getctag' extension is available the first time, but unset it when realizes it isn't. */
+ cbdav->priv->ctag_supported = TRUE;
- xprop = icalcomponent_get_next_property (comp, ICAL_X_PROPERTY);
- }
+ e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTING);
- if (xprop) {
- return icalproperty_get_value_as_string_r (xprop);
- }
+ e_soup_session_set_credentials (E_SOUP_SESSION (cbdav->priv->webdav), credentials);
- return NULL;
-}
+ success = e_webdav_session_options_sync (cbdav->priv->webdav, NULL,
+ &capabilities, &allows, cancellable, &local_error);
-/* passing NULL as 'href' removes the property */
-static void
-ecalcomp_set_href (ECalComponent *comp,
- const gchar *href)
-{
- icalcomponent *icomp;
-
- icomp = e_cal_component_get_icalcomponent (comp);
- g_return_if_fail (icomp != NULL);
-
- icomp_x_prop_set (icomp, X_E_CALDAV "HREF", href);
-}
+ if (success) {
+ ESourceWebdav *webdav_extension;
+ ECalCache *cal_cache;
+ SoupURI *soup_uri;
+ gboolean is_writable;
+ gboolean calendar_access;
-static gchar *
-ecalcomp_get_href (ECalComponent *comp)
-{
- icalcomponent *icomp;
- gchar *str;
+ webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+ soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
- str = NULL;
- icomp = e_cal_component_get_icalcomponent (comp);
- g_return_val_if_fail (icomp != NULL, NULL);
+ /* The POST added for FastMail servers, which doesn't advertise PUT on collections. */
+ is_writable = allows && (
+ g_hash_table_contains (allows, SOUP_METHOD_PUT) ||
+ g_hash_table_contains (allows, SOUP_METHOD_POST) ||
+ g_hash_table_contains (allows, SOUP_METHOD_DELETE));
- str = icomp_x_prop_get (icomp, X_E_CALDAV "HREF");
+ cbdav->priv->calendar_schedule = capabilities && g_hash_table_contains (capabilities,
E_WEBDAV_CAPABILITY_CALENDAR_SCHEDULE);
+ calendar_access = capabilities && g_hash_table_contains (capabilities,
E_WEBDAV_CAPABILITY_CALENDAR_ACCESS);
- return str;
-}
+ if (calendar_access) {
+ e_cal_backend_set_writable (E_CAL_BACKEND (cbdav), is_writable);
-/* passing NULL as 'etag' removes the property */
-static void
-ecalcomp_set_etag (ECalComponent *comp,
- const gchar *etag)
-{
- icalcomponent *icomp;
+ e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTED);
- icomp = e_cal_component_get_icalcomponent (comp);
- g_return_if_fail (icomp != NULL);
+ cbdav->priv->is_google = soup_uri && soup_uri->host && (
+ g_ascii_strcasecmp (soup_uri->host, "www.google.com") == 0 ||
+ g_ascii_strcasecmp (soup_uri->host, "apidata.googleusercontent.com") == 0);
- icomp_x_prop_set (icomp, X_E_CALDAV "ETAG", etag);
-}
-
-static gchar *
-ecalcomp_get_etag (ECalComponent *comp)
-{
- icalcomponent *icomp;
- gchar *str;
-
- str = NULL;
- icomp = e_cal_component_get_icalcomponent (comp);
- g_return_val_if_fail (icomp != NULL, NULL);
+ cbdav->priv->is_icloud = soup_uri && soup_uri->host &&
+ e_util_utf8_strstrcase (soup_uri->host, ".icloud.com");
+ } else {
+ gchar *uri;
- str = icomp_x_prop_get (icomp, X_E_CALDAV "ETAG");
+ uri = soup_uri_to_string (soup_uri, FALSE);
- /* libical 0.48 escapes quotes, thus unescape them */
- if (str && strchr (str, '\\')) {
- gint ii, jj;
+ success = FALSE;
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
+ _("Given URL “%s” doesn't reference CalDAV calendar"), uri);
- for (ii = 0, jj = 0; str[ii]; ii++) {
- if (str[ii] == '\\') {
- ii++;
- if (!str[ii])
- break;
- }
+ g_free (uri);
- str[jj] = str[ii];
- jj++;
+ e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
}
- str[jj] = 0;
- }
-
- return str;
-}
-
-/*typedef enum {
- *
- / * object is in synch,
- * now isnt that ironic? :) * /
- ECALCOMP_IN_SYNCH = 0,
- *
- / * local changes * /
- ECALCOMP_LOCALLY_CREATED,
- ECALCOMP_LOCALLY_DELETED,
- ECALCOMP_LOCALLY_MODIFIED
- *
-} ECalCompSyncState;
- *
-/ * oos = out of synch * /
-static void
-ecalcomp_set_synch_state (ECalComponent *comp,
- * ECalCompSyncState state)
-{
- icalcomponent *icomp;
- gchar *state_string;
- *
- icomp = e_cal_component_get_icalcomponent (comp);
- *
- state_string = g_strdup_printf ("%d", state);
- *
- icomp_x_prop_set (icomp, X_E_CALDAV "ETAG", state_string);
- *
- g_free (state_string);
-}*/
-
-static gchar *
-ecalcomp_gen_href (ECalComponent *comp)
-{
- gchar *href, *uid, *tmp;
- icalcomponent *icomp;
-
- icomp = e_cal_component_get_icalcomponent (comp);
- g_return_val_if_fail (icomp != NULL, NULL);
-
- uid = g_strdup (icalcomponent_get_uid (icomp));
- if (!uid || !*uid) {
- g_free (uid);
- uid = e_cal_component_gen_uid ();
-
- tmp = uid ? strchr (uid, '@') : NULL;
- if (tmp)
- *tmp = '\0';
-
- tmp = NULL;
- } else
- tmp = isodate_from_time_t (time (NULL));
-
- /* quite long, but ensures uniqueness quite well, without using UUIDs */
- href = g_strconcat (uid ? uid : "no-uid", tmp ? "-" : "", tmp ? tmp : "", ".ics", NULL);
-
- g_free (tmp);
- g_free (uid);
-
- icomp_x_prop_set (icomp, X_E_CALDAV "HREF", href);
-
- return g_strdelimit (href, " /'\"`&();|<>$%{}!\\:*?#@", '_');
-}
-
-/* ensure etag is quoted (to workaround potential server bugs) */
-static gchar *
-quote_etag (const gchar *etag)
-{
- gchar *ret;
-
- if (etag && (strlen (etag) < 2 || etag[strlen (etag) - 1] != '\"')) {
- ret = g_strdup_printf ("\"%s\"", etag);
- } else {
- ret = g_strdup (etag);
- }
-
- return ret;
-}
-
-/* ************************************************************************* */
-
-static gboolean
-status_code_to_result (SoupMessage *message,
- ECalBackendCalDAV *cbdav,
- gboolean is_opening,
- GError **perror)
-{
- ECalBackendCalDAVPrivate *priv;
- gchar *uri;
-
- g_return_val_if_fail (cbdav != NULL, FALSE);
- g_return_val_if_fail (message != NULL, FALSE);
-
- priv = cbdav->priv;
-
- if (SOUP_STATUS_IS_SUCCESSFUL (message->status_code)) {
- return TRUE;
+ g_clear_object (&cal_cache);
+ soup_uri_free (soup_uri);
}
- if (perror && *perror)
- return FALSE;
-
- switch (message->status_code) {
- case SOUP_STATUS_CANT_RESOLVE:
- case SOUP_STATUS_CANT_RESOLVE_PROXY:
- case SOUP_STATUS_CANT_CONNECT:
- case SOUP_STATUS_CANT_CONNECT_PROXY:
- g_propagate_error (
- perror,
- e_data_cal_create_error_fmt (
- OtherError,
- _("Server is unreachable (%s)"),
- message->reason_phrase && *message->reason_phrase ?
message->reason_phrase :
- (soup_status_get_phrase (message->status_code) ?
soup_status_get_phrase (message->status_code) : _("Unknown error"))));
- if (priv) {
- priv->opened = FALSE;
- e_cal_backend_set_writable (
- E_CAL_BACKEND (cbdav), FALSE);
- }
- break;
- case SOUP_STATUS_NOT_FOUND:
- if (is_opening)
- g_propagate_error (perror, EDC_ERROR (NoSuchCal));
- else
- g_propagate_error (perror, EDC_ERROR (ObjectNotFound));
- break;
+ if (success) {
+ gchar *ctag = NULL;
- case SOUP_STATUS_FORBIDDEN:
- if (cbdav->priv->using_bearer_auth && message->response_body &&
- message->response_body->data && message->response_body->length) {
- gchar *body = g_strndup (message->response_body->data,
message->response_body->length);
-
- /* Do not localize this string, it is returned by the server. */
- if (body && (e_util_strstrcase (body, "Daily Limit") ||
- e_util_strstrcase (body, "https://console.developers.google.com/"))) {
- /* Special-case this condition and provide this error up to the UI. */
- g_propagate_error (perror,
- e_data_cal_create_error_fmt (OtherError, _("Failed to login to the
server: %s"), body));
- } else {
- g_propagate_error (perror, EDC_ERROR (AuthenticationRequired));
- }
+ /* Some servers, notably Google, allow OPTIONS when not
+ authorized (aka without credentials), thus try something
+ more aggressive, just in case.
- g_free (body);
+ The 'getctag' extension is not required, thuch check
+ for unauthorized error only. */
+ if (!e_webdav_session_getctag_sync (cbdav->priv->webdav, NULL, &ctag, cancellable,
&local_error) &&
+ g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED)) {
+ success = FALSE;
} else {
- g_propagate_error (perror, EDC_ERROR (AuthenticationRequired));
- }
- break;
-
- case SOUP_STATUS_UNAUTHORIZED:
- if (priv && priv->auth_required)
- g_propagate_error (perror, EDC_ERROR (AuthenticationFailed));
- else
- g_propagate_error (perror, EDC_ERROR (AuthenticationRequired));
- break;
-
- case SOUP_STATUS_SSL_FAILED:
- g_propagate_error (
- perror,
- e_data_cal_create_error_fmt ( OtherError,
- _("Failed to connect to a server using SSL/TLS: %s"),
- message->reason_phrase && *message->reason_phrase ? message->reason_phrase :
- (soup_status_get_phrase (message->status_code) ? soup_status_get_phrase
(message->status_code) : _("Unknown error"))));
- if (is_opening && perror && *perror) {
- (*perror)->domain = SOUP_HTTP_ERROR;
- (*perror)->code = SOUP_STATUS_SSL_FAILED;
+ g_clear_error (&local_error);
}
- break;
- default:
- d (g_debug ("CalDAV:%s: Unhandled status code %d\n", G_STRFUNC, status_code));
- uri = soup_uri_to_string (soup_message_get_uri (message), FALSE);
- g_propagate_error (
- perror,
- e_data_cal_create_error_fmt (
- OtherError,
- _("Unexpected HTTP status code %d returned (%s) for URI: %s"),
- message->status_code,
- message->reason_phrase && *message->reason_phrase ?
message->reason_phrase :
- (soup_status_get_phrase (message->status_code) ?
soup_status_get_phrase (message->status_code) : _("Unknown error")),
- uri ? uri : "[null]"));
- g_free (uri);
- break;
+ g_free (ctag);
}
- return FALSE;
-}
+ if (!success) {
+ gboolean credentials_empty;
+ gboolean is_ssl_error;
-/* !TS, call with lock held */
-static gboolean
-check_state (ECalBackendCalDAV *cbdav,
- gboolean *online,
- GError **perror)
-{
- *online = FALSE;
+ credentials_empty = !credentials || !e_named_parameters_count (credentials);
+ is_ssl_error = g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED);
- if (!cbdav->priv->loaded) {
- g_propagate_error (perror, EDC_ERROR_EX (OtherError, _("CalDAV backend is not loaded yet")));
- return FALSE;
- }
+ *out_auth_result = E_SOURCE_AUTHENTICATION_ERROR;
- if (!e_backend_get_online (E_BACKEND (cbdav))) {
-
- if (!cbdav->priv->do_offline) {
- g_propagate_error (perror, EDC_ERROR (RepositoryOffline));
- return FALSE;
+ /* because evolution knows only G_IO_ERROR_CANCELLED */
+ if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_CANCELLED)) {
+ local_error->domain = G_IO_ERROR;
+ local_error->code = G_IO_ERROR_CANCELLED;
+ } else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_FORBIDDEN) &&
credentials_empty) {
+ *out_auth_result = E_SOURCE_AUTHENTICATION_REQUIRED;
+ } else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED)) {
+ if (credentials_empty)
+ *out_auth_result = E_SOURCE_AUTHENTICATION_REQUIRED;
+ else
+ *out_auth_result = E_SOURCE_AUTHENTICATION_REJECTED;
+ } else if (local_error) {
+ g_propagate_error (error, local_error);
+ local_error = NULL;
+ } else {
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+ _("Unknown error"));
}
- } else {
- *online = TRUE;
- }
-
- return TRUE;
-}
+ if (is_ssl_error) {
+ *out_auth_result = E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED;
-/* ************************************************************************* */
-/* XML Parsing code */
-
-static xmlXPathObjectPtr
-xpath_eval (xmlXPathContextPtr ctx,
- const gchar *format,
- ...)
-{
- xmlXPathObjectPtr result;
- va_list args;
- gchar *expr;
-
- if (ctx == NULL) {
- return NULL;
- }
-
- va_start (args, format);
- expr = g_strdup_vprintf (format, args);
- va_end (args);
-
- result = xmlXPathEvalExpression ((xmlChar *) expr, ctx);
- g_free (expr);
-
- if (result == NULL) {
- return NULL;
- }
-
- if (result->type == XPATH_NODESET &&
- xmlXPathNodeSetIsEmpty (result->nodesetval)) {
- xmlXPathFreeObject (result);
- return NULL;
- }
-
- return result;
-}
-
-#if 0
-static gboolean
-parse_status_node (xmlNodePtr node,
- guint *status_code)
-{
- xmlChar *content;
- gboolean res;
-
- content = xmlNodeGetContent (node);
-
- res = soup_headers_parse_status_line (
- (gchar *) content,
- NULL,
- status_code,
- NULL);
- xmlFree (content);
-
- return res;
-}
-#endif
-
-static gchar *
-xp_object_get_string (xmlXPathObjectPtr result)
-{
- gchar *ret = NULL;
-
- if (result == NULL)
- return ret;
-
- if (result->type == XPATH_STRING) {
- ret = g_strdup ((gchar *) result->stringval);
- }
-
- xmlXPathFreeObject (result);
- return ret;
-}
-
-/* like get_string but will quote the etag if necessary */
-static gchar *
-xp_object_get_etag (xmlXPathObjectPtr result)
-{
- gchar *ret = NULL;
- gchar *str;
-
- if (result == NULL)
- return ret;
-
- if (result->type == XPATH_STRING) {
- str = (gchar *) result->stringval;
-
- ret = quote_etag (str);
- }
-
- xmlXPathFreeObject (result);
- return ret;
-}
-
-static guint
-xp_object_get_status (xmlXPathObjectPtr result)
-{
- gboolean res;
- guint ret = 0;
-
- if (result == NULL)
- return ret;
-
- if (result->type == XPATH_STRING) {
- res = soup_headers_parse_status_line (
- (gchar *) result->stringval,
- NULL,
- &ret,
- NULL);
-
- if (!res) {
- ret = 0;
+ e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_SSL_FAILED);
+ e_soup_session_get_ssl_error_details (E_SOUP_SESSION (cbdav->priv->webdav),
out_certificate_pem, out_certificate_errors);
+ } else {
+ e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
}
}
- xmlXPathFreeObject (result);
- return ret;
-}
-
-#if 0
-static gint
-xp_object_get_number (xmlXPathObjectPtr result)
-{
- gint ret = -1;
-
- if (result == NULL)
- return ret;
-
- if (result->type == XPATH_STRING) {
- ret = result->boolval;
- }
-
- xmlXPathFreeObject (result);
- return ret;
-}
-#endif
-
-/*** *** *** *** *** *** */
-#define XPATH_HREF "string(/D:multistatus/D:response[%d]/D:href)"
-#define XPATH_STATUS "string(/D:multistatus/D:response[%d]/D:propstat/D:status)"
-#define XPATH_GETETAG_STATUS
"string(/D:multistatus/D:response[%d]/D:propstat/D:prop/D:getetag/../../D:status)"
-#define XPATH_GETETAG "string(/D:multistatus/D:response[%d]/D:propstat/D:prop/D:getetag)"
-#define XPATH_CALENDAR_DATA "string(/D:multistatus/D:response[%d]/D:propstat/D:prop/C:calendar-data)"
-#define XPATH_GETCTAG_STATUS "string(/D:multistatus/D:response/D:propstat/D:prop/CS:getctag/../../D:status)"
-#define XPATH_GETCTAG "string(/D:multistatus/D:response/D:propstat/D:prop/CS:getctag)"
-#define XPATH_OWNER_STATUS
"string(/D:multistatus/D:response/D:propstat/D:prop/D:owner/D:href/../../../D:status)"
-#define XPATH_OWNER "string(/D:multistatus/D:response/D:propstat/D:prop/D:owner/D:href)"
-#define XPATH_SCHEDULE_OUTBOX_URL_STATUS
"string(/D:multistatus/D:response/D:propstat/D:prop/C:schedule-outbox-URL/D:href/../../../D:status)"
-#define XPATH_SCHEDULE_OUTBOX_URL
"string(/D:multistatus/D:response/D:propstat/D:prop/C:schedule-outbox-URL/D:href)"
-
-typedef struct _CalDAVObject CalDAVObject;
+ if (capabilities)
+ g_hash_table_destroy (capabilities);
+ if (allows)
+ g_hash_table_destroy (allows);
-struct _CalDAVObject {
+ if (!success)
+ g_clear_object (&cbdav->priv->webdav);
- gchar *href;
- gchar *etag;
-
- guint status;
-
- gchar *cdata;
-};
-
-static void
-caldav_object_free (CalDAVObject *object,
- gboolean free_object_itself)
-{
- g_free (object->href);
- g_free (object->etag);
- g_free (object->cdata);
-
- if (free_object_itself) {
- g_free (object);
- }
-}
-
-static gboolean
-parse_report_response (SoupMessage *soup_message,
- CalDAVObject **objs,
- gint *len)
-{
- xmlXPathContextPtr xpctx;
- xmlXPathObjectPtr result;
- xmlDocPtr doc;
- gint i, n;
- gboolean res;
-
- g_return_val_if_fail (soup_message != NULL, FALSE);
- g_return_val_if_fail (objs != NULL || len != NULL, FALSE);
-
- res = TRUE;
- doc = xmlReadMemory (
- soup_message->response_body->data,
- soup_message->response_body->length,
- "response.xml",
- NULL,
- 0);
-
- if (doc == NULL) {
- return FALSE;
- }
-
- xpctx = xmlXPathNewContext (doc);
-
- xmlXPathRegisterNs (
- xpctx, (xmlChar *) "D",
- (xmlChar *) "DAV:");
-
- xmlXPathRegisterNs (
- xpctx, (xmlChar *) "C",
- (xmlChar *) "urn:ietf:params:xml:ns:caldav");
-
- result = xpath_eval (xpctx, "/D:multistatus/D:response");
-
- if (result == NULL || result->type != XPATH_NODESET) {
- *len = 0;
- res = FALSE;
- goto out;
- }
-
- n = xmlXPathNodeSetGetLength (result->nodesetval);
- *len = n;
-
- *objs = g_new0 (CalDAVObject, n);
-
- for (i = 0; i < n; i++) {
- CalDAVObject *object;
- xmlXPathObjectPtr xpres;
-
- object = *objs + i;
- /* see if we got a status child in the response element */
-
- xpres = xpath_eval (xpctx, XPATH_HREF, i + 1);
- /* use full path from a href, to let calendar-multiget work properly */
- object->href = xp_object_get_string (xpres);
-
- xpres = xpath_eval (xpctx,XPATH_STATUS , i + 1);
- object->status = xp_object_get_status (xpres);
-
- if (object->status && object->status != 200) {
- continue;
- }
-
- xpres = xpath_eval (xpctx, XPATH_GETETAG_STATUS, i + 1);
- object->status = xp_object_get_status (xpres);
-
- if (object->status != 200) {
- continue;
- }
-
- xpres = xpath_eval (xpctx, XPATH_GETETAG, i + 1);
- object->etag = xp_object_get_etag (xpres);
-
- xpres = xpath_eval (xpctx, XPATH_CALENDAR_DATA, i + 1);
- object->cdata = xp_object_get_string (xpres);
- }
-
-out:
- if (result != NULL)
- xmlXPathFreeObject (result);
- xmlXPathFreeContext (xpctx);
- xmlFreeDoc (doc);
- return res;
+ return success;
}
-/* returns whether was able to read the xpath_value from the server's response; *value contains the result */
static gboolean
-parse_propfind_response (SoupMessage *message,
- const gchar *xpath_status,
- const gchar *xpath_value,
- gchar **value)
-{
- xmlXPathContextPtr xpctx;
- xmlDocPtr doc;
- gboolean res = FALSE;
-
- g_return_val_if_fail (message != NULL, FALSE);
- g_return_val_if_fail (value != NULL, FALSE);
-
- doc = xmlReadMemory (
- message->response_body->data,
- message->response_body->length,
- "response.xml",
- NULL,
- 0);
-
- if (doc == NULL) {
- return FALSE;
- }
-
- xpctx = xmlXPathNewContext (doc);
- xmlXPathRegisterNs (xpctx, (xmlChar *) "D", (xmlChar *) "DAV:");
- xmlXPathRegisterNs (xpctx, (xmlChar *) "C", (xmlChar *) "urn:ietf:params:xml:ns:caldav");
- xmlXPathRegisterNs (xpctx, (xmlChar *) "CS", (xmlChar *) "http://calendarserver.org/ns/");
-
- if (xpath_status == NULL || xp_object_get_status (xpath_eval (xpctx, xpath_status)) == 200) {
- gchar *txt = xp_object_get_string (xpath_eval (xpctx, xpath_value));
-
- if (txt && *txt) {
- gint len = strlen (txt);
-
- if (*txt == '\"' && len > 2 && txt[len - 1] == '\"') {
- /* dequote */
- *value = g_strndup (txt + 1, len - 2);
- } else {
- *value = txt;
- txt = NULL;
- }
-
- res = (*value) != NULL;
- }
-
- g_free (txt);
- }
-
- xmlXPathFreeContext (xpctx);
- xmlFreeDoc (doc);
-
- return res;
-}
-
-/* ************************************************************************* */
-/* Authentication helpers for libsoup */
-
-static void
-soup_authenticate_bearer (SoupSession *session,
- SoupMessage *message,
- SoupAuth *auth,
- ECalBackendCalDAV *cbdav)
-{
- GError *local_error = NULL;
-
- caldav_setup_bearer_auth (cbdav, TRUE, E_SOUP_AUTH_BEARER (auth), NULL, &local_error);
-
- /* Stash the error to be picked up by caldav_credentials_required_sync().
- * There's no way to explicitly propagate a GError directly
- * through libsoup, so we have to work around it. */
- if (local_error != NULL) {
- g_mutex_lock (&cbdav->priv->bearer_auth_error_lock);
-
- /* Warn about an unclaimed error before we clear it.
- * This is just to verify the errors we set here are
- * actually making it back to the user. */
- g_warn_if_fail (cbdav->priv->bearer_auth_error == NULL);
- g_clear_error (&cbdav->priv->bearer_auth_error);
-
- g_propagate_error (
- &cbdav->priv->bearer_auth_error, local_error);
-
- g_mutex_unlock (&cbdav->priv->bearer_auth_error_lock);
- }
-}
-
-static void
-soup_authenticate (SoupSession *session,
- SoupMessage *msg,
- SoupAuth *auth,
- gboolean retrying,
- gpointer data)
+ecb_caldav_disconnect_sync (ECalMetaBackend *meta_backend,
+ GCancellable *cancellable,
+ GError **error)
{
ECalBackendCalDAV *cbdav;
- ESourceAuthentication *auth_extension;
- ESource *source;
- const gchar *extension_name;
-
- cbdav = E_CAL_BACKEND_CALDAV (data);
-
- source = e_backend_get_source (E_BACKEND (data));
- extension_name = E_SOURCE_EXTENSION_AUTHENTICATION;
- auth_extension = e_source_get_extension (source, extension_name);
-
- if (E_IS_SOUP_AUTH_BEARER (auth)) {
- g_object_ref (auth);
- g_warn_if_fail ((gpointer) cbdav->priv->using_bearer_auth == (gpointer) auth);
- g_clear_object (&cbdav->priv->using_bearer_auth);
- cbdav->priv->using_bearer_auth = E_SOUP_AUTH_BEARER (auth);
- }
-
- if (retrying)
- return;
-
- if (cbdav->priv->using_bearer_auth) {
- soup_authenticate_bearer (session, msg, auth, cbdav);
-
- /* do not send same password twice, but keep it for later use */
- } else {
- gchar *auth_user;
- const gchar *username;
-
- auth_user = e_source_authentication_dup_user (auth_extension);
-
- username = cbdav->priv->credentials ? e_named_parameters_get (cbdav->priv->credentials,
E_SOURCE_CREDENTIAL_USERNAME) : NULL;
- if (!username || !*username)
- username = auth_user;
-
- if (!username || !*username || !cbdav->priv->credentials ||
- !e_named_parameters_exists (cbdav->priv->credentials, E_SOURCE_CREDENTIAL_PASSWORD))
- soup_message_set_status (msg, SOUP_STATUS_FORBIDDEN);
- else
- soup_auth_authenticate (auth, username, e_named_parameters_get
(cbdav->priv->credentials, E_SOURCE_CREDENTIAL_PASSWORD));
-
- g_free (auth_user);
- }
-}
-
-/* ************************************************************************* */
-/* direct CalDAV server access functions */
-
-static void
-redirect_handler (SoupMessage *msg,
- gpointer user_data)
-{
- if (SOUP_STATUS_IS_REDIRECTION (msg->status_code)) {
- SoupSession *soup_session = user_data;
- SoupURI *new_uri;
- const gchar *new_loc;
-
- new_loc = soup_message_headers_get_list (msg->response_headers, "Location");
- if (!new_loc)
- return;
-
- new_uri = soup_uri_new_with_base (soup_message_get_uri (msg), new_loc);
- if (!new_uri) {
- soup_message_set_status_full (
- msg,
- SOUP_STATUS_MALFORMED,
- _("Invalid Redirect URL"));
- return;
- }
-
- if (new_uri->host && g_str_has_suffix (new_uri->host, "yahoo.com")) {
- /* yahoo! returns port 7070, which is unreachable;
- * it also requires https being used (below call resets port as well) */
- soup_uri_set_scheme (new_uri, SOUP_URI_SCHEME_HTTPS);
- }
-
- soup_message_set_uri (msg, new_uri);
- soup_session_requeue_message (soup_session, msg);
-
- soup_uri_free (new_uri);
- }
-}
-
-static void
-send_and_handle_redirection (ECalBackendCalDAV *cbdav,
- SoupMessage *msg,
- gchar **new_location,
- GCancellable *cancellable,
- GError **error)
-{
- gchar *old_uri = NULL;
-
- g_return_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav));
- g_return_if_fail (msg != NULL);
-
- if (new_location)
- old_uri = soup_uri_to_string (soup_message_get_uri (msg), FALSE);
-
- e_soup_ssl_trust_connect (msg, e_backend_get_source (E_BACKEND (cbdav)));
-
- if (cbdav->priv->using_bearer_auth &&
- e_soup_auth_bearer_is_expired (cbdav->priv->using_bearer_auth)) {
- GError *local_error = NULL;
-
- if (!caldav_setup_bearer_auth (cbdav, FALSE, cbdav->priv->using_bearer_auth, cancellable,
&local_error)) {
- if (local_error) {
- soup_message_set_status_full (msg, SOUP_STATUS_BAD_REQUEST,
local_error->message);
- g_propagate_error (error, local_error);
- } else {
- soup_message_set_status (msg, SOUP_STATUS_BAD_REQUEST);
- }
- return;
- }
- }
-
- soup_message_set_flags (msg, SOUP_MESSAGE_NO_REDIRECT);
- soup_message_add_header_handler (msg, "got_body", "Location", G_CALLBACK (redirect_handler),
cbdav->priv->session);
- soup_message_headers_append (msg->request_headers, "Connection", "close");
- soup_session_send_message (cbdav->priv->session, msg);
-
- if (new_location) {
- gchar *new_loc = soup_uri_to_string (soup_message_get_uri (msg), FALSE);
-
- if (new_loc && old_uri && !g_str_equal (new_loc, old_uri))
- *new_location = new_loc;
- else
- g_free (new_loc);
- }
-
- g_free (old_uri);
-
- if (SOUP_STATUS_IS_SUCCESSFUL (msg->status_code))
- e_backend_ensure_source_status_connected (E_BACKEND (cbdav));
-}
-
-static gchar *
-caldav_generate_uri (ECalBackendCalDAV *cbdav,
- const gchar *target)
-{
- gchar *uri;
- const gchar *slash;
-
- slash = strrchr (target, '/');
- if (slash)
- target = slash + 1;
-
- /* uri *have* trailing slash already */
- uri = g_strconcat (cbdav->priv->uri, target, NULL);
-
- return uri;
-}
-
-static gboolean
-caldav_server_open_calendar (ECalBackendCalDAV *cbdav,
- gboolean *server_unreachable,
- gchar **out_certificate_pem,
- GTlsCertificateFlags *out_certificate_errors,
- GCancellable *cancellable,
- GError **perror)
-{
- SoupMessage *message;
- const gchar *header;
- gboolean calendar_access;
- gboolean put_allowed;
- gboolean delete_allowed;
ESource *source;
- g_return_val_if_fail (cbdav != NULL, FALSE);
- g_return_val_if_fail (server_unreachable != NULL, FALSE);
+ g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (meta_backend), FALSE);
- message = soup_message_new (SOUP_METHOD_OPTIONS, cbdav->priv->uri);
- if (message == NULL) {
- g_propagate_error (perror, EDC_ERROR (NoSuchCal));
- return FALSE;
- }
+ cbdav = E_CAL_BACKEND_CALDAV (meta_backend);
- soup_message_headers_append (
- message->request_headers,
- "User-Agent", "Evolution/" VERSION);
+ if (cbdav->priv->webdav)
+ soup_session_abort (SOUP_SESSION (cbdav->priv->webdav));
- source = e_backend_get_source (E_BACKEND (cbdav));
- e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTING);
-
- send_and_handle_redirection (cbdav, message, NULL, cancellable, perror);
-
- if (!SOUP_STATUS_IS_SUCCESSFUL (message->status_code)) {
- e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
-
- switch (message->status_code) {
- case SOUP_STATUS_CANT_RESOLVE:
- case SOUP_STATUS_CANT_RESOLVE_PROXY:
- case SOUP_STATUS_CANT_CONNECT:
- case SOUP_STATUS_CANT_CONNECT_PROXY:
- *server_unreachable = TRUE;
- break;
- case SOUP_STATUS_SSL_FAILED:
- if (out_certificate_pem && out_certificate_errors) {
- GTlsCertificate *certificate = NULL;
-
- g_object_get (G_OBJECT (message),
- "tls-certificate", &certificate,
- "tls-errors", out_certificate_errors,
- NULL);
-
- if (certificate) {
- g_object_get (certificate, "certificate-pem", out_certificate_pem,
NULL);
- g_object_unref (certificate);
- }
- }
- break;
- }
-
- status_code_to_result (message, cbdav, TRUE, perror);
-
- g_object_unref (message);
- return FALSE;
- }
-
- /* parse the dav header, we are intreseted in the
- * calendar-access bit only at the moment */
- header = soup_message_headers_get_list (message->response_headers, "DAV");
- if (header) {
- calendar_access = soup_header_contains (header, "calendar-access");
- cbdav->priv->calendar_schedule = soup_header_contains (header, "calendar-schedule");
- } else {
- calendar_access = FALSE;
- cbdav->priv->calendar_schedule = FALSE;
- }
-
- /* parse the Allow header and look for PUT, DELETE at the
- * moment (maybe we should check more here, for REPORT eg) */
- header = soup_message_headers_get_list (message->response_headers, "Allow");
- if (header) {
- /* The POST added for FastMail servers, which doesn't advertise PUT on collections. */
- put_allowed = soup_header_contains (header, "PUT") || soup_header_contains (header, "POST");
- delete_allowed = soup_header_contains (header, "DELETE");
- } else
- put_allowed = delete_allowed = FALSE;
-
- g_object_unref (message);
-
- if (calendar_access) {
- e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTED);
- e_cal_backend_set_writable (
- E_CAL_BACKEND (cbdav),
- put_allowed && delete_allowed);
- return TRUE;
- }
+ g_clear_object (&cbdav->priv->webdav);
+ source = e_backend_get_source (E_BACKEND (meta_backend));
e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
- g_propagate_error (perror, EDC_ERROR (PermissionDenied));
- return FALSE;
-}
-
-static gpointer
-caldav_unref_thread (gpointer cbdav)
-{
- g_object_unref (cbdav);
-
- return NULL;
-}
-
-static void
-caldav_unref_in_thread (ECalBackendCalDAV *cbdav)
-{
- GThread *thread;
- g_return_if_fail (cbdav != NULL);
-
- thread = g_thread_new (NULL, caldav_unref_thread, cbdav);
- g_thread_unref (thread);
-}
-
-static gboolean
-caldav_credentials_required_sync (ECalBackendCalDAV *cbdav,
- gboolean ref_cbdav,
- gboolean first_attempt,
- GCancellable *cancellable,
- GError **error)
-{
- gboolean success = TRUE;
-
- if (ref_cbdav)
- g_object_ref (cbdav);
-
- /* This function is called when we receive a 4xx response code for
- * authentication failures. If we're using Bearer authentication,
- * there should be a GError available. Return the GError to avoid
- * inappropriately prompting for a password. */
- g_mutex_lock (&cbdav->priv->bearer_auth_error_lock);
- if (cbdav->priv->bearer_auth_error != NULL) {
- g_propagate_error (error, cbdav->priv->bearer_auth_error);
- cbdav->priv->bearer_auth_error = NULL;
- success = FALSE;
- }
- g_mutex_unlock (&cbdav->priv->bearer_auth_error_lock);
-
- if (success) {
- success = e_backend_credentials_required_sync (E_BACKEND (cbdav),
- (first_attempt || !cbdav->priv->credentials ||
- !e_named_parameters_exists (cbdav->priv->credentials, E_SOURCE_CREDENTIAL_PASSWORD))
- ? E_SOURCE_CREDENTIALS_REASON_REQUIRED :
- E_SOURCE_CREDENTIALS_REASON_REJECTED,
- NULL, 0, NULL, cancellable, error);
- }
-
- if (ref_cbdav)
- caldav_unref_in_thread (cbdav);
-
- return success;
-}
-
-static gconstpointer
-compat_libxml_output_buffer_get_content (xmlOutputBufferPtr buf,
- gsize *out_len)
-{
-#ifdef LIBXML2_NEW_BUFFER
- *out_len = xmlOutputBufferGetSize (buf);
- return xmlOutputBufferGetContent (buf);
-#else
- *out_len = buf->buffer->use;
- return buf->buffer->content;
-#endif
+ return TRUE;
}
-/* Returns whether calendar changed on the server. This works only when server
- * supports 'getctag' extension. */
-static gboolean
-check_calendar_changed_on_server (ECalBackendCalDAV *cbdav,
- gboolean save_ctag,
- GCancellable *cancellable)
+static const gchar *
+ecb_caldav_get_vcalendar_uid (icalcomponent *vcalendar)
{
- xmlOutputBufferPtr buf;
- SoupMessage *message;
- xmlDocPtr doc;
- xmlNodePtr root, node;
- xmlNsPtr ns, nsdav;
- gconstpointer buf_content;
- gsize buf_size;
- gboolean result = TRUE;
-
- g_return_val_if_fail (cbdav != NULL, TRUE);
-
- /* no support for 'getctag', thus update cache */
- if (!cbdav->priv->ctag_supported)
- return TRUE;
-
- /* Prepare the soup message */
- message = soup_message_new ("PROPFIND", cbdav->priv->uri);
- if (message == NULL)
- return FALSE;
-
- doc = xmlNewDoc ((xmlChar *) "1.0");
- root = xmlNewDocNode (doc, NULL, (xmlChar *) "propfind", NULL);
- xmlDocSetRootElement (doc, root);
- nsdav = xmlNewNs (root, (xmlChar *) "DAV:", NULL);
- ns = xmlNewNs (root, (xmlChar *) "http://calendarserver.org/ns/", (xmlChar *) "CS");
-
- node = xmlNewTextChild (root, nsdav, (xmlChar *) "prop", NULL);
- node = xmlNewTextChild (node, nsdav, (xmlChar *) "getctag", NULL);
- xmlSetNs (node, ns);
-
- buf = xmlAllocOutputBuffer (NULL);
- xmlNodeDumpOutput (buf, doc, root, 0, 1, NULL);
- xmlOutputBufferFlush (buf);
-
- soup_message_headers_append (
- message->request_headers,
- "User-Agent", "Evolution/" VERSION);
- soup_message_headers_append (
- message->request_headers,
- "Depth", "0");
-
- buf_content = compat_libxml_output_buffer_get_content (buf, &buf_size);
- soup_message_set_request (
- message,
- "application/xml",
- SOUP_MEMORY_COPY,
- buf_content, buf_size);
-
- /* Send the request now */
- send_and_handle_redirection (cbdav, message, NULL, cancellable, NULL);
-
- /* Clean up the memory */
- xmlOutputBufferClose (buf);
- xmlFreeDoc (doc);
-
- /* Check the result */
- if (message->status_code == SOUP_STATUS_UNAUTHORIZED || message->status_code ==
SOUP_STATUS_FORBIDDEN) {
- caldav_credentials_required_sync (cbdav, TRUE, FALSE, NULL, NULL);
- } else if (message->status_code != SOUP_STATUS_MULTI_STATUS) {
- /* does not support it, but report calendar changed to update cache */
- cbdav->priv->ctag_supported = FALSE;
- } else {
- gchar *ctag = NULL;
-
- if (parse_propfind_response (message, XPATH_GETCTAG_STATUS, XPATH_GETCTAG, &ctag)) {
- const gchar *my_ctag;
+ const gchar *uid = NULL;
+ icalcomponent *subcomp;
- my_ctag = e_cal_backend_store_get_key_value (
- cbdav->priv->store, CALDAV_CTAG_KEY);
+ g_return_val_if_fail (vcalendar != NULL, NULL);
+ g_return_val_if_fail (icalcomponent_isa (vcalendar) == ICAL_VCALENDAR_COMPONENT, NULL);
- if (ctag && my_ctag && g_str_equal (ctag, my_ctag)) {
- /* ctag is same, no change in the calendar */
- result = FALSE;
- } else if (save_ctag) {
- /* do not store ctag now, do it rather after complete sync */
- g_free (cbdav->priv->ctag_to_store);
- cbdav->priv->ctag_to_store = ctag;
- ctag = NULL;
- }
+ for (subcomp = icalcomponent_get_first_component (vcalendar, ICAL_ANY_COMPONENT);
+ subcomp && !uid;
+ subcomp = icalcomponent_get_next_component (vcalendar, ICAL_ANY_COMPONENT)) {
+ icalcomponent_kind kind = icalcomponent_isa (subcomp);
- g_free (ctag);
- } else {
- cbdav->priv->ctag_supported = FALSE;
+ if (kind == ICAL_VEVENT_COMPONENT ||
+ kind == ICAL_VJOURNAL_COMPONENT ||
+ kind == ICAL_VTODO_COMPONENT) {
+ uid = icalcomponent_get_uid (subcomp);
+ if (uid && !*uid)
+ uid = NULL;
}
}
- g_object_unref (message);
-
- return result;
+ return uid;
}
-/* only_hrefs is a list of requested objects to fetch; it has precedence from
- * start_time/end_time, which are used only when both positive.
- * Times are supposed to be in UTC, if set.
- */
-static gboolean
-caldav_server_list_objects (ECalBackendCalDAV *cbdav,
- CalDAVObject **objs,
- gint *len,
- GSList *only_hrefs,
- time_t start_time,
- time_t end_time,
- GCancellable *cancellable)
+static void
+ecb_caldav_update_nfo_with_vcalendar (ECalMetaBackendInfo *nfo,
+ icalcomponent *vcalendar,
+ const gchar *etag)
{
- xmlOutputBufferPtr buf;
- SoupMessage *message;
- xmlNodePtr node;
- xmlNodePtr sn;
- xmlNodePtr root;
- xmlDocPtr doc;
- xmlNsPtr nsdav;
- xmlNsPtr nscd;
- gconstpointer buf_content;
- gsize buf_size;
- gboolean result;
-
- /* Allocate the soup message */
- message = soup_message_new ("REPORT", cbdav->priv->uri);
- if (message == NULL)
- return FALSE;
-
- /* Maybe we should just do a g_strdup_printf here? */
- /* Prepare request body */
- doc = xmlNewDoc ((xmlChar *) "1.0");
- if (!only_hrefs)
- root = xmlNewDocNode (doc, NULL, (xmlChar *) "calendar-query", NULL);
- else
- root = xmlNewDocNode (doc, NULL, (xmlChar *) "calendar-multiget", NULL);
- nscd = xmlNewNs (root, (xmlChar *) "urn:ietf:params:xml:ns:caldav", (xmlChar *) "C");
- xmlSetNs (root, nscd);
- xmlDocSetRootElement (doc, root);
-
- /* Add webdav tags */
- nsdav = xmlNewNs (root, (xmlChar *) "DAV:", (xmlChar *) "D");
- node = xmlNewTextChild (root, nsdav, (xmlChar *) "prop", NULL);
- xmlNewTextChild (node, nsdav, (xmlChar *) "getetag", NULL);
- if (only_hrefs) {
- GSList *l;
-
- xmlNewTextChild (node, nscd, (xmlChar *) "calendar-data", NULL);
- for (l = only_hrefs; l; l = l->next) {
- if (l->data) {
- xmlNewTextChild (root, nsdav, (xmlChar *) "href", (xmlChar *) l->data);
- }
- }
- } else {
- node = xmlNewTextChild (root, nscd, (xmlChar *) "filter", NULL);
- node = xmlNewTextChild (node, nscd, (xmlChar *) "comp-filter", NULL);
- xmlSetProp (node, (xmlChar *) "name", (xmlChar *) "VCALENDAR");
-
- sn = xmlNewTextChild (node, nscd, (xmlChar *) "comp-filter", NULL);
- switch (e_cal_backend_get_kind (E_CAL_BACKEND (cbdav))) {
- default:
- case ICAL_VEVENT_COMPONENT:
- xmlSetProp (sn, (xmlChar *) "name", (xmlChar *) "VEVENT");
- break;
- case ICAL_VJOURNAL_COMPONENT:
- xmlSetProp (sn, (xmlChar *) "name", (xmlChar *) "VJOURNAL");
- break;
- case ICAL_VTODO_COMPONENT:
- xmlSetProp (sn, (xmlChar *) "name", (xmlChar *) "VTODO");
- break;
- }
+ icalcomponent *subcomp;
+ const gchar *uid;
- if (start_time > 0 || end_time > 0) {
- gchar *tmp;
+ g_return_if_fail (nfo != NULL);
+ g_return_if_fail (vcalendar != NULL);
- sn = xmlNewTextChild (sn, nscd, (xmlChar *) "time-range", NULL);
+ uid = ecb_caldav_get_vcalendar_uid (vcalendar);
- if (start_time > 0) {
- tmp = isodate_from_time_t (start_time);
- xmlSetProp (sn, (xmlChar *) "start", (xmlChar *) tmp);
- g_free (tmp);
- }
+ if (!etag || !*etag)
+ etag = nfo->revision;
- if (end_time > 0) {
- tmp = isodate_from_time_t (end_time);
- xmlSetProp (sn, (xmlChar *) "end", (xmlChar *) tmp);
- g_free (tmp);
- }
- }
- }
+ for (subcomp = icalcomponent_get_first_component (vcalendar, ICAL_ANY_COMPONENT);
+ subcomp;
+ subcomp = icalcomponent_get_next_component (vcalendar, ICAL_ANY_COMPONENT)) {
+ icalcomponent_kind kind = icalcomponent_isa (subcomp);
- buf = xmlAllocOutputBuffer (NULL);
- xmlNodeDumpOutput (buf, doc, root, 0, 1, NULL);
- xmlOutputBufferFlush (buf);
-
- /* Prepare the soup message */
- soup_message_headers_append (
- message->request_headers,
- "User-Agent", "Evolution/" VERSION);
- soup_message_headers_append (
- message->request_headers,
- "Depth", "1");
-
- buf_content = compat_libxml_output_buffer_get_content (buf, &buf_size);
- soup_message_set_request (
- message,
- "application/xml",
- SOUP_MEMORY_COPY,
- buf_content, buf_size);
-
- /* Send the request now */
- send_and_handle_redirection (cbdav, message, NULL, cancellable, NULL);
-
- /* Clean up the memory */
- xmlOutputBufferClose (buf);
- xmlFreeDoc (doc);
-
- /* Check the result */
- if (message->status_code != SOUP_STATUS_MULTI_STATUS) {
- switch (message->status_code) {
- case SOUP_STATUS_CANT_RESOLVE:
- case SOUP_STATUS_CANT_RESOLVE_PROXY:
- case SOUP_STATUS_CANT_CONNECT:
- case SOUP_STATUS_CANT_CONNECT_PROXY:
- cbdav->priv->opened = FALSE;
- update_slave_cmd (cbdav->priv, SLAVE_SHOULD_SLEEP);
- e_cal_backend_set_writable (
- E_CAL_BACKEND (cbdav), FALSE);
- break;
- case SOUP_STATUS_UNAUTHORIZED:
- case SOUP_STATUS_FORBIDDEN:
- caldav_credentials_required_sync (cbdav, TRUE, FALSE, NULL, NULL);
- break;
- default:
- g_warning ("Server did not response with SOUP_STATUS_MULTI_STATUS, but with code %d
(%s)", message->status_code, soup_status_get_phrase (message->status_code) ? soup_status_get_phrase
(message->status_code) : "Unknown code");
- break;
+ if (kind == ICAL_VEVENT_COMPONENT ||
+ kind == ICAL_VJOURNAL_COMPONENT ||
+ kind == ICAL_VTODO_COMPONENT) {
+ e_cal_util_set_x_property (subcomp, E_CALDAV_X_ETAG, etag);
}
-
- g_object_unref (message);
- return FALSE;
- }
-
- /* Parse the response body */
- result = parse_report_response (message, objs, len);
-
- g_object_unref (message);
- return result;
-}
-
-static gboolean
-caldav_server_download_attachment (ECalBackendCalDAV *cbdav,
- const gchar *attachment_uri,
- gchar **content,
- gsize *len,
- GError **error)
-{
- SoupMessage *message;
-
- g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav), FALSE);
- g_return_val_if_fail (attachment_uri != NULL, FALSE);
- g_return_val_if_fail (content != NULL, FALSE);
- g_return_val_if_fail (len != NULL, FALSE);
-
- message = soup_message_new (SOUP_METHOD_GET, attachment_uri);
- if (message == NULL) {
- g_propagate_error (error, EDC_ERROR (InvalidObject));
- return FALSE;
- }
-
- soup_message_headers_append (message->request_headers, "User-Agent", "Evolution/" VERSION);
- send_and_handle_redirection (cbdav, message, NULL, NULL, NULL);
-
- if (!SOUP_STATUS_IS_SUCCESSFUL (message->status_code)) {
- status_code_to_result (message, cbdav, FALSE, error);
-
- if (message->status_code == SOUP_STATUS_UNAUTHORIZED || message->status_code ==
SOUP_STATUS_FORBIDDEN)
- caldav_credentials_required_sync (cbdav, FALSE, FALSE, NULL, NULL);
-
- g_object_unref (message);
- return FALSE;
- }
-
- *len = message->response_body->length;
- *content = g_memdup (message->response_body->data, *len);
-
- g_object_unref (message);
-
- return TRUE;
-}
-
-static gboolean
-caldav_server_get_object (ECalBackendCalDAV *cbdav,
- CalDAVObject *object,
- GCancellable *cancellable,
- GError **perror)
-{
- SoupMessage *message;
- const gchar *hdr;
- gchar *uri;
-
- g_return_val_if_fail (object != NULL && object->href != NULL, FALSE);
-
- uri = caldav_generate_uri (cbdav, object->href);
- message = soup_message_new (SOUP_METHOD_GET, uri);
- if (message == NULL) {
- g_free (uri);
- g_propagate_error (perror, EDC_ERROR (NoSuchCal));
- return FALSE;
- }
-
- soup_message_headers_append (
- message->request_headers,
- "User-Agent", "Evolution/" VERSION);
-
- send_and_handle_redirection (cbdav, message, NULL, cancellable, perror);
-
- if (!SOUP_STATUS_IS_SUCCESSFUL (message->status_code)) {
- status_code_to_result (message, cbdav, FALSE, perror);
-
- if (message->status_code == SOUP_STATUS_UNAUTHORIZED || message->status_code ==
SOUP_STATUS_FORBIDDEN)
- caldav_credentials_required_sync (cbdav, FALSE, FALSE, NULL, NULL);
- else if (message->status_code != SOUP_STATUS_NOT_FOUND)
- g_warning ("Could not fetch object '%s' from server, status:%d (%s)", uri,
message->status_code, soup_status_get_phrase (message->status_code) ? soup_status_get_phrase
(message->status_code) : "Unknown code");
- g_object_unref (message);
- g_free (uri);
- return FALSE;
}
- hdr = soup_message_headers_get_list (message->response_headers, "Content-Type");
+ g_warn_if_fail (nfo->object == NULL);
+ nfo->object = icalcomponent_as_ical_string_r (vcalendar);
- if (hdr == NULL || g_ascii_strncasecmp (hdr, "text/calendar", 13)) {
- g_propagate_error (perror, EDC_ERROR (InvalidObject));
- g_object_unref (message);
- g_warning ("Object to fetch '%s' not of type text/calendar", uri);
- g_free (uri);
- return FALSE;
+ if (!nfo->uid || !*(nfo->uid)) {
+ g_free (nfo->uid);
+ nfo->uid = g_strdup (uid);
}
- hdr = soup_message_headers_get_list (message->response_headers, "ETag");
+ if (g_strcmp0 (etag, nfo->revision) != 0) {
+ gchar *copy = g_strdup (etag);
- if (hdr != NULL) {
- g_free (object->etag);
- object->etag = quote_etag (hdr);
- } else if (!object->etag) {
- g_warning ("UUHH no ETag, now that's bad! (at '%s')", uri);
+ g_free (nfo->revision);
+ nfo->revision = copy;
}
- g_free (uri);
-
- g_free (object->cdata);
- object->cdata = g_strdup (message->response_body->data);
-
- g_object_unref (message);
-
- return TRUE;
-}
-
-static void
-caldav_post_freebusy (ECalBackendCalDAV *cbdav,
- const gchar *url,
- gchar **post_fb,
- GCancellable *cancellable,
- GError **error)
-{
- SoupMessage *message;
-
- message = soup_message_new (SOUP_METHOD_POST, url);
- if (message == NULL) {
- g_propagate_error (error, EDC_ERROR (NoSuchCal));
- return;
- }
-
- soup_message_headers_append (message->request_headers, "User-Agent", "Evolution/" VERSION);
- soup_message_set_request (
- message,
- "text/calendar; charset=utf-8",
- SOUP_MEMORY_COPY,
- *post_fb, strlen (*post_fb));
-
- send_and_handle_redirection (cbdav, message, NULL, cancellable, error);
-
- if (!SOUP_STATUS_IS_SUCCESSFUL (message->status_code)) {
- status_code_to_result (message, cbdav, FALSE, error);
- if (message->status_code == SOUP_STATUS_UNAUTHORIZED || message->status_code ==
SOUP_STATUS_FORBIDDEN)
- caldav_credentials_required_sync (cbdav, FALSE, FALSE, NULL, NULL);
- else
- g_warning ("Could not post free/busy request to '%s', status:%d (%s)", url,
message->status_code, soup_status_get_phrase (message->status_code) ? soup_status_get_phrase
(message->status_code) : "Unknown code");
-
- g_object_unref (message);
-
- return;
- }
-
- g_free (*post_fb);
- *post_fb = g_strdup (message->response_body->data);
-
- g_object_unref (message);
-}
-
-static gchar *
-caldav_gen_file_from_uid (ECalBackendCalDAV *cbdav,
- const gchar *uid)
-{
- gchar *filename, *res;
-
- if (!uid)
- return NULL;
-
- filename = g_strconcat (uid, ".ics", NULL);
- res = soup_uri_encode (filename, NULL);
- g_free (filename);
-
- return res;
-}
-
-static gchar *
-caldav_gen_file_from_uid_cal (ECalBackendCalDAV *cbdav,
- icalcomponent *icalcomp)
-{
- icalcomponent_kind my_kind;
- const gchar *uid = NULL;
-
- g_return_val_if_fail (cbdav != NULL, NULL);
- g_return_val_if_fail (icalcomp != NULL, NULL);
-
- my_kind = e_cal_backend_get_kind (E_CAL_BACKEND (cbdav));
- if (icalcomponent_isa (icalcomp) == ICAL_VCALENDAR_COMPONENT) {
- icalcomponent *subcomp;
-
- for (subcomp = icalcomponent_get_first_component (icalcomp, my_kind);
- subcomp;
- subcomp = icalcomponent_get_next_component (icalcomp, my_kind)) {
- uid = icalcomponent_get_uid (subcomp);
- if (uid && *uid)
- break;
- }
- } else if (icalcomponent_isa (icalcomp) == my_kind) {
- uid = icalcomponent_get_uid (icalcomp);
- }
-
- return caldav_gen_file_from_uid (cbdav, uid);
}
static gboolean
-caldav_server_put_object (ECalBackendCalDAV *cbdav,
- CalDAVObject *object,
- icalcomponent *icalcomp,
- GCancellable *cancellable,
- GError **perror)
+ecb_caldav_multiget_response_cb (EWebDAVSession *webdav,
+ xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prop_prefix,
+ const SoupURI *request_uri,
+ const gchar *href,
+ guint status_code,
+ gpointer user_data)
{
- SoupMessage *message;
- const gchar *hdr;
- gchar *uri;
-
- hdr = NULL;
+ GSList **from_link = user_data;
- g_return_val_if_fail (object != NULL && object->cdata != NULL, FALSE);
+ g_return_val_if_fail (from_link != NULL, FALSE);
- uri = caldav_generate_uri (cbdav, object->href);
- message = soup_message_new (SOUP_METHOD_PUT, uri);
- g_free (uri);
- if (message == NULL) {
- g_propagate_error (perror, EDC_ERROR (NoSuchCal));
- return FALSE;
- }
+ if (!xpath_prop_prefix) {
+ e_xml_xpath_context_register_namespaces (xpath_ctx, "C", E_WEBDAV_NS_CALDAV, NULL);
+ } else if (status_code == SOUP_STATUS_OK) {
+ gchar *calendar_data, *etag;
- soup_message_headers_append (
- message->request_headers,
- "User-Agent", "Evolution/" VERSION);
+ g_return_val_if_fail (href != NULL, FALSE);
- /* For new items we use the If-None-Match so we don't
- * acidently override resources, for item updates we
- * use the If-Match header to avoid the Lost-update
- * problem */
- if (object->etag == NULL) {
- soup_message_headers_append (message->request_headers, "If-None-Match", "*");
- } else {
- soup_message_headers_append (
- message->request_headers,
- "If-Match", object->etag);
- }
+ calendar_data = e_xml_xpath_eval_as_string (xpath_ctx, "%s/C:calendar-data",
xpath_prop_prefix);
+ etag = e_webdav_session_util_maybe_dequote (e_xml_xpath_eval_as_string (xpath_ctx,
"%s/D:getetag", xpath_prop_prefix));
- soup_message_set_request (
- message,
- "text/calendar; charset=utf-8",
- SOUP_MEMORY_COPY,
- object->cdata,
- strlen (object->cdata));
+ if (calendar_data) {
+ icalcomponent *vcalendar;
- uri = NULL;
- send_and_handle_redirection (cbdav, message, &uri, cancellable, perror);
+ vcalendar = icalcomponent_new_from_string (calendar_data);
+ if (vcalendar) {
+ const gchar *uid;
- if (uri) {
- gchar *file = strrchr (uri, '/');
+ uid = ecb_caldav_get_vcalendar_uid (vcalendar);
+ if (uid) {
+ GSList *link;
- /* there was a redirect, update href properly */
- if (file) {
- gchar *decoded;
+ for (link = *from_link; link; link = g_slist_next (link)) {
+ ECalMetaBackendInfo *nfo = link->data;
- g_free (object->href);
+ if (!nfo)
+ continue;
- decoded = soup_uri_decode (file + 1);
- object->href = soup_uri_encode (decoded ? decoded : (file + 1), NULL);
+ if (g_strcmp0 (nfo->extra, href) == 0) {
+ /* If the server returns data in the same order as it
had been requested,
+ then this speeds up lookup for the matching
object. */
+ if (link == *from_link)
+ *from_link = g_slist_next (*from_link);
- g_free (decoded);
- }
+ ecb_caldav_update_nfo_with_vcalendar (nfo, vcalendar,
etag);
- g_free (uri);
- }
-
- if (status_code_to_result (message, cbdav, FALSE, perror)) {
- GError *local_error = NULL;
-
- hdr = soup_message_headers_get_list (message->response_headers, "ETag");
- if (hdr != NULL) {
- g_free (object->etag);
- object->etag = quote_etag (hdr);
- }
-
- /* "201 Created" can contain a Location with a link where the component was saved */
- hdr = soup_message_headers_get_list (message->response_headers, "Location");
- if (hdr) {
- /* reflect possible href change */
- gchar *file = strrchr (hdr, '/');
-
- if (file) {
- gchar *decoded;
-
- g_free (object->href);
-
- decoded = soup_uri_decode (file + 1);
- object->href = soup_uri_encode (decoded ? decoded : (file + 1), NULL);
-
- g_free (decoded);
- }
- }
-
- if (!caldav_server_get_object (cbdav, object, cancellable, &local_error)) {
- if (g_error_matches (local_error, E_DATA_CAL_ERROR, ObjectNotFound)) {
- gchar *file;
-
- /* OK, the event was properly created, but cannot be found on the place
- * where it was PUT - why didn't server tell us where it saved it? */
- g_clear_error (&local_error);
-
- /* try whether it's saved as its UID.ics file */
- file = caldav_gen_file_from_uid_cal (cbdav, icalcomp);
- if (file) {
- g_free (object->href);
- object->href = file;
-
- if (!caldav_server_get_object (cbdav, object, cancellable,
&local_error)) {
- if (g_error_matches (local_error, E_DATA_CAL_ERROR,
ObjectNotFound)) {
- g_clear_error (&local_error);
-
- /* not sure what can happen, but do not need to guess
for ever,
- * thus report success and update the calendar to get
fresh info */
- update_slave_cmd (cbdav->priv, SLAVE_SHOULD_WORK);
- g_cond_signal (&cbdav->priv->cond);
+ break;
}
}
}
- }
- }
-
- if (!local_error) {
- icalcomponent *use_comp = NULL;
- if (object->cdata) {
- /* maybe server also modified component, thus rather store the server's */
- use_comp = icalparser_parse_string (object->cdata);
+ icalcomponent_free (vcalendar);
}
-
- if (!use_comp)
- use_comp = icalcomp;
-
- put_comp_to_cache (cbdav, use_comp, object->href, object->etag);
-
- if (use_comp != icalcomp)
- icalcomponent_free (use_comp);
- } else {
- g_propagate_error (perror, local_error);
}
- }
- if (message->status_code == SOUP_STATUS_UNAUTHORIZED || message->status_code ==
SOUP_STATUS_FORBIDDEN) {
- caldav_credentials_required_sync (cbdav, FALSE, FALSE, NULL, NULL);
+ g_free (calendar_data);
+ g_free (etag);
}
- g_object_unref (message);
-
return TRUE;
}
-static void
-caldav_server_delete_object (ECalBackendCalDAV *cbdav,
- CalDAVObject *object,
- GCancellable *cancellable,
- GError **perror)
-{
- SoupMessage *message;
- gchar *uri;
-
- g_return_if_fail (object != NULL && object->href != NULL);
-
- uri = caldav_generate_uri (cbdav, object->href);
- message = soup_message_new (SOUP_METHOD_DELETE, uri);
- g_free (uri);
- if (message == NULL) {
- g_propagate_error (perror, EDC_ERROR (NoSuchCal));
- return;
- }
-
- soup_message_headers_append (
- message->request_headers,
- "User-Agent", "Evolution/" VERSION);
-
- if (object->etag != NULL) {
- soup_message_headers_append (
- message->request_headers,
- "If-Match", object->etag);
- }
-
- send_and_handle_redirection (cbdav, message, NULL, cancellable, perror);
-
- status_code_to_result (message, cbdav, FALSE, perror);
-
- if (message->status_code == SOUP_STATUS_UNAUTHORIZED || message->status_code == SOUP_STATUS_FORBIDDEN)
- caldav_credentials_required_sync (cbdav, FALSE, FALSE, NULL, NULL);
-
- g_object_unref (message);
-}
-
-static gboolean
-caldav_receive_schedule_outbox_url (ECalBackendCalDAV *cbdav,
- GCancellable *cancellable,
- GError **error)
-{
- SoupMessage *message;
- xmlOutputBufferPtr buf;
- xmlDocPtr doc;
- xmlNodePtr root, node;
- xmlNsPtr nsdav;
- gconstpointer buf_content;
- gsize buf_size;
- gchar *owner = NULL;
-
- g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav), FALSE);
- g_return_val_if_fail (cbdav->priv->schedule_outbox_url == NULL, TRUE);
-
- /* Prepare the soup message */
- message = soup_message_new ("PROPFIND", cbdav->priv->uri);
- if (message == NULL)
- return FALSE;
-
- doc = xmlNewDoc ((xmlChar *) "1.0");
- root = xmlNewDocNode (doc, NULL, (xmlChar *) "propfind", NULL);
- xmlDocSetRootElement (doc, root);
- nsdav = xmlNewNs (root, (xmlChar *) "DAV:", NULL);
-
- node = xmlNewTextChild (root, nsdav, (xmlChar *) "prop", NULL);
- xmlNewTextChild (node, nsdav, (xmlChar *) "owner", NULL);
-
- buf = xmlAllocOutputBuffer (NULL);
- xmlNodeDumpOutput (buf, doc, root, 0, 1, NULL);
- xmlOutputBufferFlush (buf);
-
- soup_message_headers_append (message->request_headers, "User-Agent", "Evolution/" VERSION);
- soup_message_headers_append (message->request_headers, "Depth", "0");
-
- buf_content = compat_libxml_output_buffer_get_content (buf, &buf_size);
- soup_message_set_request (
- message,
- "application/xml",
- SOUP_MEMORY_COPY,
- buf_content, buf_size);
-
- /* Send the request now */
- send_and_handle_redirection (cbdav, message, NULL, cancellable, error);
-
- /* Clean up the memory */
- xmlOutputBufferClose (buf);
- xmlFreeDoc (doc);
-
- /* Check the result */
- if (message->status_code == SOUP_STATUS_MULTI_STATUS && parse_propfind_response (message,
XPATH_OWNER_STATUS, XPATH_OWNER, &owner) && owner && *owner) {
- xmlNsPtr nscd;
- SoupURI *suri;
-
- g_object_unref (message);
-
- /* owner is a full path to the user's URL, thus change it in
- * calendar's uri when asking for schedule-outbox-URL */
- suri = soup_uri_new (cbdav->priv->uri);
- soup_uri_set_path (suri, owner);
- g_free (owner);
- owner = soup_uri_to_string (suri, FALSE);
- soup_uri_free (suri);
-
- message = soup_message_new ("PROPFIND", owner);
- if (message == NULL) {
- g_free (owner);
- return FALSE;
- }
-
- doc = xmlNewDoc ((xmlChar *) "1.0");
- root = xmlNewDocNode (doc, NULL, (xmlChar *) "propfind", NULL);
- xmlDocSetRootElement (doc, root);
- nsdav = xmlNewNs (root, (xmlChar *) "DAV:", NULL);
- nscd = xmlNewNs (root, (xmlChar *) "urn:ietf:params:xml:ns:caldav", (xmlChar *) "C");
-
- node = xmlNewTextChild (root, nsdav, (xmlChar *) "prop", NULL);
- xmlNewTextChild (node, nscd, (xmlChar *) "schedule-outbox-URL", NULL);
-
- buf = xmlAllocOutputBuffer (NULL);
- xmlNodeDumpOutput (buf, doc, root, 0, 1, NULL);
- xmlOutputBufferFlush (buf);
-
- soup_message_headers_append (message->request_headers, "User-Agent", "Evolution/" VERSION);
- soup_message_headers_append (message->request_headers, "Depth", "0");
-
- buf_content = compat_libxml_output_buffer_get_content (buf, &buf_size);
- soup_message_set_request (
- message,
- "application/xml",
- SOUP_MEMORY_COPY,
- buf_content, buf_size);
-
- /* Send the request now */
- send_and_handle_redirection (cbdav, message, NULL, cancellable, error);
-
- if (message->status_code == SOUP_STATUS_MULTI_STATUS && parse_propfind_response (message,
XPATH_SCHEDULE_OUTBOX_URL_STATUS, XPATH_SCHEDULE_OUTBOX_URL, &cbdav->priv->schedule_outbox_url)) {
- if (!*cbdav->priv->schedule_outbox_url) {
- g_free (cbdav->priv->schedule_outbox_url);
- cbdav->priv->schedule_outbox_url = NULL;
- } else {
- /* make it a full URI */
- suri = soup_uri_new (cbdav->priv->uri);
- soup_uri_set_path (suri, cbdav->priv->schedule_outbox_url);
- g_free (cbdav->priv->schedule_outbox_url);
- cbdav->priv->schedule_outbox_url = soup_uri_to_string (suri, FALSE);
- soup_uri_free (suri);
- }
- }
-
- /* Clean up the memory */
- xmlOutputBufferClose (buf);
- xmlFreeDoc (doc);
- } else if (message->status_code == SOUP_STATUS_UNAUTHORIZED || message->status_code ==
SOUP_STATUS_FORBIDDEN) {
- caldav_credentials_required_sync (cbdav, FALSE, FALSE, NULL, NULL);
- }
-
- if (message)
- g_object_unref (message);
-
- g_free (owner);
-
- return cbdav->priv->schedule_outbox_url != NULL;
-}
-
-/* ************************************************************************* */
-/* Synchronization foo */
-
-static gboolean extract_timezones (ECalBackendCalDAV *cbdav, icalcomponent *icomp);
-
-struct cache_comp_list
-{
- GSList *slist;
-};
-
static gboolean
-remove_complist_from_cache_and_notify_cb (gpointer key,
- gpointer value,
- gpointer data)
-{
- GSList *l;
- struct cache_comp_list *ccl = value;
- ECalBackendCalDAV *cbdav = data;
-
- for (l = ccl->slist; l; l = l->next) {
- ECalComponent *old_comp = l->data;
- ECalComponentId *id;
-
- id = e_cal_component_get_id (old_comp);
- if (!id) {
- continue;
- }
-
- if (e_cal_backend_store_remove_component (cbdav->priv->store, id->uid, id->rid)) {
- e_cal_backend_notify_component_removed ((ECalBackend *) cbdav, id, old_comp, NULL);
- }
-
- e_cal_component_free_id (id);
- }
- remove_cached_attachment (cbdav, (const gchar *) key);
-
- return FALSE;
-}
-
-static void
-free_comp_list (gpointer cclist)
-{
- struct cache_comp_list *ccl = cclist;
-
- g_return_if_fail (ccl != NULL);
-
- g_slist_foreach (ccl->slist, (GFunc) g_object_unref, NULL);
- g_slist_free (ccl->slist);
- g_free (ccl);
-}
-
-#define etags_match(_tag1, _tag2) ((_tag1 == _tag2) ? TRUE : \
- g_str_equal (_tag1 != NULL ? _tag1 : "", \
- _tag2 != NULL ? _tag2 : ""))
-
-/* start_time/end_time is an interval for checking changes. If both greater than zero,
- * only the interval is checked and the removed items are not notified, as they can
- * be still there.
-*/
-static void
-caldav_synchronize_cache (ECalBackendCalDAV *cbdav,
- time_t start_time,
- time_t end_time,
- gboolean can_check_ctag,
- GCancellable *cancellable)
-{
- CalDAVObject *sobjs, *object;
- GSList *c_objs, *c_iter; /* list of all items known from our cache */
- GTree *c_uid2complist; /* cache components list (with detached instances) sorted by (master's) uid */
- GHashTable *c_href2uid; /* connection between href and a (master's) uid */
- GSList *hrefs_to_update, *htu; /* list of href-s to update */
- gint i, len;
-
- /* intentionally do server-side checking first, and then the bool test,
- to store actual ctag value first, and then update the content, to not
- do it again the next time this function is called */
- if (!check_calendar_changed_on_server (cbdav, start_time == (time_t) 0, cancellable) &&
can_check_ctag) {
- /* no changes on the server, no update required */
- return;
- }
-
- len = 0;
- sobjs = NULL;
+ecb_caldav_multiget_from_sets_sync (ECalBackendCalDAV *cbdav,
+ GSList **in_link,
+ GSList **set2,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EXmlDocument *xml;
+ gint left_to_go = E_CALDAV_MAX_MULTIGET_AMOUNT;
+ GSList *link;
+ gboolean success = TRUE;
- /* get list of server objects */
- if (!caldav_server_list_objects (cbdav, &sobjs, &len, NULL, start_time, end_time, cancellable))
- return;
+ g_return_val_if_fail (in_link != NULL, FALSE);
+ g_return_val_if_fail (*in_link != NULL, FALSE);
+ g_return_val_if_fail (set2 != NULL, FALSE);
- c_objs = e_cal_backend_store_get_components (cbdav->priv->store);
+ xml = e_xml_document_new (E_WEBDAV_NS_CALDAV, "calendar-multiget");
+ g_return_val_if_fail (xml != NULL, FALSE);
- if (caldav_debug_show (DEBUG_SERVER_ITEMS)) {
- printf ("CalDAV - found %d objects on the server, locally stored %d objects\n", len,
g_slist_length (c_objs)); fflush (stdout);
- }
+ e_xml_document_add_namespaces (xml, "D", E_WEBDAV_NS_DAV, NULL);
- /* do not store changes in cache immediately - makes things significantly quicker */
- e_cal_backend_store_freeze_changes (cbdav->priv->store);
+ e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "prop");
+ e_xml_document_add_empty_element (xml, E_WEBDAV_NS_DAV, "getetag");
+ e_xml_document_add_empty_element (xml, E_WEBDAV_NS_CALDAV, "calendar-data");
+ e_xml_document_end_element (xml); /* prop */
- c_uid2complist = g_tree_new_full ((GCompareDataFunc) g_strcmp0, NULL, g_free, free_comp_list);
- c_href2uid = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+ link = *in_link;
- /* fill indexed hash and tree with cached components */
- for (c_iter = c_objs; c_iter; c_iter = g_slist_next (c_iter)) {
- ECalComponent *ccomp = E_CAL_COMPONENT (c_iter->data);
- const gchar *uid = NULL;
- struct cache_comp_list *ccl;
- gchar *href;
+ while (link && left_to_go > 0) {
+ ECalMetaBackendInfo *nfo = link->data;
- e_cal_component_get_uid (ccomp, &uid);
- if (!uid) {
- g_warning ("broken component with NULL Id");
- continue;
+ link = g_slist_next (link);
+ if (!link) {
+ link = *set2;
+ *set2 = NULL;
}
- href = ecalcomp_get_href (ccomp);
-
- if (href == NULL) {
- g_warning ("href of object NULL :(");
+ if (!nfo)
continue;
- }
- ccl = g_tree_lookup (c_uid2complist, uid);
- if (ccl) {
- ccl->slist = g_slist_prepend (ccl->slist, g_object_ref (ccomp));
- } else {
- ccl = g_new0 (struct cache_comp_list, 1);
- ccl->slist = g_slist_append (NULL, g_object_ref (ccomp));
-
- /* make a copy, which will be used in the c_href2uid too */
- uid = g_strdup (uid);
+ left_to_go--;
- g_tree_insert (c_uid2complist, (gpointer) uid, ccl);
- }
+ /* iCloud returns broken calendar-multiget responses, with
+ empty <DAV:href> elements, thus read one-by-one for it.
+ This is confirmed as of 2017-04-11. */
+ if (cbdav->priv->is_icloud) {
+ gchar *calendar_data = NULL, *etag = NULL;
- if (g_hash_table_lookup (c_href2uid, href) == NULL) {
- /* uid is from a component or c_uid2complist key, thus will not be
- * freed before a removal from c_uid2complist, thus do not duplicate it,
- * rather save memory */
- g_hash_table_insert (c_href2uid, href, (gpointer) uid);
- } else {
- g_free (href);
- }
- }
+ success = e_webdav_session_get_data_sync (cbdav->priv->webdav,
+ nfo->extra, NULL, &etag, &calendar_data, NULL, cancellable, error);
- /* clear it now, we do not need it later */
- g_slist_foreach (c_objs, (GFunc) g_object_unref, NULL);
- g_slist_free (c_objs);
- c_objs = NULL;
+ if (success && calendar_data) {
+ icalcomponent *vcalendar;
- hrefs_to_update = NULL;
-
- /* see if we have to update or add some objects */
- for (i = 0, object = sobjs; i < len && cbdav->priv->slave_cmd == SLAVE_SHOULD_WORK; i++, object++) {
- ECalComponent *ccomp = NULL;
- gchar *etag = NULL;
- const gchar *uid;
- struct cache_comp_list *ccl;
-
- if (object->status != 200) {
- /* just continue here, so that the object
- * doesnt get removed from the cobjs list
- * - therefore it will be removed */
- continue;
- }
-
- uid = g_hash_table_lookup (c_href2uid, object->href);
- if (uid) {
- ccl = g_tree_lookup (c_uid2complist, uid);
- if (ccl) {
- GSList *sl;
- for (sl = ccl->slist; sl && !etag; sl = sl->next) {
- ccomp = sl->data;
- if (ccomp)
- etag = ecalcomp_get_etag (ccomp);
+ vcalendar = icalcomponent_new_from_string (calendar_data);
+ if (vcalendar) {
+ ecb_caldav_update_nfo_with_vcalendar (nfo, vcalendar, etag);
+ icalcomponent_free (vcalendar);
}
-
- if (!etag)
- ccomp = NULL;
- }
- }
-
- if (!etag || !etags_match (etag, object->etag)) {
- hrefs_to_update = g_slist_prepend (hrefs_to_update, object->href);
- } else if (uid && ccl) {
- /* all components cover by this uid are up-to-date */
- GSList *p;
-
- for (p = ccl->slist; p; p = p->next) {
- g_object_unref (p->data);
}
- g_slist_free (ccl->slist);
- ccl->slist = NULL;
- }
-
- g_free (etag);
- }
-
- /* free hash table, as it is not used anymore */
- g_hash_table_destroy (c_href2uid);
- c_href2uid = NULL;
-
- if (caldav_debug_show (DEBUG_SERVER_ITEMS)) {
- printf ("CalDAV - recognized %d items to update\n", g_slist_length (hrefs_to_update)); fflush
(stdout);
- }
-
- htu = hrefs_to_update;
- while (htu && cbdav->priv->slave_cmd == SLAVE_SHOULD_WORK) {
- gint count = 0;
- GSList *to_fetch = NULL;
-
- while (count < CALDAV_MAX_MULTIGET_AMOUNT && htu) {
- to_fetch = g_slist_prepend (to_fetch, htu->data);
- htu = htu->next;
- count++;
- }
-
- if (to_fetch && cbdav->priv->slave_cmd == SLAVE_SHOULD_WORK) {
- CalDAVObject *up_sobjs = NULL;
+ g_free (calendar_data);
+ g_free (etag);
- if (caldav_debug_show (DEBUG_SERVER_ITEMS)) {
- printf ("CalDAV - going to fetch %d items\n", g_slist_length (to_fetch));
fflush (stdout);
- }
-
- count = 0;
- if (!caldav_server_list_objects (cbdav, &up_sobjs, &count, to_fetch, 0, 0,
cancellable)) {
- fprintf (stderr, "CalDAV - failed to retrieve bunch of items\n"); fflush
(stderr);
+ if (!success)
break;
- }
+ } else {
+ SoupURI *suri;
+ gchar *path = NULL;
- if (caldav_debug_show (DEBUG_SERVER_ITEMS)) {
- printf ("CalDAV - fetched bunch of %d items\n", count); fflush (stdout);
+ suri = soup_uri_new (nfo->extra);
+ if (suri) {
+ path = soup_uri_to_string (suri, TRUE);
+ soup_uri_free (suri);
}
- /* we are going to update cache */
- /* they are downloaded, so process them */
- for (i = 0, object = up_sobjs; i < count /*&& cbdav->priv->slave_cmd ==
SLAVE_SHOULD_WORK */; i++, object++) {
- if (object->status == 200 && object->href && object->etag && object->cdata &&
*object->cdata) {
- icalcomponent *icomp = icalparser_parse_string (object->cdata);
+ e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "href");
+ e_xml_document_write_string (xml, path ? path : nfo->extra);
+ e_xml_document_end_element (xml); /* href */
- if (icomp) {
- put_server_comp_to_cache (cbdav, icomp, object->href,
object->etag, c_uid2complist);
- icalcomponent_free (icomp);
- }
- }
-
- /* these free immediately */
- caldav_object_free (object, FALSE);
- }
-
- /* cache update done for fetched items */
- g_free (up_sobjs);
+ g_free (path);
}
-
- /* do not free 'data' itself, it's part of 'sobjs' */
- g_slist_free (to_fetch);
}
- /* if not interrupted and not using the time range... */
- if (cbdav->priv->slave_cmd == SLAVE_SHOULD_WORK && (!start_time || !end_time)) {
- /* ...remove old (not on server anymore) items from our cache and notify of a removal */
- g_tree_foreach (c_uid2complist, remove_complist_from_cache_and_notify_cb, cbdav);
- }
-
- if (cbdav->priv->ctag_to_store) {
- /* store only when wasn't interrupted */
- if (cbdav->priv->slave_cmd == SLAVE_SHOULD_WORK && start_time == 0 && end_time == 0) {
- e_cal_backend_store_put_key_value (cbdav->priv->store, CALDAV_CTAG_KEY,
cbdav->priv->ctag_to_store);
- }
+ if (left_to_go != E_CALDAV_MAX_MULTIGET_AMOUNT &&
+ !cbdav->priv->is_icloud && success) {
+ GSList *from_link = *in_link;
- g_free (cbdav->priv->ctag_to_store);
- cbdav->priv->ctag_to_store = NULL;
+ success = e_webdav_session_report_sync (cbdav->priv->webdav, NULL, NULL, xml,
+ ecb_caldav_multiget_response_cb, &from_link, NULL, NULL, cancellable, error);
}
- /* save cache changes to disk finally */
- e_cal_backend_store_thaw_changes (cbdav->priv->store);
+ g_object_unref (xml);
- for (i = 0, object = sobjs; i < len; i++, object++) {
- caldav_object_free (object, FALSE);
- }
+ *in_link = link;
- g_tree_destroy (c_uid2complist);
- g_slist_free (hrefs_to_update);
- g_free (sobjs);
+ return success;
}
-static void
-check_server_tweaks (ECalBackendCalDAV *cbdav)
+static gboolean
+ecb_caldav_get_calendar_items_cb (EWebDAVSession *webdav,
+ xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prop_prefix,
+ const SoupURI *request_uri,
+ const gchar *href,
+ guint status_code,
+ gpointer user_data)
{
- SoupURI *suri;
+ GHashTable *known_items = user_data; /* gchar *href ~> ECalMetaBackendInfo * */
- g_return_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav));
+ g_return_val_if_fail (xpath_ctx != NULL, FALSE);
+ g_return_val_if_fail (known_items != NULL, FALSE);
- cbdav->priv->is_google = FALSE;
- cbdav->priv->is_icloud = FALSE;
+ if (!xpath_prop_prefix) {
+ e_xml_xpath_context_register_namespaces (xpath_ctx, "C", E_WEBDAV_NS_CALDAV, NULL);
+ } else if (status_code == SOUP_STATUS_OK) {
+ ECalMetaBackendInfo *nfo;
+ gchar *etag;
- g_return_if_fail (cbdav->priv->uri);
+ g_return_val_if_fail (href != NULL, FALSE);
- suri = soup_uri_new (cbdav->priv->uri);
- g_return_if_fail (suri != NULL);
+ /* Skip collection resource, if returned by the server (like iCloud.com does) */
+ if (g_str_has_suffix (href, "/") ||
+ (request_uri && request_uri->path && g_str_has_suffix (href, request_uri->path)))
+ return TRUE;
- cbdav->priv->is_google = suri->host && (
- g_ascii_strcasecmp (suri->host, "www.google.com") == 0 ||
- g_ascii_strcasecmp (suri->host, "apidata.googleusercontent.com") == 0);
+ etag = e_webdav_session_util_maybe_dequote (e_xml_xpath_eval_as_string (xpath_ctx,
"%s/D:getetag", xpath_prop_prefix));
+ /* Return 'TRUE' to not stop on faulty data from the server */
+ g_return_val_if_fail (etag != NULL, TRUE);
- cbdav->priv->is_icloud = suri->host && e_util_utf8_strstrcase (suri->host, ".icloud.com");
+ /* UID is unknown at this moment */
+ nfo = e_cal_meta_backend_info_new ("", etag, NULL, href);
- soup_uri_free (suri);
-}
-
-static void
-time_to_refresh_caldav_calendar_cb (ESource *source,
- gpointer user_data)
-{
- ECalBackendCalDAV *cbdav = user_data;
+ g_free (etag);
+ g_return_val_if_fail (nfo != NULL, FALSE);
- g_return_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav));
+ g_hash_table_insert (known_items, g_strdup (href), nfo);
+ }
- g_cond_signal (&cbdav->priv->cond);
+ return TRUE;
}
-/* ************************************************************************* */
-
-static gpointer
-caldav_synch_slave_loop (gpointer data)
-{
- ECalBackendCalDAV *cbdav;
- time_t now;
- icaltimezone *utc = icaltimezone_get_utc_timezone ();
- gboolean know_unreachable;
-
- cbdav = E_CAL_BACKEND_CALDAV (data);
-
- g_mutex_lock (&cbdav->priv->busy_lock);
-
- know_unreachable = !cbdav->priv->opened;
-
- while (cbdav->priv->slave_cmd != SLAVE_SHOULD_DIE) {
- gboolean can_check_ctag = TRUE;
-
- if (cbdav->priv->slave_cmd == SLAVE_SHOULD_SLEEP) {
- /* just sleep until we get woken up again */
- g_cond_wait (&cbdav->priv->cond, &cbdav->priv->busy_lock);
-
- /* This means to honor SLAVE_SHOULD_SLEEP only if the backend is opened */
- if (cbdav->priv->slave_cmd == SLAVE_SHOULD_DIE ||
- cbdav->priv->opened) {
- /* check if we should die, work or sleep again */
- continue;
- }
- }
+typedef struct _CalDAVChangesData {
+ gboolean is_repeat;
+ GSList **out_modified_objects;
+ GSList **out_removed_objects;
+ GHashTable *known_items; /* gchar *href ~> ECalMetaBackendInfo * */
+} CalDAVChangesData;
- /* Ok here we go, do some real work
- * Synch it baby one more time ...
- */
- cbdav->priv->slave_busy = TRUE;
- if (cbdav->priv->slave_cmd == SLAVE_SHOULD_WORK_NO_CTAG_CHECK) {
- cbdav->priv->slave_cmd = SLAVE_SHOULD_WORK;
- can_check_ctag = FALSE;
- }
-
- if (!cbdav->priv->opened) {
- gchar *certificate_pem = NULL;
- GTlsCertificateFlags certificate_errors = 0;
- GError *local_error = NULL;
+static gboolean
+ecb_caldav_search_changes_cb (ECalCache *cal_cache,
+ const gchar *uid,
+ const gchar *rid,
+ const gchar *revision,
+ const gchar *object,
+ const gchar *extra,
+ EOfflineState offline_state,
+ gpointer user_data)
+{
+ CalDAVChangesData *ccd = user_data;
+
+ g_return_val_if_fail (ccd != NULL, FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
- if (open_calendar_wrapper (cbdav, NULL, &local_error, TRUE, &know_unreachable,
&certificate_pem, &certificate_errors)) {
- cbdav->priv->opened = TRUE;
- update_slave_cmd (cbdav->priv, SLAVE_SHOULD_WORK);
- g_cond_signal (&cbdav->priv->cond);
+ /* Can be NULL for added components in offline mode */
+ if (extra && *extra && (!rid || !*rid)) {
+ ECalMetaBackendInfo *nfo;
- check_server_tweaks (cbdav);
- know_unreachable = FALSE;
+ nfo = g_hash_table_lookup (ccd->known_items, extra);
+ if (nfo) {
+ if (g_strcmp0 (revision, nfo->revision) == 0) {
+ g_hash_table_remove (ccd->known_items, extra);
} else {
- ESourceCredentialsReason reason = E_SOURCE_CREDENTIALS_REASON_REQUIRED;
- GError *local_error2 = NULL;
-
- if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
- reason = E_SOURCE_CREDENTIALS_REASON_SSL_FAILED;
- }
-
- if (!e_backend_credentials_required_sync (E_BACKEND (cbdav), reason,
certificate_pem, certificate_errors,
- local_error, NULL, &local_error2)) {
- g_warning ("%s: Failed to call credentials required: %s", G_STRFUNC,
local_error2 ? local_error2->message : "Unknown error");
+ if (!nfo->uid || !*(nfo->uid)) {
+ g_free (nfo->uid);
+ nfo->uid = g_strdup (uid);
}
- g_clear_error (&local_error2);
- }
-
- g_clear_error (&local_error);
- g_free (certificate_pem);
- }
-
- if (cbdav->priv->opened) {
- time (&now);
- /* check for events in the month before/after today first,
- * to show user actual data as soon as possible */
- caldav_synchronize_cache (cbdav, time_add_week_with_zone (now, -5, utc),
time_add_week_with_zone (now, +5, utc), can_check_ctag, NULL);
-
- if (cbdav->priv->slave_cmd != SLAVE_SHOULD_SLEEP) {
- /* and then check for changes in a whole calendar */
- caldav_synchronize_cache (cbdav, 0, 0, can_check_ctag, NULL);
- }
-
- if (caldav_debug_show (DEBUG_SERVER_ITEMS)) {
- GSList *c_objs;
-
- c_objs = e_cal_backend_store_get_components (cbdav->priv->store);
+ *(ccd->out_modified_objects) = g_slist_prepend (*(ccd->out_modified_objects),
+ e_cal_meta_backend_info_copy (nfo));
- printf ("CalDAV - finished syncing with %d items in a cache\n",
g_slist_length (c_objs)); fflush (stdout);
-
- g_slist_foreach (c_objs, (GFunc) g_object_unref, NULL);
- g_slist_free (c_objs);
+ g_hash_table_remove (ccd->known_items, extra);
}
+ } else if (ccd->is_repeat) {
+ *(ccd->out_removed_objects) = g_slist_prepend (*(ccd->out_removed_objects),
+ e_cal_meta_backend_info_new (uid, revision, object, extra));
}
-
- cbdav->priv->slave_busy = FALSE;
-
- /* puhh that was hard, get some rest :) */
- g_cond_wait (&cbdav->priv->cond, &cbdav->priv->busy_lock);
}
- cbdav->priv->synch_slave = NULL;
-
- /* signal we are done */
- g_cond_signal (&cbdav->priv->slave_gone_cond);
-
- /* we got killed ... */
- g_mutex_unlock (&cbdav->priv->busy_lock);
- return NULL;
-}
-
-static gchar *
-maybe_append_email_domain (const gchar *username,
- const gchar *may_append)
-{
- if (!username || !*username)
- return NULL;
-
- if (strchr (username, '@'))
- return g_strdup (username);
-
- return g_strconcat (username, may_append, NULL);
+ return TRUE;
}
-static gchar *
-get_usermail (ECalBackend *backend)
+static gboolean
+ecb_caldav_get_changes_sync (ECalMetaBackend *meta_backend,
+ const gchar *last_sync_tag,
+ gboolean is_repeat,
+ gchar **out_new_sync_tag,
+ gboolean *out_repeat,
+ GSList **out_created_objects,
+ GSList **out_modified_objects,
+ GSList **out_removed_objects,
+ GCancellable *cancellable,
+ GError **error)
{
ECalBackendCalDAV *cbdav;
- ESource *source;
- ESourceAuthentication *auth_extension;
- ESourceWebdav *webdav_extension;
- const gchar *extension_name;
- gchar *usermail;
- gchar *username;
- gchar *res = NULL;
-
- g_return_val_if_fail (backend != NULL, NULL);
+ EXmlDocument *xml;
+ GHashTable *known_items; /* gchar *href ~> ECalMetaBackendInfo * */
+ GHashTableIter iter;
+ gpointer key = NULL, value = NULL;
+ gboolean success;
- source = e_backend_get_source (E_BACKEND (backend));
+ g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (meta_backend), FALSE);
+ g_return_val_if_fail (out_repeat, FALSE);
+ g_return_val_if_fail (out_new_sync_tag, FALSE);
+ g_return_val_if_fail (out_created_objects, FALSE);
+ g_return_val_if_fail (out_modified_objects, FALSE);
+ g_return_val_if_fail (out_removed_objects, FALSE);
+
+ *out_new_sync_tag = NULL;
+ *out_created_objects = NULL;
+ *out_modified_objects = NULL;
+ *out_removed_objects = NULL;
+
+ cbdav = E_CAL_BACKEND_CALDAV (meta_backend);
+
+ if (cbdav->priv->ctag_supported) {
+ gchar *new_sync_tag = NULL;
+
+ success = e_webdav_session_getctag_sync (cbdav->priv->webdav, NULL, &new_sync_tag,
cancellable, NULL);
+ if (!success) {
+ cbdav->priv->ctag_supported = g_cancellable_set_error_if_cancelled (cancellable,
error);
+ if (cbdav->priv->ctag_supported || !cbdav->priv->webdav)
+ return FALSE;
+ } else if (!is_repeat && new_sync_tag && last_sync_tag && g_strcmp0 (last_sync_tag,
new_sync_tag) == 0) {
+ *out_new_sync_tag = new_sync_tag;
+ return TRUE;
+ }
+
+ /* Do not advertise the new ctag in the first go, otherwise a failure
+ in the second go might hide some events. */
+ if (is_repeat)
+ *out_new_sync_tag = new_sync_tag;
+ else
+ g_free (new_sync_tag);
+ }
- extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
- webdav_extension = e_source_get_extension (source, extension_name);
+ xml = e_xml_document_new (E_WEBDAV_NS_CALDAV, "calendar-query");
+ g_return_val_if_fail (xml != NULL, FALSE);
- /* This will never return an empty string. */
- usermail = e_source_webdav_dup_email_address (webdav_extension);
+ e_xml_document_add_namespaces (xml, "D", E_WEBDAV_NS_DAV, NULL);
- if (usermail != NULL)
- return usermail;
+ e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "prop");
+ e_xml_document_add_empty_element (xml, E_WEBDAV_NS_DAV, "getetag");
+ e_xml_document_end_element (xml); /* prop */
- cbdav = E_CAL_BACKEND_CALDAV (backend);
+ e_xml_document_start_element (xml, NULL, "filter");
+ e_xml_document_start_element (xml, NULL, "comp-filter");
+ e_xml_document_add_attribute (xml, NULL, "name", "VCALENDAR");
+ e_xml_document_start_element (xml, NULL, "comp-filter");
- extension_name = E_SOURCE_EXTENSION_AUTHENTICATION;
- auth_extension = e_source_get_extension (source, extension_name);
- username = e_source_authentication_dup_user (auth_extension);
+ switch (e_cal_backend_get_kind (E_CAL_BACKEND (cbdav))) {
+ default:
+ case ICAL_VEVENT_COMPONENT:
+ e_xml_document_add_attribute (xml, NULL, "name", "VEVENT");
+ break;
+ case ICAL_VJOURNAL_COMPONENT:
+ e_xml_document_add_attribute (xml, NULL, "name", "VJOURNAL");
+ break;
+ case ICAL_VTODO_COMPONENT:
+ e_xml_document_add_attribute (xml, NULL, "name", "VTODO");
+ break;
+ }
- if (cbdav->priv && cbdav->priv->is_google)
- res = maybe_append_email_domain (username, "@gmail.com");
+ if (!is_repeat) {
+ icaltimezone *utc = icaltimezone_get_utc_timezone ();
+ time_t now;
+ gchar *tmp;
- g_free (username);
+ time (&now);
- return res;
-}
+ *out_repeat = TRUE;
-/* ************************************************************************* */
-/* ********** ECalBackendSync virtual function implementation ************* */
+ /* Check for events in the month before/after today first,
+ to show user actual data as soon as possible. */
+ e_xml_document_start_element (xml, NULL, "time-range");
-static gchar *
-caldav_get_backend_property (ECalBackend *backend,
- const gchar *prop_name)
-{
- g_return_val_if_fail (prop_name != NULL, FALSE);
+ tmp = isodate_from_time_t (time_add_week_with_zone (now, -5, utc));
+ e_xml_document_add_attribute (xml, NULL, "start", tmp);
+ g_free (tmp);
- if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
- ESourceWebdav *extension;
- ESource *source;
- GString *caps;
- gchar *usermail;
- const gchar *extension_name;
+ tmp = isodate_from_time_t (time_add_week_with_zone (now, +5, utc));
+ e_xml_document_add_attribute (xml, NULL, "end", tmp);
+ g_free (tmp);
- caps = g_string_new (
- CAL_STATIC_CAPABILITY_NO_THISANDPRIOR ","
- CAL_STATIC_CAPABILITY_REFRESH_SUPPORTED);
+ e_xml_document_end_element (xml); /* time-range */
+ }
- usermail = get_usermail (E_CAL_BACKEND (backend));
- if (!usermail || !*usermail)
- g_string_append (caps, "," CAL_STATIC_CAPABILITY_NO_EMAIL_ALARMS);
- g_free (usermail);
+ e_xml_document_end_element (xml); /* comp-filter / VEVENT|VJOURNAL|VTODO */
+ e_xml_document_end_element (xml); /* comp-filter / VCALENDAR*/
+ e_xml_document_end_element (xml); /* filter */
- source = e_backend_get_source (E_BACKEND (backend));
+ known_items = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, e_cal_meta_backend_info_free);
- extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
- extension = e_source_get_extension (source, extension_name);
+ success = e_webdav_session_report_sync (cbdav->priv->webdav, NULL, E_WEBDAV_DEPTH_THIS_AND_CHILDREN,
xml,
+ ecb_caldav_get_calendar_items_cb, known_items, NULL, NULL, cancellable, error);
- if (e_source_webdav_get_calendar_auto_schedule (extension)) {
- g_string_append (
- caps,
- "," CAL_STATIC_CAPABILITY_CREATE_MESSAGES
- "," CAL_STATIC_CAPABILITY_SAVE_SCHEDULES);
- }
+ g_object_unref (xml);
- return g_string_free (caps, FALSE);
+ if (success) {
+ ECalCache *cal_cache;
+ CalDAVChangesData ccd;
- } else if (g_str_equal (prop_name, CAL_BACKEND_PROPERTY_CAL_EMAIL_ADDRESS) ||
- g_str_equal (prop_name, CAL_BACKEND_PROPERTY_ALARM_EMAIL_ADDRESS)) {
- return get_usermail (E_CAL_BACKEND (backend));
-
- } else if (g_str_equal (prop_name, CAL_BACKEND_PROPERTY_DEFAULT_OBJECT)) {
- ECalComponent *comp;
- gchar *prop_value;
-
- comp = e_cal_component_new ();
-
- switch (e_cal_backend_get_kind (E_CAL_BACKEND (backend))) {
- case ICAL_VEVENT_COMPONENT:
- e_cal_component_set_new_vtype (comp, E_CAL_COMPONENT_EVENT);
- break;
- case ICAL_VTODO_COMPONENT:
- e_cal_component_set_new_vtype (comp, E_CAL_COMPONENT_TODO);
- break;
- case ICAL_VJOURNAL_COMPONENT:
- e_cal_component_set_new_vtype (comp, E_CAL_COMPONENT_JOURNAL);
- break;
- default:
- g_object_unref (comp);
- return NULL;
- }
+ ccd.is_repeat = is_repeat;
+ ccd.out_modified_objects = out_modified_objects;
+ ccd.out_removed_objects = out_removed_objects;
+ ccd.known_items = known_items;
- prop_value = e_cal_component_get_as_string (comp);
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
- g_object_unref (comp);
+ success = e_cal_cache_search_with_callback (cal_cache, NULL, ecb_caldav_search_changes_cb,
&ccd, cancellable, error);
- return prop_value;
+ g_clear_object (&cal_cache);
}
- /* Chain up to parent's get_backend_property() method. */
- return E_CAL_BACKEND_CLASS (e_cal_backend_caldav_parent_class)->
- get_backend_property (backend, prop_name);
-}
-
-static void
-caldav_shutdown (ECalBackend *backend)
-{
- ECalBackendCalDAVPrivate *priv;
- ESource *source;
-
- priv = E_CAL_BACKEND_CALDAV_GET_PRIVATE (backend);
-
- /* Chain up to parent's shutdown() method. */
- E_CAL_BACKEND_CLASS (e_cal_backend_caldav_parent_class)->shutdown (backend);
+ if (!success) {
+ g_hash_table_destroy (known_items);
+ return FALSE;
+ }
- /* tell the slave to stop before acquiring a lock,
- * as it can work at the moment, and lock can be locked */
- update_slave_cmd (priv, SLAVE_SHOULD_DIE);
+ g_hash_table_iter_init (&iter, known_items);
+ while (g_hash_table_iter_next (&iter, &key, &value)) {
+ *out_created_objects = g_slist_prepend (*out_created_objects, e_cal_meta_backend_info_copy
(value));
+ }
- g_mutex_lock (&priv->busy_lock);
+ g_hash_table_destroy (known_items);
- /* XXX Not sure if this really needs to be part of
- * shutdown or if we can just do it in dispose(). */
- source = e_backend_get_source (E_BACKEND (backend));
- if (source) {
- g_signal_handlers_disconnect_by_func (G_OBJECT (source), caldav_source_changed_cb, backend);
+ if (*out_created_objects || *out_modified_objects) {
+ GSList *link, *set2 = *out_modified_objects;
- if (priv->refresh_id) {
- e_source_refresh_remove_timeout (source, priv->refresh_id);
- priv->refresh_id = 0;
+ if (*out_created_objects) {
+ link = *out_created_objects;
+ } else {
+ link = set2;
+ set2 = NULL;
}
- }
-
- /* stop the slave */
- while (priv->synch_slave) {
- g_cond_signal (&priv->cond);
- /* wait until the slave died */
- g_cond_wait (&priv->slave_gone_cond, &priv->busy_lock);
+ do {
+ success = ecb_caldav_multiget_from_sets_sync (cbdav, &link, &set2, cancellable,
error);
+ } while (success && link);
}
- g_mutex_unlock (&priv->busy_lock);
+ return success;
}
static gboolean
-initialize_backend (ECalBackendCalDAV *cbdav,
- GError **perror)
+ecb_caldav_extract_existing_cb (EWebDAVSession *webdav,
+ xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prop_prefix,
+ const SoupURI *request_uri,
+ const gchar *href,
+ guint status_code,
+ gpointer user_data)
{
- ESourceAuthentication *auth_extension;
- ESourceOffline *offline_extension;
- ESourceWebdav *webdav_extension;
- ECalBackend *backend;
- SoupURI *soup_uri;
- ESource *source;
- gsize len;
- const gchar *cache_dir;
- const gchar *extension_name;
-
- backend = E_CAL_BACKEND (cbdav);
- cache_dir = e_cal_backend_get_cache_dir (backend);
- source = e_backend_get_source (E_BACKEND (backend));
-
- extension_name = E_SOURCE_EXTENSION_AUTHENTICATION;
- auth_extension = e_source_get_extension (source, extension_name);
-
- extension_name = E_SOURCE_EXTENSION_OFFLINE;
- offline_extension = e_source_get_extension (source, extension_name);
-
- extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
- webdav_extension = e_source_get_extension (source, extension_name);
+ GSList **out_existing_objects = user_data;
- if (!g_signal_handler_find (G_OBJECT (source), G_SIGNAL_MATCH_FUNC | G_SIGNAL_MATCH_DATA, 0, 0, NULL,
caldav_source_changed_cb, cbdav))
- g_signal_connect (G_OBJECT (source), "changed", G_CALLBACK (caldav_source_changed_cb), cbdav);
+ g_return_val_if_fail (out_existing_objects != NULL, FALSE);
- cbdav->priv->loaded = TRUE;
- cbdav->priv->do_offline = e_source_offline_get_stay_synchronized (offline_extension);
- cbdav->priv->auth_required = e_source_authentication_required (auth_extension);
+ if (!xpath_prop_prefix) {
+ e_xml_xpath_context_register_namespaces (xpath_ctx, "C", E_WEBDAV_NS_CALDAV, NULL);
+ } else if (status_code == SOUP_STATUS_OK) {
+ gchar *etag;
+ gchar *calendar_data;
- soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
-
- /* properly encode uri */
- if (soup_uri != NULL && soup_uri->path != NULL) {
- gchar *tmp, *path;
-
- if (strchr (soup_uri->path, '%')) {
- /* If path contains anything already encoded, then
- * decode it first, thus it'll be managed properly.
- * For example, the '#' in a path is in URI shown as
- * %23 and not doing this decode makes it being like
- * %2523, which is not what is wanted here. */
- tmp = soup_uri_decode (soup_uri->path);
- soup_uri_set_path (soup_uri, tmp);
- g_free (tmp);
- }
-
- tmp = soup_uri_encode (soup_uri->path, NULL);
- path = soup_uri_normalize (tmp, "/");
+ g_return_val_if_fail (href != NULL, FALSE);
- soup_uri_set_path (soup_uri, path);
+ etag = e_xml_xpath_eval_as_string (xpath_ctx, "%s/D:getetag", xpath_prop_prefix);
+ calendar_data = e_xml_xpath_eval_as_string (xpath_ctx, "%s/C:calendar-data",
xpath_prop_prefix);
- g_free (tmp);
- g_free (path);
- }
+ if (calendar_data) {
+ icalcomponent *vcalendar;
- g_free (cbdav->priv->uri);
- cbdav->priv->uri = soup_uri_to_string (soup_uri, FALSE);
+ vcalendar = icalcomponent_new_from_string (calendar_data);
+ if (vcalendar) {
+ const gchar *uid;
- soup_uri_free (soup_uri);
+ uid = ecb_caldav_get_vcalendar_uid (vcalendar);
- g_return_val_if_fail (cbdav->priv->uri != NULL, FALSE);
+ if (uid) {
+ etag = e_webdav_session_util_maybe_dequote (etag);
+ *out_existing_objects = g_slist_prepend (*out_existing_objects,
+ e_cal_meta_backend_info_new (uid, etag, NULL, href));
+ }
- /* remove trailing slashes... */
- if (cbdav->priv->uri != NULL) {
- len = strlen (cbdav->priv->uri);
- while (len--) {
- if (cbdav->priv->uri[len] == '/') {
- cbdav->priv->uri[len] = '\0';
- } else {
- break;
+ icalcomponent_free (vcalendar);
}
}
- }
-
- /* ...and append exactly one slash */
- if (cbdav->priv->uri && *cbdav->priv->uri) {
- gchar *tmp = cbdav->priv->uri;
- cbdav->priv->uri = g_strconcat (cbdav->priv->uri, "/", NULL);
-
- g_free (tmp);
- }
-
- if (cbdav->priv->store == NULL) {
- /* remove the old cache while migrating to ECalBackendStore */
- e_cal_backend_cache_remove (cache_dir, "cache.xml");
- cbdav->priv->store = e_cal_backend_store_new (
- cache_dir, E_TIMEZONE_CACHE (cbdav));
- e_cal_backend_store_load (cbdav->priv->store);
- }
-
- /* Set the local attachment store */
- if (g_mkdir_with_parents (cache_dir, 0700) < 0) {
- g_propagate_error (perror, e_data_cal_create_error_fmt (OtherError, _("Cannot create local
cache folder “%s”"), cache_dir));
- return FALSE;
- }
-
- if (!cbdav->priv->synch_slave) {
- GThread *slave;
-
- update_slave_cmd (cbdav->priv, SLAVE_SHOULD_SLEEP);
- slave = g_thread_new (NULL, caldav_synch_slave_loop, cbdav);
-
- cbdav->priv->synch_slave = slave;
- g_thread_unref (slave);
- }
-
- if (cbdav->priv->refresh_id == 0) {
- cbdav->priv->refresh_id = e_source_refresh_add_timeout (
- source, NULL, time_to_refresh_caldav_calendar_cb, cbdav, NULL);
+ g_free (calendar_data);
+ g_free (etag);
}
return TRUE;
}
static gboolean
-caldav_was_ever_connected (ECalBackendCalDAV *cbdav)
+ecb_caldav_list_existing_sync (ECalMetaBackend *meta_backend,
+ gchar **out_new_sync_tag,
+ GSList **out_existing_objects,
+ GCancellable *cancellable,
+ GError **error)
{
- gboolean has_components;
- GSList *uids;
-
- g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav), FALSE);
-
- if (!cbdav->priv->store)
- return FALSE;
-
- uids = e_cal_backend_store_get_component_ids (cbdav->priv->store);
-
- /* Assume the calendar was connected if it has any events stored;
- obviously, empty calendars will fail this check. */
- has_components = uids != NULL;
-
- g_slist_free_full (uids, (GDestroyNotify) e_cal_component_free_id);
-
- return has_components;
-}
-
-static gboolean
-open_calendar_wrapper (ECalBackendCalDAV *cbdav,
- GCancellable *cancellable,
- GError **error,
- gboolean first_attempt,
- gboolean *know_unreachable,
- gchar **out_certificate_pem,
- GTlsCertificateFlags *out_certificate_errors)
-{
- gboolean server_unreachable = FALSE;
- gboolean awaiting_credentials = FALSE;
+ ECalBackendCalDAV *cbdav;
+ icalcomponent_kind kind;
+ EXmlDocument *xml;
gboolean success;
- GError *local_error = NULL;
- g_return_val_if_fail (cbdav != NULL, FALSE);
+ g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (meta_backend), FALSE);
+ g_return_val_if_fail (out_existing_objects != NULL, FALSE);
- if (!cbdav->priv->loaded && !initialize_backend (cbdav, error))
- return FALSE;
+ *out_existing_objects = NULL;
- if (!caldav_maybe_prepare_bearer_auth (cbdav, cancellable, error))
- return FALSE;
+ cbdav = E_CAL_BACKEND_CALDAV (meta_backend);
+ kind = e_cal_backend_get_kind (E_CAL_BACKEND (cbdav));
- success = caldav_server_open_calendar (cbdav, &server_unreachable, out_certificate_pem,
out_certificate_errors, cancellable, &local_error);
+ xml = e_xml_document_new (E_WEBDAV_NS_CALDAV, "calendar-query");
+ g_return_val_if_fail (xml != NULL, FALSE);
- if (first_attempt && g_error_matches (local_error, E_DATA_CAL_ERROR, AuthenticationFailed)) {
- g_clear_error (&local_error);
- awaiting_credentials = TRUE;
- success = caldav_credentials_required_sync (cbdav, FALSE, first_attempt, cancellable,
&local_error);
- }
-
- if (success) {
- check_server_tweaks (cbdav);
-
- if (!awaiting_credentials) {
- update_slave_cmd (cbdav->priv, SLAVE_SHOULD_WORK);
- g_cond_signal (&cbdav->priv->cond);
- }
- } else if (server_unreachable) {
- cbdav->priv->opened = FALSE;
- e_cal_backend_set_writable (E_CAL_BACKEND (cbdav), FALSE);
- if (local_error) {
- if (know_unreachable && !*know_unreachable) {
- gchar *msg = g_strdup_printf (_("Server is unreachable, calendar is opened in
read-only mode.\nError message: %s"), local_error->message);
- e_cal_backend_notify_error (E_CAL_BACKEND (cbdav), msg);
- g_free (msg);
- g_clear_error (&local_error);
-
- *know_unreachable = TRUE;
- } else if (caldav_was_ever_connected (cbdav)) {
- /* This allows to open the calendar in read-only mode, which can be done
- if it was ever connected to the server. */
- g_clear_error (&local_error);
- success = TRUE;
- }
- }
- }
+ e_xml_document_add_namespaces (xml, "D", E_WEBDAV_NS_DAV, NULL);
- if (local_error != NULL)
- g_propagate_error (error, local_error);
+ e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "prop");
+ e_xml_document_add_empty_element (xml, E_WEBDAV_NS_DAV, "getetag");
+ e_xml_document_start_element (xml, E_WEBDAV_NS_CALDAV, "calendar-data");
+ e_xml_document_start_element (xml, E_WEBDAV_NS_CALDAV, "comp");
+ e_xml_document_add_attribute (xml, NULL, "name", "VCALENDAR");
+ e_xml_document_start_element (xml, E_WEBDAV_NS_CALDAV, "comp");
+ if (kind == ICAL_VEVENT_COMPONENT)
+ e_xml_document_add_attribute (xml, NULL, "name", "VEVENT");
+ else if (kind == ICAL_VJOURNAL_COMPONENT)
+ e_xml_document_add_attribute (xml, NULL, "name", "VJOURNAL");
+ else if (kind == ICAL_VTODO_COMPONENT)
+ e_xml_document_add_attribute (xml, NULL, "name", "VTODO");
+ else
+ g_warn_if_reached ();
+ e_xml_document_start_element (xml, E_WEBDAV_NS_CALDAV, "prop");
+ e_xml_document_add_attribute (xml, NULL, "name", "UID");
+ e_xml_document_end_element (xml); /* prop / UID */
+ e_xml_document_end_element (xml); /* comp / VEVENT|VJOURNAL|VTODO */
+ e_xml_document_end_element (xml); /* comp / VCALENDAR */
+ e_xml_document_end_element (xml); /* calendar-data */
+ e_xml_document_end_element (xml); /* prop */
+
+ e_xml_document_start_element (xml, E_WEBDAV_NS_CALDAV, "filter");
+ e_xml_document_start_element (xml, E_WEBDAV_NS_CALDAV, "comp-filter");
+ e_xml_document_add_attribute (xml, NULL, "name", "VCALENDAR");
+ e_xml_document_start_element (xml, E_WEBDAV_NS_CALDAV, "comp-filter");
+ if (kind == ICAL_VEVENT_COMPONENT)
+ e_xml_document_add_attribute (xml, NULL, "name", "VEVENT");
+ else if (kind == ICAL_VJOURNAL_COMPONENT)
+ e_xml_document_add_attribute (xml, NULL, "name", "VJOURNAL");
+ else if (kind == ICAL_VTODO_COMPONENT)
+ e_xml_document_add_attribute (xml, NULL, "name", "VTODO");
+ e_xml_document_end_element (xml); /* comp-filter / VEVENT|VJOURNAL|VTODO */
+ e_xml_document_end_element (xml); /* comp-filter / VCALENDAR */
+ e_xml_document_end_element (xml); /* filter */
+
+ success = e_webdav_session_report_sync (cbdav->priv->webdav, NULL, E_WEBDAV_DEPTH_THIS, xml,
+ ecb_caldav_extract_existing_cb, out_existing_objects, NULL, NULL, cancellable, error);
+
+ g_object_unref (xml);
+
+ if (success)
+ *out_existing_objects = g_slist_reverse (*out_existing_objects);
return success;
}
-static void
-caldav_do_open (ECalBackendSync *backend,
- EDataCal *cal,
- GCancellable *cancellable,
- gboolean only_if_exists,
- GError **perror)
+static gchar *
+ecb_caldav_uid_to_uri (ECalBackendCalDAV *cbdav,
+ const gchar *uid,
+ const gchar *extension)
{
- ECalBackendCalDAV *cbdav;
ESourceWebdav *webdav_extension;
- ESourceAuthentication *auth_extension;
- ESource *source;
- gboolean online;
-
- cbdav = E_CAL_BACKEND_CALDAV (backend);
-
- g_mutex_lock (&cbdav->priv->busy_lock);
-
- source = e_backend_get_source (E_BACKEND (cbdav));
- webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
- auth_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
- e_source_webdav_unset_temporary_ssl_trust (webdav_extension);
-
- /* let it decide the 'getctag' extension availability again */
- cbdav->priv->ctag_supported = TRUE;
-
- if (!cbdav->priv->loaded && !initialize_backend (cbdav, perror)) {
- g_mutex_unlock (&cbdav->priv->busy_lock);
- return;
- }
-
- online = e_backend_get_online (E_BACKEND (backend));
-
- if (!cbdav->priv->do_offline && !online) {
- g_mutex_unlock (&cbdav->priv->busy_lock);
- g_propagate_error (perror, EDC_ERROR (RepositoryOffline));
- return;
- }
-
- cbdav->priv->opened = TRUE;
- cbdav->priv->is_google = FALSE;
-
- if (online) {
- gchar *certificate_pem = NULL, *auth_method;
- GTlsCertificateFlags certificate_errors = 0;
- GError *local_error = NULL;
-
- auth_method = e_source_authentication_dup_method (auth_extension);
-
- if ((g_strcmp0 (auth_method, "Google") == 0 ||
- !open_calendar_wrapper (cbdav, cancellable, &local_error, TRUE, NULL, &certificate_pem,
&certificate_errors)) &&
- !g_cancellable_is_cancelled (cancellable)) {
- ESourceCredentialsReason reason = E_SOURCE_CREDENTIALS_REASON_REQUIRED;
- GError *local_error2 = NULL;
-
- if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
- reason = E_SOURCE_CREDENTIALS_REASON_SSL_FAILED;
- }
+ SoupURI *soup_uri;
+ gchar *uri, *tmp, *filename;
- if (!e_backend_credentials_required_sync (E_BACKEND (backend), reason,
certificate_pem, certificate_errors,
- local_error, cancellable, &local_error2)) {
- g_warning ("%s: Failed to call credentials required: %s", G_STRFUNC,
local_error2 ? local_error2->message : "Unknown error");
- }
+ g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav), NULL);
+ g_return_val_if_fail (uid != NULL, NULL);
- if (!local_error2 && (
- g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED) ||
- g_error_matches (local_error, E_DATA_CAL_ERROR, AuthenticationRequired) ||
- g_error_matches (local_error, E_DATA_CAL_ERROR, AuthenticationFailed))) {
- /* These errors are treated through the authentication */
- g_clear_error (&local_error);
- } else {
- if (local_error)
- g_propagate_error (perror, local_error);
- local_error = NULL;
- }
- g_clear_error (&local_error2);
- }
+ webdav_extension = e_source_get_extension (e_backend_get_source (E_BACKEND (cbdav)),
E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+ soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
+ g_return_val_if_fail (soup_uri != NULL, NULL);
- g_clear_error (&local_error);
- g_free (certificate_pem);
- g_free (auth_method);
+ if (extension) {
+ tmp = g_strconcat (uid, extension, NULL);
+ filename = soup_uri_encode (tmp, NULL);
+ g_free (tmp);
} else {
- e_cal_backend_set_writable (E_CAL_BACKEND (cbdav), FALSE);
+ filename = soup_uri_encode (uid, NULL);
}
- g_mutex_unlock (&cbdav->priv->busy_lock);
-}
+ if (soup_uri->path) {
+ gchar *slash = strrchr (soup_uri->path, '/');
-static void
-caldav_refresh (ECalBackendSync *backend,
- EDataCal *cal,
- GCancellable *cancellable,
- GError **perror)
-{
- ECalBackendCalDAV *cbdav;
- gboolean online;
-
- cbdav = E_CAL_BACKEND_CALDAV (backend);
-
- g_mutex_lock (&cbdav->priv->busy_lock);
-
- if (!cbdav->priv->loaded
- || cbdav->priv->slave_cmd == SLAVE_SHOULD_DIE) {
- g_mutex_unlock (&cbdav->priv->busy_lock);
- return;
+ if (slash && !slash[1])
+ *slash = '\0';
}
- if (!e_backend_get_online (E_BACKEND (backend)) &&
- e_backend_is_destination_reachable (E_BACKEND (backend), cancellable, NULL)) {
- e_backend_set_online (E_BACKEND (backend), TRUE);
- }
+ soup_uri_set_user (soup_uri, NULL);
+ soup_uri_set_password (soup_uri, NULL);
- if (!check_state (cbdav, &online, NULL) || !online) {
- g_mutex_unlock (&cbdav->priv->busy_lock);
- return;
- }
-
- update_slave_cmd (cbdav->priv, SLAVE_SHOULD_WORK_NO_CTAG_CHECK);
-
- /* wake it up */
- g_cond_signal (&cbdav->priv->cond);
- g_mutex_unlock (&cbdav->priv->busy_lock);
-}
-
-static void
-remove_comp_from_cache_cb (gpointer value,
- gpointer user_data)
-{
- ECalComponent *comp = value;
- ECalBackendStore *store = user_data;
- ECalComponentId *id;
+ tmp = g_strconcat (soup_uri->path && *soup_uri->path ? soup_uri->path : "", "/", filename, NULL);
+ soup_uri_set_path (soup_uri, tmp);
+ g_free (tmp);
- g_return_if_fail (comp != NULL);
- g_return_if_fail (store != NULL);
+ uri = soup_uri_to_string (soup_uri, FALSE);
- id = e_cal_component_get_id (comp);
- g_return_if_fail (id != NULL);
+ soup_uri_free (soup_uri);
+ g_free (filename);
- e_cal_backend_store_remove_component (store, id->uid, id->rid);
- e_cal_component_free_id (id);
+ return uri;
}
static gboolean
-remove_comp_from_cache (ECalBackendCalDAV *cbdav,
- const gchar *uid,
- const gchar *rid)
-{
- gboolean res = FALSE;
-
- if (!rid || !*rid) {
- /* get with detached instances */
- GSList *objects = e_cal_backend_store_get_components_by_uid (cbdav->priv->store, uid);
-
- if (objects) {
- g_slist_foreach (objects, (GFunc) remove_comp_from_cache_cb, cbdav->priv->store);
- g_slist_foreach (objects, (GFunc) g_object_unref, NULL);
- g_slist_free (objects);
-
- res = TRUE;
- }
- } else {
- res = e_cal_backend_store_remove_component (cbdav->priv->store, uid, rid);
- }
-
- return res;
-}
-
-static void
-add_detached_recur_to_vcalendar_cb (gpointer value,
- gpointer user_data)
+ecb_caldav_load_component_sync (ECalMetaBackend *meta_backend,
+ const gchar *uid,
+ const gchar *extra,
+ icalcomponent **out_component,
+ gchar **out_extra,
+ GCancellable *cancellable,
+ GError **error)
{
- icalcomponent *recurrence = e_cal_component_get_icalcomponent (value);
- icalcomponent *vcalendar = user_data;
-
- icalcomponent_add_component (
- vcalendar,
- icalcomponent_new_clone (recurrence));
-}
-
-static gint
-sort_master_first (gconstpointer a,
- gconstpointer b)
-{
- icalcomponent *ca, *cb;
-
- ca = e_cal_component_get_icalcomponent ((ECalComponent *) a);
- cb = e_cal_component_get_icalcomponent ((ECalComponent *) b);
-
- if (!ca) {
- if (!cb)
- return 0;
- else
- return -1;
- } else if (!cb) {
- return 1;
- }
-
- return icaltime_compare (icalcomponent_get_recurrenceid (ca), icalcomponent_get_recurrenceid (cb));
-}
-
-/* Returns new icalcomponent, with all detached instances stored in a cache.
- * The cache lock should be locked when called this function.
-*/
-static icalcomponent *
-get_comp_from_cache (ECalBackendCalDAV *cbdav,
- const gchar *uid,
- const gchar *rid,
- gchar **href,
- gchar **etag)
-{
- icalcomponent *icalcomp = NULL;
-
- if (rid == NULL || !*rid) {
- /* get with detached instances */
- GSList *objects = e_cal_backend_store_get_components_by_uid (cbdav->priv->store, uid);
-
- if (!objects) {
- return NULL;
- }
-
- if (g_slist_length (objects) == 1) {
- ECalComponent *comp = objects->data;
+ ECalBackendCalDAV *cbdav;
+ gchar *uri = NULL, *href = NULL, *etag = NULL, *bytes = NULL;
+ gsize length = -1;
+ gboolean success = FALSE;
+ GError *local_error = NULL;
- /* will be unreffed a bit later */
- if (comp)
- icalcomp = icalcomponent_new_clone (e_cal_component_get_icalcomponent (comp));
- } else {
- /* if we have detached recurrences, return a VCALENDAR */
- icalcomp = e_cal_util_new_top_level ();
+ g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (meta_backend), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (out_component != NULL, FALSE);
- objects = g_slist_sort (objects, sort_master_first);
+ cbdav = E_CAL_BACKEND_CALDAV (meta_backend);
- /* add all detached recurrences and the master object */
- g_slist_foreach (objects, add_detached_recur_to_vcalendar_cb, icalcomp);
- }
+ if (extra && *extra) {
+ uri = g_strdup (extra);
- /* every component has set same href and etag, thus it doesn't matter where it will be read */
- if (href)
- *href = ecalcomp_get_href (objects->data);
- if (etag)
- *etag = ecalcomp_get_etag (objects->data);
+ success = e_webdav_session_get_data_sync (cbdav->priv->webdav, uri, &href, &etag, &bytes,
&length, cancellable, &local_error);
- g_slist_foreach (objects, (GFunc) g_object_unref, NULL);
- g_slist_free (objects);
- } else {
- /* get the exact object */
- ECalComponent *comp = e_cal_backend_store_get_component (cbdav->priv->store, uid, rid);
-
- if (comp) {
- icalcomp = icalcomponent_new_clone (e_cal_component_get_icalcomponent (comp));
- if (href)
- *href = ecalcomp_get_href (comp);
- if (etag)
- *etag = ecalcomp_get_etag (comp);
- g_object_unref (comp);
+ if (!success) {
+ g_free (uri);
+ uri = NULL;
}
}
- return icalcomp;
-}
-
-static void
-put_server_comp_to_cache (ECalBackendCalDAV *cbdav,
- icalcomponent *icomp,
- const gchar *href,
- const gchar *etag,
- GTree *c_uid2complist)
-{
- icalcomponent_kind kind;
- ECalBackend *cal_backend;
-
- g_return_if_fail (cbdav != NULL);
- g_return_if_fail (icomp != NULL);
-
- cal_backend = E_CAL_BACKEND (cbdav);
- kind = icalcomponent_isa (icomp);
- extract_timezones (cbdav, icomp);
-
- if (kind == ICAL_VCALENDAR_COMPONENT) {
- icalcomponent *subcomp;
-
- kind = e_cal_backend_get_kind (cal_backend);
-
- for (subcomp = icalcomponent_get_first_component (icomp, kind);
- subcomp;
- subcomp = icalcomponent_get_next_component (icomp, kind)) {
- ECalComponent *new_comp, *old_comp;
+ if (!success) {
+ uri = ecb_caldav_uid_to_uri (cbdav, uid, ".ics");
+ g_return_val_if_fail (uri != NULL, FALSE);
- convert_to_url_attachment (cbdav, subcomp);
- new_comp = e_cal_component_new ();
- if (e_cal_component_set_icalcomponent (new_comp, icalcomponent_new_clone (subcomp))) {
- const gchar *uid = NULL;
- struct cache_comp_list *ccl;
-
- e_cal_component_get_uid (new_comp, &uid);
- if (!uid) {
- g_warning ("%s: no UID on component!", G_STRFUNC);
- g_object_unref (new_comp);
- continue;
- }
-
- if (href)
- ecalcomp_set_href (new_comp, href);
- if (etag)
- ecalcomp_set_etag (new_comp, etag);
-
- old_comp = NULL;
- if (c_uid2complist) {
- ccl = g_tree_lookup (c_uid2complist, uid);
- if (ccl) {
- gchar *nc_rid = e_cal_component_get_recurid_as_string
(new_comp);
- GSList *p;
-
- for (p = ccl->slist; p && !old_comp; p = p->next) {
- gchar *oc_rid;
-
- old_comp = p->data;
-
- oc_rid = e_cal_component_get_recurid_as_string
(old_comp);
- if (g_strcmp0 (nc_rid, oc_rid) != 0) {
- old_comp = NULL;
- }
-
- g_free (oc_rid);
- }
-
- g_free (nc_rid);
- }
- }
+ g_clear_error (&local_error);
- put_component_to_store (cbdav, new_comp);
+ success = e_webdav_session_get_data_sync (cbdav->priv->webdav, uri, &href, &etag, &bytes,
&length, cancellable, &local_error);
+ if (!success && !g_cancellable_is_cancelled (cancellable) &&
+ g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_NOT_FOUND)) {
+ g_free (uri);
+ uri = ecb_caldav_uid_to_uri (cbdav, uid, NULL);
- if (old_comp == NULL) {
- e_cal_backend_notify_component_created (cal_backend, new_comp);
- } else {
- e_cal_backend_notify_component_modified (cal_backend, old_comp,
new_comp);
+ if (uri) {
+ g_clear_error (&local_error);
- if (ccl)
- ccl->slist = g_slist_remove (ccl->slist, old_comp);
- g_object_unref (old_comp);
- }
+ success = e_webdav_session_get_data_sync (cbdav->priv->webdav, uri, &href,
&etag, &bytes, &length, cancellable, &local_error);
}
-
- g_object_unref (new_comp);
}
}
-}
-static gboolean
-put_comp_to_cache (ECalBackendCalDAV *cbdav,
- icalcomponent *icalcomp,
- const gchar *href,
- const gchar *etag)
-{
- icalcomponent_kind my_kind;
- ECalComponent *comp;
- gboolean res = FALSE;
+ if (success) {
+ *out_component = NULL;
- g_return_val_if_fail (cbdav != NULL, FALSE);
- g_return_val_if_fail (icalcomp != NULL, FALSE);
+ if (href && etag && bytes && length != ((gsize) -1)) {
+ icalcomponent *icalcomp, *subcomp;
- my_kind = e_cal_backend_get_kind (E_CAL_BACKEND (cbdav));
- comp = e_cal_component_new ();
+ icalcomp = icalcomponent_new_from_string (bytes);
+ if (icalcomp) {
+ e_cal_util_set_x_property (icalcomp, E_CALDAV_X_ETAG, etag);
- if (icalcomponent_isa (icalcomp) == ICAL_VCALENDAR_COMPONENT) {
- icalcomponent *subcomp;
+ for (subcomp = icalcomponent_get_first_component (icalcomp,
ICAL_ANY_COMPONENT);
+ subcomp;
+ subcomp = icalcomponent_get_next_component (icalcomp,
ICAL_ANY_COMPONENT)) {
+ icalcomponent_kind kind = icalcomponent_isa (subcomp);
- /* remove all old components from the cache first */
- for (subcomp = icalcomponent_get_first_component (icalcomp, my_kind);
- subcomp;
- subcomp = icalcomponent_get_next_component (icalcomp, my_kind)) {
- remove_comp_from_cache (cbdav, icalcomponent_get_uid (subcomp), NULL);
- }
+ if (kind == ICAL_VEVENT_COMPONENT ||
+ kind == ICAL_VJOURNAL_COMPONENT ||
+ kind == ICAL_VTODO_COMPONENT) {
+ e_cal_util_set_x_property (subcomp, E_CALDAV_X_ETAG, etag);
+ }
+ }
- /* then put new. It's because some detached instances could be removed on the server. */
- for (subcomp = icalcomponent_get_first_component (icalcomp, my_kind);
- subcomp;
- subcomp = icalcomponent_get_next_component (icalcomp, my_kind)) {
- /* because reusing the same comp doesn't clear recur_id member properly */
- g_object_unref (comp);
- comp = e_cal_component_new ();
-
- if (e_cal_component_set_icalcomponent (comp, icalcomponent_new_clone (subcomp))) {
- if (href)
- ecalcomp_set_href (comp, href);
- if (etag)
- ecalcomp_set_etag (comp, etag);
-
- if (put_component_to_store (cbdav, comp))
- res = TRUE;
+ *out_component = icalcomp;
}
}
- } else if (icalcomponent_isa (icalcomp) == my_kind) {
- remove_comp_from_cache (cbdav, icalcomponent_get_uid (icalcomp), NULL);
-
- if (e_cal_component_set_icalcomponent (comp, icalcomponent_new_clone (icalcomp))) {
- if (href)
- ecalcomp_set_href (comp, href);
- if (etag)
- ecalcomp_set_etag (comp, etag);
- res = put_component_to_store (cbdav, comp);
+ if (!*out_component) {
+ success = FALSE;
+ g_propagate_error (&local_error, EDC_ERROR (InvalidObject));
}
}
- g_object_unref (comp);
-
- return res;
-}
-
-static void
-remove_property (gpointer prop,
- gpointer icomp)
-{
- icalcomponent_remove_property (icomp, prop);
- icalproperty_free (prop);
-}
-
-static void
-strip_unneeded_x_props (icalcomponent *icomp)
-{
- icalproperty *prop;
- GSList *to_remove = NULL;
-
- g_return_if_fail (icomp != NULL);
- g_return_if_fail (icalcomponent_isa (icomp) != ICAL_VCALENDAR_COMPONENT);
-
- for (prop = icalcomponent_get_first_property (icomp, ICAL_X_PROPERTY);
- prop;
- prop = icalcomponent_get_next_property (icomp, ICAL_X_PROPERTY)) {
- if (g_str_has_prefix (icalproperty_get_x_name (prop), X_E_CALDAV)) {
- to_remove = g_slist_prepend (to_remove, prop);
- }
- }
+ g_free (uri);
+ g_free (href);
+ g_free (etag);
+ g_free (bytes);
- for (prop = icalcomponent_get_first_property (icomp, ICAL_XLICERROR_PROPERTY);
- prop;
- prop = icalcomponent_get_next_property (icomp, ICAL_XLICERROR_PROPERTY)) {
- to_remove = g_slist_prepend (to_remove, prop);
- }
+ if (local_error)
+ g_propagate_error (error, local_error);
- g_slist_foreach (to_remove, remove_property, icomp);
- g_slist_free (to_remove);
+ return success;
}
static gboolean
-is_stored_on_server (ECalBackendCalDAV *cbdav,
- const gchar *uri)
+ecb_caldav_save_component_sync (ECalMetaBackend *meta_backend,
+ gboolean overwrite_existing,
+ EConflictResolution conflict_resolution,
+ const GSList *instances,
+ const gchar *extra,
+ gchar **out_new_uid,
+ gchar **out_new_extra,
+ GCancellable *cancellable,
+ GError **error)
{
- SoupURI *my_uri, *test_uri;
- gboolean res;
-
- g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav), FALSE);
- g_return_val_if_fail (cbdav->priv->uri != NULL, FALSE);
- g_return_val_if_fail (uri != NULL, FALSE);
-
- my_uri = soup_uri_new (cbdav->priv->uri);
- g_return_val_if_fail (my_uri != NULL, FALSE);
-
- test_uri = soup_uri_new (uri);
- if (!test_uri) {
- soup_uri_free (my_uri);
- return FALSE;
- }
-
- res = my_uri->host && test_uri->host && g_ascii_strcasecmp (my_uri->host, test_uri->host) == 0;
-
- soup_uri_free (my_uri);
- soup_uri_free (test_uri);
-
- return res;
-}
+ ECalBackendCalDAV *cbdav;
+ icalcomponent *vcalendar, *subcomp;
+ gchar *href = NULL, *etag = NULL, *uid = NULL;
+ gchar *ical_string = NULL;
+ gboolean success;
-static void
-convert_to_inline_attachment (ECalBackendCalDAV *cbdav,
- icalcomponent *icalcomp)
-{
- icalcomponent *cclone;
- icalproperty *p;
- GSList *to_remove = NULL;
+ g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (meta_backend), FALSE);
+ g_return_val_if_fail (instances != NULL, FALSE);
+ g_return_val_if_fail (out_new_uid, FALSE);
+ g_return_val_if_fail (out_new_extra, FALSE);
- g_return_if_fail (icalcomp != NULL);
+ cbdav = E_CAL_BACKEND_CALDAV (meta_backend);
- cclone = icalcomponent_new_clone (icalcomp);
+ vcalendar = e_cal_meta_backend_merge_instances (meta_backend, instances, cbdav->priv->is_icloud);
+ g_return_val_if_fail (vcalendar != NULL, FALSE);
- /* Remove local url attachments first */
- for (p = icalcomponent_get_first_property (icalcomp, ICAL_ATTACH_PROPERTY);
- p;
- p = icalcomponent_get_next_property (icalcomp, ICAL_ATTACH_PROPERTY)) {
- icalattach *attach;
+ for (subcomp = icalcomponent_get_first_component (vcalendar, ICAL_ANY_COMPONENT);
+ subcomp;
+ subcomp = icalcomponent_get_next_component (vcalendar, ICAL_ANY_COMPONENT)) {
+ icalcomponent_kind kind = icalcomponent_isa (subcomp);
- attach = icalproperty_get_attach ((const icalproperty *) p);
- if (icalattach_get_is_url (attach)) {
- const gchar *url;
+ if (kind == ICAL_VEVENT_COMPONENT ||
+ kind == ICAL_VJOURNAL_COMPONENT ||
+ kind == ICAL_VTODO_COMPONENT) {
+ if (!etag)
+ etag = e_cal_util_dup_x_property (subcomp, E_CALDAV_X_ETAG);
+ if (!uid)
+ uid = g_strdup (icalcomponent_get_uid (subcomp));
- url = icalattach_get_url (attach);
- if (g_str_has_prefix (url, LOCAL_PREFIX))
- to_remove = g_slist_prepend (to_remove, p);
+ e_cal_util_remove_x_property (subcomp, E_CALDAV_X_ETAG);
}
}
- g_slist_foreach (to_remove, remove_property, icalcomp);
- g_slist_free (to_remove);
-
- /* convert local url attachments to inline attachments now */
- for (p = icalcomponent_get_first_property (cclone, ICAL_ATTACH_PROPERTY);
- p;
- p = icalcomponent_get_next_property (cclone, ICAL_ATTACH_PROPERTY)) {
- icalattach *attach;
- GFile *file;
- GError *error = NULL;
- const gchar *uri;
- gchar *basename;
- gchar *content;
- gsize len;
-
- attach = icalproperty_get_attach ((const icalproperty *) p);
- if (!icalattach_get_is_url (attach))
- continue;
-
- uri = icalattach_get_url (attach);
- if (!g_str_has_prefix (uri, LOCAL_PREFIX))
- continue;
-
- file = g_file_new_for_uri (uri);
- basename = g_file_get_basename (file);
- if (g_file_load_contents (file, NULL, &content, &len, NULL, &error)) {
- icalproperty *prop;
- icalparameter *param;
- gchar *encoded;
-
- /*
- * do a base64 encoding so it can
- * be embedded in a soap message
- */
- encoded = g_base64_encode ((guchar *) content, len);
- attach = icalattach_new_from_data (encoded, NULL, NULL);
- g_free (content);
- g_free (encoded);
-
- prop = icalproperty_new_attach (attach);
- icalattach_unref (attach);
-
- param = icalparameter_new_value (ICAL_VALUE_BINARY);
- icalproperty_add_parameter (prop, param);
- param = icalparameter_new_encoding (ICAL_ENCODING_BASE64);
- icalproperty_add_parameter (prop, param);
+ ical_string = icalcomponent_as_ical_string_r (vcalendar);
+ icalcomponent_free (vcalendar);
- param = icalparameter_new_x (basename);
- icalparameter_set_xname (param, X_E_CALDAV_ATTACHMENT_NAME);
- icalproperty_add_parameter (prop, param);
+ if (uid && ical_string && (!overwrite_existing || (extra && *extra))) {
+ gboolean force_write = FALSE;
- icalcomponent_add_property (icalcomp, prop);
- } else {
- g_warning ("%s\n", error->message);
- g_clear_error (&error);
- }
- g_free (basename);
- g_object_unref (file);
- }
- icalcomponent_free (cclone);
-}
+ if (!extra || !*extra)
+ href = ecb_caldav_uid_to_uri (cbdav, uid, ".ics");
-static void
-convert_to_url_attachment (ECalBackendCalDAV *cbdav,
- icalcomponent *icalcomp)
-{
- ECalBackend *backend;
- GSList *to_remove = NULL, *to_remove_after_download = NULL;
- icalcomponent *cclone;
- icalproperty *p;
- gint fileindex;
-
- g_return_if_fail (cbdav != NULL);
- g_return_if_fail (icalcomp != NULL);
-
- backend = E_CAL_BACKEND (cbdav);
- cclone = icalcomponent_new_clone (icalcomp);
-
- /* Remove all inline attachments first */
- for (p = icalcomponent_get_first_property (icalcomp, ICAL_ATTACH_PROPERTY);
- p;
- p = icalcomponent_get_next_property (icalcomp, ICAL_ATTACH_PROPERTY)) {
- icalattach *attach;
-
- attach = icalproperty_get_attach ((const icalproperty *) p);
- if (!icalattach_get_is_url (attach))
- to_remove = g_slist_prepend (to_remove, p);
- else if (is_stored_on_server (cbdav, icalattach_get_url (attach)))
- to_remove_after_download = g_slist_prepend (to_remove_after_download, p);
- }
- g_slist_foreach (to_remove, remove_property, icalcomp);
- g_slist_free (to_remove);
-
- /* convert inline attachments to url attachments now */
- for (p = icalcomponent_get_first_property (cclone, ICAL_ATTACH_PROPERTY), fileindex = 0;
- p;
- p = icalcomponent_get_next_property (cclone, ICAL_ATTACH_PROPERTY), fileindex++) {
- icalattach *attach;
- gsize len = -1;
- gchar *decoded = NULL;
- gchar *basename, *local_filename;
-
- attach = icalproperty_get_attach ((const icalproperty *) p);
- if (icalattach_get_is_url (attach)) {
- const gchar *attach_url = icalattach_get_url (attach);
- GError *error = NULL;
-
- if (!is_stored_on_server (cbdav, attach_url))
- continue;
-
- if (!caldav_server_download_attachment (cbdav, attach_url, &decoded, &len, &error)) {
- if (caldav_debug_show (DEBUG_ATTACHMENTS))
- g_print ("CalDAV::%s: Failed to download from a server: %s\n",
G_STRFUNC, error ? error->message : "Unknown error");
- continue;
+ if (overwrite_existing) {
+ switch (conflict_resolution) {
+ case E_CONFLICT_RESOLUTION_FAIL:
+ case E_CONFLICT_RESOLUTION_USE_NEWER:
+ case E_CONFLICT_RESOLUTION_KEEP_SERVER:
+ case E_CONFLICT_RESOLUTION_WRITE_COPY:
+ break;
+ case E_CONFLICT_RESOLUTION_KEEP_LOCAL:
+ force_write = TRUE;
+ break;
}
}
- basename = icalproperty_get_parameter_as_string_r (p, X_E_CALDAV_ATTACHMENT_NAME);
- local_filename = e_cal_backend_create_cache_filename (backend, icalcomponent_get_uid
(icalcomp), basename, fileindex);
- g_free (basename);
-
- if (local_filename) {
- GError *error = NULL;
+ success = e_webdav_session_put_data_sync (cbdav->priv->webdav, (extra && *extra) ? extra :
href,
+ force_write ? "" : overwrite_existing ? etag : NULL, E_WEBDAV_CONTENT_TYPE_CALENDAR,
+ ical_string, -1, out_new_extra, NULL, cancellable, error);
- if (decoded == NULL) {
- gchar *content;
-
- content = (gchar *) icalattach_get_data (attach);
- decoded = (gchar *) g_base64_decode (content, &len);
- }
-
- if (g_file_set_contents (local_filename, decoded, len, &error)) {
- icalproperty *prop;
- gchar *url;
-
- url = g_filename_to_uri (local_filename, NULL, NULL);
- attach = icalattach_new_from_url (url);
- prop = icalproperty_new_attach (attach);
- icalattach_unref (attach);
- icalcomponent_add_property (icalcomp, prop);
- g_free (url);
- } else {
- g_warning ("%s\n", error->message);
- g_clear_error (&error);
- }
-
- g_free (local_filename);
- }
+ /* To read the component back, because server can change it */
+ if (success)
+ *out_new_uid = g_strdup (uid);
+ } else {
+ success = FALSE;
+ g_propagate_error (error, EDC_ERROR (InvalidObject));
}
- icalcomponent_free (cclone);
+ g_free (ical_string);
+ g_free (href);
+ g_free (etag);
+ g_free (uid);
- g_slist_foreach (to_remove_after_download, remove_property, icalcomp);
- g_slist_free (to_remove_after_download);
+ return success;
}
-static void
-remove_files (const gchar *dir,
- const gchar *fileprefix)
+static gboolean
+ecb_caldav_remove_component_sync (ECalMetaBackend *meta_backend,
+ EConflictResolution conflict_resolution,
+ const gchar *uid,
+ const gchar *extra,
+ const gchar *object,
+ GCancellable *cancellable,
+ GError **error)
{
- GDir *d;
-
- g_return_if_fail (dir != NULL);
- g_return_if_fail (fileprefix != NULL);
- g_return_if_fail (*fileprefix != '\0');
+ ECalBackendCalDAV *cbdav;
+ icalcomponent *icalcomp;
+ gchar *etag = NULL;
+ gboolean success;
+ GError *local_error = NULL;
- d = g_dir_open (dir, 0, NULL);
- if (d) {
- const gchar *entry;
- gint len = strlen (fileprefix);
+ g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (meta_backend), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (object != NULL, FALSE);
- while ((entry = g_dir_read_name (d)) != NULL) {
- if (entry && strncmp (entry, fileprefix, len) == 0) {
- gchar *path;
+ cbdav = E_CAL_BACKEND_CALDAV (meta_backend);
- path = g_build_filename (dir, entry, NULL);
- if (!g_file_test (path, G_FILE_TEST_IS_DIR))
- g_unlink (path);
- g_free (path);
- }
- }
- g_dir_close (d);
+ if (!extra || !*extra) {
+ g_propagate_error (error, EDC_ERROR (InvalidObject));
+ return FALSE;
}
-}
-static void
-remove_cached_attachment (ECalBackendCalDAV *cbdav,
- const gchar *uid)
-{
- GSList *l;
- guint len;
- gchar *dir;
- gchar *fileprefix;
-
- g_return_if_fail (cbdav != NULL);
- g_return_if_fail (uid != NULL);
-
- l = e_cal_backend_store_get_components_by_uid (cbdav->priv->store, uid);
- len = g_slist_length (l);
- g_slist_foreach (l, (GFunc) g_object_unref, NULL);
- g_slist_free (l);
- if (len > 0)
- return;
-
- dir = e_cal_backend_create_cache_filename (E_CAL_BACKEND (cbdav), uid, "a", 0);
- if (!dir)
- return;
-
- fileprefix = g_strrstr (dir, G_DIR_SEPARATOR_S);
- if (fileprefix) {
- *fileprefix = '\0';
- fileprefix++;
-
- if (*fileprefix)
- fileprefix[strlen (fileprefix) - 1] = '\0';
-
- remove_files (dir, fileprefix);
+ icalcomp = icalcomponent_new_from_string (object);
+ if (!icalcomp) {
+ g_propagate_error (error, EDC_ERROR (InvalidObject));
+ return FALSE;
}
- g_free (dir);
-}
-
-/* callback for icalcomponent_foreach_tzid */
-typedef struct {
- ECalBackendStore *store;
- icalcomponent *vcal_comp;
- icalcomponent *icalcomp;
-} ForeachTzidData;
+ if (conflict_resolution == E_CONFLICT_RESOLUTION_FAIL)
+ etag = e_cal_util_dup_x_property (icalcomp, E_CALDAV_X_ETAG);
-static void
-add_timezone_cb (icalparameter *param,
- gpointer data)
-{
- icaltimezone *tz;
- const gchar *tzid;
- icalcomponent *vtz_comp;
- ForeachTzidData *f_data = (ForeachTzidData *) data;
- ETimezoneCache *cache;
-
- tzid = icalparameter_get_tzid (param);
- if (!tzid)
- return;
+ success = e_webdav_session_delete_sync (cbdav->priv->webdav, extra,
+ NULL, etag, cancellable, &local_error);
- tz = icalcomponent_get_timezone (f_data->vcal_comp, tzid);
- if (tz)
- return;
-
- cache = e_cal_backend_store_ref_timezone_cache (f_data->store);
-
- tz = icalcomponent_get_timezone (f_data->icalcomp, tzid);
- if (tz == NULL)
- tz = icaltimezone_get_builtin_timezone_from_tzid (tzid);
- if (tz == NULL)
- tz = e_timezone_cache_get_timezone (cache, tzid);
-
- vtz_comp = icaltimezone_get_component (tz);
-
- if (tz != NULL && vtz_comp != NULL)
- icalcomponent_add_component (
- f_data->vcal_comp,
- icalcomponent_new_clone (vtz_comp));
-
- g_object_unref (cache);
-}
-
-static void
-add_timezones_from_component (ECalBackendCalDAV *cbdav,
- icalcomponent *vcal_comp,
- icalcomponent *icalcomp)
-{
- ForeachTzidData f_data;
-
- g_return_if_fail (cbdav != NULL);
- g_return_if_fail (vcal_comp != NULL);
- g_return_if_fail (icalcomp != NULL);
-
- f_data.store = cbdav->priv->store;
- f_data.vcal_comp = vcal_comp;
- f_data.icalcomp = icalcomp;
-
- icalcomponent_foreach_tzid (icalcomp, add_timezone_cb, &f_data);
-}
-
-/* also removes X-EVOLUTION-CALDAV from all the components */
-static gchar *
-pack_cobj (ECalBackendCalDAV *cbdav,
- icalcomponent *icomp)
-{
- icalcomponent *calcomp;
- gchar *objstr;
-
- if (icalcomponent_isa (icomp) != ICAL_VCALENDAR_COMPONENT) {
- icalcomponent *cclone;
+ if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_NOT_FOUND)) {
+ gchar *href;
- calcomp = e_cal_util_new_top_level ();
+ href = ecb_caldav_uid_to_uri (cbdav, uid, ".ics");
+ if (href) {
+ g_clear_error (&local_error);
+ success = e_webdav_session_delete_sync (cbdav->priv->webdav, href,
+ NULL, etag, cancellable, &local_error);
- cclone = icalcomponent_new_clone (icomp);
- strip_unneeded_x_props (cclone);
- convert_to_inline_attachment (cbdav, cclone);
- icalcomponent_add_component (calcomp, cclone);
- add_timezones_from_component (cbdav, calcomp, cclone);
- } else {
- icalcomponent *subcomp;
- icalcomponent_kind my_kind = e_cal_backend_get_kind (E_CAL_BACKEND (cbdav));
-
- calcomp = icalcomponent_new_clone (icomp);
- for (subcomp = icalcomponent_get_first_component (calcomp, my_kind);
- subcomp;
- subcomp = icalcomponent_get_next_component (calcomp, my_kind)) {
- strip_unneeded_x_props (subcomp);
- convert_to_inline_attachment (cbdav, subcomp);
- add_timezones_from_component (cbdav, calcomp, subcomp);
+ g_free (href);
}
- }
-
- objstr = icalcomponent_as_ical_string_r (calcomp);
- icalcomponent_free (calcomp);
-
- g_return_val_if_fail (objstr != NULL, NULL);
- return objstr;
-
-}
+ if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_NOT_FOUND)) {
+ href = ecb_caldav_uid_to_uri (cbdav, uid, NULL);
+ if (href) {
+ g_clear_error (&local_error);
+ success = e_webdav_session_delete_sync (cbdav->priv->webdav, href,
+ NULL, etag, cancellable, &local_error);
-static void
-maybe_correct_tzid (ECalBackendCalDAV *cbdav,
- ECalComponentDateTime *dt)
-{
- icaltimezone *zone;
-
- zone = e_timezone_cache_get_timezone (E_TIMEZONE_CACHE (cbdav), dt->tzid);
- if (!zone) {
- g_free ((gchar *) dt->tzid);
- dt->tzid = g_strdup ("UTC");
- } else if (cbdav->priv->is_icloud && !dt->value->is_date) {
- const gchar *location = icaltimezone_get_location (zone);
-
- if (location && *location) {
- g_free ((gchar *) dt->tzid);
- dt->tzid = g_strdup (location);
- } else {
- /* No location available for this timezone, convert to UTC */
- dt->value->zone = zone;
- *dt->value = icaltime_convert_to_zone (*dt->value, icaltimezone_get_utc_timezone ());
- g_free ((gchar *) dt->tzid);
- dt->tzid = g_strdup ("UTC");
+ g_free (href);
+ }
}
}
-}
-static void
-sanitize_component (ECalBackendCalDAV *cbdav,
- ECalComponent *comp)
-{
- ECalComponentDateTime dt;
-
- /* Check dtstart, dtend and due's timezone, and convert it to local
- * default timezone if the timezone is not in our builtin timezone
- * list */
- e_cal_component_get_dtstart (comp, &dt);
- if (dt.value && dt.tzid) {
- maybe_correct_tzid (cbdav, &dt);
- e_cal_component_set_dtstart (comp, &dt);
- }
- e_cal_component_free_datetime (&dt);
-
- e_cal_component_get_dtend (comp, &dt);
- if (dt.value && dt.tzid) {
- maybe_correct_tzid (cbdav, &dt);
- e_cal_component_set_dtend (comp, &dt);
- }
- e_cal_component_free_datetime (&dt);
+ icalcomponent_free (icalcomp);
+ g_free (etag);
- e_cal_component_get_due (comp, &dt);
- if (dt.value && dt.tzid) {
- maybe_correct_tzid (cbdav, &dt);
- e_cal_component_set_due (comp, &dt);
- }
- e_cal_component_free_datetime (&dt);
+ if (local_error)
+ g_propagate_error (error, local_error);
- e_cal_component_abort_sequence (comp);
+ return success;
}
static gboolean
-cache_contains (ECalBackendCalDAV *cbdav,
- const gchar *uid,
- const gchar *rid)
+ecb_caldav_propfind_get_owner_cb (EWebDAVSession *webdav,
+ xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prop_prefix,
+ const SoupURI *request_uri,
+ const gchar *href,
+ guint status_code,
+ gpointer user_data)
{
- gboolean res;
- ECalComponent *comp;
-
- g_return_val_if_fail (cbdav != NULL, FALSE);
- g_return_val_if_fail (uid != NULL, FALSE);
-
- g_return_val_if_fail (cbdav->priv->store != NULL, FALSE);
+ gchar **out_owner_href = user_data;
- comp = e_cal_backend_store_get_component (cbdav->priv->store, uid, rid);
- res = comp != NULL;
+ g_return_val_if_fail (out_owner_href != NULL, FALSE);
- if (comp)
- g_object_unref (comp);
-
- return res;
-}
-
-/* Returns subcomponent of icalcomp, which is a master object, or icalcomp itself, if it's not a VCALENDAR;
- * Do not free returned pointer, it'll be freed together with the icalcomp.
-*/
-static icalcomponent *
-get_master_comp (ECalBackendCalDAV *cbdav,
- icalcomponent *icalcomp)
-{
- icalcomponent *master = icalcomp;
-
- g_return_val_if_fail (cbdav != NULL, NULL);
- g_return_val_if_fail (icalcomp != NULL, NULL);
+ if (xpath_prop_prefix &&
+ status_code == SOUP_STATUS_OK) {
+ gchar *tmp = e_xml_xpath_eval_as_string (xpath_ctx, "%s/D:owner/D:href", xpath_prop_prefix);
- if (icalcomponent_isa (icalcomp) == ICAL_VCALENDAR_COMPONENT) {
- icalcomponent *subcomp;
- icalcomponent_kind my_kind = e_cal_backend_get_kind (E_CAL_BACKEND (cbdav));
+ if (tmp && *tmp)
+ *out_owner_href = e_webdav_session_ensure_full_uri (webdav, request_uri, tmp);
- master = NULL;
-
- for (subcomp = icalcomponent_get_first_component (icalcomp, my_kind);
- subcomp;
- subcomp = icalcomponent_get_next_component (icalcomp, my_kind)) {
- struct icaltimetype sub_rid = icalcomponent_get_recurrenceid (subcomp);
+ g_free (tmp);
- if (icaltime_is_null_time (sub_rid)) {
- master = subcomp;
- break;
- }
- }
+ return FALSE;
}
- return master;
+ return TRUE;
}
static gboolean
-remove_instance (ECalBackendCalDAV *cbdav,
- icalcomponent *icalcomp,
- struct icaltimetype rid,
- ECalObjModType mod,
- gboolean also_exdate)
-{
- icalcomponent *master = icalcomp;
- struct icaltimetype master_dtstart;
- gboolean res = FALSE;
-
- g_return_val_if_fail (icalcomp != NULL, res);
- g_return_val_if_fail (!icaltime_is_null_time (rid), res);
-
- master_dtstart = icalcomponent_get_dtstart (master);
- if (master_dtstart.zone && master_dtstart.zone != rid.zone)
- rid = icaltime_convert_to_zone (rid, (icaltimezone *) master_dtstart.zone);
-
- rid = icaltime_convert_to_zone (rid, icaltimezone_get_utc_timezone ());
-
- /* remove an instance only */
- if (icalcomponent_isa (icalcomp) == ICAL_VCALENDAR_COMPONENT) {
- icalcomponent *subcomp;
- icalcomponent_kind my_kind = e_cal_backend_get_kind (E_CAL_BACKEND (cbdav));
- gint left = 0;
- gboolean start_first = FALSE;
-
- master = NULL;
-
- /* remove old instance first */
- for (subcomp = icalcomponent_get_first_component (icalcomp, my_kind);
- subcomp;
- subcomp = start_first ? icalcomponent_get_first_component (icalcomp, my_kind) :
icalcomponent_get_next_component (icalcomp, my_kind)) {
- struct icaltimetype sub_rid = icalcomponent_get_recurrenceid (subcomp);
-
- start_first = FALSE;
-
- if (icaltime_is_null_time (sub_rid)) {
- master = subcomp;
- left++;
- } else if (icaltime_compare (sub_rid, rid) == 0) {
- icalcomponent_remove_component (icalcomp, subcomp);
- icalcomponent_free (subcomp);
- if (master) {
- break;
- } else {
- /* either no master or master not as the first component, thus rescan
*/
- left = 0;
- start_first = TRUE;
- }
- } else {
- left++;
- }
- }
-
- /* whether left at least one instance or a master object */
- res = left > 0;
- } else {
- res = TRUE;
- }
-
- if (master && also_exdate) {
- e_cal_util_remove_instances (master, rid, mod);
- }
-
- return res;
-}
-
-static icalcomponent *
-replace_master (ECalBackendCalDAV *cbdav,
- icalcomponent *old_comp,
- icalcomponent *new_master)
-{
- icalcomponent *old_master;
- if (icalcomponent_isa (old_comp) != ICAL_VCALENDAR_COMPONENT) {
- icalcomponent_free (old_comp);
- return new_master;
- }
-
- old_master = get_master_comp (cbdav, old_comp);
- if (!old_master) {
- /* no master, strange */
- icalcomponent_free (new_master);
- } else {
- icalcomponent_remove_component (old_comp, old_master);
- icalcomponent_free (old_master);
-
- icalcomponent_add_component (old_comp, new_master);
- }
-
- return old_comp;
-}
-
-/* the resulting component should be unreffed when done with it;
- * the fallback_comp is cloned, if used */
-static ECalComponent *
-get_ecalcomp_master_from_cache_or_fallback (ECalBackendCalDAV *cbdav,
- const gchar *uid,
- const gchar *rid,
- ECalComponent *fallback_comp)
+ecb_caldav_propfind_get_schedule_outbox_url_cb (EWebDAVSession *webdav,
+ xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prop_prefix,
+ const SoupURI *request_uri,
+ const gchar *href,
+ guint status_code,
+ gpointer user_data)
{
- ECalComponent *comp = NULL;
- icalcomponent *icalcomp;
-
- g_return_val_if_fail (cbdav != NULL, NULL);
- g_return_val_if_fail (uid != NULL, NULL);
-
- icalcomp = get_comp_from_cache (cbdav, uid, rid, NULL, NULL);
- if (icalcomp) {
- icalcomponent *master = get_master_comp (cbdav, icalcomp);
+ gchar **out_schedule_outbox_url = user_data;
- if (master) {
- comp = e_cal_component_new_from_icalcomponent (icalcomponent_new_clone (master));
- }
+ g_return_val_if_fail (out_schedule_outbox_url != NULL, FALSE);
- icalcomponent_free (icalcomp);
- }
+ if (!xpath_prop_prefix) {
+ e_xml_xpath_context_register_namespaces (xpath_ctx, "C", E_WEBDAV_NS_CALDAV, NULL);
+ } else if (status_code == SOUP_STATUS_OK) {
+ gchar *tmp = e_xml_xpath_eval_as_string (xpath_ctx, "%s/C:schedule-outbox-URL/D:href",
xpath_prop_prefix);
- if (!comp && fallback_comp)
- comp = e_cal_component_clone (fallback_comp);
+ if (tmp && *tmp)
+ *out_schedule_outbox_url = e_webdav_session_ensure_full_uri (webdav, request_uri,
tmp);
- return comp;
-}
-
-/* a busy_lock is supposed to be locked already, when calling this function */
-static void
-do_create_objects (ECalBackendCalDAV *cbdav,
- const GSList *in_calobjs,
- GSList **uids,
- GSList **new_components,
- GCancellable *cancellable,
- GError **perror)
-{
- ECalComponent *comp;
- gboolean online, did_put = FALSE;
- struct icaltimetype current;
- icalcomponent *icalcomp;
- const gchar *in_calobj = in_calobjs->data;
- const gchar *comp_uid;
-
- if (!check_state (cbdav, &online, perror))
- return;
-
- /* We make the assumption that the in_calobjs list we're passed is always exactly one element long,
since we haven't specified "bulk-adds"
- * in our static capability list. This simplifies a lot of the logic, especially around asynchronous
results. */
- if (in_calobjs->next != NULL) {
- g_propagate_error (perror, e_data_cal_create_error (UnsupportedMethod, _("CalDAV does not
support bulk additions")));
- return;
- }
-
- comp = e_cal_component_new_from_string (in_calobj);
-
- if (comp == NULL) {
- g_propagate_error (perror, EDC_ERROR (InvalidObject));
- return;
- }
-
- icalcomp = e_cal_component_get_icalcomponent (comp);
- if (icalcomp == NULL) {
- g_object_unref (comp);
- g_propagate_error (perror, EDC_ERROR (InvalidObject));
- return;
- }
-
- comp_uid = icalcomponent_get_uid (icalcomp);
- if (!comp_uid) {
- gchar *new_uid;
-
- new_uid = e_cal_component_gen_uid ();
- if (!new_uid) {
- g_object_unref (comp);
- g_propagate_error (perror, EDC_ERROR (InvalidObject));
- return;
- }
-
- icalcomponent_set_uid (icalcomp, new_uid);
- comp_uid = icalcomponent_get_uid (icalcomp);
-
- g_free (new_uid);
- }
-
- /* check the object is not in our cache */
- if (cache_contains (cbdav, comp_uid, NULL)) {
- g_object_unref (comp);
- g_propagate_error (perror, EDC_ERROR (ObjectIdAlreadyExists));
- return;
- }
-
- /* Set the created and last modified times on the component */
- current = icaltime_current_time_with_zone (icaltimezone_get_utc_timezone ());
- e_cal_component_set_created (comp, ¤t);
- e_cal_component_set_last_modified (comp, ¤t);
-
- /* sanitize the component*/
- sanitize_component (cbdav, comp);
-
- if (online) {
- CalDAVObject object;
-
- object.href = ecalcomp_gen_href (comp);
- object.etag = NULL;
- object.cdata = pack_cobj (cbdav, icalcomp);
-
- did_put = caldav_server_put_object (cbdav, &object, icalcomp, cancellable, perror);
-
- caldav_object_free (&object, FALSE);
- } else {
- /* mark component as out of synch */
- /*ecalcomp_set_synch_state (comp, ECALCOMP_LOCALLY_CREATED); */
- }
-
- if (did_put) {
- if (uids)
- *uids = g_slist_prepend (*uids, g_strdup (comp_uid));
+ g_free (tmp);
- if (new_components)
- *new_components = g_slist_prepend(*new_components,
get_ecalcomp_master_from_cache_or_fallback (cbdav, comp_uid, NULL, comp));
+ return FALSE;
}
- g_object_unref (comp);
+ return TRUE;
}
-/* a busy_lock is supposed to be locked already, when calling this function */
-static void
-do_modify_objects (ECalBackendCalDAV *cbdav,
- const GSList *calobjs,
- ECalObjModType mod,
- GSList **old_components,
- GSList **new_components,
- GCancellable *cancellable,
- GError **error)
+static gboolean
+ecb_caldav_receive_schedule_outbox_url_sync (ECalBackendCalDAV *cbdav,
+ GCancellable *cancellable,
+ GError **error)
{
- ECalComponent *comp;
- icalcomponent *cache_comp, *master_comp;
- gboolean online, did_put = FALSE, success = TRUE;
- ECalComponentId *id;
- struct icaltimetype current;
- gchar *href = NULL, *etag = NULL;
- const gchar *calobj = calobjs->data;
-
- if (new_components)
- *new_components = NULL;
-
- if (!check_state (cbdav, &online, error))
- return;
-
- /* We make the assumption that the calobjs list we're passed is always exactly one element long,
since we haven't specified "bulk-modifies"
- * in our static capability list. This simplifies a lot of the logic, especially around asynchronous
results. */
- if (calobjs->next != NULL) {
- g_propagate_error (error, e_data_cal_create_error (UnsupportedMethod, _("CalDAV does not
support bulk modifications")));
- return;
- }
-
- comp = e_cal_component_new_from_string (calobj);
-
- if (comp == NULL) {
- g_propagate_error (error, EDC_ERROR (InvalidObject));
- return;
- }
-
- if (!e_cal_component_get_icalcomponent (comp) ||
- icalcomponent_isa (e_cal_component_get_icalcomponent (comp)) != e_cal_backend_get_kind
(E_CAL_BACKEND (cbdav))) {
- g_object_unref (comp);
- g_propagate_error (error, EDC_ERROR (InvalidObject));
- return;
- }
+ EXmlDocument *xml;
+ gchar *owner_href = NULL, *schedule_outbox_url = NULL;
+ gboolean success;
- /* Set the last modified time on the component */
- current = icaltime_current_time_with_zone (icaltimezone_get_utc_timezone ());
- e_cal_component_set_last_modified (comp, ¤t);
+ g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav), FALSE);
+ g_return_val_if_fail (cbdav->priv->schedule_outbox_url == NULL, TRUE);
- /* sanitize the component */
- sanitize_component (cbdav, comp);
+ xml = e_xml_document_new (E_WEBDAV_NS_DAV, "propfind");
+ g_return_val_if_fail (xml != NULL, FALSE);
- id = e_cal_component_get_id (comp);
- if (id == NULL) {
- g_set_error_literal (
- error, E_CAL_CLIENT_ERROR,
- E_CAL_CLIENT_ERROR_INVALID_OBJECT,
- e_cal_client_error_to_string (
- E_CAL_CLIENT_ERROR_INVALID_OBJECT));
- return;
- }
+ e_xml_document_start_element (xml, NULL, "prop");
+ e_xml_document_add_empty_element (xml, NULL, "owner");
+ e_xml_document_end_element (xml); /* prop */
- /* fetch full component from cache, it will be pushed to the server */
- cache_comp = get_comp_from_cache (cbdav, id->uid, NULL, &href, &etag);
+ success = e_webdav_session_propfind_sync (cbdav->priv->webdav, NULL, E_WEBDAV_DEPTH_THIS, xml,
+ ecb_caldav_propfind_get_owner_cb, &owner_href, cancellable, error);
- if (cache_comp == NULL) {
- e_cal_component_free_id (id);
- g_object_unref (comp);
- g_free (href);
- g_free (etag);
- g_propagate_error (error, EDC_ERROR (ObjectNotFound));
- return;
- }
+ g_object_unref (xml);
- if (!online) {
- /* mark component as out of synch */
- /*ecalcomp_set_synch_state (comp, ECALCOMP_LOCALLY_MODIFIED);*/
+ if (!success || !owner_href || !*owner_href) {
+ g_free (owner_href);
+ return FALSE;
}
- if (old_components) {
- *old_components = NULL;
-
- if (e_cal_component_is_instance (comp)) {
- /* set detached instance as the old object, if any */
- ECalComponent *old_instance = e_cal_backend_store_get_component (cbdav->priv->store,
id->uid, id->rid);
-
- /* This will give a reference to 'old_component' */
- if (old_instance) {
- *old_components = g_slist_prepend (*old_components, e_cal_component_clone
(old_instance));
- g_object_unref (old_instance);
- }
- }
-
- if (!*old_components) {
- icalcomponent *master = get_master_comp (cbdav, cache_comp);
-
- if (master) {
- /* set full component as the old object */
- *old_components = g_slist_prepend (*old_components,
e_cal_component_new_from_icalcomponent (icalcomponent_new_clone (master)));
- }
- }
+ xml = e_xml_document_new (E_WEBDAV_NS_DAV, "propfind");
+ if (!xml) {
+ g_warn_if_fail (xml != NULL);
+ g_free (owner_href);
+ return FALSE;
}
- switch (mod) {
- case E_CAL_OBJ_MOD_ONLY_THIS:
- case E_CAL_OBJ_MOD_THIS:
- if (e_cal_component_is_instance (comp)) {
- icalcomponent *new_comp = e_cal_component_get_icalcomponent (comp);
-
- /* new object is only this instance */
- if (new_components)
- *new_components = g_slist_prepend (*new_components, e_cal_component_clone
(comp));
-
- /* add the detached instance */
- if (icalcomponent_isa (cache_comp) == ICAL_VCALENDAR_COMPONENT) {
- /* do not modify the EXDATE, as the component will be put back */
- remove_instance (cbdav, cache_comp, icalcomponent_get_recurrenceid
(new_comp), mod, FALSE);
- } else {
- /* this is only a master object, thus make is a VCALENDAR component */
- icalcomponent *icomp;
-
- icomp = e_cal_util_new_top_level ();
- icalcomponent_add_component (icomp, cache_comp);
+ e_xml_document_add_namespaces (xml, "C", E_WEBDAV_NS_CALDAV, NULL);
- /* no need to free the cache_comp, as it is inside icomp */
- cache_comp = icomp;
- }
+ e_xml_document_start_element (xml, NULL, "prop");
+ e_xml_document_add_empty_element (xml, E_WEBDAV_NS_CALDAV, "schedule-outbox-URL");
+ e_xml_document_end_element (xml); /* prop */
- if (cache_comp && cbdav->priv->is_google) {
- icalcomponent_set_sequence (cache_comp, icalcomponent_get_sequence
(cache_comp) + 1);
- icalcomponent_set_sequence (new_comp, icalcomponent_get_sequence (new_comp) +
1);
- }
+ success = e_webdav_session_propfind_sync (cbdav->priv->webdav, owner_href, E_WEBDAV_DEPTH_THIS, xml,
+ ecb_caldav_propfind_get_schedule_outbox_url_cb, &schedule_outbox_url, cancellable, error);
- /* add the detached instance finally */
- icalcomponent_add_component (cache_comp, icalcomponent_new_clone (new_comp));
- } else {
- cache_comp = replace_master (cbdav, cache_comp, icalcomponent_new_clone
(e_cal_component_get_icalcomponent (comp)));
- }
- break;
- case E_CAL_OBJ_MOD_ALL:
- e_cal_recur_ensure_end_dates (comp, TRUE, resolve_tzid, cbdav);
- cache_comp = replace_master (cbdav, cache_comp, icalcomponent_new_clone
(e_cal_component_get_icalcomponent (comp)));
- break;
- case E_CAL_OBJ_MOD_THIS_AND_PRIOR:
- case E_CAL_OBJ_MOD_THIS_AND_FUTURE:
- master_comp = get_master_comp (cbdav, cache_comp);
- if (e_cal_component_is_instance (comp) && master_comp) {
- ECalComponent *mcomp;
- struct icaltimetype rid, master_dtstart;
- icalcomponent *icalcomp = e_cal_component_get_icalcomponent (comp);
- icalcomponent *split_icalcomp;
- icalproperty *prop;
-
- rid = icalcomponent_get_recurrenceid (icalcomp);
- mcomp = e_cal_component_new_from_icalcomponent (icalcomponent_new_clone
(master_comp));
-
- if (mod == E_CAL_OBJ_MOD_THIS_AND_FUTURE &&
- e_cal_util_is_first_instance (mcomp, icalcomponent_get_recurrenceid (icalcomp),
resolve_tzid, cbdav)) {
- icalproperty *prop = icalcomponent_get_first_property (icalcomp,
ICAL_RECURRENCEID_PROPERTY);
-
- if (prop)
- icalcomponent_remove_property (icalcomp, prop);
-
- e_cal_component_rescan (comp);
- e_cal_recur_ensure_end_dates (comp, TRUE, resolve_tzid, cbdav);
-
- /* Then do it like for "mod_all" */
- cache_comp = replace_master (cbdav, cache_comp, icalcomponent_new_clone
(e_cal_component_get_icalcomponent (comp)));
- g_clear_object (&mcomp);
-
- if (new_components) {
- /* read the comp from cache again, as some servers can modify it on
put */
- *new_components = g_slist_prepend (*new_components,
get_ecalcomp_master_from_cache_or_fallback (cbdav, id->uid, NULL, comp));
- }
- break;
- }
-
- prop = icalcomponent_get_first_property (icalcomp, ICAL_RECURRENCEID_PROPERTY);
- if (prop)
- icalcomponent_remove_property (icalcomp, prop);
- e_cal_component_rescan (comp);
-
- master_dtstart = icalcomponent_get_dtstart (master_comp);
- if (master_dtstart.zone && master_dtstart.zone != rid.zone)
- rid = icaltime_convert_to_zone (rid, (icaltimezone *) master_dtstart.zone);
- split_icalcomp = e_cal_util_split_at_instance (icalcomp, rid, master_dtstart);
- if (split_icalcomp) {
- ECalComponent *prev_comp;
-
- prev_comp = e_cal_component_clone (mcomp);
+ g_object_unref (xml);
+ g_free (owner_href);
- rid = icaltime_convert_to_zone (rid, icaltimezone_get_utc_timezone ());
- e_cal_util_remove_instances (master_comp, rid, mod);
- e_cal_component_rescan (mcomp);
- e_cal_recur_ensure_end_dates (mcomp, TRUE, resolve_tzid, cbdav);
+ if (success && schedule_outbox_url && *schedule_outbox_url) {
+ g_free (cbdav->priv->schedule_outbox_url);
+ cbdav->priv->schedule_outbox_url = schedule_outbox_url;
- if (new_components) {
- *new_components = g_slist_prepend (*new_components,
- get_ecalcomp_master_from_cache_or_fallback (cbdav, id->uid,
NULL, mcomp));
- }
-
- g_clear_object (&prev_comp);
- }
-
- cache_comp = replace_master (cbdav, cache_comp, icalcomponent_new_clone
(master_comp));
- if (split_icalcomp) {
- gchar *new_uid;
-
- new_uid = e_cal_component_gen_uid ();
- icalcomponent_set_uid (split_icalcomp, new_uid);
- g_free (new_uid);
-
- g_warn_if_fail (e_cal_component_set_icalcomponent (comp, split_icalcomp));
-
- e_cal_recur_ensure_end_dates (comp, TRUE, resolve_tzid, cbdav);
-
- /* sanitize the component */
- sanitize_component (cbdav, comp);
-
- if (online) {
- CalDAVObject object;
-
- object.href = ecalcomp_gen_href (comp);
- object.etag = NULL;
- object.cdata = pack_cobj (cbdav, split_icalcomp);
-
- success = caldav_server_put_object (cbdav, &object, split_icalcomp,
cancellable, error);
- if (success && new_components) {
- ECalComponent *new_comp;
-
- /* read the comp from cache again, as some servers can modify
it on put */
- new_comp = get_ecalcomp_master_from_cache_or_fallback (cbdav,
icalcomponent_get_uid (split_icalcomp), NULL, comp);
- if (new_comp)
- e_cal_backend_notify_component_created (E_CAL_BACKEND
(cbdav), new_comp);
-
- g_clear_object (&new_comp);
- }
-
- caldav_object_free (&object, FALSE);
- } else {
- /* mark component as out of synch */
- /*ecalcomp_set_synch_state (comp, ECALCOMP_LOCALLY_CREATED); */
- }
- }
-
- g_clear_object (&mcomp);
- } else {
- cache_comp = replace_master (cbdav, cache_comp, icalcomponent_new_clone
(e_cal_component_get_icalcomponent (comp)));
- }
- break;
- }
-
- if (online) {
- CalDAVObject object;
-
- object.href = href;
- object.etag = etag;
- object.cdata = pack_cobj (cbdav, cache_comp);
-
- did_put = success && caldav_server_put_object (cbdav, &object, cache_comp, cancellable,
error);
-
- caldav_object_free (&object, FALSE);
- href = NULL;
- etag = NULL;
+ schedule_outbox_url = NULL;
} else {
- /* mark component as out of synch */
- /*ecalcomp_set_synch_state (comp, ECALCOMP_LOCALLY_MODIFIED);*/
- }
-
- if (did_put) {
- if (new_components && !*new_components) {
- /* read the comp from cache again, as some servers can modify it on put */
- *new_components = g_slist_prepend (*new_components,
get_ecalcomp_master_from_cache_or_fallback (cbdav, id->uid, id->rid, NULL));
- }
- }
-
- e_cal_component_free_id (id);
- icalcomponent_free (cache_comp);
- g_object_unref (comp);
- g_free (href);
- g_free (etag);
-}
-
-/* a busy_lock is supposed to be locked already, when calling this function */
-static void
-do_remove_objects (ECalBackendCalDAV *cbdav,
- const GSList *ids,
- ECalObjModType mod,
- GSList **old_components,
- GSList **new_components,
- GCancellable *cancellable,
- GError **perror)
-{
- icalcomponent *cache_comp;
- gboolean online;
- gchar *href = NULL, *etag = NULL;
- const gchar *uid = ((ECalComponentId *) ids->data)->uid;
- const gchar *rid = ((ECalComponentId *) ids->data)->rid;
-
- if (new_components)
- *new_components = NULL;
-
- if (!check_state (cbdav, &online, perror))
- return;
-
- /* We make the assumption that the ids list we're passed is always exactly one element long, since we
haven't specified "bulk-removes"
- * in our static capability list. This simplifies a lot of the logic, especially around asynchronous
results. */
- if (ids->next != NULL) {
- g_propagate_error (perror, e_data_cal_create_error (UnsupportedMethod, _("CalDAV does not
support bulk removals")));
- return;
- }
-
- cache_comp = get_comp_from_cache (cbdav, uid, NULL, &href, &etag);
-
- if (cache_comp == NULL) {
- g_propagate_error (perror, EDC_ERROR (ObjectNotFound));
- return;
- }
-
- if (old_components) {
- ECalComponent *old = e_cal_backend_store_get_component (cbdav->priv->store, uid, rid);
-
- if (old) {
- *old_components = g_slist_prepend (*old_components, e_cal_component_clone (old));
- g_object_unref (old);
- } else {
- icalcomponent *master = get_master_comp (cbdav, cache_comp);
- if (master) {
- *old_components = g_slist_prepend (*old_components,
e_cal_component_new_from_icalcomponent (icalcomponent_new_clone (master)));
- }
- }
- }
-
- switch (mod) {
- case E_CAL_OBJ_MOD_ONLY_THIS:
- case E_CAL_OBJ_MOD_THIS:
- if (rid && *rid) {
- /* remove one instance from the component */
- if (remove_instance (cbdav, cache_comp, icaltime_from_string (rid), mod, mod !=
E_CAL_OBJ_MOD_ONLY_THIS)) {
- if (new_components) {
- icalcomponent *master = get_master_comp (cbdav, cache_comp);
- if (master) {
- *new_components = g_slist_prepend (*new_components,
e_cal_component_new_from_icalcomponent (icalcomponent_new_clone (master)));
- }
- }
- } else {
- /* this was the last instance, thus delete whole component */
- rid = NULL;
- remove_comp_from_cache (cbdav, uid, NULL);
- }
- } else {
- /* remove whole object */
- remove_comp_from_cache (cbdav, uid, NULL);
- }
- break;
- case E_CAL_OBJ_MOD_ALL:
- remove_comp_from_cache (cbdav, uid, NULL);
- break;
- case E_CAL_OBJ_MOD_THIS_AND_PRIOR:
- case E_CAL_OBJ_MOD_THIS_AND_FUTURE:
- if (remove_instance (cbdav, cache_comp, icaltime_from_string (rid), mod, TRUE)) {
- if (new_components) {
- icalcomponent *master = get_master_comp (cbdav, cache_comp);
- if (master) {
- *new_components = g_slist_prepend (*new_components,
e_cal_component_new_from_icalcomponent (icalcomponent_new_clone (master)));
- }
- }
- }
- break;
+ success = FALSE;
}
- if (online) {
- CalDAVObject caldav_object;
-
- caldav_object.href = href;
- caldav_object.etag = etag;
- caldav_object.cdata = NULL;
-
- if (mod == E_CAL_OBJ_MOD_THIS && rid && *rid) {
- caldav_object.cdata = pack_cobj (cbdav, cache_comp);
-
- caldav_server_put_object (cbdav, &caldav_object, cache_comp, cancellable, perror);
- } else
- caldav_server_delete_object (cbdav, &caldav_object, cancellable, perror);
-
- caldav_object_free (&caldav_object, FALSE);
- href = NULL;
- etag = NULL;
- } else {
- /* mark component as out of synch */
- /*if (mod == E_CAL_OBJ_MOD_THIS && rid && *rid)
- ecalcomp_set_synch_state (cache_comp_master, ECALCOMP_LOCALLY_MODIFIED);
- else
- ecalcomp_set_synch_state (cache_comp_master, ECALCOMP_LOCALLY_DELETED);*/
- }
- remove_cached_attachment (cbdav, uid);
+ g_free (schedule_outbox_url);
- icalcomponent_free (cache_comp);
- g_free (href);
- g_free (etag);
+ return success;
}
static void
-extract_objects (icalcomponent *icomp,
- icalcomponent_kind ekind,
- GSList **objects,
- GError **error)
+ecb_caldav_extract_objects (icalcomponent *icomp,
+ icalcomponent_kind ekind,
+ GSList **out_objects,
+ GError **error)
{
- icalcomponent *scomp;
- icalcomponent_kind kind;
+ icalcomponent *scomp;
+ icalcomponent_kind kind;
GSList *link;
+ g_return_if_fail (icomp != NULL);
+ g_return_if_fail (out_objects != NULL);
+
kind = icalcomponent_isa (icomp);
if (kind == ekind) {
- *objects = g_slist_prepend (NULL, icomp);
+ *out_objects = g_slist_prepend (NULL, icalcomponent_new_clone (icomp));
return;
}
@@ -4636,648 +1294,107 @@ extract_objects (icalcomponent *icomp,
return;
}
- *objects = NULL;
+ *out_objects = NULL;
scomp = icalcomponent_get_first_component (icomp, ekind);
while (scomp) {
- *objects = g_slist_prepend (*objects, scomp);
+ *out_objects = g_slist_prepend (*out_objects, scomp);
scomp = icalcomponent_get_next_component (icomp, ekind);
}
- for (link = *objects; link; link = g_slist_next (link)) {
+ for (link = *out_objects; link; link = g_slist_next (link)) {
/* Remove components from toplevel here */
icalcomponent_remove_component (icomp, link->data);
}
-}
-
-static gboolean
-extract_timezones (ECalBackendCalDAV *cbdav,
- icalcomponent *icomp)
-{
- ETimezoneCache *timezone_cache;
- GSList *timezones = NULL, *iter;
- icaltimezone *zone;
- GError *err = NULL;
- g_return_val_if_fail (cbdav != NULL, FALSE);
- g_return_val_if_fail (icomp != NULL, FALSE);
-
- timezone_cache = E_TIMEZONE_CACHE (cbdav);
-
- extract_objects (icomp, ICAL_VTIMEZONE_COMPONENT, &timezones, &err);
- if (err) {
- g_error_free (err);
- return FALSE;
- }
-
- zone = icaltimezone_new ();
- for (iter = timezones; iter; iter = iter->next) {
- if (icaltimezone_set_component (zone, iter->data)) {
- e_timezone_cache_add_timezone (timezone_cache, zone);
- } else {
- icalcomponent_free (iter->data);
- }
- }
-
- icaltimezone_free (zone, TRUE);
- g_slist_free (timezones);
-
- return TRUE;
+ *out_objects = g_slist_reverse (*out_objects);
}
-static void
-process_object (ECalBackendCalDAV *cbdav,
- ECalComponent *ecomp,
- gboolean online,
- icalproperty_method method,
- GCancellable *cancellable,
- GError **error)
+static gchar *
+ecb_caldav_maybe_append_email_domain (const gchar *username,
+ const gchar *may_append)
{
- ESourceRegistry *registry;
- ECalBackend *backend;
- struct icaltimetype now;
- gchar *new_obj_str;
- gboolean is_declined, is_in_cache;
- ECalObjModType mod;
- ECalComponentId *id = e_cal_component_get_id (ecomp);
- GError *err = NULL;
-
- backend = E_CAL_BACKEND (cbdav);
-
- if (id == NULL) {
- g_set_error_literal (
- error, E_CAL_CLIENT_ERROR,
- E_CAL_CLIENT_ERROR_INVALID_OBJECT,
- e_cal_client_error_to_string (
- E_CAL_CLIENT_ERROR_INVALID_OBJECT));
- return;
- }
-
- registry = e_cal_backend_get_registry (E_CAL_BACKEND (cbdav));
-
- /* ctime, mtime */
- now = icaltime_current_time_with_zone (icaltimezone_get_utc_timezone ());
- e_cal_component_set_created (ecomp, &now);
- e_cal_component_set_last_modified (ecomp, &now);
-
- /* just to check whether component exists in a cache */
- is_in_cache = cache_contains (cbdav, id->uid, NULL) || cache_contains (cbdav, id->uid, id->rid);
-
- new_obj_str = e_cal_component_get_as_string (ecomp);
- mod = e_cal_component_is_instance (ecomp) ? E_CAL_OBJ_MOD_THIS : E_CAL_OBJ_MOD_ALL;
-
- switch (method) {
- case ICAL_METHOD_PUBLISH:
- case ICAL_METHOD_REQUEST:
- case ICAL_METHOD_REPLY:
- is_declined = e_cal_backend_user_declined (
- registry, e_cal_component_get_icalcomponent (ecomp));
- if (is_in_cache) {
- if (!is_declined) {
- GSList *new_components = NULL, *old_components = NULL;
- GSList new_obj_strs = {0,};
-
- new_obj_strs.data = new_obj_str;
- do_modify_objects (cbdav, &new_obj_strs, mod,
- &old_components, &new_components, cancellable, &err);
- if (!err && new_components && new_components->data) {
- if (!old_components || !old_components->data) {
- e_cal_backend_notify_component_created (backend,
new_components->data);
- } else {
- e_cal_backend_notify_component_modified (backend,
old_components->data, new_components->data);
- }
- }
-
- e_util_free_nullable_object_slist (old_components);
- e_util_free_nullable_object_slist (new_components);
- } else {
- GSList *new_components = NULL, *old_components = NULL;
- GSList ids = {0,};
-
- ids.data = id;
- do_remove_objects (cbdav, &ids, mod, &old_components, &new_components,
cancellable, &err);
- if (!err && old_components && old_components->data) {
- if (new_components && new_components->data) {
- e_cal_backend_notify_component_modified (backend,
old_components->data, new_components->data);
- } else {
- e_cal_backend_notify_component_removed (backend, id,
old_components->data, NULL);
- }
- }
-
- e_util_free_nullable_object_slist (old_components);
- e_util_free_nullable_object_slist (new_components);
- }
- } else if (!is_declined) {
- GSList *new_components = NULL;
- GSList new_objs = {0,};
-
- new_objs.data = new_obj_str;
-
- do_create_objects (cbdav, &new_objs, NULL, &new_components, cancellable, &err);
-
- if (!err) {
- if (new_components && new_components->data)
- e_cal_backend_notify_component_created (backend,
new_components->data);
- }
-
- e_util_free_nullable_object_slist (new_components);
- }
- break;
- case ICAL_METHOD_CANCEL:
- if (is_in_cache) {
- GSList *new_components = NULL, *old_components = NULL;
- GSList ids = {0,};
-
- ids.data = id;
- do_remove_objects (cbdav, &ids, E_CAL_OBJ_MOD_THIS, &old_components, &new_components,
cancellable, &err);
- if (!err && old_components && old_components->data) {
- if (new_components && new_components->data) {
- e_cal_backend_notify_component_modified (backend,
old_components->data, new_components->data);
- } else {
- e_cal_backend_notify_component_removed (backend, id,
old_components->data, NULL);
- }
- }
-
- e_util_free_nullable_object_slist (old_components);
- e_util_free_nullable_object_slist (new_components);
- } else {
- err = EDC_ERROR (ObjectNotFound);
- }
- break;
-
- default:
- err = EDC_ERROR (UnsupportedMethod);
- break;
- }
+ if (!username || !*username)
+ return NULL;
- e_cal_component_free_id (id);
- g_free (new_obj_str);
+ if (strchr (username, '@'))
+ return g_strdup (username);
- if (err)
- g_propagate_error (error, err);
+ return g_strconcat (username, may_append, NULL);
}
-static void
-do_receive_objects (ECalBackendSync *backend,
- EDataCal *cal,
- GCancellable *cancellable,
- const gchar *calobj,
- GError **perror)
+static gchar *
+ecb_caldav_get_usermail (ECalBackendCalDAV *cbdav)
{
- ECalBackendCalDAV *cbdav;
- icalcomponent *icomp;
- icalcomponent_kind kind;
- icalproperty_method tmethod;
- gboolean online;
- GSList *objects, *iter;
- GError *err = NULL;
-
- cbdav = E_CAL_BACKEND_CALDAV (backend);
-
- if (!check_state (cbdav, &online, perror))
- return;
-
- icomp = icalparser_parse_string (calobj);
-
- /* Try to parse cal object string */
- if (icomp == NULL) {
- g_propagate_error (perror, EDC_ERROR (InvalidObject));
- return;
- }
-
- kind = e_cal_backend_get_kind (E_CAL_BACKEND (backend));
- extract_objects (icomp, kind, &objects, &err);
-
- if (err) {
- icalcomponent_free (icomp);
- g_propagate_error (perror, err);
- return;
- }
-
- /* Extract optional timezone compnents */
- extract_timezones (cbdav, icomp);
-
- if (icalcomponent_get_first_property (icomp, ICAL_METHOD_PROPERTY))
- tmethod = icalcomponent_get_method (icomp);
- else
- tmethod = ICAL_METHOD_PUBLISH;
-
- for (iter = objects; iter && !err; iter = iter->next) {
- icalcomponent *scomp;
- ECalComponent *ecomp;
- icalproperty_method method;
-
- scomp = (icalcomponent *) iter->data;
- ecomp = e_cal_component_new ();
+ ESource *source;
+ ESourceAuthentication *auth_extension;
+ ESourceWebdav *webdav_extension;
+ const gchar *extension_name;
+ gchar *usermail;
+ gchar *username;
+ gchar *res = NULL;
- e_cal_component_set_icalcomponent (ecomp, scomp);
+ g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav), NULL);
- if (icalcomponent_get_first_property (scomp, ICAL_METHOD_PROPERTY)) {
- method = icalcomponent_get_method (scomp);
- } else {
- method = tmethod;
- }
+ source = e_backend_get_source (E_BACKEND (cbdav));
- process_object (cbdav, ecomp, online, method, cancellable, &err);
- g_object_unref (ecomp);
- }
+ extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
+ webdav_extension = e_source_get_extension (source, extension_name);
- g_slist_free (objects);
+ /* This will never return an empty string. */
+ usermail = e_source_webdav_dup_email_address (webdav_extension);
- icalcomponent_free (icomp);
+ if (usermail)
+ return usermail;
- if (err)
- g_propagate_error (perror, err);
-}
+ extension_name = E_SOURCE_EXTENSION_AUTHENTICATION;
+ auth_extension = e_source_get_extension (source, extension_name);
+ username = e_source_authentication_dup_user (auth_extension);
-#define caldav_busy_stub(_func_name, _params, _call_func, _call_params) \
-static void \
-_func_name _params \
-{ \
- ECalBackendCalDAV *cbdav; \
- SlaveCommand old_slave_cmd; \
- gboolean was_slave_busy; \
- \
- cbdav = E_CAL_BACKEND_CALDAV (backend); \
- \
- /* this is done before locking */ \
- old_slave_cmd = cbdav->priv->slave_cmd; \
- was_slave_busy = cbdav->priv->slave_busy; \
- if (was_slave_busy) { \
- /* let it pause its work and do our job */ \
- update_slave_cmd (cbdav->priv, SLAVE_SHOULD_SLEEP); \
- } \
- \
- g_mutex_lock (&cbdav->priv->busy_lock); \
- _call_func _call_params; \
- \
- /* this is done before unlocking */ \
- if (was_slave_busy) { \
- update_slave_cmd (cbdav->priv, old_slave_cmd); \
- g_cond_signal (&cbdav->priv->cond); \
- } \
- \
- g_mutex_unlock (&cbdav->priv->busy_lock); \
-}
+ if (cbdav->priv->is_google)
+ res = ecb_caldav_maybe_append_email_domain (username, "@gmail.com");
-caldav_busy_stub (
- caldav_create_objects,
- (ECalBackendSync *backend,
- EDataCal *cal,
- GCancellable *cancellable,
- const GSList *in_calobjs,
- GSList **uids,
- GSList **new_components,
- GError **perror),
- do_create_objects,
- (cbdav,
- in_calobjs,
- uids,
- new_components,
- cancellable,
- perror))
-
-caldav_busy_stub (
- caldav_modify_objects,
- (ECalBackendSync *backend,
- EDataCal *cal,
- GCancellable *cancellable,
- const GSList *calobjs,
- ECalObjModType mod,
- GSList **old_components,
- GSList **new_components,
- GError **perror),
- do_modify_objects,
- (cbdav,
- calobjs,
- mod,
- old_components,
- new_components,
- cancellable,
- perror))
-
-caldav_busy_stub (
- caldav_remove_objects,
- (ECalBackendSync *backend,
- EDataCal *cal,
- GCancellable *cancellable,
- const GSList *ids,
- ECalObjModType mod,
- GSList **old_components,
- GSList **new_components,
- GError **perror),
- do_remove_objects,
- (cbdav,
- ids,
- mod,
- old_components,
- new_components,
- cancellable,
- perror))
-
-caldav_busy_stub (
- caldav_receive_objects,
- (ECalBackendSync *backend,
- EDataCal *cal,
- GCancellable *cancellable,
- const gchar *calobj,
- GError **perror),
- do_receive_objects,
- (backend,
- cal,
- cancellable,
- calobj,
- perror))
+ g_free (username);
-static void
-caldav_send_objects (ECalBackendSync *backend,
- EDataCal *cal,
- GCancellable *cancellable,
- const gchar *calobj,
- GSList **users,
- gchar **modified_calobj,
- GError **perror)
-{
- *users = NULL;
- *modified_calobj = g_strdup (calobj);
+ return res;
}
static gboolean
-caldav_server_download_uid (ECalBackendCalDAV *cbdav,
- const gchar *uid,
- GCancellable *cancellable,
- GError **error)
+ecb_caldav_get_free_busy_from_schedule_outbox_sync (ECalBackendCalDAV *cbdav,
+ const GSList *users,
+ time_t start,
+ time_t end,
+ GSList **out_freebusy,
+ GCancellable *cancellable,
+ GError **error)
{
- CalDAVObject obj;
- GError *local_error = NULL;
-
- g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav), FALSE);
- g_return_val_if_fail (uid != NULL, FALSE);
-
- obj.href = g_strdup (uid);
- obj.etag = NULL;
- obj.status = 0;
- obj.cdata = NULL;
-
- if (!caldav_server_get_object (cbdav, &obj, cancellable, &local_error)) {
- if (g_error_matches (local_error, E_DATA_CAL_ERROR, ObjectNotFound)) {
- gchar *file;
-
- /* OK, the event was properly created, but cannot be found on the place
- * where it was PUT - why didn't server tell us where it saved it? */
- g_clear_error (&local_error);
-
- /* try whether it's saved as its UID.ics file */
- file = caldav_gen_file_from_uid (cbdav, uid);
- if (file) {
- g_free (obj.href);
- obj.href = file;
-
- if (!caldav_server_get_object (cbdav, &obj, cancellable, &local_error)) {
- }
- }
- }
- }
-
- if (!local_error) {
- icalcomponent *use_comp = NULL;
-
- if (obj.cdata) {
- /* maybe server also modified component, thus rather store the server's */
- use_comp = icalparser_parse_string (obj.cdata);
- put_comp_to_cache (cbdav, use_comp, obj.href, obj.etag);
- }
-
- if (use_comp)
- icalcomponent_free (use_comp);
- else
- local_error = EDC_ERROR (ObjectNotFound);
- }
-
- if (local_error) {
- g_propagate_error (error, local_error);
-
- return FALSE;
- }
-
- g_free (obj.href);
- g_free (obj.etag);
- g_free (obj.cdata);
-
- return TRUE;
-}
-
-static void
-caldav_get_object (ECalBackendSync *backend,
- EDataCal *cal,
- GCancellable *cancellable,
- const gchar *uid,
- const gchar *rid,
- gchar **object,
- GError **perror)
-{
- ECalBackendCalDAV *cbdav;
- icalcomponent *icalcomp;
-
- cbdav = E_CAL_BACKEND_CALDAV (backend);
-
- *object = NULL;
- icalcomp = get_comp_from_cache (cbdav, uid, rid, NULL, NULL);
-
- if (!icalcomp && e_backend_get_online (E_BACKEND (backend))) {
- /* try to fetch from the server, maybe the event was received only recently */
- if (caldav_server_download_uid (cbdav, uid, cancellable, NULL)) {
- icalcomp = get_comp_from_cache (cbdav, uid, rid, NULL, NULL);
- }
- }
-
- if (!icalcomp) {
- g_propagate_error (perror, EDC_ERROR (ObjectNotFound));
- return;
- }
-
- *object = icalcomponent_as_ical_string_r (icalcomp);
- icalcomponent_free (icalcomp);
-}
-
-static void
-caldav_add_timezone (ECalBackendSync *backend,
- EDataCal *cal,
- GCancellable *cancellable,
- const gchar *tzobj,
- GError **error)
-{
- ETimezoneCache *timezone_cache;
- icalcomponent *tz_comp;
-
- timezone_cache = E_TIMEZONE_CACHE (backend);
-
- tz_comp = icalparser_parse_string (tzobj);
- if (!tz_comp) {
- g_propagate_error (error, EDC_ERROR (InvalidObject));
- return;
- }
-
- if (icalcomponent_isa (tz_comp) == ICAL_VTIMEZONE_COMPONENT) {
- icaltimezone *zone;
-
- zone = icaltimezone_new ();
- icaltimezone_set_component (zone, tz_comp);
-
- e_timezone_cache_add_timezone (timezone_cache, zone);
-
- icaltimezone_free (zone, TRUE);
- } else {
- icalcomponent_free (tz_comp);
- }
-}
-
-static void
-caldav_get_object_list (ECalBackendSync *backend,
- EDataCal *cal,
- GCancellable *cancellable,
- const gchar *sexp_string,
- GSList **objects,
- GError **perror)
-{
- ECalBackendCalDAV *cbdav;
- ECalBackendSExp *sexp;
- ETimezoneCache *cache;
- gboolean do_search;
- GSList *list, *iter;
- time_t occur_start = -1, occur_end = -1;
- gboolean prunning_by_time;
-
- cbdav = E_CAL_BACKEND_CALDAV (backend);
-
- sexp = e_cal_backend_sexp_new (sexp_string);
-
- if (sexp == NULL) {
- g_propagate_error (perror, EDC_ERROR (InvalidQuery));
- return;
- }
-
- if (g_str_equal (sexp_string, "#t")) {
- do_search = FALSE;
- } else {
- do_search = TRUE;
- }
-
- *objects = NULL;
-
- prunning_by_time = e_cal_backend_sexp_evaluate_occur_times (sexp, &occur_start, &occur_end);
-
- cache = E_TIMEZONE_CACHE (backend);
-
- list = prunning_by_time ?
- e_cal_backend_store_get_components_occuring_in_range (cbdav->priv->store, occur_start,
occur_end)
- : e_cal_backend_store_get_components (cbdav->priv->store);
-
- for (iter = list; iter; iter = g_slist_next (iter)) {
- ECalComponent *comp = E_CAL_COMPONENT (iter->data);
-
- if (!do_search ||
- e_cal_backend_sexp_match_comp (sexp, comp, cache)) {
- *objects = g_slist_prepend (*objects, e_cal_component_get_as_string (comp));
- }
-
- g_object_unref (comp);
- }
-
- g_object_unref (sexp);
- g_slist_free (list);
-}
-
-static void
-caldav_start_view (ECalBackend *backend,
- EDataCalView *query)
-{
- ECalBackendCalDAV *cbdav;
- ECalBackendSExp *sexp;
- ETimezoneCache *cache;
- gboolean do_search;
- GSList *list, *iter;
- const gchar *sexp_string;
- time_t occur_start = -1, occur_end = -1;
- gboolean prunning_by_time;
- cbdav = E_CAL_BACKEND_CALDAV (backend);
-
- sexp = e_data_cal_view_get_sexp (query);
- sexp_string = e_cal_backend_sexp_text (sexp);
-
- if (g_str_equal (sexp_string, "#t")) {
- do_search = FALSE;
- } else {
- do_search = TRUE;
- }
- prunning_by_time = e_cal_backend_sexp_evaluate_occur_times (
- sexp,
- &occur_start,
- &occur_end);
-
- cache = E_TIMEZONE_CACHE (backend);
-
- list = prunning_by_time ?
- e_cal_backend_store_get_components_occuring_in_range (cbdav->priv->store, occur_start,
occur_end)
- : e_cal_backend_store_get_components (cbdav->priv->store);
-
- for (iter = list; iter; iter = g_slist_next (iter)) {
- ECalComponent *comp = E_CAL_COMPONENT (iter->data);
-
- if (!do_search ||
- e_cal_backend_sexp_match_comp (sexp, comp, cache)) {
- e_data_cal_view_notify_components_added_1 (query, comp);
- }
-
- g_object_unref (comp);
- }
-
- g_slist_free (list);
-
- e_data_cal_view_notify_complete (query, NULL /* Success */);
-}
-
-static void
-caldav_get_free_busy (ECalBackendSync *backend,
- EDataCal *cal,
- GCancellable *cancellable,
- const GSList *users,
- time_t start,
- time_t end,
- GSList **freebusy,
- GError **error)
-{
- ECalBackendCalDAV *cbdav;
icalcomponent *icalcomp;
ECalComponent *comp;
ECalComponentDateTime dt;
- ECalComponentOrganizer organizer = {NULL};
+ ECalComponentOrganizer organizer = { NULL };
ESourceAuthentication *auth_extension;
ESource *source;
struct icaltimetype dtvalue;
icaltimezone *utc;
gchar *str;
- const GSList *u;
+ GSList *link;
GSList *attendees = NULL, *to_free = NULL;
const gchar *extension_name;
gchar *usermail;
- GError *err = NULL;
+ GByteArray *response = NULL;
+ GError *local_error = NULL;
- cbdav = E_CAL_BACKEND_CALDAV (backend);
+ g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav), FALSE);
- if (!cbdav->priv->calendar_schedule) {
- g_propagate_error (error, EDC_ERROR_EX (OtherError, _("Calendar doesn’t support Free/Busy")));
- return;
- }
+ if (!cbdav->priv->calendar_schedule)
+ return FALSE;
if (!cbdav->priv->schedule_outbox_url) {
- caldav_receive_schedule_outbox_url (cbdav, cancellable, error);
- if (!cbdav->priv->schedule_outbox_url) {
+ if (!ecb_caldav_receive_schedule_outbox_url_sync (cbdav, cancellable, error) ||
+ !cbdav->priv->schedule_outbox_url) {
cbdav->priv->calendar_schedule = FALSE;
- if (error && !*error)
- g_propagate_error (error, EDC_ERROR_EX (OtherError, _("Schedule outbox url
not found")));
- return;
+ return FALSE;
}
}
@@ -5301,17 +1418,17 @@ caldav_get_free_busy (ECalBackendSync *backend,
dtvalue = icaltime_from_timet_with_zone (end, FALSE, utc);
e_cal_component_set_dtend (comp, &dt);
- usermail = get_usermail (E_CAL_BACKEND (backend));
+ usermail = ecb_caldav_get_usermail (cbdav);
if (usermail != NULL && *usermail == '\0') {
g_free (usermail);
usermail = NULL;
}
- source = e_backend_get_source (E_BACKEND (backend));
+ source = e_backend_get_source (E_BACKEND (cbdav));
extension_name = E_SOURCE_EXTENSION_AUTHENTICATION;
auth_extension = e_source_get_extension (source, extension_name);
- if (usermail == NULL)
+ if (!usermail)
usermail = e_source_authentication_dup_user (auth_extension);
organizer.value = g_strconcat ("mailto:", usermail, NULL);
@@ -5320,9 +1437,9 @@ caldav_get_free_busy (ECalBackendSync *backend,
g_free (usermail);
- for (u = users; u; u = u->next) {
+ for (link = (GSList *) users; link; link = g_slist_next (link)) {
ECalComponentAttendee *ca;
- gchar *temp = g_strconcat ("mailto:", (const gchar *) u->data, NULL);
+ gchar *temp = g_strconcat ("mailto:", (const gchar *) link->data, NULL);
ca = g_new0 (ECalComponentAttendee, 1);
@@ -5337,11 +1454,8 @@ caldav_get_free_busy (ECalBackendSync *backend,
e_cal_component_set_attendee_list (comp, attendees);
- g_slist_foreach (attendees, (GFunc) g_free, NULL);
- g_slist_free (attendees);
-
- g_slist_foreach (to_free, (GFunc) g_free, NULL);
- g_slist_free (to_free);
+ g_slist_free_full (attendees, g_free);
+ g_slist_free_full (to_free, g_free);
e_cal_component_abort_sequence (comp);
@@ -5355,387 +1469,361 @@ caldav_get_free_busy (ECalBackendSync *backend,
icalcomponent_free (icalcomp);
g_object_unref (comp);
- caldav_post_freebusy (cbdav, cbdav->priv->schedule_outbox_url, &str, cancellable, &err);
-
- if (!err) {
+ if (e_webdav_session_post_sync (cbdav->priv->webdav, cbdav->priv->schedule_outbox_url, str, -1, NULL,
&response, cancellable, &local_error) &&
+ response) {
/* parse returned xml */
xmlDocPtr doc;
+ xmlXPathContextPtr xpath_ctx = NULL;
+
+ doc = e_xml_parse_data (response->data, response->len);
+
+ if (!doc) {
+ g_set_error_literal (&local_error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
+ _("Failed to parse response data"));
+ } else {
+ xpath_ctx = e_xml_new_xpath_context_with_namespaces (doc,
+ "D", E_WEBDAV_NS_DAV,
+ "C", E_WEBDAV_NS_CALDAV,
+ NULL);
+ }
- doc = xmlReadMemory (str, strlen (str), "response.xml", NULL, 0);
- if (doc != NULL) {
- xmlXPathContextPtr xpctx;
- xmlXPathObjectPtr result;
+ if (xpath_ctx) {
+ xmlXPathObjectPtr xpath_obj_response;
- xpctx = xmlXPathNewContext (doc);
- xmlXPathRegisterNs (xpctx, (xmlChar *) "D", (xmlChar *) "DAV:");
- xmlXPathRegisterNs (xpctx, (xmlChar *) "C", (xmlChar *)
"urn:ietf:params:xml:ns:caldav");
+ xpath_obj_response = e_xml_xpath_eval (xpath_ctx, "/C:schedule-response/C:response");
- result = xpath_eval (xpctx, "/C:schedule-response/C:response");
+ if (xpath_obj_response) {
+ gint response_index, response_length;
- if (result == NULL || result->type != XPATH_NODESET) {
- err = EDC_ERROR_EX (OtherError, _("Unexpected result in schedule-response"));
- } else {
- gint i, n;
+ response_length = xmlXPathNodeSetGetLength (xpath_obj_response->nodesetval);
- n = xmlXPathNodeSetGetLength (result->nodesetval);
- for (i = 0; i < n; i++) {
+ for (response_index = 0; response_index < response_length; response_index++) {
gchar *tmp;
- tmp = xp_object_get_string (xpath_eval (xpctx,
"string(/C:schedule-response/C:response[%d]/C:calendar-data)", i + 1));
+ tmp = e_xml_xpath_eval_as_string
(xpath_ctx,"/C:schedule-response/C:response[%d]/C:calendar-data", response_index + 1);
if (tmp && *tmp) {
- GSList *objects = NULL, *o;
+ GSList *objects = NULL;
icalcomp = icalparser_parse_string (tmp);
if (icalcomp)
- extract_objects (icalcomp, ICAL_VFREEBUSY_COMPONENT,
&objects, &err);
- if (icalcomp && !err) {
- for (o = objects; o; o = o->next) {
- gchar *obj_str =
icalcomponent_as_ical_string_r (o->data);
+ ecb_caldav_extract_objects (icalcomp,
ICAL_VFREEBUSY_COMPONENT, &objects, &local_error);
+ if (icalcomp && !local_error) {
+ for (link = objects; link; link = g_slist_next
(link)) {
+ gchar *obj_str =
icalcomponent_as_ical_string_r (link->data);
if (obj_str && *obj_str)
- *freebusy = g_slist_append
(*freebusy, obj_str);
+ *out_freebusy = g_slist_prepend
(*out_freebusy, obj_str);
else
g_free (obj_str);
}
}
- g_slist_foreach (objects, (GFunc) icalcomponent_free, NULL);
- g_slist_free (objects);
+ g_slist_free_full (objects, (GDestroyNotify)
icalcomponent_free);
if (icalcomp)
icalcomponent_free (icalcomp);
- if (err)
- g_error_free (err);
- err = NULL;
+ g_clear_error (&local_error);
}
g_free (tmp);
}
+
+ xmlXPathFreeObject (xpath_obj_response);
}
- if (result != NULL)
- xmlXPathFreeObject (result);
- xmlXPathFreeContext (xpctx);
- xmlFreeDoc (doc);
+ xmlXPathFreeContext (xpath_ctx);
}
+
+ if (doc)
+ xmlFreeDoc (doc);
}
+ if (response)
+ g_byte_array_free (response, TRUE);
g_free (str);
- if (err)
- g_propagate_error (error, err);
-}
+ if (local_error)
+ g_propagate_error (error, local_error);
-static void
-caldav_notify_online_cb (ECalBackend *backend,
- GParamSpec *pspec)
-{
- ECalBackendCalDAV *cbdav;
- gboolean online;
+ return local_error != NULL;
+}
- cbdav = E_CAL_BACKEND_CALDAV (backend);
+static gboolean
+ecb_caldav_get_free_busy_from_principal_sync (ECalBackendCalDAV *cbdav,
+ const gchar *usermail,
+ time_t start,
+ time_t end,
+ GSList **out_freebusy,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EWebDAVResource *resource;
+ GSList *principals = NULL;
+ EXmlDocument *xml;
+ gchar *href;
+ gchar *content_type = NULL;
+ GByteArray *content = NULL;
+ gboolean success;
- /*g_mutex_lock (&cbdav->priv->busy_lock);*/
+ g_return_val_if_fail (E_IS_CAL_BACKEND_CALDAV (cbdav), FALSE);
+ g_return_val_if_fail (usermail != NULL, FALSE);
+ g_return_val_if_fail (out_freebusy != NULL, FALSE);
- online = e_backend_get_online (E_BACKEND (backend));
+ if (!e_webdav_session_principal_property_search_sync (cbdav->priv->webdav, NULL, TRUE,
+ E_WEBDAV_NS_CALDAV, "calendar-user-address-set", usermail, &principals, cancellable, error)) {
+ return FALSE;
+ }
- if (!cbdav->priv->loaded) {
- /*g_mutex_unlock (&cbdav->priv->busy_lock);*/
- return;
+ if (!principals || principals->next || !principals->data) {
+ g_slist_free_full (principals, e_webdav_resource_free);
+ return FALSE;
}
- if (online) {
- /* Wake up the slave thread */
- update_slave_cmd (cbdav->priv, SLAVE_SHOULD_WORK);
- g_cond_signal (&cbdav->priv->cond);
- } else {
- soup_session_abort (cbdav->priv->session);
- update_slave_cmd (cbdav->priv, SLAVE_SHOULD_SLEEP);
+ resource = principals->data;
+ href = g_strdup (resource->href);
+
+ g_slist_free_full (principals, e_webdav_resource_free);
+
+ if (!href || !*href) {
+ g_free (href);
+ return FALSE;
}
- /*g_mutex_unlock (&cbdav->priv->busy_lock);*/
-}
+ xml = e_xml_document_new (E_WEBDAV_NS_CALDAV, "free-busy-query");
-static gpointer
-caldav_source_changed_thread (gpointer data)
-{
- ECalBackendCalDAV *cbdav = data;
- SlaveCommand old_slave_cmd;
- gboolean old_slave_busy;
+ e_xml_document_start_element (xml, NULL, "time-range");
+ e_xml_document_add_attribute_time (xml, NULL, "start", start);
+ e_xml_document_add_attribute_time (xml, NULL, "end", end);
+ e_xml_document_end_element (xml); /* time-range */
- g_return_val_if_fail (cbdav != NULL, NULL);
+ success = e_webdav_session_report_sync (cbdav->priv->webdav, NULL, E_WEBDAV_DEPTH_INFINITY, xml,
NULL, NULL, &content_type, &content, cancellable, error);
- old_slave_cmd = cbdav->priv->slave_cmd;
- old_slave_busy = cbdav->priv->slave_busy;
- if (old_slave_busy)
- update_slave_cmd (cbdav->priv, SLAVE_SHOULD_SLEEP);
+ g_object_unref (xml);
- g_mutex_lock (&cbdav->priv->busy_lock);
+ if (success && content_type && content && content->data && content->len &&
+ g_ascii_strcasecmp (content_type, "text/calendar") == 0) {
+ icalcomponent *vcalendar;
- /* guard the call with busy_lock, thus the two threads (this 'source changed'
- * thread and the 'backend open' thread) will not clash on internal data
- * when they are called in once */
- initialize_backend (cbdav, NULL);
+ vcalendar = icalcomponent_new_from_string ((const gchar *) content->data);
+ if (vcalendar) {
+ GSList *comps = NULL, *link;
- /* always wakeup thread, even when it was sleeping */
- g_cond_signal (&cbdav->priv->cond);
+ ecb_caldav_extract_objects (vcalendar, ICAL_VFREEBUSY_COMPONENT, &comps, NULL);
- if (old_slave_busy)
- update_slave_cmd (cbdav->priv, old_slave_cmd);
+ for (link = comps; link; link = g_slist_next (link)) {
+ icalcomponent *subcomp = link->data;
+ gchar *obj_str;
- g_mutex_unlock (&cbdav->priv->busy_lock);
+ if (!icalcomponent_get_first_property (subcomp, ICAL_ATTENDEE_PROPERTY)) {
+ icalproperty *prop;
+ gchar *mailto;
- cbdav->priv->updating_source = FALSE;
+ mailto = g_strconcat ("mailto:", usermail, NULL);
+ prop = icalproperty_new_attendee (mailto);
+ g_free (mailto);
- g_object_unref (cbdav);
+ icalcomponent_add_property (subcomp, prop);
+ }
- return NULL;
-}
+ obj_str = icalcomponent_as_ical_string_r (subcomp);
-static void
-caldav_source_changed_cb (ESource *source,
- ECalBackendCalDAV *cbdav)
-{
- GThread *thread;
+ if (obj_str && *obj_str)
+ *out_freebusy = g_slist_prepend (*out_freebusy, obj_str);
+ else
+ g_free (obj_str);
+ }
- g_return_if_fail (source != NULL);
- g_return_if_fail (cbdav != NULL);
+ success = comps != NULL;
- if (cbdav->priv->updating_source ||
- !cbdav->priv->loaded ||
- !e_cal_backend_is_opened (E_CAL_BACKEND (cbdav)))
- return;
+ g_slist_free_full (comps, (GDestroyNotify) icalcomponent_free);
+ } else {
+ success = FALSE;
+ }
+ }
- cbdav->priv->updating_source = TRUE;
+ if (content)
+ g_byte_array_free (content, TRUE);
+ g_free (content_type);
+ g_free (href);
- thread = g_thread_new (NULL, caldav_source_changed_thread, g_object_ref (cbdav));
- g_thread_unref (thread);
+ return success;
}
-static ESourceAuthenticationResult
-caldav_authenticate_sync (EBackend *backend,
- const ENamedParameters *credentials,
- gchar **out_certificate_pem,
- GTlsCertificateFlags *out_certificate_errors,
- GCancellable *cancellable,
- GError **error)
+static void
+ecb_caldav_get_free_busy_sync (ECalBackendSync *sync_backend,
+ EDataCal *cal,
+ GCancellable *cancellable,
+ const GSList *users,
+ time_t start,
+ time_t end,
+ GSList **out_freebusy,
+ GError **error)
{
ECalBackendCalDAV *cbdav;
- ESourceAuthenticationResult result;
- GError *local_error = NULL;
- cbdav = E_CAL_BACKEND_CALDAV (backend);
+ g_return_if_fail (E_IS_CAL_BACKEND_CALDAV (sync_backend));
+ g_return_if_fail (out_freebusy != NULL);
- g_mutex_lock (&cbdav->priv->busy_lock);
+ cbdav = E_CAL_BACKEND_CALDAV (sync_backend);
- e_named_parameters_free (cbdav->priv->credentials);
- cbdav->priv->credentials = e_named_parameters_new_clone (credentials);
-
- open_calendar_wrapper (cbdav, cancellable, &local_error, FALSE, NULL, out_certificate_pem,
out_certificate_errors);
-
- if (local_error == NULL) {
- result = E_SOURCE_AUTHENTICATION_ACCEPTED;
+ if (e_backend_get_online (E_BACKEND (cbdav)) &&
+ cbdav->priv->webdav) {
+ const GSList *link;
+ GError *local_error = NULL;
- update_slave_cmd (cbdav->priv, SLAVE_SHOULD_WORK);
- g_cond_signal (&cbdav->priv->cond);
- } else if (g_error_matches (local_error, E_DATA_CAL_ERROR, AuthenticationFailed) ||
- g_error_matches (local_error, E_DATA_CAL_ERROR, AuthenticationRequired)) {
- const gchar *username;
- gchar *auth_user = NULL;
+ if (ecb_caldav_get_free_busy_from_schedule_outbox_sync (cbdav, users, start, end,
out_freebusy, cancellable, &local_error))
+ return;
- username = e_named_parameters_get (cbdav->priv->credentials, E_SOURCE_CREDENTIAL_USERNAME);
+ g_clear_error (&local_error);
- if (!username || !*username) {
- ESource *source;
- ESourceAuthentication *auth_extension;
+ if (g_cancellable_set_error_if_cancelled (cancellable, error))
+ return;
- source = e_backend_get_source (backend);
- auth_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
- auth_user = e_source_authentication_dup_user (auth_extension);
+ *out_freebusy = NULL;
- username = auth_user;
+ for (link = users; link && !g_cancellable_is_cancelled (cancellable); link = g_slist_next
(link)) {
+ if (!ecb_caldav_get_free_busy_from_principal_sync (cbdav, link->data, start, end,
out_freebusy, cancellable, &local_error))
+ g_clear_error (&local_error);
}
- if (username && *username) {
- if (!cbdav->priv->using_bearer_auth &&
- !e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_PASSWORD))
- result = E_SOURCE_AUTHENTICATION_REQUIRED;
- else
- result = E_SOURCE_AUTHENTICATION_REJECTED;
- g_clear_error (&local_error);
- } else {
- result = E_SOURCE_AUTHENTICATION_ERROR;
- g_propagate_error (error, local_error);
- }
+ g_clear_error (&local_error);
- g_free (auth_user);
- } else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
- result = E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED;
- g_propagate_error (error, local_error);
- } else {
- result = E_SOURCE_AUTHENTICATION_ERROR;
- g_propagate_error (error, local_error);
+ if (*out_freebusy || g_cancellable_set_error_if_cancelled (cancellable, error))
+ return;
}
- g_mutex_unlock (&cbdav->priv->busy_lock);
-
- return result;
+ /* Chain up to parent's method. */
+ E_CAL_BACKEND_SYNC_CLASS (e_cal_backend_caldav_parent_class)->get_free_busy_sync (sync_backend, cal,
cancellable,
+ users, start, end, out_freebusy, error);
}
-/* ************************************************************************* */
-/* ***************************** GObject Foo ******************************* */
-
-static void
-e_cal_backend_caldav_dispose (GObject *object)
+static gchar *
+ecb_caldav_get_backend_property (ECalBackend *backend,
+ const gchar *prop_name)
{
- ECalBackendCalDAVPrivate *priv;
-
- priv = E_CAL_BACKEND_CALDAV_GET_PRIVATE (object);
+ g_return_val_if_fail (prop_name != NULL, NULL);
- g_clear_object (&priv->store);
- g_clear_object (&priv->session);
- g_clear_object (&priv->using_bearer_auth);
+ if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
+ ESourceWebdav *extension;
+ ESource *source;
+ GString *caps;
+ gchar *usermail;
+ const gchar *extension_name;
- /* Chain up to parent's dispose() method. */
- G_OBJECT_CLASS (parent_class)->dispose (object);
-}
+ caps = g_string_new (
+ CAL_STATIC_CAPABILITY_NO_THISANDPRIOR ","
+ CAL_STATIC_CAPABILITY_REFRESH_SUPPORTED);
+ g_string_append (caps, ",");
+ g_string_append (caps, e_cal_meta_backend_get_capabilities (E_CAL_META_BACKEND (backend)));
-static void
-e_cal_backend_caldav_finalize (GObject *object)
-{
- ECalBackendCalDAVPrivate *priv;
+ usermail = ecb_caldav_get_usermail (E_CAL_BACKEND_CALDAV (backend));
+ if (!usermail || !*usermail)
+ g_string_append (caps, "," CAL_STATIC_CAPABILITY_NO_EMAIL_ALARMS);
+ g_free (usermail);
- priv = E_CAL_BACKEND_CALDAV_GET_PRIVATE (object);
+ source = e_backend_get_source (E_BACKEND (backend));
- g_mutex_clear (&priv->busy_lock);
- g_cond_clear (&priv->cond);
- g_cond_clear (&priv->slave_gone_cond);
+ extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
+ extension = e_source_get_extension (source, extension_name);
- g_free (priv->uri);
- e_named_parameters_free (priv->credentials);
- priv->credentials = NULL;
- g_free (priv->schedule_outbox_url);
+ if (e_source_webdav_get_calendar_auto_schedule (extension)) {
+ g_string_append (
+ caps,
+ "," CAL_STATIC_CAPABILITY_CREATE_MESSAGES
+ "," CAL_STATIC_CAPABILITY_SAVE_SCHEDULES);
+ }
- if (priv->ctag_to_store) {
- g_free (priv->ctag_to_store);
- priv->ctag_to_store = NULL;
+ return g_string_free (caps, FALSE);
+ } else if (g_str_equal (prop_name, CAL_BACKEND_PROPERTY_CAL_EMAIL_ADDRESS) ||
+ g_str_equal (prop_name, CAL_BACKEND_PROPERTY_ALARM_EMAIL_ADDRESS)) {
+ return ecb_caldav_get_usermail (E_CAL_BACKEND_CALDAV (backend));
}
- g_clear_error (&priv->bearer_auth_error);
- g_mutex_clear (&priv->bearer_auth_error_lock);
-
- /* Chain up to parent's finalize() method. */
- G_OBJECT_CLASS (parent_class)->finalize (object);
+ /* Chain up to parent's method. */
+ return E_CAL_BACKEND_CLASS (e_cal_backend_caldav_parent_class)->get_backend_property (backend,
prop_name);
}
-static gboolean
-caldav_backend_initable_init (GInitable *initable,
- GCancellable *cancellable,
- GError **error)
+static gchar *
+ecb_caldav_dup_component_revision_cb (ECalCache *cal_cache,
+ icalcomponent *icalcomp)
{
- ECalBackendCalDAVPrivate *priv;
-
- priv = E_CAL_BACKEND_CALDAV_GET_PRIVATE (initable);
-
- g_mutex_init (&priv->bearer_auth_error_lock);
+ g_return_val_if_fail (icalcomp != NULL, NULL);
- return TRUE;
+ return e_cal_util_dup_x_property (icalcomp, E_CALDAV_X_ETAG);
}
static void
-e_cal_backend_caldav_init (ECalBackendCalDAV *cbdav)
+e_cal_backend_caldav_constructed (GObject *object)
{
- cbdav->priv = E_CAL_BACKEND_CALDAV_GET_PRIVATE (cbdav);
- cbdav->priv->session = soup_session_sync_new ();
- g_object_set (
- cbdav->priv->session,
- SOUP_SESSION_TIMEOUT, 90,
- SOUP_SESSION_SSL_STRICT, TRUE,
- SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, TRUE,
- SOUP_SESSION_ACCEPT_LANGUAGE_AUTO, TRUE,
- NULL);
+ ECalBackendCalDAV *cbdav = E_CAL_BACKEND_CALDAV (object);
+ ECalCache *cal_cache;
- e_binding_bind_property (
- cbdav, "proxy-resolver",
- cbdav->priv->session, "proxy-resolver",
- G_BINDING_SYNC_CREATE);
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_cal_backend_caldav_parent_class)->constructed (object);
- if (G_UNLIKELY (caldav_debug_show (DEBUG_MESSAGE)))
- caldav_debug_setup (cbdav->priv->session);
+ cal_cache = e_cal_meta_backend_ref_cache (E_CAL_META_BACKEND (cbdav));
- cbdav->priv->loaded = FALSE;
- cbdav->priv->opened = FALSE;
+ g_signal_connect (cal_cache, "dup-component-revision",
+ G_CALLBACK (ecb_caldav_dup_component_revision_cb), NULL);
- /* Thinks the 'getctag' extension is available the first time, but unset it when realizes it isn't. */
- cbdav->priv->ctag_supported = TRUE;
- cbdav->priv->ctag_to_store = NULL;
+ g_clear_object (&cal_cache);
+}
+
+static void
+e_cal_backend_caldav_dispose (GObject *object)
+{
+ ECalBackendCalDAV *cbdav = E_CAL_BACKEND_CALDAV (object);
- cbdav->priv->schedule_outbox_url = NULL;
+ g_clear_object (&cbdav->priv->webdav);
- cbdav->priv->is_google = FALSE;
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_cal_backend_caldav_parent_class)->dispose (object);
+}
- g_mutex_init (&cbdav->priv->busy_lock);
- g_cond_init (&cbdav->priv->cond);
- g_cond_init (&cbdav->priv->slave_gone_cond);
+static void
+e_cal_backend_caldav_finalize (GObject *object)
+{
+ ECalBackendCalDAV *cbdav = E_CAL_BACKEND_CALDAV (object);
- /* Slave control ... */
- cbdav->priv->slave_cmd = SLAVE_SHOULD_SLEEP;
- cbdav->priv->slave_busy = FALSE;
+ g_clear_pointer (&cbdav->priv->schedule_outbox_url, g_free);
- g_signal_connect (
- cbdav->priv->session, "authenticate",
- G_CALLBACK (soup_authenticate), cbdav);
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_cal_backend_caldav_parent_class)->finalize (object);
+}
- g_signal_connect (
- cbdav, "notify::online",
- G_CALLBACK (caldav_notify_online_cb), NULL);
+static void
+e_cal_backend_caldav_init (ECalBackendCalDAV *cbdav)
+{
+ cbdav->priv = G_TYPE_INSTANCE_GET_PRIVATE (cbdav, E_TYPE_CAL_BACKEND_CALDAV,
ECalBackendCalDAVPrivate);
}
static void
-e_cal_backend_caldav_class_init (ECalBackendCalDAVClass *class)
+e_cal_backend_caldav_class_init (ECalBackendCalDAVClass *klass)
{
GObjectClass *object_class;
- EBackendClass *backend_class;
ECalBackendClass *cal_backend_class;
- ECalBackendSyncClass *sync_class;
+ ECalBackendSyncClass *cal_backend_sync_class;
+ ECalMetaBackendClass *cal_meta_backend_class;
+
+ g_type_class_add_private (klass, sizeof (ECalBackendCalDAVPrivate));
- object_class = G_OBJECT_CLASS (class);
- backend_class = E_BACKEND_CLASS (class);
- cal_backend_class = E_CAL_BACKEND_CLASS (class);
- sync_class = E_CAL_BACKEND_SYNC_CLASS (class);
+ cal_meta_backend_class = E_CAL_META_BACKEND_CLASS (klass);
+ cal_meta_backend_class->connect_sync = ecb_caldav_connect_sync;
+ cal_meta_backend_class->disconnect_sync = ecb_caldav_disconnect_sync;
+ cal_meta_backend_class->get_changes_sync = ecb_caldav_get_changes_sync;
+ cal_meta_backend_class->list_existing_sync = ecb_caldav_list_existing_sync;
+ cal_meta_backend_class->load_component_sync = ecb_caldav_load_component_sync;
+ cal_meta_backend_class->save_component_sync = ecb_caldav_save_component_sync;
+ cal_meta_backend_class->remove_component_sync = ecb_caldav_remove_component_sync;
- caldav_debug_init ();
+ cal_backend_sync_class = E_CAL_BACKEND_SYNC_CLASS (klass);
+ cal_backend_sync_class->get_free_busy_sync = ecb_caldav_get_free_busy_sync;
- parent_class = (ECalBackendSyncClass *) g_type_class_peek_parent (class);
- g_type_class_add_private (class, sizeof (ECalBackendCalDAVPrivate));
+ cal_backend_class = E_CAL_BACKEND_CLASS (klass);
+ cal_backend_class->get_backend_property = ecb_caldav_get_backend_property;
+ object_class = G_OBJECT_CLASS (klass);
+ object_class->constructed = e_cal_backend_caldav_constructed;
object_class->dispose = e_cal_backend_caldav_dispose;
object_class->finalize = e_cal_backend_caldav_finalize;
-
- backend_class->authenticate_sync = caldav_authenticate_sync;
-
- cal_backend_class->get_backend_property = caldav_get_backend_property;
- cal_backend_class->shutdown = caldav_shutdown;
- cal_backend_class->start_view = caldav_start_view;
-
- sync_class->open_sync = caldav_do_open;
- sync_class->refresh_sync = caldav_refresh;
-
- sync_class->create_objects_sync = caldav_create_objects;
- sync_class->modify_objects_sync = caldav_modify_objects;
- sync_class->remove_objects_sync = caldav_remove_objects;
-
- sync_class->receive_objects_sync = caldav_receive_objects;
- sync_class->send_objects_sync = caldav_send_objects;
- sync_class->get_object_sync = caldav_get_object;
- sync_class->get_object_list_sync = caldav_get_object_list;
- sync_class->add_timezone_sync = caldav_add_timezone;
- sync_class->get_free_busy_sync = caldav_get_free_busy;
}
-
-static void
-e_caldav_backend_initable_init (GInitableIface *interface)
-{
- interface->init = caldav_backend_initable_init;
-}
-
diff --git a/src/calendar/backends/caldav/e-cal-backend-caldav.h
b/src/calendar/backends/caldav/e-cal-backend-caldav.h
index 3ea3273..3a8b639 100644
--- a/src/calendar/backends/caldav/e-cal-backend-caldav.h
+++ b/src/calendar/backends/caldav/e-cal-backend-caldav.h
@@ -47,12 +47,12 @@ typedef struct _ECalBackendCalDAVClass ECalBackendCalDAVClass;
typedef struct _ECalBackendCalDAVPrivate ECalBackendCalDAVPrivate;
struct _ECalBackendCalDAV {
- ECalBackendSync parent;
+ ECalMetaBackend parent;
ECalBackendCalDAVPrivate *priv;
};
struct _ECalBackendCalDAVClass {
- ECalBackendSyncClass parent_class;
+ ECalMetaBackendClass parent_class;
};
GType e_cal_backend_caldav_get_type (void);
diff --git a/src/calendar/backends/gtasks/e-cal-backend-gtasks.c
b/src/calendar/backends/gtasks/e-cal-backend-gtasks.c
index 51f5372..fd2e964 100644
--- a/src/calendar/backends/gtasks/e-cal-backend-gtasks.c
+++ b/src/calendar/backends/gtasks/e-cal-backend-gtasks.c
@@ -26,238 +26,50 @@
#define d(x)
-#define E_CAL_BACKEND_GTASKS_GET_PRIVATE(obj) \
- (G_TYPE_INSTANCE_GET_PRIVATE \
- ((obj), E_TYPE_CAL_BACKEND_GTASKS, ECalBackendGTasksPrivate))
-
#define EDC_ERROR(_code) e_data_cal_create_error (_code, NULL)
#define EDC_ERROR_EX(_code, _msg) e_data_cal_create_error (_code, _msg)
-#define GTASKS_KEY_LAST_UPDATED "last-updated"
-#define GTASKS_KEY_VERSION "version"
+#define GTASKS_DEFAULT_TASKLIST_NAME "@default"
#define X_EVO_GTASKS_SELF_LINK "X-EVOLUTION-GTASKS-SELF-LINK"
/* Current data version; when doesn't match with the stored,
then fetches everything again. */
-#define GTASKS_DATA_VERSION "1"
-
-#define PROPERTY_LOCK(_gtasks) g_mutex_lock (&(_gtasks)->priv->property_mutex)
-#define PROPERTY_UNLOCK(_gtasks) g_mutex_unlock (&(_gtasks)->priv->property_mutex)
+#define GTASKS_DATA_VERSION 1
+#define GTASKS_DATA_VERSION_KEY "gtasks-data-version"
struct _ECalBackendGTasksPrivate {
GDataAuthorizer *authorizer;
GDataTasksService *service;
GDataTasksTasklist *tasklist;
-
- ECalBackendStore *store;
- GCancellable *cancellable;
- GMutex property_mutex;
-
- guint refresh_id;
+ GHashTable *preloaded; /* gchar *uid ~> ECalComponent * */
};
-G_DEFINE_TYPE (ECalBackendGTasks, e_cal_backend_gtasks, E_TYPE_CAL_BACKEND)
+G_DEFINE_TYPE (ECalBackendGTasks, e_cal_backend_gtasks, E_TYPE_CAL_META_BACKEND)
static gboolean
-ecb_gtasks_check_data_version_locked (ECalBackendGTasks *gtasks)
+ecb_gtasks_check_data_version (ECalCache *cal_cache)
{
#ifdef HAVE_LIBGDATA_TASKS_PAGINATION_FUNCTIONS
- const gchar *key;
- gboolean data_version_correct;
-
- g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (gtasks), FALSE);
-
- key = e_cal_backend_store_get_key_value (gtasks->priv->store, GTASKS_KEY_VERSION);
- data_version_correct = g_strcmp0 (key, GTASKS_DATA_VERSION) == 0;
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
- return data_version_correct;
+ return GTASKS_DATA_VERSION == e_cache_get_key_int (E_CACHE (cal_cache), GTASKS_DATA_VERSION_KEY,
NULL);
#else
return TRUE;
#endif
}
static void
-ecb_gtasks_store_data_version_locked (ECalBackendGTasks *gtasks)
+ecb_gtasks_store_data_version (ECalCache *cal_cache)
{
#ifdef HAVE_LIBGDATA_TASKS_PAGINATION_FUNCTIONS
- e_cal_backend_store_put_key_value (gtasks->priv->store, GTASKS_KEY_VERSION, GTASKS_DATA_VERSION);
-#endif
-}
-
-static GCancellable *
-ecb_gtasks_ref_cancellable (ECalBackendGTasks *gtasks)
-{
- GCancellable *cancellable = NULL;
-
- g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (gtasks), NULL);
-
- PROPERTY_LOCK (gtasks);
-
- if (gtasks->priv->cancellable)
- cancellable = g_object_ref (gtasks->priv->cancellable);
-
- PROPERTY_UNLOCK (gtasks);
-
- return cancellable;
-}
-
-static void
-ecb_gtasks_take_cancellable (ECalBackendGTasks *gtasks,
- GCancellable *cancellable)
-{
- GCancellable *old_cancellable;
-
- g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (gtasks));
-
- PROPERTY_LOCK (gtasks);
-
- old_cancellable = gtasks->priv->cancellable;
- gtasks->priv->cancellable = cancellable;
-
- PROPERTY_UNLOCK (gtasks);
-
- if (old_cancellable) {
- g_cancellable_cancel (old_cancellable);
- g_clear_object (&old_cancellable);
- }
-}
-
-static void
-ecb_gtasks_icomp_x_prop_set (icalcomponent *comp,
- const gchar *key,
- const gchar *value)
-{
- icalproperty *xprop;
-
- /* Find the old one first */
- xprop = icalcomponent_get_first_property (comp, ICAL_X_PROPERTY);
-
- while (xprop) {
- const gchar *str = icalproperty_get_x_name (xprop);
-
- if (!strcmp (str, key)) {
- if (value) {
- icalproperty_set_value_from_string (xprop, value, "NO");
- } else {
- icalcomponent_remove_property (comp, xprop);
- icalproperty_free (xprop);
- }
- break;
- }
-
- xprop = icalcomponent_get_next_property (comp, ICAL_X_PROPERTY);
- }
-
- if (!xprop && value) {
- xprop = icalproperty_new_x (value);
- icalproperty_set_x_name (xprop, key);
- icalcomponent_add_property (comp, xprop);
- }
-}
-
-static gchar *
-ecb_gtasks_icomp_x_prop_get (icalcomponent *comp,
- const gchar *key)
-{
- icalproperty *xprop;
-
- /* Find the old one first */
- xprop = icalcomponent_get_first_property (comp, ICAL_X_PROPERTY);
-
- while (xprop) {
- const gchar *str = icalproperty_get_x_name (xprop);
-
- if (!strcmp (str, key)) {
- break;
- }
-
- xprop = icalcomponent_get_next_property (comp, ICAL_X_PROPERTY);
- }
-
- if (xprop) {
- return icalproperty_get_value_as_string_r (xprop);
- }
-
- return NULL;
-}
-
-/* May hold PROPERTY_LOCK() when calling this */
-static ECalComponent *
-ecb_gtasks_get_cached_comp (ECalBackendGTasks *gtasks,
- const gchar *uid)
-{
- g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (gtasks), NULL);
- g_return_val_if_fail (uid != NULL, NULL);
-
- return e_cal_backend_store_get_component (gtasks->priv->store, uid, NULL);
-}
-
-static gboolean
-ecb_gtasks_is_authorized (ECalBackend *backend)
-{
- ECalBackendGTasks *gtasks = E_CAL_BACKEND_GTASKS (backend);
-
- if (!gtasks->priv->service ||
- !gtasks->priv->tasklist)
- return FALSE;
-
- return gdata_service_is_authorized (GDATA_SERVICE (gtasks->priv->service));
-}
-
-static void
-ecb_gtasks_prepare_tasklist (ECalBackendGTasks *gtasks,
- GCancellable *cancellable,
- GError **error)
-{
- ESourceResource *resource;
- ESource *source;
- GDataFeed *feed;
- GDataQuery *query;
- gchar *id;
-
- g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (gtasks));
- g_return_if_fail (gtasks->priv->service != NULL);
- g_return_if_fail (gdata_service_is_authorized (GDATA_SERVICE (gtasks->priv->service)));
-
- source = e_backend_get_source (E_BACKEND (gtasks));
- resource = e_source_get_extension (source, E_SOURCE_EXTENSION_RESOURCE);
- id = e_source_resource_dup_identity (resource);
-
- query = gdata_query_new_with_limits (NULL, 0, 1);
- /* This also verifies that the service can connect to the server with given credentials */
- feed = gdata_tasks_service_query_all_tasklists (gtasks->priv->service, query, cancellable, NULL,
NULL, error);
- if (feed) {
- /* If the tasklist ID is not set, then pick the first from the list, most likely the "Default
List" */
- if (!id || !*id) {
- GList *entries;
+ GError *local_error = NULL;
- entries = gdata_feed_get_entries (feed);
- if (entries) {
- GDataEntry *entry = entries->data;
- if (entry) {
- g_free (id);
- id = g_strdup (gdata_entry_get_id (entry));
- }
- }
- }
- }
- g_clear_object (&feed);
- g_object_unref (query);
+ g_return_if_fail (E_IS_CAL_CACHE (cal_cache));
- if (!id || !*id) {
- /* But the tests for change will not work */
- g_free (id);
- id = g_strdup ("@default");
+ if (!e_cache_set_key_int (E_CACHE (cal_cache), GTASKS_DATA_VERSION_KEY, GTASKS_DATA_VERSION,
&local_error)) {
+ g_warning ("%s: Failed to store data version: %s\n", G_STRFUNC, local_error ?
local_error->message : "Unknown error");
}
-
- g_clear_object (>asks->priv->tasklist);
-
- if (g_str_has_prefix (id, "gtasks::"))
- gtasks->priv->tasklist = gdata_tasks_tasklist_new (id + 8);
- else
- gtasks->priv->tasklist = gdata_tasks_tasklist_new (id);
-
- g_free (id);
+#endif
}
static void
@@ -348,7 +160,7 @@ ecb_gtasks_gdata_to_comp (GDataTasksTask *task)
data_link = gdata_entry_look_up_link (entry, GDATA_LINK_SELF);
if (data_link)
- ecb_gtasks_icomp_x_prop_set (icomp, X_EVO_GTASKS_SELF_LINK, gdata_link_get_uri (data_link));
+ e_cal_util_set_x_property (icomp, X_EVO_GTASKS_SELF_LINK, gdata_link_get_uri (data_link));
comp = e_cal_component_new_from_icalcomponent (icomp);
g_warn_if_fail (comp != NULL);
@@ -358,7 +170,8 @@ ecb_gtasks_gdata_to_comp (GDataTasksTask *task)
static GDataTasksTask *
ecb_gtasks_comp_to_gdata (ECalComponent *comp,
- ECalComponent *cached_comp)
+ ECalComponent *cached_comp,
+ gboolean ignore_uid)
{
GDataEntry *entry;
GDataTasksTask *task;
@@ -374,7 +187,7 @@ ecb_gtasks_comp_to_gdata (ECalComponent *comp,
g_return_val_if_fail (icomp != NULL, NULL);
text = icalcomponent_get_uid (icomp);
- task = gdata_tasks_task_new (text && *text ? text : NULL);
+ task = gdata_tasks_task_new ((!ignore_uid && text && *text) ? text : NULL);
entry = GDATA_ENTRY (task);
tt = icalcomponent_get_due (icomp);
@@ -412,7 +225,7 @@ ecb_gtasks_comp_to_gdata (ECalComponent *comp,
else if (icalcomponent_get_status (icomp) == ICAL_STATUS_NEEDSACTION)
gdata_tasks_task_set_status (task, "needsAction");
- tmp = ecb_gtasks_icomp_x_prop_get (icomp, X_EVO_GTASKS_SELF_LINK);
+ tmp = e_cal_util_dup_x_property (icomp, X_EVO_GTASKS_SELF_LINK);
if (!tmp || !*tmp) {
g_free (tmp);
tmp = NULL;
@@ -420,7 +233,7 @@ ecb_gtasks_comp_to_gdata (ECalComponent *comp,
/* If the passed-in component doesn't contain the libgdata self link,
then get it from the cached comp */
if (cached_comp) {
- tmp = ecb_gtasks_icomp_x_prop_get (
+ tmp = e_cal_util_dup_x_property (
e_cal_component_get_icalcomponent (cached_comp),
X_EVO_GTASKS_SELF_LINK);
}
@@ -439,351 +252,146 @@ ecb_gtasks_comp_to_gdata (ECalComponent *comp,
return task;
}
-struct EGTasksUpdateData
+static gboolean
+ecb_gtasks_is_authorized (ECalBackendGTasks *cbgtasks)
{
- ECalBackendGTasks *gtasks;
- gint64 taskslist_time;
-};
+ g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (cbgtasks), FALSE);
-static gpointer
-ecb_gtasks_update_thread (gpointer user_data)
-{
- struct EGTasksUpdateData *update_data = user_data;
- ECalBackendGTasks *gtasks;
- GTimeVal last_updated;
- GDataFeed *feed;
- GDataTasksQuery *tasks_query;
- const gchar *key;
- GCancellable *cancellable;
- GError *local_error = NULL;
-
- g_return_val_if_fail (update_data != NULL, NULL);
-
- gtasks = update_data->gtasks;
-
- g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (gtasks), NULL);
-
- if (!ecb_gtasks_is_authorized (E_CAL_BACKEND (gtasks))) {
- g_clear_object (>asks);
- g_free (update_data);
- return NULL;
- }
-
- PROPERTY_LOCK (gtasks);
+ if (!cbgtasks->priv->service ||
+ !cbgtasks->priv->tasklist)
+ return FALSE;
- if (ecb_gtasks_check_data_version_locked (gtasks)) {
- key = e_cal_backend_store_get_key_value (gtasks->priv->store, GTASKS_KEY_LAST_UPDATED);
- if (!key || !g_time_val_from_iso8601 (key, &last_updated))
- last_updated.tv_sec = 0;
- } else {
- last_updated.tv_sec = 0;
- }
+ return gdata_service_is_authorized (GDATA_SERVICE (cbgtasks->priv->service));
+}
- PROPERTY_UNLOCK (gtasks);
+static gboolean
+ecb_gtasks_request_authorization (ECalBackendGTasks *cbgtasks,
+ const ENamedParameters *credentials,
+ GCancellable *cancellable,
+ GError **error)
+{
+ /* Make sure we have the GDataService configured
+ * before requesting authorization. */
- cancellable = ecb_gtasks_ref_cancellable (gtasks);
+ if (!cbgtasks->priv->authorizer) {
+ ESource *source;
+ EGDataOAuth2Authorizer *authorizer;
- tasks_query = gdata_tasks_query_new (NULL);
- gdata_query_set_max_results (GDATA_QUERY (tasks_query), 100);
- gdata_tasks_query_set_show_completed (tasks_query, TRUE);
- gdata_tasks_query_set_show_hidden (tasks_query, TRUE);
+ source = e_backend_get_source (E_BACKEND (cbgtasks));
- if (last_updated.tv_sec > 0) {
- gdata_query_set_updated_min (GDATA_QUERY (tasks_query), last_updated.tv_sec);
- gdata_tasks_query_set_show_deleted (tasks_query, TRUE);
+ /* Only OAuth2 is supported with Google Tasks */
+ authorizer = e_gdata_oauth2_authorizer_new (source);
+ cbgtasks->priv->authorizer = GDATA_AUTHORIZER (authorizer);
}
- feed = gdata_tasks_service_query_tasks (gtasks->priv->service, gtasks->priv->tasklist,
- GDATA_QUERY (tasks_query), cancellable, NULL, NULL, &local_error);
-
- if (!local_error)
- e_backend_ensure_source_status_connected (E_BACKEND (gtasks));
-
-#ifdef HAVE_LIBGDATA_TASKS_PAGINATION_FUNCTIONS
- while (feed && !g_cancellable_is_cancelled (cancellable) && !local_error) {
-#else
- if (feed) {
-#endif
- GList *link;
- const gchar *uid;
-
- PROPERTY_LOCK (gtasks);
-
- e_cal_backend_store_freeze_changes (gtasks->priv->store);
-
- for (link = gdata_feed_get_entries (feed); link; link = g_list_next (link)) {
- GDataTasksTask *task = link->data;
- ECalComponent *cached_comp;
-
- if (!GDATA_IS_TASKS_TASK (task))
- continue;
-
- uid = gdata_entry_get_id (GDATA_ENTRY (task));
- if (!uid || !*uid)
- continue;
-
- cached_comp = ecb_gtasks_get_cached_comp (gtasks, uid);
-
- if (gdata_tasks_task_is_deleted (task)) {
- ECalComponentId id;
-
- id.uid = (gchar *) uid;
- id.rid = NULL;
-
- e_cal_backend_notify_component_removed ((ECalBackend *) gtasks, &id,
cached_comp, NULL);
- if (cached_comp)
- e_cal_backend_store_remove_component (gtasks->priv->store, uid, NULL);
- } else {
- ECalComponent *new_comp;
-
- new_comp = ecb_gtasks_gdata_to_comp (task);
- if (new_comp) {
- if (cached_comp) {
- struct icaltimetype *cached_tt = NULL, *new_tt = NULL;
-
- e_cal_component_get_last_modified (cached_comp, &cached_tt);
- e_cal_component_get_last_modified (new_comp, &new_tt);
-
- if (!cached_tt || !new_tt ||
- icaltime_compare (*cached_tt, *new_tt) != 0) {
- /* Google doesn't store/provide 'created', thus use
'created,
- as first seen by the backend' */
- if (cached_tt)
- e_cal_component_set_created (new_comp,
cached_tt);
-
- e_cal_backend_store_put_component
(gtasks->priv->store, new_comp);
- e_cal_backend_notify_component_modified ((ECalBackend
*) gtasks, cached_comp, new_comp);
- }
-
- if (cached_tt)
- e_cal_component_free_icaltimetype (cached_tt);
- if (new_tt)
- e_cal_component_free_icaltimetype (new_tt);
- } else {
- e_cal_backend_store_put_component (gtasks->priv->store,
new_comp);
- e_cal_backend_notify_component_created ((ECalBackend *)
gtasks, new_comp);
- }
- }
-
- g_clear_object (&new_comp);
- }
-
- g_clear_object (&cached_comp);
- }
-
- e_cal_backend_store_thaw_changes (gtasks->priv->store);
-
- PROPERTY_UNLOCK (gtasks);
-
-#ifdef HAVE_LIBGDATA_TASKS_PAGINATION_FUNCTIONS
- if (!gdata_feed_get_entries (feed))
- break;
-
- gdata_query_next_page (GDATA_QUERY (tasks_query));
-
- g_clear_object (&feed);
-
- feed = gdata_tasks_service_query_tasks (gtasks->priv->service, gtasks->priv->tasklist,
- GDATA_QUERY (tasks_query), cancellable, NULL, NULL, &local_error);
-#endif
+ if (E_IS_GDATA_OAUTH2_AUTHORIZER (cbgtasks->priv->authorizer)) {
+ e_gdata_oauth2_authorizer_set_credentials (E_GDATA_OAUTH2_AUTHORIZER
(cbgtasks->priv->authorizer), credentials);
}
- g_clear_object (&tasks_query);
- g_clear_object (&feed);
-
- if (!g_cancellable_is_cancelled (cancellable) && !local_error) {
- gchar *strtm;
-
- PROPERTY_LOCK (gtasks);
-
- last_updated.tv_sec = update_data->taskslist_time;
- last_updated.tv_usec = 0;
-
- strtm = g_time_val_to_iso8601 (&last_updated);
- e_cal_backend_store_put_key_value (gtasks->priv->store, GTASKS_KEY_LAST_UPDATED, strtm);
- g_free (strtm);
+ if (!cbgtasks->priv->service) {
+ GDataTasksService *tasks_service;
- ecb_gtasks_store_data_version_locked (gtasks);
+ tasks_service = gdata_tasks_service_new (cbgtasks->priv->authorizer);
+ cbgtasks->priv->service = tasks_service;
- PROPERTY_UNLOCK (gtasks);
+ e_binding_bind_property (
+ cbgtasks, "proxy-resolver",
+ cbgtasks->priv->service, "proxy-resolver",
+ G_BINDING_SYNC_CREATE);
}
- g_clear_object (&cancellable);
- g_clear_object (>asks);
- g_clear_error (&local_error);
- g_free (update_data);
+ /* If we're using OAuth tokens, then as far as the backend
+ * is concerned it's always authorized. The GDataAuthorizer
+ * will take care of everything in the background. */
+ if (!GDATA_IS_CLIENT_LOGIN_AUTHORIZER (cbgtasks->priv->authorizer))
+ return TRUE;
- return NULL;
+ /* Otherwise it's up to us to obtain a login secret, but
+ there is currently no way to do it, thus simply fail. */
+ return FALSE;
}
-static void
-ecb_gtasks_start_update (ECalBackendGTasks *gtasks)
+static gboolean
+ecb_gtasks_prepare_tasklist (ECalBackendGTasks *cbgtasks,
+ GCancellable *cancellable,
+ GError **error)
{
+ ESourceResource *resource;
+ ESource *source;
GDataFeed *feed;
- GCancellable *cancellable;
+ GDataQuery *query;
+ gchar *id;
GError *local_error = NULL;
- gchar *id = NULL;
- gint64 taskslist_time = 0;
- gboolean changed = TRUE;
-
- g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (gtasks));
-
- if (!ecb_gtasks_is_authorized ((ECalBackend *) gtasks))
- return;
- cancellable = ecb_gtasks_ref_cancellable (gtasks);
- g_return_if_fail (cancellable != NULL);
+ g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (cbgtasks), FALSE);
+ g_return_val_if_fail (cbgtasks->priv->service != NULL, FALSE);
+ g_return_val_if_fail (gdata_service_is_authorized (GDATA_SERVICE (cbgtasks->priv->service)), FALSE);
- g_object_get (gtasks->priv->tasklist, "id", &id, NULL);
- g_return_if_fail (id != NULL);
-
- /* Check whether the tasklist changed */
- feed = gdata_tasks_service_query_all_tasklists (gtasks->priv->service, NULL, cancellable, NULL, NULL,
&local_error);
+ source = e_backend_get_source (E_BACKEND (cbgtasks));
+ resource = e_source_get_extension (source, E_SOURCE_EXTENSION_RESOURCE);
+ id = e_source_resource_dup_identity (resource);
- if (!local_error)
- e_backend_ensure_source_status_connected (E_BACKEND (gtasks));
+ query = gdata_query_new_with_limits (NULL, 0, 1);
+ /* This also verifies that the service can connect to the server with given credentials */
+ feed = gdata_tasks_service_query_all_tasklists (cbgtasks->priv->service, query, cancellable, NULL,
NULL, &local_error);
if (feed) {
- GList *link;
-
- for (link = gdata_feed_get_entries (feed); link; link = g_list_next (link)) {
- GDataEntry *entry = link->data;
-
- if (entry && g_strcmp0 (id, gdata_entry_get_id (entry)) == 0) {
- taskslist_time = gdata_entry_get_updated (entry);
-
- if (taskslist_time > 0) {
- PROPERTY_LOCK (gtasks);
-
- if (ecb_gtasks_check_data_version_locked (gtasks)) {
- GTimeVal stored;
- const gchar *key;
-
- key = e_cal_backend_store_get_key_value (gtasks->priv->store,
GTASKS_KEY_LAST_UPDATED);
- if (key && g_time_val_from_iso8601 (key, &stored))
- changed = taskslist_time != stored.tv_sec;
- }
+ /* If the tasklist ID is not set, then pick the first from the list, most likely the "Default
List" */
+ if (!id || !*id) {
+ GList *entries;
- PROPERTY_UNLOCK (gtasks);
+ entries = gdata_feed_get_entries (feed);
+ if (entries) {
+ GDataEntry *entry = entries->data;
+ if (entry) {
+ g_free (id);
+ id = g_strdup (gdata_entry_get_id (entry));
}
-
- break;
}
}
-
- g_clear_object (&feed);
}
+ g_clear_object (&feed);
+ g_object_unref (query);
- if (changed && !g_cancellable_is_cancelled (cancellable)) {
- GThread *thread;
- struct EGTasksUpdateData *data;
-
- data = g_new0 (struct EGTasksUpdateData, 1);
- data->gtasks = g_object_ref (gtasks);
- data->taskslist_time = taskslist_time;
-
- thread = g_thread_new (NULL, ecb_gtasks_update_thread, data);
- g_thread_unref (thread);
+ if (!id || !*id) {
+ /* But the tests for change will not work */
+ g_free (id);
+ id = g_strdup (GTASKS_DEFAULT_TASKLIST_NAME);
}
- if (local_error) {
- g_debug ("%s: Failed to get all tasklists: %s", G_STRFUNC, local_error->message);
- g_clear_error (&local_error);
- }
+ g_clear_object (&cbgtasks->priv->tasklist);
+ cbgtasks->priv->tasklist = gdata_tasks_tasklist_new (id);
- g_clear_object (&cancellable);
g_free (id);
-}
-
-static void
-ecb_gtasks_time_to_refresh_data_cb (ESource *source,
- gpointer user_data)
-{
- ECalBackendGTasks *gtasks = user_data;
- g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (gtasks));
-
- if (!ecb_gtasks_is_authorized (E_CAL_BACKEND (gtasks)) ||
- !e_backend_get_online (E_BACKEND (gtasks))) {
- return;
- }
-
- ecb_gtasks_start_update (gtasks);
-}
-
-static gboolean
-ecb_gtasks_request_authorization (ECalBackend *backend,
- const ENamedParameters *credentials,
- GCancellable *cancellable,
- GError **error)
-{
- ECalBackendGTasks *gtasks = E_CAL_BACKEND_GTASKS (backend);
-
- /* Make sure we have the GDataService configured
- * before requesting authorization. */
-
- if (!gtasks->priv->authorizer) {
- ESource *source;
- EGDataOAuth2Authorizer *authorizer;
-
- source = e_backend_get_source (E_BACKEND (backend));
-
- /* Only OAuth2 is supported with Google Tasks */
- authorizer = e_gdata_oauth2_authorizer_new (source);
- gtasks->priv->authorizer = GDATA_AUTHORIZER (authorizer);
- }
-
- if (E_IS_GDATA_OAUTH2_AUTHORIZER (gtasks->priv->authorizer)) {
- e_gdata_oauth2_authorizer_set_credentials (E_GDATA_OAUTH2_AUTHORIZER
(gtasks->priv->authorizer), credentials);
- }
-
- if (!gtasks->priv->service) {
- GDataTasksService *tasks_service;
-
- tasks_service = gdata_tasks_service_new (gtasks->priv->authorizer);
- gtasks->priv->service = tasks_service;
-
- e_binding_bind_property (
- backend, "proxy-resolver",
- gtasks->priv->service, "proxy-resolver",
- G_BINDING_SYNC_CREATE);
+ if (local_error) {
+ g_propagate_error (error, local_error);
+ return FALSE;
}
- /* If we're using OAuth tokens, then as far as the backend
- * is concerned it's always authorized. The GDataAuthorizer
- * will take care of everything in the background. */
- if (!GDATA_IS_CLIENT_LOGIN_AUTHORIZER (gtasks->priv->authorizer))
- return TRUE;
-
- /* Otherwise it's up to us to obtain a login secret, but
- there is currently no way to do it, thus simply fail. */
- return FALSE;
+ return TRUE;
}
static gchar *
-ecb_gtasks_get_backend_property (ECalBackend *backend,
+ecb_gtasks_get_backend_property (ECalBackend *cal_backend,
const gchar *prop_name)
{
- g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (backend), NULL);
+ g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (cal_backend), NULL);
g_return_val_if_fail (prop_name != NULL, NULL);
if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
- GString *caps;
-
- caps = g_string_new (
- CAL_STATIC_CAPABILITY_NO_THISANDFUTURE ","
- CAL_STATIC_CAPABILITY_NO_THISANDPRIOR ","
- CAL_STATIC_CAPABILITY_REFRESH_SUPPORTED);
-
- return g_string_free (caps, FALSE);
-
+ return g_strjoin (",",
+ CAL_STATIC_CAPABILITY_NO_THISANDFUTURE,
+ CAL_STATIC_CAPABILITY_NO_THISANDPRIOR,
+ e_cal_meta_backend_get_capabilities (E_CAL_META_BACKEND (cal_backend)),
+ NULL);
} else if (g_str_equal (prop_name, CAL_BACKEND_PROPERTY_CAL_EMAIL_ADDRESS) ||
g_str_equal (prop_name, CAL_BACKEND_PROPERTY_ALARM_EMAIL_ADDRESS)) {
ESourceAuthentication *authentication;
ESource *source;
const gchar *user;
- source = e_backend_get_source (E_BACKEND (backend));
+ source = e_backend_get_source (E_BACKEND (cal_backend));
authentication = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
user = e_source_authentication_get_user (authentication);
@@ -791,821 +399,612 @@ ecb_gtasks_get_backend_property (ECalBackend *backend,
return NULL;
return g_strdup (user);
-
- } else if (g_str_equal (prop_name, CAL_BACKEND_PROPERTY_DEFAULT_OBJECT)) {
- ECalComponent *comp;
- gchar *prop_value;
-
- comp = e_cal_component_new ();
- e_cal_component_set_new_vtype (comp, E_CAL_COMPONENT_TODO);
-
- prop_value = e_cal_component_get_as_string (comp);
-
- g_object_unref (comp);
-
- return prop_value;
}
/* Chain up to parent's method. */
- return E_CAL_BACKEND_CLASS (e_cal_backend_gtasks_parent_class)->get_backend_property (backend,
prop_name);
+ return E_CAL_BACKEND_CLASS (e_cal_backend_gtasks_parent_class)->get_backend_property (cal_backend,
prop_name);
}
-static void
-ecb_gtasks_update_connection_sync (ECalBackendGTasks *gtasks,
- const ENamedParameters *credentials,
- GCancellable *cancellable,
- GError **error)
+static gboolean
+ecb_gtasks_connect_sync (ECalMetaBackend *meta_backend,
+ const ENamedParameters *credentials,
+ ESourceAuthenticationResult *out_auth_result,
+ gchar **out_certificate_pem,
+ GTlsCertificateFlags *out_certificate_errors,
+ GCancellable *cancellable,
+ GError **error)
{
- ECalBackend *backend;
+ ECalBackendGTasks *cbgtasks;
gboolean success;
GError *local_error = NULL;
- g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (gtasks));
+ g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (meta_backend), FALSE);
+ g_return_val_if_fail (out_auth_result != NULL, FALSE);
- backend = E_CAL_BACKEND (gtasks);
+ cbgtasks = E_CAL_BACKEND_GTASKS (meta_backend);
- success = ecb_gtasks_request_authorization (backend, credentials, cancellable, &local_error);
- if (success)
- success = gdata_authorizer_refresh_authorization (gtasks->priv->authorizer, cancellable,
&local_error);
+ *out_auth_result = E_SOURCE_AUTHENTICATION_ACCEPTED;
- if (success) {
- e_cal_backend_set_writable (backend, TRUE);
+ if (ecb_gtasks_is_authorized (cbgtasks))
+ return TRUE;
- ecb_gtasks_prepare_tasklist (gtasks, cancellable, &local_error);
- if (!local_error)
- ecb_gtasks_start_update (gtasks);
- } else {
- e_cal_backend_set_writable (backend, FALSE);
+ success = ecb_gtasks_request_authorization (cbgtasks, credentials, cancellable, &local_error);
+ if (success)
+ success = gdata_authorizer_refresh_authorization (cbgtasks->priv->authorizer, cancellable,
&local_error);
+ if (success)
+ success = ecb_gtasks_prepare_tasklist (cbgtasks, cancellable, &local_error);
+
+ if (!success) {
+ if (g_error_matches (local_error, GDATA_SERVICE_ERROR,
GDATA_SERVICE_ERROR_AUTHENTICATION_REQUIRED)) {
+ if (!e_named_parameters_exists (credentials, E_SOURCE_CREDENTIAL_PASSWORD))
+ *out_auth_result = E_SOURCE_AUTHENTICATION_REQUIRED;
+ else
+ *out_auth_result = E_SOURCE_AUTHENTICATION_REJECTED;
+ g_clear_error (&local_error);
+ } else {
+ *out_auth_result = E_SOURCE_AUTHENTICATION_ERROR;
+ g_propagate_error (error, local_error);
+ }
}
- if (local_error)
- g_propagate_error (error, local_error);
+ return success;
}
-static ESourceAuthenticationResult
-ecb_gtasks_authenticate_sync (EBackend *backend,
- const ENamedParameters *credentials,
- gchar **out_certificate_pem,
- GTlsCertificateFlags *out_certificate_errors,
- GCancellable *cancellable,
- GError **error)
+static gboolean
+ecb_gtasks_disconnect_sync (ECalMetaBackend *meta_backend,
+ GCancellable *cancellable,
+ GError **error)
{
- ECalBackendGTasks *gtasks;
- ESourceAuthenticationResult result;
- GError *local_error = NULL;
+ ECalBackendGTasks *cbgtasks;
- gtasks = E_CAL_BACKEND_GTASKS (backend);
+ g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (meta_backend), FALSE);
- ecb_gtasks_update_connection_sync (gtasks, credentials, cancellable, &local_error);
+ cbgtasks = E_CAL_BACKEND_GTASKS (meta_backend);
- if (local_error == NULL) {
- result = E_SOURCE_AUTHENTICATION_ACCEPTED;
+ g_clear_object (&cbgtasks->priv->service);
+ g_clear_object (&cbgtasks->priv->authorizer);
+ g_clear_object (&cbgtasks->priv->tasklist);
- } else if (g_error_matches (local_error, GDATA_SERVICE_ERROR,
GDATA_SERVICE_ERROR_AUTHENTICATION_REQUIRED)) {
- if (!e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_PASSWORD))
- result = E_SOURCE_AUTHENTICATION_REQUIRED;
- else
- result = E_SOURCE_AUTHENTICATION_REJECTED;
- g_clear_error (&local_error);
- } else {
- result = E_SOURCE_AUTHENTICATION_ERROR;
- g_propagate_error (error, local_error);
- }
-
- return result;
+ return TRUE;
}
-static void
-ecb_gtasks_open (ECalBackend *backend,
- EDataCal *cal,
- guint32 opid,
- GCancellable *cancellable,
- gboolean only_if_exists)
+static gboolean
+ecb_gtasks_check_tasklist_changed_sync (ECalBackendGTasks *cbgtasks,
+ const gchar *last_sync_tag,
+ gboolean *out_changed,
+ gint64 *out_taskslist_time,
+ GCancellable *cancellable,
+ GError **error)
{
- ECalBackendGTasks *gtasks;
+ GDataFeed *feed;
+ gchar *id = NULL;
+ gint64 taskslist_time = 0;
GError *local_error = NULL;
- g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
- g_return_if_fail (E_IS_DATA_CAL (cal));
+ g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (cbgtasks), FALSE);
+ g_return_val_if_fail (out_changed != NULL, FALSE);
+ g_return_val_if_fail (out_taskslist_time != NULL, FALSE);
- if (ecb_gtasks_is_authorized (backend)) {
- e_data_cal_respond_open (cal, opid, NULL);
- return;
- }
+ *out_changed = TRUE;
+ *out_taskslist_time = 0;
- gtasks = E_CAL_BACKEND_GTASKS (backend);
+ g_object_get (cbgtasks->priv->tasklist, "id", &id, NULL);
+ g_return_val_if_fail (id != NULL, FALSE);
- e_cal_backend_set_writable (backend, FALSE);
+ /* Check whether the tasklist changed */
+ feed = gdata_tasks_service_query_all_tasklists (cbgtasks->priv->service, NULL, cancellable, NULL,
NULL, &local_error);
- ecb_gtasks_take_cancellable (gtasks, g_cancellable_new ());
+ if (local_error) {
+ g_propagate_error (error, local_error);
+ return FALSE;
+ }
- if (e_backend_get_online (E_BACKEND (backend))) {
- ESource *source;
- gchar *auth_method = NULL;
+ if (feed) {
+ GList *link;
- source = e_backend_get_source (E_BACKEND (backend));
+ for (link = gdata_feed_get_entries (feed); link; link = g_list_next (link)) {
+ GDataEntry *entry = link->data;
- if (e_source_has_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
- ESourceAuthentication *auth_extension;
+ if (entry && g_strcmp0 (id, gdata_entry_get_id (entry)) == 0) {
+ ECalCache *cal_cache;
- auth_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
- auth_method = e_source_authentication_dup_method (auth_extension);
- }
+ cal_cache = e_cal_meta_backend_ref_cache (E_CAL_META_BACKEND (cbgtasks));
+ taskslist_time = gdata_entry_get_updated (entry);
- if (g_strcmp0 (auth_method, "Google") == 0) {
- e_backend_credentials_required_sync (
- E_BACKEND (backend), E_SOURCE_CREDENTIALS_REASON_REQUIRED,
- NULL, 0, NULL, cancellable, &local_error);
- } else {
- ecb_gtasks_update_connection_sync (gtasks, NULL, cancellable, &local_error);
- }
+ if (taskslist_time > 0 && last_sync_tag && ecb_gtasks_check_data_version
(cal_cache)) {
+ GTimeVal stored;
- g_free (auth_method);
- }
+ if (g_time_val_from_iso8601 (last_sync_tag, &stored))
+ *out_changed = taskslist_time != stored.tv_sec;
+ }
- e_data_cal_respond_open (cal, opid, local_error);
-}
+ g_clear_object (&cal_cache);
-static void
-ecb_gtasks_refresh (ECalBackend *backend,
- EDataCal *cal,
- guint32 opid,
- GCancellable *cancellable)
-{
- g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
- g_return_if_fail (E_IS_DATA_CAL (cal));
+ break;
+ }
+ }
- if (!ecb_gtasks_is_authorized (backend) ||
- !e_backend_get_online (E_BACKEND (backend))) {
- e_data_cal_respond_refresh (cal, opid, EDC_ERROR (RepositoryOffline));
- return;
+ g_clear_object (&feed);
}
- ecb_gtasks_start_update (E_CAL_BACKEND_GTASKS (backend));
+ g_free (id);
- e_data_cal_respond_refresh (cal, opid, NULL);
+ *out_taskslist_time = taskslist_time;
+
+ return TRUE;
}
-static void
-ecb_gtasks_get_object (ECalBackend *backend,
- EDataCal *cal,
- guint32 opid,
- GCancellable *cancellable,
- const gchar *uid,
- const gchar *rid)
+static gboolean
+ecb_gtasks_get_changes_sync (ECalMetaBackend *meta_backend,
+ const gchar *last_sync_tag,
+ gboolean is_repeat,
+ gchar **out_new_sync_tag,
+ gboolean *out_repeat,
+ GSList **out_created_objects, /* ECalMetaBackendInfo * */
+ GSList **out_modified_objects, /* ECalMetaBackendInfo * */
+ GSList **out_removed_objects, /* ECalMetaBackendInfo * */
+ GCancellable *cancellable,
+ GError **error)
{
- ECalBackendGTasks *gtasks;
- ECalComponent *cached_comp;
- gchar *comp_str = NULL;
+ ECalBackendGTasks *cbgtasks;
+ ECalCache *cal_cache;
+ gint64 taskslist_time = 0;
+ GTimeVal last_updated;
+ GDataFeed *feed;
+ GDataTasksQuery *tasks_query;
+ gboolean changed = TRUE;
GError *local_error = NULL;
- g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
- g_return_if_fail (E_IS_DATA_CAL (cal));
-
- gtasks = E_CAL_BACKEND_GTASKS (backend);
+ g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (meta_backend), FALSE);
+ g_return_val_if_fail (out_new_sync_tag != NULL, FALSE);
+ g_return_val_if_fail (out_created_objects != NULL, FALSE);
+ g_return_val_if_fail (out_modified_objects != NULL, FALSE);
+ g_return_val_if_fail (out_removed_objects != NULL, FALSE);
- PROPERTY_LOCK (gtasks);
+ cbgtasks = E_CAL_BACKEND_GTASKS (meta_backend);
- cached_comp = ecb_gtasks_get_cached_comp (gtasks, uid);
- if (cached_comp)
- comp_str = e_cal_component_get_as_string (cached_comp);
- else
- local_error = EDC_ERROR (ObjectNotFound);
+ *out_created_objects = NULL;
+ *out_modified_objects = NULL;
+ *out_removed_objects = NULL;
- PROPERTY_UNLOCK (gtasks);
+ if (!ecb_gtasks_check_tasklist_changed_sync (cbgtasks, last_sync_tag, &changed, &taskslist_time,
cancellable, error))
+ return FALSE;
- e_data_cal_respond_get_object (cal, opid, local_error, comp_str);
+ if (!changed)
+ return TRUE;
- g_clear_object (&cached_comp);
- g_free (comp_str);
-}
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
-static void
-ecb_gtasks_get_object_list (ECalBackend *backend,
- EDataCal *cal,
- guint32 opid,
- GCancellable *cancellable,
- const gchar *sexp_str)
-{
- ECalBackendGTasks *gtasks;
- ECalBackendSExp *sexp;
- ETimezoneCache *cache;
- gboolean do_search;
- GSList *list, *iter, *calobjs = NULL;
- time_t occur_start = -1, occur_end = -1;
- gboolean prunning_by_time;
-
- g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
- g_return_if_fail (E_IS_DATA_CAL (cal));
-
- gtasks = E_CAL_BACKEND_GTASKS (backend);
-
- sexp = e_cal_backend_sexp_new (sexp_str);
- if (sexp == NULL) {
- e_data_cal_respond_get_object_list (cal, opid, EDC_ERROR (InvalidQuery), NULL);
- return;
+ if (!ecb_gtasks_check_data_version (cal_cache) ||
+ !last_sync_tag ||
+ !g_time_val_from_iso8601 (last_sync_tag, &last_updated)) {
+ last_updated.tv_sec = 0;
}
- do_search = !g_str_equal (sexp_str, "#t");
- prunning_by_time = e_cal_backend_sexp_evaluate_occur_times (sexp, &occur_start, &occur_end);
-
- cache = E_TIMEZONE_CACHE (backend);
-
- PROPERTY_LOCK (gtasks);
-
- list = prunning_by_time ?
- e_cal_backend_store_get_components_occuring_in_range (gtasks->priv->store, occur_start,
occur_end)
- : e_cal_backend_store_get_components (gtasks->priv->store);
-
- PROPERTY_UNLOCK (gtasks);
-
- for (iter = list; iter; iter = g_slist_next (iter)) {
- ECalComponent *comp = E_CAL_COMPONENT (iter->data);
-
- if (!do_search || e_cal_backend_sexp_match_comp (sexp, comp, cache)) {
- calobjs = g_slist_prepend (calobjs, e_cal_component_get_as_string (comp));
- }
+ tasks_query = gdata_tasks_query_new (NULL);
+ gdata_query_set_max_results (GDATA_QUERY (tasks_query), 100);
+ gdata_tasks_query_set_show_completed (tasks_query, TRUE);
+ gdata_tasks_query_set_show_hidden (tasks_query, TRUE);
- g_object_unref (comp);
+ if (last_updated.tv_sec > 0) {
+ gdata_query_set_updated_min (GDATA_QUERY (tasks_query), last_updated.tv_sec);
+ gdata_tasks_query_set_show_deleted (tasks_query, TRUE);
}
- g_object_unref (sexp);
- g_slist_free (list);
-
- e_data_cal_respond_get_object_list (cal, opid, NULL, calobjs);
+ feed = gdata_tasks_service_query_tasks (cbgtasks->priv->service, cbgtasks->priv->tasklist,
+ GDATA_QUERY (tasks_query), cancellable, NULL, NULL, &local_error);
- g_slist_foreach (calobjs, (GFunc) g_free, NULL);
- g_slist_free (calobjs);
-}
+#ifdef HAVE_LIBGDATA_TASKS_PAGINATION_FUNCTIONS
+ while (feed && !g_cancellable_is_cancelled (cancellable) && !local_error) {
+#else
+ if (feed) {
+#endif
+ GList *link;
-static void
-ecb_gtasks_get_free_busy (ECalBackend *backend,
- EDataCal *cal,
- guint32 opid,
- GCancellable *cancellable,
- const GSList *users,
- time_t start,
- time_t end)
-{
- g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
- g_return_if_fail (E_IS_DATA_CAL (cal));
+ for (link = gdata_feed_get_entries (feed); link && !g_cancellable_is_cancelled (cancellable);
link = g_list_next (link)) {
+ GDataTasksTask *task = link->data;
+ ECalComponent *cached_comp = NULL;
+ gchar *uid;
- e_data_cal_respond_get_free_busy (cal, opid, EDC_ERROR (NotSupported), NULL);
-}
+ if (!GDATA_IS_TASKS_TASK (task))
+ continue;
-static void
-ecb_gtasks_create_objects (ECalBackend *backend,
- EDataCal *cal,
- guint32 opid,
- GCancellable *cancellable,
- const GSList *calobjs)
-{
- ECalBackendGTasks *gtasks;
- GSList *new_uids = NULL, *new_calcomps = NULL;
- const GSList *link;
- GError *local_error = NULL;
+ uid = g_strdup (gdata_entry_get_id (GDATA_ENTRY (task)));
+ if (!uid || !*uid) {
+ g_free (uid);
+ continue;
+ }
- g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
- g_return_if_fail (E_IS_DATA_CAL (cal));
+ if (!e_cal_cache_get_component (cal_cache, uid, NULL, &cached_comp, cancellable,
NULL))
+ cached_comp = NULL;
- gtasks = E_CAL_BACKEND_GTASKS (backend);
+ if (gdata_tasks_task_is_deleted (task)) {
+ *out_removed_objects = g_slist_prepend (*out_removed_objects,
+ e_cal_meta_backend_info_new (uid, NULL, NULL, NULL));
+ } else {
+ ECalComponent *new_comp;
- if (!ecb_gtasks_is_authorized (backend) ||
- !e_backend_get_online (E_BACKEND (backend))) {
- e_data_cal_respond_create_objects (cal, opid, EDC_ERROR (RepositoryOffline), NULL, NULL);
- return;
- }
+ new_comp = ecb_gtasks_gdata_to_comp (task);
+ if (new_comp) {
+ gchar *revision, *object;
- for (link = calobjs; link && !local_error; link = link->next) {
- const gchar *icalstr = link->data;
- ECalComponent *comp;
- icalcomponent *icomp;
- const gchar *uid;
- GDataTasksTask *new_task, *comp_task;
+ revision = e_cal_cache_dup_component_revision (cal_cache,
e_cal_component_get_icalcomponent (new_comp));
+ object = e_cal_component_get_as_string (new_comp);
- if (!icalstr) {
- local_error = EDC_ERROR (InvalidObject);
- break;
- }
+ if (cached_comp) {
+ struct icaltimetype *cached_tt = NULL, *new_tt = NULL;
- comp = e_cal_component_new_from_string (icalstr);
- if (comp == NULL) {
- local_error = EDC_ERROR (InvalidObject);
- break;
- }
+ e_cal_component_get_last_modified (cached_comp, &cached_tt);
+ e_cal_component_get_last_modified (new_comp, &new_tt);
- icomp = e_cal_component_get_icalcomponent (comp);
- if (!icomp) {
- g_object_unref (comp);
- local_error = EDC_ERROR (InvalidObject);
- break;
- }
+ if (!cached_tt || !new_tt ||
+ icaltime_compare (*cached_tt, *new_tt) != 0) {
+ /* Google doesn't store/provide 'created', thus use
'created,
+ as first seen by the backend' */
+ if (cached_tt)
+ e_cal_component_set_created (new_comp,
cached_tt);
- uid = icalcomponent_get_uid (icomp);
- if (uid) {
- PROPERTY_LOCK (gtasks);
+ *out_modified_objects = g_slist_prepend
(*out_modified_objects,
+ e_cal_meta_backend_info_new (uid, revision,
object, NULL));
+ }
- if (e_cal_backend_store_has_component (gtasks->priv->store, uid, NULL)) {
- PROPERTY_UNLOCK (gtasks);
- g_object_unref (comp);
- local_error = EDC_ERROR (ObjectIdAlreadyExists);
- break;
- }
+ if (cached_tt)
+ e_cal_component_free_icaltimetype (cached_tt);
+ if (new_tt)
+ e_cal_component_free_icaltimetype (new_tt);
+ } else {
+ *out_created_objects = g_slist_prepend (*out_created_objects,
+ e_cal_meta_backend_info_new (uid, revision, object,
NULL));
+ }
- PROPERTY_UNLOCK (gtasks);
+ g_free (revision);
+ g_free (object);
+ }
- icalcomponent_set_uid (icomp, "");
- }
+ g_clear_object (&new_comp);
+ }
- comp_task = ecb_gtasks_comp_to_gdata (comp, NULL);
- if (!comp_task) {
- g_object_unref (comp);
- local_error = EDC_ERROR (InvalidObject);
- break;
+ g_clear_object (&cached_comp);
+ g_free (uid);
}
- new_task = gdata_tasks_service_insert_task (gtasks->priv->service, comp_task,
gtasks->priv->tasklist, cancellable, &local_error);
-
- g_object_unref (comp_task);
- g_object_unref (comp);
-
- if (!new_task)
+#ifdef HAVE_LIBGDATA_TASKS_PAGINATION_FUNCTIONS
+ if (!gdata_feed_get_entries (feed))
break;
- comp = ecb_gtasks_gdata_to_comp (new_task);
- g_object_unref (new_task);
+ gdata_query_next_page (GDATA_QUERY (tasks_query));
- if (!comp) {
- local_error = EDC_ERROR (InvalidObject);
- break;
- }
+ g_clear_object (&feed);
- icomp = e_cal_component_get_icalcomponent (comp);
- uid = icalcomponent_get_uid (icomp);
+ feed = gdata_tasks_service_query_tasks (cbgtasks->priv->service, cbgtasks->priv->tasklist,
+ GDATA_QUERY (tasks_query), cancellable, NULL, NULL, &local_error);
+#endif
+ }
- if (!uid) {
- g_object_unref (comp);
- local_error = EDC_ERROR (InvalidObject);
- break;
- }
+ g_clear_object (&tasks_query);
+ g_clear_object (&feed);
- PROPERTY_LOCK (gtasks);
- e_cal_backend_store_put_component (gtasks->priv->store, comp);
- PROPERTY_UNLOCK (gtasks);
+ if (!g_cancellable_is_cancelled (cancellable) && !local_error) {
+ last_updated.tv_sec = taskslist_time;
+ last_updated.tv_usec = 0;
- e_cal_backend_notify_component_created (backend, comp);
+ *out_new_sync_tag = g_time_val_to_iso8601 (&last_updated);
- new_uids = g_slist_prepend (new_uids, g_strdup (uid));
- new_calcomps = g_slist_prepend (new_calcomps, comp);
+ ecb_gtasks_store_data_version (cal_cache);
}
- new_uids = g_slist_reverse (new_uids);
- new_calcomps = g_slist_reverse (new_calcomps);
+ g_clear_object (&cal_cache);
- e_data_cal_respond_create_objects (cal, opid, local_error, new_uids, new_calcomps);
+ if (local_error) {
+ g_propagate_error (error, local_error);
+ return FALSE;
+ }
- g_slist_free_full (new_uids, g_free);
- e_util_free_nullable_object_slist (new_calcomps);
+ return TRUE;
}
-static void
-ecb_gtasks_modify_objects (ECalBackend *backend,
- EDataCal *cal,
- guint32 opid,
- GCancellable *cancellable,
- const GSList *calobjs,
- ECalObjModType mod)
+static gboolean
+ecb_gtasks_load_component_sync (ECalMetaBackend *meta_backend,
+ const gchar *uid,
+ const gchar *extra,
+ icalcomponent **out_instances,
+ gchar **out_extra,
+ GCancellable *cancellable,
+ GError **error)
{
- ECalBackendGTasks *gtasks;
- GSList *old_calcomps = NULL, *new_calcomps = NULL;
- const GSList *link;
- GError *local_error = NULL;
-
- g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
- g_return_if_fail (E_IS_DATA_CAL (cal));
-
- gtasks = E_CAL_BACKEND_GTASKS (backend);
-
- if (!ecb_gtasks_is_authorized (backend) ||
- !e_backend_get_online (E_BACKEND (backend))) {
- e_data_cal_respond_modify_objects (cal, opid, EDC_ERROR (RepositoryOffline), NULL, NULL);
- return;
- }
+ ECalBackendGTasks *cbgtasks;
- for (link = calobjs; link && !local_error; link = link->next) {
- const gchar *icalstr = link->data;
- ECalComponent *comp, *cached_comp;
- icalcomponent *icomp;
- const gchar *uid;
- GDataTasksTask *new_task, *comp_task;
+ g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (meta_backend), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (out_instances != NULL, FALSE);
- if (!icalstr) {
- local_error = EDC_ERROR (InvalidObject);
- break;
- }
-
- comp = e_cal_component_new_from_string (icalstr);
- if (comp == NULL) {
- local_error = EDC_ERROR (InvalidObject);
- break;
- }
-
- icomp = e_cal_component_get_icalcomponent (comp);
- if (!icomp) {
- g_object_unref (comp);
- local_error = EDC_ERROR (InvalidObject);
- break;
- }
-
- uid = icalcomponent_get_uid (icomp);
- if (!uid) {
- g_object_unref (comp);
- local_error = EDC_ERROR (InvalidObject);
- break;
- }
+ cbgtasks = E_CAL_BACKEND_GTASKS (meta_backend);
- PROPERTY_LOCK (gtasks);
+ /* Only "load" preloaded during save, otherwise fail with an error,
+ because the backend provides objects within get_changes_sync() */
- cached_comp = ecb_gtasks_get_cached_comp (gtasks, uid);
-
- PROPERTY_UNLOCK (gtasks);
-
- if (!cached_comp) {
- g_object_unref (comp);
- local_error = EDC_ERROR (ObjectNotFound);
- break;
- }
-
- comp_task = ecb_gtasks_comp_to_gdata (comp, cached_comp);
- g_object_unref (comp);
+ if (cbgtasks->priv->preloaded) {
+ ECalComponent *comp;
- if (!comp_task) {
- g_object_unref (cached_comp);
- local_error = EDC_ERROR (ObjectNotFound);
- break;
- }
+ comp = g_hash_table_lookup (cbgtasks->priv->preloaded, uid);
+ if (comp) {
+ icalcomponent *icalcomp;
- new_task = gdata_tasks_service_update_task (gtasks->priv->service, comp_task, cancellable,
&local_error);
- g_object_unref (comp_task);
+ icalcomp = e_cal_component_get_icalcomponent (comp);
+ if (icalcomp)
+ *out_instances = icalcomponent_new_clone (icalcomp);
- if (!local_error)
- e_backend_ensure_source_status_connected (E_BACKEND (backend));
+ g_hash_table_remove (cbgtasks->priv->preloaded, uid);
- if (!new_task) {
- g_object_unref (cached_comp);
- break;
+ if (icalcomp)
+ return TRUE;
}
-
- comp = ecb_gtasks_gdata_to_comp (new_task);
- g_object_unref (new_task);
-
- PROPERTY_LOCK (gtasks);
- e_cal_backend_store_put_component (gtasks->priv->store, comp);
- PROPERTY_UNLOCK (gtasks);
-
- e_cal_backend_notify_component_modified (backend, cached_comp, comp);
-
- old_calcomps = g_slist_prepend (old_calcomps, cached_comp);
- new_calcomps = g_slist_prepend (new_calcomps, comp);
}
- old_calcomps = g_slist_reverse (old_calcomps);
- new_calcomps = g_slist_reverse (new_calcomps);
-
- e_data_cal_respond_modify_objects (cal, opid, local_error, old_calcomps, new_calcomps);
+ g_propagate_error (error, EDC_ERROR (ObjectNotFound));
- e_util_free_nullable_object_slist (old_calcomps);
- e_util_free_nullable_object_slist (new_calcomps);
+ return FALSE;
}
-static void
-ecb_gtasks_remove_objects (ECalBackend *backend,
- EDataCal *cal,
- guint32 opid,
- GCancellable *cancellable,
- const GSList *ids,
- ECalObjModType mod)
+static gboolean
+ecb_gtasks_save_component_sync (ECalMetaBackend *meta_backend,
+ gboolean overwrite_existing,
+ EConflictResolution conflict_resolution,
+ const GSList *instances, /* ECalComponent * */
+ const gchar *extra,
+ gchar **out_new_uid,
+ gchar **out_new_extra,
+ GCancellable *cancellable,
+ GError **error)
{
- ECalBackendGTasks *gtasks;
- GSList *old_calcomps = NULL, *removed_ids = NULL;
- const GSList *link;
- GError *local_error = NULL;
+ ECalBackendGTasks *cbgtasks;
+ ECalCache *cal_cache;
+ GDataTasksTask *new_task, *comp_task;
+ ECalComponent *comp, *cached_comp = NULL;
+ icalcomponent *icalcomp;
+ const gchar *uid;
+
+ g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (meta_backend), FALSE);
+ g_return_val_if_fail (out_new_uid != NULL, FALSE);
- g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
- g_return_if_fail (E_IS_DATA_CAL (cal));
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ g_return_val_if_fail (cal_cache != NULL, FALSE);
- gtasks = E_CAL_BACKEND_GTASKS (backend);
+ cbgtasks = E_CAL_BACKEND_GTASKS (meta_backend);
- if (!ecb_gtasks_is_authorized (backend) ||
- !e_backend_get_online (E_BACKEND (backend))) {
- e_data_cal_respond_remove_objects (cal, opid, EDC_ERROR (RepositoryOffline), NULL, NULL,
NULL);
- return;
+ if (g_slist_length ((GSList *) instances) != 1) {
+ g_propagate_error (error, EDC_ERROR (InvalidArg));
+ g_clear_object (&cal_cache);
+ return FALSE;
}
- for (link = ids; link; link = link->next) {
- const ECalComponentId *id = link->data;
- ECalComponentId *tmp_id;
- ECalComponent *cached_comp;
- GDataTasksTask *task;
+ comp = instances->data;
- if (!id || !id->uid) {
- local_error = EDC_ERROR (InvalidObject);
- break;
- }
+ if (!comp) {
+ g_propagate_error (error, EDC_ERROR (InvalidObject));
+ g_clear_object (&cal_cache);
+ return FALSE;
+ }
- PROPERTY_LOCK (gtasks);
- cached_comp = ecb_gtasks_get_cached_comp (gtasks, id->uid);
- PROPERTY_UNLOCK (gtasks);
+ if (!overwrite_existing || !e_cal_cache_get_component (cal_cache,
+ icalcomponent_get_uid (e_cal_component_get_icalcomponent (comp)),
+ NULL, &cached_comp, cancellable, NULL)) {
+ cached_comp = NULL;
+ }
- if (!cached_comp) {
- local_error = EDC_ERROR (ObjectNotFound);
- break;
- }
+ comp_task = ecb_gtasks_comp_to_gdata (comp, cached_comp, !overwrite_existing);
- task = ecb_gtasks_comp_to_gdata (cached_comp, NULL);
- if (!task) {
- g_object_unref (cached_comp);
- local_error = EDC_ERROR (InvalidObject);
- break;
- }
-
- /* Ignore protocol errors here, libgdata 0.15.1 results with "Error code 204 when deleting an
entry: No Content",
- while the delete succeeded */
- if (!gdata_tasks_service_delete_task (gtasks->priv->service, task, cancellable, &local_error)
&&
- !g_error_matches (local_error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR)) {
- g_object_unref (cached_comp);
- g_object_unref (task);
- break;
- }
+ g_clear_object (&cached_comp);
+ g_clear_object (&cal_cache);
- if (!local_error)
- e_backend_ensure_source_status_connected (E_BACKEND (backend));
+ if (!comp_task) {
+ g_propagate_error (error, EDC_ERROR (InvalidObject));
+ return FALSE;
+ }
- g_clear_error (&local_error);
+ if (overwrite_existing)
+ new_task = gdata_tasks_service_update_task (cbgtasks->priv->service, comp_task, cancellable,
error);
+ else
+ new_task = gdata_tasks_service_insert_task (cbgtasks->priv->service, comp_task,
cbgtasks->priv->tasklist, cancellable, error);
- g_object_unref (task);
+ g_object_unref (comp_task);
- PROPERTY_LOCK (gtasks);
- e_cal_backend_store_remove_component (gtasks->priv->store, id->uid, NULL);
- PROPERTY_UNLOCK (gtasks);
+ if (!new_task)
+ return FALSE;
- tmp_id = e_cal_component_id_new (id->uid, NULL);
- e_cal_backend_notify_component_removed (backend, tmp_id, cached_comp, NULL);
+ comp = ecb_gtasks_gdata_to_comp (new_task);
+ g_object_unref (new_task);
- old_calcomps = g_slist_prepend (old_calcomps, cached_comp);
- removed_ids = g_slist_prepend (removed_ids, tmp_id);
+ if (!comp) {
+ g_propagate_error (error, EDC_ERROR (InvalidObject));
+ return FALSE;
}
- old_calcomps = g_slist_reverse (old_calcomps);
- removed_ids = g_slist_reverse (removed_ids);
-
- e_data_cal_respond_remove_objects (cal, opid, local_error, removed_ids, old_calcomps, NULL);
-
- g_slist_free_full (removed_ids, (GDestroyNotify) e_cal_component_free_id);
- e_util_free_nullable_object_slist (old_calcomps);
-}
-
-static void
-ecb_gtasks_receive_objects (ECalBackend *backend,
- EDataCal *cal,
- guint32 opid,
- GCancellable *cancellable,
- const gchar *calobj)
-{
- g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
- g_return_if_fail (E_IS_DATA_CAL (cal));
+ icalcomp = e_cal_component_get_icalcomponent (comp);
+ uid = icalcomp ? icalcomponent_get_uid (icalcomp) : NULL;
- e_data_cal_respond_receive_objects (cal, opid, EDC_ERROR (NotSupported));
-}
+ if (!icalcomp || !uid) {
+ g_object_unref (comp);
+ g_propagate_error (error, EDC_ERROR (InvalidObject));
+ return FALSE;
+ }
-static void
-ecb_gtasks_send_objects (ECalBackend *backend,
- EDataCal *cal,
- guint32 opid,
- GCancellable *cancellable,
- const gchar *calobj)
-{
- g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
- g_return_if_fail (E_IS_DATA_CAL (cal));
+ if (cbgtasks->priv->preloaded) {
+ *out_new_uid = g_strdup (uid);
+ g_hash_table_insert (cbgtasks->priv->preloaded, g_strdup (uid), comp);
+ } else {
+ g_object_unref (comp);
+ }
- e_data_cal_respond_send_objects (cal, opid, EDC_ERROR (NotSupported), NULL, NULL);
+ return TRUE;
}
-static void
-ecb_gtasks_get_attachment_uris (ECalBackend *backend,
- EDataCal *cal,
- guint32 opid,
- GCancellable *cancellable,
- const gchar *uid,
- const gchar *rid)
+static gboolean
+ecb_gtasks_remove_component_sync (ECalMetaBackend *meta_backend,
+ EConflictResolution conflict_resolution,
+ const gchar *uid,
+ const gchar *extra,
+ const gchar *object,
+ GCancellable *cancellable,
+ GError **error)
{
- g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
- g_return_if_fail (E_IS_DATA_CAL (cal));
-
- e_data_cal_respond_get_attachment_uris (cal, opid, EDC_ERROR (NotSupported), NULL);
-}
+ ECalBackendGTasks *cbgtasks;
+ GDataTasksTask *task;
+ ECalComponent *cached_comp = NULL;
+ GError *local_error = NULL;
-static void
-ecb_gtasks_discard_alarm (ECalBackend *backend,
- EDataCal *cal,
- guint32 opid,
- GCancellable *cancellable,
- const gchar *uid,
- const gchar *rid,
- const gchar *auid)
-{
- g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
- g_return_if_fail (E_IS_DATA_CAL (cal));
+ g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (meta_backend), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (object != NULL, FALSE);
- e_data_cal_respond_discard_alarm (cal, opid, EDC_ERROR (NotSupported));
-}
+ cached_comp = e_cal_component_new_from_string (object);
+ if (!cached_comp) {
+ g_propagate_error (error, EDC_ERROR (InvalidObject));
+ return FALSE;
+ }
-static void
-ecb_gtasks_start_view (ECalBackend *backend,
- EDataCalView *view)
-{
- ECalBackendGTasks *gtasks;
- ECalBackendSExp *sexp;
- ETimezoneCache *cache;
- const gchar *sexp_str;
- gboolean do_search;
- GSList *list, *iter;
- time_t occur_start = -1, occur_end = -1;
- gboolean prunning_by_time;
+ cbgtasks = E_CAL_BACKEND_GTASKS (meta_backend);
- g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
- g_return_if_fail (E_IS_DATA_CAL_VIEW (view));
+ task = ecb_gtasks_comp_to_gdata (cached_comp, NULL, FALSE);
+ if (!task) {
+ g_object_unref (cached_comp);
+ g_propagate_error (error, EDC_ERROR (InvalidObject));
- g_object_ref (view);
+ return FALSE;
+ }
- gtasks = E_CAL_BACKEND_GTASKS (backend);
- sexp = e_data_cal_view_get_sexp (view);
- sexp_str = e_cal_backend_sexp_text (sexp);
- do_search = !g_str_equal (sexp_str, "#t");
- prunning_by_time = e_cal_backend_sexp_evaluate_occur_times (sexp, &occur_start, &occur_end);
+ /* Ignore protocol errors here, libgdata 0.15.1 results with "Error code 204 when deleting an entry:
No Content",
+ while the delete succeeded */
+ if (!gdata_tasks_service_delete_task (cbgtasks->priv->service, task, cancellable, &local_error) &&
+ !g_error_matches (local_error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR)) {
+ g_object_unref (cached_comp);
+ g_object_unref (task);
+ g_propagate_error (error, local_error);
- cache = E_TIMEZONE_CACHE (backend);
+ return FALSE;
+ } else {
+ g_clear_error (&local_error);
+ }
- list = prunning_by_time ?
- e_cal_backend_store_get_components_occuring_in_range (gtasks->priv->store, occur_start,
occur_end)
- : e_cal_backend_store_get_components (gtasks->priv->store);
+ g_object_unref (cached_comp);
+ g_object_unref (task);
- for (iter = list; iter; iter = g_slist_next (iter)) {
- ECalComponent *comp = E_CAL_COMPONENT (iter->data);
+ return TRUE;
+}
- if (!do_search || e_cal_backend_sexp_match_comp (sexp, comp, cache)) {
- e_data_cal_view_notify_components_added_1 (view, comp);
- }
+static gboolean
+ecb_gtasks_requires_reconnect (ECalMetaBackend *meta_backend)
+{
+ ESource *source;
+ ESourceResource *resource;
+ gchar *id;
+ ECalBackendGTasks *cbgtasks;
+ gboolean changed;
- g_object_unref (comp);
- }
+ g_return_val_if_fail (E_IS_CAL_BACKEND_GTASKS (meta_backend), FALSE);
- g_slist_free (list);
+ cbgtasks = E_CAL_BACKEND_GTASKS (meta_backend);
+ if (!cbgtasks->priv->tasklist)
+ return TRUE;
- e_data_cal_view_notify_complete (view, NULL /* Success */);
+ source = e_backend_get_source (E_BACKEND (cbgtasks));
+ resource = e_source_get_extension (source, E_SOURCE_EXTENSION_RESOURCE);
+ id = e_source_resource_dup_identity (resource);
- g_object_unref (view);
-}
+ changed = id && *id && g_strcmp0 (id, gdata_entry_get_id (GDATA_ENTRY (cbgtasks->priv->tasklist))) !=
0 &&
+ g_strcmp0 (GTASKS_DEFAULT_TASKLIST_NAME, gdata_entry_get_id (GDATA_ENTRY
(cbgtasks->priv->tasklist))) != 0;
-static void
-ecb_gtasks_stop_view (ECalBackend *backend,
- EDataCalView *view)
-{
- g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
- g_return_if_fail (E_IS_DATA_CAL_VIEW (view));
-}
+ g_free (id);
-static void
-ecb_gtasks_add_timezone (ECalBackend *backend,
- EDataCal *cal,
- guint32 opid,
- GCancellable *cancellable,
- const gchar *tzobject)
-{
- /* Nothing to do, times are in UTC */
- e_data_cal_respond_add_timezone (cal, opid, NULL);
+ return changed;
}
-static void
-ecb_gtasks_shutdown (ECalBackend *backend)
+static gchar *
+ecb_gtasks_dup_component_revision (ECalCache *cal_cache,
+ icalcomponent *icalcomp,
+ gpointer user_data)
{
- ECalBackendGTasks *gtasks;
-
- g_return_if_fail (E_IS_CAL_BACKEND_GTASKS (backend));
-
- gtasks = E_CAL_BACKEND_GTASKS (backend);
+ icalproperty *prop;
+ gchar *revision = NULL;
- ecb_gtasks_take_cancellable (gtasks, NULL);
+ g_return_val_if_fail (icalcomp != NULL, NULL);
- if (gtasks->priv->refresh_id) {
- ESource *source = e_backend_get_source (E_BACKEND (backend));
- if (source)
- e_source_refresh_remove_timeout (source, gtasks->priv->refresh_id);
+ prop = icalcomponent_get_first_property (icalcomp, ICAL_LASTMODIFIED_PROPERTY);
+ if (prop) {
+ struct icaltimetype itt;
- gtasks->priv->refresh_id = 0;
+ itt = icalproperty_get_lastmodified (prop);
+ revision = icaltime_as_ical_string_r (itt);
}
- /* Chain up to parent's method. */
- E_CAL_BACKEND_CLASS (e_cal_backend_gtasks_parent_class)->shutdown (backend);
+ return revision;
}
static void
-e_cal_backend_gtasks_init (ECalBackendGTasks *gtasks)
+e_cal_backend_gtasks_init (ECalBackendGTasks *cbgtasks)
{
- gtasks->priv = E_CAL_BACKEND_GTASKS_GET_PRIVATE (gtasks);
- gtasks->priv->cancellable = NULL;
-
- g_mutex_init (>asks->priv->property_mutex);
+ cbgtasks->priv = G_TYPE_INSTANCE_GET_PRIVATE (cbgtasks, E_TYPE_CAL_BACKEND_GTASKS,
ECalBackendGTasksPrivate);
+ cbgtasks->priv->preloaded = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
}
static void
ecb_gtasks_constructed (GObject *object)
{
- ECalBackendGTasks *gtasks = E_CAL_BACKEND_GTASKS (object);
- ESource *source;
+ ECalBackendGTasks *cbgtasks = E_CAL_BACKEND_GTASKS (object);
+ ECalCache *cal_cache;
/* Chain up to parent's method. */
G_OBJECT_CLASS (e_cal_backend_gtasks_parent_class)->constructed (object);
- gtasks->priv->store = e_cal_backend_store_new (
- e_cal_backend_get_cache_dir (E_CAL_BACKEND (gtasks)),
- E_TIMEZONE_CACHE (gtasks));
- e_cal_backend_store_load (gtasks->priv->store);
+ cal_cache = e_cal_meta_backend_ref_cache (E_CAL_META_BACKEND (cbgtasks));
+ g_return_if_fail (cal_cache != NULL);
+
+ g_signal_connect (cal_cache, "dup-component-revision", G_CALLBACK
(ecb_gtasks_dup_component_revision), NULL);
- source = e_backend_get_source (E_BACKEND (gtasks));
- gtasks->priv->refresh_id = e_source_refresh_add_timeout (
- source, NULL, ecb_gtasks_time_to_refresh_data_cb, gtasks, NULL);
+ g_clear_object (&cal_cache);
+
+ /* Set it as always writable, regardless online/offline state */
+ e_cal_backend_set_writable (E_CAL_BACKEND (cbgtasks), TRUE);
}
static void
ecb_gtasks_dispose (GObject *object)
{
- ECalBackendGTasks *gtasks = E_CAL_BACKEND_GTASKS (object);
-
- ecb_gtasks_take_cancellable (gtasks, NULL);
+ ECalBackendGTasks *cbgtasks = E_CAL_BACKEND_GTASKS (object);
- g_clear_object (>asks->priv->cancellable);
- g_clear_object (>asks->priv->service);
- g_clear_object (>asks->priv->authorizer);
- g_clear_object (>asks->priv->tasklist);
- g_clear_object (>asks->priv->store);
+ g_clear_object (&cbgtasks->priv->service);
+ g_clear_object (&cbgtasks->priv->authorizer);
+ g_clear_object (&cbgtasks->priv->tasklist);
- if (gtasks->priv->refresh_id) {
- ESource *source = e_backend_get_source (E_BACKEND (object));
- if (source)
- e_source_refresh_remove_timeout (source, gtasks->priv->refresh_id);
-
- gtasks->priv->refresh_id = 0;
- }
+ g_hash_table_destroy (cbgtasks->priv->preloaded);
+ cbgtasks->priv->preloaded = NULL;
/* Chain up to parent's method. */
G_OBJECT_CLASS (e_cal_backend_gtasks_parent_class)->dispose (object);
}
static void
-ecb_gtasks_finalize (GObject *object)
-{
- ECalBackendGTasks *gtasks = E_CAL_BACKEND_GTASKS (object);
-
- g_mutex_clear (>asks->priv->property_mutex);
-
- /* Chain up to parent's method. */
- G_OBJECT_CLASS (e_cal_backend_gtasks_parent_class)->finalize (object);
-}
-
-static void
-e_cal_backend_gtasks_class_init (ECalBackendGTasksClass *class)
+e_cal_backend_gtasks_class_init (ECalBackendGTasksClass *klass)
{
GObjectClass *object_class;
- EBackendClass *backend_class;
ECalBackendClass *cal_backend_class;
+ ECalMetaBackendClass *cal_meta_backend_class;
- g_type_class_add_private (class, sizeof (ECalBackendGTasksPrivate));
-
- object_class = (GObjectClass *) class;
- object_class->constructed = ecb_gtasks_constructed;
- object_class->dispose = ecb_gtasks_dispose;
- object_class->finalize = ecb_gtasks_finalize;
+ g_type_class_add_private (klass, sizeof (ECalBackendGTasksPrivate));
- backend_class = (EBackendClass *) class;
- backend_class->authenticate_sync = ecb_gtasks_authenticate_sync;
+ cal_meta_backend_class = E_CAL_META_BACKEND_CLASS (klass);
+ cal_meta_backend_class->connect_sync = ecb_gtasks_connect_sync;
+ cal_meta_backend_class->disconnect_sync = ecb_gtasks_disconnect_sync;
+ cal_meta_backend_class->get_changes_sync = ecb_gtasks_get_changes_sync;
+ cal_meta_backend_class->load_component_sync = ecb_gtasks_load_component_sync;
+ cal_meta_backend_class->save_component_sync = ecb_gtasks_save_component_sync;
+ cal_meta_backend_class->remove_component_sync = ecb_gtasks_remove_component_sync;
+ cal_meta_backend_class->requires_reconnect = ecb_gtasks_requires_reconnect;
- cal_backend_class = (ECalBackendClass *) class;
+ cal_backend_class = E_CAL_BACKEND_CLASS (klass);
cal_backend_class->get_backend_property = ecb_gtasks_get_backend_property;
- cal_backend_class->open = ecb_gtasks_open;
- cal_backend_class->refresh = ecb_gtasks_refresh;
- cal_backend_class->get_object = ecb_gtasks_get_object;
- cal_backend_class->get_object_list = ecb_gtasks_get_object_list;
- cal_backend_class->get_free_busy = ecb_gtasks_get_free_busy;
- cal_backend_class->create_objects = ecb_gtasks_create_objects;
- cal_backend_class->modify_objects = ecb_gtasks_modify_objects;
- cal_backend_class->remove_objects = ecb_gtasks_remove_objects;
- cal_backend_class->receive_objects = ecb_gtasks_receive_objects;
- cal_backend_class->send_objects = ecb_gtasks_send_objects;
- cal_backend_class->get_attachment_uris = ecb_gtasks_get_attachment_uris;
- cal_backend_class->discard_alarm = ecb_gtasks_discard_alarm;
- cal_backend_class->start_view = ecb_gtasks_start_view;
- cal_backend_class->stop_view = ecb_gtasks_stop_view;
- cal_backend_class->add_timezone = ecb_gtasks_add_timezone;
- cal_backend_class->shutdown = ecb_gtasks_shutdown;
+
+ object_class = G_OBJECT_CLASS (klass);
+ object_class->constructed = ecb_gtasks_constructed;
+ object_class->dispose = ecb_gtasks_dispose;
}
diff --git a/src/calendar/backends/gtasks/e-cal-backend-gtasks.h
b/src/calendar/backends/gtasks/e-cal-backend-gtasks.h
index 6823a98..747d044 100644
--- a/src/calendar/backends/gtasks/e-cal-backend-gtasks.h
+++ b/src/calendar/backends/gtasks/e-cal-backend-gtasks.h
@@ -47,12 +47,12 @@ typedef struct _ECalBackendGTasksClass ECalBackendGTasksClass;
typedef struct _ECalBackendGTasksPrivate ECalBackendGTasksPrivate;
struct _ECalBackendGTasks {
- ECalBackend parent;
+ ECalMetaBackend parent;
ECalBackendGTasksPrivate *priv;
};
struct _ECalBackendGTasksClass {
- ECalBackendClass parent_class;
+ ECalMetaBackendClass parent_class;
};
GType e_cal_backend_gtasks_get_type (void);
diff --git a/src/calendar/backends/http/e-cal-backend-http.c b/src/calendar/backends/http/e-cal-backend-http.c
index 1cb7f13..51a4246 100644
--- a/src/calendar/backends/http/e-cal-backend-http.c
+++ b/src/calendar/backends/http/e-cal-backend-http.c
@@ -26,227 +26,25 @@
#include <libsoup/soup.h>
#include <libedata-cal/libedata-cal.h>
-#include "e-cal-backend-http.h"
-#define E_CAL_BACKEND_HTTP_GET_PRIVATE(obj) \
- (G_TYPE_INSTANCE_GET_PRIVATE \
- ((obj), E_TYPE_CAL_BACKEND_HTTP, ECalBackendHttpPrivate))
+#include "e-cal-backend-http.h"
#define EDC_ERROR(_code) e_data_cal_create_error (_code, NULL)
#define EDC_ERROR_EX(_code, _msg) e_data_cal_create_error (_code, _msg)
-G_DEFINE_TYPE (ECalBackendHttp, e_cal_backend_http, E_TYPE_CAL_BACKEND_SYNC)
+G_DEFINE_TYPE (ECalBackendHttp, e_cal_backend_http, E_TYPE_CAL_META_BACKEND)
-/* Private part of the ECalBackendHttp structure */
struct _ECalBackendHttpPrivate {
- /* signal handler id for source's 'changed' signal */
- gulong source_changed_id;
- /* URI to get remote calendar data from */
- gchar *uri;
-
- /* The file cache */
- ECalBackendStore *store;
-
- /* Soup handles for remote file */
- SoupSession *soup_session;
-
- /* Reload */
- guint reload_timeout_id;
- guint is_loading : 1;
-
- /* Flags */
- gboolean opened;
- gboolean requires_auth;
+ ESoupSession *session;
- gchar *username;
- gchar *password;
+ SoupRequestHTTP *request;
+ GInputStream *input_stream;
+ GHashTable *components; /* gchar *uid ~> icalcomponent * */
};
-#define d(x)
-
-static void e_cal_backend_http_add_timezone (ECalBackendSync *backend,
- EDataCal *cal,
- GCancellable *cancellable,
- const gchar *tzobj,
- GError **perror);
-
-static void
-soup_authenticate (SoupSession *session,
- SoupMessage *msg,
- SoupAuth *auth,
- gboolean retrying,
- gpointer data)
-{
- ECalBackendHttp *cbhttp;
- ESourceAuthentication *auth_extension;
- ESource *source;
- const gchar *extension_name;
- const gchar *username;
- gchar *auth_user;
-
- if (retrying)
- return;
-
- cbhttp = E_CAL_BACKEND_HTTP (data);
-
- source = e_backend_get_source (E_BACKEND (data));
- extension_name = E_SOURCE_EXTENSION_AUTHENTICATION;
- auth_extension = e_source_get_extension (source, extension_name);
-
- auth_user = e_source_authentication_dup_user (auth_extension);
-
- username = cbhttp->priv->username;
- if (!username || !*username)
- username = auth_user;
-
- if (!username || !*username || !cbhttp->priv->password)
- soup_message_set_status (msg, SOUP_STATUS_FORBIDDEN);
- else
- soup_auth_authenticate (auth, username, cbhttp->priv->password);
-
- g_free (auth_user);
-}
-
-/* Dispose handler for the file backend */
-static void
-e_cal_backend_http_dispose (GObject *object)
-{
- ECalBackendHttp *cbhttp;
- ECalBackendHttpPrivate *priv;
-
- cbhttp = E_CAL_BACKEND_HTTP (object);
- priv = cbhttp->priv;
-
- if (priv->reload_timeout_id) {
- ESource *source = e_backend_get_source (E_BACKEND (cbhttp));
- e_source_refresh_remove_timeout (source, priv->reload_timeout_id);
- priv->reload_timeout_id = 0;
- }
-
- if (priv->soup_session) {
- soup_session_abort (priv->soup_session);
- g_object_unref (priv->soup_session);
- priv->soup_session = NULL;
- }
- if (priv->source_changed_id) {
- g_signal_handler_disconnect (
- e_backend_get_source (E_BACKEND (cbhttp)),
- priv->source_changed_id);
- priv->source_changed_id = 0;
- }
-
- /* Chain up to parent's dispose() method. */
- G_OBJECT_CLASS (e_cal_backend_http_parent_class)->dispose (object);
-}
-
-/* Finalize handler for the file backend */
-static void
-e_cal_backend_http_finalize (GObject *object)
-{
- ECalBackendHttpPrivate *priv;
-
- priv = E_CAL_BACKEND_HTTP_GET_PRIVATE (object);
-
- /* Clean up */
-
- if (priv->store) {
- g_object_unref (priv->store);
- priv->store = NULL;
- }
-
- g_free (priv->uri);
- g_free (priv->username);
- g_free (priv->password);
-
- /* Chain up to parent's finalize() method. */
- G_OBJECT_CLASS (e_cal_backend_http_parent_class)->finalize (object);
-}
-
-static void
-e_cal_backend_http_constructed (GObject *object)
-{
- ECalBackendHttp *backend;
- SoupSession *soup_session;
-
- /* Chain up to parent's constructed() method. */
- G_OBJECT_CLASS (e_cal_backend_http_parent_class)->constructed (object);
-
- soup_session = soup_session_sync_new ();
- g_object_set (
- soup_session,
- SOUP_SESSION_TIMEOUT, 90,
- SOUP_SESSION_SSL_STRICT, TRUE,
- SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, TRUE,
- SOUP_SESSION_ACCEPT_LANGUAGE_AUTO, TRUE,
- NULL);
-
- backend = E_CAL_BACKEND_HTTP (object);
- backend->priv->soup_session = soup_session;
-
- e_binding_bind_property (
- backend, "proxy-resolver",
- backend->priv->soup_session, "proxy-resolver",
- G_BINDING_SYNC_CREATE);
-
- g_signal_connect (
- backend->priv->soup_session, "authenticate",
- G_CALLBACK (soup_authenticate), backend);
-
- if (g_getenv ("WEBCAL_DEBUG") != NULL) {
- SoupLogger *logger;
-
- logger = soup_logger_new (
- SOUP_LOGGER_LOG_BODY, 1024 * 1024);
- soup_session_add_feature (
- backend->priv->soup_session,
- SOUP_SESSION_FEATURE (logger));
- g_object_unref (logger);
- }
-}
-
-/* Calendar backend methods */
-
static gchar *
-e_cal_backend_http_get_backend_property (ECalBackend *backend,
- const gchar *prop_name)
-{
- g_return_val_if_fail (prop_name != NULL, NULL);
-
- if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
- return g_strjoin (
- ","
- CAL_STATIC_CAPABILITY_NO_EMAIL_ALARMS,
- CAL_STATIC_CAPABILITY_REFRESH_SUPPORTED,
- NULL);
-
- } else if (g_str_equal (prop_name, CAL_BACKEND_PROPERTY_CAL_EMAIL_ADDRESS) ||
- g_str_equal (prop_name, CAL_BACKEND_PROPERTY_ALARM_EMAIL_ADDRESS)) {
- /* A HTTP backend has no particular email address associated
- * with it (although that would be a useful feature some day).
- */
- return NULL;
-
- } else if (g_str_equal (prop_name, CAL_BACKEND_PROPERTY_DEFAULT_OBJECT)) {
- icalcomponent *icalcomp;
- icalcomponent_kind kind;
- gchar *prop_value;
-
- kind = e_cal_backend_get_kind (E_CAL_BACKEND (backend));
- icalcomp = e_cal_util_new_component (kind);
- prop_value = icalcomponent_as_ical_string_r (icalcomp);
- icalcomponent_free (icalcomp);
-
- return prop_value;
- }
-
- /* Chain up to parent's get_backend_property() method. */
- return E_CAL_BACKEND_CLASS (e_cal_backend_http_parent_class)->
- get_backend_property (backend, prop_name);
-}
-
-static gchar *
-webcal_to_http_method (const gchar *webcal_str,
- gboolean secure)
+ecb_http_webcal_to_http_method (const gchar *webcal_str,
+ gboolean secure)
{
if (secure && (strncmp ("http://", webcal_str, sizeof ("http://") - 1) == 0))
return g_strconcat ("https://", webcal_str + sizeof ("http://") - 1, NULL);
@@ -260,1321 +58,556 @@ webcal_to_http_method (const gchar *webcal_str,
return g_strconcat ("http://", webcal_str + sizeof ("webcal://") - 1, NULL);
}
-static gboolean
-notify_and_remove_from_cache (gpointer key,
- gpointer value,
- gpointer user_data)
-{
- const gchar *calobj = value;
- ECalBackendHttp *cbhttp = E_CAL_BACKEND_HTTP (user_data);
- ECalComponent *comp = e_cal_component_new_from_string (calobj);
- ECalComponentId *id = e_cal_component_get_id (comp);
-
- if (id) {
- e_cal_backend_store_remove_component (cbhttp->priv->store, id->uid, id->rid);
- e_cal_backend_notify_component_removed (E_CAL_BACKEND (cbhttp), id, comp, NULL);
-
- e_cal_component_free_id (id);
- }
-
- g_object_unref (comp);
-
- return TRUE;
-}
-
-static void
-empty_cache (ECalBackendHttp *cbhttp)
-{
- ECalBackendHttpPrivate *priv;
- GSList *comps, *l;
-
- priv = cbhttp->priv;
-
- if (!priv->store)
- return;
-
- comps = e_cal_backend_store_get_components (priv->store);
-
- for (l = comps; l != NULL; l = g_slist_next (l)) {
- ECalComponentId *id;
- ECalComponent *comp = l->data;
-
- id = e_cal_component_get_id (comp);
-
- e_cal_backend_notify_component_removed ((ECalBackend *) cbhttp, id, comp, NULL);
-
- e_cal_component_free_id (id);
- g_object_unref (comp);
- }
- g_slist_free (comps);
-
- e_cal_backend_store_put_key_value (priv->store, "ETag", NULL);
- e_cal_backend_store_clean (priv->store);
-}
-
-/* TODO Do not replicate this in every backend */
-static icaltimezone *
-resolve_tzid (const gchar *tzid,
- gpointer user_data)
-{
- ETimezoneCache *timezone_cache;
-
- timezone_cache = E_TIMEZONE_CACHE (user_data);
-
- return e_timezone_cache_get_timezone (timezone_cache, tzid);
-}
-
-static gboolean
-put_component_to_store (ECalBackendHttp *cb,
- ECalComponent *comp)
+static gchar *
+ecb_http_dup_uri (ECalBackendHttp *cbhttp)
{
- time_t time_start, time_end;
- ECalBackendHttpPrivate *priv;
- ECalComponent *cache_comp;
- const gchar *uid;
- gchar *rid;
-
- priv = cb->priv;
-
- e_cal_component_get_uid (comp, &uid);
- rid = e_cal_component_get_recurid_as_string (comp);
- cache_comp = e_cal_backend_store_get_component (priv->store, uid, rid);
- g_free (rid);
-
- if (cache_comp) {
- gboolean changed = TRUE;
- struct icaltimetype stamp1, stamp2;
-
- stamp1 = icaltime_null_time ();
- stamp2 = icaltime_null_time ();
-
- e_cal_component_get_dtstamp (comp, &stamp1);
- e_cal_component_get_dtstamp (cache_comp, &stamp2);
-
- changed = (icaltime_is_null_time (stamp1) && !icaltime_is_null_time (stamp2)) ||
- (!icaltime_is_null_time (stamp1) && icaltime_is_null_time (stamp2)) ||
- (icaltime_compare (stamp1, stamp2) != 0);
-
- if (!changed) {
- struct icaltimetype *last_modified1 = NULL, *last_modified2 = NULL;
-
- e_cal_component_get_last_modified (comp, &last_modified1);
- e_cal_component_get_last_modified (cache_comp, &last_modified2);
-
- changed = (last_modified1 != NULL && last_modified2 == NULL) ||
- (last_modified1 == NULL && last_modified2 != NULL) ||
- (last_modified1 != NULL && last_modified2 != NULL && icaltime_compare
(*last_modified1, *last_modified2) != 0);
-
- if (last_modified1)
- e_cal_component_free_icaltimetype (last_modified1);
- if (last_modified2)
- e_cal_component_free_icaltimetype (last_modified2);
-
- if (!changed) {
- gint *sequence1 = NULL, *sequence2 = NULL;
-
- e_cal_component_get_sequence (comp, &sequence1);
- e_cal_component_get_sequence (cache_comp, &sequence2);
-
- changed = (sequence1 != NULL && sequence2 == NULL) ||
- (sequence1 == NULL && sequence2 != NULL) ||
- (sequence1 != NULL && sequence2 != NULL && *sequence1 !=
*sequence2);
-
- if (sequence1)
- e_cal_component_free_sequence (sequence1);
- if (sequence2)
- e_cal_component_free_sequence (sequence2);
- }
- }
-
- g_object_unref (cache_comp);
-
- if (!changed)
- return FALSE;
- }
-
- e_cal_util_get_component_occur_times (
- comp, &time_start, &time_end,
- resolve_tzid, cb, icaltimezone_get_utc_timezone (),
- e_cal_backend_get_kind (E_CAL_BACKEND (cb)));
-
- e_cal_backend_store_put_component_with_time_range (priv->store, comp, time_start, time_end);
+ ESource *source;
+ ESourceSecurity *security_extension;
+ ESourceWebdav *webdav_extension;
+ SoupURI *soup_uri;
+ gboolean secure_connection;
+ const gchar *extension_name;
+ gchar *uri_string, *uri;
- return TRUE;
-}
+ g_return_val_if_fail (E_IS_CAL_BACKEND_HTTP (cbhttp), NULL);
-static SoupMessage *
-cal_backend_http_new_message (ECalBackendHttp *backend,
- const gchar *uri)
-{
- SoupMessage *soup_message;
-
- /* create message to be sent to server */
- soup_message = soup_message_new (SOUP_METHOD_GET, uri);
- if (soup_message == NULL)
- return NULL;
-
- soup_message_headers_append (
- soup_message->request_headers,
- "User-Agent", "Evolution/" VERSION);
- soup_message_headers_append (
- soup_message->request_headers,
- "Connection", "close");
- soup_message_set_flags (
- soup_message, SOUP_MESSAGE_NO_REDIRECT);
- if (backend->priv->store != NULL) {
- const gchar *etag;
-
- etag = e_cal_backend_store_get_key_value (
- backend->priv->store, "ETag");
-
- if (etag != NULL && *etag != '\0')
- soup_message_headers_append (
- soup_message->request_headers,
- "If-None-Match", etag);
- }
+ source = e_backend_get_source (E_BACKEND (cbhttp));
- return soup_message;
-}
+ extension_name = E_SOURCE_EXTENSION_SECURITY;
+ security_extension = e_source_get_extension (source, extension_name);
-static void
-cal_backend_http_cancelled (GCancellable *cancellable,
- gpointer user_data)
-{
- struct {
- SoupSession *soup_session;
- SoupMessage *soup_message;
- } *cancel_data = user_data;
-
- soup_session_cancel_message (
- cancel_data->soup_session,
- cancel_data->soup_message,
- SOUP_STATUS_CANCELLED);
-}
+ extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
+ webdav_extension = e_source_get_extension (source, extension_name);
-static void
-cal_backend_http_extract_ssl_failed_data (SoupMessage *msg,
- gchar **out_certificate_pem,
- GTlsCertificateFlags *out_certificate_errors)
-{
- GTlsCertificate *certificate = NULL;
+ secure_connection = e_source_security_get_secure (security_extension);
- g_return_if_fail (SOUP_IS_MESSAGE (msg));
+ soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
+ uri_string = soup_uri_to_string (soup_uri, FALSE);
+ soup_uri_free (soup_uri);
- if (!out_certificate_pem || !out_certificate_errors)
- return;
+ uri = ecb_http_webcal_to_http_method (uri_string, secure_connection);
- g_object_get (G_OBJECT (msg),
- "tls-certificate", &certificate,
- "tls-errors", out_certificate_errors,
- NULL);
+ g_free (uri_string);
- if (certificate) {
- g_object_get (certificate, "certificate-pem", out_certificate_pem, NULL);
- g_object_unref (certificate);
- }
+ return uri;
}
static gboolean
-cal_backend_http_load (ECalBackendHttp *backend,
- const gchar *uri,
+ecb_http_connect_sync (ECalMetaBackend *meta_backend,
+ const ENamedParameters *credentials,
+ ESourceAuthenticationResult *out_auth_result,
gchar **out_certificate_pem,
GTlsCertificateFlags *out_certificate_errors,
- GCancellable *cancellable,
- GError **error)
+ GCancellable *cancellable,
+ GError **error)
{
- ECalBackendHttpPrivate *priv = backend->priv;
- ETimezoneCache *timezone_cache;
- SoupMessage *soup_message;
- SoupSession *soup_session;
- icalcomponent *icalcomp, *subcomp;
- icalcomponent_kind kind;
- const gchar *newuri;
- SoupURI *uri_parsed;
- GHashTable *old_cache;
- GSList *comps_in_cache;
+ ECalBackendHttp *cbhttp;
ESource *source;
- guint status_code;
- gulong cancel_id = 0;
-
- struct {
- SoupSession *soup_session;
- SoupMessage *soup_message;
- } cancel_data;
-
- timezone_cache = E_TIMEZONE_CACHE (backend);
-
- soup_session = backend->priv->soup_session;
- soup_message = cal_backend_http_new_message (backend, uri);
-
- if (soup_message == NULL) {
- g_set_error (
- error, SOUP_HTTP_ERROR,
- SOUP_STATUS_MALFORMED,
- _("Malformed URI: %s"), uri);
- return FALSE;
- }
+ SoupRequestHTTP *request;
+ GInputStream *input_stream = NULL;
+ gchar *uri;
+ gboolean success;
+ GError *local_error = NULL;
- if (G_IS_CANCELLABLE (cancellable)) {
- cancel_data.soup_session = soup_session;
- cancel_data.soup_message = soup_message;
+ g_return_val_if_fail (E_IS_CAL_BACKEND_HTTP (meta_backend), FALSE);
+ g_return_val_if_fail (out_auth_result != NULL, FALSE);
- cancel_id = g_cancellable_connect (
- cancellable,
- G_CALLBACK (cal_backend_http_cancelled),
- &cancel_data, (GDestroyNotify) NULL);
- }
+ cbhttp = E_CAL_BACKEND_HTTP (meta_backend);
- source = e_backend_get_source (E_BACKEND (backend));
-
- e_soup_ssl_trust_connect (soup_message, source);
+ if (cbhttp->priv->request && cbhttp->priv->input_stream)
+ return TRUE;
- e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTING);
+ source = e_backend_get_source (E_BACKEND (meta_backend));
- status_code = soup_session_send_message (soup_session, soup_message);
+ g_clear_object (&cbhttp->priv->input_stream);
+ g_clear_object (&cbhttp->priv->request);
- if (G_IS_CANCELLABLE (cancellable))
- g_cancellable_disconnect (cancellable, cancel_id);
+ uri = ecb_http_dup_uri (cbhttp);
- if (status_code == SOUP_STATUS_NOT_MODIFIED) {
- e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTED);
+ if (!uri || !*uri) {
+ g_free (uri);
- /* attempts with ETag can result in 304 status code */
- g_object_unref (soup_message);
- priv->opened = TRUE;
- return TRUE;
+ g_propagate_error (error, EDC_ERROR_EX (OtherError, _("URI not set")));
+ return FALSE;
}
- /* Handle redirection ourselves */
- if (SOUP_STATUS_IS_REDIRECTION (status_code)) {
- gboolean success;
-
- newuri = soup_message_headers_get_list (
- soup_message->response_headers, "Location");
+ e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTING);
- d (g_message ("Redirected from %s to %s\n", async_context->uri, newuri));
+ e_soup_session_set_credentials (cbhttp->priv->session, credentials);
- if (newuri != NULL) {
- gchar *redirected_uri;
+ request = e_soup_session_new_request (cbhttp->priv->session, SOUP_METHOD_GET, uri, &local_error);
+ success = request != NULL;
- if (newuri[0]=='/') {
- g_warning ("Hey! Relative URI returned! Working around...\n");
+ if (success) {
+ SoupMessage *message;
- uri_parsed = soup_uri_new (uri);
- soup_uri_set_path (uri_parsed, newuri);
- soup_uri_set_query (uri_parsed, NULL);
- /* g_free (newuri); */
+ message = soup_request_http_get_message (request);
- newuri = soup_uri_to_string (uri_parsed, FALSE);
- g_message ("Translated URI: %s\n", newuri);
- soup_uri_free (uri_parsed);
- }
+ input_stream = e_soup_session_send_request_sync (cbhttp->priv->session, request, cancellable,
&local_error);
- redirected_uri =
- webcal_to_http_method (newuri, FALSE);
- success = cal_backend_http_load (
- backend, redirected_uri, out_certificate_pem, out_certificate_errors,
cancellable, error);
- g_free (redirected_uri);
+ success = input_stream != NULL;
- } else {
- g_set_error (
- error, SOUP_HTTP_ERROR,
- SOUP_STATUS_BAD_REQUEST,
- _("Redirected to Invalid URI"));
+ if (success && message && !SOUP_STATUS_IS_SUCCESSFUL (message->status_code)) {
+ g_clear_object (&input_stream);
success = FALSE;
}
if (success) {
e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTED);
} else {
- e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
- }
+ guint status_code = message ? message->status_code : SOUP_STATUS_MALFORMED;
+ gboolean credentials_empty;
+
+ credentials_empty = !credentials || !e_named_parameters_count (credentials);
+
+ *out_auth_result = E_SOURCE_AUTHENTICATION_ERROR;
+
+ /* because evolution knows only G_IO_ERROR_CANCELLED */
+ if (status_code == SOUP_STATUS_CANCELLED) {
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED,
+ "%s", message->reason_phrase);
+ } else if (status_code == SOUP_STATUS_FORBIDDEN && credentials_empty) {
+ *out_auth_result = E_SOURCE_AUTHENTICATION_REQUIRED;
+ } else if (status_code == SOUP_STATUS_UNAUTHORIZED) {
+ if (credentials_empty)
+ *out_auth_result = E_SOURCE_AUTHENTICATION_REQUIRED;
+ else
+ *out_auth_result = E_SOURCE_AUTHENTICATION_REJECTED;
+ } else if (local_error) {
+ g_propagate_error (error, local_error);
+ local_error = NULL;
+ } else {
+ g_set_error_literal (error, SOUP_HTTP_ERROR, status_code,
+ message ? message->reason_phrase : soup_status_get_phrase
(status_code));
+ }
- g_object_unref (soup_message);
- return success;
- }
+ if (status_code == SOUP_STATUS_SSL_FAILED) {
+ *out_auth_result = E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED;
- /* check status code */
- if (!SOUP_STATUS_IS_SUCCESSFUL (status_code)) {
- /* because evolution knows only G_IO_ERROR_CANCELLED */
- if (status_code == SOUP_STATUS_CANCELLED)
- g_set_error (
- error, G_IO_ERROR, G_IO_ERROR_CANCELLED,
- "%s", soup_message->reason_phrase);
- else
- g_set_error (
- error, SOUP_HTTP_ERROR, status_code,
- "%s", soup_message->reason_phrase);
-
- if (status_code == SOUP_STATUS_SSL_FAILED) {
- e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_SSL_FAILED);
- cal_backend_http_extract_ssl_failed_data (soup_message, out_certificate_pem,
out_certificate_errors);
- } else {
- e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
+ e_source_set_connection_status (source,
E_SOURCE_CONNECTION_STATUS_SSL_FAILED);
+ e_soup_session_get_ssl_error_details (cbhttp->priv->session,
out_certificate_pem, out_certificate_errors);
+ } else {
+ e_source_set_connection_status (source,
E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
+ }
}
- g_object_unref (soup_message);
- empty_cache (backend);
- return FALSE;
- }
-
- e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_CONNECTED);
-
- if (priv->store) {
- const gchar *etag;
-
- etag = soup_message_headers_get_one (
- soup_message->response_headers, "ETag");
-
- if (etag != NULL && *etag == '\0')
- etag = NULL;
-
- e_cal_backend_store_put_key_value (priv->store, "ETag", etag);
- }
-
- /* get the calendar from the response */
- icalcomp = icalparser_parse_string (soup_message->response_body->data);
-
- if (!icalcomp) {
- g_set_error (
- error, SOUP_HTTP_ERROR,
- SOUP_STATUS_MALFORMED,
- _("Bad file format."));
- g_object_unref (soup_message);
- empty_cache (backend);
- return FALSE;
- }
-
- if (icalcomponent_isa (icalcomp) != ICAL_VCALENDAR_COMPONENT) {
- g_set_error (
- error, SOUP_HTTP_ERROR,
- SOUP_STATUS_MALFORMED,
- _("Not a calendar."));
- icalcomponent_free (icalcomp);
- g_object_unref (soup_message);
- empty_cache (backend);
- return FALSE;
- }
-
- g_object_unref (soup_message);
- soup_message = NULL;
-
- /* Update cache */
- old_cache = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
-
- comps_in_cache = e_cal_backend_store_get_components (priv->store);
- while (comps_in_cache != NULL) {
- const gchar *uid;
- ECalComponent *comp = comps_in_cache->data;
-
- e_cal_component_get_uid (comp, &uid);
- g_hash_table_insert (old_cache, g_strdup (uid), e_cal_component_get_as_string (comp));
+ g_clear_object (&message);
+ } else {
+ e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
- comps_in_cache = g_slist_remove (comps_in_cache, comps_in_cache->data);
- g_object_unref (comp);
+ g_set_error (error, E_DATA_CAL_ERROR, OtherError, _("Malformed URI “%s“: %s"),
+ uri, local_error ? local_error->message : _("Unknown error"));
}
- kind = e_cal_backend_get_kind (E_CAL_BACKEND (backend));
- subcomp = icalcomponent_get_first_component (icalcomp, ICAL_ANY_COMPONENT);
- e_cal_backend_store_freeze_changes (priv->store);
- while (subcomp) {
- ECalComponent *comp;
- icalcomponent_kind subcomp_kind;
- icalproperty *prop = NULL;
-
- subcomp_kind = icalcomponent_isa (subcomp);
- prop = icalcomponent_get_first_property (subcomp, ICAL_UID_PROPERTY);
- if (!prop && subcomp_kind == kind) {
- gchar *new_uid = e_cal_component_gen_uid ();
- icalcomponent_set_uid (subcomp, new_uid);
- g_free (new_uid);
- }
-
- if (subcomp_kind == kind) {
- comp = e_cal_component_new ();
- if (e_cal_component_set_icalcomponent (comp, icalcomponent_new_clone (subcomp))) {
- const gchar *uid;
- gpointer orig_key, orig_value;
-
- e_cal_component_get_uid (comp, &uid);
-
- if (!put_component_to_store (backend, comp)) {
- g_hash_table_remove (old_cache, uid);
- } else if (g_hash_table_lookup_extended (old_cache, uid, &orig_key,
&orig_value)) {
- ECalComponent *orig_comp = e_cal_component_new_from_string
(orig_value);
-
- e_cal_backend_notify_component_modified (E_CAL_BACKEND (backend),
orig_comp, comp);
-
- g_hash_table_remove (old_cache, uid);
- if (orig_comp)
- g_object_unref (orig_comp);
- } else {
- e_cal_backend_notify_component_created (E_CAL_BACKEND (backend),
comp);
- }
- }
-
- g_object_unref (comp);
- } else if (subcomp_kind == ICAL_VTIMEZONE_COMPONENT) {
- icaltimezone *zone;
+ if (success) {
+ cbhttp->priv->request = request;
+ cbhttp->priv->input_stream = input_stream;
- zone = icaltimezone_new ();
- icaltimezone_set_component (zone, icalcomponent_new_clone (subcomp));
- e_timezone_cache_add_timezone (timezone_cache, zone);
-
- icaltimezone_free (zone, 1);
- }
-
- subcomp = icalcomponent_get_next_component (icalcomp, ICAL_ANY_COMPONENT);
+ *out_auth_result = E_SOURCE_AUTHENTICATION_ACCEPTED;
+ } else {
+ g_clear_object (&request);
+ g_clear_object (&input_stream);
}
- e_cal_backend_store_thaw_changes (priv->store);
-
- /* notify the removals */
- g_hash_table_foreach_remove (old_cache, (GHRFunc) notify_and_remove_from_cache, backend);
- g_hash_table_destroy (old_cache);
-
- /* free memory */
- icalcomponent_free (icalcomp);
-
- priv->opened = TRUE;
+ g_clear_error (&local_error);
+ g_free (uri);
- return TRUE;
+ return success;
}
-static const gchar *
-cal_backend_http_ensure_uri (ECalBackendHttp *backend)
+static gboolean
+ecb_http_disconnect_sync (ECalMetaBackend *meta_backend,
+ GCancellable *cancellable,
+ GError **error)
{
+ ECalBackendHttp *cbhttp;
ESource *source;
- ESourceSecurity *security_extension;
- ESourceWebdav *webdav_extension;
- SoupURI *soup_uri;
- gboolean secure_connection;
- const gchar *extension_name;
- gchar *uri_string;
- if (backend->priv->uri != NULL)
- return backend->priv->uri;
+ g_return_val_if_fail (E_IS_CAL_BACKEND_HTTP (meta_backend), FALSE);
- source = e_backend_get_source (E_BACKEND (backend));
+ cbhttp = E_CAL_BACKEND_HTTP (meta_backend);
- extension_name = E_SOURCE_EXTENSION_SECURITY;
- security_extension = e_source_get_extension (source, extension_name);
+ if (cbhttp->priv->session)
+ soup_session_abort (SOUP_SESSION (cbhttp->priv->session));
- extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
- webdav_extension = e_source_get_extension (source, extension_name);
+ g_clear_object (&cbhttp->priv->input_stream);
+ g_clear_object (&cbhttp->priv->request);
- secure_connection = e_source_security_get_secure (security_extension);
-
- soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
- uri_string = soup_uri_to_string (soup_uri, FALSE);
- soup_uri_free (soup_uri);
-
- backend->priv->uri = webcal_to_http_method (
- uri_string, secure_connection);
+ if (cbhttp->priv->components) {
+ g_hash_table_destroy (cbhttp->priv->components);
+ cbhttp->priv->components = NULL;
+ }
- g_free (uri_string);
+ source = e_backend_get_source (E_BACKEND (meta_backend));
+ e_source_set_connection_status (source, E_SOURCE_CONNECTION_STATUS_DISCONNECTED);
- return backend->priv->uri;
+ return TRUE;
}
-static void
-begin_retrieval_cb (GTask *task,
- gpointer source_object,
- gpointer task_tada,
- GCancellable *cancellable)
+static gchar *
+ecb_http_read_stream_sync (GInputStream *input_stream,
+ goffset expected_length,
+ GCancellable *cancellable,
+ GError **error)
{
- ECalBackendHttp *backend = source_object;
- const gchar *uri;
- gchar *certificate_pem = NULL;
- GTlsCertificateFlags certificate_errors = 0;
- GError *error = NULL;
-
- if (!e_backend_get_online (E_BACKEND (backend)) ||
- backend->priv->is_loading)
- return;
-
- d (g_message ("Starting retrieval...\n"));
-
- backend->priv->is_loading = TRUE;
-
- uri = cal_backend_http_ensure_uri (backend);
- cal_backend_http_load (backend, uri, &certificate_pem, &certificate_errors, cancellable, &error);
-
- if (g_error_matches (error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED) ||
- g_error_matches (error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
- GError *local_error = NULL;
- ESourceCredentialsReason reason = E_SOURCE_CREDENTIALS_REASON_REQUIRED;
-
- if (g_error_matches (error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
- reason = E_SOURCE_CREDENTIALS_REASON_SSL_FAILED;
- }
-
- e_backend_credentials_required_sync (E_BACKEND (backend),
- reason, certificate_pem, certificate_errors, error, cancellable, &local_error);
-
- g_clear_error (&error);
- error = local_error;
- } else if (g_error_matches (error, SOUP_HTTP_ERROR, SOUP_STATUS_FORBIDDEN)) {
- GError *local_error = NULL;
+ GString *icalstr;
+ void *buffer;
+ gsize nread = 0;
+ gboolean success = FALSE;
- e_backend_credentials_required_sync (E_BACKEND (backend),
E_SOURCE_CREDENTIALS_REASON_REJECTED,
- certificate_pem, certificate_errors, error, cancellable, &local_error);
+ g_return_val_if_fail (G_IS_INPUT_STREAM (input_stream), NULL);
- g_clear_error (&error);
- error = local_error;
- }
-
- g_free (certificate_pem);
- backend->priv->is_loading = FALSE;
+ icalstr = g_string_sized_new (expected_length > 0 ? expected_length + 1 : 1024);
- /* Ignore cancellations. */
- if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
- g_error_free (error);
+ buffer = g_malloc (16384);
- } else if (error != NULL) {
- e_cal_backend_notify_error (
- E_CAL_BACKEND (backend),
- error->message);
- empty_cache (backend);
- g_error_free (error);
+ while (success = g_input_stream_read_all (input_stream, buffer, 16384, &nread, cancellable, error),
+ success && nread > 0) {
+ g_string_append_len (icalstr, (const gchar *) buffer, nread);
}
- d (g_message ("Retrieval really done.\n"));
-}
-
-static void
-http_cal_schedule_begin_retrieval (ECalBackendHttp *cbhttp)
-{
- GTask *task;
-
- task = g_task_new (cbhttp, NULL, NULL, NULL);
+ g_free (buffer);
- g_task_run_in_thread (task, begin_retrieval_cb);
-
- g_object_unref (task);
+ return g_string_free (icalstr, !success);
}
-static void
-source_changed_cb (ESource *source,
- ECalBackendHttp *cbhttp)
+static gboolean
+ecb_http_get_changes_sync (ECalMetaBackend *meta_backend,
+ const gchar *last_sync_tag,
+ gboolean is_repeat,
+ gchar **out_new_sync_tag,
+ gboolean *out_repeat,
+ GSList **out_created_objects,
+ GSList **out_modified_objects,
+ GSList **out_removed_objects,
+ GCancellable *cancellable,
+ GError **error)
{
- g_return_if_fail (E_IS_CAL_BACKEND_HTTP (cbhttp));
-
- g_object_ref (cbhttp);
-
- if (cbhttp->priv->uri != NULL) {
- gboolean uri_changed;
- const gchar *new_uri;
- gchar *old_uri;
-
- old_uri = g_strdup (cbhttp->priv->uri);
-
- g_free (cbhttp->priv->uri);
- cbhttp->priv->uri = NULL;
-
- new_uri = cal_backend_http_ensure_uri (cbhttp);
+ ECalBackendHttp *cbhttp;
+ SoupMessage *message;
+ gchar *icalstring;
+ icalcomponent *vcalendar;
+ gboolean success = TRUE;
- uri_changed = (g_strcmp0 (old_uri, new_uri) != 0);
+ g_return_val_if_fail (E_IS_CAL_BACKEND_HTTP (meta_backend), FALSE);
+ g_return_val_if_fail (out_new_sync_tag != NULL, FALSE);
+ g_return_val_if_fail (out_created_objects != NULL, FALSE);
+ g_return_val_if_fail (out_modified_objects != NULL, FALSE);
+ g_return_val_if_fail (out_removed_objects != NULL, FALSE);
- if (uri_changed && !cbhttp->priv->is_loading)
- http_cal_schedule_begin_retrieval (cbhttp);
+ cbhttp = E_CAL_BACKEND_HTTP (meta_backend);
- g_free (old_uri);
+ if (!cbhttp->priv->request || !cbhttp->priv->input_stream) {
+ g_propagate_error (error, EDC_ERROR (RepositoryOffline));
+ return FALSE;
}
- g_object_unref (cbhttp);
-}
+ message = soup_request_http_get_message (cbhttp->priv->request);
+ if (message) {
+ const gchar *new_etag;
-static void
-http_cal_reload_cb (ESource *source,
- gpointer user_data)
-{
- ECalBackendHttp *cbhttp = user_data;
+ new_etag = soup_message_headers_get_one (message->response_headers, "ETag");
+ if (new_etag && !*new_etag) {
+ new_etag = NULL;
+ } else if (g_strcmp0 (last_sync_tag, new_etag) == 0) {
+ /* Nothing changed */
+ g_object_unref (message);
- g_return_if_fail (E_IS_CAL_BACKEND_HTTP (cbhttp));
+ ecb_http_disconnect_sync (meta_backend, cancellable, NULL);
- if (!e_backend_get_online (E_BACKEND (cbhttp)))
- return;
-
- http_cal_schedule_begin_retrieval (cbhttp);
-}
+ return TRUE;
+ }
-/* Open handler for the file backend */
-static void
-e_cal_backend_http_open (ECalBackendSync *backend,
- EDataCal *cal,
- GCancellable *cancellable,
- gboolean only_if_exists,
- GError **perror)
-{
- ECalBackendHttp *cbhttp;
- ECalBackendHttpPrivate *priv;
- ESource *source;
- ESourceWebdav *webdav_extension;
- const gchar *extension_name;
- const gchar *cache_dir;
- gboolean opened = TRUE;
- gchar *tmp;
- GError *local_error = NULL;
+ *out_new_sync_tag = g_strdup (new_etag);
+ }
- cbhttp = E_CAL_BACKEND_HTTP (backend);
- priv = cbhttp->priv;
+ g_clear_object (&message);
- /* already opened, thus can skip all this initialization */
- if (priv->opened)
- return;
+ icalstring = ecb_http_read_stream_sync (cbhttp->priv->input_stream,
+ soup_request_get_content_length (SOUP_REQUEST (cbhttp->priv->request)), cancellable, error);
- source = e_backend_get_source (E_BACKEND (backend));
- cache_dir = e_cal_backend_get_cache_dir (E_CAL_BACKEND (backend));
+ if (!icalstring) {
+ /* The error is already set */
+ e_cal_meta_backend_empty_cache_sync (meta_backend, cancellable, NULL);
+ ecb_http_disconnect_sync (meta_backend, cancellable, NULL);
+ return FALSE;
+ }
- extension_name = E_SOURCE_EXTENSION_WEBDAV_BACKEND;
- webdav_extension = e_source_get_extension (source, extension_name);
+ vcalendar = icalparser_parse_string (icalstring);
- e_source_webdav_unset_temporary_ssl_trust (webdav_extension);
+ g_free (icalstring);
- if (priv->source_changed_id == 0) {
- priv->source_changed_id = g_signal_connect (
- source, "changed",
- G_CALLBACK (source_changed_cb), cbhttp);
+ if (!vcalendar) {
+ g_set_error (error, SOUP_HTTP_ERROR, SOUP_STATUS_MALFORMED, _("Bad file format."));
+ e_cal_meta_backend_empty_cache_sync (meta_backend, cancellable, NULL);
+ ecb_http_disconnect_sync (meta_backend, cancellable, NULL);
+ return FALSE;
}
- /* always read uri again */
- tmp = priv->uri;
- priv->uri = NULL;
- g_free (tmp);
-
- if (priv->store == NULL) {
- /* remove the old cache while migrating to ECalBackendStore */
- e_cal_backend_cache_remove (cache_dir, "cache.xml");
- priv->store = e_cal_backend_store_new (
- cache_dir, E_TIMEZONE_CACHE (backend));
- e_cal_backend_store_load (priv->store);
-
- if (!priv->store) {
- g_propagate_error (
- perror, EDC_ERROR_EX (OtherError,
- _("Could not create cache file")));
- return;
- }
+ if (icalcomponent_isa (vcalendar) != ICAL_VCALENDAR_COMPONENT) {
+ icalcomponent_free (vcalendar);
+ g_set_error (error, SOUP_HTTP_ERROR, SOUP_STATUS_MALFORMED, _("Not a calendar."));
+ e_cal_meta_backend_empty_cache_sync (meta_backend, cancellable, NULL);
+ ecb_http_disconnect_sync (meta_backend, cancellable, NULL);
+ return FALSE;
}
- e_cal_backend_set_writable (E_CAL_BACKEND (backend), FALSE);
-
- if (e_backend_get_online (E_BACKEND (backend))) {
- gchar *certificate_pem = NULL;
- GTlsCertificateFlags certificate_errors = 0;
- const gchar *uri;
+ success = e_cal_meta_backend_gather_timezones_sync (meta_backend, vcalendar, TRUE, cancellable,
error);
+ if (success) {
+ icalcomponent_kind kind = e_cal_backend_get_kind (E_CAL_BACKEND (meta_backend));
+ icalcomponent *subcomp;
+ GHashTable *components;
- uri = cal_backend_http_ensure_uri (cbhttp);
+ components = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify)
icalcomponent_free);
- opened = cal_backend_http_load (cbhttp, uri, &certificate_pem,
- &certificate_errors, cancellable, &local_error);
+ while (subcomp = icalcomponent_get_first_component (vcalendar, kind), subcomp) {
+ icalcomponent *existing_icalcomp;
+ gpointer orig_key, orig_value;
+ const gchar *uid;
- if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED) ||
- g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED) ||
- (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_FORBIDDEN) &&
- !cbhttp->priv->password)) {
- GError *local_error2 = NULL;
- ESourceCredentialsReason reason = E_SOURCE_CREDENTIALS_REASON_REQUIRED;
+ icalcomponent_remove_component (vcalendar, subcomp);
- if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
- reason = E_SOURCE_CREDENTIALS_REASON_SSL_FAILED;
+ if (!icalcomponent_get_first_property (subcomp, ICAL_UID_PROPERTY)) {
+ gchar *new_uid = e_cal_component_gen_uid ();
+ icalcomponent_set_uid (subcomp, new_uid);
+ g_free (new_uid);
}
- e_backend_credentials_required_sync (E_BACKEND (cbhttp), reason, certificate_pem,
- certificate_errors, local_error, cancellable, &local_error2);
- g_clear_error (&local_error);
- local_error = local_error2;
- } else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_FORBIDDEN)) {
- GError *local_error2 = NULL;
+ uid = icalcomponent_get_uid (subcomp);
- e_backend_credentials_required_sync (E_BACKEND (cbhttp),
E_SOURCE_CREDENTIALS_REASON_REJECTED,
- certificate_pem, certificate_errors, local_error, cancellable, &local_error2);
-
- g_clear_error (&local_error);
- local_error = local_error2;
- }
-
- g_free (certificate_pem);
-
- if (local_error != NULL)
- g_propagate_error (perror, local_error);
- }
-
- if (opened) {
- if (!priv->reload_timeout_id)
- priv->reload_timeout_id = e_source_refresh_add_timeout (source, NULL,
http_cal_reload_cb, backend, NULL);
- }
-}
-
-static void
-e_cal_backend_http_refresh (ECalBackendSync *backend,
- EDataCal *cal,
- GCancellable *cancellable,
- GError **perror)
-{
- ECalBackendHttp *cbhttp;
- ECalBackendHttpPrivate *priv;
- ESource *source;
-
- cbhttp = E_CAL_BACKEND_HTTP (backend);
- priv = cbhttp->priv;
-
- if (!priv->opened ||
- priv->is_loading)
- return;
-
- source = e_backend_get_source (E_BACKEND (cbhttp));
- g_return_if_fail (source != NULL);
-
- e_source_refresh_force_timeout (source);
-}
+ if (!g_hash_table_lookup_extended (components, uid, &orig_key, &orig_value)) {
+ orig_key = NULL;
+ orig_value = NULL;
+ }
-/* Set_mode handler for the http backend */
-static void
-e_cal_backend_http_notify_online_cb (ECalBackend *backend,
- GParamSpec *pspec)
-{
- gboolean loaded;
- gboolean online;
+ existing_icalcomp = orig_value;
+ if (existing_icalcomp) {
+ if (icalcomponent_isa (existing_icalcomp) != ICAL_VCALENDAR_COMPONENT) {
+ icalcomponent *vcal;
- online = e_backend_get_online (E_BACKEND (backend));
- loaded = e_cal_backend_is_opened (backend);
+ vcal = e_cal_util_new_top_level ();
- if (online && loaded)
- http_cal_schedule_begin_retrieval (E_CAL_BACKEND_HTTP (backend));
-}
+ g_warn_if_fail (g_hash_table_steal (components, uid));
-/* Get_object_component handler for the http backend */
-static void
-e_cal_backend_http_get_object (ECalBackendSync *backend,
- EDataCal *cal,
- GCancellable *cancellable,
- const gchar *uid,
- const gchar *rid,
- gchar **object,
- GError **error)
-{
- ECalBackendHttp *cbhttp;
- ECalBackendHttpPrivate *priv;
- ECalComponent *comp = NULL;
+ icalcomponent_add_component (vcal, existing_icalcomp);
+ g_hash_table_insert (components, g_strdup (uid), vcal);
- cbhttp = E_CAL_BACKEND_HTTP (backend);
- priv = cbhttp->priv;
+ g_free (orig_key);
- if (!priv->store) {
- g_propagate_error (error, EDC_ERROR (ObjectNotFound));
- return;
- }
+ existing_icalcomp = vcal;
+ }
- if (rid && *rid) {
- comp = e_cal_backend_store_get_component (priv->store, uid, rid);
- if (!comp) {
- g_propagate_error (error, EDC_ERROR (ObjectNotFound));
- return;
+ icalcomponent_add_component (existing_icalcomp, subcomp);
+ } else {
+ g_hash_table_insert (components, g_strdup (uid), subcomp);
+ }
}
- *object = e_cal_component_get_as_string (comp);
- g_object_unref (comp);
- } else {
- *object = e_cal_backend_store_get_components_by_uid_as_ical_string (priv->store, uid);
- if (!*object)
- g_propagate_error (error, EDC_ERROR (ObjectNotFound));
- }
-}
-
-/* Add_timezone handler for the file backend */
-static void
-e_cal_backend_http_add_timezone (ECalBackendSync *backend,
- EDataCal *cal,
- GCancellable *cancellable,
- const gchar *tzobj,
- GError **error)
-{
- ETimezoneCache *timezone_cache;
- icalcomponent *tz_comp;
- icaltimezone *zone;
+ g_warn_if_fail (cbhttp->priv->components == NULL);
+ cbhttp->priv->components = components;
- timezone_cache = E_TIMEZONE_CACHE (backend);
+ icalcomponent_free (vcalendar);
- tz_comp = icalparser_parse_string (tzobj);
- if (!tz_comp) {
- g_propagate_error (error, EDC_ERROR (InvalidObject));
- return;
+ success = E_CAL_META_BACKEND_CLASS (e_cal_backend_http_parent_class)->get_changes_sync
(meta_backend,
+ last_sync_tag, is_repeat, out_new_sync_tag, out_repeat, out_created_objects,
+ out_modified_objects, out_removed_objects, cancellable, error);
+ } else {
+ icalcomponent_free (vcalendar);
}
- if (icalcomponent_isa (tz_comp) != ICAL_VTIMEZONE_COMPONENT) {
- icalcomponent_free (tz_comp);
- g_propagate_error (error, EDC_ERROR (InvalidObject));
- return;
- }
+ if (!success)
+ ecb_http_disconnect_sync (meta_backend, cancellable, NULL);
- zone = icaltimezone_new ();
- icaltimezone_set_component (zone, tz_comp);
- e_timezone_cache_add_timezone (timezone_cache, zone);
+ return success;
}
-/* Get_objects_in_range handler for the file backend */
-static void
-e_cal_backend_http_get_object_list (ECalBackendSync *backend,
- EDataCal *cal,
- GCancellable *cancellable,
- const gchar *sexp,
- GSList **objects,
- GError **perror)
+static gboolean
+ecb_http_list_existing_sync (ECalMetaBackend *meta_backend,
+ gchar **out_new_sync_tag,
+ GSList **out_existing_objects, /* ECalMetaBackendInfo * */
+ GCancellable *cancellable,
+ GError **error)
{
ECalBackendHttp *cbhttp;
- ECalBackendHttpPrivate *priv;
- GSList *components, *l;
- ECalBackendSExp *cbsexp;
- ETimezoneCache *timezone_cache;
- time_t occur_start = -1, occur_end = -1;
- gboolean prunning_by_time;
-
- cbhttp = E_CAL_BACKEND_HTTP (backend);
- priv = cbhttp->priv;
-
- timezone_cache = E_TIMEZONE_CACHE (backend);
-
- if (!priv->store) {
- g_propagate_error (perror, EDC_ERROR (NoSuchCal));
- return;
- }
-
- /* process all components in the cache */
- cbsexp = e_cal_backend_sexp_new (sexp);
-
- *objects = NULL;
- prunning_by_time = e_cal_backend_sexp_evaluate_occur_times (
- cbsexp,
- &occur_start,
- &occur_end);
+ ECalCache *cal_cache;
+ icalcomponent_kind kind;
+ GHashTableIter iter;
+ gpointer key, value;
- components = prunning_by_time ?
- e_cal_backend_store_get_components_occuring_in_range (priv->store, occur_start, occur_end)
- : e_cal_backend_store_get_components (priv->store);
+ g_return_val_if_fail (E_IS_CAL_BACKEND_HTTP (meta_backend), FALSE);
+ g_return_val_if_fail (out_existing_objects != NULL, FALSE);
- for (l = components; l != NULL; l = g_slist_next (l)) {
- if (e_cal_backend_sexp_match_comp (cbsexp, E_CAL_COMPONENT (l->data), timezone_cache)) {
- *objects = g_slist_append (*objects, e_cal_component_get_as_string (l->data));
- }
- }
+ cbhttp = E_CAL_BACKEND_HTTP (meta_backend);
- g_slist_foreach (components, (GFunc) g_object_unref, NULL);
- g_slist_free (components);
- g_object_unref (cbsexp);
-}
+ *out_existing_objects = NULL;
-static void
-e_cal_backend_http_start_view (ECalBackend *backend,
- EDataCalView *query)
-{
- ECalBackendHttp *cbhttp;
- ECalBackendHttpPrivate *priv;
- GSList *components, *l;
- GSList *objects = NULL;
- ECalBackendSExp *cbsexp;
- ETimezoneCache *timezone_cache;
- time_t occur_start = -1, occur_end = -1;
- gboolean prunning_by_time;
+ g_return_val_if_fail (cbhttp->priv->components != NULL, FALSE);
- cbhttp = E_CAL_BACKEND_HTTP (backend);
- priv = cbhttp->priv;
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ g_return_val_if_fail (cal_cache != NULL, FALSE);
- timezone_cache = E_TIMEZONE_CACHE (backend);
+ kind = e_cal_backend_get_kind (E_CAL_BACKEND (meta_backend));
- cbsexp = e_data_cal_view_get_sexp (query);
+ g_hash_table_iter_init (&iter, cbhttp->priv->components);
+ while (g_hash_table_iter_next (&iter, &key, &value)) {
+ icalcomponent *icalcomp = value;
+ ECalMetaBackendInfo *nfo;
+ const gchar *uid;
+ gchar *revision, *object;
- d (g_message (G_STRLOC ": Starting query (%s)", e_cal_backend_sexp_text (cbsexp)));
+ if (icalcomp && icalcomponent_isa (icalcomp) == ICAL_VCALENDAR_COMPONENT)
+ icalcomp = icalcomponent_get_first_component (icalcomp, kind);
- if (!priv->store) {
- GError *error = EDC_ERROR (NoSuchCal);
- e_data_cal_view_notify_complete (query, error);
- g_error_free (error);
- return;
- }
+ if (!icalcomp)
+ continue;
- /* process all components in the cache */
- objects = NULL;
- prunning_by_time = e_cal_backend_sexp_evaluate_occur_times (
- cbsexp,
- &occur_start,
- &occur_end);
+ uid = icalcomponent_get_uid (icalcomp);
+ revision = e_cal_cache_dup_component_revision (cal_cache, icalcomp);
+ object = icalcomponent_as_ical_string_r (value);
- components = prunning_by_time ?
- e_cal_backend_store_get_components_occuring_in_range (priv->store, occur_start, occur_end)
- : e_cal_backend_store_get_components (priv->store);
+ nfo = e_cal_meta_backend_info_new (uid, revision, object, NULL);
- for (l = components; l != NULL; l = g_slist_next (l)) {
- ECalComponent *comp = l->data;
+ *out_existing_objects = g_slist_prepend (*out_existing_objects, nfo);
- if (e_cal_backend_sexp_match_comp (cbsexp, comp, timezone_cache)) {
- objects = g_slist_append (objects, comp);
- }
+ g_free (revision);
+ g_free (object);
}
- e_data_cal_view_notify_components_added (query, objects);
+ g_object_unref (cal_cache);
- g_slist_free_full (components, g_object_unref);
- g_slist_free (objects);
+ ecb_http_disconnect_sync (meta_backend, cancellable, NULL);
- e_data_cal_view_notify_complete (query, NULL /* Success */);
+ return TRUE;
}
-/***** static icaltimezone *
-resolve_tzid (const gchar *tzid,
- * gpointer user_data)
-{
- icalcomponent *vcalendar_comp = user_data;
- *
- if (!tzid || !tzid[0])
- return NULL;
- * else if (!strcmp (tzid, "UTC"))
- return icaltimezone_get_utc_timezone ();
- *
- return icalcomponent_get_timezone (vcalendar_comp, tzid);
-} *****/
-
static gboolean
-free_busy_instance (ECalComponent *comp,
- time_t instance_start,
- time_t instance_end,
- gpointer data)
+ecb_http_load_component_sync (ECalMetaBackend *meta_backend,
+ const gchar *uid,
+ const gchar *extra,
+ icalcomponent **out_component,
+ gchar **out_extra,
+ GCancellable *cancellable,
+ GError **error)
{
- icalcomponent *vfb = data;
- icalproperty *prop;
- icalparameter *param;
- struct icalperiodtype ipt;
- icaltimezone *utc_zone;
-
- utc_zone = icaltimezone_get_utc_timezone ();
-
- ipt.start = icaltime_from_timet_with_zone (instance_start, FALSE, utc_zone);
- ipt.end = icaltime_from_timet_with_zone (instance_end, FALSE, utc_zone);
- ipt.duration = icaldurationtype_null_duration ();
-
- /* add busy information to the vfb component */
- prop = icalproperty_new (ICAL_FREEBUSY_PROPERTY);
- icalproperty_set_freebusy (prop, ipt);
+ ECalBackendHttp *cbhttp;
+ gpointer key = NULL, value = NULL;
- param = icalparameter_new_fbtype (ICAL_FBTYPE_BUSY);
- icalproperty_add_parameter (prop, param);
+ g_return_val_if_fail (E_IS_CAL_BACKEND_HTTP (meta_backend), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (out_component != NULL, FALSE);
- icalcomponent_add_property (vfb, prop);
+ cbhttp = E_CAL_BACKEND_HTTP (meta_backend);
+ g_return_val_if_fail (cbhttp->priv->components != NULL, FALSE);
- return TRUE;
-}
-
-static icalcomponent *
-create_user_free_busy (ECalBackendHttp *cbhttp,
- const gchar *address,
- const gchar *cn,
- time_t start,
- time_t end)
-{
- GSList *slist = NULL, *l;
- icalcomponent *vfb;
- icaltimezone *utc_zone;
- ECalBackendSExp *obj_sexp;
- ECalBackendHttpPrivate *priv;
- ECalBackendStore *store;
- gchar *query, *iso_start, *iso_end;
-
- priv = cbhttp->priv;
- store = priv->store;
-
- /* create the (unique) VFREEBUSY object that we'll return */
- vfb = icalcomponent_new_vfreebusy ();
- if (address != NULL) {
- icalproperty *prop;
- icalparameter *param;
-
- prop = icalproperty_new_organizer (address);
- if (prop != NULL && cn != NULL) {
- param = icalparameter_new_cn (cn);
- icalproperty_add_parameter (prop, param);
- }
- if (prop != NULL)
- icalcomponent_add_property (vfb, prop);
+ if (!cbhttp->priv->components ||
+ !g_hash_table_contains (cbhttp->priv->components, uid)) {
+ g_propagate_error (error, EDC_ERROR (ObjectNotFound));
+ return FALSE;
}
- utc_zone = icaltimezone_get_utc_timezone ();
- icalcomponent_set_dtstart (vfb, icaltime_from_timet_with_zone (start, FALSE, utc_zone));
- icalcomponent_set_dtend (vfb, icaltime_from_timet_with_zone (end, FALSE, utc_zone));
-
- /* add all objects in the given interval */
- iso_start = isodate_from_time_t (start);
- iso_end = isodate_from_time_t (end);
- query = g_strdup_printf (
- "occur-in-time-range? (make-time \"%s\") (make-time \"%s\")",
- iso_start, iso_end);
- obj_sexp = e_cal_backend_sexp_new (query);
- g_free (query);
- g_free (iso_start);
- g_free (iso_end);
-
- if (!obj_sexp)
- return vfb;
-
- slist = e_cal_backend_store_get_components (store);
-
- for (l = slist; l; l = g_slist_next (l)) {
- ECalComponent *comp = l->data;
- icalcomponent *icalcomp, *vcalendar_comp;
- icalproperty *prop;
-
- icalcomp = e_cal_component_get_icalcomponent (comp);
- if (!icalcomp)
- continue;
- /* If the event is TRANSPARENT, skip it. */
- prop = icalcomponent_get_first_property (
- icalcomp,
- ICAL_TRANSP_PROPERTY);
- if (prop) {
- icalproperty_transp transp_val = icalproperty_get_transp (prop);
- if (transp_val == ICAL_TRANSP_TRANSPARENT ||
- transp_val == ICAL_TRANSP_TRANSPARENTNOCONFLICT)
- continue;
- }
+ g_warn_if_fail (g_hash_table_lookup_extended (cbhttp->priv->components, uid, &key, &value));
+ g_warn_if_fail (g_hash_table_steal (cbhttp->priv->components, uid));
- if (!e_cal_backend_sexp_match_comp (
- obj_sexp, l->data,
- E_TIMEZONE_CACHE (cbhttp)))
- continue;
-
- vcalendar_comp = icalcomponent_get_parent (icalcomp);
- if (!vcalendar_comp)
- vcalendar_comp = icalcomp;
- e_cal_recur_generate_instances (
- comp, start, end,
- free_busy_instance,
- vfb,
- resolve_tzid,
- vcalendar_comp,
- icaltimezone_get_utc_timezone ());
- }
- g_object_unref (obj_sexp);
+ *out_component = value;
- return vfb;
-}
+ g_free (key);
-/* Get_free_busy handler for the file backend */
-static void
-e_cal_backend_http_get_free_busy (ECalBackendSync *backend,
- EDataCal *cal,
- GCancellable *cancellable,
- const GSList *users,
- time_t start,
- time_t end,
- GSList **freebusy,
- GError **error)
-{
- ESourceRegistry *registry;
- ECalBackendHttp *cbhttp;
- ECalBackendHttpPrivate *priv;
- gchar *address, *name;
- icalcomponent *vfb;
- gchar *calobj;
+ if (!g_hash_table_size (cbhttp->priv->components)) {
+ g_hash_table_destroy (cbhttp->priv->components);
+ cbhttp->priv->components = NULL;
- cbhttp = E_CAL_BACKEND_HTTP (backend);
- priv = cbhttp->priv;
-
- if (!priv->store) {
- g_propagate_error (error, EDC_ERROR (NoSuchCal));
- return;
+ ecb_http_disconnect_sync (meta_backend, cancellable, NULL);
}
- registry = e_cal_backend_get_registry (E_CAL_BACKEND (backend));
-
- if (users == NULL) {
- if (e_cal_backend_mail_account_get_default (registry, &address, &name)) {
- vfb = create_user_free_busy (cbhttp, address, name, start, end);
- calobj = icalcomponent_as_ical_string_r (vfb);
- *freebusy = g_slist_append (*freebusy, calobj);
- icalcomponent_free (vfb);
- g_free (address);
- g_free (name);
- }
- } else {
- const GSList *l;
- for (l = users; l != NULL; l = l->next ) {
- address = l->data;
- if (e_cal_backend_mail_account_is_valid (registry, address, &name)) {
- vfb = create_user_free_busy (cbhttp, address, name, start, end);
- calobj = icalcomponent_as_ical_string_r (vfb);
- *freebusy = g_slist_append (*freebusy, calobj);
- icalcomponent_free (vfb);
- g_free (name);
- }
- }
- }
+ return value != NULL;
}
static void
-e_cal_backend_http_create_objects (ECalBackendSync *backend,
- EDataCal *cal,
- GCancellable *cancellable,
- const GSList *calobjs,
- GSList **uids,
- GSList **new_components,
- GError **perror)
+e_cal_backend_http_constructed (GObject *object)
{
- g_propagate_error (perror, EDC_ERROR (PermissionDenied));
-}
+ ECalBackendHttp *cbhttp = E_CAL_BACKEND_HTTP (object);
-static void
-e_cal_backend_http_modify_objects (ECalBackendSync *backend,
- EDataCal *cal,
- GCancellable *cancellable,
- const GSList *calobjs,
- ECalObjModType mod,
- GSList **old_components,
- GSList **new_components,
- GError **perror)
-{
- g_propagate_error (perror, EDC_ERROR (PermissionDenied));
-}
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_cal_backend_http_parent_class)->constructed (object);
-/* Remove_objects handler for the file backend */
-static void
-e_cal_backend_http_remove_objects (ECalBackendSync *backend,
- EDataCal *cal,
- GCancellable *cancellable,
- const GSList *ids,
- ECalObjModType mod,
- GSList **old_components,
- GSList **new_components,
- GError **perror)
-{
- *old_components = *new_components = NULL;
+ cbhttp->priv->session = e_soup_session_new (e_backend_get_source (E_BACKEND (cbhttp)));
- g_propagate_error (perror, EDC_ERROR (PermissionDenied));
-}
+ e_soup_session_setup_logging (cbhttp->priv->session, g_getenv ("WEBCAL_DEBUG"));
-/* Update_objects handler for the file backend. */
-static void
-e_cal_backend_http_receive_objects (ECalBackendSync *backend,
- EDataCal *cal,
- GCancellable *cancellable,
- const gchar *calobj,
- GError **perror)
-{
- g_propagate_error (perror, EDC_ERROR (PermissionDenied));
+ e_binding_bind_property (
+ cbhttp, "proxy-resolver",
+ cbhttp->priv->session, "proxy-resolver",
+ G_BINDING_SYNC_CREATE);
}
static void
-e_cal_backend_http_send_objects (ECalBackendSync *backend,
- EDataCal *cal,
- GCancellable *cancellable,
- const gchar *calobj,
- GSList **users,
- gchar **modified_calobj,
- GError **perror)
-{
- *users = NULL;
- *modified_calobj = NULL;
-
- g_propagate_error (perror, EDC_ERROR (PermissionDenied));
-}
-
-static ESourceAuthenticationResult
-e_cal_backend_http_authenticate_sync (EBackend *backend,
- const ENamedParameters *credentials,
- gchar **out_certificate_pem,
- GTlsCertificateFlags *out_certificate_errors,
- GCancellable *cancellable,
- GError **error)
+e_cal_backend_http_dispose (GObject *object)
{
ECalBackendHttp *cbhttp;
- ESourceAuthenticationResult result;
- const gchar *uri, *username;
- GError *local_error = NULL;
- cbhttp = E_CAL_BACKEND_HTTP (backend);
+ cbhttp = E_CAL_BACKEND_HTTP (object);
- g_free (cbhttp->priv->username);
- cbhttp->priv->username = NULL;
+ g_clear_object (&cbhttp->priv->request);
+ g_clear_object (&cbhttp->priv->input_stream);
- g_free (cbhttp->priv->password);
- cbhttp->priv->password = g_strdup (e_named_parameters_get (credentials,
E_SOURCE_CREDENTIAL_PASSWORD));
+ if (cbhttp->priv->session) {
+ soup_session_abort (SOUP_SESSION (cbhttp->priv->session));
+ g_clear_object (&cbhttp->priv->session);
+ }
- username = e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_USERNAME);
- if (username && *username) {
- cbhttp->priv->username = g_strdup (username);
+ if (cbhttp->priv->components) {
+ g_hash_table_destroy (cbhttp->priv->components);
+ cbhttp->priv->components = NULL;
}
- uri = cal_backend_http_ensure_uri (cbhttp);
- if (cal_backend_http_load (cbhttp, uri, out_certificate_pem, out_certificate_errors, cancellable,
&local_error)) {
- if (!cbhttp->priv->reload_timeout_id) {
- ESource *source = e_backend_get_source (backend);
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_cal_backend_http_parent_class)->dispose (object);
+}
- cbhttp->priv->reload_timeout_id = e_source_refresh_add_timeout (source, NULL,
http_cal_reload_cb, backend, NULL);
- }
- }
+static void
+e_cal_backend_http_finalize (GObject *object)
+{
+ ECalBackendHttp *cbhttp = E_CAL_BACKEND_HTTP (object);
- if (local_error == NULL) {
- result = E_SOURCE_AUTHENTICATION_ACCEPTED;
- } else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED)) {
- result = E_SOURCE_AUTHENTICATION_REJECTED;
- g_clear_error (&local_error);
- } else if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
- result = E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED;
- g_propagate_error (error, local_error);
- } else {
- result = E_SOURCE_AUTHENTICATION_ERROR;
- g_propagate_error (error, local_error);
- }
+ g_clear_object (&cbhttp->priv->session);
- return result;
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_cal_backend_http_parent_class)->finalize (object);
}
-/* Object initialization function for the file backend */
static void
e_cal_backend_http_init (ECalBackendHttp *cbhttp)
{
- cbhttp->priv = E_CAL_BACKEND_HTTP_GET_PRIVATE (cbhttp);
+ cbhttp->priv = G_TYPE_INSTANCE_GET_PRIVATE (cbhttp, E_TYPE_CAL_BACKEND_HTTP, ECalBackendHttpPrivate);
- g_signal_connect (
- cbhttp, "notify::online",
- G_CALLBACK (e_cal_backend_http_notify_online_cb), NULL);
+ e_cal_backend_set_writable (E_CAL_BACKEND (cbhttp), FALSE);
}
-/* Class initialization function for the file backend */
static void
-e_cal_backend_http_class_init (ECalBackendHttpClass *class)
+e_cal_backend_http_class_init (ECalBackendHttpClass *klass)
{
GObjectClass *object_class;
- EBackendClass *backend_class;
- ECalBackendClass *cal_backend_class;
- ECalBackendSyncClass *sync_class;
-
- g_type_class_add_private (class, sizeof (ECalBackendHttpPrivate));
-
- object_class = (GObjectClass *) class;
- backend_class = E_BACKEND_CLASS (class);
- cal_backend_class = (ECalBackendClass *) class;
- sync_class = (ECalBackendSyncClass *) class;
-
+ ECalBackendSyncClass *cal_backend_sync_class;
+ ECalMetaBackendClass *cal_meta_backend_class;
+
+ g_type_class_add_private (klass, sizeof (ECalBackendHttpPrivate));
+
+ cal_meta_backend_class = E_CAL_META_BACKEND_CLASS (klass);
+ cal_meta_backend_class->connect_sync = ecb_http_connect_sync;
+ cal_meta_backend_class->disconnect_sync = ecb_http_disconnect_sync;
+ cal_meta_backend_class->get_changes_sync = ecb_http_get_changes_sync;
+ cal_meta_backend_class->list_existing_sync = ecb_http_list_existing_sync;
+ cal_meta_backend_class->load_component_sync = ecb_http_load_component_sync;
+
+ /* Setting these methods to NULL will cause "Not supported" error,
+ which is more accurate than "Permission denied" error */
+ cal_backend_sync_class = E_CAL_BACKEND_SYNC_CLASS (klass);
+ cal_backend_sync_class->create_objects_sync = NULL;
+ cal_backend_sync_class->modify_objects_sync = NULL;
+ cal_backend_sync_class->remove_objects_sync = NULL;
+
+ object_class = G_OBJECT_CLASS (klass);
+ object_class->constructed = e_cal_backend_http_constructed;
object_class->dispose = e_cal_backend_http_dispose;
object_class->finalize = e_cal_backend_http_finalize;
- object_class->constructed = e_cal_backend_http_constructed;
-
- backend_class->authenticate_sync = e_cal_backend_http_authenticate_sync;
-
- /* Execute one method at a time. */
- cal_backend_class->use_serial_dispatch_queue = TRUE;
- cal_backend_class->get_backend_property = e_cal_backend_http_get_backend_property;
- cal_backend_class->start_view = e_cal_backend_http_start_view;
-
- sync_class->open_sync = e_cal_backend_http_open;
- sync_class->refresh_sync = e_cal_backend_http_refresh;
- sync_class->create_objects_sync = e_cal_backend_http_create_objects;
- sync_class->modify_objects_sync = e_cal_backend_http_modify_objects;
- sync_class->remove_objects_sync = e_cal_backend_http_remove_objects;
- sync_class->receive_objects_sync = e_cal_backend_http_receive_objects;
- sync_class->send_objects_sync = e_cal_backend_http_send_objects;
- sync_class->get_object_sync = e_cal_backend_http_get_object;
- sync_class->get_object_list_sync = e_cal_backend_http_get_object_list;
- sync_class->add_timezone_sync = e_cal_backend_http_add_timezone;
- sync_class->get_free_busy_sync = e_cal_backend_http_get_free_busy;
}
diff --git a/src/calendar/backends/http/e-cal-backend-http.h b/src/calendar/backends/http/e-cal-backend-http.h
index b45ff33..b7c5df3 100644
--- a/src/calendar/backends/http/e-cal-backend-http.h
+++ b/src/calendar/backends/http/e-cal-backend-http.h
@@ -47,12 +47,12 @@ typedef struct _ECalBackendHttpClass ECalBackendHttpClass;
typedef struct _ECalBackendHttpPrivate ECalBackendHttpPrivate;
struct _ECalBackendHttp {
- ECalBackendSync backend;
+ ECalMetaBackend backend;
ECalBackendHttpPrivate *priv;
};
struct _ECalBackendHttpClass {
- ECalBackendSyncClass parent_class;
+ ECalMetaBackendClass parent_class;
};
GType e_cal_backend_http_get_type (void);
diff --git a/src/calendar/libecal/e-cal-util.c b/src/calendar/libecal/e-cal-util.c
index bd10022..a9426f8 100644
--- a/src/calendar/libecal/e-cal-util.c
+++ b/src/calendar/libecal/e-cal-util.c
@@ -1496,7 +1496,6 @@ componenttime_to_utc_timet (const ECalComponentDateTime *dt_time,
if (dt_time->tzid)
zone = tz_cb (dt_time->tzid, tz_cb_data);
- // zone = icaltimezone_get_utc_timezone ();
timet = icaltime_as_timet_with_zone (
*dt_time->value, zone ? zone : default_zone);
}
@@ -1530,6 +1529,7 @@ e_cal_util_get_component_occur_times (ECalComponent *comp,
{
struct icalrecurrencetype ir;
ECalComponentDateTime dt_start, dt_end;
+ time_t duration;
g_return_if_fail (comp != NULL);
g_return_if_fail (start != NULL);
@@ -1545,6 +1545,14 @@ e_cal_util_get_component_occur_times (ECalComponent *comp,
e_cal_component_free_datetime (&dt_start);
+ e_cal_component_get_dtend (comp, &dt_end);
+ duration = componenttime_to_utc_timet (&dt_end, tz_cb, tz_cb_data, default_timezone);
+ if (duration <= 0 || *start == _TIME_MIN || *start > duration)
+ duration = 0;
+ else
+ duration = duration - *start;
+ e_cal_component_free_datetime (&dt_end);
+
/* find out end date of component */
*end = _TIME_MAX;
@@ -1578,6 +1586,8 @@ e_cal_util_get_component_occur_times (ECalComponent *comp,
} else {
/* ALARMS, EVENTS: DTEND and reccurences */
+ time_t may_end = _TIME_MIN;
+
if (e_cal_component_has_recurrences (comp)) {
GSList *rrules = NULL;
GSList *exrules = NULL;
@@ -1600,9 +1610,9 @@ e_cal_util_get_component_occur_times (ECalComponent *comp,
&ir, prop, utc_zone, TRUE);
if (rule_end == -1) /* repeats forever */
- *end = _TIME_MAX;
- else if (rule_end > *end) /* new maximum */
- *end = rule_end;
+ may_end = _TIME_MAX;
+ else if (rule_end + duration > may_end) /* new maximum */
+ may_end = rule_end + duration;
}
/* Do the EXRULEs. */
@@ -1617,9 +1627,9 @@ e_cal_util_get_component_occur_times (ECalComponent *comp,
&ir, prop, utc_zone, TRUE);
if (rule_end == -1) /* repeats forever */
- *end = _TIME_MAX;
- else if (rule_end > *end)
- *end = rule_end;
+ may_end = _TIME_MAX;
+ else if (rule_end + duration > may_end)
+ may_end = rule_end + duration;
}
/* Do the RDATEs */
@@ -1639,12 +1649,14 @@ e_cal_util_get_component_occur_times (ECalComponent *comp,
rdate_end = icaltime_as_timet (p->u.end);
if (rdate_end == -1) /* repeats forever */
- *end = _TIME_MAX;
- else if (rdate_end > *end)
- *end = rdate_end;
+ may_end = _TIME_MAX;
+ else if (rdate_end > may_end)
+ may_end = rdate_end;
}
e_cal_component_free_period_list (rdates);
+ } else if (*start != _TIME_MIN) {
+ may_end = *start;
}
/* Get dtend of the component and convert it to UTC */
@@ -1656,11 +1668,179 @@ e_cal_util_get_component_occur_times (ECalComponent *comp,
dtend_time = componenttime_to_utc_timet (
&dt_end, tz_cb, tz_cb_data, default_timezone);
- if (dtend_time == -1 || (dtend_time > *end))
- *end = dtend_time;
+ if (dtend_time == -1 || (dtend_time > may_end))
+ may_end = dtend_time;
+ } else {
+ may_end = _TIME_MAX;
}
e_cal_component_free_datetime (&dt_end);
+
+ *end = may_end == _TIME_MIN ? _TIME_MAX : may_end;
+ }
+}
+
+/**
+ * e_cal_util_find_x_property:
+ * @icalcomp: an icalcomponent
+ * @x_name: name of the X property
+ *
+ * Searches for an X property named @x_name within X properties
+ * of @icalcomp and returns it.
+ *
+ * Returns: (nullable) (transfer none): the first X icalproperty named
+ * @x_name, or %NULL, when none found. The returned structure is owned
+ * by @icalcomp.
+ *
+ * Since: 3.26
+ **/
+icalproperty *
+e_cal_util_find_x_property (icalcomponent *icalcomp,
+ const gchar *x_name)
+{
+ icalproperty *prop;
+
+ g_return_val_if_fail (icalcomp != NULL, NULL);
+ g_return_val_if_fail (x_name != NULL, NULL);
+
+ for (prop = icalcomponent_get_first_property (icalcomp, ICAL_X_PROPERTY);
+ prop;
+ prop = icalcomponent_get_next_property (icalcomp, ICAL_X_PROPERTY)) {
+ const gchar *prop_name = icalproperty_get_x_name (prop);
+
+ if (g_strcmp0 (prop_name, x_name) == 0)
+ break;
+ }
+
+ return prop;
+}
+
+/**
+ * e_cal_util_dup_x_property:
+ * @icalcomp: an icalcomponent
+ * @x_name: name of the X property
+ *
+ * Searches for an X property named @x_name within X properties
+ * of @icalcomp and returns its value as a newly allocated string.
+ * Free it with g_free(), when no longer needed.
+ *
+ * Returns: (nullable) (transfer full): Newly allocated value of the first @x_name
+ * X property in @icalcomp, or %NULL, if not found.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_cal_util_dup_x_property (icalcomponent *icalcomp,
+ const gchar *x_name)
+{
+ icalproperty *prop;
+
+ g_return_val_if_fail (icalcomp != NULL, NULL);
+ g_return_val_if_fail (x_name != NULL, NULL);
+
+ prop = e_cal_util_find_x_property (icalcomp, x_name);
+
+ if (!prop)
+ return NULL;
+
+ return icalproperty_get_value_as_string_r (prop);
+}
+
+/**
+ * e_cal_util_get_x_property:
+ * @icalcomp: an icalcomponent
+ * @x_name: name of the X property
+ *
+ * Searches for an X property named @x_name within X properties
+ * of @icalcomp and returns its value. The returned string is
+ * owned by libical. See e_cal_util_dup_x_property().
+ *
+ * Returns: (nullable) (transfer none): Value of the first @x_name
+ * X property in @icalcomp, or %NULL, if not found.
+ *
+ * Since: 3.26
+ **/
+const gchar *
+e_cal_util_get_x_property (icalcomponent *icalcomp,
+ const gchar *x_name)
+{
+ icalproperty *prop;
+
+ g_return_val_if_fail (icalcomp != NULL, NULL);
+ g_return_val_if_fail (x_name != NULL, NULL);
+
+ prop = e_cal_util_find_x_property (icalcomp, x_name);
+
+ if (!prop)
+ return NULL;
+
+ return icalproperty_get_value_as_string (prop);
+}
+
+/**
+ * e_cal_util_set_x_property:
+ * @icalcomp: an icalcomponent
+ * @x_name: name of the X property
+ * @value: (nullable): a value to set, or %NULL
+ *
+ * Sets a value of the first X property named @x_name in @icalcomp,
+ * if any such already exists, or adds a new property with this name
+ * and value. As a special case, if @value is %NULL, then removes
+ * the first X property names @x_name from @icalcomp instead.
+ *
+ * Since: 3.26
+ **/
+void
+e_cal_util_set_x_property (icalcomponent *icalcomp,
+ const gchar *x_name,
+ const gchar *value)
+{
+ icalproperty *prop;
+
+ g_return_if_fail (icalcomp != NULL);
+ g_return_if_fail (x_name != NULL);
+
+ if (!value) {
+ e_cal_util_remove_x_property (icalcomp, x_name);
+ return;
+ }
+
+ prop = e_cal_util_find_x_property (icalcomp, x_name);
+ if (prop) {
+ icalproperty_set_value_from_string (prop, value, "NO");
+ } else {
+ prop = icalproperty_new_x (value);
+ icalproperty_set_x_name (prop, x_name);
+ icalcomponent_add_property (icalcomp, prop);
}
}
+/**
+ * e_cal_util_remove_x_property:
+ * @icalcomp: an icalcomponent
+ * @x_name: name of the X property
+ *
+ * Removes the first X property named @x_name in @icalcomp.
+ *
+ * Returns: %TRUE, when any such had been found and removed, %FALSE otherwise.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_util_remove_x_property (icalcomponent *icalcomp,
+ const gchar *x_name)
+{
+ icalproperty *prop;
+
+ g_return_val_if_fail (icalcomp != NULL, FALSE);
+ g_return_val_if_fail (x_name != NULL, FALSE);
+
+ prop = e_cal_util_find_x_property (icalcomp, x_name);
+ if (!prop)
+ return FALSE;
+
+ icalcomponent_remove_property (icalcomp, prop);
+ icalproperty_free (prop);
+
+ return TRUE;
+}
diff --git a/src/calendar/libecal/e-cal-util.h b/src/calendar/libecal/e-cal-util.h
index 86c90cd..ce4caab 100644
--- a/src/calendar/libecal/e-cal-util.h
+++ b/src/calendar/libecal/e-cal-util.h
@@ -248,6 +248,18 @@ void e_cal_util_get_component_occur_times
const icaltimezone *default_timezone,
icalcomponent_kind kind);
+icalproperty * e_cal_util_find_x_property (icalcomponent *icalcomp,
+ const gchar *x_name);
+gchar * e_cal_util_dup_x_property (icalcomponent *icalcomp,
+ const gchar *x_name);
+const gchar * e_cal_util_get_x_property (icalcomponent *icalcomp,
+ const gchar *x_name);
+void e_cal_util_set_x_property (icalcomponent *icalcomp,
+ const gchar *x_name,
+ const gchar *value);
+gboolean e_cal_util_remove_x_property (icalcomponent *icalcomp,
+ const gchar *x_name);
+
#ifndef EDS_DISABLE_DEPRECATED
/* Used for mode stuff */
typedef enum {
diff --git a/src/calendar/libedata-cal/CMakeLists.txt b/src/calendar/libedata-cal/CMakeLists.txt
index 484c21c..15df5d2 100644
--- a/src/calendar/libedata-cal/CMakeLists.txt
+++ b/src/calendar/libedata-cal/CMakeLists.txt
@@ -17,6 +17,8 @@ set(SOURCES
e-cal-backend-sync.c
e-cal-backend-util.c
e-cal-backend-store.c
+ e-cal-cache.c
+ e-cal-meta-backend.c
e-data-cal.c
e-data-cal-factory.c
e-data-cal-view.c
@@ -32,9 +34,11 @@ set(HEADERS
e-cal-backend-sync.h
e-cal-backend-util.h
e-cal-backend-sexp.h
+ e-cal-backend-store.h
+ e-cal-cache.h
+ e-cal-meta-backend.h
e-data-cal.h
e-data-cal-factory.h
- e-cal-backend-store.h
e-data-cal-view.h
e-subprocess-cal-factory.h
)
diff --git a/src/calendar/libedata-cal/e-cal-backend-sexp.c b/src/calendar/libedata-cal/e-cal-backend-sexp.c
index 5da3e40..466d9d2 100644
--- a/src/calendar/libedata-cal/e-cal-backend-sexp.c
+++ b/src/calendar/libedata-cal/e-cal-backend-sexp.c
@@ -427,8 +427,8 @@ matches_attendee (ECalComponent *comp,
for (l = a_list; l; l = l->next) {
ECalComponentAttendee *att = l->data;
- if ((att->value && e_util_strstrcase (att->value, str)) || (att->cn != NULL &&
- e_util_strstrcase (att->cn, str))) {
+ if ((att->value && e_util_utf8_strstrcasedecomp (att->value, str)) ||
+ (att->cn != NULL && e_util_utf8_strstrcasedecomp (att->cn, str))) {
matches = TRUE;
break;
}
@@ -451,8 +451,8 @@ matches_organizer (ECalComponent *comp,
if (str && !*str)
return TRUE;
- if ((org.value && e_util_strstrcase (org.value, str)) ||
- (org.cn && e_util_strstrcase (org.cn, str)))
+ if ((org.value && e_util_utf8_strstrcasedecomp (org.value, str)) ||
+ (org.cn && e_util_utf8_strstrcasedecomp (org.cn, str)))
return TRUE;
return FALSE;
@@ -536,23 +536,24 @@ matches_any (ECalComponent *comp,
static gboolean
matches_priority (ECalComponent *comp ,const gchar *pr)
{
+ gboolean res = FALSE;
gint *priority = NULL;
e_cal_component_get_priority (comp, &priority);
- if (!priority || !*priority)
- return FALSE;
+ if (!priority)
+ return g_str_equal (pr, "UNDEFINED");
if (g_str_equal (pr, "HIGH") && *priority <= 4)
- return TRUE;
+ res = TRUE;
else if (g_str_equal (pr, "NORMAL") && *priority == 5)
- return TRUE;
+ res = TRUE;
else if (g_str_equal (pr, "LOW") && *priority > 5)
- return TRUE;
- else if (g_str_equal (pr, "UNDEFINED") && (!priority || !*priority))
- return TRUE;
+ res = TRUE;
- return FALSE;
+ e_cal_component_free_priority (priority);
+
+ return res;
}
static gboolean
@@ -565,24 +566,34 @@ matches_status (ECalComponent *comp ,const gchar *str)
e_cal_component_get_status (comp, &status);
- if (g_str_equal (str, "NOT STARTED") && status == ICAL_STATUS_NONE)
- return TRUE;
- else if (g_str_equal (str, "COMPLETED") && status == ICAL_STATUS_COMPLETED)
- return TRUE;
- else if (g_str_equal (str, "CANCELLED") && status == ICAL_STATUS_CANCELLED)
- return TRUE;
- else if (g_str_equal (str, "IN PROGRESS") && status == ICAL_STATUS_INPROCESS)
- return TRUE;
- else if (g_str_equal (str, "NEEDS ACTION") && status == ICAL_STATUS_NEEDSACTION)
- return TRUE;
- else if (g_str_equal (str, "TENTATIVE") && status == ICAL_STATUS_TENTATIVE)
- return TRUE;
- else if (g_str_equal (str, "CONFIRMED") && status == ICAL_STATUS_CONFIRMED)
- return TRUE;
- else if (g_str_equal (str, "DRAFT") && status == ICAL_STATUS_DRAFT)
- return TRUE;
- else if (g_str_equal (str, "FINAL") && status == ICAL_STATUS_FINAL)
- return TRUE;
+ switch (status) {
+ case ICAL_STATUS_NONE:
+ return g_str_equal (str, "NOT STARTED");
+ case ICAL_STATUS_COMPLETED:
+ return g_str_equal (str, "COMPLETED");
+ case ICAL_STATUS_CANCELLED:
+ return g_str_equal (str, "CANCELLED");
+ case ICAL_STATUS_INPROCESS:
+ return g_str_equal (str, "IN PROGRESS");
+ case ICAL_STATUS_NEEDSACTION:
+ return g_str_equal (str, "NEEDS ACTION");
+ case ICAL_STATUS_TENTATIVE:
+ return g_str_equal (str, "TENTATIVE");
+ case ICAL_STATUS_CONFIRMED:
+ return g_str_equal (str, "CONFIRMED");
+ case ICAL_STATUS_DRAFT:
+ return g_str_equal (str, "DRAFT");
+ case ICAL_STATUS_FINAL:
+ return g_str_equal (str, "FINAL");
+ case ICAL_STATUS_SUBMITTED:
+ return g_str_equal (str, "SUBMITTED");
+ case ICAL_STATUS_PENDING:
+ return g_str_equal (str, "PENDING");
+ case ICAL_STATUS_FAILED:
+ return g_str_equal (str, "FAILED");
+ case ICAL_STATUS_X:
+ break;
+ }
return FALSE;
}
@@ -628,10 +639,13 @@ func_percent_complete (ESExp *esexp,
e_cal_component_get_percent (ctx->comp, &percent);
- if (percent && *percent) {
- result = e_sexp_result_new (esexp, ESEXP_RES_INT);
- result->value.number = *percent;
+ result = e_sexp_result_new (esexp, ESEXP_RES_INT);
+ if (percent) {
+ result->value.number = *percent;
+ e_cal_component_free_percent (percent);
+ } else {
+ result->value.number = -1;
}
return result;
@@ -925,6 +939,8 @@ func_has_categories (ESExp *esexp,
result = e_sexp_result_new (esexp, ESEXP_RES_BOOL);
result->value.boolean = FALSE;
+ e_cal_component_free_categories_list (categories);
+
return result;
}
@@ -1199,9 +1215,6 @@ e_cal_backend_sexp_new (const gchar *text)
e_sexp_input_text (sexp->priv->search_sexp, text, strlen (text));
if (e_sexp_parse (sexp->priv->search_sexp) == -1) {
- g_warning (
- "%s: Error in parsing: %s",
- G_STRFUNC, e_sexp_get_error (sexp->priv->search_sexp));
g_object_unref (sexp);
sexp = NULL;
}
diff --git a/src/calendar/libedata-cal/e-cal-backend.c b/src/calendar/libedata-cal/e-cal-backend.c
index 18115e8..4f1934c 100644
--- a/src/calendar/libedata-cal/e-cal-backend.c
+++ b/src/calendar/libedata-cal/e-cal-backend.c
@@ -68,6 +68,7 @@ struct _ECalBackendPrivate {
GQueue pending_operations;
guint32 next_operation_id;
GSimpleAsyncResult *blocked;
+ gboolean blocked_by_custom_op;
};
struct _AsyncContext {
@@ -102,6 +103,11 @@ struct _DispatchNode {
GSimpleAsyncResult *simple;
GCancellable *cancellable;
+
+ GWeakRef *cal_backend_weak_ref;
+ ECalBackendCustomOpFunc custom_func;
+ gpointer custom_func_user_data;
+ GDestroyNotify custom_func_user_data_free;
};
struct _SignalClosure {
@@ -176,6 +182,12 @@ dispatch_node_free (DispatchNode *dispatch_node)
g_clear_object (&dispatch_node->simple);
g_clear_object (&dispatch_node->cancellable);
+ if (dispatch_node->custom_func_user_data_free)
+ dispatch_node->custom_func_user_data_free (dispatch_node->custom_func_user_data);
+
+ if (dispatch_node->cal_backend_weak_ref)
+ e_weak_ref_free (dispatch_node->cal_backend_weak_ref);
+
g_slice_free (DispatchNode, dispatch_node);
}
@@ -223,13 +235,35 @@ cal_backend_push_operation (ECalBackend *backend,
g_mutex_unlock (&backend->priv->operation_lock);
}
+static void cal_backend_unblock_operations (ECalBackend *backend, GSimpleAsyncResult *simple);
+
static void
cal_backend_dispatch_thread (DispatchNode *node)
{
GCancellable *cancellable = node->cancellable;
GError *local_error = NULL;
- if (g_cancellable_set_error_if_cancelled (cancellable, &local_error)) {
+ if (node->custom_func) {
+ ECalBackend *cal_backend;
+
+ cal_backend = g_weak_ref_get (node->cal_backend_weak_ref);
+ if (cal_backend &&
+ !g_cancellable_is_cancelled (cancellable)) {
+ node->custom_func (cal_backend, node->custom_func_user_data, cancellable,
&local_error);
+
+ if (local_error) {
+ if (!g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ e_cal_backend_notify_error (cal_backend, local_error->message);
+
+ g_clear_error (&local_error);
+ }
+ }
+
+ if (cal_backend) {
+ cal_backend_unblock_operations (cal_backend, NULL);
+ e_util_unref_in_thread (cal_backend);
+ }
+ } else if (g_cancellable_set_error_if_cancelled (cancellable, &local_error)) {
g_simple_async_result_take_error (node->simple, local_error);
g_simple_async_result_complete_in_idle (node->simple);
} else {
@@ -254,7 +288,8 @@ cal_backend_dispatch_next_operation (ECalBackend *backend)
/* We can't dispatch additional operations
* while a blocking operation is in progress. */
- if (backend->priv->blocked != NULL) {
+ if (backend->priv->blocked != NULL ||
+ backend->priv->blocked_by_custom_op) {
g_mutex_unlock (&backend->priv->operation_lock);
return FALSE;
}
@@ -268,8 +303,12 @@ cal_backend_dispatch_next_operation (ECalBackend *backend)
/* If this a blocking operation, block any
* further dispatching until this finishes. */
- if (node->blocking_operation)
- backend->priv->blocked = g_object_ref (node->simple);
+ if (node->blocking_operation) {
+ if (node->simple)
+ backend->priv->blocked = g_object_ref (node->simple);
+ else
+ backend->priv->blocked_by_custom_op = TRUE;
+ }
g_mutex_unlock (&backend->priv->operation_lock);
@@ -291,6 +330,7 @@ cal_backend_unblock_operations (ECalBackend *backend,
g_mutex_lock (&backend->priv->operation_lock);
if (backend->priv->blocked == simple)
g_clear_object (&backend->priv->blocked);
+ backend->priv->blocked_by_custom_op = FALSE;
g_mutex_unlock (&backend->priv->operation_lock);
while (cal_backend_dispatch_next_operation (backend))
@@ -913,6 +953,7 @@ e_cal_backend_class_init (ECalBackendClass *class)
backend_class = E_BACKEND_CLASS (class);
backend_class->prepare_shutdown = cal_backend_prepare_shutdown;
+ class->use_serial_dispatch_queue = TRUE;
class->get_backend_property = cal_backend_get_backend_property;
class->shutdown = cal_backend_shutdown;
@@ -4574,3 +4615,52 @@ e_cal_backend_prepare_for_completion (ECalBackend *backend,
return simple;
}
+/**
+ * e_cal_backend_schedule_custom_operation:
+ * @cal_backend: an #ECalBackend
+ * @use_cancellable: (nullable): an optional #GCancellable to use for @func
+ * @func: a function to call in a dedicated thread
+ * @user_data: user data being passed to @func
+ * @user_data_free: (nullable): optional destroy call back for @user_data
+ *
+ * Schedules user function @func to be run in a dedicated thread as
+ * a blocking operation.
+ *
+ * The function adds its own reference to @use_cancellable, if not %NULL.
+ *
+ * The error returned from @func is propagated to client using
+ * e_cal_backend_notify_error() function. If it's not desired,
+ * then left the error unchanged and notify about errors manually.
+ *
+ * Since: 3.26
+ **/
+void
+e_cal_backend_schedule_custom_operation (ECalBackend *cal_backend,
+ GCancellable *use_cancellable,
+ ECalBackendCustomOpFunc func,
+ gpointer user_data,
+ GDestroyNotify user_data_free)
+{
+ DispatchNode *node;
+
+ g_return_if_fail (E_IS_CAL_BACKEND (cal_backend));
+ g_return_if_fail (func != NULL);
+
+ g_mutex_lock (&cal_backend->priv->operation_lock);
+
+ node = g_slice_new0 (DispatchNode);
+ node->blocking_operation = TRUE;
+ node->cal_backend_weak_ref = e_weak_ref_new (cal_backend);
+ node->custom_func = func;
+ node->custom_func_user_data = user_data;
+ node->custom_func_user_data_free = user_data_free;
+
+ if (G_IS_CANCELLABLE (use_cancellable))
+ node->cancellable = g_object_ref (use_cancellable);
+
+ g_queue_push_tail (&cal_backend->priv->pending_operations, node);
+
+ g_mutex_unlock (&cal_backend->priv->operation_lock);
+
+ cal_backend_dispatch_next_operation (cal_backend);
+}
diff --git a/src/calendar/libedata-cal/e-cal-backend.h b/src/calendar/libedata-cal/e-cal-backend.h
index e7bc505..91d52c7 100644
--- a/src/calendar/libedata-cal/e-cal-backend.h
+++ b/src/calendar/libedata-cal/e-cal-backend.h
@@ -116,7 +116,7 @@ struct _ECalBackend {
/**
* ECalBackendClass:
* @use_serial_dispatch_queue: Whether a serial dispatch queue should
- * be used for this backend or not.
+ * be used for this backend or not. The default is %TRUE.
* @get_backend_property: Fetch a property value by name from the backend
* @open: Open the backend
* @refresh: Refresh the backend
@@ -529,6 +529,30 @@ GSimpleAsyncResult *
guint opid,
GQueue **result_queue);
+/**
+ * ECalBackendCustomOpFunc:
+ * @cal_backend: an #ECalBackend
+ * @user_data: a function user data, as provided to e_cal_backend_schedule_custom_operation()
+ * @cancellable: an optional #GCancellable, as provided to e_cal_backend_schedule_custom_operation()
+ * @error: return location for a #GError, or %NULL
+ *
+ * A callback prototype being called in a dedicated thread, scheduled
+ * by e_cal_backend_schedule_custom_operation().
+ *
+ * Since: 3.26
+ **/
+typedef void (* ECalBackendCustomOpFunc) (ECalBackend *cal_backend,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error);
+
+void e_cal_backend_schedule_custom_operation
+ (ECalBackend *cal_backend,
+ GCancellable *use_cancellable,
+ ECalBackendCustomOpFunc func,
+ gpointer user_data,
+ GDestroyNotify user_data_free);
+
G_END_DECLS
#endif /* E_CAL_BACKEND_H */
diff --git a/src/calendar/libedata-cal/e-cal-cache.c b/src/calendar/libedata-cal/e-cal-cache.c
new file mode 100644
index 0000000..fd97743
--- /dev/null
+++ b/src/calendar/libedata-cal/e-cal-cache.c
@@ -0,0 +1,3651 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * SECTION: e-cal-cache
+ * @include: libedata-cal/libedata-cal.h
+ * @short_description: An #ECache descendant for calendars
+ *
+ * The #ECalCache is an API for storing and looking up calendar
+ * components in an #ECache.
+ *
+ * The API is thread safe, in the similar way as the #ECache is.
+ *
+ * Any operations which can take a lot of time to complete (depending
+ * on the size of your calendar) can be cancelled using a #GCancellable.
+ **/
+
+#include "evolution-data-server-config.h"
+
+#include <glib/gi18n-lib.h>
+#include <glib/gstdio.h>
+#include <sqlite3.h>
+
+#include <libebackend/libebackend.h>
+#include <libecal/libecal.h>
+
+#include "e-cal-backend-sexp.h"
+
+#include "e-cal-cache.h"
+
+#define E_CAL_CACHE_VERSION 1
+
+#define ECC_TABLE_TIMEZONES "timezones"
+
+#define ECC_COLUMN_OCCUR_START "occur_start"
+#define ECC_COLUMN_OCCUR_END "occur_end"
+#define ECC_COLUMN_DUE "due"
+#define ECC_COLUMN_COMPLETED "completed"
+#define ECC_COLUMN_SUMMARY "summary"
+#define ECC_COLUMN_COMMENT "comment"
+#define ECC_COLUMN_DESCRIPTION "description"
+#define ECC_COLUMN_LOCATION "location"
+#define ECC_COLUMN_ATTENDEES "attendees"
+#define ECC_COLUMN_ORGANIZER "organizer"
+#define ECC_COLUMN_CLASSIFICATION "classification"
+#define ECC_COLUMN_STATUS "status"
+#define ECC_COLUMN_PRIORITY "priority"
+#define ECC_COLUMN_PERCENT_COMPLETE "percent_complete"
+#define ECC_COLUMN_CATEGORIES "categories"
+#define ECC_COLUMN_HAS_ALARM "has_alarm"
+#define ECC_COLUMN_HAS_ATTACHMENT "has_attachment"
+#define ECC_COLUMN_HAS_START "has_start"
+#define ECC_COLUMN_HAS_RECURRENCES "has_recurrences"
+#define ECC_COLUMN_EXTRA "bdata"
+
+struct _ECalCachePrivate {
+ GHashTable *loaded_timezones; /* gchar *tzid ~> icaltimezone * */
+ GHashTable *modified_timezones; /* gchar *tzid ~> icaltimezone * */
+ GRecMutex timezones_lock;
+
+ GHashTable *sexps; /* gint ~> ECalBackendSExp * */
+ GMutex sexps_lock;
+};
+
+enum {
+ DUP_COMPONENT_REVISION,
+ LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL];
+
+static void ecc_timezone_cache_init (ETimezoneCacheInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (ECalCache, e_cal_cache, E_TYPE_CACHE,
+ G_IMPLEMENT_INTERFACE (E_TYPE_EXTENSIBLE, NULL)
+ G_IMPLEMENT_INTERFACE (E_TYPE_TIMEZONE_CACHE, ecc_timezone_cache_init))
+
+G_DEFINE_BOXED_TYPE (ECalCacheOfflineChange, e_cal_cache_offline_change, e_cal_cache_offline_change_copy,
e_cal_cache_offline_change_free)
+G_DEFINE_BOXED_TYPE (ECalCacheSearchData, e_cal_cache_search_data, e_cal_cache_search_data_copy,
e_cal_cache_search_data_free)
+
+/**
+ * e_cal_cache_offline_change_new:
+ * @uid: a unique component identifier
+ * @rid: (nullable): a Recurrence-ID of the component
+ * @revision: (nullable): a revision of the component
+ * @object: (nullable): component itself
+ * @state: an #EOfflineState
+ *
+ * Creates a new #ECalCacheOfflineChange with the offline @state
+ * information for the given @uid.
+ *
+ * Returns: (transfer full): A new #ECalCacheOfflineChange. Free it with
+ * e_cal_cache_offline_change_free() when no longer needed.
+ *
+ * Since: 3.26
+ **/
+ECalCacheOfflineChange *
+e_cal_cache_offline_change_new (const gchar *uid,
+ const gchar *rid,
+ const gchar *revision,
+ const gchar *object,
+ EOfflineState state)
+{
+ ECalCacheOfflineChange *change;
+
+ g_return_val_if_fail (uid != NULL, NULL);
+
+ change = g_new0 (ECalCacheOfflineChange, 1);
+ change->uid = g_strdup (uid);
+ change->rid = g_strdup (rid);
+ change->revision = g_strdup (revision);
+ change->object = g_strdup (object);
+ change->state = state;
+
+ return change;
+}
+
+/**
+ * e_cal_cache_offline_change_copy:
+ * @change: (nullable): a source #ECalCacheOfflineChange to copy, or %NULL
+ *
+ * Returns: (transfer full): Copy of the given @change. Free it with
+ * e_cal_cache_offline_change_free() when no longer needed.
+ * If the @change is %NULL, then returns %NULL as well.
+ *
+ * Since: 3.26
+ **/
+ECalCacheOfflineChange *
+e_cal_cache_offline_change_copy (const ECalCacheOfflineChange *change)
+{
+ if (!change)
+ return NULL;
+
+ return e_cal_cache_offline_change_new (change->uid, change->rid, change->revision, change->object,
change->state);
+}
+
+/**
+ * e_cal_cache_offline_change_free:
+ * @change: (nullable): an #ECalCacheOfflineChange
+ *
+ * Frees the @change structure, previously allocated with e_cal_cache_offline_change_new()
+ * or e_cal_cache_offline_change_copy().
+ *
+ * Since: 3.26
+ **/
+void
+e_cal_cache_offline_change_free (gpointer change)
+{
+ ECalCacheOfflineChange *chng = change;
+
+ if (chng) {
+ g_free (chng->uid);
+ g_free (chng->rid);
+ g_free (chng->revision);
+ g_free (chng->object);
+ g_free (chng);
+ }
+}
+
+/**
+ * e_cal_cache_search_data_new:
+ * @uid: a component UID; cannot be %NULL
+ * @rid: (nullable): a component Recurrence-ID; can be %NULL
+ * @object: the component as an iCal string; cannot be %NULL
+ * @extra: (nullable): any extra data stored with the component, or %NULL
+ *
+ * Creates a new #ECalCacheSearchData prefilled with the given values.
+ *
+ * Returns: (transfer full): A new #ECalCacheSearchData. Free it with
+ * e_cal_cache_search_data_free() when no longer needed.
+ *
+ * Since: 3.26
+ **/
+ECalCacheSearchData *
+e_cal_cache_search_data_new (const gchar *uid,
+ const gchar *rid,
+ const gchar *object,
+ const gchar *extra)
+{
+ ECalCacheSearchData *data;
+
+ g_return_val_if_fail (uid != NULL, NULL);
+ g_return_val_if_fail (object != NULL, NULL);
+
+ data = g_new0 (ECalCacheSearchData, 1);
+ data->uid = g_strdup (uid);
+ data->rid = (rid && *rid) ? g_strdup (rid) : NULL;
+ data->object = g_strdup (object);
+ data->extra = g_strdup (extra);
+
+ return data;
+}
+
+/**
+ * e_cal_cache_search_data_copy:
+ * @data: (nullable): a source #ECalCacheSearchData to copy, or %NULL
+ *
+ * Returns: (transfer full): Copy of the given @data. Free it with
+ * e_cal_cache_search_data_free() when no longer needed.
+ * If the @data is %NULL, then returns %NULL as well.
+ *
+ * Since: 3.26
+ **/
+ECalCacheSearchData *
+e_cal_cache_search_data_copy (const ECalCacheSearchData *data)
+{
+ if (!data)
+ return NULL;
+
+ return e_cal_cache_search_data_new (data->uid, data->rid, data->object, data->extra);
+}
+
+/**
+ * e_cal_cache_search_data_free:
+ * @ptr: (nullable): an #ECalCacheSearchData
+ *
+ * Frees the @ptr structure, previously allocated with e_cal_cache_search_data_new()
+ * or e_cal_cache_search_data_copy().
+ *
+ * Since: 3.26
+ **/
+void
+e_cal_cache_search_data_free (gpointer ptr)
+{
+ ECalCacheSearchData *data = ptr;
+
+ if (data) {
+ g_free (data->uid);
+ g_free (data->rid);
+ g_free (data->object);
+ g_free (data->extra);
+ g_free (data);
+ }
+}
+
+static gint
+ecc_take_sexp_object (ECalCache *cal_cache,
+ ECalBackendSExp *sexp)
+{
+ gint sexp_id;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), 0);
+ g_return_val_if_fail (E_IS_CAL_BACKEND_SEXP (sexp), 0);
+
+ g_mutex_lock (&cal_cache->priv->sexps_lock);
+
+ sexp_id = GPOINTER_TO_INT (sexp);
+ while (g_hash_table_contains (cal_cache->priv->sexps, GINT_TO_POINTER (sexp_id))) {
+ sexp_id++;
+ }
+
+ g_hash_table_insert (cal_cache->priv->sexps, GINT_TO_POINTER (sexp_id), sexp);
+
+ g_mutex_unlock (&cal_cache->priv->sexps_lock);
+
+ return sexp_id;
+}
+
+static void
+ecc_free_sexp_object (ECalCache *cal_cache,
+ gint sexp_id)
+{
+ g_return_if_fail (E_IS_CAL_CACHE (cal_cache));
+
+ g_mutex_lock (&cal_cache->priv->sexps_lock);
+
+ g_warn_if_fail (g_hash_table_remove (cal_cache->priv->sexps, GINT_TO_POINTER (sexp_id)));
+
+ g_mutex_unlock (&cal_cache->priv->sexps_lock);
+}
+
+static ECalBackendSExp *
+ecc_ref_sexp_object (ECalCache *cal_cache,
+ gint sexp_id)
+{
+ ECalBackendSExp *sexp;
+
+ g_mutex_lock (&cal_cache->priv->sexps_lock);
+
+ sexp = g_hash_table_lookup (cal_cache->priv->sexps, GINT_TO_POINTER (sexp_id));
+ if (sexp)
+ g_object_ref (sexp);
+
+ g_mutex_unlock (&cal_cache->priv->sexps_lock);
+
+ return sexp;
+}
+
+/* check_sexp(sexp_id, icalstring) */
+static void
+ecc_check_sexp_func (sqlite3_context *context,
+ gint argc,
+ sqlite3_value **argv)
+{
+ ECalCache *cal_cache;
+ ECalBackendSExp *sexp_obj;
+ gint sexp_id;
+ const gchar *icalstring;
+
+ g_return_if_fail (context != NULL);
+ g_return_if_fail (argc == 2);
+
+ cal_cache = sqlite3_user_data (context);
+ sexp_id = sqlite3_value_int (argv[0]);
+ icalstring = (const gchar *) sqlite3_value_text (argv[1]);
+
+ if (!E_IS_CAL_CACHE (cal_cache) || !icalstring || !*icalstring) {
+ sqlite3_result_int (context, 0);
+ return;
+ }
+
+ sexp_obj = ecc_ref_sexp_object (cal_cache, sexp_id);
+ if (!sexp_obj) {
+ sqlite3_result_int (context, 0);
+ return;
+ }
+
+ if (e_cal_backend_sexp_match_object (sexp_obj, icalstring, E_TIMEZONE_CACHE (cal_cache)))
+ sqlite3_result_int (context, 1);
+ else
+ sqlite3_result_int (context, 0);
+
+ g_object_unref (sexp_obj);
+}
+
+/* negate(x) */
+static void
+ecc_negate_func (sqlite3_context *context,
+ gint argc,
+ sqlite3_value **argv)
+{
+ gint val;
+
+ g_return_if_fail (context != NULL);
+ g_return_if_fail (argc == 1);
+
+ val = sqlite3_value_int (argv[0]);
+ sqlite3_result_int (context, !val);
+}
+
+static gboolean
+e_cal_cache_get_string (ECache *cache,
+ gint ncols,
+ const gchar **column_names,
+ const gchar **column_values,
+ gpointer user_data)
+{
+ gchar **pvalue = user_data;
+
+ g_return_val_if_fail (ncols == 1, FALSE);
+ g_return_val_if_fail (column_names != NULL, FALSE);
+ g_return_val_if_fail (column_values != NULL, FALSE);
+ g_return_val_if_fail (pvalue != NULL, FALSE);
+
+ if (!*pvalue)
+ *pvalue = g_strdup (column_values[0]);
+
+ return TRUE;
+}
+
+static gboolean
+e_cal_cache_get_strings (ECache *cache,
+ gint ncols,
+ const gchar **column_names,
+ const gchar **column_values,
+ gpointer user_data)
+{
+ GSList **pstrings = user_data;
+
+ g_return_val_if_fail (ncols == 1, FALSE);
+ g_return_val_if_fail (column_names != NULL, FALSE);
+ g_return_val_if_fail (column_values != NULL, FALSE);
+ g_return_val_if_fail (pstrings != NULL, FALSE);
+
+ *pstrings = g_slist_prepend (*pstrings, g_strdup (column_values[0]));
+
+ return TRUE;
+}
+
+static void
+e_cal_cache_populate_other_columns (ECalCache *cal_cache,
+ GSList **out_other_columns)
+{
+ g_return_if_fail (out_other_columns != NULL);
+
+ *out_other_columns = NULL;
+
+ #define add_column(name, type, idx_name) \
+ *out_other_columns = g_slist_prepend (*out_other_columns, \
+ e_cache_column_info_new (name, type, idx_name))
+
+ add_column (ECC_COLUMN_OCCUR_START, "TEXT", "IDX_OCCURSTART");
+ add_column (ECC_COLUMN_OCCUR_END, "TEXT", "IDX_OCCUREND");
+ add_column (ECC_COLUMN_DUE, "TEXT", "IDX_DUE");
+ add_column (ECC_COLUMN_COMPLETED, "TEXT", "IDX_COMPLETED");
+ add_column (ECC_COLUMN_SUMMARY, "TEXT", "IDX_SUMMARY");
+ add_column (ECC_COLUMN_COMMENT, "TEXT", NULL);
+ add_column (ECC_COLUMN_DESCRIPTION, "TEXT", NULL);
+ add_column (ECC_COLUMN_LOCATION, "TEXT", NULL);
+ add_column (ECC_COLUMN_ATTENDEES, "TEXT", NULL);
+ add_column (ECC_COLUMN_ORGANIZER, "TEXT", NULL);
+ add_column (ECC_COLUMN_CLASSIFICATION, "TEXT", NULL);
+ add_column (ECC_COLUMN_STATUS, "TEXT", NULL);
+ add_column (ECC_COLUMN_PRIORITY, "INTEGER", NULL);
+ add_column (ECC_COLUMN_PERCENT_COMPLETE, "INTEGER", NULL);
+ add_column (ECC_COLUMN_CATEGORIES, "TEXT", NULL);
+ add_column (ECC_COLUMN_HAS_ALARM, "INTEGER", NULL);
+ add_column (ECC_COLUMN_HAS_ATTACHMENT, "INTEGER", NULL);
+ add_column (ECC_COLUMN_HAS_START, "INTEGER", NULL);
+ add_column (ECC_COLUMN_HAS_RECURRENCES, "INTEGER", NULL);
+ add_column (ECC_COLUMN_EXTRA, "TEXT", NULL);
+
+ #undef add_column
+
+ *out_other_columns = g_slist_reverse (*out_other_columns);
+}
+
+static gchar *
+ecc_encode_id_sql (const gchar *uid,
+ const gchar *rid)
+{
+ g_return_val_if_fail (uid != NULL, NULL);
+
+ if (rid && *rid)
+ return g_strdup_printf ("%s\n%s", uid, rid);
+
+ return g_strdup (uid);
+}
+
+static gboolean
+ecc_decode_id_sql (const gchar *id,
+ gchar **out_uid,
+ gchar **out_rid)
+{
+ gchar **split;
+
+ g_return_val_if_fail (id != NULL, FALSE);
+ g_return_val_if_fail (out_uid != NULL, FALSE);
+ g_return_val_if_fail (out_rid != NULL, FALSE);
+
+ *out_uid = NULL;
+ *out_rid = NULL;
+
+ if (!*id)
+ return FALSE;
+
+ split = g_strsplit (id, "\n", 2);
+
+ if (!split || !split[0] || !*split[0]) {
+ g_strfreev (split);
+ return FALSE;
+ }
+
+ *out_uid = split[0];
+
+ if (split[1])
+ *out_rid = split[1];
+
+ /* array elements are taken by the out arguments */
+ g_free (split);
+
+ return TRUE;
+}
+
+static icaltimezone *
+ecc_resolve_tzid_cb (const gchar *tzid,
+ gpointer user_data)
+{
+ ECalCache *cal_cache = user_data;
+ icaltimezone *zone = NULL;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), NULL);
+
+ if (e_cal_cache_get_timezone (cal_cache, tzid, &zone, NULL, NULL) && zone)
+ return zone;
+
+ zone = icaltimezone_get_builtin_timezone (tzid);
+ if (!zone)
+ zone = icaltimezone_get_builtin_timezone_from_tzid (tzid);
+ if (!zone) {
+ tzid = e_cal_match_tzid (tzid);
+ zone = icaltimezone_get_builtin_timezone (tzid);
+ }
+
+ if (!zone)
+ zone = icaltimezone_get_builtin_timezone_from_tzid (tzid);
+
+ return zone;
+}
+
+static gchar *
+ecc_encode_itt_to_sql (struct icaltimetype itt)
+{
+ return g_strdup_printf ("%04d%02d%02d%02d%02d%02d",
+ itt.year, itt.month, itt.day,
+ itt.hour, itt.minute, itt.second);
+}
+
+static gchar *
+ecc_encode_time_to_sql (ECalCache *cal_cache,
+ const ECalComponentDateTime *dt)
+{
+ struct icaltimetype itt;
+ icaltimezone *zone = NULL;
+
+ if (!dt || !dt->value || (!dt->value->is_utc && (!dt->tzid || !*dt->tzid)))
+ return NULL;
+
+ itt = *dt->value;
+ zone = ecc_resolve_tzid_cb (dt->tzid, cal_cache);
+
+ icaltimezone_convert_time (&itt, zone, icaltimezone_get_utc_timezone ());
+
+ return ecc_encode_itt_to_sql (itt);
+}
+
+static gchar *
+ecc_encode_timet_to_sql (ECalCache *cal_cache,
+ time_t tt)
+{
+ struct icaltimetype itt;
+
+ if (tt <= 0)
+ return NULL;
+
+ itt = icaltime_from_timet_with_zone (tt, FALSE, icaltimezone_get_utc_timezone ());
+
+ return ecc_encode_itt_to_sql (itt);
+}
+
+static gchar *
+ecc_extract_text_list (const GSList *list)
+{
+ const GSList *link;
+ GString *value;
+
+ if (!list)
+ return NULL;
+
+ value = g_string_new ("");
+
+ for (link = list; link; link = g_slist_next (link)) {
+ ECalComponentText *text = link->data;
+
+ if (text && text->value) {
+ gchar *str;
+
+ str = e_util_utf8_decompose (text->value);
+ if (str)
+ g_string_append (value, str);
+ g_free (str);
+ }
+ }
+
+ return g_string_free (value, !value->len);
+}
+
+static gchar *
+ecc_extract_comment (ECalComponent *comp)
+{
+ GSList *list = NULL;
+ gchar *value;
+
+ g_return_val_if_fail (E_IS_CAL_COMPONENT (comp), NULL);
+
+ e_cal_component_get_comment_list (comp, &list);
+ value = ecc_extract_text_list (list);
+ e_cal_component_free_text_list (list);
+
+ return value;
+}
+
+static gchar *
+ecc_extract_description (ECalComponent *comp)
+{
+ GSList *list = NULL;
+ gchar *value;
+
+ g_return_val_if_fail (E_IS_CAL_COMPONENT (comp), NULL);
+
+ e_cal_component_get_description_list (comp, &list);
+ value = ecc_extract_text_list (list);
+ e_cal_component_free_text_list (list);
+
+ return value;
+}
+
+static void
+ecc_encode_mail (GString *out_value,
+ const gchar *in_cn,
+ const gchar *in_val)
+{
+ gchar *cn = NULL, *val = NULL;
+
+ g_return_if_fail (in_val != NULL);
+
+ if (in_cn && *in_cn)
+ cn = e_util_utf8_decompose (in_cn);
+
+ if (in_val) {
+ const gchar *str = in_val;
+
+ if (g_ascii_strncasecmp (str, "mailto:", 7) == 0) {
+ str += 7;
+ }
+
+ if (*str)
+ val = e_util_utf8_decompose (str);
+ }
+
+ if ((cn && *cn) || (val && *val)) {
+ if (out_value->len)
+ g_string_append_c (out_value, '\n');
+ if (cn && *cn)
+ g_string_append (out_value, cn);
+ if (val && *val) {
+ if (cn && *cn)
+ g_string_append_c (out_value, '\t');
+ g_string_append (out_value, val);
+ }
+ }
+
+ g_free (cn);
+ g_free (val);
+}
+
+static gchar *
+ecc_extract_attendees (ECalComponent *comp)
+{
+ GSList *attendees = NULL, *link;
+ GString *value;
+
+ g_return_val_if_fail (E_IS_CAL_COMPONENT (comp), NULL);
+
+ e_cal_component_get_attendee_list (comp, &attendees);
+ if (!attendees)
+ return NULL;
+
+ value = g_string_new ("");
+
+ for (link = attendees; link; link = g_slist_next (link)) {
+ ECalComponentAttendee *att = link->data;
+
+ if (!att)
+ continue;
+
+ ecc_encode_mail (value, att->cn, att->value);
+ }
+
+ e_cal_component_free_attendee_list (attendees);
+
+ if (value->len) {
+ /* This way it is encoded as:
+ <\n> <common-name> <\t> <mail> <\n> <common-name> <\t> <mail> <\n> ... </n> */
+ g_string_prepend (value, "\n");
+ g_string_append (value, "\n");
+ }
+
+ return g_string_free (value, !value->len);
+}
+
+static gchar *
+ecc_extract_organizer (ECalComponent *comp)
+{
+ ECalComponentOrganizer org;
+ GString *value;
+
+ g_return_val_if_fail (E_IS_CAL_COMPONENT (comp), NULL);
+
+ e_cal_component_get_organizer (comp, &org);
+
+ if (!org.value)
+ return NULL;
+
+ value = g_string_new ("");
+
+ ecc_encode_mail (value, org.cn, org.value);
+
+ return g_string_free (value, !value->len);
+}
+
+static gchar *
+ecc_extract_categories (ECalComponent *comp)
+{
+ GSList *categories, *link;
+ GString *value;
+
+ g_return_val_if_fail (E_IS_CAL_COMPONENT (comp), NULL);
+
+ e_cal_component_get_categories_list (comp, &categories);
+
+ if (!categories)
+ return NULL;
+
+ value = g_string_new ("");
+
+ for (link = categories; link; link = g_slist_next (link)) {
+ const gchar *category = link->data;
+
+ if (category && *category) {
+ if (value->len)
+ g_string_append_c (value, '\n');
+ g_string_append (value, category);
+ }
+ }
+
+ e_cal_component_free_categories_list (categories);
+
+ if (value->len) {
+ /* This way it is encoded as:
+ <\n> <category> <\n> <category> <\n> ... </n>
+ which allows to search for exact category with: LIKE "%\ncategory\n%"
+ */
+ g_string_prepend (value, "\n");
+ g_string_append (value, "\n");
+ }
+
+ return g_string_free (value, !value->len);
+}
+
+static const gchar *
+ecc_get_classification_as_string (ECalComponentClassification classification)
+{
+ const gchar *str;
+
+ switch (classification) {
+ case E_CAL_COMPONENT_CLASS_PUBLIC:
+ str = "public";
+ break;
+ case E_CAL_COMPONENT_CLASS_PRIVATE:
+ str = "private";
+ break;
+ case E_CAL_COMPONENT_CLASS_CONFIDENTIAL:
+ str = "confidential";
+ break;
+ default:
+ str = NULL;
+ break;
+ }
+
+ return str;
+}
+
+static const gchar *
+ecc_get_status_as_string (icalproperty_status status)
+{
+ switch (status) {
+ case ICAL_STATUS_NONE:
+ return "not started";
+ case ICAL_STATUS_COMPLETED:
+ return "completed";
+ case ICAL_STATUS_CANCELLED:
+ return "cancelled";
+ case ICAL_STATUS_INPROCESS:
+ return "in progress";
+ case ICAL_STATUS_NEEDSACTION:
+ return "needs action";
+ case ICAL_STATUS_TENTATIVE:
+ return "tentative";
+ case ICAL_STATUS_CONFIRMED:
+ return "confirmed";
+ case ICAL_STATUS_DRAFT:
+ return "draft";
+ case ICAL_STATUS_FINAL:
+ return "final";
+ case ICAL_STATUS_SUBMITTED:
+ return "submitted";
+ case ICAL_STATUS_PENDING:
+ return "pending";
+ case ICAL_STATUS_FAILED:
+ return "failed";
+ case ICAL_STATUS_X:
+ break;
+ }
+
+ return NULL;
+}
+
+static void
+ecc_fill_other_columns (ECalCache *cal_cache,
+ ECacheColumnValues *other_columns,
+ ECalComponent *comp)
+{
+ time_t occur_start = -1, occur_end = -1;
+ ECalComponentDateTime dt;
+ ECalComponentText text;
+ ECalComponentClassification classification = E_CAL_COMPONENT_CLASS_PUBLIC;
+ icalcomponent *icalcomp;
+ icalproperty_status status = ICAL_STATUS_NONE;
+ struct icaltimetype *itt;
+ const gchar *str = NULL;
+ gint *pint = NULL;
+ gboolean has;
+
+ g_return_if_fail (E_IS_CAL_CACHE (cal_cache));
+ g_return_if_fail (other_columns != NULL);
+ g_return_if_fail (E_IS_CAL_COMPONENT (comp));
+
+ #define add_value(_col, _val) e_cache_column_values_take_value (other_columns, _col, _val)
+
+ icalcomp = e_cal_component_get_icalcomponent (comp);
+
+ e_cal_util_get_component_occur_times (
+ comp, &occur_start, &occur_end,
+ ecc_resolve_tzid_cb, cal_cache, icaltimezone_get_utc_timezone (),
+ icalcomponent_isa (icalcomp));
+
+ e_cal_component_get_dtstart (comp, &dt);
+ add_value (ECC_COLUMN_OCCUR_START, dt.value && ((dt.tzid && *dt.tzid) || dt.value->is_utc) ?
ecc_encode_timet_to_sql (cal_cache, occur_start) : NULL);
+
+ has = dt.value != NULL;
+ add_value (ECC_COLUMN_HAS_START, g_strdup (has ? "1" : "0"));
+ e_cal_component_free_datetime (&dt);
+
+ e_cal_component_get_dtend (comp, &dt);
+ add_value (ECC_COLUMN_OCCUR_END, dt.value && ((dt.tzid && *dt.tzid) || dt.value->is_utc) ?
ecc_encode_timet_to_sql (cal_cache, occur_end) : NULL);
+ e_cal_component_free_datetime (&dt);
+
+ e_cal_component_get_due (comp, &dt);
+ add_value (ECC_COLUMN_DUE, ecc_encode_time_to_sql (cal_cache, &dt));
+ e_cal_component_free_datetime (&dt);
+
+ itt = NULL;
+ e_cal_component_get_completed (comp, &itt);
+ add_value (ECC_COLUMN_COMPLETED, itt ? ecc_encode_itt_to_sql (*itt) : NULL);
+ if (itt)
+ e_cal_component_free_icaltimetype (itt);
+
+ text.value = NULL;
+ e_cal_component_get_summary (comp, &text);
+ add_value (ECC_COLUMN_SUMMARY, text.value ? e_util_utf8_decompose (text.value) : NULL);
+
+ e_cal_component_get_location (comp, &str);
+ add_value (ECC_COLUMN_LOCATION, str ? e_util_utf8_decompose (str) : NULL);
+
+ e_cal_component_get_classification (comp, &classification);
+ add_value (ECC_COLUMN_CLASSIFICATION, g_strdup (ecc_get_classification_as_string (classification)));
+
+ e_cal_component_get_status (comp, &status);
+ add_value (ECC_COLUMN_STATUS, g_strdup (ecc_get_status_as_string (status)));
+
+ e_cal_component_get_priority (comp, &pint);
+ add_value (ECC_COLUMN_PRIORITY, pint && *pint ? g_strdup_printf ("%d", *pint) : NULL);
+ if (pint)
+ e_cal_component_free_priority (pint);
+
+ e_cal_component_get_percent (comp, &pint);
+ add_value (ECC_COLUMN_PERCENT_COMPLETE, pint && *pint ? g_strdup_printf ("%d", *pint) : NULL);
+ if (pint)
+ e_cal_component_free_percent (pint);
+
+ has = e_cal_component_has_alarms (comp);
+ add_value (ECC_COLUMN_HAS_ALARM, g_strdup (has ? "1" : "0"));
+
+ has = e_cal_component_has_attachments (comp);
+ add_value (ECC_COLUMN_HAS_ATTACHMENT, g_strdup (has ? "1" : "0"));
+
+ has = e_cal_component_has_recurrences (comp) ||
+ e_cal_component_is_instance (comp);
+ add_value (ECC_COLUMN_HAS_RECURRENCES, g_strdup (has ? "1" : "0"));
+
+ add_value (ECC_COLUMN_COMMENT, ecc_extract_comment (comp));
+ add_value (ECC_COLUMN_DESCRIPTION, ecc_extract_description (comp));
+ add_value (ECC_COLUMN_ATTENDEES, ecc_extract_attendees (comp));
+ add_value (ECC_COLUMN_ORGANIZER, ecc_extract_organizer (comp));
+ add_value (ECC_COLUMN_CATEGORIES, ecc_extract_categories (comp));
+}
+
+static gchar *
+ecc_range_as_where_clause (const gchar *start_str,
+ const gchar *end_str)
+{
+ GString *stmt;
+
+ if (!start_str && !end_str)
+ return NULL;
+
+ stmt = g_string_sized_new (64);
+
+ if (start_str) {
+ e_cache_sqlite_stmt_append_printf (stmt,
+ "(" ECC_COLUMN_OCCUR_END " IS NULL OR " ECC_COLUMN_OCCUR_END ">=%Q)",
+ start_str);
+ }
+
+ if (end_str) {
+ if (start_str) {
+ g_string_prepend (stmt, "(");
+ g_string_append (stmt, " AND ");
+ }
+
+ e_cache_sqlite_stmt_append_printf (stmt,
+ "(" ECC_COLUMN_OCCUR_START " IS NULL OR " ECC_COLUMN_OCCUR_START "<=%Q)",
+ end_str);
+
+ if (start_str)
+ g_string_append (stmt, ")");
+ }
+
+ return g_string_free (stmt, FALSE);
+}
+
+typedef struct _SExpToSqlContext {
+ ECalCache *cal_cache;
+ guint not_level;
+ gboolean requires_check_sexp;
+} SExpToSqlContext;
+
+static ESExpResult *
+ecc_sexp_func_and_or (ESExp *esexp,
+ gint argc,
+ ESExpTerm **argv,
+ gpointer user_data,
+ const gchar *oper)
+{
+ SExpToSqlContext *ctx = user_data;
+ ESExpResult *result, *r1;
+ GString *stmt;
+ gint ii;
+
+ g_return_val_if_fail (ctx != NULL, NULL);
+
+ stmt = g_string_new ("(");
+
+ for (ii = 0; ii < argc; ii++) {
+ r1 = e_sexp_term_eval (esexp, argv[ii]);
+
+ if (r1 && r1->type == ESEXP_RES_STRING && r1->value.string) {
+ if (stmt->len > 1)
+ g_string_append_printf (stmt, " %s ", oper);
+
+ g_string_append_printf (stmt, "(%s)", r1->value.string);
+ } else {
+ ctx->requires_check_sexp = TRUE;
+ }
+
+ e_sexp_result_free (esexp, r1);
+ }
+
+ if (stmt->len == 1 && !ctx->not_level) {
+ if (g_str_equal (oper, "AND"))
+ g_string_append_c (stmt, '1');
+ else
+ g_string_append_c (stmt, '0');
+ }
+
+ g_string_append_c (stmt, ')');
+
+ result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+ result->value.string = g_string_free (stmt, stmt->len <= 2);
+
+ return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_and (ESExp *esexp,
+ gint argc,
+ ESExpTerm **argv,
+ gpointer user_data)
+{
+ return ecc_sexp_func_and_or (esexp, argc, argv, user_data, "AND");
+}
+
+static ESExpResult *
+ecc_sexp_func_or (ESExp *esexp,
+ gint argc,
+ ESExpTerm **argv,
+ gpointer user_data)
+{
+ return ecc_sexp_func_and_or (esexp, argc, argv, user_data, "OR");
+}
+
+static ESExpResult *
+ecc_sexp_func_not (ESExp *esexp,
+ gint argc,
+ ESExpTerm **argv,
+ gpointer user_data)
+{
+ SExpToSqlContext *ctx = user_data;
+ ESExpResult *result, *r1;
+
+ g_return_val_if_fail (ctx != NULL, NULL);
+
+ if (argc != 1)
+ return NULL;
+
+ result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+
+ ctx->not_level++;
+
+ r1 = e_sexp_term_eval (esexp, argv[0]);
+
+ ctx->not_level--;
+
+ if (r1 && r1->type == ESEXP_RES_STRING && r1->value.string) {
+ result->value.string = g_strdup_printf ("negate(%s)", r1->value.string);
+ } else {
+ ctx->requires_check_sexp = TRUE;
+ }
+
+ e_sexp_result_free (esexp, r1);
+
+ return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_uid (ESExp *esexp,
+ gint argc,
+ ESExpResult **argv,
+ gpointer user_data)
+{
+ SExpToSqlContext *ctx = user_data;
+ ESExpResult *result;
+ const gchar *uid;
+
+ g_return_val_if_fail (ctx != NULL, NULL);
+
+ if (argc != 1 ||
+ argv[0]->type != ESEXP_RES_STRING) {
+ return NULL;
+ }
+
+ uid = argv[0]->value.string;
+
+ result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+
+ if (!uid) {
+ result->value.string = g_strdup (E_CACHE_COLUMN_UID " IS NULL");
+ } else {
+ gchar *stmt;
+
+ stmt = e_cache_sqlite_stmt_printf (E_CACHE_COLUMN_UID "=%Q OR " E_CACHE_COLUMN_UID " LIKE
'%q\n%%'", uid, uid);
+
+ result->value.string = g_strdup (stmt);
+
+ e_cache_sqlite_stmt_free (stmt);
+ }
+
+ return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_occur_in_time_range (ESExp *esexp,
+ gint argc,
+ ESExpResult **argv,
+ gpointer user_data)
+{
+ SExpToSqlContext *ctx = user_data;
+ ESExpResult *result;
+
+ g_return_val_if_fail (ctx != NULL, NULL);
+
+ if ((argc != 2 && argc != 3) ||
+ argv[0]->type != ESEXP_RES_TIME ||
+ argv[1]->type != ESEXP_RES_TIME ||
+ (argc == 3 && argv[2]->type != ESEXP_RES_STRING)) {
+ return NULL;
+ }
+
+ result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+
+ if (!ctx->not_level) {
+ struct icaltimetype itt_start, itt_end;
+ gchar *start_str, *end_str;
+
+ /* The default zone argument, if any, is ignored here */
+ itt_start = icaltime_from_timet_with_zone (argv[0]->value.time, 0, NULL);
+ itt_end = icaltime_from_timet_with_zone (argv[1]->value.time, 0, NULL);
+
+ start_str = ecc_encode_itt_to_sql (itt_start);
+ end_str = ecc_encode_itt_to_sql (itt_end);
+
+ result->value.string = ecc_range_as_where_clause (start_str, end_str);
+
+ if (!result->value.string)
+ result->value.string = g_strdup ("1=1");
+
+ g_free (start_str);
+ g_free (end_str);
+ } else {
+ result->value.string = NULL;
+ }
+
+ ctx->requires_check_sexp = TRUE;
+
+ return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_due_in_time_range (ESExp *esexp,
+ gint argc,
+ ESExpResult **argv,
+ gpointer user_data)
+{
+ SExpToSqlContext *ctx = user_data;
+ ESExpResult *result;
+ gchar *start_str, *end_str;
+
+ g_return_val_if_fail (ctx != NULL, NULL);
+
+ if (argc != 2 ||
+ argv[0]->type != ESEXP_RES_TIME ||
+ argv[1]->type != ESEXP_RES_TIME) {
+ return NULL;
+ }
+
+ start_str = ecc_encode_timet_to_sql (ctx->cal_cache, argv[0]->value.time);
+ end_str = ecc_encode_timet_to_sql (ctx->cal_cache, argv[1]->value.time);
+
+ result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+ result->value.string = g_strdup_printf ("(%s NOT NULL AND %s>='%s' AND %s<='%s')",
+ ECC_COLUMN_DUE, ECC_COLUMN_DUE, start_str,
+ ECC_COLUMN_DUE, end_str);
+
+ g_free (start_str);
+ g_free (end_str);
+
+ return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_contains (ESExp *esexp,
+ gint argc,
+ ESExpResult **argv,
+ gpointer user_data)
+{
+ SExpToSqlContext *ctx = user_data;
+ ESExpResult *result;
+ const gchar *field, *column = NULL;
+ gchar *str;
+
+ g_return_val_if_fail (ctx != NULL, NULL);
+
+ if (argc != 2 ||
+ argv[0]->type != ESEXP_RES_STRING ||
+ argv[1]->type != ESEXP_RES_STRING) {
+ return NULL;
+ }
+
+ field = argv[0]->value.string;
+ str = e_util_utf8_decompose (argv[1]->value.string);
+
+ if (g_str_equal (field, "comment"))
+ column = ECC_COLUMN_COMMENT;
+ else if (g_str_equal (field, "description"))
+ column = ECC_COLUMN_DESCRIPTION;
+ else if (g_str_equal (field, "summary"))
+ column = ECC_COLUMN_SUMMARY;
+ else if (g_str_equal (field, "location"))
+ column = ECC_COLUMN_LOCATION;
+ else if (g_str_equal (field, "attendee"))
+ column = ECC_COLUMN_ATTENDEES;
+ else if (g_str_equal (field, "organizer"))
+ column = ECC_COLUMN_ORGANIZER;
+ else if (g_str_equal (field, "classification"))
+ column = ECC_COLUMN_CLASSIFICATION;
+ else if (g_str_equal (field, "status"))
+ column = ECC_COLUMN_STATUS;
+ else if (g_str_equal (field, "priority"))
+ column = ECC_COLUMN_PRIORITY;
+
+ result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+
+ /* everything matches an empty string */
+ if (!str || !*str) {
+ result->value.string = g_strdup ("1=1");
+ } else if (column) {
+ gchar *stmt;
+
+ if (g_str_equal (column, ECC_COLUMN_PRIORITY)) {
+ if (g_ascii_strcasecmp (str, "UNDEFINED") == 0)
+ stmt = e_cache_sqlite_stmt_printf ("%s IS NULL", column);
+ else if (g_ascii_strcasecmp (str, "HIGH") == 0)
+ stmt = e_cache_sqlite_stmt_printf ("%s<=4", column);
+ else if (g_ascii_strcasecmp (str, "NORMAL") == 0)
+ stmt = e_cache_sqlite_stmt_printf ("%s=5", column);
+ else if (g_ascii_strcasecmp (str, "LOW") == 0)
+ stmt = e_cache_sqlite_stmt_printf ("%s>5", column);
+ else
+ stmt = e_cache_sqlite_stmt_printf ("%s IS NOT NULL", column);
+ } else if (g_str_equal (column, ECC_COLUMN_CLASSIFICATION) ||
+ g_str_equal (column, ECC_COLUMN_STATUS)) {
+ stmt = e_cache_sqlite_stmt_printf ("%s='%q'", column, str);
+ } else {
+ stmt = e_cache_sqlite_stmt_printf ("%s LIKE '%%%q%%'", column, str);
+ }
+ result->value.string = g_strdup (stmt);
+ e_cache_sqlite_stmt_free (stmt);
+ } else if (g_str_equal (field, "any")) {
+ GString *stmt;
+
+ stmt = g_string_new ("");
+
+ e_cache_sqlite_stmt_append_printf (stmt, "(%s LIKE '%%%q%%'", ECC_COLUMN_COMMENT, str);
+ e_cache_sqlite_stmt_append_printf (stmt, " OR %s LIKE '%%%q%%'", ECC_COLUMN_DESCRIPTION, str);
+ e_cache_sqlite_stmt_append_printf (stmt, " OR %s LIKE '%%%q%%'", ECC_COLUMN_SUMMARY, str);
+ e_cache_sqlite_stmt_append_printf (stmt, " OR %s LIKE '%%%q%%')", ECC_COLUMN_LOCATION, str);
+
+ result->value.string = g_string_free (stmt, FALSE);
+ } else {
+ ctx->requires_check_sexp = TRUE;
+ }
+
+ g_free (str);
+
+ return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_has_start (ESExp *esexp,
+ gint argc,
+ ESExpResult **argv,
+ gpointer user_data)
+{
+ SExpToSqlContext *ctx = user_data;
+ ESExpResult *result;
+
+ g_return_val_if_fail (ctx != NULL, NULL);
+
+ result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+ result->value.string = g_strdup_printf ("(%s NOT NULL AND %s=1)",
+ ECC_COLUMN_HAS_START, ECC_COLUMN_HAS_START);
+
+ return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_has_alarms (ESExp *esexp,
+ gint argc,
+ ESExpResult **argv,
+ gpointer user_data)
+{
+ SExpToSqlContext *ctx = user_data;
+ ESExpResult *result;
+
+ g_return_val_if_fail (ctx != NULL, NULL);
+
+ result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+ result->value.string = g_strdup_printf ("(%s NOT NULL AND %s=1)",
+ ECC_COLUMN_HAS_ALARM, ECC_COLUMN_HAS_ALARM);
+
+ return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_has_alarms_in_range (ESExp *esexp,
+ gint argc,
+ ESExpResult **argv,
+ gpointer user_data)
+{
+ SExpToSqlContext *ctx = user_data;
+ ESExpResult *result;
+
+ g_return_val_if_fail (ctx != NULL, NULL);
+
+ ctx->requires_check_sexp = TRUE;
+
+ if (!ctx->not_level)
+ return ecc_sexp_func_has_alarms (esexp, argc, argv, user_data);
+
+ result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+ result->value.string = NULL;
+
+ return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_has_recurrences (ESExp *esexp,
+ gint argc,
+ ESExpResult **argv,
+ gpointer user_data)
+{
+ SExpToSqlContext *ctx = user_data;
+ ESExpResult *result;
+
+ g_return_val_if_fail (ctx != NULL, NULL);
+
+ result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+ result->value.string = g_strdup_printf ("(%s NOT NULL AND %s=1)",
+ ECC_COLUMN_HAS_RECURRENCES, ECC_COLUMN_HAS_RECURRENCES);
+
+ return result;
+}
+
+/* (has-categories? STR+)
+ * (has-categories? #f)
+ */
+static ESExpResult *
+ecc_sexp_func_has_categories (ESExp *esexp,
+ gint argc,
+ ESExpResult **argv,
+ gpointer user_data)
+{
+ SExpToSqlContext *ctx = user_data;
+ ESExpResult *result;
+ gboolean unfiled;
+
+ g_return_val_if_fail (ctx != NULL, NULL);
+
+ if (argc < 1)
+ return NULL;
+
+ unfiled = argc == 1 && argv[0]->type == ESEXP_RES_BOOL;
+
+ result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+
+ if (unfiled) {
+ result->value.string = g_strdup_printf ("%s IS NULL",
+ ECC_COLUMN_CATEGORIES);
+ } else {
+ GString *tmp;
+ gint ii;
+
+ tmp = g_string_new ("(" ECC_COLUMN_CATEGORIES " NOT NULL");
+
+ for (ii = 0; ii < argc; ii++) {
+ if (argv[ii]->type != ESEXP_RES_STRING) {
+ g_warn_if_reached ();
+ continue;
+ }
+
+ e_cache_sqlite_stmt_append_printf (tmp, " AND " ECC_COLUMN_CATEGORIES " LIKE
'%%\n%q\n%%'",
+ argv[ii]->value.string);
+ }
+
+ g_string_append_c (tmp, ')');
+
+ result->value.string = g_string_free (tmp, FALSE);
+ }
+
+ return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_is_completed (ESExp *esexp,
+ gint argc,
+ ESExpResult **argv,
+ gpointer user_data)
+{
+ SExpToSqlContext *ctx = user_data;
+ ESExpResult *result;
+
+ g_return_val_if_fail (ctx != NULL, NULL);
+
+ result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+ result->value.string = g_strdup_printf ("%s NOT NULL",
+ ECC_COLUMN_COMPLETED);
+
+ return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_completed_before (ESExp *esexp,
+ gint argc,
+ ESExpResult **argv,
+ gpointer user_data)
+{
+ SExpToSqlContext *ctx = user_data;
+ gchar *tmp;
+ ESExpResult *result;
+
+ g_return_val_if_fail (ctx != NULL, NULL);
+
+ if (argc != 1 ||
+ argv[0]->type != ESEXP_RES_TIME) {
+ return NULL;
+ }
+
+ tmp = ecc_encode_timet_to_sql (ctx->cal_cache, argv[0]->value.time);
+
+ result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+ result->value.string = g_strdup_printf ("(%s NOT NULL AND %s<'%s')",
+ ECC_COLUMN_COMPLETED, ECC_COLUMN_COMPLETED, tmp);
+
+ g_free (tmp);
+
+ return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_has_attachment (ESExp *esexp,
+ gint argc,
+ ESExpResult **argv,
+ gpointer user_data)
+{
+ SExpToSqlContext *ctx = user_data;
+ ESExpResult *result;
+
+ g_return_val_if_fail (ctx != NULL, NULL);
+
+ result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+ result->value.string = g_strdup_printf ("(%s NOT NULL AND %s=1)",
+ ECC_COLUMN_HAS_ATTACHMENT, ECC_COLUMN_HAS_ATTACHMENT);
+
+ return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_percent_complete (ESExp *esexp,
+ gint argc,
+ ESExpResult **argv,
+ gpointer user_data)
+{
+ SExpToSqlContext *ctx = user_data;
+ ESExpResult *result;
+
+ g_return_val_if_fail (ctx != NULL, NULL);
+
+ result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+ result->value.string = g_strdup (ECC_COLUMN_PERCENT_COMPLETE);
+
+ return result;
+}
+
+/* check_sexp(sexp_id, icalstring); that's a fallback for anything
+ not being part of the summary */
+static ESExpResult *
+ecc_sexp_func_check_sexp (ESExp *esexp,
+ gint argc,
+ ESExpResult **argv,
+ gpointer user_data)
+{
+ SExpToSqlContext *ctx = user_data;
+ ESExpResult *result;
+
+ g_return_val_if_fail (ctx != NULL, NULL);
+
+ result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+ result->value.string = NULL;
+
+ ctx->requires_check_sexp = TRUE;
+
+ return result;
+}
+
+static ESExpResult *
+ecc_sexp_func_icheck_sexp (ESExp *esexp,
+ gint argc,
+ ESExpTerm **argv,
+ gpointer user_data)
+{
+ SExpToSqlContext *ctx = user_data;
+ ESExpResult *result;
+
+ g_return_val_if_fail (ctx != NULL, NULL);
+
+ result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
+ result->value.string = NULL;
+
+ ctx->requires_check_sexp = TRUE;
+
+ return result;
+}
+
+static struct {
+ const gchar *name;
+ gpointer func;
+ gint type; /* 1 for term-function, 0 for result-function */
+} symbols[] = {
+ { "and", ecc_sexp_func_and, 1 },
+ { "or", ecc_sexp_func_or, 1 },
+ { "not", ecc_sexp_func_not, 1 },
+ { "<", ecc_sexp_func_icheck_sexp, 1 },
+ { ">", ecc_sexp_func_icheck_sexp, 1 },
+ { "=", ecc_sexp_func_icheck_sexp, 1 },
+ { "+", ecc_sexp_func_check_sexp, 0 },
+ { "-", ecc_sexp_func_check_sexp, 0 },
+ { "cast-int", ecc_sexp_func_check_sexp, 0 },
+ { "cast-string", ecc_sexp_func_check_sexp, 0 },
+ { "if", ecc_sexp_func_icheck_sexp, 1 },
+ { "begin", ecc_sexp_func_icheck_sexp, 1 },
+
+ /* Time-related functions */
+ { "time-now", e_cal_backend_sexp_func_time_now, 0 },
+ { "make-time", e_cal_backend_sexp_func_make_time, 0 },
+ { "time-add-day", e_cal_backend_sexp_func_time_add_day, 0 },
+ { "time-day-begin", e_cal_backend_sexp_func_time_day_begin, 0 },
+ { "time-day-end", e_cal_backend_sexp_func_time_day_end, 0 },
+
+ /* Component-related functions */
+ { "uid?", ecc_sexp_func_uid, 0 },
+ { "occur-in-time-range?", ecc_sexp_func_occur_in_time_range, 0 },
+ { "due-in-time-range?", ecc_sexp_func_due_in_time_range, 0 },
+ { "contains?", ecc_sexp_func_contains, 0 },
+ { "has-start?", ecc_sexp_func_has_start, 0 },
+ { "has-alarms?", ecc_sexp_func_has_alarms, 0 },
+ { "has-alarms-in-range?", ecc_sexp_func_has_alarms_in_range, 0 },
+ { "has-recurrences?", ecc_sexp_func_has_recurrences, 0 },
+ { "has-categories?", ecc_sexp_func_has_categories, 0 },
+ { "is-completed?", ecc_sexp_func_is_completed, 0 },
+ { "completed-before?", ecc_sexp_func_completed_before, 0 },
+ { "has-attachments?", ecc_sexp_func_has_attachment, 0 },
+ { "percent-complete?", ecc_sexp_func_percent_complete, 0 },
+ { "occurrences-count?", ecc_sexp_func_check_sexp, 0 }
+};
+
+static gboolean
+ecc_convert_sexp_to_sql (ECalCache *cal_cache,
+ const gchar *sexp_str,
+ gint sexp_id,
+ gchar **out_where_clause,
+ GCancellable *cancellable,
+ GError **error)
+{
+ SExpToSqlContext ctx;
+ ESExp *sexp_parser;
+ gint esexp_error, ii;
+ gboolean success = FALSE;
+
+ g_return_val_if_fail (out_where_clause != NULL, FALSE);
+
+ *out_where_clause = NULL;
+
+ /* Include everything */
+ if (!sexp_str || !*sexp_str)
+ return TRUE;
+
+ ctx.cal_cache = cal_cache;
+ ctx.not_level = 0;
+ ctx.requires_check_sexp = FALSE;
+
+ sexp_parser = e_sexp_new ();
+
+ for (ii = 0; ii < G_N_ELEMENTS (symbols); ii++) {
+ if (symbols[ii].type == 1) {
+ e_sexp_add_ifunction (sexp_parser, 0, symbols[ii].name, symbols[ii].func, &ctx);
+ } else {
+ e_sexp_add_function (sexp_parser, 0, symbols[ii].name, symbols[ii].func, &ctx);
+ }
+ }
+
+ e_sexp_input_text (sexp_parser, sexp_str, strlen (sexp_str));
+ esexp_error = e_sexp_parse (sexp_parser);
+
+ if (esexp_error != -1) {
+ ESExpResult *result;
+
+ result = e_sexp_eval (sexp_parser);
+
+ if (result) {
+ if (result->type == ESEXP_RES_STRING) {
+ if (ctx.requires_check_sexp) {
+ if (result->value.string) {
+ *out_where_clause = g_strdup_printf ("((%s) AND
check_sexp(%d,%s))",
+ result->value.string, sexp_id, E_CACHE_COLUMN_OBJECT);
+ } else {
+ *out_where_clause = g_strdup_printf ("check_sexp(%d,%s)",
+ sexp_id, E_CACHE_COLUMN_OBJECT);
+ }
+ } else {
+ /* Just steal the string from the ESExpResult */
+ *out_where_clause = result->value.string;
+ result->value.string = NULL;
+ }
+ success = TRUE;
+ }
+ }
+
+ e_sexp_result_free (sexp_parser, result);
+ }
+
+ g_object_unref (sexp_parser);
+
+ if (!success) {
+ g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY,
+ _("Invalid query: %s"), sexp_str);
+ }
+
+ return success;
+}
+
+typedef struct {
+ gint extra_idx;
+ ECalCacheSearchFunc func;
+ gpointer func_user_data;
+} SearchContext;
+
+static gboolean
+ecc_search_foreach_cb (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ EOfflineState offline_state,
+ gint ncols,
+ const gchar *column_names[],
+ const gchar *column_values[],
+ gpointer user_data)
+{
+ SearchContext *ctx = user_data;
+ gchar *comp_uid = NULL, *comp_rid = NULL;
+ gboolean can_continue;
+
+ g_return_val_if_fail (ctx != NULL, FALSE);
+ g_return_val_if_fail (ctx->func != NULL, FALSE);
+
+ if (ctx->extra_idx == -1) {
+ gint ii;
+
+ for (ii = 0; ii < ncols; ii++) {
+ if (column_names[ii] && g_ascii_strcasecmp (column_names[ii], ECC_COLUMN_EXTRA) == 0)
{
+ ctx->extra_idx = ii;
+ break;
+ }
+ }
+ }
+
+ g_return_val_if_fail (ctx->extra_idx != -1, FALSE);
+
+ g_warn_if_fail (ecc_decode_id_sql (uid, &comp_uid, &comp_rid));
+
+ /* This type-cast for performance reason */
+ can_continue = ctx->func ((ECalCache *) cache, comp_uid, comp_rid, revision, object,
+ column_values[ctx->extra_idx], offline_state, ctx->func_user_data);
+
+ g_free (comp_uid);
+ g_free (comp_rid);
+
+ return can_continue;
+}
+
+static gboolean
+ecc_search_internal (ECalCache *cal_cache,
+ const gchar *sexp_str,
+ gint sexp_id,
+ ECalCacheSearchFunc func,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gchar *where_clause = NULL;
+ SearchContext ctx;
+ gboolean success;
+
+ if (!ecc_convert_sexp_to_sql (cal_cache, sexp_str, sexp_id, &where_clause, cancellable, error)) {
+ return FALSE;
+ }
+
+ ctx.extra_idx = -1;
+ ctx.func = func;
+ ctx.func_user_data = user_data;
+
+ success = e_cache_foreach (E_CACHE (cal_cache), E_CACHE_EXCLUDE_DELETED,
+ where_clause, ecc_search_foreach_cb, &ctx,
+ cancellable, error);
+
+ g_free (where_clause);
+
+ return success;
+}
+
+static gboolean
+ecc_init_aux_tables (ECalCache *cal_cache,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gchar *stmt;
+ gboolean success;
+
+ stmt = e_cache_sqlite_stmt_printf ("CREATE TABLE IF NOT EXISTS %Q ("
+ "tzid TEXT PRIMARY KEY, "
+ "zone TEXT)",
+ ECC_TABLE_TIMEZONES);
+ success = e_cache_sqlite_exec (E_CACHE (cal_cache), stmt, cancellable, error);
+ e_cache_sqlite_stmt_free (stmt);
+
+ return success;
+}
+
+static gboolean
+ecc_init_sqlite_functions (ECalCache *cal_cache,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gint ret;
+ gpointer sqlitedb;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+
+ sqlitedb = e_cache_get_sqlitedb (E_CACHE (cal_cache));
+ g_return_val_if_fail (sqlitedb != NULL, FALSE);
+
+ /* check_sexp(sexp_id, icalstring) */
+ ret = sqlite3_create_function (sqlitedb,
+ "check_sexp", 2, SQLITE_UTF8 | SQLITE_DETERMINISTIC,
+ cal_cache, ecc_check_sexp_func,
+ NULL, NULL);
+
+ if (ret == SQLITE_OK) {
+ /* negate(x) */
+ ret = sqlite3_create_function (sqlitedb,
+ "negate", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC,
+ NULL, ecc_negate_func,
+ NULL, NULL);
+ }
+
+ if (ret != SQLITE_OK) {
+ const gchar *errmsg = sqlite3_errmsg (sqlitedb);
+
+ g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_ENGINE,
+ _("Failed to create SQLite function, error code '%d': %s"),
+ ret, errmsg ? errmsg : _("Unknown error"));
+
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+e_cal_cache_migrate (ECache *cache,
+ gint from_version,
+ GCancellable *cancellable,
+ GError **error)
+{
+ /* ECalCache *cal_cache = E_CAL_CACHE (cache); */
+ gboolean success = TRUE;
+
+ /* Add any version-related changes here */
+ /*if (from_version < E_CAL_CACHE_VERSION) {
+ }*/
+
+ return success;
+}
+
+static gboolean
+e_cal_cache_initialize (ECalCache *cal_cache,
+ const gchar *filename,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECache *cache;
+ GSList *other_columns = NULL;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+ g_return_val_if_fail (filename != NULL, FALSE);
+
+ cache = E_CACHE (cal_cache);
+
+ e_cal_cache_populate_other_columns (cal_cache, &other_columns);
+
+ success = e_cache_initialize_sync (cache, filename, other_columns, cancellable, error);
+ if (!success)
+ goto exit;
+
+ e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+
+ success = success && ecc_init_aux_tables (cal_cache, cancellable, error);
+
+ success = success && ecc_init_sqlite_functions (cal_cache, cancellable, error);
+
+ /* Check for data migration */
+ success = success && e_cal_cache_migrate (cache, e_cache_get_version (cache), cancellable, error);
+
+ e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+ if (!success)
+ goto exit;
+
+ if (e_cache_get_version (cache) != E_CAL_CACHE_VERSION)
+ e_cache_set_version (cache, E_CAL_CACHE_VERSION);
+
+ exit:
+ g_slist_free_full (other_columns, e_cache_column_info_free);
+
+ return success;
+}
+
+/**
+ * e_cal_cache_new:
+ * @filename: file name to load or create the new cache
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new #ECalCache.
+ *
+ * Returns: (transfer full) (nullable): A new #ECalCache or %NULL on error
+ *
+ * Since: 3.26
+ **/
+ECalCache *
+e_cal_cache_new (const gchar *filename,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalCache *cal_cache;
+
+ g_return_val_if_fail (filename != NULL, NULL);
+
+ cal_cache = g_object_new (E_TYPE_CAL_CACHE, NULL);
+
+ if (!e_cal_cache_initialize (cal_cache, filename, cancellable, error)) {
+ g_object_unref (cal_cache);
+ cal_cache = NULL;
+ }
+
+ return cal_cache;
+}
+
+/**
+ * e_cal_cache_dup_component_revision:
+ * @cal_cache: an #ECalCache
+ * @icalcomp: an icalcomponent
+ *
+ * Returns the @icalcomp revision, used to detect changes.
+ * The returned string should be freed with g_free(), when
+ * no longer needed.
+ *
+ * Returns: (transfer full): A newly allocated string containing
+ * revision of the @icalcomp.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_cal_cache_dup_component_revision (ECalCache *cal_cache,
+ icalcomponent *icalcomp)
+{
+ gchar *revision = NULL;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), NULL);
+ g_return_val_if_fail (icalcomp != NULL, NULL);
+
+ g_signal_emit (cal_cache, signals[DUP_COMPONENT_REVISION], 0, icalcomp, &revision);
+
+ return revision;
+}
+
+/**
+ * e_cal_cache_contains:
+ * @cal_cache: an #ECalCache
+ * @uid: component UID
+ * @rid: (nullable): optional component Recurrence-ID or %NULL
+ * @deleted_flag: one of #ECacheDeletedFlag enum
+ *
+ * Checkes whether the @cal_cache contains an object with
+ * the given @uid and @rid. The @rid can be an empty string
+ * or %NULL to search for the master object, otherwise the check
+ * is done for a detached instance, not for a recurrence instance.
+ *
+ * Returns: Whether the the object had been found.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_contains (ECalCache *cal_cache,
+ const gchar *uid,
+ const gchar *rid,
+ ECacheDeletedFlag deleted_flag)
+{
+ gchar *id;
+ gboolean found;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ id = ecc_encode_id_sql (uid, rid);
+
+ found = e_cache_contains (E_CACHE (cal_cache), id, deleted_flag);
+
+ g_free (id);
+
+ return found;
+}
+
+/**
+ * e_cal_cache_put_component:
+ * @cal_cache: an #ECalCache
+ * @component: an #ECalComponent to put into the @cal_cache
+ * @extra: (nullable): an extra data to store in association with the @component
+ * @offline_flag: one of #ECacheOfflineFlag, whether putting this component in offline
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Adds a @component into the @cal_cache. Any existing with the same UID
+ * and RID is replaced.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_put_component (ECalCache *cal_cache,
+ ECalComponent *component,
+ const gchar *extra,
+ ECacheOfflineFlag offline_flag,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GSList *components = NULL;
+ GSList *extras = NULL;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+
+ components = g_slist_prepend (components, component);
+ if (extra)
+ extras = g_slist_prepend (extras, (gpointer) extra);
+
+ success = e_cal_cache_put_components (cal_cache, components, extras, offline_flag, cancellable,
error);
+
+ g_slist_free (components);
+ g_slist_free (extras);
+
+ return success;
+}
+
+/**
+ * e_cal_cache_put_components:
+ * @cal_cache: an #ECalCache
+ * @components: (element-type ECalComponent): a #GSList of #ECalComponent to put into the @cal_cache
+ * @extras: (nullable) (element-type utf8): an extra data to store in association with the @components
+ * @offline_flag: one of #ECacheOfflineFlag, whether putting these components in offline
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Adds a list of @components into the @cal_cache. Any existing with the same UID
+ * and RID are replaced.
+ *
+ * If @extras is not %NULL, it's length should be the same as the length
+ * of the @components.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_put_components (ECalCache *cal_cache,
+ const GSList *components,
+ const GSList *extras,
+ ECacheOfflineFlag offline_flag,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const GSList *clink, *elink;
+ ECache *cache;
+ ECacheColumnValues *other_columns;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+ g_return_val_if_fail (extras == NULL || g_slist_length ((GSList *) components) == g_slist_length
((GSList *) extras), FALSE);
+
+ cache = E_CACHE (cal_cache);
+ other_columns = e_cache_column_values_new ();
+
+ e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+ e_cache_freeze_revision_change (cache);
+
+ for (clink = components, elink = extras; clink; clink = g_slist_next (clink), elink = g_slist_next
(elink)) {
+ ECalComponent *component = clink->data;
+ const gchar *extra = elink ? elink->data : NULL;
+ ECalComponentId *id;
+ gchar *uid, *rev, *icalstring;
+
+ g_return_val_if_fail (E_IS_CAL_COMPONENT (component), FALSE);
+
+ icalstring = e_cal_component_get_as_string (component);
+ g_return_val_if_fail (icalstring != NULL, FALSE);
+
+ e_cache_column_values_remove_all (other_columns);
+
+ if (extra)
+ e_cache_column_values_take_value (other_columns, ECC_COLUMN_EXTRA, g_strdup (extra));
+
+ id = e_cal_component_get_id (component);
+ if (id) {
+ uid = ecc_encode_id_sql (id->uid, id->rid);
+ } else {
+ g_warn_if_reached ();
+ uid = g_strdup ("");
+ }
+ e_cal_component_free_id (id);
+
+ rev = e_cal_cache_dup_component_revision (cal_cache, e_cal_component_get_icalcomponent
(component));
+
+ success = e_cache_put (cache, uid, rev, icalstring, other_columns, offline_flag, cancellable,
error);
+
+ g_free (icalstring);
+ g_free (rev);
+ g_free (uid);
+
+ if (!success)
+ break;
+ }
+
+ e_cache_thaw_revision_change (cache);
+ e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+ e_cache_column_values_free (other_columns);
+
+ return success;
+}
+
+/**
+ * e_cal_cache_remove_component:
+ * @cal_cache: an #ECalCache
+ * @uid: a UID of the component to remove
+ * @rid: (nullable): an optional Recurrence-ID to remove
+ * @offline_flag: one of #ECacheOfflineFlag, whether removing this component in offline
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Removes a component identified by @uid and @rid from the @cal_cache.
+ * When the @rid is %NULL, or an empty string, then removes the master
+ * object only, without any detached instance.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_remove_component (ECalCache *cal_cache,
+ const gchar *uid,
+ const gchar *rid,
+ ECacheOfflineFlag offline_flag,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalComponentId id;
+ GSList *ids = NULL;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+
+ id.uid = (gchar *) uid;
+ id.rid = (gchar *) rid;
+
+ ids = g_slist_prepend (ids, &id);
+
+ success = e_cal_cache_remove_components (cal_cache, ids, offline_flag, cancellable, error);
+
+ g_slist_free (ids);
+
+ return success;
+}
+
+/**
+ * e_cal_cache_remove_components:
+ * @cal_cache: an #ECalCache
+ * @ids: (element-type ECalComponentId): a #GSList of components to remove
+ * @offline_flag: one of #ECacheOfflineFlag, whether removing these comonents in offline
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Removes components identified by @uid and @rid from the @cal_cache
+ * in the @ids list. When the @rid is %NULL, or an empty string, then
+ * removes the master object only, without any detached instance.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_remove_components (ECalCache *cal_cache,
+ const GSList *ids,
+ ECacheOfflineFlag offline_flag,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECache *cache;
+ const GSList *link;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+
+ cache = E_CACHE (cal_cache);
+
+ e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+ e_cache_freeze_revision_change (cache);
+
+ for (link = ids; success && link; link = g_slist_next (link)) {
+ const ECalComponentId *id = link->data;
+ gchar *uid;
+
+ g_warn_if_fail (id != NULL);
+
+ if (!id)
+ continue;
+
+ uid = ecc_encode_id_sql (id->uid, id->rid);
+
+ success = e_cache_remove (cache, uid, offline_flag, cancellable, error);
+
+ g_free (uid);
+ }
+
+ e_cache_thaw_revision_change (cache);
+ e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+ return success;
+}
+
+/**
+ * e_cal_cache_get_component:
+ * @cal_cache: an #ECalCache
+ * @uid: a UID of the component
+ * @rid: (nullable): an optional Recurrence-ID
+ * @out_component: (out) (transfer full): return location for an #ECalComponent
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets a component identified by @uid, and optionally by the @rid,
+ * from the @cal_cache. The returned @out_component should be freed with
+ * g_object_unref(), when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_get_component (ECalCache *cal_cache,
+ const gchar *uid,
+ const gchar *rid,
+ ECalComponent **out_component,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gchar *icalstring = NULL;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (out_component != NULL, FALSE);
+
+ success = e_cal_cache_get_component_as_string (cal_cache, uid, rid, &icalstring, cancellable, error);
+ if (success) {
+ *out_component = e_cal_component_new_from_string (icalstring);
+ g_free (icalstring);
+ }
+
+ return success;
+}
+
+/**
+ * e_cal_cache_get_component_as_string:
+ * @cal_cache: an #ECalCache
+ * @uid: a UID of the component
+ * @rid: (nullable): an optional Recurrence-ID
+ * @out_icalstring: (out) (transfer full): return location for an iCalendar string
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets a component identified by @uid, and optionally by the @rid,
+ * from the @cal_cache. The returned @out_icalstring should be freed with
+ * g_free(), when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_get_component_as_string (ECalCache *cal_cache,
+ const gchar *uid,
+ const gchar *rid,
+ gchar **out_icalstring,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gchar *id;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (out_icalstring != NULL, FALSE);
+
+ id = ecc_encode_id_sql (uid, rid);
+
+ *out_icalstring = e_cache_get (E_CACHE (cal_cache), id, NULL, NULL, cancellable, error);
+
+ g_free (id);
+
+ return *out_icalstring != NULL;
+}
+
+/**
+ * e_cal_cache_set_component_extra:
+ * @cal_cache: an #ECalCache
+ * @uid: a UID of the component
+ * @rid: (nullable): an optional Recurrence-ID
+ * @extra: (nullable): extra data to set for the component
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Sets or replaces the extra data associated with a component
+ * identified by @uid and optionally @rid.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_set_component_extra (ECalCache *cal_cache,
+ const gchar *uid,
+ const gchar *rid,
+ const gchar *extra,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gchar *id, *stmt;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ id = ecc_encode_id_sql (uid, rid);
+
+ if (!e_cache_contains (E_CACHE (cal_cache), id, E_CACHE_INCLUDE_DELETED)) {
+ g_free (id);
+
+ if (rid && *rid)
+ g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s”, “%s” not
found"), uid, rid);
+ else
+ g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s” not
found"), uid);
+
+ return FALSE;
+ }
+
+ if (extra) {
+ stmt = e_cache_sqlite_stmt_printf (
+ "UPDATE " E_CACHE_TABLE_OBJECTS " SET " ECC_COLUMN_EXTRA "=%Q"
+ " WHERE " E_CACHE_COLUMN_UID "=%Q",
+ extra, id);
+ } else {
+ stmt = e_cache_sqlite_stmt_printf (
+ "UPDATE " E_CACHE_TABLE_OBJECTS " SET " ECC_COLUMN_EXTRA "=NULL"
+ " WHERE " E_CACHE_COLUMN_UID "=%Q",
+ id);
+ }
+
+ success = e_cache_sqlite_exec (E_CACHE (cal_cache), stmt, cancellable, error);
+
+ e_cache_sqlite_stmt_free (stmt);
+ g_free (id);
+
+ return success;
+}
+
+/**
+ * e_cal_cache_get_component_extra:
+ * @cal_cache: an #ECalCache
+ * @uid: a UID of the component
+ * @rid: (nullable): an optional Recurrence-ID
+ * @out_extra: (out) (transfer full): return location to store the extra data
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets the extra data previously set for @uid and @rid, either with
+ * e_cal_cache_set_component_extra() or when adding components.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_get_component_extra (ECalCache *cal_cache,
+ const gchar *uid,
+ const gchar *rid,
+ gchar **out_extra,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gchar *id, *stmt;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ id = ecc_encode_id_sql (uid, rid);
+
+ if (!e_cache_contains (E_CACHE (cal_cache), id, E_CACHE_INCLUDE_DELETED)) {
+ g_free (id);
+
+ if (rid && *rid)
+ g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s”, “%s” not
found"), uid, rid);
+ else
+ g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s” not
found"), uid);
+
+ return FALSE;
+ }
+
+ stmt = e_cache_sqlite_stmt_printf (
+ "SELECT " ECC_COLUMN_EXTRA " FROM " E_CACHE_TABLE_OBJECTS
+ " WHERE " E_CACHE_COLUMN_UID "=%Q",
+ id);
+
+ success = e_cache_sqlite_select (E_CACHE (cal_cache), stmt, e_cal_cache_get_string, out_extra,
cancellable, error);
+
+ e_cache_sqlite_stmt_free (stmt);
+ g_free (id);
+
+ return success;
+}
+
+static GSList *
+ecc_icalstrings_to_components (GSList *icalstrings)
+{
+ GSList *link;
+
+ for (link = icalstrings; link; link = g_slist_next (link)) {
+ gchar *icalstring = link->data;
+
+ link->data = e_cal_component_new_from_string (icalstring);
+
+ g_free (icalstring);
+ }
+
+ return icalstrings;
+}
+
+/**
+ * e_cal_cache_get_components_by_uid:
+ * @cal_cache: an #ECalCache
+ * @uid: a UID of the component
+ * @out_components: (out) (transfer full) (element-type ECalComponent): return location for the components
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets the master object and all detached instances for a component
+ * identified by the @uid. Free the returned #GSList with
+ * g_slist_free_full (components, g_object_unref); when
+ * no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_get_components_by_uid (ECalCache *cal_cache,
+ const gchar *uid,
+ GSList **out_components,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GSList *icalstrings = NULL;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (out_components != NULL, FALSE);
+
+ success = e_cal_cache_get_components_by_uid_as_string (cal_cache, uid, &icalstrings, cancellable,
error);
+ if (success) {
+ *out_components = ecc_icalstrings_to_components (icalstrings);
+ }
+
+ return success;
+}
+
+/**
+ * e_cal_cache_get_components_by_uid_as_string:
+ * @cal_cache: an #ECalCache
+ * @uid: a UID of the component
+ * @out_icalstrings: (out) (transfer full) (element-type utf8): return location for the iCal strings
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets the master object and all detached instances as string
+ * for a component identified by the @uid. Free the returned #GSList
+ * with g_slist_free_full (icalstrings, g_free); when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_get_components_by_uid_as_string (ECalCache *cal_cache,
+ const gchar *uid,
+ GSList **out_icalstrings,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gchar *stmt;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (out_icalstrings != NULL, FALSE);
+
+ *out_icalstrings = NULL;
+
+ /* Using 'ORDER BY' to get the master object first */
+ stmt = e_cache_sqlite_stmt_printf (
+ "SELECT " E_CACHE_COLUMN_OBJECT " FROM " E_CACHE_TABLE_OBJECTS
+ " WHERE " E_CACHE_COLUMN_UID "=%Q OR " E_CACHE_COLUMN_UID " LIKE '%q\n%%'"
+ " ORDER BY " E_CACHE_COLUMN_UID,
+ uid, uid);
+
+ success = e_cache_sqlite_select (E_CACHE (cal_cache), stmt, e_cal_cache_get_strings, out_icalstrings,
cancellable, error);
+
+ e_cache_sqlite_stmt_free (stmt);
+
+ if (success && !*out_icalstrings) {
+ success = FALSE;
+ g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s” not found"), uid);
+ } else if (success) {
+ *out_icalstrings = g_slist_reverse (*out_icalstrings);
+ }
+
+ return success;
+}
+
+/**
+ * e_cal_cache_get_components_in_range:
+ * @cal_cache: an #ECalCache
+ * @range_start: start of the range, as time_t, inclusive
+ * @range_end: end of the range, as time_t, exclusive
+ * @out_components: (out) (transfer full) (element-type ECalComponent): return location for the components
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets a list of components which occur in the given time range.
+ * It's not an error if none is found.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_get_components_in_range (ECalCache *cal_cache,
+ time_t range_start,
+ time_t range_end,
+ GSList **out_components,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GSList *icalstrings = NULL;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+ g_return_val_if_fail (out_components != NULL, FALSE);
+
+ success = e_cal_cache_get_components_in_range_as_strings (cal_cache, range_start, range_end,
&icalstrings, cancellable, error);
+ if (success)
+ *out_components = ecc_icalstrings_to_components (icalstrings);
+
+ return success;
+}
+
+static gboolean
+ecc_search_icalstrings_cb (ECalCache *cal_cache,
+ const gchar *uid,
+ const gchar *rid,
+ const gchar *revision,
+ const gchar *object,
+ const gchar *extra,
+ EOfflineState offline_state,
+ gpointer user_data)
+{
+ GSList **out_icalstrings = user_data;
+
+ g_return_val_if_fail (out_icalstrings != NULL, FALSE);
+ g_return_val_if_fail (object != NULL, FALSE);
+
+ *out_icalstrings = g_slist_prepend (*out_icalstrings, g_strdup (object));
+
+ return TRUE;
+}
+
+/**
+ * e_cal_cache_get_components_in_range_as_strings:
+ * @cal_cache: an #ECalCache
+ * @range_start: start of the range, as time_t, inclusive
+ * @range_end: end of the range, as time_t, exclusive
+ * @out_icalstrings: (out) (transfer full) (element-type utf8): return location for the iCal strings
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets a list of components, as iCal strings, which occur in the given time range.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_get_components_in_range_as_strings (ECalCache *cal_cache,
+ time_t range_start,
+ time_t range_end,
+ GSList **out_icalstrings,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gchar *sexp;
+ struct icaltimetype itt_start, itt_end;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+ g_return_val_if_fail (out_icalstrings != NULL, FALSE);
+
+ *out_icalstrings = NULL;
+
+ itt_start = icaltime_from_timet_with_zone (range_start, FALSE, NULL);
+ itt_end = icaltime_from_timet_with_zone (range_end, FALSE, NULL);
+
+ sexp = g_strdup_printf ("(occur-in-time-range? (make-time \"%04d%02d%02dT%02d%02d%02dZ\") (make-time
\"%04d%02d%02dT%02d%02d%02dZ\"))",
+ itt_start.year, itt_start.month, itt_start.day, itt_start.hour, itt_start.minute,
itt_start.second,
+ itt_end.year, itt_end.month, itt_end.day, itt_end.hour, itt_end.minute, itt_end.second);
+
+ success = e_cal_cache_search_with_callback (cal_cache, sexp, ecc_search_icalstrings_cb,
+ out_icalstrings, cancellable, error);
+
+ g_free (sexp);
+
+ if (success) {
+ *out_icalstrings = g_slist_reverse (*out_icalstrings);
+ } else {
+ g_slist_free_full (*out_icalstrings, g_free);
+ *out_icalstrings = NULL;
+ }
+
+ return success;
+}
+
+static gboolean
+ecc_search_data_cb (ECalCache *cal_cache,
+ const gchar *uid,
+ const gchar *rid,
+ const gchar *revision,
+ const gchar *object,
+ const gchar *extra,
+ EOfflineState offline_state,
+ gpointer user_data)
+{
+ GSList **out_data = user_data;
+
+ g_return_val_if_fail (out_data != NULL, FALSE);
+ g_return_val_if_fail (object != NULL, FALSE);
+
+ *out_data = g_slist_prepend (*out_data,
+ e_cal_cache_search_data_new (uid, rid, object, extra));
+
+ return TRUE;
+}
+
+static gboolean
+ecc_search_components_cb (ECalCache *cal_cache,
+ const gchar *uid,
+ const gchar *rid,
+ const gchar *revision,
+ const gchar *object,
+ const gchar *extra,
+ EOfflineState offline_state,
+ gpointer user_data)
+{
+ GSList **out_components = user_data;
+
+ g_return_val_if_fail (out_components != NULL, FALSE);
+ g_return_val_if_fail (object != NULL, FALSE);
+
+ *out_components = g_slist_prepend (*out_components,
+ e_cal_component_new_from_string (object));
+
+ return TRUE;
+}
+
+static gboolean
+ecc_search_ids_cb (ECalCache *cal_cache,
+ const gchar *uid,
+ const gchar *rid,
+ const gchar *revision,
+ const gchar *object,
+ const gchar *extra,
+ EOfflineState offline_state,
+ gpointer user_data)
+{
+ GSList **out_ids = user_data;
+
+ g_return_val_if_fail (out_ids != NULL, FALSE);
+ g_return_val_if_fail (object != NULL, FALSE);
+
+ *out_ids = g_slist_prepend (*out_ids, e_cal_component_id_new (uid, rid));
+
+ return TRUE;
+}
+
+/**
+ * e_cal_cache_search:
+ * @cal_cache: an #ECalCache
+ * @sexp: (nullable): search expression; use %NULL or an empty string to list all stored components
+ * @out_data: (out) (transfer full) (element-type ECalCacheSearchData): stored components, as search data,
satisfied by @sexp
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Searches the @cal_cache with the given @sexp and
+ * returns those components which satisfy the search
+ * expression as a #GSList of #ECalCacheSearchData.
+ * The @out_data should be freed with
+ * g_slist_free_full (data, e_cal_cache_search_data_free);
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_search (ECalCache *cal_cache,
+ const gchar *sexp,
+ GSList **out_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+ g_return_val_if_fail (out_data != NULL, FALSE);
+
+ *out_data = NULL;
+
+ success = e_cal_cache_search_with_callback (cal_cache, sexp, ecc_search_data_cb,
+ out_data, cancellable, error);
+ if (success) {
+ *out_data = g_slist_reverse (*out_data);
+ } else {
+ g_slist_free_full (*out_data, e_cal_cache_search_data_free);
+ *out_data = NULL;
+ }
+
+ return success;
+}
+
+/**
+ * e_cal_cache_search_components:
+ * @cal_cache: an #ECalCache
+ * @sexp: (nullable): search expression; use %NULL or an empty string to list all stored components
+ * @out_components: (out) (transfer full) (element-type ECalComponent): stored components satisfied by @sexp
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Searches the @cal_cache with the given @sexp and
+ * returns those components which satisfy the search
+ * expression. The @out_components should be freed with
+ * g_slist_free_full (components, g_object_unref); when
+ * no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_search_components (ECalCache *cal_cache,
+ const gchar *sexp,
+ GSList **out_components,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+ g_return_val_if_fail (out_components != NULL, FALSE);
+
+ *out_components = NULL;
+
+ success = e_cal_cache_search_with_callback (cal_cache, sexp, ecc_search_components_cb,
+ out_components, cancellable, error);
+ if (success) {
+ *out_components = g_slist_reverse (*out_components);
+ } else {
+ g_slist_free_full (*out_components, g_object_unref);
+ *out_components = NULL;
+ }
+
+ return success;
+}
+
+/**
+ * e_cal_cache_search_ids:
+ * @cal_cache: an #ECalCache
+ * @sexp: (nullable): search expression; use %NULL or an empty string to list all stored components
+ * @out_ids: (out) (transfer full) (element-type ECalComponentId): IDs of stored components satisfied by
@sexp
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Searches the @cal_cache with the given @sexp and returns ECalComponentId
+ * for those components which satisfy the search expression.
+ * The @out_ids should be freed with
+ * g_slist_free_full (components, (GDestroyNotify) e_cal_component_free_id);
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_search_ids (ECalCache *cal_cache,
+ const gchar *sexp,
+ GSList **out_ids,
+ GCancellable *cancellable,
+ GError **error)
+
+{
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+ g_return_val_if_fail (out_ids != NULL, FALSE);
+
+ *out_ids = NULL;
+
+ success = e_cal_cache_search_with_callback (cal_cache, sexp, ecc_search_ids_cb,
+ out_ids, cancellable, error);
+ if (success) {
+ *out_ids = g_slist_reverse (*out_ids);
+ } else {
+ g_slist_free_full (*out_ids, g_object_unref);
+ *out_ids = NULL;
+ }
+
+ return success;
+}
+
+/**
+ * e_cal_cache_search_with_callback:
+ * @cal_cache: an #ECalCache
+ * @sexp: (nullable): search expression; use %NULL or an empty string to list all stored components
+ * @func: an #ECalCacheSearchFunc callback to call for each row which satisfies @sexp
+ * @user_data: user data for @func
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Searches the @cal_cache with the given @sexp and calls @func for each
+ * row which satisfy the search expression.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_search_with_callback (ECalCache *cal_cache,
+ const gchar *sexp,
+ ECalCacheSearchFunc func,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalBackendSExp *bsexp = NULL;
+ gint sexp_id = -1;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+ g_return_val_if_fail (func != NULL, FALSE);
+
+ if (sexp && *sexp) {
+ bsexp = e_cal_backend_sexp_new (sexp);
+ if (!bsexp) {
+ g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY,
+ _("Invalid query: %s"), sexp);
+ return FALSE;
+ }
+
+ sexp_id = ecc_take_sexp_object (cal_cache, bsexp);
+ }
+
+ success = ecc_search_internal (cal_cache, sexp, sexp_id, func, user_data, cancellable, error);
+
+ if (bsexp)
+ ecc_free_sexp_object (cal_cache, sexp_id);
+
+ return success;
+}
+
+/**
+ * e_cal_cache_get_offline_changes:
+ * @cal_cache: an #ECalCache
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * The same as e_cache_get_offline_changes(), only splits the saved UID
+ * into UID and RID and saved the data into #ECalCacheOfflineChange structure.
+ *
+ * Returns: (transfer full) (element-type ECalCacheOfflineChange): A newly allocated list of all
+ * offline changes. Free it with g_slist_free_full (slist, e_cal_cache_offline_change_free);
+ * when no longer needed.
+ *
+ * Since: 3.26
+ **/
+GSList *
+e_cal_cache_get_offline_changes (ECalCache *cal_cache,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GSList *changes, *link;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), NULL);
+
+ changes = e_cache_get_offline_changes (E_CACHE (cal_cache), cancellable, error);
+
+ for (link = changes; link; link = g_slist_next (link)) {
+ ECacheOfflineChange *cache_change = link->data;
+ ECalCacheOfflineChange *cal_change;
+ gchar *uid = NULL, *rid = NULL;
+
+ if (!cache_change || !ecc_decode_id_sql (cache_change->uid, &uid, &rid)) {
+ g_warn_if_reached ();
+
+ e_cache_offline_change_free (cache_change);
+ link->data = NULL;
+
+ continue;
+ }
+
+ cal_change = e_cal_cache_offline_change_new (uid, rid, cache_change->revision,
cache_change->object, cache_change->state);
+ link->data = cal_change;
+
+ e_cache_offline_change_free (cache_change);
+ g_free (uid);
+ g_free (rid);
+ }
+
+ return changes;
+}
+
+/**
+ * e_cal_cache_delete_attachments:
+ * @cal_cache: an #ECalCache
+ * @component: an icalcomponent
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Deletes all locally stored attachments beside the cache file from the disk.
+ * This doesn't modify the @component. It's usually called before the @component
+ * is being removed from the @cal_cache.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_delete_attachments (ECalCache *cal_cache,
+ icalcomponent *component,
+ GCancellable *cancellable,
+ GError **error)
+{
+ icalproperty *prop;
+ gchar *cache_dirname = NULL;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+ g_return_val_if_fail (component != NULL, FALSE);
+
+ for (prop = icalcomponent_get_first_property (component, ICAL_ATTACH_PROPERTY);
+ prop;
+ prop = icalcomponent_get_next_property (component, ICAL_ATTACH_PROPERTY)) {
+ icalattach *attach = icalproperty_get_attach (prop);
+
+ if (attach && icalattach_get_is_url (attach)) {
+ const gchar *url;
+
+ url = icalattach_get_url (attach);
+ if (url) {
+ gsize buf_size;
+ gchar *buf;
+
+ buf_size = strlen (url);
+ buf = g_malloc0 (buf_size + 1);
+
+ icalvalue_decode_ical_string (url, buf, buf_size);
+
+ if (g_str_has_prefix (buf, "file://")) {
+ gchar *filename;
+
+ filename = g_filename_from_uri (buf, NULL, NULL);
+ if (filename) {
+ if (!cache_dirname)
+ cache_dirname = g_path_get_dirname
(e_cache_get_filename (E_CACHE (cal_cache)));
+
+ if (g_str_has_prefix (filename, cache_dirname) &&
+ g_unlink (filename) == -1) {
+ /* Ignore these errors */
+ }
+
+ g_free (filename);
+ }
+ }
+
+ g_free (buf);
+ }
+ }
+ }
+
+ g_free (cache_dirname);
+
+ return TRUE;
+}
+
+/**
+ * e_cal_cache_put_timezone:
+ * @cal_cache: an #ECalCache
+ * @zone: an icaltimezone to put
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Puts the @zone into the @cal_cache using its timezone ID as
+ * an identificator. The function adds a new or replaces existing,
+ * if any such already exists in the @cal_cache.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_put_timezone (ECalCache *cal_cache,
+ const icaltimezone *zone,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean success;
+ gchar *stmt;
+ const gchar *tzid;
+ gchar *component_str;
+ icalcomponent *component;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+ g_return_val_if_fail (zone != NULL, FALSE);
+
+ tzid = icaltimezone_get_tzid ((icaltimezone *) zone);
+ if (!tzid) {
+ g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Cannot add timezone
without tzid"));
+ return FALSE;
+ }
+
+ component = icaltimezone_get_component ((icaltimezone *) zone);
+ if (!component) {
+ g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Cannot add timezone
without component"));
+ return FALSE;
+ }
+
+ component_str = icalcomponent_as_ical_string_r (component);
+ if (!component_str) {
+ g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Cannot add timezone
with invalid component"));
+ return FALSE;
+ }
+
+ stmt = e_cache_sqlite_stmt_printf (
+ "INSERT or REPLACE INTO " ECC_TABLE_TIMEZONES " (tzid, zone) VALUES (%Q, %Q)",
+ tzid, component_str);
+
+ success = e_cache_sqlite_exec (E_CACHE (cal_cache), stmt, cancellable, error);
+
+ e_cache_sqlite_stmt_free (stmt);
+
+ g_free (component_str);
+
+ return success;
+}
+
+static icaltimezone *
+ecc_timezone_from_string (const gchar *icalstring)
+{
+ icalcomponent *component;
+
+ g_return_val_if_fail (icalstring != NULL, NULL);
+
+ component = icalcomponent_new_from_string (icalstring);
+ if (component) {
+ icaltimezone *zone;
+
+ zone = icaltimezone_new ();
+ if (!icaltimezone_set_component (zone, component)) {
+ icalcomponent_free (component);
+ icaltimezone_free (zone, 1);
+ } else {
+ return zone;
+ }
+ }
+
+ return NULL;
+}
+
+/**
+ * e_cal_cache_get_timezone:
+ * @cal_cache: an #ECalCache
+ * @tzid: a timezone ID to get
+ * @out_zone: (out) (transfer none): return location for the icaltimezone
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets a timezone with given @tzid, which had been previously put
+ * into the @cal_cache with e_cal_cache_put_timezone().
+ * The returned icaltimezone is owned by the @cal_cache and should
+ * not be freed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_get_timezone (ECalCache *cal_cache,
+ const gchar *tzid,
+ icaltimezone **out_zone,
+ GCancellable *cancellable,
+ GError **error)
+
+{
+ gchar *zone_str = NULL;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+ g_return_val_if_fail (tzid != NULL, FALSE);
+ g_return_val_if_fail (out_zone != NULL, FALSE);
+
+ g_rec_mutex_lock (&cal_cache->priv->timezones_lock);
+
+ *out_zone = g_hash_table_lookup (cal_cache->priv->loaded_timezones, tzid);
+ if (*out_zone) {
+ g_rec_mutex_unlock (&cal_cache->priv->timezones_lock);
+ return TRUE;
+ }
+
+ *out_zone = g_hash_table_lookup (cal_cache->priv->modified_timezones, tzid);
+ if (*out_zone) {
+ g_rec_mutex_unlock (&cal_cache->priv->timezones_lock);
+ return TRUE;
+ }
+
+ success = e_cal_cache_dup_timezone_as_string (cal_cache, tzid, &zone_str, cancellable, error);
+
+ if (success && zone_str) {
+ icaltimezone *zone;
+
+ zone = ecc_timezone_from_string (zone_str);
+ if (zone) {
+ g_hash_table_insert (cal_cache->priv->loaded_timezones, g_strdup (tzid), zone);
+ *out_zone = zone;
+ } else {
+ success = FALSE;
+ }
+ }
+
+ g_rec_mutex_unlock (&cal_cache->priv->timezones_lock);
+
+ g_free (zone_str);
+
+ return success;
+}
+
+/**
+ * e_cal_cache_dup_timezone_as_string:
+ * @cal_cache: an #ECalCache
+ * @tzid: a timezone ID to get
+ * @out_zone_string: (out) (transfer full): return location for the icaltimezone as iCal string
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets a timezone with given @tzid, which had been previously put
+ * into the @cal_cache with e_cal_cache_put_timezone().
+ * The returned string is an iCal string for that icaltimezone and
+ * should be freed with g_free() when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_dup_timezone_as_string (ECalCache *cal_cache,
+ const gchar *tzid,
+ gchar **out_zone_string,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gchar *stmt;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+ g_return_val_if_fail (tzid != NULL, FALSE);
+ g_return_val_if_fail (out_zone_string, FALSE);
+
+ *out_zone_string = NULL;
+
+ stmt = e_cache_sqlite_stmt_printf (
+ "SELECT zone FROM " ECC_TABLE_TIMEZONES " WHERE tzid=%Q",
+ tzid);
+
+ success = e_cache_sqlite_select (E_CACHE (cal_cache), stmt, e_cal_cache_get_string, out_zone_string,
cancellable, error) &&
+ *out_zone_string != NULL;
+
+ e_cache_sqlite_stmt_free (stmt);
+
+ return success;
+}
+
+static gboolean
+e_cal_cache_get_uint64_cb (ECache *cache,
+ gint ncols,
+ const gchar **column_names,
+ const gchar **column_values,
+ gpointer user_data)
+{
+ guint64 *pui64 = user_data;
+
+ g_return_val_if_fail (pui64 != NULL, FALSE);
+
+ if (ncols == 1) {
+ *pui64 = column_values[0] ? g_ascii_strtoull (column_values[0], NULL, 10) : 0;
+ } else {
+ *pui64 = 0;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+e_cal_cache_load_zones_cb (ECache *cache,
+ gint ncols,
+ const gchar *column_names[],
+ const gchar *column_values[],
+ gpointer user_data)
+{
+ GHashTable *loaded_zones = user_data;
+
+ g_return_val_if_fail (loaded_zones != NULL, FALSE);
+ g_return_val_if_fail (ncols == 2, FALSE);
+
+ /* Do not overwrite already loaded timezones, they can be used anywhere around */
+ if (!g_hash_table_lookup (loaded_zones, column_values[0])) {
+ icaltimezone *zone;
+
+ zone = ecc_timezone_from_string (column_values[1]);
+ if (zone) {
+ g_hash_table_insert (loaded_zones, g_strdup (column_values[0]), zone);
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * e_cal_cache_list_timezones:
+ * @cal_cache: an #ECalCache
+ * @out_timezones: (out) (transfer container) (element-type icaltimezone): return location for the list of
stored timezones
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets a list of all stored timezones by the @cal_cache.
+ * Only the returned list should be freed with g_list_free()
+ * when no longer needed; the icaltimezone-s are owned
+ * by the @cal_cache.
+ *
+ * Note: The list can contain timezones previously stored
+ * in the cache, but removed from it since they were loaded,
+ * because these are freed only when also the @cal_cache is freed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_list_timezones (ECalCache *cal_cache,
+ GList **out_timezones,
+ GCancellable *cancellable,
+ GError **error)
+{
+ guint64 n_stored = 0;
+ gchar *stmt;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+ g_return_val_if_fail (out_timezones != NULL, FALSE);
+
+ g_rec_mutex_lock (&cal_cache->priv->timezones_lock);
+
+ success = e_cache_sqlite_select (E_CACHE (cal_cache),
+ "SELECT COUNT(*) FROM " ECC_TABLE_TIMEZONES,
+ e_cal_cache_get_uint64_cb, &n_stored, cancellable, error);
+
+ if (success && n_stored != g_hash_table_size (cal_cache->priv->loaded_timezones)) {
+ if (n_stored == 0) {
+ g_rec_mutex_unlock (&cal_cache->priv->timezones_lock);
+ *out_timezones = NULL;
+
+ return TRUE;
+ }
+
+ stmt = e_cache_sqlite_stmt_printf ("SELECT tzid, zone FROM " ECC_TABLE_TIMEZONES);
+ success = e_cache_sqlite_select (E_CACHE (cal_cache), stmt,
+ e_cal_cache_load_zones_cb, cal_cache->priv->loaded_timezones, cancellable, error);
+ e_cache_sqlite_stmt_free (stmt);
+ }
+
+ if (success) {
+ GList *loaded, *modified;
+
+ loaded = g_hash_table_get_values (cal_cache->priv->loaded_timezones);
+ modified = g_hash_table_get_values (cal_cache->priv->modified_timezones);
+
+ if (loaded && modified)
+ *out_timezones = g_list_concat (loaded, modified);
+ else
+ *out_timezones = loaded ? loaded : modified;
+ }
+
+ g_rec_mutex_unlock (&cal_cache->priv->timezones_lock);
+
+ return success;
+}
+
+/**
+ * e_cal_cache_remove_timezones:
+ * @cal_cache: an #ECalCache
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Removes all stored timezones from the @cal_cache.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_cache_remove_timezones (ECalCache *cal_cache,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+
+ e_cache_lock (E_CACHE (cal_cache), E_CACHE_LOCK_WRITE);
+
+ g_rec_mutex_lock (&cal_cache->priv->timezones_lock);
+
+ success = e_cache_sqlite_exec (E_CACHE (cal_cache), "DELETE FROM " ECC_TABLE_TIMEZONES, cancellable,
error);
+
+ g_rec_mutex_unlock (&cal_cache->priv->timezones_lock);
+
+ e_cache_unlock (E_CACHE (cal_cache), success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+ return success;
+}
+
+void _e_cal_cache_remove_loaded_timezones (ECalCache *cal_cache);
+
+/* Private function, not meant to be part of the public API */
+void
+_e_cal_cache_remove_loaded_timezones (ECalCache *cal_cache)
+{
+ g_return_if_fail (E_IS_CAL_CACHE (cal_cache));
+
+ g_rec_mutex_lock (&cal_cache->priv->timezones_lock);
+
+ g_hash_table_remove_all (cal_cache->priv->loaded_timezones);
+ g_hash_table_remove_all (cal_cache->priv->modified_timezones);
+
+ g_rec_mutex_unlock (&cal_cache->priv->timezones_lock);
+}
+
+/**
+ * e_cal_cache_resolve_timezone_cb:
+ * @tzid: a timezone ID
+ * @cal_cache: an #ECalCache
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * An #ECalRecurResolveTimezoneCb callback, which can be used
+ * with e_cal_recur_generate_instances_sync(). The @cal_cache
+ * is supposed to be an #ECalCache instance. See also
+ * e_cal_cache_resolve_timezone_simple_cb().
+ *
+ * Returns: (transfer none) (nullable): the resolved icaltimezone, or %NULL, if not found
+ *
+ * Since: 3.26
+ **/
+icaltimezone *
+e_cal_cache_resolve_timezone_cb (const gchar *tzid,
+ gpointer cal_cache,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), NULL);
+
+ return e_cal_cache_resolve_timezone_simple_cb (tzid, cal_cache);
+}
+
+/**
+ * e_cal_cache_resolve_timezone_simple_cb:
+ * @tzid: a timezone ID
+ * @cal_cache: an #ECalCache
+ *
+ * An #ECalRecurResolveTimezoneFn callback, which can be used
+ * with e_cal_recur_ensure_end_dates() and simialr functions.
+ * The @cal_cache is supposed to be an #ECalCache instance. See
+ * also e_cal_cache_resolve_timezone_cb().
+ *
+ * Returns: (transfer none) (nullable): the resolved icaltimezone, or %NULL, if not found
+ *
+ * Since: 3.26
+ **/
+icaltimezone *
+e_cal_cache_resolve_timezone_simple_cb (const gchar *tzid,
+ gpointer cal_cache)
+{
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), NULL);
+
+ return e_timezone_cache_get_timezone (E_TIMEZONE_CACHE (cal_cache), tzid);
+}
+
+static gboolean
+ecc_search_delete_attachment_cb (ECalCache *cal_cache,
+ const gchar *uid,
+ const gchar *rid,
+ const gchar *revision,
+ const gchar *object,
+ const gchar *extra,
+ EOfflineState offline_state,
+ gpointer user_data)
+{
+ icalcomponent *icalcomp;
+ GCancellable *cancellable = user_data;
+ GError *local_error = NULL;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+ g_return_val_if_fail (object != NULL, FALSE);
+
+ icalcomp = icalcomponent_new_from_string (object);
+ if (!icalcomp)
+ return TRUE;
+
+ if (!e_cal_cache_delete_attachments (cal_cache, icalcomp, cancellable, &local_error)) {
+ if (rid && !*rid)
+ rid = NULL;
+
+ g_debug ("%s: Failed to remove attachments for '%s%s%s': %s", G_STRFUNC,
+ uid, rid ? "|" : "", rid ? rid : "", local_error ? local_error->message : "Unknown
error");
+ g_clear_error (&local_error);
+ }
+
+ icalcomponent_free (icalcomp);
+
+ return !g_cancellable_is_cancelled (cancellable);
+}
+
+static gboolean
+ecc_empty_aux_tables (ECache *cache,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gchar *stmt;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cache), FALSE);
+
+ stmt = e_cache_sqlite_stmt_printf ("DELETE FROM %Q", ECC_TABLE_TIMEZONES);
+ success = e_cache_sqlite_exec (cache, stmt, cancellable, error);
+ e_cache_sqlite_stmt_free (stmt);
+
+ return success;
+}
+
+/* The default revision is a concatenation of
+ <DTSTAMP> "-" <LAST-MODIFIED> "-" <SEQUENCE> */
+static gchar *
+ecc_dup_component_revision (ECalCache *cal_cache,
+ icalcomponent *icalcomp)
+{
+ struct icaltimetype itt;
+ icalproperty *prop;
+ GString *revision;
+
+ g_return_val_if_fail (icalcomp != NULL, NULL);
+
+ revision = g_string_sized_new (48);
+
+ itt = icalcomponent_get_dtstamp (icalcomp);
+ if (icaltime_is_null_time (itt) || !icaltime_is_valid_time (itt)) {
+ g_string_append_c (revision, 'x');
+ } else {
+ g_string_append_printf (revision, "%04d%02d%02d%02d%02d%02d",
+ itt.year, itt.month, itt.day,
+ itt.hour, itt.minute, itt.second);
+ }
+
+ g_string_append_c (revision, '-');
+
+ prop = icalcomponent_get_first_property (icalcomp, ICAL_LASTMODIFIED_PROPERTY);
+ if (prop)
+ itt = icalproperty_get_lastmodified (prop);
+
+ if (!prop || icaltime_is_null_time (itt) || !icaltime_is_valid_time (itt)) {
+ g_string_append_c (revision, 'x');
+ } else {
+ g_string_append_printf (revision, "%04d%02d%02d%02d%02d%02d",
+ itt.year, itt.month, itt.day,
+ itt.hour, itt.minute, itt.second);
+ }
+
+ g_string_append_c (revision, '-');
+
+ prop = icalcomponent_get_first_property (icalcomp, ICAL_SEQUENCE_PROPERTY);
+ if (!prop) {
+ g_string_append_c (revision, 'x');
+ } else {
+ g_string_append_printf (revision, "%d", icalproperty_get_sequence (prop));
+ }
+
+ return g_string_free (revision, FALSE);
+}
+
+static gboolean
+e_cal_cache_put_locked (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ ECacheColumnValues *other_columns,
+ EOfflineState offline_state,
+ gboolean is_replace,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalCache *cal_cache;
+ ECalComponent *comp;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cache), FALSE);
+ g_return_val_if_fail (E_CACHE_CLASS (e_cal_cache_parent_class)->put_locked != NULL, FALSE);
+
+ cal_cache = E_CAL_CACHE (cache);
+
+ comp = e_cal_component_new_from_string (object);
+ if (!comp)
+ return FALSE;
+
+ ecc_fill_other_columns (cal_cache, other_columns, comp);
+
+ success = E_CACHE_CLASS (e_cal_cache_parent_class)->put_locked (cache, uid, revision, object,
other_columns, offline_state,
+ is_replace, cancellable, error);
+
+ g_clear_object (&comp);
+
+ return success;
+}
+
+static gboolean
+e_cal_cache_remove_all_locked (ECache *cache,
+ const GSList *uids,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CAL_CACHE (cache), FALSE);
+ g_return_val_if_fail (E_CACHE_CLASS (e_cal_cache_parent_class)->remove_all_locked != NULL, FALSE);
+
+ /* Cannot free content of priv->loaded_timezones and priv->modified_timezones,
+ because those can be used anywhere */
+ success = ecc_empty_aux_tables (cache, cancellable, error) &&
+ e_cal_cache_search_with_callback (E_CAL_CACHE (cache), NULL,
+ ecc_search_delete_attachment_cb, cancellable, cancellable, error);
+
+ success = success && E_CACHE_CLASS (e_cal_cache_parent_class)->remove_all_locked (cache, uids,
cancellable, error);
+
+ return success;
+}
+
+static void
+cal_cache_free_zone (gpointer ptr)
+{
+ icaltimezone *zone = ptr;
+
+ if (zone)
+ icaltimezone_free (zone, 1);
+}
+
+static void
+ecc_add_cached_timezone (ETimezoneCache *cache,
+ icaltimezone *zone)
+{
+ ECalCache *cal_cache;
+ const gchar *tzid;
+
+ cal_cache = E_CAL_CACHE (cache);
+
+ tzid = icaltimezone_get_tzid (zone);
+ if (tzid == NULL)
+ return;
+
+ e_cal_cache_put_timezone (cal_cache, zone, NULL, NULL);
+}
+
+static icaltimezone *
+ecc_get_cached_timezone (ETimezoneCache *cache,
+ const gchar *tzid)
+{
+ ECalCache *cal_cache;
+ icaltimezone *zone = NULL;
+ icaltimezone *builtin_zone = NULL;
+ icalcomponent *icalcomp;
+ icalproperty *prop;
+ const gchar *builtin_tzid;
+
+ cal_cache = E_CAL_CACHE (cache);
+
+ if (g_str_equal (tzid, "UTC"))
+ return icaltimezone_get_utc_timezone ();
+
+ g_rec_mutex_lock (&cal_cache->priv->timezones_lock);
+
+ /* See if we already have it in the cache. */
+ zone = g_hash_table_lookup (cal_cache->priv->loaded_timezones, tzid);
+ if (zone)
+ goto exit;
+
+ zone = g_hash_table_lookup (cal_cache->priv->modified_timezones, tzid);
+ if (zone)
+ goto exit;
+
+ /* Try the location first */
+ /*zone = icaltimezone_get_builtin_timezone (tzid);
+ if (zone)
+ goto exit;*/
+
+ /* Try to replace the original time zone with a more complete
+ * and/or potentially updated built-in time zone. Note this also
+ * applies to TZIDs which match built-in time zones exactly: they
+ * are extracted via icaltimezone_get_builtin_timezone_from_tzid(). */
+
+ builtin_tzid = e_cal_match_tzid (tzid);
+
+ if (builtin_tzid)
+ builtin_zone = icaltimezone_get_builtin_timezone_from_tzid (builtin_tzid);
+
+ if (!builtin_zone) {
+ e_cal_cache_get_timezone (cal_cache, tzid, &zone, NULL, NULL);
+ goto exit;
+ }
+
+ /* Use the built-in time zone *and* rename it. Likely the caller
+ * is asking for a specific TZID because it has an event with such
+ * a TZID. Returning an icaltimezone with a different TZID would
+ * lead to broken VCALENDARs in the caller. */
+
+ icalcomp = icaltimezone_get_component (builtin_zone);
+ icalcomp = icalcomponent_new_clone (icalcomp);
+
+ prop = icalcomponent_get_first_property (icalcomp, ICAL_ANY_PROPERTY);
+
+ while (prop != NULL) {
+ if (icalproperty_isa (prop) == ICAL_TZID_PROPERTY) {
+ icalproperty_set_value_from_string (prop, tzid, "NO");
+ break;
+ }
+
+ prop = icalcomponent_get_next_property (icalcomp, ICAL_ANY_PROPERTY);
+ }
+
+ if (icalcomp != NULL) {
+ zone = icaltimezone_new ();
+ if (icaltimezone_set_component (zone, icalcomp)) {
+ tzid = icaltimezone_get_tzid (zone);
+ g_hash_table_insert (cal_cache->priv->modified_timezones, g_strdup (tzid), zone);
+ } else {
+ icalcomponent_free (icalcomp);
+ icaltimezone_free (zone, 1);
+ zone = NULL;
+ }
+ }
+
+ exit:
+ g_rec_mutex_unlock (&cal_cache->priv->timezones_lock);
+
+ return zone;
+}
+
+static GList *
+ecc_list_cached_timezones (ETimezoneCache *cache)
+{
+ GList *timezones = NULL;
+
+ e_cal_cache_list_timezones (E_CAL_CACHE (cache), &timezones, NULL, NULL);
+
+ return timezones;
+}
+
+static void
+e_cal_cache_finalize (GObject *object)
+{
+ ECalCache *cal_cache = E_CAL_CACHE (object);
+
+ g_hash_table_destroy (cal_cache->priv->loaded_timezones);
+ g_hash_table_destroy (cal_cache->priv->modified_timezones);
+ g_hash_table_destroy (cal_cache->priv->sexps);
+
+ g_rec_mutex_clear (&cal_cache->priv->timezones_lock);
+ g_mutex_clear (&cal_cache->priv->sexps_lock);
+
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_cal_cache_parent_class)->finalize (object);
+}
+
+static void
+e_cal_cache_class_init (ECalCacheClass *klass)
+{
+ GObjectClass *object_class;
+ ECacheClass *cache_class;
+
+ g_type_class_add_private (klass, sizeof (ECalCachePrivate));
+
+ object_class = G_OBJECT_CLASS (klass);
+ object_class->finalize = e_cal_cache_finalize;
+
+ cache_class = E_CACHE_CLASS (klass);
+ cache_class->put_locked = e_cal_cache_put_locked;
+ cache_class->remove_all_locked = e_cal_cache_remove_all_locked;
+
+ klass->dup_component_revision = ecc_dup_component_revision;
+
+ /**
+ * @ECalCache:dup-component-revision:
+ * A signal being called to get revision of an icalcomponent.
+ * The default implementation uses a concatenation of
+ * DTSTAMP '-' LASTMODIFIED '-' SEQUENCE.
+ **/
+ signals[DUP_COMPONENT_REVISION] = g_signal_new (
+ "dup-component-revision",
+ G_OBJECT_CLASS_TYPE (klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+ G_STRUCT_OFFSET (ECalCacheClass, dup_component_revision),
+ g_signal_accumulator_first_wins,
+ NULL,
+ g_cclosure_marshal_generic,
+ G_TYPE_STRING, 1,
+ G_TYPE_POINTER);
+}
+
+static void
+ecc_timezone_cache_init (ETimezoneCacheInterface *iface)
+{
+ iface->add_timezone = ecc_add_cached_timezone;
+ iface->get_timezone = ecc_get_cached_timezone;
+ iface->list_timezones = ecc_list_cached_timezones;
+}
+
+static void
+e_cal_cache_init (ECalCache *cal_cache)
+{
+ cal_cache->priv = G_TYPE_INSTANCE_GET_PRIVATE (cal_cache, E_TYPE_CAL_CACHE, ECalCachePrivate);
+ cal_cache->priv->loaded_timezones = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
cal_cache_free_zone);
+ cal_cache->priv->modified_timezones = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
cal_cache_free_zone);
+
+ cal_cache->priv->sexps = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_object_unref);
+
+ g_rec_mutex_init (&cal_cache->priv->timezones_lock);
+ g_mutex_init (&cal_cache->priv->sexps_lock);
+}
diff --git a/src/calendar/libedata-cal/e-cal-cache.h b/src/calendar/libedata-cal/e-cal-cache.h
new file mode 100644
index 0000000..ede88a7
--- /dev/null
+++ b/src/calendar/libedata-cal/e-cal-cache.h
@@ -0,0 +1,335 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2016 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#if !defined (__LIBEDATA_CAL_H_INSIDE__) && !defined (LIBEDATA_CAL_COMPILATION)
+#error "Only <libedata-cal/libedata-cal.h> should be included directly."
+#endif
+
+#ifndef E_CAL_CACHE_H
+#define E_CAL_CACHE_H
+
+#include <libebackend/libebackend.h>
+#include <libecal/libecal.h>
+
+/* Standard GObject macros */
+#define E_TYPE_CAL_CACHE \
+ (e_cal_cache_get_type ())
+#define E_CAL_CACHE(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST \
+ ((obj), E_TYPE_CAL_CACHE, ECalCache))
+#define E_CAL_CACHE_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_CAST \
+ ((cls), E_TYPE_CAL_CACHE, ECalCacheClass))
+#define E_IS_CAL_CACHE(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE \
+ ((obj), E_TYPE_CAL_CACHE))
+#define E_IS_CAL_CACHE_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_TYPE \
+ ((cls), E_TYPE_CAL_CACHE))
+#define E_CAL_CACHE_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS \
+ ((obj), E_TYPE_CAL_CACHE, ECalCacheClass))
+
+G_BEGIN_DECLS
+
+typedef struct _ECalCache ECalCache;
+typedef struct _ECalCacheClass ECalCacheClass;
+typedef struct _ECalCachePrivate ECalCachePrivate;
+
+/**
+ * ECalCacheOfflineChange:
+ * @uid: UID of the component
+ * @rid: Recurrence-ID of the component
+ * @revision: stored revision of the component
+ * @object: the component itself, as iCalalendar string
+ * @state: an #EOfflineState of the component
+ *
+ * Holds the information about offline change for one component.
+ *
+ * Since: 3.26
+ **/
+typedef struct {
+ gchar *uid;
+ gchar *rid;
+ gchar *revision;
+ gchar *object;
+ EOfflineState state;
+} ECalCacheOfflineChange;
+
+#define E_TYPE_CAL_CACHE_OFFLINE_CHANGE (e_cal_cache_offline_change_get_type ())
+
+GType e_cal_cache_offline_change_get_type
+ (void) G_GNUC_CONST;
+ECalCacheOfflineChange *
+ e_cal_cache_offline_change_new (const gchar *uid,
+ const gchar *rid,
+ const gchar *revision,
+ const gchar *object,
+ EOfflineState state);
+ECalCacheOfflineChange *
+ e_cal_cache_offline_change_copy (const ECalCacheOfflineChange *change);
+void e_cal_cache_offline_change_free (/* ECalCacheOfflineChange */ gpointer change);
+
+/**
+ * ECalCacheSearchData:
+ * @uid: the UID of this component
+ * @rid: (nullable): the Recurrence-ID of this component
+ * @object: the component string
+ * @extra: any extra data associated with the component
+ *
+ * This structure is used to represent components returned
+ * by the #ECalCache from various functions
+ * such as e_cal_cache_search().
+ *
+ * The @extra parameter will contain any data which was
+ * previously passed for this component in e_cal_cache_add_component()
+ * or set with e_cal_cache_set_component_extra().
+ *
+ * These should be freed with e_cal_cache_search_data_free().
+ *
+ * Since: 3.26
+ **/
+typedef struct {
+ gchar *uid;
+ gchar *rid;
+ gchar *object;
+ gchar *extra;
+} ECalCacheSearchData;
+
+#define E_TYPE_CAL_CACHE_SEARCH_DATA (e_cal_cache_search_data_get_type ())
+
+GType e_cal_cache_search_data_get_type
+ (void) G_GNUC_CONST;
+ECalCacheSearchData *
+ e_cal_cache_search_data_new (const gchar *uid,
+ const gchar *rid,
+ const gchar *object,
+ const gchar *extra);
+ECalCacheSearchData *
+ e_cal_cache_search_data_copy (const ECalCacheSearchData *data);
+void e_cal_cache_search_data_free (/* ECalCacheSearchData * */ gpointer data);
+
+/**
+ * ECalCacheSearchFunc:
+ * @cal_cache: an #ECalCache
+ * @uid: a unique object identifier
+ * @rid: (nullable): an optional Recurrence-ID of the object
+ * @revision: the object revision
+ * @object: the object itself
+ * @extra: extra data stored with the object
+ * @offline_state: objects offline state, one of #EOfflineState
+ * @user_data: user data, as used in e_cache_cache_search_with_callback()
+ *
+ * A callback called for each object row when using
+ * e_cal_cache_search_with_callback() function.
+ *
+ * Returns: %TRUE to continue, %FALSE to stop walk through.
+ *
+ * Since: 3.26
+ **/
+typedef gboolean (* ECalCacheSearchFunc) (ECalCache *cal_cache,
+ const gchar *uid,
+ const gchar *rid,
+ const gchar *revision,
+ const gchar *object,
+ const gchar *extra,
+ EOfflineState offline_state,
+ gpointer user_data);
+
+/**
+ * ECalCache:
+ *
+ * Contains only private data that should be read and manipulated using
+ * the functions below.
+ *
+ * Since: 3.26
+ **/
+struct _ECalCache {
+ /*< private >*/
+ ECache parent;
+ ECalCachePrivate *priv;
+};
+
+/**
+ * ECalCacheClass:
+ *
+ * Class structure for the #ECalCache class.
+ *
+ * Since: 3.26
+ */
+struct _ECalCacheClass {
+ /*< private >*/
+ ECacheClass parent_class;
+
+ /* Signals */
+ gchar * (* dup_component_revision)
+ (ECalCache *cal_cache,
+ icalcomponent *icalcomp);
+
+ /* Padding for future expansion */
+ gpointer reserved[10];
+};
+
+GType e_cal_cache_get_type (void) G_GNUC_CONST;
+
+ECalCache * e_cal_cache_new (const gchar *filename,
+ GCancellable *cancellable,
+ GError **error);
+gchar * e_cal_cache_dup_component_revision
+ (ECalCache *cal_cache,
+ icalcomponent *icalcomp);
+gboolean e_cal_cache_contains (ECalCache *cal_cache,
+ const gchar *uid,
+ const gchar *rid,
+ ECacheDeletedFlag deleted_flag);
+gboolean e_cal_cache_put_component (ECalCache *cal_cache,
+ ECalComponent *component,
+ const gchar *extra,
+ ECacheOfflineFlag offline_flag,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_cache_put_components (ECalCache *cal_cache,
+ const GSList *components, /* ECalComponent * */
+ const GSList *extras, /* gchar * */
+ ECacheOfflineFlag offline_flag,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_cache_remove_component (ECalCache *cal_cache,
+ const gchar *uid,
+ const gchar *rid,
+ ECacheOfflineFlag offline_flag,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_cache_remove_components (ECalCache *cal_cache,
+ const GSList *ids, /* ECalComponentId * */
+ ECacheOfflineFlag offline_flag,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_cache_get_component (ECalCache *cal_cache,
+ const gchar *uid,
+ const gchar *rid,
+ ECalComponent **out_component,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_cache_get_component_as_string
+ (ECalCache *cal_cache,
+ const gchar *uid,
+ const gchar *rid,
+ gchar **out_icalstring,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_cache_set_component_extra (ECalCache *cal_cache,
+ const gchar *uid,
+ const gchar *rid,
+ const gchar *extra,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_cache_get_component_extra (ECalCache *cal_cache,
+ const gchar *uid,
+ const gchar *rid,
+ gchar **out_extra,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_cache_get_components_by_uid
+ (ECalCache *cal_cache,
+ const gchar *uid,
+ GSList **out_components, /* ECalComponent * */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_cache_get_components_by_uid_as_string
+ (ECalCache *cal_cache,
+ const gchar *uid,
+ GSList **out_icalstrings, /* gchar * */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_cache_get_components_in_range
+ (ECalCache *cal_cache,
+ time_t range_start,
+ time_t range_end,
+ GSList **out_components, /* ECalComponent * */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_cache_get_components_in_range_as_strings
+ (ECalCache *cal_cache,
+ time_t range_start,
+ time_t range_end,
+ GSList **out_icalstrings, /* gchar * */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_cache_search (ECalCache *cal_cache,
+ const gchar *sexp,
+ GSList **out_data, /* ECalCacheSearchData * * */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_cache_search_components (ECalCache *cal_cache,
+ const gchar *sexp,
+ GSList **out_components, /* ECalComponent * */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_cache_search_ids (ECalCache *cal_cache,
+ const gchar *sexp,
+ GSList **out_ids, /* ECalComponentId * */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_cache_search_with_callback
+ (ECalCache *cal_cache,
+ const gchar *sexp,
+ ECalCacheSearchFunc func,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error);
+GSList * e_cal_cache_get_offline_changes (ECalCache *cal_cache,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_cache_delete_attachments (ECalCache *cal_cache,
+ icalcomponent *component,
+ GCancellable *cancellable,
+ GError **error);
+
+gboolean e_cal_cache_put_timezone (ECalCache *cal_cache,
+ const icaltimezone *zone,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_cache_get_timezone (ECalCache *cal_cache,
+ const gchar *tzid,
+ icaltimezone **out_zone,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_cache_dup_timezone_as_string
+ (ECalCache *cal_cache,
+ const gchar *tzid,
+ gchar **out_zone_string,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_cache_list_timezones (ECalCache *cal_cache,
+ GList **out_timezones,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_cache_remove_timezones (ECalCache *cal_cache,
+ GCancellable *cancellable,
+ GError **error);
+icaltimezone * e_cal_cache_resolve_timezone_cb (const gchar *tzid,
+ gpointer cal_cache,
+ GCancellable *cancellable,
+ GError **error);
+icaltimezone * e_cal_cache_resolve_timezone_simple_cb
+ (const gchar *tzid,
+ gpointer cal_cache);
+
+G_END_DECLS
+
+#endif /* E_CAL_CACHE_H */
diff --git a/src/calendar/libedata-cal/e-cal-meta-backend.c b/src/calendar/libedata-cal/e-cal-meta-backend.c
new file mode 100644
index 0000000..49ee547
--- /dev/null
+++ b/src/calendar/libedata-cal/e-cal-meta-backend.c
@@ -0,0 +1,4570 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * SECTION: e-cal-meta-backend
+ * @include: libedata-cal/libedata-cal.h
+ * @short_description: An #ECalBackend descendant for calendar backends
+ *
+ * The #ECalMetaBackend is an abstract #ECalBackend descendant which
+ * aims to implement all evolution-data-server internals for the backend
+ * itself and lefts the backend do as minimum work as possible, like
+ * loading and saving components, listing available components and so on,
+ * thus the backend implementation can focus on things like converting
+ * (possibly) remote data into iCalendar objects and back.
+ *
+ * As the #ECalMetaBackend uses an #ECalCache, the offline support
+ * is provided by default.
+ *
+ * The structure is thread safe.
+ **/
+
+#include "evolution-data-server-config.h"
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+
+#include "e-cal-backend-sexp.h"
+#include "e-cal-backend-sync.h"
+#include "e-cal-backend-util.h"
+#include "e-cal-meta-backend.h"
+
+#define ECMB_KEY_SYNC_TAG "ecmb::sync-tag"
+#define ECMB_KEY_EVER_CONNECTED "ecmb::ever-connected"
+#define ECMB_KEY_CONNECTED_WRITABLE "ecmb::connected-writable"
+
+#define LOCAL_PREFIX "file://"
+
+struct _ECalMetaBackendPrivate {
+ GMutex connect_lock;
+ GMutex property_lock;
+ GError *create_cache_error;
+ ECalCache *cache;
+ ENamedParameters *last_credentials;
+ GHashTable *view_cancellables;
+ GCancellable *refresh_cancellable; /* Set when refreshing the content */
+ GCancellable *source_changed_cancellable; /* Set when processing source changed signal */
+ GCancellable *go_offline_cancellable; /* Set when going offline */
+ gboolean current_online_state; /* The only state of the internal structures;
+ used to detect false notifications on EBackend::online */
+ gulong source_changed_id;
+ gulong notify_online_id;
+ gulong revision_changed_id;
+ guint refresh_timeout_id;
+
+ gboolean refresh_after_authenticate;
+ gint ever_connected;
+ gint connected_writable;
+
+ /* Last successful connect data, for some extensions */
+ guint16 authentication_port;
+ gchar *authentication_host;
+ gchar *authentication_user;
+ gchar *authentication_method;
+ gchar *authentication_proxy_uid;
+ gchar *authentication_credential_name;
+ SoupURI *webdav_soup_uri;
+};
+
+enum {
+ PROP_0,
+ PROP_CACHE
+};
+
+enum {
+ REFRESH_COMPLETED,
+ SOURCE_CHANGED,
+ LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL];
+
+G_DEFINE_ABSTRACT_TYPE (ECalMetaBackend, e_cal_meta_backend, E_TYPE_CAL_BACKEND_SYNC)
+
+G_DEFINE_BOXED_TYPE (ECalMetaBackendInfo, e_cal_meta_backend_info, e_cal_meta_backend_info_copy,
e_cal_meta_backend_info_free)
+
+static void ecmb_schedule_refresh (ECalMetaBackend *meta_backend);
+static void ecmb_schedule_source_changed (ECalMetaBackend *meta_backend);
+static void ecmb_schedule_go_offline (ECalMetaBackend *meta_backend);
+static gboolean ecmb_load_component_wrapper_sync (ECalMetaBackend *meta_backend,
+ ECalCache *cal_cache,
+ const gchar *uid,
+ const gchar *preloaded_object,
+ const gchar *preloaded_extra,
+ gchar **out_new_uid,
+ GCancellable *cancellable,
+ GError **error);
+static gboolean ecmb_save_component_wrapper_sync (ECalMetaBackend *meta_backend,
+ ECalCache *cal_cache,
+ gboolean overwrite_existing,
+ EConflictResolution conflict_resolution,
+ const GSList *in_instances,
+ const gchar *extra,
+ const gchar *orig_uid,
+ gboolean *out_requires_put,
+ gchar **out_new_uid,
+ gchar **out_new_extra,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * e_cal_meta_backend_info_new:
+ * @uid: a component UID; cannot be %NULL
+ * @revision: (nullable): the component revision; can be %NULL
+ * @object: (nullable): the component object as an iCalendar string; can be %NULL
+ * @extra: (nullable): extra backend-specific data; can be %NULL
+ *
+ * Creates a new #ECalMetaBackendInfo prefilled with the given values.
+ *
+ * Returns: (transfer full): A new #ECalMetaBackendInfo. Free it with
+ * e_cal_meta_backend_info_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+ECalMetaBackendInfo *
+e_cal_meta_backend_info_new (const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ const gchar *extra)
+{
+ ECalMetaBackendInfo *info;
+
+ g_return_val_if_fail (uid != NULL, NULL);
+
+ info = g_new0 (ECalMetaBackendInfo, 1);
+ info->uid = g_strdup (uid);
+ info->revision = g_strdup (revision);
+ info->object = g_strdup (object);
+ info->extra = g_strdup (extra);
+
+ return info;
+}
+
+/**
+ * e_cal_meta_backend_info_copy:
+ * @src: (nullable): a source ECalMetaBackendInfo to copy, or %NULL
+ *
+ * Returns: (transfer full): Copy of the given @src. Free it with
+ * e_cal_meta_backend_info_free() when no longer needed.
+ * If the @src is %NULL, then returns %NULL as well.
+ *
+ * Since: 3.26
+ **/
+ECalMetaBackendInfo *
+e_cal_meta_backend_info_copy (const ECalMetaBackendInfo *src)
+{
+ if (!src)
+ return NULL;
+
+ return e_cal_meta_backend_info_new (src->uid, src->revision, src->object, src->extra);
+}
+
+/**
+ * e_cal_meta_backend_info_free:
+ * @ptr: (nullable): an #ECalMetaBackendInfo
+ *
+ * Frees the @ptr structure, previously allocated with e_cal_meta_backend_info_new()
+ * or e_cal_meta_backend_info_copy().
+ *
+ * Since: 3.26
+ **/
+void
+e_cal_meta_backend_info_free (gpointer ptr)
+{
+ ECalMetaBackendInfo *info = ptr;
+
+ if (info) {
+ g_free (info->uid);
+ g_free (info->revision);
+ g_free (info->object);
+ g_free (info->extra);
+ g_free (info);
+ }
+}
+
+/* Unref returned cancellable with g_object_unref(), when done with it */
+static GCancellable *
+ecmb_create_view_cancellable (ECalMetaBackend *meta_backend,
+ EDataCalView *view)
+{
+ GCancellable *cancellable;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), NULL);
+ g_return_val_if_fail (E_IS_DATA_CAL_VIEW (view), NULL);
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ cancellable = g_cancellable_new ();
+ g_hash_table_insert (meta_backend->priv->view_cancellables, view, g_object_ref (cancellable));
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ return cancellable;
+}
+
+static GCancellable *
+ecmb_steal_view_cancellable (ECalMetaBackend *meta_backend,
+ EDataCalView *view)
+{
+ GCancellable *cancellable;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), NULL);
+ g_return_val_if_fail (E_IS_DATA_CAL_VIEW (view), NULL);
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ cancellable = g_hash_table_lookup (meta_backend->priv->view_cancellables, view);
+ if (cancellable) {
+ g_object_ref (cancellable);
+ g_hash_table_remove (meta_backend->priv->view_cancellables, view);
+ }
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ return cancellable;
+}
+
+static void
+ecmb_update_connection_values (ECalMetaBackend *meta_backend)
+{
+ ESource *source;
+
+ g_return_if_fail (E_IS_CAL_META_BACKEND (meta_backend));
+
+ source = e_backend_get_source (E_BACKEND (meta_backend));
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ meta_backend->priv->authentication_port = 0;
+ g_clear_pointer (&meta_backend->priv->authentication_host, g_free);
+ g_clear_pointer (&meta_backend->priv->authentication_user, g_free);
+ g_clear_pointer (&meta_backend->priv->authentication_method, g_free);
+ g_clear_pointer (&meta_backend->priv->authentication_proxy_uid, g_free);
+ g_clear_pointer (&meta_backend->priv->authentication_credential_name, g_free);
+ g_clear_pointer (&meta_backend->priv->webdav_soup_uri, (GDestroyNotify) soup_uri_free);
+
+ if (source && e_source_has_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
+ ESourceAuthentication *auth_extension;
+
+ auth_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
+
+ meta_backend->priv->authentication_port = e_source_authentication_get_port (auth_extension);
+ meta_backend->priv->authentication_host = e_source_authentication_dup_host (auth_extension);
+ meta_backend->priv->authentication_user = e_source_authentication_dup_user (auth_extension);
+ meta_backend->priv->authentication_method = e_source_authentication_dup_method
(auth_extension);
+ meta_backend->priv->authentication_proxy_uid = e_source_authentication_dup_proxy_uid
(auth_extension);
+ meta_backend->priv->authentication_credential_name =
e_source_authentication_dup_credential_name (auth_extension);
+ }
+
+ if (source && e_source_has_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND)) {
+ ESourceWebdav *webdav_extension;
+
+ webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+
+ meta_backend->priv->webdav_soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
+ }
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ e_cal_meta_backend_set_ever_connected (meta_backend, TRUE);
+ e_cal_meta_backend_set_connected_writable (meta_backend, e_cal_backend_get_writable (E_CAL_BACKEND
(meta_backend)));
+}
+
+static gboolean
+ecmb_connect_wrapper_sync (ECalMetaBackend *meta_backend,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ENamedParameters *credentials;
+ ESourceAuthenticationResult auth_result = E_SOURCE_AUTHENTICATION_UNKNOWN;
+ ESourceCredentialsReason creds_reason = E_SOURCE_CREDENTIALS_REASON_ERROR;
+ gchar *certificate_pem = NULL;
+ GTlsCertificateFlags certificate_errors = 0;
+ GError *local_error = NULL;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+
+ if (!e_backend_get_online (E_BACKEND (meta_backend))) {
+ g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_REPOSITORY_OFFLINE,
+ e_client_error_to_string (E_CLIENT_ERROR_REPOSITORY_OFFLINE));
+
+ return FALSE;
+ }
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+ credentials = e_named_parameters_new_clone (meta_backend->priv->last_credentials);
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ g_mutex_lock (&meta_backend->priv->connect_lock);
+
+ if (e_cal_meta_backend_connect_sync (meta_backend, credentials, &auth_result, &certificate_pem,
&certificate_errors,
+ cancellable, &local_error)) {
+ ecmb_update_connection_values (meta_backend);
+ g_mutex_unlock (&meta_backend->priv->connect_lock);
+ e_named_parameters_free (credentials);
+
+ return TRUE;
+ }
+
+ g_mutex_unlock (&meta_backend->priv->connect_lock);
+
+ e_named_parameters_free (credentials);
+
+ g_warn_if_fail (auth_result != E_SOURCE_AUTHENTICATION_ACCEPTED);
+
+ switch (auth_result) {
+ case E_SOURCE_AUTHENTICATION_UNKNOWN:
+ if (local_error)
+ g_propagate_error (error, local_error);
+ g_free (certificate_pem);
+ return FALSE;
+ case E_SOURCE_AUTHENTICATION_ERROR:
+ creds_reason = E_SOURCE_CREDENTIALS_REASON_ERROR;
+ break;
+ case E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED:
+ creds_reason = E_SOURCE_CREDENTIALS_REASON_SSL_FAILED;
+ break;
+ case E_SOURCE_AUTHENTICATION_ACCEPTED:
+ g_warn_if_reached ();
+ break;
+ case E_SOURCE_AUTHENTICATION_REJECTED:
+ creds_reason = E_SOURCE_CREDENTIALS_REASON_REJECTED;
+ break;
+ case E_SOURCE_AUTHENTICATION_REQUIRED:
+ creds_reason = E_SOURCE_CREDENTIALS_REASON_REQUIRED;
+ break;
+ }
+
+ e_backend_schedule_credentials_required (E_BACKEND (meta_backend), creds_reason, certificate_pem,
certificate_errors,
+ local_error, cancellable, G_STRFUNC);
+
+ g_clear_error (&local_error);
+ g_free (certificate_pem);
+
+ return FALSE;
+}
+
+static gboolean
+ecmb_gather_locally_cached_objects_cb (ECalCache *cal_cache,
+ const gchar *uid,
+ const gchar *rid,
+ const gchar *revision,
+ const gchar *object,
+ const gchar *extra,
+ EOfflineState offline_state,
+ gpointer user_data)
+{
+ GHashTable *locally_cached = user_data;
+
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (locally_cached != NULL, FALSE);
+
+ if (offline_state == E_OFFLINE_STATE_SYNCED) {
+ g_hash_table_insert (locally_cached,
+ e_cal_component_id_new (uid, rid),
+ g_strdup (revision));
+ }
+
+ return TRUE;
+}
+
+static gboolean
+ecmb_get_changes_sync (ECalMetaBackend *meta_backend,
+ const gchar *last_sync_tag,
+ gboolean is_repeat,
+ gchar **out_new_sync_tag,
+ gboolean *out_repeat,
+ GSList **out_created_objects,
+ GSList **out_modified_objects,
+ GSList **out_removed_objects,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GHashTable *locally_cached; /* ECalComponentId * ~> gchar *revision */
+ GHashTableIter iter;
+ GSList *existing_objects = NULL, *link;
+ ECalCache *cal_cache;
+ gpointer key, value;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (out_created_objects, FALSE);
+ g_return_val_if_fail (out_modified_objects, FALSE);
+ g_return_val_if_fail (out_removed_objects, FALSE);
+
+ *out_created_objects = NULL;
+ *out_modified_objects = NULL;
+ *out_removed_objects = NULL;
+
+ if (!e_backend_get_online (E_BACKEND (meta_backend)))
+ return TRUE;
+
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ g_return_val_if_fail (cal_cache != NULL, FALSE);
+
+ if (!ecmb_connect_wrapper_sync (meta_backend, cancellable, error) ||
+ !e_cal_meta_backend_list_existing_sync (meta_backend, out_new_sync_tag, &existing_objects,
cancellable, error)) {
+ g_object_unref (cal_cache);
+ return FALSE;
+ }
+
+ locally_cached = g_hash_table_new_full (
+ (GHashFunc) e_cal_component_id_hash,
+ (GEqualFunc) e_cal_component_id_equal,
+ (GDestroyNotify) e_cal_component_free_id,
+ g_free);
+
+ g_warn_if_fail (e_cal_cache_search_with_callback (cal_cache, NULL,
+ ecmb_gather_locally_cached_objects_cb, locally_cached, cancellable, error));
+
+ for (link = existing_objects; link; link = g_slist_next (link)) {
+ ECalMetaBackendInfo *nfo = link->data;
+ ECalComponentId id;
+
+ if (!nfo)
+ continue;
+
+ id.uid = nfo->uid;
+ id.rid = NULL;
+
+ if (!g_hash_table_contains (locally_cached, &id)) {
+ link->data = NULL;
+
+ *out_created_objects = g_slist_prepend (*out_created_objects, nfo);
+ } else {
+ const gchar *local_revision = g_hash_table_lookup (locally_cached, &id);
+
+ if (g_strcmp0 (local_revision, nfo->revision) != 0) {
+ link->data = NULL;
+
+ *out_modified_objects = g_slist_prepend (*out_modified_objects, nfo);
+ }
+
+ g_hash_table_remove (locally_cached, &id);
+ }
+ }
+
+ /* What left in the hash table is removed from the remote side */
+ g_hash_table_iter_init (&iter, locally_cached);
+ while (g_hash_table_iter_next (&iter, &key, &value)) {
+ const ECalComponentId *id = key;
+ const gchar *revision = value;
+ ECalMetaBackendInfo *nfo;
+
+ if (!id) {
+ g_warn_if_reached ();
+ continue;
+ }
+
+ /* Skit detached instances, if the master object is still in the cache */
+ if (id->rid && *id->rid) {
+ ECalComponentId master_id;
+
+ master_id.uid = id->uid;
+ master_id.rid = NULL;
+
+ if (!g_hash_table_contains (locally_cached, &master_id))
+ continue;
+ }
+
+ nfo = e_cal_meta_backend_info_new (id->uid, revision, NULL, NULL);
+ *out_removed_objects = g_slist_prepend (*out_removed_objects, nfo);
+ }
+
+ g_slist_free_full (existing_objects, e_cal_meta_backend_info_free);
+ g_hash_table_destroy (locally_cached);
+ g_object_unref (cal_cache);
+
+ *out_created_objects = g_slist_reverse (*out_created_objects);
+ *out_modified_objects = g_slist_reverse (*out_modified_objects);
+ *out_removed_objects = g_slist_reverse (*out_removed_objects);
+
+ return TRUE;
+}
+
+static gboolean
+ecmb_search_sync (ECalMetaBackend *meta_backend,
+ const gchar *expr,
+ GSList **out_icalstrings,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalCache *cal_cache;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (out_icalstrings != NULL, FALSE);
+
+ *out_icalstrings = NULL;
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+
+ g_return_val_if_fail (cal_cache != NULL, FALSE);
+
+ success = e_cal_cache_search (cal_cache, expr, out_icalstrings, cancellable, error);
+
+ if (success) {
+ GSList *link;
+
+ for (link = *out_icalstrings; link; link = g_slist_next (link)) {
+ ECalCacheSearchData *search_data = link->data;
+ gchar *icalstring = NULL;
+
+ if (search_data) {
+ icalstring = g_strdup (search_data->object);
+ e_cal_cache_search_data_free (search_data);
+ }
+
+ link->data = icalstring;
+ }
+ }
+
+ g_object_unref (cal_cache);
+
+ return success;
+}
+
+static gboolean
+ecmb_search_components_sync (ECalMetaBackend *meta_backend,
+ const gchar *expr,
+ GSList **out_components,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalCache *cal_cache;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (out_components != NULL, FALSE);
+
+ *out_components = NULL;
+
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ g_return_val_if_fail (cal_cache != NULL, FALSE);
+
+ success = e_cal_cache_search_components (cal_cache, expr, out_components, cancellable, error);
+
+ g_object_unref (cal_cache);
+
+ return success;
+}
+
+static gboolean
+ecmb_requires_reconnect (ECalMetaBackend *meta_backend)
+{
+ ESource *source;
+ gboolean requires = FALSE;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+
+ source = e_backend_get_source (E_BACKEND (meta_backend));
+ g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ if (e_source_has_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
+ ESourceAuthentication *auth_extension;
+
+ auth_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
+
+ e_source_extension_property_lock (E_SOURCE_EXTENSION (auth_extension));
+
+ requires = meta_backend->priv->authentication_port != e_source_authentication_get_port
(auth_extension) ||
+ g_strcmp0 (meta_backend->priv->authentication_host, e_source_authentication_get_host
(auth_extension)) != 0 ||
+ g_strcmp0 (meta_backend->priv->authentication_user, e_source_authentication_get_user
(auth_extension)) != 0 ||
+ g_strcmp0 (meta_backend->priv->authentication_method,
e_source_authentication_get_method (auth_extension)) != 0 ||
+ g_strcmp0 (meta_backend->priv->authentication_proxy_uid,
e_source_authentication_get_proxy_uid (auth_extension)) != 0 ||
+ g_strcmp0 (meta_backend->priv->authentication_credential_name,
e_source_authentication_get_credential_name (auth_extension)) != 0;
+
+ e_source_extension_property_unlock (E_SOURCE_EXTENSION (auth_extension));
+ }
+
+ if (!requires && e_source_has_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND)) {
+ ESourceWebdav *webdav_extension;
+ SoupURI *soup_uri;
+
+ webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+ soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
+
+ requires = (!meta_backend->priv->webdav_soup_uri && soup_uri) ||
+ (soup_uri && meta_backend->priv->webdav_soup_uri &&
+ !soup_uri_equal (meta_backend->priv->webdav_soup_uri, soup_uri));
+
+ if (soup_uri)
+ soup_uri_free (soup_uri);
+ }
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ return requires;
+}
+
+static void
+ecmb_start_view_thread_func (ECalBackend *cal_backend,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EDataCalView *view = user_data;
+ ECalBackendSExp *sexp;
+ GSList *components = NULL;
+ const gchar *expr = NULL;
+ GError *local_error = NULL;
+
+ g_return_if_fail (E_IS_CAL_META_BACKEND (cal_backend));
+ g_return_if_fail (E_IS_DATA_CAL_VIEW (view));
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error))
+ return;
+
+ /* Fill the view with known (locally stored) components satisfying the expression */
+ sexp = e_data_cal_view_get_sexp (view);
+ if (sexp)
+ expr = e_cal_backend_sexp_text (sexp);
+
+ if (e_cal_meta_backend_search_components_sync (E_CAL_META_BACKEND (cal_backend), expr, &components,
cancellable, &local_error) && components) {
+ if (!g_cancellable_is_cancelled (cancellable))
+ e_data_cal_view_notify_components_added (view, components);
+
+ g_slist_free_full (components, g_object_unref);
+ }
+
+ e_data_cal_view_notify_complete (view, local_error);
+
+ g_clear_error (&local_error);
+}
+
+static gboolean
+ecmb_upload_local_changes_sync (ECalMetaBackend *meta_backend,
+ ECalCache *cal_cache,
+ EConflictResolution conflict_resolution,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GSList *offline_changes, *link;
+ GHashTable *covered_uids;
+ ECache *cache;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (E_IS_CAL_CACHE (cal_cache), FALSE);
+
+ cache = E_CACHE (cal_cache);
+ covered_uids = g_hash_table_new (g_str_hash, g_str_equal);
+
+ offline_changes = e_cal_cache_get_offline_changes (cal_cache, cancellable, error);
+ for (link = offline_changes; link && success; link = g_slist_next (link)) {
+ ECalCacheOfflineChange *change = link->data;
+ gchar *extra = NULL;
+
+ success = !g_cancellable_set_error_if_cancelled (cancellable, error);
+ if (!success)
+ break;
+
+ if (!change || g_hash_table_contains (covered_uids, change->uid))
+ continue;
+
+ g_hash_table_insert (covered_uids, change->uid, NULL);
+
+ if (!e_cal_cache_get_component_extra (cal_cache, change->uid, NULL, &extra, cancellable,
NULL))
+ extra = NULL;
+
+ if (change->state == E_OFFLINE_STATE_LOCALLY_CREATED ||
+ change->state == E_OFFLINE_STATE_LOCALLY_MODIFIED) {
+ GSList *instances = NULL;
+
+ success = e_cal_cache_get_components_by_uid (cal_cache, change->uid, &instances,
cancellable, error);
+ if (success) {
+ success = ecmb_save_component_wrapper_sync (meta_backend, cal_cache,
+ change->state == E_OFFLINE_STATE_LOCALLY_MODIFIED,
+ conflict_resolution, instances, extra, change->uid, NULL, NULL, NULL,
cancellable, error);
+ }
+
+ g_slist_free_full (instances, g_object_unref);
+ } else if (change->state == E_OFFLINE_STATE_LOCALLY_DELETED) {
+ GError *local_error = NULL;
+
+ success = e_cal_meta_backend_remove_component_sync (meta_backend, conflict_resolution,
+ change->uid, extra, change->object, cancellable, &local_error);
+
+ if (!success) {
+ if (g_error_matches (local_error, E_DATA_CAL_ERROR, ObjectNotFound)) {
+ g_clear_error (&local_error);
+ success = TRUE;
+ } else if (local_error) {
+ g_propagate_error (error, local_error);
+ }
+ }
+ } else {
+ g_warn_if_reached ();
+ }
+
+ g_free (extra);
+ }
+
+ g_slist_free_full (offline_changes, e_cal_cache_offline_change_free);
+ g_hash_table_destroy (covered_uids);
+
+ if (success)
+ success = e_cache_clear_offline_changes (cache, cancellable, error);
+
+ return success;
+}
+
+static gboolean
+ecmb_maybe_remove_from_cache (ECalMetaBackend *meta_backend,
+ ECalCache *cal_cache,
+ ECacheOfflineFlag offline_flag,
+ const gchar *uid,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalBackend *cal_backend;
+ GSList *comps = NULL, *link;
+ GError *local_error = NULL;
+
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ if (!e_cal_cache_get_components_by_uid (cal_cache, uid, &comps, cancellable, &local_error)) {
+ if (g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND)) {
+ g_clear_error (&local_error);
+ return TRUE;
+ }
+
+ g_propagate_error (error, local_error);
+ return FALSE;
+ }
+
+ cal_backend = E_CAL_BACKEND (meta_backend);
+
+ for (link = comps; link; link = g_slist_next (link)) {
+ ECalComponent *comp = link->data;
+ ECalComponentId *id;
+
+ g_warn_if_fail (E_IS_CAL_COMPONENT (comp));
+
+ if (!E_IS_CAL_COMPONENT (comp))
+ continue;
+
+ id = e_cal_component_get_id (comp);
+ if (id) {
+ if (!e_cal_cache_delete_attachments (cal_cache, e_cal_component_get_icalcomponent
(comp), cancellable, error) ||
+ !e_cal_cache_remove_component (cal_cache, id->uid, id->rid, offline_flag,
cancellable, error)) {
+ e_cal_component_free_id (id);
+ g_slist_free_full (comps, g_object_unref);
+
+ return FALSE;
+ }
+
+ e_cal_backend_notify_component_removed (cal_backend, id, comp, NULL);
+ e_cal_component_free_id (id);
+ }
+ }
+
+ g_slist_free_full (comps, g_object_unref);
+
+ return TRUE;
+}
+
+static void
+ecmb_refresh_thread_func (ECalBackend *cal_backend,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalMetaBackend *meta_backend;
+ ECalCache *cal_cache;
+ gboolean success, repeat = TRUE, is_repeat = FALSE;
+ GString *invalid_objects = NULL;
+
+ g_return_if_fail (E_IS_CAL_META_BACKEND (cal_backend));
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error))
+ goto done;
+
+ meta_backend = E_CAL_META_BACKEND (cal_backend);
+
+ if (!e_backend_get_online (E_BACKEND (meta_backend)) ||
+ !ecmb_connect_wrapper_sync (meta_backend, cancellable, NULL)) {
+ /* Ignore connection errors here */
+ g_mutex_lock (&meta_backend->priv->property_lock);
+ meta_backend->priv->refresh_after_authenticate = TRUE;
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+ goto done;
+ }
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+ meta_backend->priv->refresh_after_authenticate = FALSE;
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ if (!cal_cache) {
+ g_warn_if_reached ();
+ goto done;
+ }
+
+ success = ecmb_upload_local_changes_sync (meta_backend, cal_cache, E_CONFLICT_RESOLUTION_FAIL,
cancellable, error);
+
+ while (repeat && success &&
+ !g_cancellable_set_error_if_cancelled (cancellable, error)) {
+ GSList *created_objects = NULL, *modified_objects = NULL, *removed_objects = NULL, *link;
+ gchar *last_sync_tag, *new_sync_tag = NULL;
+
+ repeat = FALSE;
+
+ last_sync_tag = e_cache_dup_key (E_CACHE (cal_cache), ECMB_KEY_SYNC_TAG, NULL);
+ if (last_sync_tag && !*last_sync_tag) {
+ g_free (last_sync_tag);
+ last_sync_tag = NULL;
+ }
+
+ success = e_cal_meta_backend_get_changes_sync (meta_backend, last_sync_tag, is_repeat,
&new_sync_tag, &repeat,
+ &created_objects, &modified_objects, &removed_objects, cancellable, error);
+
+ if (success) {
+ GHashTable *covered_uids;
+
+ covered_uids = g_hash_table_new (g_str_hash, g_str_equal);
+
+ /* Removed objects first */
+ for (link = removed_objects; link && success; link = g_slist_next (link)) {
+ ECalMetaBackendInfo *nfo = link->data;
+
+ if (!nfo) {
+ g_warn_if_reached ();
+ continue;
+ }
+
+ success = ecmb_maybe_remove_from_cache (meta_backend, cal_cache,
E_CACHE_IS_ONLINE, nfo->uid, cancellable, error);
+ }
+
+ /* Then modified objects */
+ for (link = modified_objects; link && success; link = g_slist_next (link)) {
+ ECalMetaBackendInfo *nfo = link->data;
+ GError *local_error = NULL;
+
+ if (!nfo || !nfo->uid) {
+ g_warn_if_reached ();
+ continue;
+ }
+
+ if (!*nfo->uid ||
+ g_hash_table_contains (covered_uids, nfo->uid))
+ continue;
+
+ g_hash_table_insert (covered_uids, nfo->uid, NULL);
+
+ success = ecmb_load_component_wrapper_sync (meta_backend, cal_cache,
nfo->uid, nfo->object, nfo->extra, NULL, cancellable, &local_error);
+
+ /* Do not stop on invalid objects, just notify about them later, and load as
many as possible */
+ if (!success && g_error_matches (local_error, E_DATA_CAL_ERROR,
InvalidObject)) {
+ if (!invalid_objects) {
+ invalid_objects = g_string_new (local_error->message);
+ } else {
+ g_string_append_c (invalid_objects, '\n');
+ g_string_append (invalid_objects, local_error->message);
+ }
+ g_clear_error (&local_error);
+ success = TRUE;
+ } else if (local_error) {
+ g_propagate_error (error, local_error);
+ }
+ }
+
+ g_hash_table_remove_all (covered_uids);
+
+ /* Finally created objects */
+ for (link = created_objects; link && success; link = g_slist_next (link)) {
+ ECalMetaBackendInfo *nfo = link->data;
+ GError *local_error = NULL;
+
+ if (!nfo || !nfo->uid) {
+ g_warn_if_reached ();
+ continue;
+ }
+
+ if (!*nfo->uid)
+ continue;
+
+ success = ecmb_load_component_wrapper_sync (meta_backend, cal_cache,
nfo->uid, nfo->object, nfo->extra, NULL, cancellable, &local_error);
+
+ /* Do not stop on invalid objects, just notify about them later, and load as
many as possible */
+ if (!success && g_error_matches (local_error, E_DATA_CAL_ERROR,
InvalidObject)) {
+ if (!invalid_objects) {
+ invalid_objects = g_string_new (local_error->message);
+ } else {
+ g_string_append_c (invalid_objects, '\n');
+ g_string_append (invalid_objects, local_error->message);
+ }
+ g_clear_error (&local_error);
+ success = TRUE;
+ } else if (local_error) {
+ g_propagate_error (error, local_error);
+ }
+ }
+
+ g_hash_table_destroy (covered_uids);
+ }
+
+ if (success && new_sync_tag)
+ e_cache_set_key (E_CACHE (cal_cache), ECMB_KEY_SYNC_TAG, new_sync_tag, NULL);
+
+ g_slist_free_full (created_objects, e_cal_meta_backend_info_free);
+ g_slist_free_full (modified_objects, e_cal_meta_backend_info_free);
+ g_slist_free_full (removed_objects, e_cal_meta_backend_info_free);
+ g_free (last_sync_tag);
+ g_free (new_sync_tag);
+
+ is_repeat = TRUE;
+ }
+
+ g_object_unref (cal_cache);
+
+ done:
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ if (meta_backend->priv->refresh_cancellable == cancellable)
+ g_clear_object (&meta_backend->priv->refresh_cancellable);
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ if (invalid_objects) {
+ e_cal_backend_notify_error (E_CAL_BACKEND (meta_backend), invalid_objects->str);
+
+ g_string_free (invalid_objects, TRUE);
+ }
+
+ g_signal_emit (meta_backend, signals[REFRESH_COMPLETED], 0, NULL);
+}
+
+static void
+ecmb_source_refresh_timeout_cb (ESource *source,
+ gpointer user_data)
+{
+ GWeakRef *weak_ref = user_data;
+ ECalMetaBackend *meta_backend;
+
+ g_return_if_fail (weak_ref != NULL);
+
+ meta_backend = g_weak_ref_get (weak_ref);
+ if (meta_backend) {
+ ecmb_schedule_refresh (meta_backend);
+ g_object_unref (meta_backend);
+ }
+}
+
+static void
+ecmb_source_changed_thread_func (ECalBackend *cal_backend,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalMetaBackend *meta_backend;
+
+ g_return_if_fail (E_IS_CAL_META_BACKEND (cal_backend));
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error))
+ return;
+
+ meta_backend = E_CAL_META_BACKEND (cal_backend);
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+ if (!meta_backend->priv->refresh_timeout_id) {
+ ESource *source = e_backend_get_source (E_BACKEND (meta_backend));
+
+ if (e_source_has_extension (source, E_SOURCE_EXTENSION_REFRESH)) {
+ meta_backend->priv->refresh_timeout_id = e_source_refresh_add_timeout (source, NULL,
+ ecmb_source_refresh_timeout_cb, e_weak_ref_new (meta_backend),
(GDestroyNotify) e_weak_ref_free);
+ }
+ }
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ g_signal_emit (meta_backend, signals[SOURCE_CHANGED], 0, NULL);
+
+ if (e_backend_get_online (E_BACKEND (meta_backend)) &&
+ e_cal_meta_backend_requires_reconnect (meta_backend)) {
+ gboolean can_refresh;
+
+ g_mutex_lock (&meta_backend->priv->connect_lock);
+ can_refresh = e_cal_meta_backend_disconnect_sync (meta_backend, cancellable, error);
+ g_mutex_unlock (&meta_backend->priv->connect_lock);
+
+ if (can_refresh)
+ ecmb_schedule_refresh (meta_backend);
+ }
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ if (meta_backend->priv->source_changed_cancellable == cancellable)
+ g_clear_object (&meta_backend->priv->source_changed_cancellable);
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+}
+
+static void
+ecmb_go_offline_thread_func (ECalBackend *cal_backend,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalMetaBackend *meta_backend;
+
+ g_return_if_fail (E_IS_CAL_META_BACKEND (cal_backend));
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error))
+ return;
+
+ meta_backend = E_CAL_META_BACKEND (cal_backend);
+
+ g_mutex_lock (&meta_backend->priv->connect_lock);
+ e_cal_meta_backend_disconnect_sync (meta_backend, cancellable, error);
+ g_mutex_unlock (&meta_backend->priv->connect_lock);
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ if (meta_backend->priv->go_offline_cancellable == cancellable)
+ g_clear_object (&meta_backend->priv->go_offline_cancellable);
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+}
+
+static ECalComponent *
+ecmb_find_in_instances (const GSList *instances, /* ECalComponent * */
+ const gchar *uid,
+ const gchar *rid)
+{
+ GSList *link;
+
+ for (link = (GSList *) instances; link; link = g_slist_next (link)) {
+ ECalComponent *comp = link->data;
+ ECalComponentId *id;
+
+ if (!comp)
+ continue;
+
+ id = e_cal_component_get_id (comp);
+ if (!id)
+ continue;
+
+ if (g_strcmp0 (id->uid, uid) == 0 &&
+ g_strcmp0 (id->rid, rid) == 0) {
+ e_cal_component_free_id (id);
+ return comp;
+ }
+
+ e_cal_component_free_id (id);
+ }
+
+ return NULL;
+}
+
+static gboolean
+ecmb_put_one_component (ECalMetaBackend *meta_backend,
+ ECalCache *cal_cache,
+ ECacheOfflineFlag offline_flag,
+ ECalComponent *comp,
+ const gchar *extra,
+ GSList **inout_cache_instances,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (comp != NULL, FALSE);
+ g_return_val_if_fail (inout_cache_instances != NULL, FALSE);
+
+ if (e_cal_component_has_attachments (comp)) {
+ success = e_cal_meta_backend_store_inline_attachments_sync (meta_backend,
+ e_cal_component_get_icalcomponent (comp), cancellable, error);
+ e_cal_component_rescan (comp);
+ }
+
+ success = success && e_cal_cache_put_component (cal_cache, comp, extra, offline_flag, cancellable,
error);
+
+ if (success) {
+ ECalComponent *existing = NULL;
+ ECalComponentId *id;
+
+ id = e_cal_component_get_id (comp);
+ if (id) {
+ existing = ecmb_find_in_instances (*inout_cache_instances, id->uid, id->rid);
+
+ e_cal_component_free_id (id);
+ }
+
+ if (existing) {
+ e_cal_backend_notify_component_modified (E_CAL_BACKEND (meta_backend), existing,
comp);
+ *inout_cache_instances = g_slist_remove (*inout_cache_instances, existing);
+
+ g_clear_object (&existing);
+ } else {
+ e_cal_backend_notify_component_created (E_CAL_BACKEND (meta_backend), comp);
+ }
+ }
+
+ return success;
+}
+
+static gboolean
+ecmb_put_instances (ECalMetaBackend *meta_backend,
+ ECalCache *cal_cache,
+ const gchar *uid,
+ ECacheOfflineFlag offline_flag,
+ const GSList *new_instances, /* ECalComponent * */
+ const gchar *extra,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GSList *cache_instances = NULL, *link;
+ gboolean success = TRUE;
+ GError *local_error = NULL;
+
+ if (!e_cal_cache_get_components_by_uid (cal_cache, uid, &cache_instances, cancellable, &local_error))
{
+ if (g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND)) {
+ g_clear_error (&local_error);
+ } else {
+ g_propagate_error (error, local_error);
+
+ return FALSE;
+ }
+ }
+
+ for (link = (GSList *) new_instances; link && success; link = g_slist_next (link)) {
+ ECalComponent *comp = link->data;
+
+ success = ecmb_put_one_component (meta_backend, cal_cache, offline_flag, comp, extra,
&cache_instances, cancellable, error);
+ }
+
+ /* What left got removed from the remote side, notify about it */
+ if (success && cache_instances) {
+ ECalBackend *cal_backend = E_CAL_BACKEND (meta_backend);
+ GSList *link;
+
+ for (link = cache_instances; link && success; link = g_slist_next (link)) {
+ ECalComponent *comp = link->data;
+ ECalComponentId *id;
+
+ id = e_cal_component_get_id (comp);
+ if (!id)
+ continue;
+
+ success = e_cal_cache_delete_attachments (cal_cache,
e_cal_component_get_icalcomponent (comp), cancellable, error);
+ if (!success)
+ break;
+
+ success = e_cal_cache_remove_component (cal_cache, id->uid, id->rid, offline_flag,
cancellable, error);
+
+ e_cal_backend_notify_component_removed (cal_backend, id, comp, NULL);
+
+ e_cal_component_free_id (id);
+ }
+ }
+
+ g_slist_free_full (cache_instances, g_object_unref);
+
+ return success;
+}
+
+static void
+ecmb_gather_timezones (ECalMetaBackend *meta_backend,
+ ETimezoneCache *timezone_cache,
+ icalcomponent *icalcomp)
+{
+ icalcomponent *subcomp;
+ icaltimezone *zone;
+
+ g_return_if_fail (E_IS_CAL_META_BACKEND (meta_backend));
+ g_return_if_fail (E_IS_TIMEZONE_CACHE (timezone_cache));
+ g_return_if_fail (icalcomp != NULL);
+
+ zone = icaltimezone_new ();
+
+ for (subcomp = icalcomponent_get_first_component (icalcomp, ICAL_VTIMEZONE_COMPONENT);
+ subcomp;
+ subcomp = icalcomponent_get_next_component (icalcomp, ICAL_VTIMEZONE_COMPONENT)) {
+ icalcomponent *clone;
+
+ clone = icalcomponent_new_clone (subcomp);
+
+ if (icaltimezone_set_component (zone, clone)) {
+ e_timezone_cache_add_timezone (timezone_cache, zone);
+ } else {
+ icalcomponent_free (clone);
+ }
+ }
+
+ icaltimezone_free (zone, TRUE);
+}
+
+static gboolean
+ecmb_load_component_wrapper_sync (ECalMetaBackend *meta_backend,
+ ECalCache *cal_cache,
+ const gchar *uid,
+ const gchar *preloaded_object,
+ const gchar *preloaded_extra,
+ gchar **out_new_uid,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECacheOfflineFlag offline_flag = E_CACHE_IS_ONLINE;
+ icalcomponent *icalcomp = NULL;
+ GSList *new_instances = NULL;
+ gchar *extra = NULL;
+ const gchar *loaded_uid = NULL;
+ gboolean success = TRUE;
+
+ if (preloaded_object && *preloaded_object) {
+ icalcomp = icalcomponent_new_from_string (preloaded_object);
+ if (!icalcomp) {
+ g_propagate_error (error, e_data_cal_create_error_fmt (InvalidObject, _("Preloaded
object for UID “%s” is invalid"), uid));
+ return FALSE;
+ }
+ } else if (!e_cal_meta_backend_load_component_sync (meta_backend, uid, preloaded_extra, &icalcomp,
&extra, cancellable, error)) {
+ g_free (extra);
+ return FALSE;
+ } else if (!icalcomp) {
+ g_propagate_error (error, e_data_cal_create_error_fmt (InvalidObject, _("Received object for
UID “%s” is invalid"), uid));
+ g_free (extra);
+ return FALSE;
+ }
+
+ if (icalcomponent_isa (icalcomp) == ICAL_VCALENDAR_COMPONENT) {
+ icalcomponent_kind kind;
+ icalcomponent *subcomp;
+
+ ecmb_gather_timezones (meta_backend, E_TIMEZONE_CACHE (cal_cache), icalcomp);
+
+ kind = e_cal_backend_get_kind (E_CAL_BACKEND (meta_backend));
+
+ for (subcomp = icalcomponent_get_first_component (icalcomp, kind);
+ subcomp && success;
+ subcomp = icalcomponent_get_next_component (icalcomp, kind)) {
+ ECalComponent *comp = e_cal_component_new_from_icalcomponent (icalcomponent_new_clone
(subcomp));
+
+ if (comp) {
+ new_instances = g_slist_prepend (new_instances, comp);
+
+ if (!loaded_uid)
+ loaded_uid = icalcomponent_get_uid (e_cal_component_get_icalcomponent
(comp));
+ }
+ }
+ } else {
+ ECalComponent *comp = e_cal_component_new_from_icalcomponent (icalcomp);
+
+ icalcomp = NULL;
+
+ if (comp) {
+ new_instances = g_slist_prepend (new_instances, comp);
+
+ if (!loaded_uid)
+ loaded_uid = icalcomponent_get_uid (e_cal_component_get_icalcomponent (comp));
+ }
+ }
+
+ if (new_instances) {
+ new_instances = g_slist_reverse (new_instances);
+
+ success = ecmb_put_instances (meta_backend, cal_cache, loaded_uid ? loaded_uid : uid,
offline_flag,
+ new_instances, extra ? extra : preloaded_extra, cancellable, error);
+
+ if (success && out_new_uid)
+ *out_new_uid = g_strdup (loaded_uid ? loaded_uid : uid);
+ } else {
+ g_propagate_error (error, e_data_cal_create_error_fmt (InvalidObject, _("Received object for
UID “%s” doesn't contain any expected component"), uid));
+ success = FALSE;
+ }
+
+ g_slist_free_full (new_instances, g_object_unref);
+ if (icalcomp)
+ icalcomponent_free (icalcomp);
+ g_free (extra);
+
+ return success;
+}
+
+static gboolean
+ecmb_save_component_wrapper_sync (ECalMetaBackend *meta_backend,
+ ECalCache *cal_cache,
+ gboolean overwrite_existing,
+ EConflictResolution conflict_resolution,
+ const GSList *in_instances,
+ const gchar *extra,
+ const gchar *orig_uid,
+ gboolean *out_requires_put,
+ gchar **out_new_uid,
+ gchar **out_new_extra,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GSList *link, *instances = NULL;
+ gchar *new_uid = NULL, *new_extra = NULL;
+ gboolean has_attachments = FALSE, success = TRUE;
+
+ if (out_requires_put)
+ *out_requires_put = TRUE;
+
+ if (out_new_uid)
+ *out_new_uid = NULL;
+
+ for (link = (GSList *) in_instances; link && !has_attachments; link = g_slist_next (link)) {
+ has_attachments = e_cal_component_has_attachments (link->data);
+ }
+
+ if (has_attachments) {
+ instances = g_slist_copy ((GSList *) in_instances);
+
+ for (link = instances; link; link = g_slist_next (link)) {
+ ECalComponent *comp = link->data;
+
+ if (success && e_cal_component_has_attachments (comp)) {
+ comp = e_cal_component_clone (comp);
+ link->data = comp;
+
+ success = e_cal_meta_backend_inline_local_attachments_sync (meta_backend,
+ e_cal_component_get_icalcomponent (comp), cancellable, error);
+ e_cal_component_rescan (comp);
+ } else {
+ g_object_ref (comp);
+ }
+ }
+ }
+
+ success = success && e_cal_meta_backend_save_component_sync (meta_backend, overwrite_existing,
conflict_resolution,
+ instances ? instances : in_instances, extra, &new_uid, &new_extra, cancellable, error);
+
+ if (success && new_uid && *new_uid) {
+ gchar *loaded_uid = NULL;
+
+ success = ecmb_load_component_wrapper_sync (meta_backend, cal_cache, new_uid, NULL,
+ new_extra ? new_extra : extra, &loaded_uid, cancellable, error);
+
+ if (success && g_strcmp0 (loaded_uid, orig_uid) != 0)
+ success = ecmb_maybe_remove_from_cache (meta_backend, cal_cache, E_CACHE_IS_ONLINE,
orig_uid, cancellable, error);
+
+ if (success && out_new_uid)
+ *out_new_uid = loaded_uid;
+ else
+ g_free (loaded_uid);
+
+ if (out_requires_put)
+ *out_requires_put = FALSE;
+ }
+
+ g_free (new_uid);
+
+ if (success && out_new_extra)
+ *out_new_extra = new_extra;
+ else
+ g_free (new_extra);
+
+ g_slist_free_full (instances, g_object_unref);
+
+ return success;
+}
+
+static void
+ecmb_open_sync (ECalBackendSync *sync_backend,
+ EDataCal *cal,
+ GCancellable *cancellable,
+ gboolean only_if_exists,
+ GError **error)
+{
+ ECalMetaBackend *meta_backend;
+ ESource *source;
+
+ g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+
+ if (e_cal_backend_is_opened (E_CAL_BACKEND (sync_backend)))
+ return;
+
+ meta_backend = E_CAL_META_BACKEND (sync_backend);
+ if (meta_backend->priv->create_cache_error) {
+ g_propagate_error (error, meta_backend->priv->create_cache_error);
+ meta_backend->priv->create_cache_error = NULL;
+ return;
+ }
+
+ source = e_backend_get_source (E_BACKEND (sync_backend));
+
+ if (!meta_backend->priv->source_changed_id) {
+ meta_backend->priv->source_changed_id = g_signal_connect_swapped (source, "changed",
+ G_CALLBACK (ecmb_schedule_source_changed), meta_backend);
+ }
+
+ if (e_source_has_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND)) {
+ ESourceWebdav *webdav_extension;
+
+ webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+ e_source_webdav_unset_temporary_ssl_trust (webdav_extension);
+ }
+
+ if (e_cal_meta_backend_get_ever_connected (meta_backend)) {
+ e_cal_backend_set_writable (E_CAL_BACKEND (meta_backend),
+ e_cal_meta_backend_get_connected_writable (meta_backend));
+ } else {
+ if (!ecmb_connect_wrapper_sync (meta_backend, cancellable, error)) {
+ g_mutex_lock (&meta_backend->priv->property_lock);
+ meta_backend->priv->refresh_after_authenticate = TRUE;
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ return;
+ }
+ }
+
+ ecmb_schedule_refresh (E_CAL_META_BACKEND (sync_backend));
+}
+
+static void
+ecmb_refresh_sync (ECalBackendSync *sync_backend,
+ EDataCal *cal,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalMetaBackend *meta_backend;
+
+ g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+
+ meta_backend = E_CAL_META_BACKEND (sync_backend);
+
+ if (!e_backend_get_online (E_BACKEND (sync_backend)))
+ return;
+
+ if (ecmb_connect_wrapper_sync (meta_backend, cancellable, error))
+ ecmb_schedule_refresh (meta_backend);
+}
+
+static void
+ecmb_get_object_sync (ECalBackendSync *sync_backend,
+ EDataCal *cal,
+ GCancellable *cancellable,
+ const gchar *uid,
+ const gchar *rid,
+ gchar **calobj,
+ GError **error)
+{
+ ECalMetaBackend *meta_backend;
+ ECalCache *cal_cache;
+ GError *local_error = NULL;
+
+ g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+ g_return_if_fail (uid && *uid);
+ g_return_if_fail (calobj != NULL);
+
+ meta_backend = E_CAL_META_BACKEND (sync_backend);
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+
+ g_return_if_fail (cal_cache != NULL);
+
+ if (!e_cal_cache_get_component_as_string (cal_cache, uid, rid, calobj, cancellable, &local_error) &&
+ g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND)) {
+ gchar *loaded_uid = NULL;
+ gboolean found = FALSE;
+
+ g_clear_error (&local_error);
+
+ /* Ignore errors here, just try whether it's on the remote side, but not in the local cache */
+ if (e_backend_get_online (E_BACKEND (meta_backend)) &&
+ ecmb_connect_wrapper_sync (meta_backend, cancellable, NULL) &&
+ ecmb_load_component_wrapper_sync (meta_backend, cal_cache, uid, NULL, NULL, &loaded_uid,
cancellable, NULL)) {
+ found = e_cal_cache_get_component_as_string (cal_cache, loaded_uid, rid, calobj,
cancellable, NULL);
+ }
+
+ if (!found)
+ g_propagate_error (error, e_data_cal_create_error (ObjectNotFound, NULL));
+
+ g_free (loaded_uid);
+ } else if (local_error) {
+ g_propagate_error (error, e_data_cal_create_error (OtherError, local_error->message));
+ g_clear_error (&local_error);
+ }
+
+ g_object_unref (cal_cache);
+}
+
+static void
+ecmb_get_object_list_sync (ECalBackendSync *sync_backend,
+ EDataCal *cal,
+ GCancellable *cancellable,
+ const gchar *sexp,
+ GSList **calobjs,
+ GError **error)
+{
+ g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+ g_return_if_fail (calobjs != NULL);
+
+ *calobjs = NULL;
+
+ e_cal_meta_backend_search_sync (E_CAL_META_BACKEND (sync_backend), sexp, calobjs, cancellable, error);
+}
+
+static gboolean
+ecmb_add_free_busy_instance_cb (icalcomponent *icalcomp,
+ struct icaltimetype instance_start,
+ struct icaltimetype instance_end,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ icalcomponent *vfreebusy = user_data;
+ icalproperty *prop, *classification;
+ icalparameter *param;
+ struct icalperiodtype ipt;
+
+ ipt.start = instance_start;
+ ipt.end = instance_end;
+ ipt.duration = icaldurationtype_null_duration ();
+
+ /* Add busy information to the VFREEBUSY component */
+ prop = icalproperty_new (ICAL_FREEBUSY_PROPERTY);
+ icalproperty_set_freebusy (prop, ipt);
+
+ param = icalparameter_new_fbtype (ICAL_FBTYPE_BUSY);
+ icalproperty_add_parameter (prop, param);
+
+ classification = icalcomponent_get_first_property (icalcomp, ICAL_CLASS_PROPERTY);
+ if (!classification || icalproperty_get_class (classification) == ICAL_CLASS_PUBLIC) {
+ const gchar *str;
+
+ str = icalcomponent_get_summary (icalcomp);
+ if (str && *str) {
+ param = icalparameter_new_x (str);
+ icalparameter_set_xname (param, "X-SUMMARY");
+ icalproperty_add_parameter (prop, param);
+ }
+
+ str = icalcomponent_get_location (icalcomp);
+ if (str && *str) {
+ param = icalparameter_new_x (str);
+ icalparameter_set_xname (param, "X-LOCATION");
+ icalproperty_add_parameter (prop, param);
+ }
+ }
+
+ icalcomponent_add_property (vfreebusy, prop);
+
+ return TRUE;
+}
+
+static void
+ecmb_get_free_busy_sync (ECalBackendSync *sync_backend,
+ EDataCal *cal,
+ GCancellable *cancellable,
+ const GSList *users,
+ time_t start,
+ time_t end,
+ GSList **out_freebusy,
+ GError **error)
+{
+ ECalMetaBackend *meta_backend;
+ ECalCache *cal_cache;
+ GSList *link, *components = NULL;
+ gchar *cal_email_address, *mailto;
+ icalcomponent *vfreebusy, *icalcomp;
+ icalproperty *prop;
+ icaltimezone *utc_zone;
+
+ g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+ g_return_if_fail (out_freebusy != NULL);
+
+ meta_backend = E_CAL_META_BACKEND (sync_backend);
+
+ *out_freebusy = NULL;
+
+ if (!users)
+ return;
+
+ cal_email_address = e_cal_backend_get_backend_property (E_CAL_BACKEND (meta_backend),
CAL_BACKEND_PROPERTY_CAL_EMAIL_ADDRESS);
+ if (!cal_email_address)
+ return;
+
+ for (link = (GSList *) users; link; link = g_slist_next (link)) {
+ const gchar *user = link->data;
+
+ if (user && g_ascii_strcasecmp (user, cal_email_address) == 0)
+ break;
+ }
+
+ if (!link) {
+ g_free (cal_email_address);
+ return;
+ }
+
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ if (!cal_cache) {
+ g_warn_if_reached ();
+ g_free (cal_email_address);
+ return;
+ }
+
+ if (!e_cal_cache_get_components_in_range (cal_cache, start, end, &components, cancellable, error)) {
+ g_clear_object (&cal_cache);
+ g_free (cal_email_address);
+ return;
+ }
+
+ vfreebusy = icalcomponent_new_vfreebusy ();
+
+ mailto = g_strconcat ("mailto:", cal_email_address, NULL);
+ prop = icalproperty_new_organizer (mailto);
+ g_free (mailto);
+
+ if (prop)
+ icalcomponent_add_property (vfreebusy, prop);
+
+ utc_zone = icaltimezone_get_utc_timezone ();
+ icalcomponent_set_dtstart (vfreebusy, icaltime_from_timet_with_zone (start, FALSE, utc_zone));
+ icalcomponent_set_dtend (vfreebusy, icaltime_from_timet_with_zone (end, FALSE, utc_zone));
+
+ for (link = components; link; link = g_slist_next (link)) {
+ ECalComponent *comp = link->data;
+
+ if (!E_IS_CAL_COMPONENT (comp)) {
+ g_warn_if_reached ();
+ continue;
+ }
+
+ icalcomp = e_cal_component_get_icalcomponent (comp);
+ if (!icalcomp)
+ continue;
+
+ /* If the event is TRANSPARENT, skip it. */
+ prop = icalcomponent_get_first_property (icalcomp, ICAL_TRANSP_PROPERTY);
+ if (prop) {
+ icalproperty_transp transp_val = icalproperty_get_transp (prop);
+ if (transp_val == ICAL_TRANSP_TRANSPARENT ||
+ transp_val == ICAL_TRANSP_TRANSPARENTNOCONFLICT)
+ continue;
+ }
+
+ if (!e_cal_recur_generate_instances_sync (icalcomp,
+ icaltime_from_timet_with_zone (start, FALSE, NULL),
+ icaltime_from_timet_with_zone (end, FALSE, NULL),
+ ecmb_add_free_busy_instance_cb, vfreebusy,
+ e_cal_cache_resolve_timezone_cb, cal_cache,
+ utc_zone, cancellable, error)) {
+ break;
+ }
+ }
+
+ *out_freebusy = g_slist_prepend (*out_freebusy, icalcomponent_as_ical_string_r (vfreebusy));
+
+ g_slist_free_full (components, g_object_unref);
+ icalcomponent_free (vfreebusy);
+ g_object_unref (cal_cache);
+ g_free (cal_email_address);
+}
+
+static gboolean
+ecmb_create_object_sync (ECalMetaBackend *meta_backend,
+ ECalCache *cal_cache,
+ ECacheOfflineFlag *offline_flag,
+ EConflictResolution conflict_resolution,
+ ECalComponent *comp,
+ gchar **out_new_uid,
+ ECalComponent **out_new_comp,
+ GCancellable *cancellable,
+ GError **error)
+{
+ icalcomponent *icalcomp;
+ struct icaltimetype itt;
+ const gchar *uid;
+ gchar *new_uid = NULL, *new_extra = NULL;
+ gboolean success, requires_put = TRUE;
+
+ g_return_val_if_fail (comp != NULL, FALSE);
+
+ icalcomp = e_cal_component_get_icalcomponent (comp);
+ if (!icalcomp) {
+ g_propagate_error (error, e_data_cal_create_error (InvalidObject, NULL));
+ return FALSE;
+ }
+
+ uid = icalcomponent_get_uid (icalcomp);
+ if (!uid) {
+ gchar *new_uid;
+
+ new_uid = e_cal_component_gen_uid ();
+ if (!new_uid) {
+ g_propagate_error (error, e_data_cal_create_error (InvalidObject, NULL));
+ return FALSE;
+ }
+
+ icalcomponent_set_uid (icalcomp, new_uid);
+ uid = icalcomponent_get_uid (icalcomp);
+
+ g_free (new_uid);
+ }
+
+ if (e_cal_cache_contains (cal_cache, uid, NULL, E_CACHE_EXCLUDE_DELETED)) {
+ g_propagate_error (error, e_data_cal_create_error (ObjectIdAlreadyExists, NULL));
+ return FALSE;
+ }
+
+ /* Set the created and last modified times on the component */
+ itt = icaltime_current_time_with_zone (icaltimezone_get_utc_timezone ());
+ e_cal_component_set_created (comp, &itt);
+ e_cal_component_set_last_modified (comp, &itt);
+
+ if (*offline_flag == E_CACHE_OFFLINE_UNKNOWN) {
+ if (e_backend_get_online (E_BACKEND (meta_backend)) &&
+ ecmb_connect_wrapper_sync (meta_backend, cancellable, NULL)) {
+ *offline_flag = E_CACHE_IS_ONLINE;
+ } else {
+ *offline_flag = E_CACHE_IS_OFFLINE;
+ }
+ }
+
+ if (*offline_flag == E_CACHE_IS_ONLINE) {
+ GSList *instances;
+
+ instances = g_slist_prepend (NULL, comp);
+
+ if (!ecmb_save_component_wrapper_sync (meta_backend, cal_cache, FALSE, conflict_resolution,
instances, NULL, uid,
+ &requires_put, &new_uid, &new_extra, cancellable, error)) {
+ g_slist_free (instances);
+ return FALSE;
+ }
+
+ g_slist_free (instances);
+ }
+
+ if (requires_put) {
+ success = e_cal_cache_put_component (cal_cache, comp, new_extra, *offline_flag, cancellable,
error);
+ if (success && !out_new_comp) {
+ e_cal_backend_notify_component_created (E_CAL_BACKEND (meta_backend), comp);
+ }
+ } else {
+ success = TRUE;
+ }
+
+ if (success) {
+ if (out_new_uid)
+ *out_new_uid = g_strdup (new_uid ? new_uid : icalcomponent_get_uid
(e_cal_component_get_icalcomponent (comp)));
+ if (out_new_comp) {
+ if (new_uid) {
+ if (!e_cal_cache_get_component (cal_cache, new_uid, NULL, out_new_comp,
cancellable, NULL))
+ *out_new_comp = g_object_ref (comp);
+ } else {
+ *out_new_comp = g_object_ref (comp);
+ }
+ }
+ }
+
+ g_free (new_uid);
+ g_free (new_extra);
+
+ return success;
+}
+
+static void
+ecmb_create_objects_sync (ECalBackendSync *sync_backend,
+ EDataCal *cal,
+ GCancellable *cancellable,
+ const GSList *calobjs,
+ GSList **out_uids,
+ GSList **out_new_components,
+ GError **error)
+{
+ ECalMetaBackend *meta_backend;
+ ECalCache *cal_cache;
+ ECacheOfflineFlag offline_flag = E_CACHE_OFFLINE_UNKNOWN;
+ EConflictResolution conflict_resolution = E_CONFLICT_RESOLUTION_FAIL;
+ icalcomponent_kind backend_kind;
+ GSList *link;
+ gboolean success = TRUE;
+
+ g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+ g_return_if_fail (calobjs != NULL);
+ g_return_if_fail (out_uids != NULL);
+ g_return_if_fail (out_new_components != NULL);
+
+ if (!e_cal_backend_get_writable (E_CAL_BACKEND (sync_backend))) {
+ g_propagate_error (error, e_data_cal_create_error (PermissionDenied, NULL));
+ return;
+ }
+
+ meta_backend = E_CAL_META_BACKEND (sync_backend);
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ g_return_if_fail (cal_cache != NULL);
+
+ backend_kind = e_cal_backend_get_kind (E_CAL_BACKEND (meta_backend));
+
+ for (link = (GSList *) calobjs; link && success; link = g_slist_next (link)) {
+ ECalComponent *comp, *new_comp = NULL;
+ gchar *new_uid = NULL;
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error))
+ break;
+
+ comp = e_cal_component_new_from_string (link->data);
+ if (!comp ||
+ !e_cal_component_get_icalcomponent (comp) ||
+ backend_kind != icalcomponent_isa (e_cal_component_get_icalcomponent (comp))) {
+ g_clear_object (&comp);
+
+ g_propagate_error (error, e_data_cal_create_error (InvalidObject, NULL));
+ break;
+ }
+
+ success = ecmb_create_object_sync (meta_backend, cal_cache, &offline_flag,
conflict_resolution,
+ comp, &new_uid, &new_comp, cancellable, error);
+
+ if (success) {
+ *out_uids = g_slist_prepend (*out_uids, new_uid);
+ *out_new_components = g_slist_prepend (*out_new_components, new_comp);
+ }
+
+ g_object_unref (comp);
+ }
+
+ *out_uids = g_slist_reverse (*out_uids);
+ *out_new_components = g_slist_reverse (*out_new_components);
+
+ g_object_unref (cal_cache);
+}
+
+static gboolean
+ecmb_modify_object_sync (ECalMetaBackend *meta_backend,
+ ECalCache *cal_cache,
+ ECacheOfflineFlag *offline_flag,
+ EConflictResolution conflict_resolution,
+ ECalObjModType mod,
+ ECalComponent *comp,
+ ECalComponent **out_old_comp,
+ ECalComponent **out_new_comp,
+ GCancellable *cancellable,
+ GError **error)
+{
+ struct icaltimetype itt;
+ ECalComponentId *id;
+ ECalComponent *old_comp = NULL, *new_comp = NULL, *master_comp, *existing_comp = NULL;
+ GSList *instances = NULL;
+ gchar *extra = NULL, *new_uid = NULL, *new_extra = NULL;
+ gboolean success = TRUE, requires_put = TRUE;
+ GError *local_error = NULL;
+
+ g_return_val_if_fail (comp != NULL, FALSE);
+
+ id = e_cal_component_get_id (comp);
+ if (!id) {
+ g_propagate_error (error, e_data_cal_create_error (InvalidObject, NULL));
+ return FALSE;
+ }
+
+ if (!e_cal_cache_get_components_by_uid (cal_cache, id->uid, &instances, cancellable, &local_error)) {
+ if (g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND)) {
+ g_clear_error (&local_error);
+ local_error = e_data_cal_create_error (ObjectNotFound, NULL);
+ }
+
+ g_propagate_error (error, local_error);
+ e_cal_component_free_id (id);
+
+ return FALSE;
+ }
+
+ master_comp = ecmb_find_in_instances (instances, id->uid, NULL);
+ if (e_cal_component_is_instance (comp)) {
+ /* Set detached instance as the old object */
+ existing_comp = ecmb_find_in_instances (instances, id->uid, id->rid);
+
+ if (!existing_comp && mod == E_CAL_OBJ_MOD_ONLY_THIS) {
+ g_propagate_error (error, e_data_cal_create_error (ObjectNotFound, NULL));
+
+ g_slist_free_full (instances, g_object_unref);
+ e_cal_component_free_id (id);
+
+ return FALSE;
+ }
+ }
+
+ if (!existing_comp)
+ existing_comp = master_comp;
+
+ if (!e_cal_cache_get_component_extra (cal_cache, id->uid, id->rid, &extra, cancellable, NULL) &&
id->rid) {
+ if (!e_cal_cache_get_component_extra (cal_cache, id->uid, NULL, &extra, cancellable, NULL))
+ extra = NULL;
+ }
+
+ /* Set the last modified time on the component */
+ itt = icaltime_current_time_with_zone (icaltimezone_get_utc_timezone ());
+ e_cal_component_set_last_modified (comp, &itt);
+
+ /* Remember old and new components */
+ if (out_old_comp && existing_comp)
+ old_comp = e_cal_component_clone (existing_comp);
+
+ if (out_new_comp)
+ new_comp = e_cal_component_clone (comp);
+
+ switch (mod) {
+ case E_CAL_OBJ_MOD_ONLY_THIS:
+ case E_CAL_OBJ_MOD_THIS:
+ if (e_cal_component_is_instance (comp)) {
+ if (existing_comp != master_comp) {
+ instances = g_slist_remove (instances, existing_comp);
+ g_clear_object (&existing_comp);
+ }
+ } else {
+ instances = g_slist_remove (instances, master_comp);
+ g_clear_object (&master_comp);
+ existing_comp = NULL;
+ }
+
+ instances = g_slist_append (instances, e_cal_component_clone (comp));
+ break;
+ case E_CAL_OBJ_MOD_ALL:
+ e_cal_recur_ensure_end_dates (comp, TRUE, e_cal_cache_resolve_timezone_simple_cb, cal_cache);
+
+ /* Replace master object */
+ instances = g_slist_remove (instances, master_comp);
+ g_clear_object (&master_comp);
+ existing_comp = NULL;
+
+ instances = g_slist_prepend (instances, e_cal_component_clone (comp));
+ break;
+ case E_CAL_OBJ_MOD_THIS_AND_PRIOR:
+ case E_CAL_OBJ_MOD_THIS_AND_FUTURE:
+ if (e_cal_component_is_instance (comp) && master_comp) {
+ struct icaltimetype rid, master_dtstart;
+ icalcomponent *icalcomp = e_cal_component_get_icalcomponent (comp);
+ icalcomponent *split_icalcomp;
+ icalproperty *prop;
+
+ rid = icalcomponent_get_recurrenceid (icalcomp);
+
+ if (mod == E_CAL_OBJ_MOD_THIS_AND_FUTURE &&
+ e_cal_util_is_first_instance (master_comp, icalcomponent_get_recurrenceid
(icalcomp),
+ e_cal_cache_resolve_timezone_simple_cb, cal_cache)) {
+ icalproperty *prop = icalcomponent_get_first_property (icalcomp,
ICAL_RECURRENCEID_PROPERTY);
+
+ if (prop)
+ icalcomponent_remove_property (icalcomp, prop);
+
+ e_cal_component_rescan (comp);
+
+ /* Then do it like for "mod_all" */
+ e_cal_recur_ensure_end_dates (comp, TRUE,
e_cal_cache_resolve_timezone_simple_cb, cal_cache);
+
+ /* Replace master */
+ instances = g_slist_remove (instances, master_comp);
+ g_clear_object (&master_comp);
+ existing_comp = NULL;
+
+ instances = g_slist_prepend (instances, e_cal_component_clone (comp));
+
+ if (out_new_comp) {
+ g_clear_object (&new_comp);
+ new_comp = e_cal_component_clone (comp);
+ }
+ break;
+ }
+
+ prop = icalcomponent_get_first_property (icalcomp, ICAL_RECURRENCEID_PROPERTY);
+ if (prop)
+ icalcomponent_remove_property (icalcomp, prop);
+ e_cal_component_rescan (comp);
+
+ master_dtstart = icalcomponent_get_dtstart (e_cal_component_get_icalcomponent
(master_comp));
+ split_icalcomp = e_cal_util_split_at_instance (icalcomp, rid, master_dtstart);
+ if (split_icalcomp) {
+ rid = icaltime_convert_to_zone (rid, icaltimezone_get_utc_timezone ());
+ e_cal_util_remove_instances (e_cal_component_get_icalcomponent (master_comp),
rid, mod);
+ e_cal_component_rescan (master_comp);
+ e_cal_recur_ensure_end_dates (master_comp, TRUE,
e_cal_cache_resolve_timezone_simple_cb, cal_cache);
+
+ if (out_new_comp) {
+ g_clear_object (&new_comp);
+ new_comp = e_cal_component_clone (master_comp);
+ }
+ }
+
+ if (split_icalcomp) {
+ gchar *new_uid;
+
+ new_uid = e_cal_component_gen_uid ();
+ icalcomponent_set_uid (split_icalcomp, new_uid);
+ g_free (new_uid);
+
+ g_warn_if_fail (e_cal_component_set_icalcomponent (comp, split_icalcomp));
+
+ e_cal_recur_ensure_end_dates (comp, TRUE,
e_cal_cache_resolve_timezone_simple_cb, cal_cache);
+
+ success = ecmb_create_object_sync (meta_backend, cal_cache, offline_flag,
E_CONFLICT_RESOLUTION_FAIL,
+ comp, NULL, NULL, cancellable, error);
+ }
+ } else {
+ /* Replace master */
+ instances = g_slist_remove (instances, master_comp);
+ g_clear_object (&master_comp);
+ existing_comp = NULL;
+
+ instances = g_slist_prepend (instances, e_cal_component_clone (comp));
+ }
+ break;
+ }
+
+ if (success && *offline_flag == E_CACHE_OFFLINE_UNKNOWN) {
+ if (e_backend_get_online (E_BACKEND (meta_backend)) &&
+ ecmb_connect_wrapper_sync (meta_backend, cancellable, NULL)) {
+ *offline_flag = E_CACHE_IS_ONLINE;
+ } else {
+ *offline_flag = E_CACHE_IS_OFFLINE;
+ }
+ }
+
+ if (success && *offline_flag == E_CACHE_IS_ONLINE) {
+ success = ecmb_save_component_wrapper_sync (meta_backend, cal_cache, TRUE,
conflict_resolution,
+ instances, extra, id->uid, &requires_put, &new_uid, &new_extra, cancellable, error);
+ }
+
+ if (success && requires_put)
+ success = ecmb_put_instances (meta_backend, cal_cache, id->uid, *offline_flag, instances,
new_extra ? new_extra : extra, cancellable, error);
+
+ if (!success) {
+ g_clear_object (&old_comp);
+ g_clear_object (&new_comp);
+ }
+
+ if (out_old_comp)
+ *out_old_comp = old_comp;
+ if (out_new_comp) {
+ if (new_uid) {
+ if (!e_cal_cache_get_component (cal_cache, new_uid, id->rid, out_new_comp,
cancellable, NULL))
+ *out_new_comp = NULL;
+ } else {
+ *out_new_comp = new_comp ? g_object_ref (new_comp) : NULL;
+ }
+ }
+
+ g_slist_free_full (instances, g_object_unref);
+ e_cal_component_free_id (id);
+ g_clear_object (&new_comp);
+ g_free (new_extra);
+ g_free (new_uid);
+ g_free (extra);
+
+ return success;
+}
+
+static void
+ecmb_modify_objects_sync (ECalBackendSync *sync_backend,
+ EDataCal *cal,
+ GCancellable *cancellable,
+ const GSList *calobjs,
+ ECalObjModType mod,
+ GSList **out_old_components,
+ GSList **out_new_components,
+ GError **error)
+{
+ ECalMetaBackend *meta_backend;
+ ECalCache *cal_cache;
+ ECacheOfflineFlag offline_flag = E_CACHE_OFFLINE_UNKNOWN;
+ EConflictResolution conflict_resolution = E_CONFLICT_RESOLUTION_FAIL;
+ icalcomponent_kind backend_kind;
+ GSList *link;
+ gboolean success = TRUE;
+
+ g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+ g_return_if_fail (calobjs != NULL);
+ g_return_if_fail (out_old_components != NULL);
+ g_return_if_fail (out_new_components != NULL);
+
+ if (!e_cal_backend_get_writable (E_CAL_BACKEND (sync_backend))) {
+ g_propagate_error (error, e_data_cal_create_error (PermissionDenied, NULL));
+ return;
+ }
+
+ meta_backend = E_CAL_META_BACKEND (sync_backend);
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ g_return_if_fail (cal_cache != NULL);
+
+ backend_kind = e_cal_backend_get_kind (E_CAL_BACKEND (meta_backend));
+
+ for (link = (GSList *) calobjs; link && success; link = g_slist_next (link)) {
+ ECalComponent *comp, *old_comp = NULL, *new_comp = NULL;
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error))
+ break;
+
+ comp = e_cal_component_new_from_string (link->data);
+ if (!comp ||
+ !e_cal_component_get_icalcomponent (comp) ||
+ backend_kind != icalcomponent_isa (e_cal_component_get_icalcomponent (comp))) {
+ g_propagate_error (error, e_data_cal_create_error (InvalidObject, NULL));
+ break;
+ }
+
+ success = ecmb_modify_object_sync (meta_backend, cal_cache, &offline_flag,
conflict_resolution,
+ mod, comp, &old_comp, &new_comp, cancellable, error);
+
+ if (success) {
+ *out_old_components = g_slist_prepend (*out_old_components, old_comp);
+ *out_new_components = g_slist_prepend (*out_new_components, new_comp);
+ }
+
+ g_object_unref (comp);
+ }
+
+ *out_old_components = g_slist_reverse (*out_old_components);
+ *out_new_components = g_slist_reverse (*out_new_components);
+
+ g_object_unref (cal_cache);
+}
+
+static gboolean
+ecmb_remove_object_sync (ECalMetaBackend *meta_backend,
+ ECalCache *cal_cache,
+ ECacheOfflineFlag *offline_flag,
+ EConflictResolution conflict_resolution,
+ ECalObjModType mod,
+ const gchar *uid,
+ const gchar *rid,
+ ECalComponent **out_old_comp,
+ ECalComponent **out_new_comp,
+ GCancellable *cancellable,
+ GError **error)
+{
+ struct icaltimetype itt;
+ ECalComponent *old_comp = NULL, *new_comp = NULL, *master_comp, *existing_comp = NULL;
+ GSList *instances = NULL;
+ gboolean success = TRUE;
+ GError *local_error = NULL;
+
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ if (rid && !*rid)
+ rid = NULL;
+
+ if ((mod == E_CAL_OBJ_MOD_THIS_AND_PRIOR ||
+ mod == E_CAL_OBJ_MOD_THIS_AND_FUTURE) && !rid) {
+ /* Require Recurrence-ID for these types */
+ g_propagate_error (error, e_data_cal_create_error (ObjectNotFound, NULL));
+ return FALSE;
+ }
+
+ if (!e_cal_cache_get_components_by_uid (cal_cache, uid, &instances, cancellable, &local_error)) {
+ if (g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND)) {
+ g_clear_error (&local_error);
+ local_error = e_data_cal_create_error (ObjectNotFound, NULL);
+ }
+
+ g_propagate_error (error, local_error);
+
+ return FALSE;
+ }
+
+ master_comp = ecmb_find_in_instances (instances, uid, NULL);
+ if (rid) {
+ /* Set detached instance as the old object */
+ existing_comp = ecmb_find_in_instances (instances, uid, rid);
+ }
+
+ if (!existing_comp)
+ existing_comp = master_comp;
+
+ /* Remember old and new components */
+ if (out_old_comp && existing_comp)
+ old_comp = e_cal_component_clone (existing_comp);
+
+ if (*offline_flag == E_CACHE_OFFLINE_UNKNOWN) {
+ if (e_backend_get_online (E_BACKEND (meta_backend)) &&
+ ecmb_connect_wrapper_sync (meta_backend, cancellable, NULL)) {
+ *offline_flag = E_CACHE_IS_ONLINE;
+ } else {
+ *offline_flag = E_CACHE_IS_OFFLINE;
+ }
+ }
+
+ switch (mod) {
+ case E_CAL_OBJ_MOD_ALL:
+ /* Will remove the whole component below */
+ break;
+ case E_CAL_OBJ_MOD_ONLY_THIS:
+ case E_CAL_OBJ_MOD_THIS:
+ if (rid) {
+ if (existing_comp != master_comp) {
+ instances = g_slist_remove (instances, existing_comp);
+ g_clear_object (&existing_comp);
+ } else if (mod == E_CAL_OBJ_MOD_ONLY_THIS) {
+ success = FALSE;
+ g_propagate_error (error, e_data_cal_create_error (ObjectNotFound, NULL));
+ } else {
+ itt = icaltime_from_string (rid);
+ if (!itt.zone) {
+ ECalComponentDateTime dt;
+
+ e_cal_component_get_dtstart (master_comp, &dt);
+ if (dt.value && dt.tzid) {
+ icaltimezone *zone = e_cal_cache_resolve_timezone_simple_cb
(dt.tzid, cal_cache);
+
+ if (zone)
+ itt = icaltime_convert_to_zone (itt, zone);
+ }
+ e_cal_component_free_datetime (&dt);
+
+ itt = icaltime_convert_to_zone (itt, icaltimezone_get_utc_timezone
());
+ }
+
+ e_cal_util_remove_instances (e_cal_component_get_icalcomponent (master_comp),
itt, mod);
+ }
+
+ if (success && out_new_comp)
+ new_comp = e_cal_component_clone (master_comp);
+ } else {
+ mod = E_CAL_OBJ_MOD_ALL;
+ }
+ break;
+ case E_CAL_OBJ_MOD_THIS_AND_PRIOR:
+ case E_CAL_OBJ_MOD_THIS_AND_FUTURE:
+ if (master_comp) {
+ time_t fromtt, instancett;
+ GSList *link, *previous = instances;
+
+ itt = icaltime_from_string (rid);
+ if (!itt.zone) {
+ ECalComponentDateTime dt;
+
+ e_cal_component_get_dtstart (master_comp, &dt);
+ if (dt.value && dt.tzid) {
+ icaltimezone *zone = e_cal_cache_resolve_timezone_simple_cb (dt.tzid,
cal_cache);
+
+ if (zone)
+ itt = icaltime_convert_to_zone (itt, zone);
+ }
+ e_cal_component_free_datetime (&dt);
+
+ itt = icaltime_convert_to_zone (itt, icaltimezone_get_utc_timezone ());
+ }
+
+ e_cal_util_remove_instances (e_cal_component_get_icalcomponent (master_comp), itt,
mod);
+
+ fromtt = icaltime_as_timet (itt);
+
+ /* Remove detached instances */
+ for (link = instances; link && fromtt > 0;) {
+ ECalComponent *comp = link->data;
+ ECalComponentRange range;
+
+ if (!e_cal_component_is_instance (comp)) {
+ previous = link;
+ link = g_slist_next (link);
+ continue;
+ }
+
+ e_cal_component_get_recurid (comp, &range);
+ if (range.datetime.value)
+ instancett = icaltime_as_timet (*range.datetime.value);
+ else
+ instancett = 0;
+ e_cal_component_free_range (&range);
+
+ if (instancett > 0 && (
+ (mod == E_CAL_OBJ_MOD_THIS_AND_PRIOR && instancett <= fromtt) ||
+ (mod == E_CAL_OBJ_MOD_THIS_AND_FUTURE && instancett >= fromtt))) {
+ GSList *prev_instances = instances;
+
+ instances = g_slist_remove (instances, comp);
+ g_clear_object (&comp);
+
+ /* Restart the lookup */
+ if (previous == prev_instances)
+ previous = instances;
+
+ link = previous;
+ } else {
+ previous = link;
+ link = g_slist_next (link);
+ }
+ }
+ } else {
+ mod = E_CAL_OBJ_MOD_ALL;
+ }
+ break;
+ }
+
+ if (success) {
+ gchar *extra = NULL;
+
+ if (!e_cal_cache_get_component_extra (cal_cache, uid, NULL, &extra, cancellable, NULL))
+ extra = NULL;
+
+ if (mod == E_CAL_OBJ_MOD_ALL) {
+ if (*offline_flag == E_CACHE_IS_ONLINE) {
+ gchar *ical_string = NULL;
+
+ g_warn_if_fail (e_cal_cache_get_component_as_string (cal_cache, uid, NULL,
&ical_string, cancellable, NULL));
+
+ success = e_cal_meta_backend_remove_component_sync (meta_backend,
conflict_resolution, uid, extra, ical_string, cancellable, error);
+
+ g_free (ical_string);
+ }
+
+ success = success && ecmb_maybe_remove_from_cache (meta_backend, cal_cache,
*offline_flag, uid, cancellable, error);
+ } else {
+ gboolean requires_put = TRUE;
+ gchar *new_uid = NULL, *new_extra = NULL;
+
+ if (master_comp) {
+ icalcomponent *icalcomp = e_cal_component_get_icalcomponent (master_comp);
+
+ icalcomponent_set_sequence (icalcomp, icalcomponent_get_sequence (icalcomp) +
1);
+
+ e_cal_component_rescan (master_comp);
+
+ /* Set the last modified time on the component */
+ itt = icaltime_current_time_with_zone (icaltimezone_get_utc_timezone ());
+ e_cal_component_set_last_modified (master_comp, &itt);
+ }
+
+ if (*offline_flag == E_CACHE_IS_ONLINE) {
+ success = ecmb_save_component_wrapper_sync (meta_backend, cal_cache, TRUE,
conflict_resolution,
+ instances, extra, uid, &requires_put, &new_uid, &new_extra,
cancellable, error);
+ }
+
+ if (success && requires_put)
+ success = ecmb_put_instances (meta_backend, cal_cache, uid, *offline_flag,
instances, new_extra ? new_extra : extra, cancellable, error);
+
+ if (success && new_uid && !requires_put) {
+ g_clear_object (&new_comp);
+
+ if (!e_cal_cache_get_component (cal_cache, new_uid, NULL, &new_comp,
cancellable, NULL))
+ new_comp = NULL;
+ }
+
+ g_free (new_uid);
+ g_free (new_extra);
+ }
+
+ g_free (extra);
+ }
+
+ if (!success) {
+ g_clear_object (&old_comp);
+ g_clear_object (&new_comp);
+ }
+
+ if (out_old_comp)
+ *out_old_comp = old_comp;
+ if (out_new_comp)
+ *out_new_comp = new_comp;
+
+ g_slist_free_full (instances, g_object_unref);
+
+ return success;
+}
+
+static void
+ecmb_remove_objects_sync (ECalBackendSync *sync_backend,
+ EDataCal *cal,
+ GCancellable *cancellable,
+ const GSList *ids,
+ ECalObjModType mod,
+ GSList **out_old_components,
+ GSList **out_new_components,
+ GError **error)
+{
+ ECalMetaBackend *meta_backend;
+ ECalCache *cal_cache;
+ ECacheOfflineFlag offline_flag = E_CACHE_OFFLINE_UNKNOWN;
+ EConflictResolution conflict_resolution = E_CONFLICT_RESOLUTION_FAIL;
+ GSList *link;
+ gboolean success = TRUE;
+
+ g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+ g_return_if_fail (ids != NULL);
+ g_return_if_fail (out_old_components != NULL);
+ g_return_if_fail (out_new_components != NULL);
+
+ if (!e_cal_backend_get_writable (E_CAL_BACKEND (sync_backend))) {
+ g_propagate_error (error, e_data_cal_create_error (PermissionDenied, NULL));
+ return;
+ }
+
+ meta_backend = E_CAL_META_BACKEND (sync_backend);
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ g_return_if_fail (cal_cache != NULL);
+
+ for (link = (GSList *) ids; link && success; link = g_slist_next (link)) {
+ ECalComponent *old_comp = NULL, *new_comp = NULL;
+ ECalComponentId *id = link->data;
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error))
+ break;
+
+ if (!id) {
+ g_propagate_error (error, e_data_cal_create_error (InvalidObject, NULL));
+ break;
+ }
+
+ success = ecmb_remove_object_sync (meta_backend, cal_cache, &offline_flag,
conflict_resolution,
+ mod, id->uid, id->rid, &old_comp, &new_comp, cancellable, error);
+
+ if (success) {
+ *out_old_components = g_slist_prepend (*out_old_components, old_comp);
+ *out_new_components = g_slist_prepend (*out_new_components, new_comp);
+ }
+ }
+
+ *out_old_components = g_slist_reverse (*out_old_components);
+ *out_new_components = g_slist_reverse (*out_new_components);
+
+ g_object_unref (cal_cache);
+}
+
+static gboolean
+ecmb_receive_object_sync (ECalMetaBackend *meta_backend,
+ ECalCache *cal_cache,
+ ECacheOfflineFlag *offline_flag,
+ EConflictResolution conflict_resolution,
+ ECalComponent *comp,
+ icalproperty_method method,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ESourceRegistry *registry;
+ ECalBackend *cal_backend;
+ gboolean is_declined, is_in_cache;
+ ECalObjModType mod;
+ ECalComponentId *id;
+ gboolean success = FALSE;
+
+ g_return_val_if_fail (E_IS_CAL_COMPONENT (comp), FALSE);
+
+ id = e_cal_component_get_id (comp);
+
+ if (!id && method == ICAL_METHOD_PUBLISH) {
+ gchar *new_uid;
+
+ new_uid = e_cal_component_gen_uid ();
+ e_cal_component_set_uid (comp, new_uid);
+ g_free (new_uid);
+
+ id = e_cal_component_get_id (comp);
+ }
+
+ if (!id) {
+ g_propagate_error (error, e_data_cal_create_error (InvalidObject, NULL));
+ return FALSE;
+ }
+
+ cal_backend = E_CAL_BACKEND (meta_backend);
+ registry = e_cal_backend_get_registry (cal_backend);
+
+ /* just to check whether component exists in a cache */
+ is_in_cache = e_cal_cache_contains (cal_cache, id->uid, NULL, E_CACHE_EXCLUDE_DELETED) ||
+ (id->rid && *id->rid && e_cal_cache_contains (cal_cache, id->uid, id->rid,
E_CACHE_EXCLUDE_DELETED));
+
+ mod = e_cal_component_is_instance (comp) ? E_CAL_OBJ_MOD_THIS : E_CAL_OBJ_MOD_ALL;
+
+ switch (method) {
+ case ICAL_METHOD_PUBLISH:
+ case ICAL_METHOD_REQUEST:
+ case ICAL_METHOD_REPLY:
+ is_declined = e_cal_backend_user_declined (registry, e_cal_component_get_icalcomponent
(comp));
+ if (is_in_cache) {
+ if (!is_declined) {
+ success = ecmb_modify_object_sync (meta_backend, cal_cache, offline_flag,
conflict_resolution,
+ mod, comp, NULL, NULL, cancellable, error);
+ } else {
+ success = ecmb_remove_object_sync (meta_backend, cal_cache, offline_flag,
conflict_resolution,
+ mod, id->uid, id->rid, NULL, NULL, cancellable, error);
+ }
+ } else if (!is_declined) {
+ success = ecmb_create_object_sync (meta_backend, cal_cache, offline_flag,
conflict_resolution,
+ comp, NULL, NULL, cancellable, error);
+ }
+ break;
+ case ICAL_METHOD_CANCEL:
+ if (is_in_cache) {
+ success = ecmb_remove_object_sync (meta_backend, cal_cache, offline_flag,
conflict_resolution,
+ E_CAL_OBJ_MOD_THIS, id->uid, id->rid, NULL, NULL, cancellable, error);
+ } else {
+ g_propagate_error (error, e_data_cal_create_error (ObjectNotFound, NULL));
+ }
+ break;
+
+ default:
+ g_propagate_error (error, e_data_cal_create_error (UnsupportedMethod, NULL));
+ break;
+ }
+
+ e_cal_component_free_id (id);
+
+ return success;
+}
+
+static void
+ecmb_receive_objects_sync (ECalBackendSync *sync_backend,
+ EDataCal *cal,
+ GCancellable *cancellable,
+ const gchar *calobj,
+ GError **error)
+{
+ ECalMetaBackend *meta_backend;
+ ECacheOfflineFlag offline_flag = E_CACHE_OFFLINE_UNKNOWN;
+ EConflictResolution conflict_resolution = E_CONFLICT_RESOLUTION_FAIL;
+ ECalCache *cal_cache;
+ ECalComponent *comp;
+ icalcomponent *icalcomp, *subcomp;
+ icalcomponent_kind kind;
+ icalproperty_method top_method;
+ GSList *comps = NULL, *link;
+ gboolean success = TRUE;
+
+ g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+ g_return_if_fail (calobj != NULL);
+
+ if (!e_cal_backend_get_writable (E_CAL_BACKEND (sync_backend))) {
+ g_propagate_error (error, e_data_cal_create_error (PermissionDenied, NULL));
+ return;
+ }
+
+ meta_backend = E_CAL_META_BACKEND (sync_backend);
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ g_return_if_fail (cal_cache != NULL);
+
+ icalcomp = icalparser_parse_string (calobj);
+ if (!icalcomp) {
+ g_propagate_error (error, e_data_cal_create_error (InvalidObject, NULL));
+ g_object_unref (cal_cache);
+ return;
+ }
+
+ kind = e_cal_backend_get_kind (E_CAL_BACKEND (meta_backend));
+
+ if (icalcomponent_isa (icalcomp) == ICAL_VCALENDAR_COMPONENT) {
+ for (subcomp = icalcomponent_get_first_component (icalcomp, kind);
+ subcomp && success;
+ subcomp = icalcomponent_get_next_component (icalcomp, kind)) {
+ comp = e_cal_component_new_from_icalcomponent (icalcomponent_new_clone (subcomp));
+
+ if (comp)
+ comps = g_slist_prepend (comps, comp);
+ }
+ } else if (icalcomponent_isa (icalcomp) == kind) {
+ comp = e_cal_component_new_from_icalcomponent (icalcomponent_new_clone (icalcomp));
+
+ if (comp)
+ comps = g_slist_prepend (comps, comp);
+ }
+
+ if (!comps) {
+ g_propagate_error (error, e_data_cal_create_error (InvalidObject, NULL));
+ icalcomponent_free (icalcomp);
+ g_object_unref (cal_cache);
+ return;
+ }
+
+ comps = g_slist_reverse (comps);
+
+ if (icalcomponent_isa (icalcomp) == ICAL_VCALENDAR_COMPONENT)
+ ecmb_gather_timezones (meta_backend, E_TIMEZONE_CACHE (cal_cache), icalcomp);
+
+ if (icalcomponent_get_first_property (icalcomp, ICAL_METHOD_PROPERTY))
+ top_method = icalcomponent_get_method (icalcomp);
+ else
+ top_method = ICAL_METHOD_PUBLISH;
+
+ for (link = comps; link && success; link = g_slist_next (link)) {
+ ECalComponent *comp = link->data;
+ icalproperty_method method;
+
+ subcomp = e_cal_component_get_icalcomponent (comp);
+
+ if (icalcomponent_get_first_property (subcomp, ICAL_METHOD_PROPERTY)) {
+ method = icalcomponent_get_method (subcomp);
+ } else {
+ method = top_method;
+ }
+
+ success = ecmb_receive_object_sync (meta_backend, cal_cache, &offline_flag,
conflict_resolution,
+ comp, method, cancellable, error);
+ }
+
+ g_slist_free_full (comps, g_object_unref);
+ icalcomponent_free (icalcomp);
+ g_object_unref (cal_cache);
+}
+
+static void
+ecmb_send_objects_sync (ECalBackendSync *sync_backend,
+ EDataCal *cal,
+ GCancellable *cancellable,
+ const gchar *calobj,
+ GSList **out_users,
+ gchar **out_modified_calobj,
+ GError **error)
+{
+ g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+ g_return_if_fail (calobj != NULL);
+ g_return_if_fail (out_users != NULL);
+ g_return_if_fail (out_modified_calobj != NULL);
+
+ *out_users = NULL;
+ *out_modified_calobj = g_strdup (calobj);
+}
+
+static void
+ecmb_add_attachment_uris (ECalComponent *comp,
+ GSList **out_uris)
+{
+ icalcomponent *icalcomp;
+ icalproperty *prop;
+
+ g_return_if_fail (E_IS_CAL_COMPONENT (comp));
+ g_return_if_fail (out_uris != NULL);
+
+ icalcomp = e_cal_component_get_icalcomponent (comp);
+ g_return_if_fail (icalcomp != NULL);
+
+ for (prop = icalcomponent_get_first_property (icalcomp, ICAL_ATTACH_PROPERTY);
+ prop;
+ prop = icalcomponent_get_next_property (icalcomp, ICAL_ATTACH_PROPERTY)) {
+ icalattach *attach = icalproperty_get_attach (prop);
+
+ if (attach && icalattach_get_is_url (attach)) {
+ const gchar *url;
+
+ url = icalattach_get_url (attach);
+ if (url) {
+ gsize buf_size;
+ gchar *buf;
+
+ buf_size = strlen (url);
+ buf = g_malloc0 (buf_size + 1);
+
+ icalvalue_decode_ical_string (url, buf, buf_size);
+
+ *out_uris = g_slist_prepend (*out_uris, g_strdup (buf));
+
+ g_free (buf);
+ }
+ }
+ }
+}
+
+static void
+ecmb_get_attachment_uris_sync (ECalBackendSync *sync_backend,
+ EDataCal *cal,
+ GCancellable *cancellable,
+ const gchar *uid,
+ const gchar *rid,
+ GSList **out_uris,
+ GError **error)
+{
+ ECalMetaBackend *meta_backend;
+ ECalCache *cal_cache;
+ ECalComponent *comp;
+ GError *local_error = NULL;
+
+ g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+ g_return_if_fail (uid != NULL);
+ g_return_if_fail (out_uris != NULL);
+
+ *out_uris = NULL;
+
+ meta_backend = E_CAL_META_BACKEND (sync_backend);
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ g_return_if_fail (cal_cache != NULL);
+
+ if (rid && *rid) {
+ if (e_cal_cache_get_component (cal_cache, uid, rid, &comp, cancellable, &local_error) &&
comp) {
+ ecmb_add_attachment_uris (comp, out_uris);
+ g_object_unref (comp);
+ }
+ } else {
+ GSList *comps = NULL, *link;
+
+ if (e_cal_cache_get_components_by_uid (cal_cache, uid, &comps, cancellable, &local_error)) {
+ for (link = comps; link; link = g_slist_next (link)) {
+ comp = link->data;
+
+ ecmb_add_attachment_uris (comp, out_uris);
+ }
+
+ g_slist_free_full (comps, g_object_unref);
+ }
+ }
+
+ g_object_unref (cal_cache);
+
+ *out_uris = g_slist_reverse (*out_uris);
+
+ if (local_error) {
+ if (g_error_matches (local_error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND)) {
+ g_clear_error (&local_error);
+ local_error = e_data_cal_create_error (ObjectNotFound, NULL);
+ }
+
+ g_propagate_error (error, local_error);
+ }
+}
+
+static void
+ecmb_discard_alarm_sync (ECalBackendSync *sync_backend,
+ EDataCal *cal,
+ GCancellable *cancellable,
+ const gchar *uid,
+ const gchar *rid,
+ const gchar *auid,
+ GError **error)
+{
+ g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+ g_return_if_fail (uid != NULL);
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error))
+ return;
+
+ g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_NOT_SUPPORTED,
+ e_client_error_to_string (E_CLIENT_ERROR_NOT_SUPPORTED));
+}
+
+static void
+ecmb_get_timezone_sync (ECalBackendSync *sync_backend,
+ EDataCal *cal,
+ GCancellable *cancellable,
+ const gchar *tzid,
+ gchar **tzobject,
+ GError **error)
+{
+ ECalCache *cal_cache;
+ icaltimezone *zone;
+ gchar *timezone_str = NULL;
+ GError *local_error = NULL;
+
+ g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+ g_return_if_fail (tzid != NULL);
+ g_return_if_fail (tzobject != NULL);
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error))
+ return;
+
+ cal_cache = e_cal_meta_backend_ref_cache (E_CAL_META_BACKEND (sync_backend));
+ g_return_if_fail (cal_cache != NULL);
+
+ zone = e_timezone_cache_get_timezone (E_TIMEZONE_CACHE (cal_cache), tzid);
+ if (zone) {
+ icalcomponent *icalcomp;
+
+ icalcomp = icaltimezone_get_component (zone);
+
+ if (!icalcomp) {
+ local_error = e_data_cal_create_error (InvalidObject, NULL);
+ } else {
+ timezone_str = icalcomponent_as_ical_string_r (icalcomp);
+ }
+ }
+
+ g_object_unref (cal_cache);
+
+ if (!local_error && !timezone_str)
+ local_error = e_data_cal_create_error (ObjectNotFound, NULL);
+
+ *tzobject = timezone_str;
+
+ if (local_error)
+ g_propagate_error (error, local_error);
+}
+
+static void
+ecmb_add_timezone_sync (ECalBackendSync *sync_backend,
+ EDataCal *cal,
+ GCancellable *cancellable,
+ const gchar *tzobject,
+ GError **error)
+{
+ icalcomponent *tz_comp;
+
+ g_return_if_fail (E_IS_CAL_META_BACKEND (sync_backend));
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error))
+ return;
+
+ if (!tzobject || !*tzobject) {
+ g_propagate_error (error, e_data_cal_create_error (InvalidObject, NULL));
+ return;
+ }
+
+ tz_comp = icalparser_parse_string (tzobject);
+ if (!tz_comp ||
+ icalcomponent_isa (tz_comp) != ICAL_VTIMEZONE_COMPONENT) {
+ g_propagate_error (error, e_data_cal_create_error (InvalidObject, NULL));
+ } else {
+ ECalCache *cal_cache;
+ icaltimezone *zone;
+
+ zone = icaltimezone_new ();
+ icaltimezone_set_component (zone, tz_comp);
+
+ tz_comp = NULL;
+
+ cal_cache = e_cal_meta_backend_ref_cache (E_CAL_META_BACKEND (sync_backend));
+ if (cal_cache) {
+ e_timezone_cache_add_timezone (E_TIMEZONE_CACHE (cal_cache), zone);
+ icaltimezone_free (zone, 1);
+ g_object_unref (cal_cache);
+ } else {
+ g_warn_if_reached ();
+ }
+ }
+
+ if (tz_comp)
+ icalcomponent_free (tz_comp);
+}
+
+static gchar *
+ecmb_get_backend_property (ECalBackend *cal_backend,
+ const gchar *prop_name)
+{
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (cal_backend), NULL);
+ g_return_val_if_fail (prop_name != NULL, NULL);
+
+ if (g_str_equal (prop_name, CAL_BACKEND_PROPERTY_REVISION)) {
+ ECalCache *cal_cache;
+ gchar *revision = NULL;
+
+ cal_cache = e_cal_meta_backend_ref_cache (E_CAL_META_BACKEND (cal_backend));
+ if (cal_cache) {
+ revision = e_cache_dup_revision (E_CACHE (cal_cache));
+ g_object_unref (cal_cache);
+ } else {
+ g_warn_if_reached ();
+ }
+
+ return revision;
+ } else if (g_str_equal (prop_name, CAL_BACKEND_PROPERTY_DEFAULT_OBJECT)) {
+ ECalComponent *comp;
+ gchar *prop_value;
+
+ comp = e_cal_component_new ();
+
+ switch (e_cal_backend_get_kind (cal_backend)) {
+ case ICAL_VEVENT_COMPONENT:
+ e_cal_component_set_new_vtype (comp, E_CAL_COMPONENT_EVENT);
+ break;
+ case ICAL_VTODO_COMPONENT:
+ e_cal_component_set_new_vtype (comp, E_CAL_COMPONENT_TODO);
+ break;
+ case ICAL_VJOURNAL_COMPONENT:
+ e_cal_component_set_new_vtype (comp, E_CAL_COMPONENT_JOURNAL);
+ break;
+ default:
+ g_object_unref (comp);
+ return NULL;
+ }
+
+ prop_value = e_cal_component_get_as_string (comp);
+
+ g_object_unref (comp);
+
+ return prop_value;
+ } else if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
+ return g_strdup (e_cal_meta_backend_get_capabilities (E_CAL_META_BACKEND (cal_backend)));
+ }
+
+ /* Chain up to parent's method. */
+ return E_CAL_BACKEND_CLASS (e_cal_meta_backend_parent_class)->get_backend_property (cal_backend,
prop_name);
+}
+
+static void
+ecmb_start_view (ECalBackend *cal_backend,
+ EDataCalView *view)
+{
+ GCancellable *cancellable;
+
+ g_return_if_fail (E_IS_CAL_META_BACKEND (cal_backend));
+
+ cancellable = ecmb_create_view_cancellable (E_CAL_META_BACKEND (cal_backend), view);
+
+ e_cal_backend_schedule_custom_operation (cal_backend, cancellable,
+ ecmb_start_view_thread_func, g_object_ref (view), g_object_unref);
+
+ g_object_unref (cancellable);
+}
+
+static void
+ecmb_stop_view (ECalBackend *cal_backend,
+ EDataCalView *view)
+{
+ GCancellable *cancellable;
+
+ g_return_if_fail (E_IS_CAL_META_BACKEND (cal_backend));
+
+ cancellable = ecmb_steal_view_cancellable (E_CAL_META_BACKEND (cal_backend), view);
+ if (cancellable) {
+ g_cancellable_cancel (cancellable);
+ g_object_unref (cancellable);
+ }
+}
+
+static ESourceAuthenticationResult
+ecmb_authenticate_sync (EBackend *backend,
+ const ENamedParameters *credentials,
+ gchar **out_certificate_pem,
+ GTlsCertificateFlags *out_certificate_errors,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalMetaBackend *meta_backend;
+ ESourceAuthenticationResult auth_result = E_SOURCE_AUTHENTICATION_UNKNOWN;
+ gboolean success, refresh_after_authenticate = FALSE;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (backend), E_SOURCE_AUTHENTICATION_ERROR);
+
+ meta_backend = E_CAL_META_BACKEND (backend);
+
+ if (!e_backend_get_online (E_BACKEND (meta_backend))) {
+ g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_REPOSITORY_OFFLINE,
+ e_client_error_to_string (E_CLIENT_ERROR_REPOSITORY_OFFLINE));
+
+ return E_SOURCE_AUTHENTICATION_ERROR;
+ }
+
+ g_mutex_lock (&meta_backend->priv->connect_lock);
+ success = e_cal_meta_backend_connect_sync (meta_backend, credentials, &auth_result,
+ out_certificate_pem, out_certificate_errors, cancellable, error);
+
+ if (success) {
+ ecmb_update_connection_values (meta_backend);
+ auth_result = E_SOURCE_AUTHENTICATION_ACCEPTED;
+ } else {
+ if (auth_result == E_SOURCE_AUTHENTICATION_UNKNOWN)
+ auth_result = E_SOURCE_AUTHENTICATION_ERROR;
+ }
+ g_mutex_unlock (&meta_backend->priv->connect_lock);
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ e_named_parameters_free (meta_backend->priv->last_credentials);
+ if (success) {
+ meta_backend->priv->last_credentials = e_named_parameters_new_clone (credentials);
+
+ refresh_after_authenticate = meta_backend->priv->refresh_after_authenticate;
+ meta_backend->priv->refresh_after_authenticate = FALSE;
+ } else {
+ meta_backend->priv->last_credentials = NULL;
+ }
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ if (refresh_after_authenticate)
+ ecmb_schedule_refresh (meta_backend);
+
+ return auth_result;
+}
+
+static void
+ecmb_schedule_refresh (ECalMetaBackend *meta_backend)
+{
+ GCancellable *cancellable;
+
+ g_return_if_fail (E_IS_CAL_META_BACKEND (meta_backend));
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ if (meta_backend->priv->refresh_cancellable) {
+ /* Already refreshing the content */
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+ return;
+ }
+
+ cancellable = g_cancellable_new ();
+ meta_backend->priv->refresh_cancellable = g_object_ref (cancellable);
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ e_cal_backend_schedule_custom_operation (E_CAL_BACKEND (meta_backend), cancellable,
+ ecmb_refresh_thread_func, NULL, NULL);
+
+ g_object_unref (cancellable);
+}
+
+static void
+ecmb_schedule_source_changed (ECalMetaBackend *meta_backend)
+{
+ GCancellable *cancellable;
+
+ g_return_if_fail (E_IS_CAL_META_BACKEND (meta_backend));
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ if (meta_backend->priv->source_changed_cancellable) {
+ /* Already updating */
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+ return;
+ }
+
+ cancellable = g_cancellable_new ();
+ meta_backend->priv->source_changed_cancellable = g_object_ref (cancellable);
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ e_cal_backend_schedule_custom_operation (E_CAL_BACKEND (meta_backend), cancellable,
+ ecmb_source_changed_thread_func, NULL, NULL);
+
+ g_object_unref (cancellable);
+}
+
+static void
+ecmb_schedule_go_offline (ECalMetaBackend *meta_backend)
+{
+ GCancellable *cancellable;
+
+ g_return_if_fail (E_IS_CAL_META_BACKEND (meta_backend));
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ /* Cancel anything ongoing now, but disconnect in a dedicated thread */
+ if (meta_backend->priv->refresh_cancellable) {
+ g_cancellable_cancel (meta_backend->priv->refresh_cancellable);
+ g_clear_object (&meta_backend->priv->refresh_cancellable);
+ }
+
+ if (meta_backend->priv->source_changed_cancellable) {
+ g_cancellable_cancel (meta_backend->priv->source_changed_cancellable);
+ g_clear_object (&meta_backend->priv->source_changed_cancellable);
+ }
+
+ if (meta_backend->priv->go_offline_cancellable) {
+ /* Already going offline */
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+ return;
+ }
+
+ cancellable = g_cancellable_new ();
+ meta_backend->priv->go_offline_cancellable = g_object_ref (cancellable);
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ e_cal_backend_schedule_custom_operation (E_CAL_BACKEND (meta_backend), cancellable,
+ ecmb_go_offline_thread_func, NULL, NULL);
+
+ g_object_unref (cancellable);
+}
+
+static void
+ecmb_notify_online_cb (GObject *object,
+ GParamSpec *param,
+ gpointer user_data)
+{
+ ECalMetaBackend *meta_backend = user_data;
+ gboolean new_value;
+
+ g_return_if_fail (E_IS_CAL_META_BACKEND (meta_backend));
+
+ new_value = e_backend_get_online (E_BACKEND (meta_backend));
+ if (!new_value == !meta_backend->priv->current_online_state)
+ return;
+
+ meta_backend->priv->current_online_state = new_value;
+
+ if (new_value)
+ ecmb_schedule_refresh (meta_backend);
+ else
+ ecmb_schedule_go_offline (meta_backend);
+}
+
+static void
+ecmb_cancel_view_cb (gpointer key,
+ gpointer value,
+ gpointer user_data)
+{
+ GCancellable *cancellable = value;
+
+ g_return_if_fail (G_IS_CANCELLABLE (cancellable));
+
+ g_cancellable_cancel (cancellable);
+}
+
+static void
+e_cal_meta_backend_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case PROP_CACHE:
+ e_cal_meta_backend_set_cache (
+ E_CAL_META_BACKEND (object),
+ g_value_get_object (value));
+ return;
+ }
+
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+e_cal_meta_backend_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case PROP_CACHE:
+ g_value_take_object (
+ value,
+ e_cal_meta_backend_ref_cache (
+ E_CAL_META_BACKEND (object)));
+ return;
+ }
+
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+e_cal_meta_backend_constructed (GObject *object)
+{
+ ECalMetaBackend *meta_backend = E_CAL_META_BACKEND (object);
+
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_cal_meta_backend_parent_class)->constructed (object);
+
+ meta_backend->priv->current_online_state = e_backend_get_online (E_BACKEND (meta_backend));
+
+ meta_backend->priv->notify_online_id = g_signal_connect (meta_backend, "notify::online",
+ G_CALLBACK (ecmb_notify_online_cb), meta_backend);
+
+ if (!meta_backend->priv->cache) {
+ ECalCache *cache;
+ gchar *filename;
+
+ filename = g_build_filename (e_cal_backend_get_cache_dir (E_CAL_BACKEND (meta_backend)),
"cache.db", NULL);
+ cache = e_cal_cache_new (filename, NULL, &meta_backend->priv->create_cache_error);
+ g_prefix_error (&meta_backend->priv->create_cache_error, _("Failed to create cache ”%s”:"),
filename);
+
+ g_free (filename);
+
+ if (cache) {
+ e_cal_meta_backend_set_cache (meta_backend, cache);
+ g_clear_object (&cache);
+ }
+ }
+}
+
+static void
+e_cal_meta_backend_dispose (GObject *object)
+{
+ ECalMetaBackend *meta_backend = E_CAL_META_BACKEND (object);
+ ESource *source = e_backend_get_source (E_BACKEND (meta_backend));
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ if (meta_backend->priv->refresh_timeout_id) {
+ if (source)
+ e_source_refresh_remove_timeout (source, meta_backend->priv->refresh_timeout_id);
+ meta_backend->priv->refresh_timeout_id = 0;
+ }
+
+ if (meta_backend->priv->source_changed_id) {
+ if (source)
+ g_signal_handler_disconnect (source, meta_backend->priv->source_changed_id);
+ meta_backend->priv->source_changed_id = 0;
+ }
+
+ if (meta_backend->priv->notify_online_id) {
+ g_signal_handler_disconnect (meta_backend, meta_backend->priv->notify_online_id);
+ meta_backend->priv->notify_online_id = 0;
+ }
+
+ if (meta_backend->priv->revision_changed_id) {
+ if (meta_backend->priv->cache)
+ g_signal_handler_disconnect (meta_backend->priv->cache,
meta_backend->priv->revision_changed_id);
+ meta_backend->priv->revision_changed_id = 0;
+ }
+
+ g_hash_table_foreach (meta_backend->priv->view_cancellables, ecmb_cancel_view_cb, NULL);
+
+ if (meta_backend->priv->refresh_cancellable) {
+ g_cancellable_cancel (meta_backend->priv->refresh_cancellable);
+ g_clear_object (&meta_backend->priv->refresh_cancellable);
+ }
+
+ if (meta_backend->priv->source_changed_cancellable) {
+ g_cancellable_cancel (meta_backend->priv->source_changed_cancellable);
+ g_clear_object (&meta_backend->priv->source_changed_cancellable);
+ }
+
+ if (meta_backend->priv->go_offline_cancellable) {
+ g_cancellable_cancel (meta_backend->priv->go_offline_cancellable);
+ g_clear_object (&meta_backend->priv->go_offline_cancellable);
+ }
+
+ e_named_parameters_free (meta_backend->priv->last_credentials);
+ meta_backend->priv->last_credentials = NULL;
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_cal_meta_backend_parent_class)->dispose (object);
+}
+
+static void
+e_cal_meta_backend_finalize (GObject *object)
+{
+ ECalMetaBackend *meta_backend = E_CAL_META_BACKEND (object);
+
+ g_clear_object (&meta_backend->priv->cache);
+ g_clear_object (&meta_backend->priv->refresh_cancellable);
+ g_clear_object (&meta_backend->priv->source_changed_cancellable);
+ g_clear_object (&meta_backend->priv->go_offline_cancellable);
+ g_clear_error (&meta_backend->priv->create_cache_error);
+ g_clear_pointer (&meta_backend->priv->authentication_host, g_free);
+ g_clear_pointer (&meta_backend->priv->authentication_user, g_free);
+ g_clear_pointer (&meta_backend->priv->authentication_method, g_free);
+ g_clear_pointer (&meta_backend->priv->authentication_proxy_uid, g_free);
+ g_clear_pointer (&meta_backend->priv->authentication_credential_name, g_free);
+ g_clear_pointer (&meta_backend->priv->webdav_soup_uri, (GDestroyNotify) soup_uri_free);
+
+ g_mutex_clear (&meta_backend->priv->connect_lock);
+ g_mutex_clear (&meta_backend->priv->property_lock);
+ g_hash_table_destroy (meta_backend->priv->view_cancellables);
+
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_cal_meta_backend_parent_class)->finalize (object);
+}
+
+static void
+e_cal_meta_backend_class_init (ECalMetaBackendClass *klass)
+{
+ GObjectClass *object_class;
+ EBackendClass *backend_class;
+ ECalBackendClass *cal_backend_class;
+ ECalBackendSyncClass *cal_backend_sync_class;
+
+ g_type_class_add_private (klass, sizeof (ECalMetaBackendPrivate));
+
+ klass->get_changes_sync = ecmb_get_changes_sync;
+ klass->search_sync = ecmb_search_sync;
+ klass->search_components_sync = ecmb_search_components_sync;
+ klass->requires_reconnect = ecmb_requires_reconnect;
+
+ cal_backend_sync_class = E_CAL_BACKEND_SYNC_CLASS (klass);
+ cal_backend_sync_class->open_sync = ecmb_open_sync;
+ cal_backend_sync_class->refresh_sync = ecmb_refresh_sync;
+ cal_backend_sync_class->get_object_sync = ecmb_get_object_sync;
+ cal_backend_sync_class->get_object_list_sync = ecmb_get_object_list_sync;
+ cal_backend_sync_class->get_free_busy_sync = ecmb_get_free_busy_sync;
+ cal_backend_sync_class->create_objects_sync = ecmb_create_objects_sync;
+ cal_backend_sync_class->modify_objects_sync = ecmb_modify_objects_sync;
+ cal_backend_sync_class->remove_objects_sync = ecmb_remove_objects_sync;
+ cal_backend_sync_class->receive_objects_sync = ecmb_receive_objects_sync;
+ cal_backend_sync_class->send_objects_sync = ecmb_send_objects_sync;
+ cal_backend_sync_class->get_attachment_uris_sync = ecmb_get_attachment_uris_sync;
+ cal_backend_sync_class->discard_alarm_sync = ecmb_discard_alarm_sync;
+ cal_backend_sync_class->get_timezone_sync = ecmb_get_timezone_sync;
+ cal_backend_sync_class->add_timezone_sync = ecmb_add_timezone_sync;
+
+ cal_backend_class = E_CAL_BACKEND_CLASS (klass);
+ cal_backend_class->get_backend_property = ecmb_get_backend_property;
+ cal_backend_class->start_view = ecmb_start_view;
+ cal_backend_class->stop_view = ecmb_stop_view;
+
+ backend_class = E_BACKEND_CLASS (klass);
+ backend_class->authenticate_sync = ecmb_authenticate_sync;
+
+ object_class = G_OBJECT_CLASS (klass);
+ object_class->set_property = e_cal_meta_backend_set_property;
+ object_class->get_property = e_cal_meta_backend_get_property;
+ object_class->constructed = e_cal_meta_backend_constructed;
+ object_class->dispose = e_cal_meta_backend_dispose;
+ object_class->finalize = e_cal_meta_backend_finalize;
+
+ /**
+ * ECalMetaBackend:cache:
+ *
+ * The #ECalCache being used for this meta backend.
+ **/
+ g_object_class_install_property (
+ object_class,
+ PROP_CACHE,
+ g_param_spec_object (
+ "cache",
+ "Cache",
+ "Calendar Cache",
+ E_TYPE_CAL_CACHE,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ /* This signal is meant for testing purposes mainly */
+ signals[REFRESH_COMPLETED] = g_signal_new (
+ "refresh-completed",
+ G_OBJECT_CLASS_TYPE (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 0, G_TYPE_NONE);
+
+ /**
+ * ECalMetaBackend::source-changed
+ *
+ * This signal is emitted whenever the underlying backend #ESource
+ * changes. Unlike the #ESource's 'changed' signal this one is
+ * tight to the #ECalMetaBackend itself and is emitted from
+ * a dedicated thread, thus it doesn't block the main thread.
+ *
+ * Since: 3.26
+ **/
+ signals[SOURCE_CHANGED] = g_signal_new (
+ "source-changed",
+ G_OBJECT_CLASS_TYPE (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (ECalMetaBackendClass, source_changed),
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 0, G_TYPE_NONE);
+}
+
+static void
+e_cal_meta_backend_init (ECalMetaBackend *meta_backend)
+{
+ meta_backend->priv = G_TYPE_INSTANCE_GET_PRIVATE (meta_backend, E_TYPE_CAL_META_BACKEND,
ECalMetaBackendPrivate);
+
+ g_mutex_init (&meta_backend->priv->connect_lock);
+ g_mutex_init (&meta_backend->priv->property_lock);
+
+ meta_backend->priv->view_cancellables = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL,
g_object_unref);
+ meta_backend->priv->current_online_state = FALSE;
+ meta_backend->priv->refresh_after_authenticate = FALSE;
+ meta_backend->priv->ever_connected = -1;
+ meta_backend->priv->connected_writable = -1;
+}
+
+/**
+ * e_cal_meta_backend_get_capabilities:
+ * @meta_backend: an #ECalMetaBackend
+ *
+ * Returns: an #ECalBackend::capabilities property to be used by
+ * the descendant in conjunction to the descendant's capabilities
+ * in the result of e_cal_backend_get_backend_property() with
+ * #CLIENT_BACKEND_PROPERTY_CAPABILITIES.
+ *
+ * Since: 3.26
+ **/
+const gchar *
+e_cal_meta_backend_get_capabilities (ECalMetaBackend *meta_backend)
+{
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), NULL);
+
+ return CAL_STATIC_CAPABILITY_REFRESH_SUPPORTED ","
+ CAL_STATIC_CAPABILITY_BULK_ADDS ","
+ CAL_STATIC_CAPABILITY_BULK_MODIFIES ","
+ CAL_STATIC_CAPABILITY_BULK_REMOVES;
+}
+
+/**
+ * e_cal_meta_backend_set_ever_connected:
+ * @meta_backend: an #ECalMetaBackend
+ * @value: value to set
+ *
+ * Sets whether the @meta_backend ever made a successful connection
+ * to its destination.
+ *
+ * This is used by the @meta_backend itself, during the opening phase,
+ * when it had not been connected yet, then it does so immediately, to
+ * eventually report settings error easily.
+ *
+ * Since: 3.26
+ **/
+void
+e_cal_meta_backend_set_ever_connected (ECalMetaBackend *meta_backend,
+ gboolean value)
+{
+ ECalCache *cal_cache;
+
+ g_return_if_fail (E_IS_CAL_META_BACKEND (meta_backend));
+
+ if ((value ? 1 : 0) == meta_backend->priv->ever_connected)
+ return;
+
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ meta_backend->priv->ever_connected = value ? 1 : 0;
+ e_cache_set_key_int (E_CACHE (cal_cache), ECMB_KEY_EVER_CONNECTED,
meta_backend->priv->ever_connected, NULL);
+ g_clear_object (&cal_cache);
+}
+
+/**
+ * e_cal_meta_backend_get_ever_connected:
+ * @meta_backend: an #ECalMetaBackend
+ *
+ * Returns: Whether the @meta_backend ever made a successful connection
+ * to its destination.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_get_ever_connected (ECalMetaBackend *meta_backend)
+{
+ gboolean result;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+
+ if (meta_backend->priv->ever_connected == -1) {
+ ECalCache *cal_cache;
+
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ result = e_cache_get_key_int (E_CACHE (cal_cache), ECMB_KEY_EVER_CONNECTED, NULL) == 1;
+ g_clear_object (&cal_cache);
+
+ meta_backend->priv->ever_connected = result ? 1 : 0;
+ } else {
+ result = meta_backend->priv->ever_connected == 1;
+ }
+
+ return result;
+}
+
+/**
+ * e_cal_meta_backend_set_connected_writable:
+ * @meta_backend: an #ECalMetaBackend
+ * @value: value to set
+ *
+ * Sets whether the @meta_backend connected to a writable destination.
+ * This value has meaning only if e_cal_meta_backend_get_ever_connected()
+ * is %TRUE.
+ *
+ * This is used by the @meta_backend itself, during the opening phase,
+ * to set the backend writable or not also in the offline mode.
+ *
+ * Since: 3.26
+ **/
+void
+e_cal_meta_backend_set_connected_writable (ECalMetaBackend *meta_backend,
+ gboolean value)
+{
+ ECalCache *cal_cache;
+
+ g_return_if_fail (E_IS_CAL_META_BACKEND (meta_backend));
+
+ if ((value ? 1 : 0) == meta_backend->priv->connected_writable)
+ return;
+
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ meta_backend->priv->connected_writable = value ? 1 : 0;
+ e_cache_set_key_int (E_CACHE (cal_cache), ECMB_KEY_CONNECTED_WRITABLE,
meta_backend->priv->connected_writable, NULL);
+ g_clear_object (&cal_cache);
+}
+
+/**
+ * e_cal_meta_backend_get_connected_writable:
+ * @meta_backend: an #ECalMetaBackend
+ *
+ * This value has meaning only if e_cal_meta_backend_get_ever_connected()
+ * is %TRUE.
+ *
+ * Returns: Whether the @meta_backend connected to a writable destination.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_get_connected_writable (ECalMetaBackend *meta_backend)
+{
+ gboolean result;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+
+ if (meta_backend->priv->connected_writable == -1) {
+ ECalCache *cal_cache;
+
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ result = e_cache_get_key_int (E_CACHE (cal_cache), ECMB_KEY_CONNECTED_WRITABLE, NULL) == 1;
+ g_clear_object (&cal_cache);
+
+ meta_backend->priv->connected_writable = result ? 1 : 0;
+ } else {
+ result = meta_backend->priv->connected_writable == 1;
+ }
+
+ return result;
+}
+
+static void
+ecmb_cache_revision_changed_cb (ECache *cache,
+ gpointer user_data)
+{
+ ECalMetaBackend *meta_backend = user_data;
+ gchar *revision;
+
+ g_return_if_fail (E_IS_CACHE (cache));
+ g_return_if_fail (E_IS_CAL_META_BACKEND (meta_backend));
+
+ revision = e_cache_dup_revision (cache);
+ if (revision) {
+ e_cal_backend_notify_property_changed (E_CAL_BACKEND (meta_backend),
+ CAL_BACKEND_PROPERTY_REVISION, revision);
+ g_free (revision);
+ }
+}
+
+/**
+ * e_cal_meta_backend_set_cache:
+ * @meta_backend: an #ECalMetaBackend
+ * @cache: an #ECalCache to use
+ *
+ * Sets the @cache as the cache to be used by the @meta_backend.
+ * By default, a cache.db in ECalBackend::cache-dir is created
+ * in the constructed method. This function can be used to override
+ * the default.
+ *
+ * Note the @meta_backend adds its own reference to the @cache.
+ *
+ * Since: 3.26
+ **/
+void
+e_cal_meta_backend_set_cache (ECalMetaBackend *meta_backend,
+ ECalCache *cache)
+{
+ g_return_if_fail (E_IS_CAL_META_BACKEND (meta_backend));
+ g_return_if_fail (E_IS_CAL_CACHE (cache));
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ if (meta_backend->priv->cache == cache) {
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+ return;
+ }
+
+ g_clear_error (&meta_backend->priv->create_cache_error);
+
+ if (meta_backend->priv->cache) {
+ g_signal_handler_disconnect (meta_backend->priv->cache,
+ meta_backend->priv->revision_changed_id);
+ }
+
+ g_clear_object (&meta_backend->priv->cache);
+ meta_backend->priv->cache = g_object_ref (cache);
+
+ meta_backend->priv->revision_changed_id = g_signal_connect_object (meta_backend->priv->cache,
+ "revision-changed", G_CALLBACK (ecmb_cache_revision_changed_cb), meta_backend, 0);
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ g_object_notify (G_OBJECT (meta_backend), "cache");
+}
+
+/**
+ * e_cal_meta_backend_ref_cache:
+ * @meta_backend: an #ECalMetaBackend
+ *
+ * Returns: (transfer full): Referenced #ECalCache, which is used by @meta_backend.
+ * Unref it with g_object_unref() when no longer needed.
+ *
+ * Since: 3.26
+ **/
+ECalCache *
+e_cal_meta_backend_ref_cache (ECalMetaBackend *meta_backend)
+{
+ ECalCache *cache;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), NULL);
+
+ g_mutex_lock (&meta_backend->priv->property_lock);
+
+ if (meta_backend->priv->cache)
+ cache = g_object_ref (meta_backend->priv->cache);
+ else
+ cache = NULL;
+
+ g_mutex_unlock (&meta_backend->priv->property_lock);
+
+ return cache;
+}
+
+static gint
+sort_master_first_cb (gconstpointer a,
+ gconstpointer b)
+{
+ icalcomponent *ca, *cb;
+
+ ca = e_cal_component_get_icalcomponent ((ECalComponent *) a);
+ cb = e_cal_component_get_icalcomponent ((ECalComponent *) b);
+
+ if (!ca) {
+ if (!cb)
+ return 0;
+ else
+ return -1;
+ } else if (!cb) {
+ return 1;
+ }
+
+ return icaltime_compare (icalcomponent_get_recurrenceid (ca), icalcomponent_get_recurrenceid (cb));
+}
+
+typedef struct {
+ ECalCache *cache;
+ gboolean replace_tzid_with_location;
+ icalcomponent *vcalendar;
+ icalcomponent *icalcomp;
+} ForeachTzidData;
+
+static void
+add_timezone_cb (icalparameter *param,
+ gpointer user_data)
+{
+ icaltimezone *tz;
+ const gchar *tzid;
+ icalcomponent *vtz_comp;
+ ForeachTzidData *f_data = user_data;
+
+ tzid = icalparameter_get_tzid (param);
+ if (!tzid)
+ return;
+
+ tz = icalcomponent_get_timezone (f_data->vcalendar, tzid);
+ if (tz)
+ return;
+
+ tz = icalcomponent_get_timezone (f_data->icalcomp, tzid);
+ if (!tz)
+ tz = icaltimezone_get_builtin_timezone_from_tzid (tzid);
+ if (!tz && f_data->cache)
+ tz = e_timezone_cache_get_timezone (E_TIMEZONE_CACHE (f_data->cache), tzid);
+ if (!tz)
+ return;
+
+ if (f_data->replace_tzid_with_location) {
+ const gchar *location;
+
+ location = icaltimezone_get_location (tz);
+ if (location && *location) {
+ icalparameter_set_tzid (param, location);
+ tzid = location;
+
+ if (icalcomponent_get_timezone (f_data->vcalendar, tzid))
+ return;
+ }
+ }
+
+ vtz_comp = icaltimezone_get_component (tz);
+
+ if (vtz_comp) {
+ icalcomponent *clone = icalcomponent_new_clone (vtz_comp);
+
+ if (f_data->replace_tzid_with_location) {
+ icalproperty *prop;
+
+ prop = icalcomponent_get_first_property (clone, ICAL_TZID_PROPERTY);
+ if (prop) {
+ icalproperty_set_tzid (prop, tzid);
+ }
+ }
+
+ icalcomponent_add_component (f_data->vcalendar, clone);
+ }
+}
+
+/**
+ * e_cal_meta_backend_merge_instances:
+ * @meta_backend: an #ECalMetaBackend
+ * @instances: (element-type ECalComponent): component instances to merge
+ * @replace_tzid_with_location: whether to replace TZID-s with locations
+ *
+ * Merges all the instances provided in @instances list into one VCALENDAR
+ * object, which would eventually contain also all the used timezones.
+ * The @instances list should contain the master object and eventually all
+ * the detached instances for one component (they all have the same UID).
+ *
+ * Any TZID property parameters can be replaced with corresponding timezone
+ * location, which will not influence the timezone itself.
+ *
+ * Returns: (transfer full): an #icalcomponent containing a VCALENDAR
+ * component which consists of all the given instances. Free
+ * the returned pointer with icalcomponent_free() when no longer needed.
+ *
+ * See: e_cal_meta_backend_save_component_sync()
+ *
+ * Since: 3.26
+ **/
+icalcomponent *
+e_cal_meta_backend_merge_instances (ECalMetaBackend *meta_backend,
+ const GSList *instances,
+ gboolean replace_tzid_with_location)
+{
+ ECalCache *cal_cache;
+ ForeachTzidData f_data;
+ icalcomponent *vcalendar;
+ GSList *link, *sorted;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), NULL);
+ g_return_val_if_fail (instances != NULL, NULL);
+
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ g_return_val_if_fail (cal_cache != NULL, NULL);
+
+ sorted = g_slist_sort (g_slist_copy ((GSList *) instances), sort_master_first_cb);
+
+ vcalendar = e_cal_util_new_top_level ();
+
+ f_data.cache = cal_cache;
+ f_data.replace_tzid_with_location = replace_tzid_with_location;
+ f_data.vcalendar = vcalendar;
+
+ for (link = sorted; link; link = g_slist_next (link)) {
+ ECalComponent *comp = link->data;
+ icalcomponent *icalcomp;
+
+ if (!E_IS_CAL_COMPONENT (comp)) {
+ g_warn_if_reached ();
+ continue;
+ }
+
+ icalcomp = icalcomponent_new_clone (e_cal_component_get_icalcomponent (comp));
+ icalcomponent_add_component (vcalendar, icalcomp);
+
+ f_data.icalcomp = icalcomp;
+
+ icalcomponent_foreach_tzid (icalcomp, add_timezone_cb, &f_data);
+ }
+
+ g_clear_object (&f_data.cache);
+ g_slist_free (sorted);
+
+ return vcalendar;
+}
+
+static void
+ecmb_remove_all_but_filename_parameter (icalproperty *prop)
+{
+ icalparameter *param;
+
+ g_return_if_fail (prop != NULL);
+
+ while (param = icalproperty_get_first_parameter (prop, ICAL_ANY_PARAMETER), param) {
+ if (icalparameter_isa (param) == ICAL_FILENAME_PARAMETER) {
+ param = icalproperty_get_next_parameter (prop, ICAL_ANY_PARAMETER);
+ if (!param)
+ break;
+ }
+
+ icalproperty_remove_parameter_by_ref (prop, param);
+ }
+}
+
+/**
+ * e_cal_meta_backend_inline_local_attachments_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @component: an icalcomponent to work with
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Changes all URL attachments which point to a local file in @component
+ * to inline attachments, aka adds the file content into the @component.
+ * It also populates FILENAME parameter on the attachment.
+ * This is called automatically before e_cal_meta_backend_save_component_sync().
+ *
+ * The reverse operation is e_cal_meta_backend_store_inline_attachments_sync().
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_inline_local_attachments_sync (ECalMetaBackend *meta_backend,
+ icalcomponent *component,
+ GCancellable *cancellable,
+ GError **error)
+{
+ icalproperty *prop;
+ const gchar *uid;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (component != NULL, FALSE);
+
+ uid = icalcomponent_get_uid (component);
+
+ for (prop = icalcomponent_get_first_property (component, ICAL_ATTACH_PROPERTY);
+ prop && success;
+ prop = icalcomponent_get_next_property (component, ICAL_ATTACH_PROPERTY)) {
+ icalattach *attach;
+
+ attach = icalproperty_get_attach (prop);
+ if (icalattach_get_is_url (attach)) {
+ const gchar *url;
+
+ url = icalattach_get_url (attach);
+ if (g_str_has_prefix (url, LOCAL_PREFIX)) {
+ GFile *file;
+ gchar *basename;
+ gchar *content;
+ gsize len;
+
+ file = g_file_new_for_uri (url);
+ basename = g_file_get_basename (file);
+ if (g_file_load_contents (file, cancellable, &content, &len, NULL, error)) {
+ icalattach *new_attach;
+ icalparameter *param;
+ gchar *base64;
+
+ base64 = g_base64_encode ((const guchar *) content, len);
+ new_attach = icalattach_new_from_data (base64, NULL, NULL);
+ g_free (content);
+ g_free (base64);
+
+ ecmb_remove_all_but_filename_parameter (prop);
+
+ icalproperty_set_attach (prop, new_attach);
+ icalattach_unref (new_attach);
+
+ param = icalparameter_new_value (ICAL_VALUE_BINARY);
+ icalproperty_add_parameter (prop, param);
+
+ param = icalparameter_new_encoding (ICAL_ENCODING_BASE64);
+ icalproperty_add_parameter (prop, param);
+
+ /* Preserve existing FILENAME parameter */
+ if (!icalproperty_get_first_parameter (prop,
ICAL_FILENAME_PARAMETER)) {
+ const gchar *use_filename = basename;
+
+ /* generated filename by Evolution */
+ if (uid && g_str_has_prefix (use_filename, uid) &&
+ use_filename[strlen (uid)] == '-') {
+ use_filename += strlen (uid) + 1;
+ }
+
+ param = icalparameter_new_filename (use_filename);
+ icalproperty_add_parameter (prop, param);
+ }
+ } else {
+ success = FALSE;
+ }
+
+ g_object_unref (file);
+ g_free (basename);
+ }
+ }
+ }
+
+ return success;
+}
+
+/**
+ * e_cal_meta_backend_store_inline_attachments_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @component: an icalcomponent to work with
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Changes all inline attachments to URL attachments in @component, which
+ * will point to a local file instead. The function expects FILENAME parameter
+ * to be set on the attachment as the file name of it.
+ * This is called automatically after e_cal_meta_backend_load_component_sync().
+ *
+ * The reverse operation is e_cal_meta_backend_inline_local_attachments_sync().
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_store_inline_attachments_sync (ECalMetaBackend *meta_backend,
+ icalcomponent *component,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gint fileindex;
+ icalproperty *prop;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (component != NULL, FALSE);
+
+ for (prop = icalcomponent_get_first_property (component, ICAL_ATTACH_PROPERTY), fileindex = 0;
+ prop && success;
+ prop = icalcomponent_get_next_property (component, ICAL_ATTACH_PROPERTY), fileindex++) {
+ icalattach *attach;
+
+ attach = icalproperty_get_attach (prop);
+ if (!icalattach_get_is_url (attach)) {
+ icalparameter *param;
+ const gchar *basename;
+ gsize len = -1;
+ gchar *decoded = NULL;
+ gchar *local_filename;
+
+ param = icalproperty_get_first_parameter (prop, ICAL_FILENAME_PARAMETER);
+ basename = param ? icalparameter_get_filename (param) : NULL;
+ if (!basename || !*basename)
+ basename = _("attachment.dat");
+
+ local_filename = e_cal_backend_create_cache_filename (E_CAL_BACKEND (meta_backend),
icalcomponent_get_uid (component), basename, fileindex);
+
+ if (local_filename) {
+ const gchar *content;
+
+ content = (const gchar *) icalattach_get_data (attach);
+ decoded = (gchar *) g_base64_decode (content, &len);
+
+ if (g_file_set_contents (local_filename, decoded, len, error)) {
+ icalattach *new_attach;
+ gchar *url;
+
+ ecmb_remove_all_but_filename_parameter (prop);
+
+ url = g_filename_to_uri (local_filename, NULL, NULL);
+ new_attach = icalattach_new_from_url (url);
+
+ icalproperty_set_attach (prop, new_attach);
+
+ icalattach_unref (new_attach);
+ g_free (url);
+ } else {
+ success = FALSE;
+ }
+
+ g_free (decoded);
+ }
+
+ g_free (local_filename);
+ }
+ }
+
+ return success;
+}
+
+/**
+ * e_cal_meta_backend_gather_timezones_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @vcalendar: a VCALENDAR icalcomponent
+ * @remove_existing: whether to remove any existing first
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Extracts all VTIMEZONE components from the @vcalendar and adds them
+ * to the cache, thus they are available when needed. The function does
+ * nothing when the @vcalendar doesn't hold a VCALENDAR component.
+ *
+ * Set the @remove_existing argument to %TRUE to remove all cached timezones
+ * first and then add the existing in the @vcalendar, or set it to %FALSE
+ * to preserver existing timezones and merge them with those in @vcalendar.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_gather_timezones_sync (ECalMetaBackend *meta_backend,
+ icalcomponent *vcalendar,
+ gboolean remove_existing,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalCache *cal_cache;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (vcalendar != NULL, FALSE);
+
+ if (icalcomponent_isa (vcalendar) != ICAL_VCALENDAR_COMPONENT)
+ return TRUE;
+
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ g_return_val_if_fail (cal_cache != NULL, FALSE);
+
+ e_cache_lock (E_CACHE (cal_cache), E_CACHE_LOCK_WRITE);
+
+ if (remove_existing)
+ success = e_cal_cache_remove_timezones (cal_cache, cancellable, error);
+
+ if (success)
+ ecmb_gather_timezones (meta_backend, E_TIMEZONE_CACHE (cal_cache), vcalendar);
+
+ e_cache_unlock (E_CACHE (cal_cache), success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+ g_object_unref (cal_cache);
+
+ return TRUE;
+}
+
+/**
+ * e_cal_meta_backend_empty_cache_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Empties the local cache by removing all known components from it
+ * and notifies about such removal any opened views. It removes also
+ * all known time zones.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_empty_cache_sync (ECalMetaBackend *meta_backend,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalBackend *cal_backend;
+ ECalCache *cal_cache;
+ GSList *ids = NULL, *link;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ g_return_val_if_fail (cal_cache != NULL, FALSE);
+
+ e_cache_lock (E_CACHE (cal_cache), E_CACHE_LOCK_WRITE);
+
+ cal_backend = E_CAL_BACKEND (meta_backend);
+
+ success = e_cal_cache_search_ids (cal_cache, NULL, &ids, cancellable, error);
+ if (success)
+ success = e_cache_remove_all (E_CACHE (cal_cache), cancellable, error);
+
+ e_cache_unlock (E_CACHE (cal_cache), success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+ g_object_unref (cal_cache);
+
+ if (success) {
+ for (link = ids; link; link = g_slist_next (link)) {
+ ECalComponentId *id = link->data;
+
+ if (!id)
+ continue;
+
+ e_cal_backend_notify_component_removed (cal_backend, id, NULL, NULL);
+ }
+ }
+
+ g_slist_free_full (ids, (GDestroyNotify) e_cal_component_free_id);
+
+ return success;
+}
+
+/**
+ * e_cal_meta_backend_connect_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @credentials: (nullable): an #ENamedParameters with previously used credentials, or %NULL
+ * @out_auth_result: (out): an #ESourceAuthenticationResult with an authentication result
+ * @out_certificate_pem: (out) (transfer full): a PEM encoded certificate on failure, or %NULL
+ * @out_certificate_errors: (out): a #GTlsCertificateFlags on failure, or 0
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * This is called always before any operation which requires a connection
+ * to the remote side. It can fail with an #E_CLIENT_ERROR_REPOSITORY_OFFLINE
+ * error to indicate that the remote side cannot be currently reached. Other
+ * errors are propagated to the caller/client side. This method is not called
+ * when the backend is offline.
+ *
+ * The descendant should also call e_cal_backend_set_writable() after successful
+ * connect to the remote side. This value is stored for later use, when being
+ * opened offline.
+ *
+ * The @credentials parameter consists of the previously used credentials.
+ * It's always %NULL with the first connection attempt. To get the credentials,
+ * just set the @out_auth_result to %E_SOURCE_AUTHENTICATION_REQUIRED for
+ * the first time and the function will be called again once the credentials
+ * are available. See the documentation of #ESourceAuthenticationResult for
+ * other available results.
+ *
+ * The out parameters are passed to e_backend_schedule_credentials_required()
+ * and are ignored when the descendant returns %TRUE, aka they are used
+ * only if the connection fails. The @out_certificate_pem and @out_certificate_errors
+ * should be used together and they can be left untouched if the failure reason was
+ * not related to certificate. Use @out_auth_result %E_SOURCE_AUTHENTICATION_UNKNOWN
+ * to indicate other error than @credentials error, otherwise the @error is used
+ * according to @out_auth_result value.
+ *
+ * It is mandatory to implement this virtual method by the descendant.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_connect_sync (ECalMetaBackend *meta_backend,
+ const ENamedParameters *credentials,
+ ESourceAuthenticationResult *out_auth_result,
+ gchar **out_certificate_pem,
+ GTlsCertificateFlags *out_certificate_errors,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalMetaBackendClass *klass;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+
+ klass = E_CAL_META_BACKEND_GET_CLASS (meta_backend);
+ g_return_val_if_fail (klass != NULL, FALSE);
+ g_return_val_if_fail (klass->connect_sync != NULL, FALSE);
+
+ return klass->connect_sync (meta_backend, credentials, out_auth_result, out_certificate_pem,
out_certificate_errors, cancellable, error);
+}
+
+/**
+ * e_cal_meta_backend_disconnect_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * This is called when the backend goes into offline mode or
+ * when the disconnect is required. The implementation should
+ * not report any error when it is called and the @meta_backend
+ * is not connected.
+ *
+ * It is mandatory to implement this virtual method by the descendant.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_disconnect_sync (ECalMetaBackend *meta_backend,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalMetaBackendClass *klass;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+
+ klass = E_CAL_META_BACKEND_GET_CLASS (meta_backend);
+ g_return_val_if_fail (klass != NULL, FALSE);
+ g_return_val_if_fail (klass->disconnect_sync != NULL, FALSE);
+
+ return klass->disconnect_sync (meta_backend, cancellable, error);
+}
+
+/**
+ * e_cal_meta_backend_get_changes_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @last_sync_tag: (nullable): optional sync tag from the last check
+ * @is_repeat: set to %TRUE when this is the repeated call
+ * @out_new_sync_tag: (out) (transfer full): new sync tag to store on success
+ * @out_repeat: (out): whether to repeat this call again; default is %FALSE
+ * @out_created_objects: (out) (element-type ECalMetaBackendInfo) (transfer full):
+ * a #GSList of #ECalMetaBackendInfo object infos which had been created since
+ * the last check
+ * @out_modified_objects: (out) (element-type ECalMetaBackendInfo) (transfer full):
+ * a #GSList of #ECalMetaBackendInfo object infos which had been modified since
+ * the last check
+ * @out_removed_objects: (out) (element-type ECalMetaBackendInfo) (transfer full):
+ * a #GSList of #ECalMetaBackendInfo object infos which had been removed since
+ * the last check
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gathers the changes since the last check which had been done
+ * on the remote side.
+ *
+ * The @last_sync_tag can be used as a tag of the last check. This can be %NULL,
+ * when there was no previous call or when the descendant doesn't store any
+ * such tags. The @out_new_sync_tag can be populated with a value to be stored
+ * and used the next time.
+ *
+ * The @out_repeat can be set to %TRUE when the descendant didn't finish
+ * read of all the changes. In that case the @meta_backend calls this
+ * function again with the @out_new_sync_tag as the @last_sync_tag, but also
+ * notifies about the found changes immediately. The @is_repeat is set
+ * to %TRUE as well in this case, otherwise it's %FALSE.
+ *
+ * The descendant can populate also ECalMetaBackendInfo::object of
+ * the @out_created_objects and @out_modified_objects, if known, in which
+ * case this will be used instead of loading it with e_cal_meta_backend_load_component_sync().
+ *
+ * It is optional to implement this virtual method by the descendant.
+ * The default implementation calls e_cal_meta_backend_list_existing_sync()
+ * and then compares the list with the current content of the local cache
+ * and populates the respective lists appropriately.
+ *
+ * Each output #GSList should be freed with
+ * g_slist_free_full (objects, e_cal_meta_backend_info_free);
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_get_changes_sync (ECalMetaBackend *meta_backend,
+ const gchar *last_sync_tag,
+ gboolean is_repeat,
+ gchar **out_new_sync_tag,
+ gboolean *out_repeat,
+ GSList **out_created_objects,
+ GSList **out_modified_objects,
+ GSList **out_removed_objects,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalMetaBackendClass *klass;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (out_new_sync_tag != NULL, FALSE);
+ g_return_val_if_fail (out_repeat != NULL, FALSE);
+ g_return_val_if_fail (out_created_objects != NULL, FALSE);
+ g_return_val_if_fail (out_created_objects != NULL, FALSE);
+ g_return_val_if_fail (out_modified_objects != NULL, FALSE);
+ g_return_val_if_fail (out_removed_objects != NULL, FALSE);
+
+ klass = E_CAL_META_BACKEND_GET_CLASS (meta_backend);
+ g_return_val_if_fail (klass != NULL, FALSE);
+ g_return_val_if_fail (klass->get_changes_sync != NULL, FALSE);
+
+ return klass->get_changes_sync (meta_backend,
+ last_sync_tag,
+ is_repeat,
+ out_new_sync_tag,
+ out_repeat,
+ out_created_objects,
+ out_modified_objects,
+ out_removed_objects,
+ cancellable,
+ error);
+}
+
+/**
+ * e_cal_meta_backend_list_existing_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @out_new_sync_tag: (out) (transfer full): optional return location for a new sync tag
+ * @out_existing_objects: (out) (element-type ECalMetaBackendInfo) (transfer full):
+ * a #GSList of #ECalMetaBackendInfo object infos which are stored on the remote side
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Used to get list of all existing objects on the remote side. The descendant
+ * can optionally provide @out_new_sync_tag, which will be stored on success, if
+ * not %NULL. The descendant can populate also ECalMetaBackendInfo::object of
+ * the @out_existing_objects, if known, in which case this will be used instead
+ * of loading it with e_cal_meta_backend_load_component_sync().
+ *
+ * It is mandatory to implement this virtual method by the descendant, unless
+ * it implements its own get_changes_sync().
+ *
+ * The @out_existing_objects #GSList should be freed with
+ * g_slist_free_full (objects, e_cal_meta_backend_info_free);
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_list_existing_sync (ECalMetaBackend *meta_backend,
+ gchar **out_new_sync_tag,
+ GSList **out_existing_objects,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalMetaBackendClass *klass;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (out_existing_objects != NULL, FALSE);
+
+ klass = E_CAL_META_BACKEND_GET_CLASS (meta_backend);
+ g_return_val_if_fail (klass != NULL, FALSE);
+ g_return_val_if_fail (klass->list_existing_sync != NULL, FALSE);
+
+ return klass->list_existing_sync (meta_backend, out_new_sync_tag, out_existing_objects, cancellable,
error);
+}
+
+/**
+ * e_cal_meta_backend_load_component_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @uid: a component UID
+ * @extra: (nullable): optional extra data stored with the component, or %NULL
+ * @out_component: (out) (transfer full): a loaded component, as icalcomponent
+ * @out_extra: (out) (transfer full): an extra data to store to #ECalCache with this component
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Loads a component from the remote side. Any detached instances should be
+ * returned together with the master object. The @out_component can be either
+ * a VCALENDAR component, which would contain the master object and all of
+ * its detached instances, eventually also used time zones, or the requested
+ * component of type VEVENT, VJOURNAL or VTODO.
+ *
+ * It is mandatory to implement this virtual method by the descendant.
+ *
+ * The returned @out_component should be freed with icalcomponent_free(),
+ * when no longer needed.
+ *
+ * The returned @out_extra should be freed with g_free(), when no longer
+ * needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_load_component_sync (ECalMetaBackend *meta_backend,
+ const gchar *uid,
+ const gchar *extra,
+ icalcomponent **out_component,
+ gchar **out_extra,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalMetaBackendClass *klass;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (out_component != NULL, FALSE);
+ g_return_val_if_fail (out_extra != NULL, FALSE);
+
+ klass = E_CAL_META_BACKEND_GET_CLASS (meta_backend);
+ g_return_val_if_fail (klass != NULL, FALSE);
+ g_return_val_if_fail (klass->load_component_sync != NULL, FALSE);
+
+ return klass->load_component_sync (meta_backend, uid, extra, out_component, out_extra, cancellable,
error);
+}
+
+/**
+ * e_cal_meta_backend_save_component_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @overwrite_existing: %TRUE when can overwrite existing components, %FALSE otherwise
+ * @conflict_resolution: one of #EConflictResolution, what to do on conflicts
+ * @instances: (element-type ECalComponent): instances of the component to save
+ * @extra: (nullable): extra data saved with the components in an #ECalCache
+ * @out_new_uid: (out) (transfer full): return location for the UID of the saved component
+ * @out_new_extra: (out) (transfer full): return location for the extra data to store with the component
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Saves one component into the remote side. The @instances contain the master
+ * object and all the detached instances of the same component (all have the same UID).
+ * When the @overwrite_existing is %TRUE, then the descendant can overwrite an object
+ * with the same UID on the remote side (usually used for modify). The @conflict_resolution
+ * defines what to do when the remote side had made any changes to the object since
+ * the last update.
+ *
+ * The descendant can use e_cal_meta_backend_merge_instances() to merge
+ * the instances into one VCALENDAR component, which will contain also
+ * used time zones.
+ *
+ * The components in @instances have already converted locally stored attachments
+ * into inline attachments, thus it's not needed to call
+ * e_cal_meta_backend_inline_local_attachments_sync() by the descendant.
+ *
+ * The @out_new_uid can be populated with a UID of the saved component as the server
+ * assigned it to it. This UID, if set, is loaded from the remote side afterwards,
+ * also to see whether any changes had been made to the component by the remote side.
+ *
+ * The @out_new_extra can be populated with a new extra data to save with the component.
+ * Left it %NULL, to keep the same value as the @extra.
+ *
+ * The descendant can use an #E_CLIENT_ERROR_OUT_OF_SYNC error to indicate that
+ * the save failed due to made changes on the remote side, and let the @meta_backend
+ * to resolve this conflict based on the @conflict_resolution on its own.
+ * The #E_CLIENT_ERROR_OUT_OF_SYNC error should not be used when the descendant
+ * is able to resolve the conflicts itself.
+ *
+ * It is mandatory to implement this virtual method by the writable descendant.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_save_component_sync (ECalMetaBackend *meta_backend,
+ gboolean overwrite_existing,
+ EConflictResolution conflict_resolution,
+ const GSList *instances,
+ const gchar *extra,
+ gchar **out_new_uid,
+ gchar **out_new_extra,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalMetaBackendClass *klass;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (instances != NULL, FALSE);
+ g_return_val_if_fail (out_new_uid != NULL, FALSE);
+ g_return_val_if_fail (out_new_extra != NULL, FALSE);
+
+ klass = E_CAL_META_BACKEND_GET_CLASS (meta_backend);
+ g_return_val_if_fail (klass != NULL, FALSE);
+
+ if (!klass->save_component_sync) {
+ g_propagate_error (error, e_data_cal_create_error (NotSupported, NULL));
+ return FALSE;
+ }
+
+ return klass->save_component_sync (meta_backend,
+ overwrite_existing,
+ conflict_resolution,
+ instances,
+ extra,
+ out_new_uid,
+ out_new_extra,
+ cancellable,
+ error);
+}
+
+/**
+ * e_cal_meta_backend_remove_component_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @conflict_resolution: an #EConflictResolution to use
+ * @uid: a component UID
+ * @extra: (nullable): extra data being saved with the component in the local cache, or %NULL
+ * @object: (nullable): corresponding iCalendar object, as stored in the local cache, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Removes a component from the remote side, with all its detached instances.
+ * The @object is not %NULL when it's removing locally deleted object
+ * in offline mode. Being it %NULL, the descendant can obtain the object
+ * from the #ECalCache.
+ *
+ * It is mandatory to implement this virtual method by the writable descendant.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_remove_component_sync (ECalMetaBackend *meta_backend,
+ EConflictResolution conflict_resolution,
+ const gchar *uid,
+ const gchar *extra,
+ const gchar *object,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalMetaBackendClass *klass;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ klass = E_CAL_META_BACKEND_GET_CLASS (meta_backend);
+ g_return_val_if_fail (klass != NULL, FALSE);
+
+ if (!klass->remove_component_sync) {
+ g_propagate_error (error, e_data_cal_create_error (NotSupported, NULL));
+ return FALSE;
+ }
+
+ return klass->remove_component_sync (meta_backend, conflict_resolution, uid, extra, object,
cancellable, error);
+}
+
+/**
+ * e_cal_meta_backend_search_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @expr: (nullable): a search expression, or %NULL
+ * @out_icalstrings: (out) (transfer full) (element-type utf8): return location for the found components as
iCal strings
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Searches @meta_backend with given expression @expr and returns
+ * found components as a #GSList of iCal strings @out_icalstrings.
+ * Free the returned @out_icalstrings with g_slist_free_full (icalstrings, g_free);
+ * when no longer needed.
+ * When the @expr is %NULL, all objects are returned. To get
+ * #ECalComponent-s instead, call e_cal_meta_backend_search_components_sync().
+ *
+ * It is optional to implement this virtual method by the descendant.
+ * The default implementation searches @meta_backend's cache. It's also
+ * not required to be online for searching, thus @meta_backend doesn't
+ * ensure it.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_search_sync (ECalMetaBackend *meta_backend,
+ const gchar *expr,
+ GSList **out_icalstrings,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalMetaBackendClass *klass;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (out_icalstrings != NULL, FALSE);
+
+ klass = E_CAL_META_BACKEND_GET_CLASS (meta_backend);
+ g_return_val_if_fail (klass != NULL, FALSE);
+ g_return_val_if_fail (klass->search_sync != NULL, FALSE);
+
+ return klass->search_sync (meta_backend, expr, out_icalstrings, cancellable, error);
+}
+
+/**
+ * e_cal_meta_backend_search_components_sync:
+ * @meta_backend: an #ECalMetaBackend
+ * @expr: (nullable): a search expression, or %NULL
+ * @out_components: (out) (transfer full) (element-type ECalComponent): return location for the found
#ECalComponent-s
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Searches @meta_backend with given expression @expr and returns
+ * found components as a #GSList of #ECalComponont-s @out_components.
+ * Free the returned @out_components with g_slist_free_full (components, g_object_unref);
+ * when no longer needed.
+ * When the @expr is %NULL, all objects are returned. To get iCal
+ * strings instead, call e_cal_meta_backend_search_sync().
+ *
+ * It is optional to implement this virtual method by the descendant.
+ * The default implementation searches @meta_backend's cache. It's also
+ * not required to be online for searching, thus @meta_backend doesn't
+ * ensure it.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_search_components_sync (ECalMetaBackend *meta_backend,
+ const gchar *expr,
+ GSList **out_components,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalMetaBackendClass *klass;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+ g_return_val_if_fail (out_components != NULL, FALSE);
+
+ klass = E_CAL_META_BACKEND_GET_CLASS (meta_backend);
+ g_return_val_if_fail (klass != NULL, FALSE);
+ g_return_val_if_fail (klass->search_components_sync != NULL, FALSE);
+
+ return klass->search_components_sync (meta_backend, expr, out_components, cancellable, error);
+}
+
+/**
+ * e_cal_meta_backend_requires_reconnect:
+ * @meta_backend: an #ECalMetaBackend
+ *
+ * Determines, whether current source content requires reconnect of the backend.
+ *
+ * It is optional to implement this virtual method by the descendant. The default
+ * implementation compares %E_SOURCE_EXTENSION_AUTHENTICATION and
+ * %E_SOURCE_EXTENSION_WEBDAV_BACKEND, if existing in the source,
+ * with the values after the last successful connect and returns
+ * %TRUE when they changed. It always return %TRUE when there was
+ * no successful connect done yet.
+ *
+ * Returns: %TRUE, when reconnect is required, %FALSE otherwise.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cal_meta_backend_requires_reconnect (ECalMetaBackend *meta_backend)
+{
+ ECalMetaBackendClass *klass;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND (meta_backend), FALSE);
+
+ klass = E_CAL_META_BACKEND_GET_CLASS (meta_backend);
+ g_return_val_if_fail (klass != NULL, FALSE);
+ g_return_val_if_fail (klass->requires_reconnect != NULL, FALSE);
+
+ return klass->requires_reconnect (meta_backend);
+}
diff --git a/src/calendar/libedata-cal/e-cal-meta-backend.h b/src/calendar/libedata-cal/e-cal-meta-backend.h
new file mode 100644
index 0000000..5f97093
--- /dev/null
+++ b/src/calendar/libedata-cal/e-cal-meta-backend.h
@@ -0,0 +1,282 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#if !defined (__LIBEDATA_CAL_H_INSIDE__) && !defined (LIBEDATA_CAL_COMPILATION)
+#error "Only <libedata-cal/libedata-cal.h> should be included directly."
+#endif
+
+#ifndef E_CAL_META_BACKEND_H
+#define E_CAL_META_BACKEND_H
+
+#include <libebackend/libebackend.h>
+#include <libedata-cal/e-cal-backend-sync.h>
+#include <libedata-cal/e-cal-cache.h>
+#include <libecal/libecal.h>
+
+/* Standard GObject macros */
+#define E_TYPE_CAL_META_BACKEND \
+ (e_cal_meta_backend_get_type ())
+#define E_CAL_META_BACKEND(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST \
+ ((obj), E_TYPE_CAL_META_BACKEND, ECalMetaBackend))
+#define E_CAL_META_BACKEND_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_CAST \
+ ((cls), E_TYPE_CAL_META_BACKEND, ECalMetaBackendClass))
+#define E_IS_CAL_META_BACKEND(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE \
+ ((obj), E_TYPE_CAL_META_BACKEND))
+#define E_IS_CAL_META_BACKEND_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_TYPE \
+ ((cls), E_TYPE_CAL_META_BACKEND))
+#define E_CAL_META_BACKEND_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS \
+ ((obj), E_TYPE_CAL_META_BACKEND, ECalMetaBackendClass))
+
+G_BEGIN_DECLS
+
+typedef struct _ECalMetaBackendInfo {
+ gchar *uid;
+ gchar *revision;
+ gchar *object;
+ gchar *extra;
+} ECalMetaBackendInfo;
+
+#define E_TYPE_CAL_META_BACKEND_INFO (e_cal_meta_backend_info_get_type ())
+
+GType e_cal_meta_backend_info_get_type
+ (void) G_GNUC_CONST;
+ECalMetaBackendInfo *
+ e_cal_meta_backend_info_new (const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ const gchar *extra);
+ECalMetaBackendInfo *
+ e_cal_meta_backend_info_copy (const ECalMetaBackendInfo *src);
+void e_cal_meta_backend_info_free (gpointer ptr /* ECalMetaBackendInfo * */);
+
+typedef struct _ECalMetaBackend ECalMetaBackend;
+typedef struct _ECalMetaBackendClass ECalMetaBackendClass;
+typedef struct _ECalMetaBackendPrivate ECalMetaBackendPrivate;
+
+/**
+ * ECalMetaBackend:
+ *
+ * Contains only private data that should be read and manipulated using
+ * the functions below.
+ *
+ * Since: 3.26
+ **/
+struct _ECalMetaBackend {
+ /*< private >*/
+ ECalBackendSync parent;
+ ECalMetaBackendPrivate *priv;
+};
+
+/**
+ * ECalMetaBackendClass:
+ *
+ * Class structure for the #ECalMetaBackend class.
+ *
+ * Since: 3.26
+ */
+struct _ECalMetaBackendClass {
+ /*< private >*/
+ ECalBackendSyncClass parent_class;
+
+ /* Virtual methods */
+ gboolean (* connect_sync) (ECalMetaBackend *meta_backend,
+ const ENamedParameters *credentials,
+ ESourceAuthenticationResult *out_auth_result,
+ gchar **out_certificate_pem,
+ GTlsCertificateFlags *out_certificate_errors,
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (* disconnect_sync) (ECalMetaBackend *meta_backend,
+ GCancellable *cancellable,
+ GError **error);
+
+ gboolean (* get_changes_sync) (ECalMetaBackend *meta_backend,
+ const gchar *last_sync_tag,
+ gboolean is_repeat,
+ gchar **out_new_sync_tag,
+ gboolean *out_repeat,
+ GSList **out_created_objects, /* ECalMetaBackendInfo * */
+ GSList **out_modified_objects, /* ECalMetaBackendInfo * */
+ GSList **out_removed_objects, /* ECalMetaBackendInfo * */
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (* list_existing_sync) (ECalMetaBackend *meta_backend,
+ gchar **out_new_sync_tag,
+ GSList **out_existing_objects, /* ECalMetaBackendInfo * */
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (* load_component_sync) (ECalMetaBackend *meta_backend,
+ const gchar *uid,
+ const gchar *extra,
+ icalcomponent **out_component,
+ gchar **out_extra,
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (* save_component_sync) (ECalMetaBackend *meta_backend,
+ gboolean overwrite_existing,
+ EConflictResolution conflict_resolution,
+ const GSList *instances, /* ECalComponent * */
+ const gchar *extra,
+ gchar **out_new_uid,
+ gchar **out_new_extra,
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (* remove_component_sync)
+ (ECalMetaBackend *meta_backend,
+ EConflictResolution conflict_resolution,
+ const gchar *uid,
+ const gchar *extra,
+ const gchar *object,
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (* search_sync) (ECalMetaBackend *meta_backend,
+ const gchar *expr,
+ GSList **out_icalstrings, /* gchar * */
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (* search_components_sync)
+ (ECalMetaBackend *meta_backend,
+ const gchar *expr,
+ GSList **out_components, /* ECalComponent * */
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (* requires_reconnect) (ECalMetaBackend *meta_backend);
+
+ /* Signals */
+ void (* source_changed) (ECalMetaBackend *meta_backend);
+
+ /* Padding for future expansion */
+ gpointer reserved[10];
+};
+
+GType e_cal_meta_backend_get_type (void) G_GNUC_CONST;
+
+const gchar * e_cal_meta_backend_get_capabilities
+ (ECalMetaBackend *meta_backend);
+void e_cal_meta_backend_set_ever_connected
+ (ECalMetaBackend *meta_backend,
+ gboolean value);
+gboolean e_cal_meta_backend_get_ever_connected
+ (ECalMetaBackend *meta_backend);
+void e_cal_meta_backend_set_connected_writable
+ (ECalMetaBackend *meta_backend,
+ gboolean value);
+gboolean e_cal_meta_backend_get_connected_writable
+ (ECalMetaBackend *meta_backend);
+void e_cal_meta_backend_set_cache (ECalMetaBackend *meta_backend,
+ ECalCache *cache);
+ECalCache * e_cal_meta_backend_ref_cache (ECalMetaBackend *meta_backend);
+icalcomponent * e_cal_meta_backend_merge_instances
+ (ECalMetaBackend *meta_backend,
+ const GSList *instances, /* ECalComponent * */
+ gboolean replace_tzid_with_location);
+gboolean e_cal_meta_backend_inline_local_attachments_sync
+ (ECalMetaBackend *meta_backend,
+ icalcomponent *component,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_meta_backend_store_inline_attachments_sync
+ (ECalMetaBackend *meta_backend,
+ icalcomponent *component,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_meta_backend_gather_timezones_sync
+ (ECalMetaBackend *meta_backend,
+ icalcomponent *vcalendar,
+ gboolean remove_existing,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_meta_backend_empty_cache_sync
+ (ECalMetaBackend *meta_backend,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_meta_backend_connect_sync (ECalMetaBackend *meta_backend,
+ const ENamedParameters *credentials,
+ ESourceAuthenticationResult *out_auth_result,
+ gchar **out_certificate_pem,
+ GTlsCertificateFlags *out_certificate_errors,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_meta_backend_disconnect_sync
+ (ECalMetaBackend *meta_backend,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_meta_backend_get_changes_sync
+ (ECalMetaBackend *meta_backend,
+ const gchar *last_sync_tag,
+ gboolean is_repeat,
+ gchar **out_new_sync_tag,
+ gboolean *out_repeat,
+ GSList **out_created_objects, /* ECalMetaBackendInfo * */
+ GSList **out_modified_objects, /* ECalMetaBackendInfo * */
+ GSList **out_removed_objects, /* ECalMetaBackendInfo * */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_meta_backend_list_existing_sync
+ (ECalMetaBackend *meta_backend,
+ gchar **out_new_sync_tag,
+ GSList **out_existing_objects, /* ECalMetaBackendInfo * */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_meta_backend_load_component_sync
+ (ECalMetaBackend *meta_backend,
+ const gchar *uid,
+ const gchar *extra,
+ icalcomponent **out_component,
+ gchar **out_extra,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_meta_backend_save_component_sync
+ (ECalMetaBackend *meta_backend,
+ gboolean overwrite_existing,
+ EConflictResolution conflict_resolution,
+ const GSList *instances, /* ECalComponent * */
+ const gchar *extra,
+ gchar **out_new_uid,
+ gchar **out_new_extra,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_meta_backend_remove_component_sync
+ (ECalMetaBackend *meta_backend,
+ EConflictResolution conflict_resolution,
+ const gchar *uid,
+ const gchar *extra,
+ const gchar *object,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_meta_backend_search_sync (ECalMetaBackend *meta_backend,
+ const gchar *expr,
+ GSList **out_icalstrings, /* gchar * */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_meta_backend_search_components_sync
+ (ECalMetaBackend *meta_backend,
+ const gchar *expr,
+ GSList **out_components, /* ECalComponent * */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cal_meta_backend_requires_reconnect
+ (ECalMetaBackend *meta_backend);
+
+G_END_DECLS
+
+#endif /* E_CAL_META_BACKEND_H */
diff --git a/src/calendar/libedata-cal/libedata-cal.h b/src/calendar/libedata-cal/libedata-cal.h
index c37bef9..a754d70 100644
--- a/src/calendar/libedata-cal/libedata-cal.h
+++ b/src/calendar/libedata-cal/libedata-cal.h
@@ -31,6 +31,8 @@
#include <libedata-cal/e-cal-backend-store.h>
#include <libedata-cal/e-cal-backend-sync.h>
#include <libedata-cal/e-cal-backend-util.h>
+#include <libedata-cal/e-cal-cache.h>
+#include <libedata-cal/e-cal-meta-backend.h>
#include <libedata-cal/e-data-cal-factory.h>
#include <libedata-cal/e-data-cal.h>
#include <libedata-cal/e-data-cal-view.h>
diff --git a/src/libebackend/CMakeLists.txt b/src/libebackend/CMakeLists.txt
index f328f0b..844bcbc 100644
--- a/src/libebackend/CMakeLists.txt
+++ b/src/libebackend/CMakeLists.txt
@@ -10,6 +10,7 @@ set(DEPENDENCIES
set(SOURCES
e-backend.c
e-backend-factory.c
+ e-cache.c
e-cache-reaper.c
e-cache-reaper-utils.c
e-cache-reaper-utils.h
@@ -36,6 +37,7 @@ set(HEADERS
e-backend.h
e-backend-enums.h
e-backend-factory.h
+ e-cache.h
e-cache-reaper.h
e-collection-backend.h
e-collection-backend-factory.h
diff --git a/src/libebackend/e-backend-enums.h b/src/libebackend/e-backend-enums.h
index 9b53344..5c36569 100644
--- a/src/libebackend/e-backend-enums.h
+++ b/src/libebackend/e-backend-enums.h
@@ -82,4 +82,47 @@ typedef enum { /*< flags >*/
E_SOURCE_PERMISSION_REMOVABLE = 1 << 1
} ESourcePermissionFlags;
+/**
+ * EOfflineState:
+ * @E_OFFLINE_STATE_UNKNOWN: Unknown offline state.
+ * @E_OFFLINE_STATE_SYNCED: The object if synchnized with no local changes.
+ * @E_OFFLINE_STATE_LOCALLY_CREATED: The object is locally created.
+ * @E_OFFLINE_STATE_LOCALLY_MODIFIED: The object is locally modified.
+ * @E_OFFLINE_STATE_LOCALLY_DELETED: The object is locally deleted.
+ *
+ * Defines offline state of an object. Locally changed objects require
+ * synchronization with their remote storage.
+ *
+ * Since: 3.26
+ **/
+typedef enum {
+ E_OFFLINE_STATE_UNKNOWN = -1,
+ E_OFFLINE_STATE_SYNCED,
+ E_OFFLINE_STATE_LOCALLY_CREATED,
+ E_OFFLINE_STATE_LOCALLY_MODIFIED,
+ E_OFFLINE_STATE_LOCALLY_DELETED
+} EOfflineState;
+
+/**
+ * EConflictResolution:
+ * @E_CONFLICT_RESOLUTION_FAIL: Fail when a write-conflict occurs.
+ * @E_CONFLICT_RESOLUTION_USE_NEWER: Use newer version of the object,
+ * which can be either the server version or the local version of it.
+ * @E_CONFLICT_RESOLUTION_KEEP_SERVER: Keep server object on conflict.
+ * @E_CONFLICT_RESOLUTION_KEEP_LOCAL: Write local version of the object on conflict.
+ * @E_CONFLICT_RESOLUTION_WRITE_COPY: Create a new copy of the object on conflict.
+ *
+ * Defines what to do when a conflict between the locally stored and
+ * remotely stored object versions happen during object modify or remove.
+ *
+ * Since: 3.26
+ **/
+typedef enum {
+ E_CONFLICT_RESOLUTION_FAIL = 0,
+ E_CONFLICT_RESOLUTION_USE_NEWER,
+ E_CONFLICT_RESOLUTION_KEEP_SERVER,
+ E_CONFLICT_RESOLUTION_KEEP_LOCAL,
+ E_CONFLICT_RESOLUTION_WRITE_COPY
+} EConflictResolution;
+
#endif /* E_BACKEND_ENUMS_H */
diff --git a/src/libebackend/e-backend.c b/src/libebackend/e-backend.c
index 389b9b4..5c35f50 100644
--- a/src/libebackend/e-backend.c
+++ b/src/libebackend/e-backend.c
@@ -133,7 +133,10 @@ backend_update_online_state_timeout_cb (gpointer user_data)
if (current_source && g_source_is_destroyed (current_source))
return FALSE;
- backend = E_BACKEND (user_data);
+ backend = g_weak_ref_get (user_data);
+ if (!backend)
+ return FALSE;
+
connectable = e_backend_ref_connectable (backend);
g_mutex_lock (&backend->priv->update_online_state_lock);
@@ -180,8 +183,8 @@ backend_update_online_state_timeout_cb (gpointer user_data)
g_mutex_unlock (&backend->priv->network_monitor_cancellable_lock);
}
- if (connectable != NULL)
- g_object_unref (connectable);
+ g_clear_object (&connectable);
+ g_clear_object (&backend);
return FALSE;
}
@@ -211,7 +214,7 @@ backend_update_online_state (EBackend *backend)
g_source_set_callback (
timeout_source,
backend_update_online_state_timeout_cb,
- backend, (GDestroyNotify) g_object_unref);
+ e_weak_ref_new (backend), (GDestroyNotify) e_weak_ref_free);
g_source_attach (timeout_source, main_context);
backend->priv->update_online_state =
g_source_ref (timeout_source);
@@ -220,6 +223,8 @@ backend_update_online_state (EBackend *backend)
g_main_context_unref (main_context);
g_mutex_unlock (&backend->priv->update_online_state_lock);
+
+ g_object_unref (backend);
}
static void
@@ -314,6 +319,7 @@ backend_source_authenticate_thread (gpointer user_data)
ESourceCredentialsReason reason = E_SOURCE_CREDENTIALS_REASON_ERROR;
switch (auth_result) {
+ case E_SOURCE_AUTHENTICATION_UNKNOWN:
case E_SOURCE_AUTHENTICATION_ERROR:
reason = E_SOURCE_CREDENTIALS_REASON_ERROR;
break;
diff --git a/src/libebackend/e-cache.c b/src/libebackend/e-cache.c
new file mode 100644
index 0000000..f35e0f0
--- /dev/null
+++ b/src/libebackend/e-cache.c
@@ -0,0 +1,3068 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * SECTION: e-cache
+ * @include: libebackend/libebackend.h
+ * @short_description: An SQLite data cache
+ *
+ * The #ECache is an abstract class which consists of the common
+ * parts which can be used by its descendants. It also allows
+ * storing offline state for the stored objects.
+ *
+ * The API is thread safe, with special considerations to be made
+ * around e_cache_lock() and e_cache_unlock() for
+ * the sake of isolating transactions across threads.
+ **/
+
+#include "evolution-data-server-config.h"
+
+#include <errno.h>
+#include <sqlite3.h>
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+#include <glib/gstdio.h>
+
+#include <camel/camel.h>
+
+#include "e-sqlite3-vfs.h"
+
+#include "e-cache.h"
+
+#define E_CACHE_KEY_VERSION "version"
+#define E_CACHE_KEY_REVISION "revision"
+
+/* The number of SQLite virtual machine instructions that are
+ * evaluated at a time, the user passed GCancellable is
+ * checked between each batch of evaluated instructions.
+ */
+#define E_CACHE_CANCEL_BATCH_SIZE 200
+
+/* How many rows to read when e_cache_foreach_update() */
+#define E_CACHE_UPDATE_BATCH_SIZE 100
+
+struct _ECachePrivate {
+ gchar *filename;
+ sqlite3 *db;
+
+ GRecMutex lock; /* Main API lock */
+ guint32 in_transaction; /* Nested transaction counter */
+ ECacheLockType lock_type; /* The lock type acquired for the current transaction */
+ GCancellable *cancellable; /* User passed GCancellable, we abort an operation if cancelled */
+
+ guint32 revision_change_frozen;
+ gint revision_counter;
+ gint64 last_revision_time;
+ gboolean needs_revision_change;
+};
+
+enum {
+ BEFORE_PUT,
+ BEFORE_REMOVE,
+ REVISION_CHANGED,
+ LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL];
+
+G_DEFINE_QUARK (e-cache-error-quark, e_cache_error)
+
+G_DEFINE_ABSTRACT_TYPE (ECache, e_cache, G_TYPE_OBJECT)
+
+G_DEFINE_BOXED_TYPE (ECacheColumnValues, e_cache_column_values, e_cache_column_values_copy,
e_cache_column_values_free)
+G_DEFINE_BOXED_TYPE (ECacheOfflineChange, e_cache_offline_change, e_cache_offline_change_copy,
e_cache_offline_change_free)
+G_DEFINE_BOXED_TYPE (ECacheColumnInfo, e_cache_column_info, e_cache_column_info_copy,
e_cache_column_info_free)
+
+/**
+ * e_cache_column_values_new:
+ *
+ * Creates a new #ECacheColumnValues to store values for additional columns.
+ * The column names are compared case insensitively.
+ *
+ * Returns: (transfer full): a new #ECacheColumnValues. Free with e_cache_column_values_free(),
+ * when no longer needed.
+ *
+ * Since: 3.26
+ **/
+ECacheColumnValues *
+e_cache_column_values_new (void)
+{
+ return (ECacheColumnValues *) g_hash_table_new_full (camel_strcase_hash, camel_strcase_equal, g_free,
g_free);
+}
+
+/**
+ * e_cache_column_values_copy:
+ * @other_columns: (nullable): an #ECacheColumnValues
+ *
+ * Returns: (transfer full): Copy of the @other_columns. Free with
+ * e_cache_column_values_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+ECacheColumnValues *
+e_cache_column_values_copy (ECacheColumnValues *other_columns)
+{
+ GHashTableIter iter;
+ gpointer name, value;
+ ECacheColumnValues *copy;
+
+ if (!other_columns)
+ return NULL;
+
+ copy = e_cache_column_values_new ();
+
+ e_cache_column_values_init_iter (other_columns, &iter);
+ while (g_hash_table_iter_next (&iter, &name, &value)) {
+ e_cache_column_values_put (copy, name, value);
+ }
+
+ return copy;
+}
+
+/**
+ * e_cache_column_values_free:
+ * @other_columns: (nullable): an #ECacheColumnValues
+ *
+ * Frees previously allocated @other_columns with
+ * e_cache_column_values_new() or e_cache_column_values_copy().
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_column_values_free (ECacheColumnValues *other_columns)
+{
+ if (other_columns)
+ g_hash_table_destroy ((GHashTable *) other_columns);
+}
+
+/**
+ * e_cache_column_values_put:
+ * @other_columns: an #ECacheColumnValues
+ * @name: a column name
+ * @value: (nullable): a column value
+ *
+ * Puts the @value for column @name. If contains a value for the same
+ * column, then it is replaced. This creates a copy of both @name
+ * and @value.
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_column_values_put (ECacheColumnValues *other_columns,
+ const gchar *name,
+ const gchar *value)
+{
+ GHashTable *hash_table = (GHashTable *) other_columns;
+
+ g_return_if_fail (other_columns != NULL);
+ g_return_if_fail (name != NULL);
+
+ g_hash_table_insert (hash_table, g_strdup (name), g_strdup (value));
+}
+
+/**
+ * e_cache_column_values_take_value:
+ * @other_columns: an #ECacheColumnValues
+ * @name: a column name
+ * @value: (nullable) (in) (transfer full): a column value
+ *
+ * Puts the @value for column @name. If contains a value for the same
+ * column, then it is replaced. This creates a copy of the @name, but
+ * takes owner ship of the @value.
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_column_values_take_value (ECacheColumnValues *other_columns,
+ const gchar *name,
+ gchar *value)
+{
+ GHashTable *hash_table = (GHashTable *) other_columns;
+
+ g_return_if_fail (other_columns != NULL);
+ g_return_if_fail (name != NULL);
+
+ g_hash_table_insert (hash_table, g_strdup (name), value);
+}
+
+/**
+ * e_cache_column_values_take:
+ * @other_columns: an #ECacheColumnValues
+ * @name: (in) (transfer full): a column name
+ * @value: (nullable) (in) (transfer full): a column value
+ *
+ * Puts the @value for column @name. If contains a value for the same
+ * column, then it is replaced. This creates takes ownership of both
+ * the @name and the @value.
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_column_values_take (ECacheColumnValues *other_columns,
+ gchar *name,
+ gchar *value)
+{
+ GHashTable *hash_table = (GHashTable *) other_columns;
+
+ g_return_if_fail (other_columns != NULL);
+ g_return_if_fail (name != NULL);
+
+ g_hash_table_insert (hash_table, name, value);
+}
+
+/**
+ * e_cache_column_values_contains:
+ * @other_columns: an #ECacheColumnValues
+ * @name: a column name
+ *
+ * Returns: Whether @other_columns contains column named @name.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_column_values_contains (ECacheColumnValues *other_columns,
+ const gchar *name)
+{
+ GHashTable *hash_table = (GHashTable *) other_columns;
+
+ g_return_val_if_fail (other_columns != NULL, FALSE);
+ g_return_val_if_fail (name != NULL, FALSE);
+
+ return g_hash_table_contains (hash_table, name);
+}
+
+/**
+ * e_cache_column_values_remove:
+ * @other_columns: an #ECacheColumnValues
+ * @name: a column name
+ *
+ * Removes value for the column named @name from @other_columns.
+ *
+ * Returns: Whether such column existed and had been removed.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_column_values_remove (ECacheColumnValues *other_columns,
+ const gchar *name)
+{
+ GHashTable *hash_table = (GHashTable *) other_columns;
+
+ g_return_val_if_fail (other_columns != NULL, FALSE);
+ g_return_val_if_fail (name != NULL, FALSE);
+
+ return g_hash_table_remove (hash_table, name);
+}
+
+/**
+ * e_cache_column_values_remove_all:
+ * @other_columns: an #ECacheColumnValues
+ *
+ * Removes all values from the @other_columns, leaving it empty.
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_column_values_remove_all (ECacheColumnValues *other_columns)
+{
+ GHashTable *hash_table = (GHashTable *) other_columns;
+
+ g_return_if_fail (other_columns != NULL);
+
+ g_hash_table_remove_all (hash_table);
+}
+
+/**
+ * e_cache_column_values_lookup:
+ * @other_columns: an #ECacheColumnValues
+ * @name: a column name
+ *
+ * Looks up currently stored value for the column named @name.
+ * As the values can be %NULL one cannot distinguish between
+ * a column which doesn't have stored any value and a column
+ * which has stored %NULL value. Use e_cache_column_values_contains()
+ * to check whether such column exitst in the @other_columns.
+ * The returned pointer is owned by @other_columns and is valid until
+ * the value is overwritten of the @other_columns freed.
+ *
+ * Returns: Stored value for the column named @name, or %NULL, if
+ * no such column values is stored.
+ *
+ * Since: 3.26
+ **/
+const gchar *
+e_cache_column_values_lookup (ECacheColumnValues *other_columns,
+ const gchar *name)
+{
+ GHashTable *hash_table = (GHashTable *) other_columns;
+
+ g_return_val_if_fail (other_columns != NULL, NULL);
+ g_return_val_if_fail (name != NULL, NULL);
+
+ return g_hash_table_lookup (hash_table, name);
+}
+
+/**
+ * e_cache_column_values_get_size:
+ * @other_columns: an #ECacheColumnValues
+ *
+ * Returns: How many columns are stored in the @other_columns.
+ *
+ * Since: 3.26
+ **/
+guint
+e_cache_column_values_get_size (ECacheColumnValues *other_columns)
+{
+ GHashTable *hash_table = (GHashTable *) other_columns;
+
+ g_return_val_if_fail (other_columns != NULL, 0);
+
+ return g_hash_table_size (hash_table);
+}
+
+/**
+ * e_cache_column_values_init_iter:
+ * @other_columns: an #ECacheColumnValues
+ * @iter: a #GHashTableIter
+ *
+ * Initialized the @iter, thus the @other_columns can be traversed
+ * with g_hash_table_iter_next(). The key is a column name and
+ * the value is the corresponding column value.
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_column_values_init_iter (ECacheColumnValues *other_columns,
+ GHashTableIter *iter)
+{
+ GHashTable *hash_table = (GHashTable *) other_columns;
+
+ g_return_if_fail (other_columns != NULL);
+ g_return_if_fail (iter != NULL);
+
+ g_hash_table_iter_init (iter, hash_table);
+}
+
+/**
+ * e_cache_offline_change_new:
+ * @uid: a unique object identifier
+ * @revision: (nullable): a revision of the object
+ * @object: (nullable): object itself
+ * @state: an #EOfflineState
+ *
+ * Creates a new #ECacheOfflineChange with the offline @state
+ * information for the given @uid.
+ *
+ * Returns: (transfer full): A new #ECacheOfflineChange. Free it with
+ * e_cache_offline_change_free() when no longer needed.
+ *
+ * Since: 3.26
+ **/
+ECacheOfflineChange *
+e_cache_offline_change_new (const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ EOfflineState state)
+{
+ ECacheOfflineChange *change;
+
+ g_return_val_if_fail (uid != NULL, NULL);
+
+ change = g_new0 (ECacheOfflineChange, 1);
+ change->uid = g_strdup (uid);
+ change->revision = g_strdup (revision);
+ change->object = g_strdup (object);
+ change->state = state;
+
+ return change;
+}
+
+/**
+ * e_cache_offline_change_copy:
+ * @change: (nullable): a source #ECacheOfflineChange to copy, or %NULL
+ *
+ * Returns: (transfer full): Copy of the given @change. Free it with
+ * e_cache_offline_change_free() when no longer needed.
+ * If the @change is %NULL, then returns %NULL as well.
+ *
+ * Since: 3.26
+ **/
+ECacheOfflineChange *
+e_cache_offline_change_copy (const ECacheOfflineChange *change)
+{
+ if (!change)
+ return NULL;
+
+ return e_cache_offline_change_new (change->uid, change->revision, change->object, change->state);
+}
+
+/**
+ * e_cache_offline_change_free:
+ * @change: (nullable): an #ECacheOfflineChange
+ *
+ * Frees the @change structure, previously allocated with e_cache_offline_change_new()
+ * or e_cache_offline_change_copy().
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_offline_change_free (gpointer change)
+{
+ ECacheOfflineChange *chng = change;
+
+ if (chng) {
+ g_free (chng->uid);
+ g_free (chng->revision);
+ g_free (chng->object);
+ g_free (chng);
+ }
+}
+
+/**
+ * e_cache_column_info_new:
+ * @name: a column name
+ * @type: a column type
+ * @index_name: (nullable): an index name for this column, or %NULL
+ *
+ * Returns: (transfer full): A new #ECacheColumnInfo. Free it with
+ * e_cache_column_info_free() when no longer needed.
+ *
+ * Since: 3.26
+ **/
+ECacheColumnInfo *
+e_cache_column_info_new (const gchar *name,
+ const gchar *type,
+ const gchar *index_name)
+{
+ ECacheColumnInfo *info;
+
+ g_return_val_if_fail (name != NULL, NULL);
+ g_return_val_if_fail (type != NULL, NULL);
+
+ info = g_new0 (ECacheColumnInfo, 1);
+ info->name = g_strdup (name);
+ info->type = g_strdup (type);
+ info->index_name = g_strdup (index_name);
+
+ return info;
+}
+
+/**
+ * e_cache_column_info_copy:
+ * @info: (nullable): a source #ECacheColumnInfo to copy, or %NULL
+ *
+ * Returns: (transfer full): Copy of the given @info. Free it with
+ * e_cache_column_info_free() when no longer needed.
+ * If the @info is %NULL, then returns %NULL as well.
+ *
+ * Since: 3.26
+ **/
+ECacheColumnInfo *
+e_cache_column_info_copy (const ECacheColumnInfo *info)
+{
+ if (!info)
+ return NULL;
+
+ return e_cache_column_info_new (info->name, info->type, info->index_name);
+}
+
+/**
+ * e_cache_column_info_free:
+ * @info: (nullable): an #ECacheColumnInfo
+ *
+ * Frees the @info structure, previously allocated with e_cache_column_info_new()
+ * or e_cache_column_info_copy().
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_column_info_free (gpointer info)
+{
+ ECacheColumnInfo *nfo = info;
+
+ if (nfo) {
+ g_free (nfo->name);
+ g_free (nfo->type);
+ g_free (nfo->index_name);
+ g_free (nfo);
+ }
+}
+
+#define E_CACHE_SET_ERROR_FROM_SQLITE(error, code, message, stmt) \
+ G_STMT_START { \
+ if (code == SQLITE_CONSTRAINT) { \
+ g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_CONSTRAINT, message); \
+ } else if (code == SQLITE_ABORT) { \
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED, "Operation cancelled: %s",
message); \
+ } else { \
+ g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_ENGINE, \
+ "SQLite error code '%d': %s (statement:%s)", code, message, stmt); \
+ } \
+ } G_STMT_END
+
+struct CacheSQLiteExecData {
+ ECache *cache;
+ ECacheSelectFunc callback;
+ gpointer user_data;
+};
+
+static gint
+e_cache_sqlite_exec_cb (gpointer user_data,
+ gint ncols,
+ gchar **column_values,
+ gchar **column_names)
+{
+ struct CacheSQLiteExecData *cse = user_data;
+
+ g_return_val_if_fail (cse != NULL, SQLITE_MISUSE);
+ g_return_val_if_fail (cse->callback != NULL, SQLITE_MISUSE);
+
+ if (!cse->callback (cse->cache, ncols, (const gchar **) column_names, (const gchar **) column_values,
cse->user_data))
+ return SQLITE_ABORT;
+
+ return SQLITE_OK;
+}
+
+static gboolean
+e_cache_sqlite_exec_internal (ECache *cache,
+ const gchar *stmt,
+ ECacheSelectFunc callback,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ struct CacheSQLiteExecData cse;
+ GCancellable *previous_cancellable;
+ gchar *errmsg = NULL;
+ gint ret = -1, retries = 0;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+ g_return_val_if_fail (stmt != NULL, FALSE);
+
+ g_rec_mutex_lock (&cache->priv->lock);
+
+ previous_cancellable = cache->priv->cancellable;
+ if (cancellable)
+ cache->priv->cancellable = cancellable;
+
+ cse.cache = cache;
+ cse.callback = callback;
+ cse.user_data = user_data;
+
+ ret = sqlite3_exec (cache->priv->db, stmt, callback ? e_cache_sqlite_exec_cb : NULL, &cse, &errmsg);
+
+ while (ret == SQLITE_BUSY || ret == SQLITE_LOCKED || ret == -1) {
+ /* try for ~15 seconds, then give up */
+ if (retries > 150)
+ break;
+ retries++;
+
+ if (errmsg) {
+ sqlite3_free (errmsg);
+ errmsg = NULL;
+ }
+ g_thread_yield ();
+ g_usleep (100 * 1000); /* Sleep for 100 ms */
+
+ ret = sqlite3_exec (cache->priv->db, stmt, callback ? e_cache_sqlite_exec_cb : NULL, &cse,
&errmsg);
+ }
+
+ cache->priv->cancellable = previous_cancellable;
+
+ g_rec_mutex_unlock (&cache->priv->lock);
+
+ if (ret != SQLITE_OK) {
+ E_CACHE_SET_ERROR_FROM_SQLITE (error, ret, errmsg, stmt);
+ sqlite3_free (errmsg);
+ return FALSE;
+ }
+
+ if (errmsg)
+ sqlite3_free (errmsg);
+
+ return TRUE;
+}
+
+static gboolean
+e_cache_sqlite_exec_printf (ECache *cache,
+ const gchar *format,
+ ECacheSelectFunc callback,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error,
+ ...)
+{
+ gboolean success;
+ va_list args;
+ gchar *stmt;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+ g_return_val_if_fail (format != NULL, FALSE);
+
+ va_start (args, error);
+ stmt = sqlite3_vmprintf (format, args);
+
+ success = e_cache_sqlite_exec_internal (cache, stmt, callback, user_data, cancellable, error);
+
+ sqlite3_free (stmt);
+ va_end (args);
+
+ return success;
+}
+
+static gboolean
+e_cache_read_key_value (ECache *cache,
+ gint ncols,
+ const gchar **column_names,
+ const gchar **column_values,
+ gpointer user_data)
+{
+ gchar **pvalue = user_data;
+
+ g_return_val_if_fail (ncols == 1, FALSE);
+ g_return_val_if_fail (column_names != NULL, FALSE);
+ g_return_val_if_fail (column_values != NULL, FALSE);
+ g_return_val_if_fail (pvalue != NULL, FALSE);
+
+ if (!*pvalue)
+ *pvalue = g_strdup (column_values[0]);
+
+ return TRUE;
+}
+
+static gchar *
+e_cache_build_user_key (const gchar *key)
+{
+ return g_strconcat ("user::", key, NULL);
+}
+
+static gboolean
+e_cache_set_key_internal (ECache *cache,
+ gboolean is_user_key,
+ const gchar *key,
+ const gchar *value,
+ GError **error)
+{
+ gchar *tmp = NULL;
+ const gchar *usekey;
+ gboolean success;
+
+ if (is_user_key) {
+ tmp = e_cache_build_user_key (key);
+ usekey = tmp;
+ } else {
+ usekey = key;
+ }
+
+ if (value) {
+ success = e_cache_sqlite_exec_printf (cache,
+ "INSERT or REPLACE INTO " E_CACHE_TABLE_KEYS " (key, value) VALUES (%Q, %Q)",
+ NULL, NULL, NULL, error,
+ usekey, value);
+ } else {
+ success = e_cache_sqlite_exec_printf (cache,
+ "DELETE FROM " E_CACHE_TABLE_KEYS " WHERE key = %Q",
+ NULL, NULL, NULL, error,
+ usekey);
+ }
+
+ g_free (tmp);
+
+ return success;
+}
+
+static gchar *
+e_cache_dup_key_internal (ECache *cache,
+ gboolean is_user_key,
+ const gchar *key,
+ GError **error)
+{
+ gchar *tmp = NULL;
+ const gchar *usekey;
+ gchar *value = NULL;
+
+ if (is_user_key) {
+ tmp = e_cache_build_user_key (key);
+ usekey = tmp;
+ } else {
+ usekey = key;
+ }
+
+ if (!e_cache_sqlite_exec_printf (cache,
+ "SELECT value FROM " E_CACHE_TABLE_KEYS " WHERE key = %Q",
+ e_cache_read_key_value, &value, NULL, error,
+ usekey)) {
+ g_warn_if_fail (value == NULL);
+ }
+
+ g_free (tmp);
+
+ return value;
+}
+
+static gint
+e_cache_check_cancelled_cb (gpointer user_data)
+{
+ ECache *cache = user_data;
+
+ /* Do not use E_IS_CACHE() here, for performance reasons */
+ g_return_val_if_fail (cache != NULL, SQLITE_ABORT);
+
+ if (cache->priv->cancellable &&
+ g_cancellable_is_cancelled (cache->priv->cancellable)) {
+ return SQLITE_ABORT;
+ }
+
+ return SQLITE_OK;
+}
+
+static gboolean
+e_cache_init_sqlite (ECache *cache,
+ const gchar *filename,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gint ret;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+ g_return_val_if_fail (filename != NULL, FALSE);
+ g_return_val_if_fail (cache->priv->filename == NULL, FALSE);
+
+ cache->priv->filename = g_strdup (filename);
+
+ ret = sqlite3_open (filename, &cache->priv->db);
+ if (ret != SQLITE_OK) {
+ if (!cache->priv->db) {
+ g_set_error_literal (error, E_CACHE_ERROR, E_CACHE_ERROR_LOAD, _("Out of memory"));
+ } else {
+ const gchar *errmsg = sqlite3_errmsg (cache->priv->db);
+
+ g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_ENGINE,
+ _("Can't open database %s: %s"), filename, errmsg);
+
+ sqlite3_close (cache->priv->db);
+ cache->priv->db = NULL;
+ }
+
+ return FALSE;
+ }
+
+ /* Handle GCancellable */
+ sqlite3_progress_handler (
+ cache->priv->db,
+ E_CACHE_CANCEL_BATCH_SIZE,
+ e_cache_check_cancelled_cb,
+ cache);
+
+ return e_cache_sqlite_exec_internal (cache, "ATTACH DATABASE ':memory:' AS mem", NULL, NULL,
cancellable, error) &&
+ e_cache_sqlite_exec_internal (cache, "PRAGMA foreign_keys = ON", NULL, NULL,
cancellable, error) &&
+ e_cache_sqlite_exec_internal (cache, "PRAGMA case_sensitive_like = ON", NULL, NULL,
cancellable, error);
+}
+
+static gboolean
+e_cache_garther_column_names_cb (ECache *cache,
+ gint ncols,
+ const gchar *column_names[],
+ const gchar *column_values[],
+ gpointer user_data)
+{
+ GHashTable *known_columns = user_data;
+ gint ii;
+
+ g_return_val_if_fail (known_columns != NULL, FALSE);
+ g_return_val_if_fail (column_names != NULL, FALSE);
+ g_return_val_if_fail (column_values != NULL, FALSE);
+
+ for (ii = 0; ii < ncols; ii++) {
+ if (column_names[ii] && camel_strcase_equal (column_names[ii], "name")) {
+ if (column_values[ii])
+ g_hash_table_insert (known_columns, g_strdup (column_values[ii]), NULL);
+ break;
+ }
+ }
+
+ return TRUE;
+}
+
+static gboolean
+e_cache_init_tables (ECache *cache,
+ const GSList *other_columns,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GHashTable *known_columns;
+ GString *objects_stmt;
+ const GSList *link;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+ g_return_val_if_fail (cache->priv->db != NULL, FALSE);
+
+ if (!e_cache_sqlite_exec_internal (cache,
+ "CREATE TABLE IF NOT EXISTS " E_CACHE_TABLE_KEYS " ("
+ "key TEXT PRIMARY KEY,"
+ "value TEXT)",
+ NULL, NULL, cancellable, error)) {
+ return FALSE;
+ }
+
+ objects_stmt = g_string_new ("");
+
+ g_string_append (objects_stmt, "CREATE TABLE IF NOT EXISTS " E_CACHE_TABLE_OBJECTS " ("
+ E_CACHE_COLUMN_UID " TEXT PRIMARY KEY,"
+ E_CACHE_COLUMN_REVISION " TEXT,"
+ E_CACHE_COLUMN_OBJECT " TEXT,"
+ E_CACHE_COLUMN_STATE " INTEGER");
+
+ for (link = other_columns; link; link = g_slist_next (link)) {
+ const ECacheColumnInfo *info = link->data;
+
+ if (!info)
+ continue;
+
+ g_string_append (objects_stmt, ",");
+ g_string_append (objects_stmt, info->name);
+ g_string_append (objects_stmt, " ");
+ g_string_append (objects_stmt, info->type);
+ }
+
+ g_string_append (objects_stmt, ")");
+
+ if (!e_cache_sqlite_exec_internal (cache, objects_stmt->str, NULL, NULL, cancellable, error)) {
+ g_string_free (objects_stmt, TRUE);
+
+ return FALSE;
+ }
+
+ g_string_free (objects_stmt, TRUE);
+
+ /* Verify that all other columns are there and remove those unused */
+ known_columns = g_hash_table_new_full (camel_strcase_hash, camel_strcase_equal, g_free, NULL);
+
+ if (!e_cache_sqlite_exec_internal (cache, "PRAGMA table_info (" E_CACHE_TABLE_OBJECTS ")",
+ e_cache_garther_column_names_cb, known_columns, cancellable, error)) {
+ g_string_free (objects_stmt, TRUE);
+
+ return FALSE;
+ }
+
+ g_hash_table_remove (known_columns, E_CACHE_COLUMN_UID);
+ g_hash_table_remove (known_columns, E_CACHE_COLUMN_REVISION);
+ g_hash_table_remove (known_columns, E_CACHE_COLUMN_OBJECT);
+ g_hash_table_remove (known_columns, E_CACHE_COLUMN_STATE);
+
+ for (link = other_columns; link; link = g_slist_next (link)) {
+ const ECacheColumnInfo *info = link->data;
+
+ if (!info)
+ continue;
+
+ if (g_hash_table_remove (known_columns, info->name))
+ continue;
+
+ if (!e_cache_sqlite_exec_printf (cache,
+ "ALTER TABLE " E_CACHE_TABLE_OBJECTS " ADD COLUMN %Q %s",
+ NULL, NULL, cancellable, error,
+ info->name, info->type)) {
+ g_hash_table_destroy (known_columns);
+
+ return FALSE;
+ }
+ }
+
+ g_hash_table_destroy (known_columns);
+
+ for (link = other_columns; link; link = g_slist_next (link)) {
+ const ECacheColumnInfo *info = link->data;
+
+ if (!info || !info->index_name)
+ continue;
+
+ if (!e_cache_sqlite_exec_printf (cache,
+ "CREATE INDEX IF NOT EXISTS %Q ON " E_CACHE_TABLE_OBJECTS " (%s)",
+ NULL, NULL, cancellable, error,
+ info->index_name, info->name)) {
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * e_cache_initialize_sync:
+ * @cache: an #ECache
+ * @filename: a filename of an SQLite database to use
+ * @other_columns: (element-type ECacheColumnInfo) (nullable): an optional
+ * #GSList with additional columns to add to the objects table
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Initializes the @cache and opens the @filename database.
+ * This should be called by the descendant.
+ *
+ * The @other_columns are added to the objects table (@E_CACHE_TABLE_OBJECTS).
+ * Values for these columns are returned by e_cache_get()
+ * and can be stored with e_cache_put().
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_initialize_sync (ECache *cache,
+ const gchar *filename,
+ const GSList *other_columns,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gchar *dirname;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+ g_return_val_if_fail (cache->priv->filename == NULL, FALSE);
+
+ /* Ensure existance of the directories leading up to 'filename' */
+ dirname = g_path_get_dirname (filename);
+ if (g_mkdir_with_parents (dirname, 0777) < 0) {
+ g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_LOAD,
+ _("Can not make parent directory: %s"),
+ g_strerror (errno));
+ g_free (dirname);
+
+ return FALSE;
+ }
+
+ g_free (dirname);
+
+ g_rec_mutex_lock (&cache->priv->lock);
+
+ success = e_cache_init_sqlite (cache, filename, cancellable, error) &&
+ e_cache_init_tables (cache, other_columns, cancellable, error);
+
+ g_rec_mutex_unlock (&cache->priv->lock);
+
+ return success;
+}
+
+/**
+ * e_cache_get_filename:
+ * @cache: an #ECache
+ *
+ * Returns: a filename of the @cache, with which it had been initialized.
+ *
+ * Since: 3.26
+ **/
+const gchar *
+e_cache_get_filename (ECache *cache)
+{
+ g_return_val_if_fail (E_IS_CACHE (cache), NULL);
+
+ return cache->priv->filename;
+}
+
+/**
+ * e_cache_get_version:
+ * @cache: an #ECache
+ *
+ * Returns: A cache data version. This is meant to be used by the descendants.
+ *
+ * Since: 3.26
+ **/
+gint
+e_cache_get_version (ECache *cache)
+{
+ gchar *value;
+ gint version = -1;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), -1);
+
+ value = e_cache_dup_key_internal (cache, FALSE, E_CACHE_KEY_VERSION, NULL);
+
+ if (value) {
+ version = g_ascii_strtoll (value, NULL, 10);
+ g_free (value);
+ }
+
+ return version;
+}
+
+/**
+ * e_cache_set_version:
+ * @cache: an #ECache
+ * @version: a cache data version to set
+ *
+ * Sets a cache data version. This is meant to be used by the descendants.
+ * The @version should be greater than zero.
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_set_version (ECache *cache,
+ gint version)
+{
+ gchar *value;
+
+ g_return_if_fail (E_IS_CACHE (cache));
+ g_return_if_fail (version > 0);
+
+ value = g_strdup_printf ("%d", version);
+ e_cache_set_key_internal (cache, FALSE, E_CACHE_KEY_VERSION, value, NULL);
+ g_free (value);
+}
+
+/**
+ * e_cache_dup_revision:
+ * @cache: an #ECache
+ *
+ * Returns: (transfer full): A revision of the whole @cache. This is meant to be
+ * used by the descendants. Free the returned pointer with g_free(), when no
+ * longer needed.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_cache_dup_revision (ECache *cache)
+{
+ g_return_val_if_fail (E_IS_CACHE (cache), NULL);
+
+ return e_cache_dup_key_internal (cache, FALSE, E_CACHE_KEY_REVISION, NULL);
+}
+
+/**
+ * e_cache_set_revision:
+ * @cache: an #ECache
+ * @revision: (nullable): a revision to set; use %NULL to unset it
+ *
+ * Sets the @revision of the whole @cache. This is not meant to be
+ * used by the descendants, because the revision is updated automatically
+ * when needed. The descendants can listen to "revision-changed" signal.
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_set_revision (ECache *cache,
+ const gchar *revision)
+{
+ g_return_if_fail (E_IS_CACHE (cache));
+
+ e_cache_set_key_internal (cache, FALSE, E_CACHE_KEY_REVISION, revision, NULL);
+
+ g_signal_emit (cache, signals[REVISION_CHANGED], 0, NULL);
+}
+
+/**
+ * e_cache_change_revision:
+ * @cache: an #ECache
+ *
+ * Instructs the @cache to change its revision. In case the revision
+ * change is frozen with e_cache_freeze_revision_change() it notes to
+ * change the revision once the revision change is fully thaw.
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_change_revision (ECache *cache)
+{
+ g_return_if_fail (E_IS_CACHE (cache));
+
+ g_rec_mutex_lock (&cache->priv->lock);
+
+ if (e_cache_is_revision_change_frozen (cache)) {
+ cache->priv->needs_revision_change = TRUE;
+ } else {
+ gchar time_string[100] = { 0 };
+ const struct tm *tm = NULL;
+ time_t t;
+ gint64 revision_time;
+ gchar *revision;
+
+ revision_time = g_get_real_time () / (1000 * 1000);
+ t = (time_t) revision_time;
+
+ if (revision_time != cache->priv->last_revision_time) {
+ cache->priv->revision_counter = 0;
+ cache->priv->last_revision_time = revision_time;
+ }
+
+ tm = gmtime (&t);
+ if (tm)
+ strftime (time_string, 100, "%Y-%m-%dT%H:%M:%SZ", tm);
+
+ revision = g_strdup_printf ("%s(%d)", time_string, cache->priv->revision_counter++);
+
+ e_cache_set_revision (cache, revision);
+
+ g_free (revision);
+ }
+
+ g_rec_mutex_unlock (&cache->priv->lock);
+}
+
+/**
+ * e_cache_freeze_revision_change:
+ * @cache: an #ECache
+ *
+ * Freezes automatic revision change for the @cache. The function
+ * can be called multiple times, but each such call requires its
+ * pair function e_cache_thaw_revision_change() call. See also
+ * e_cache_change_revision().
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_freeze_revision_change (ECache *cache)
+{
+ g_return_if_fail (E_IS_CACHE (cache));
+
+ g_rec_mutex_lock (&cache->priv->lock);
+
+ cache->priv->revision_change_frozen++;
+ g_warn_if_fail (cache->priv->revision_change_frozen != 0);
+
+ g_rec_mutex_unlock (&cache->priv->lock);
+}
+
+/**
+ * e_cache_thaw_revision_change:
+ * @cache: an #ECache
+ *
+ * Thaws automatic revision change for the @cache. It's the pair
+ * function of e_cache_freeze_revision_change().
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_thaw_revision_change (ECache *cache)
+{
+ g_return_if_fail (E_IS_CACHE (cache));
+
+ g_rec_mutex_lock (&cache->priv->lock);
+
+ if (!cache->priv->revision_change_frozen) {
+ g_warn_if_fail (cache->priv->revision_change_frozen > 0);
+ } else {
+ cache->priv->revision_change_frozen--;
+ if (!cache->priv->revision_change_frozen &&
+ cache->priv->needs_revision_change) {
+ cache->priv->needs_revision_change = FALSE;
+ e_cache_change_revision (cache);
+ }
+ }
+
+ g_rec_mutex_unlock (&cache->priv->lock);
+}
+
+/**
+ * e_cache_is_revision_change_frozen:
+ * @cache: an #ECache
+ *
+ * Returns: Whether automatic revision change for the @cache
+ * is currently frozen.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_is_revision_change_frozen (ECache *cache)
+{
+ gboolean frozen;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+
+ g_rec_mutex_lock (&cache->priv->lock);
+ frozen = cache->priv->revision_change_frozen > 0;
+ g_rec_mutex_unlock (&cache->priv->lock);
+
+ return frozen;
+}
+
+/**
+ * e_cache_erase:
+ * @cache: an #ECache
+ *
+ * Erases the cache and all of its content from the disk.
+ * The only valid operation after this is to free the @cache.
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_erase (ECache *cache)
+{
+ ECacheClass *klass;
+
+ g_return_if_fail (E_IS_CACHE (cache));
+
+ if (!cache->priv->db)
+ return;
+
+ klass = E_CACHE_GET_CLASS (cache);
+ g_return_if_fail (klass != NULL);
+
+ if (klass->erase)
+ klass->erase (cache);
+
+ sqlite3_close (cache->priv->db);
+ cache->priv->db = NULL;
+
+ g_unlink (cache->priv->filename);
+
+ g_free (cache->priv->filename);
+ cache->priv->filename = NULL;
+}
+
+static gboolean
+e_cache_count_rows_cb (ECache *cache,
+ gint ncols,
+ const gchar **column_names,
+ const gchar **column_values,
+ gpointer user_data)
+{
+ guint *pnrows = user_data;
+
+ g_return_val_if_fail (pnrows != NULL, FALSE);
+
+ *pnrows = (*pnrows) + 1;
+
+ return TRUE;
+}
+
+/**
+ * e_cache_contains:
+ * @cache: an #ECache
+ * @uid: a unique identifier of an object
+ * @deleted_flag: one of #ECacheDeletedFlag enum
+ *
+ * Checkes whether the @cache contains an object with
+ * the given @uid.
+ *
+ * Returns: Whether the the object had been found.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_contains (ECache *cache,
+ const gchar *uid,
+ ECacheDeletedFlag deleted_flag)
+{
+ guint nrows = 0;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ if (deleted_flag == E_CACHE_INCLUDE_DELETED) {
+ e_cache_sqlite_exec_printf (cache,
+ "SELECT " E_CACHE_COLUMN_UID " FROM " E_CACHE_TABLE_OBJECTS
+ " WHERE " E_CACHE_COLUMN_UID " = %Q"
+ " LIMIT 2",
+ e_cache_count_rows_cb, &nrows, NULL, NULL,
+ uid);
+ } else {
+ e_cache_sqlite_exec_printf (cache,
+ "SELECT " E_CACHE_COLUMN_UID " FROM " E_CACHE_TABLE_OBJECTS
+ " WHERE " E_CACHE_COLUMN_UID " = %Q AND " E_CACHE_COLUMN_STATE " != %d"
+ " LIMIT 2",
+ e_cache_count_rows_cb, &nrows, NULL, NULL,
+ uid, E_OFFLINE_STATE_LOCALLY_DELETED);
+ }
+
+ g_warn_if_fail (nrows <= 1);
+
+ return nrows > 0;
+}
+
+struct GetObjectData {
+ gchar *object;
+ gchar **out_revision;
+ ECacheColumnValues **out_other_columns;
+};
+
+static gboolean
+e_cache_get_object_cb (ECache *cache,
+ gint ncols,
+ const gchar **column_names,
+ const gchar **column_values,
+ gpointer user_data)
+{
+ struct GetObjectData *gd = user_data;
+ gint ii;
+
+ g_return_val_if_fail (gd != NULL, FALSE);
+ g_return_val_if_fail (column_names != NULL, FALSE);
+ g_return_val_if_fail (column_values != NULL, FALSE);
+
+ for (ii = 0; ii < ncols; ii++) {
+ if (g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_UID) == 0 ||
+ g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_STATE) == 0) {
+ /* Skip these two */
+ } else if (g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_REVISION) == 0) {
+ if (gd->out_revision)
+ *gd->out_revision = g_strdup (column_values[ii]);
+ } else if (g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_OBJECT) == 0) {
+ gd->object = g_strdup (column_values[ii]);
+ } else if (gd->out_other_columns) {
+ if (!*gd->out_other_columns)
+ *gd->out_other_columns = e_cache_column_values_new ();
+
+ e_cache_column_values_put (*gd->out_other_columns, column_names[ii],
column_values[ii]);
+ } else if (gd->object && (!gd->out_revision || *gd->out_revision)) {
+ /* Short-break the cycle when the other columns are not requested and
+ the object/revision values were already read. */
+ break;
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * e_cache_get:
+ * @cache: an #ECache
+ * @uid: a unique identifier of an object
+ * @out_revision: (out) (nullable) (transfer full): an out variable for a revision
+ * of the object, or %NULL to ignore
+ * @out_other_columns: (out) (nullable) (transfer full): an out
+ * variable for #ECacheColumnValues other columns, as defined when creating the @cache, or %NULL to ignore
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Returns an object with the given @uid. This function does not consider locally
+ * deleted objects. The @out_revision is set to the object revision, if not %NULL.
+ * Free it with g_free() when no longer needed. Similarly the @out_other_columns
+ * contains a column name to column value strings for additional columns which had
+ * been requested when calling e_cache_initialize_sync(), if not %NULL.
+ * Free the returned #ECacheColumnValues with e_cache_column_values_free(), when
+ * no longer needed.
+ *
+ * Returns: (nullable) (transfer full): An object with the given @uid. Free it
+ * with g_free(), when no longer needed. Returns %NULL on error, like when
+ * the object could not be found.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_cache_get (ECache *cache,
+ const gchar *uid,
+ gchar **out_revision,
+ ECacheColumnValues **out_other_columns,
+ GCancellable *cancellable,
+ GError **error)
+{
+ struct GetObjectData gd;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), NULL);
+ g_return_val_if_fail (uid != NULL, NULL);
+
+ if (out_revision)
+ *out_revision = NULL;
+
+ if (out_other_columns)
+ *out_other_columns = NULL;
+
+ gd.object = NULL;
+ gd.out_revision = out_revision;
+ gd.out_other_columns = out_other_columns;
+
+ if (e_cache_sqlite_exec_printf (cache,
+ "SELECT * FROM " E_CACHE_TABLE_OBJECTS
+ " WHERE " E_CACHE_COLUMN_UID " = %Q AND " E_CACHE_COLUMN_STATE " != %d",
+ e_cache_get_object_cb, &gd, cancellable, error,
+ uid, E_OFFLINE_STATE_LOCALLY_DELETED) &&
+ !gd.object) {
+ g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s” not found"), uid);
+ }
+
+ return gd.object;
+}
+
+static gboolean
+e_cache_put_locked (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ ECacheColumnValues *other_columns,
+ EOfflineState offline_state,
+ gboolean is_replace,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECacheColumnValues *my_other_columns = NULL;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (object != NULL, FALSE);
+
+ if (!other_columns) {
+ my_other_columns = e_cache_column_values_new ();
+ other_columns = my_other_columns;
+ }
+
+ g_signal_emit (cache,
+ signals[BEFORE_PUT],
+ 0,
+ uid, revision, object, other_columns,
+ is_replace, cancellable, error,
+ &success);
+
+ if (success) {
+ ECacheClass *klass;
+
+ klass = E_CACHE_GET_CLASS (cache);
+ g_return_val_if_fail (klass != NULL, FALSE);
+ g_return_val_if_fail (klass->put_locked != NULL, FALSE);
+
+ success = klass->put_locked (cache, uid, revision, object, other_columns, offline_state,
is_replace, cancellable, error);
+
+ if (success)
+ e_cache_change_revision (cache);
+ }
+
+ e_cache_column_values_free (my_other_columns);
+
+ return success;
+}
+
+/**
+ * e_cache_put:
+ * @cache: an #ECache
+ * @uid: a unique identifier of an object
+ * @revision: (nullable): a revision of the object
+ * @object: the object itself
+ * @other_columns: (nullable): an #ECacheColumnValues with other columns to set; can be %NULL
+ * @offline_flag: one of #ECacheOfflineFlag, whether putting this object in offline
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Stores an object into the cache. Depending on @offline_flag, this update
+ * the object's offline state accordingly. When the @offline_flag is set
+ * to %E_CACHE_IS_ONLINE, then it's set to #E_OFFLINE_STATE_SYNCED, like
+ * to be fully synchronized with the server, regardless of its previous
+ * offline state. Overwriting locally deleted object behaves like an addition
+ * of a completely new object.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_put (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ ECacheColumnValues *other_columns,
+ ECacheOfflineFlag offline_flag,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EOfflineState offline_state;
+ gboolean success = TRUE, is_replace;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (object != NULL, FALSE);
+
+ e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+
+ if (offline_flag == E_CACHE_IS_ONLINE) {
+ is_replace = e_cache_contains (cache, uid, E_CACHE_EXCLUDE_DELETED);
+ offline_state = E_OFFLINE_STATE_SYNCED;
+ } else {
+ is_replace = e_cache_contains (cache, uid, E_CACHE_INCLUDE_DELETED);
+ if (is_replace) {
+ GError *local_error = NULL;
+
+ offline_state = e_cache_get_offline_state (cache, uid, cancellable, &local_error);
+
+ if (local_error) {
+ success = FALSE;
+ g_propagate_error (error, local_error);
+ } else if (offline_state != E_OFFLINE_STATE_LOCALLY_CREATED) {
+ offline_state = E_OFFLINE_STATE_LOCALLY_MODIFIED;
+ }
+ } else {
+ offline_state = E_OFFLINE_STATE_LOCALLY_CREATED;
+ }
+ }
+
+ success = success && e_cache_put_locked (cache, uid, revision, object, other_columns,
+ offline_state, is_replace, cancellable, error);
+
+ e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+ return success;
+}
+
+/**
+ * e_cache_remove:
+ * @cache: an #ECache
+ * @uid: a unique identifier of an object
+ * @offline_flag: one of #ECacheOfflineFlag, whether removing the object in offline
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Removes the object with the given @uid from the @cache. Based on the @offline_flag,
+ * it can remove also any information about locally made offline changes. Removing
+ * the object with %E_CACHE_IS_OFFLINE will still remember it for later use
+ * with e_cache_get_offline_changes().
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_remove (ECache *cache,
+ const gchar *uid,
+ ECacheOfflineFlag offline_flag,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECacheClass *klass;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ klass = E_CACHE_GET_CLASS (cache);
+ g_return_val_if_fail (klass != NULL, FALSE);
+ g_return_val_if_fail (klass->remove_locked != NULL, FALSE);
+
+ e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+
+ if (offline_flag == E_CACHE_IS_ONLINE) {
+ success = klass->remove_locked (cache, uid, cancellable, error);
+ } else {
+ EOfflineState offline_state;
+
+ offline_state = e_cache_get_offline_state (cache, uid, cancellable, error);
+ if (offline_state == E_OFFLINE_STATE_UNKNOWN) {
+ success = FALSE;
+ } else if (offline_state == E_OFFLINE_STATE_LOCALLY_CREATED) {
+ success = klass->remove_locked (cache, uid, cancellable, error);
+ } else {
+ g_signal_emit (cache,
+ signals[BEFORE_REMOVE],
+ 0,
+ uid, cancellable, error,
+ &success);
+
+ if (success) {
+ success = e_cache_set_offline_state (cache, uid,
+ E_OFFLINE_STATE_LOCALLY_DELETED, cancellable, error);
+ }
+ }
+ }
+
+ if (success)
+ e_cache_change_revision (cache);
+
+ e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+ return success;
+}
+
+/**
+ * e_cache_remove_all:
+ * @cache: an #ECache
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Removes all objects from the @cache in one call.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_remove_all (ECache *cache,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECacheClass *klass;
+ GSList *uids = NULL;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+
+ klass = E_CACHE_GET_CLASS (cache);
+ g_return_val_if_fail (klass != NULL, FALSE);
+ g_return_val_if_fail (klass->remove_all_locked != NULL, FALSE);
+
+ e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+
+ success = e_cache_get_uids (cache, E_CACHE_INCLUDE_DELETED, &uids, NULL, cancellable, error);
+
+ if (success && uids)
+ success = klass->remove_all_locked (cache, uids, cancellable, error);
+
+ if (success) {
+ e_cache_sqlite_maybe_vacuum (cache, cancellable, NULL);
+ e_cache_change_revision (cache);
+ }
+
+ e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+ g_slist_free_full (uids, g_free);
+
+ return success;
+}
+
+static gboolean
+e_cache_get_uint64_cb (ECache *cache,
+ gint ncols,
+ const gchar **column_names,
+ const gchar **column_values,
+ gpointer user_data)
+{
+ guint64 *pui64 = user_data;
+
+ g_return_val_if_fail (pui64 != NULL, FALSE);
+
+ if (ncols == 1) {
+ *pui64 = column_values[0] ? g_ascii_strtoull (column_values[0], NULL, 10) : 0;
+ } else {
+ *pui64 = 0;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+e_cache_get_int64_cb (ECache *cache,
+ gint ncols,
+ const gchar **column_names,
+ const gchar **column_values,
+ gpointer user_data)
+{
+ gint64 *pi64 = user_data;
+
+ g_return_val_if_fail (pi64 != NULL, FALSE);
+
+ if (ncols == 1) {
+ *pi64 = column_values[0] ? g_ascii_strtoll (column_values[0], NULL, 10) : 0;
+ } else {
+ *pi64 = 0;
+ }
+
+ return TRUE;
+}
+
+/**
+ * e_cache_get_count:
+ * @cache: an #ECache
+ * @deleted_flag: one of #ECacheDeletedFlag enum
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Returns: Count of objects stored in the @cache.
+ *
+ * Since: 3.26
+ **/
+guint
+e_cache_get_count (ECache *cache,
+ ECacheDeletedFlag deleted_flag,
+ GCancellable *cancellable,
+ GError **error)
+{
+ guint64 nobjects = 0;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), 0);
+
+ if (deleted_flag == E_CACHE_INCLUDE_DELETED) {
+ e_cache_sqlite_exec_printf (cache,
+ "SELECT COUNT(*) FROM " E_CACHE_TABLE_OBJECTS,
+ e_cache_get_uint64_cb, &nobjects, cancellable, error);
+ } else {
+ e_cache_sqlite_exec_printf (cache,
+ "SELECT COUNT(*) FROM " E_CACHE_TABLE_OBJECTS
+ " WHERE " E_CACHE_COLUMN_STATE " != %d",
+ e_cache_get_uint64_cb, &nobjects, NULL, NULL,
+ E_OFFLINE_STATE_LOCALLY_DELETED);
+ }
+
+ return nobjects;
+}
+
+struct GatherRowsData {
+ GSList **out_uids;
+ GSList **out_revisions;
+ GSList **out_objects;
+};
+
+static gboolean
+e_cache_gather_rows_data_cb (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ EOfflineState offline_state,
+ gint ncols,
+ const gchar *column_names[],
+ const gchar *column_values[],
+ gpointer user_data)
+{
+ struct GatherRowsData *gd = user_data;
+
+ g_return_val_if_fail (gd != NULL, FALSE);
+
+ if (gd->out_uids)
+ *gd->out_uids = g_slist_prepend (*gd->out_uids, g_strdup (uid));
+
+ if (gd->out_revisions)
+ *gd->out_revisions = g_slist_prepend (*gd->out_revisions, g_strdup (revision));
+
+ if (gd->out_objects)
+ *gd->out_objects = g_slist_prepend (*gd->out_objects, g_strdup (object));
+
+ return TRUE;
+}
+
+/**
+ * e_cache_get_uids:
+ * @cache: an #ECache
+ * @deleted_flag: one of #ECacheDeletedFlag enum
+ * @out_uids: (out) (transfer full) (element-type utf8): a pointer to #GSList to store the found uid to
+ * @out_revisions: (out) (transfer full) (element-type utf8) (nullable): a pointer to #GSList to store
+ * the found revisions to, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets a list of unique object identifiers stored in the @cache, optionally
+ * together with their revisions. The uids are not returned in any particular
+ * order, but the position between @out_uids and @out_revisions matches
+ * the same object.
+ *
+ * Both @out_uids and @out_revisions contain newly allocated #GSList, which
+ * should be freed with g_slist_free_full (slist, g_free); when no longer needed.
+ *
+ * Returns: Whether succeeded. It doesn't necessarily mean that there was
+ * any object stored in the @cache.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_get_uids (ECache *cache,
+ ECacheDeletedFlag deleted_flag,
+ GSList **out_uids,
+ GSList **out_revisions,
+ GCancellable *cancellable,
+ GError **error)
+{
+ struct GatherRowsData gr;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+ g_return_val_if_fail (out_uids, FALSE);
+
+ gr.out_uids = out_uids;
+ gr.out_revisions = out_revisions;
+ gr.out_objects = NULL;
+
+ return e_cache_foreach (cache, deleted_flag, NULL,
+ e_cache_gather_rows_data_cb, &gr, cancellable, error);
+}
+
+/**
+ * e_cache_get_objects:
+ * @cache: an #ECache
+ * @deleted_flag: one of #ECacheDeletedFlag enum
+ * @out_objects: (out) (transfer full) (element-type utf8): a pointer to #GSList to store the found objects
to
+ * @out_revisions: (out) (transfer full) (element-type utf8) (nullable): a pointer to #GSList to store
+ * the found revisions to, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets a list of objects stored in the @cache, optionally together with
+ * their revisions. The uids are not returned in any particular order,
+ * but the position between @out_objects and @out_revisions matches
+ * the same object.
+ *
+ * Both @out_objects and @out_revisions contain newly allocated #GSList, which
+ * should be freed with g_slist_free_full (slist, g_free); when no longer needed.
+ *
+ * Returns: Whether succeeded. It doesn't necessarily mean that there was
+ * any object stored in the @cache.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_get_objects (ECache *cache,
+ ECacheDeletedFlag deleted_flag,
+ GSList **out_objects,
+ GSList **out_revisions,
+ GCancellable *cancellable,
+ GError **error)
+{
+ struct GatherRowsData gr;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+ g_return_val_if_fail (out_objects, FALSE);
+
+ gr.out_uids = NULL;
+ gr.out_revisions = out_revisions;
+ gr.out_objects = out_objects;
+
+ return e_cache_foreach (cache, deleted_flag, NULL,
+ e_cache_gather_rows_data_cb, &gr, cancellable, error);
+}
+
+struct ForeachData {
+ gint uid_index;
+ gint revision_index;
+ gint object_index;
+ gint state_index;
+ ECacheForeachFunc func;
+ gpointer user_data;
+};
+
+static gboolean
+e_cache_foreach_cb (ECache *cache,
+ gint ncols,
+ const gchar *column_names[],
+ const gchar *column_values[],
+ gpointer user_data)
+{
+ struct ForeachData *fe = user_data;
+ EOfflineState offline_state;
+
+ g_return_val_if_fail (fe != NULL, FALSE);
+ g_return_val_if_fail (fe->func != NULL, FALSE);
+ g_return_val_if_fail (column_names != NULL, FALSE);
+ g_return_val_if_fail (column_values != NULL, FALSE);
+
+ if (fe->uid_index == -1 ||
+ fe->revision_index == -1 ||
+ fe->object_index == -1 ||
+ fe->state_index == -1) {
+ gint ii;
+
+ for (ii = 0; ii < ncols && (fe->uid_index == -1 ||
+ fe->revision_index == -1 ||
+ fe->object_index == -1 ||
+ fe->state_index == -1); ii++) {
+ if (!column_names[ii])
+ continue;
+
+ if (fe->uid_index == -1 && g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_UID)
== 0) {
+ fe->uid_index = ii;
+ } else if (fe->revision_index == -1 && g_ascii_strcasecmp (column_names[ii],
E_CACHE_COLUMN_REVISION) == 0) {
+ fe->revision_index = ii;
+ } else if (fe->object_index == -1 && g_ascii_strcasecmp (column_names[ii],
E_CACHE_COLUMN_OBJECT) == 0) {
+ fe->object_index = ii;
+ } else if (fe->state_index == -1 && g_ascii_strcasecmp (column_names[ii],
E_CACHE_COLUMN_STATE) == 0) {
+ fe->state_index = ii;
+ }
+ }
+ }
+
+ g_return_val_if_fail (fe->uid_index >= 0 && fe->uid_index < ncols, FALSE);
+ g_return_val_if_fail (fe->revision_index >= 0 && fe->revision_index < ncols, FALSE);
+ g_return_val_if_fail (fe->object_index >= 0 && fe->object_index < ncols, FALSE);
+ g_return_val_if_fail (fe->state_index >= 0 && fe->state_index < ncols, FALSE);
+
+ if (!column_values[fe->state_index])
+ offline_state = E_OFFLINE_STATE_UNKNOWN;
+ else
+ offline_state = g_ascii_strtoull (column_values[fe->state_index], NULL, 10);
+
+ return fe->func (cache, column_values[fe->uid_index], column_values[fe->revision_index],
column_values[fe->object_index],
+ offline_state, ncols, column_names, column_values, fe->user_data);
+}
+
+/**
+ * e_cache_foreach:
+ * @cache: an #ECache
+ * @deleted_flag: one of #ECacheDeletedFlag enum
+ * @where_clause: (nullable): an optional SQLite WHERE clause part, or %NULL
+ * @func: an #ECacheForeachFunc function to call for each object
+ * @user_data: user data for the @func
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Calls @func for each found object, which satisfies the criteria
+ * for both @deleted_flag and @where_clause.
+ *
+ * Note the @func should not call any SQLite commands, because it's invoked
+ * within a SELECT statement execution.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_foreach (ECache *cache,
+ ECacheDeletedFlag deleted_flag,
+ const gchar *where_clause,
+ ECacheForeachFunc func,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ struct ForeachData fe;
+ GString *stmt;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+ g_return_val_if_fail (func, FALSE);
+
+ stmt = g_string_new ("SELECT * FROM " E_CACHE_TABLE_OBJECTS);
+
+ if (where_clause) {
+ g_string_append (stmt, " WHERE ");
+
+ if (deleted_flag == E_CACHE_INCLUDE_DELETED) {
+ g_string_append (stmt, where_clause);
+ } else {
+ g_string_append_printf (stmt, E_CACHE_COLUMN_STATE "!=%d AND (%s)",
+ E_OFFLINE_STATE_LOCALLY_DELETED, where_clause);
+ }
+ } else if (deleted_flag != E_CACHE_INCLUDE_DELETED) {
+ g_string_append_printf (stmt, " WHERE " E_CACHE_COLUMN_STATE "!=%d",
E_OFFLINE_STATE_LOCALLY_DELETED);
+ }
+
+ fe.func = func;
+ fe.user_data = user_data;
+ fe.uid_index = -1;
+ fe.revision_index = -1;
+ fe.object_index = -1;
+ fe.state_index = -1;
+
+ success = e_cache_sqlite_exec_internal (cache, stmt->str, e_cache_foreach_cb, &fe, cancellable,
error);
+
+ g_string_free (stmt, TRUE);
+
+ return success;
+}
+
+struct ForeachUpdateRowData {
+ gchar *uid;
+ gchar *revision;
+ gchar *object;
+ EOfflineState offline_state;
+ gint ncols;
+ GPtrArray *column_values;
+};
+
+static void
+foreach_update_row_data_free (gpointer ptr)
+{
+ struct ForeachUpdateRowData *fr = ptr;
+
+ if (fr) {
+ g_free (fr->uid);
+ g_free (fr->revision);
+ g_free (fr->object);
+ g_ptr_array_free (fr->column_values, TRUE);
+ g_free (fr);
+ }
+}
+
+struct ForeachUpdateData {
+ gint uid_index;
+ gint revision_index;
+ gint object_index;
+ gint state_index;
+ GSList *rows; /* struct ForeachUpdateRowData * */
+ GPtrArray *column_names;
+};
+
+static gboolean
+e_cache_foreach_update_cb (ECache *cache,
+ gint ncols,
+ const gchar *column_names[],
+ const gchar *column_values[],
+ gpointer user_data)
+{
+ struct ForeachUpdateData *fu = user_data;
+ struct ForeachUpdateRowData *rd;
+ EOfflineState offline_state;
+ GPtrArray *cnames, *cvalues;
+ gint ii;
+
+ g_return_val_if_fail (fu != NULL, FALSE);
+ g_return_val_if_fail (column_names != NULL, FALSE);
+ g_return_val_if_fail (column_values != NULL, FALSE);
+
+ if (fu->uid_index == -1 ||
+ fu->revision_index == -1 ||
+ fu->object_index == -1 ||
+ fu->state_index == -1) {
+ gint ii;
+
+ for (ii = 0; ii < ncols && (fu->uid_index == -1 ||
+ fu->revision_index == -1 ||
+ fu->object_index == -1 ||
+ fu->state_index == -1); ii++) {
+ if (!column_names[ii])
+ continue;
+
+ if (fu->uid_index == -1 && g_ascii_strcasecmp (column_names[ii], E_CACHE_COLUMN_UID)
== 0) {
+ fu->uid_index = ii;
+ } else if (fu->revision_index == -1 && g_ascii_strcasecmp (column_names[ii],
E_CACHE_COLUMN_REVISION) == 0) {
+ fu->revision_index = ii;
+ } else if (fu->object_index == -1 && g_ascii_strcasecmp (column_names[ii],
E_CACHE_COLUMN_OBJECT) == 0) {
+ fu->object_index = ii;
+ } else if (fu->state_index == -1 && g_ascii_strcasecmp (column_names[ii],
E_CACHE_COLUMN_STATE) == 0) {
+ fu->state_index = ii;
+ }
+ }
+ }
+
+ g_return_val_if_fail (fu->uid_index >= 0 && fu->uid_index < ncols, FALSE);
+ g_return_val_if_fail (fu->revision_index >= 0 && fu->revision_index < ncols, FALSE);
+ g_return_val_if_fail (fu->object_index >= 0 && fu->object_index < ncols, FALSE);
+ g_return_val_if_fail (fu->state_index >= 0 && fu->state_index < ncols, FALSE);
+
+ if (!column_values[fu->state_index])
+ offline_state = E_OFFLINE_STATE_UNKNOWN;
+ else
+ offline_state = g_ascii_strtoull (column_values[fu->state_index], NULL, 10);
+
+ cnames = fu->column_names ? NULL : g_ptr_array_new_full (ncols, g_free);
+ cvalues = g_ptr_array_new_full (ncols, g_free);
+
+ for (ii = 0; ii < ncols; ii++) {
+ if (fu->uid_index == ii ||
+ fu->revision_index == ii ||
+ fu->object_index == ii ||
+ fu->state_index == ii) {
+ continue;
+ }
+
+ if (cnames)
+ g_ptr_array_add (cnames, g_strdup (column_names[ii]));
+
+ g_ptr_array_add (cvalues, g_strdup (column_values[ii]));
+ }
+
+ rd = g_new0 (struct ForeachUpdateRowData, 1);
+ rd->uid = g_strdup (column_values[fu->uid_index]);
+ rd->revision = g_strdup (column_values[fu->revision_index]);
+ rd->object = g_strdup (column_values[fu->object_index]);
+ rd->offline_state = offline_state;
+ rd->ncols = ncols;
+ rd->column_values = cvalues;
+
+ if (cnames)
+ fu->column_names = cnames;
+
+ fu->rows = g_slist_prepend (fu->rows, rd);
+
+ g_return_val_if_fail ((gint) fu->column_names->len != ncols, FALSE);
+
+ return TRUE;
+}
+
+/**
+ * e_cache_foreach_update:
+ * @cache: an #ECache
+ * @deleted_flag: one of #ECacheDeletedFlag enum
+ * @where_clause: (nullable): an optional SQLite WHERE clause part, or %NULL
+ * @func: an #ECacheUpdateFunc function to call for each object
+ * @user_data: user data for the @func
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Calls @func for each found object, which satisfies the criteria for both
+ * @deleted_flag and @where_clause, letting the caller update values where
+ * necessary. The return value of @func is used to determine whether the call
+ * was successful, not whether there are any changes to be saved. If anything
+ * fails during the call then the all changes are reverted.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_foreach_update (ECache *cache,
+ ECacheDeletedFlag deleted_flag,
+ const gchar *where_clause,
+ ECacheUpdateFunc func,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GString *stmt_begin;
+ gchar *uid = NULL;
+ gint n_results;
+ gboolean has_where = TRUE;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+ g_return_val_if_fail (func, FALSE);
+
+ e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+
+ stmt_begin = g_string_new ("SELECT * FROM " E_CACHE_TABLE_OBJECTS);
+
+ if (where_clause) {
+ g_string_append (stmt_begin, " WHERE ");
+
+ if (deleted_flag == E_CACHE_INCLUDE_DELETED) {
+ g_string_append (stmt_begin, where_clause);
+ } else {
+ g_string_append_printf (stmt_begin, E_CACHE_COLUMN_STATE "!=%d AND (%s)",
+ E_OFFLINE_STATE_LOCALLY_DELETED, where_clause);
+ }
+ } else if (deleted_flag != E_CACHE_INCLUDE_DELETED) {
+ g_string_append_printf (stmt_begin, " WHERE " E_CACHE_COLUMN_STATE "!=%d",
E_OFFLINE_STATE_LOCALLY_DELETED);
+ } else {
+ has_where = FALSE;
+ }
+
+ do {
+ GString *stmt;
+ GSList *link;
+ struct ForeachUpdateData fu;
+
+ fu.uid_index = -1;
+ fu.revision_index = -1;
+ fu.object_index = -1;
+ fu.state_index = -1;
+ fu.rows = NULL;
+ fu.column_names = NULL;
+
+ stmt = g_string_new (stmt_begin->str);
+
+ if (uid) {
+ if (has_where)
+ g_string_append (stmt, " AND ");
+ else
+ g_string_append (stmt, " WHERE ");
+
+ e_cache_sqlite_stmt_append_printf (stmt, E_CACHE_COLUMN_UID ">%Q", uid);
+ }
+
+ g_string_append_printf (stmt, " ORDER BY " E_CACHE_COLUMN_UID " ASC LIMIT %d",
E_CACHE_UPDATE_BATCH_SIZE);
+
+ success = e_cache_sqlite_exec_internal (cache, stmt->str, e_cache_foreach_update_cb, &fu,
cancellable, error);
+
+ g_string_free (stmt, TRUE);
+
+ if (success) {
+ n_results = 0;
+ fu.rows = g_slist_reverse (fu.rows);
+
+ for (link = fu.rows; success && link; link = g_slist_next (link), n_results++) {
+ struct ForeachUpdateRowData *fr = link->data;
+
+ success = fr && fr->column_values && fu.column_names;
+ if (success) {
+ gchar *new_revision = NULL;
+ gchar *new_object = NULL;
+ EOfflineState new_offline_state = fr->offline_state;
+ ECacheColumnValues *new_other_columns = NULL;
+
+ success = func (cache, fr->uid, fr->revision, fr->object,
fr->offline_state,
+ fr->ncols, (const gchar **) fu.column_names->pdata,
+ (const gchar **) fr->column_values->pdata,
+ &new_revision, &new_object, &new_offline_state,
&new_other_columns,
+ user_data);
+
+ if (success && (
+ (new_revision && g_strcmp0 (new_revision, fr->revision) != 0) ||
+ (new_object && g_strcmp0 (new_object, fr->object) != 0) ||
+ (new_offline_state != fr->offline_state) ||
+ (new_other_columns && e_cache_column_values_get_size
(new_other_columns) > 0))) {
+ success = e_cache_put_locked (cache,
+ fr->uid,
+ new_revision ? new_revision : fr->revision,
+ new_object ? new_object : fr->object,
+ new_other_columns,
+ new_offline_state,
+ TRUE, cancellable, error);
+ }
+
+ g_free (new_revision);
+ g_free (new_object);
+ e_cache_column_values_free (new_other_columns);
+
+ if (!g_slist_next (link)) {
+ g_free (uid);
+ uid = g_strdup (fr->uid);
+ }
+ }
+ }
+ }
+
+ g_slist_free_full (fu.rows, foreach_update_row_data_free);
+ if (fu.column_names)
+ g_ptr_array_free (fu.column_names, TRUE);
+ } while (success && n_results == E_CACHE_UPDATE_BATCH_SIZE);
+
+ g_string_free (stmt_begin, TRUE);
+ g_free (uid);
+
+ e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+ return success;
+}
+
+/**
+ * e_cache_get_offline_state:
+ * @cache: an #ECache
+ * @uid: a unique identifier of an object
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Returns: Current offline state #EOfflineState for the given object.
+ * It returns %E_OFFLINE_STATE_UNKNOWN when the object could not be
+ * found or other error happened.
+ *
+ * Since: 3.26
+ **/
+EOfflineState
+e_cache_get_offline_state (ECache *cache,
+ const gchar *uid,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EOfflineState offline_state = E_OFFLINE_STATE_UNKNOWN;
+ gint64 value = offline_state;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), E_OFFLINE_STATE_UNKNOWN);
+ g_return_val_if_fail (uid != NULL, E_OFFLINE_STATE_UNKNOWN);
+
+ if (!e_cache_contains (cache, uid, E_CACHE_INCLUDE_DELETED)) {
+ g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s” not found"), uid);
+ return offline_state;
+ }
+
+ if (e_cache_sqlite_exec_printf (cache,
+ "SELECT " E_CACHE_COLUMN_STATE " FROM " E_CACHE_TABLE_OBJECTS
+ " WHERE " E_CACHE_COLUMN_UID " = %Q",
+ e_cache_get_int64_cb, &value, cancellable, error,
+ uid)) {
+ offline_state = value;
+ }
+
+ return offline_state;
+}
+
+/**
+ * e_cache_set_offline_state:
+ * @cache: an #ECache
+ * @uid: a unique identifier of an object
+ * @state: an #EOfflineState to set
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Sets an offline @state for the object identified by @uid.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_set_offline_state (ECache *cache,
+ const gchar *uid,
+ EOfflineState state,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ if (!e_cache_contains (cache, uid, E_CACHE_INCLUDE_DELETED)) {
+ g_set_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND, _("Object “%s” not found"), uid);
+ return FALSE;
+ }
+
+ return e_cache_sqlite_exec_printf (cache,
+ "UPDATE " E_CACHE_TABLE_OBJECTS " SET " E_CACHE_COLUMN_STATE "=%d"
+ " WHERE " E_CACHE_COLUMN_UID " = %Q",
+ NULL, NULL, cancellable, error,
+ state, uid);
+}
+
+static gboolean
+e_cache_get_offline_changes_cb (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ EOfflineState offline_state,
+ gint ncols,
+ const gchar *column_names[],
+ const gchar *column_values[],
+ gpointer user_data)
+{
+ GSList **pchanges = user_data;
+
+ g_return_val_if_fail (pchanges != NULL, FALSE);
+
+ if (offline_state == E_OFFLINE_STATE_LOCALLY_CREATED ||
+ offline_state == E_OFFLINE_STATE_LOCALLY_MODIFIED ||
+ offline_state == E_OFFLINE_STATE_LOCALLY_DELETED) {
+ *pchanges = g_slist_prepend (*pchanges, e_cache_offline_change_new (uid, revision, object,
offline_state));
+ }
+
+ return TRUE;
+}
+
+/**
+ * e_cache_get_offline_changes:
+ * @cache: an #ECache
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gathers the list of all offline changes being done so far.
+ * The returned #GSList contains #ECacheOfflineChange structure.
+ * Use e_cache_clear_offline_changes() to clear all offline
+ * changes at once.
+ *
+ * Returns: (transfer full) (element-type ECacheOfflineChange): A newly allocated list of all
+ * offline changes. Free it with g_slist_free_full (slist, e_cache_offline_change_free);
+ * when no longer needed.
+ *
+ * Since: 3.26
+ **/
+GSList *
+e_cache_get_offline_changes (ECache *cache,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GSList *changes = NULL;
+ gchar *stmt;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), NULL);
+
+ stmt = e_cache_sqlite_stmt_printf (E_CACHE_COLUMN_STATE "!=%d", E_OFFLINE_STATE_SYNCED);
+
+ if (!e_cache_foreach (cache, E_CACHE_INCLUDE_DELETED, stmt, e_cache_get_offline_changes_cb, &changes,
cancellable, error)) {
+ g_slist_free_full (changes, e_cache_offline_change_free);
+ changes = NULL;
+ }
+
+ e_cache_sqlite_stmt_free (stmt);
+
+ return changes;
+}
+
+/**
+ * e_cache_clear_offline_changes:
+ * @cache: an #ECache
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Marks all objects as being fully synchronized with the server and
+ * removes those which are marked as locally deleted.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_clear_offline_changes (ECache *cache,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECacheClass *klass;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+
+ klass = E_CACHE_GET_CLASS (cache);
+ g_return_val_if_fail (klass != NULL, FALSE);
+ g_return_val_if_fail (klass->clear_offline_changes_locked != NULL, FALSE);
+
+ e_cache_lock (cache, E_CACHE_LOCK_WRITE);
+
+ success = klass->clear_offline_changes_locked (cache, cancellable, error);
+
+ e_cache_unlock (cache, success ? E_CACHE_UNLOCK_COMMIT : E_CACHE_UNLOCK_ROLLBACK);
+
+ return success;
+}
+
+/**
+ * e_cache_set_key:
+ * @cache: an #ECache
+ * @key: a key name
+ * @value: (nullable): a value to set, or %NULL to delete the key
+ * @error: return location for a #GError, or %NULL
+ *
+ * Sets a @value of the user @key, or deletes it, if the @value is %NULL.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_set_key (ECache *cache,
+ const gchar *key,
+ const gchar *value,
+ GError **error)
+{
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+ g_return_val_if_fail (key != NULL, FALSE);
+
+ return e_cache_set_key_internal (cache, TRUE, key, value, error);
+}
+
+/**
+ * e_cache_dup_key:
+ * @cache: an #ECache
+ * @key: a key name
+ * @error: return location for a #GError, or %NULL
+ *
+ * Returns: (transfer full): a value of the @key. Free the returned string
+ * with g_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_cache_dup_key (ECache *cache,
+ const gchar *key,
+ GError **error)
+{
+ g_return_val_if_fail (E_IS_CACHE (cache), NULL);
+ g_return_val_if_fail (key != NULL, NULL);
+
+ return e_cache_dup_key_internal (cache, TRUE, key, error);
+}
+
+/**
+ * e_cache_set_key_int:
+ * @cache: an #ECache
+ * @key: a key name
+ * @value: an integer value to set
+ * @error: return location for a #GError, or %NULL
+ *
+ * Sets an integer @value for the user @key.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_set_key_int (ECache *cache,
+ const gchar *key,
+ gint value,
+ GError **error)
+{
+ gchar *str_value;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+ g_return_val_if_fail (key != NULL, FALSE);
+
+ str_value = g_strdup_printf ("%d", value);
+ success = e_cache_set_key (cache, key, str_value, error);
+ g_free (str_value);
+
+ return success;
+}
+
+/**
+ * e_cache_get_key_int:
+ * @cache: an #ECache
+ * @key: a key name
+ * @error: return location for a #GError, or %NULL
+ *
+ * Reads the user @key value as an integer.
+ *
+ * Returns: The user @key value or -1 on error.
+ *
+ * Since: 3.26
+ **/
+gint
+e_cache_get_key_int (ECache *cache,
+ const gchar *key,
+ GError **error)
+{
+ gchar *str_value;
+ gint value;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), -1);
+
+ str_value = e_cache_dup_key (cache, key, error);
+ if (!str_value)
+ return -1;
+
+ value = g_ascii_strtoll (str_value, NULL, 10);
+ g_free (str_value);
+
+ return value;
+}
+
+/**
+ * e_cache_lock:
+ * @cache: an #ECache
+ * @lock_type: an #ECacheLockType
+ *
+ * Locks the @cache thus other threads cannot use it.
+ * This can be called recursively within one thread.
+ * Each call should have its pair e_cache_unlock().
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_lock (ECache *cache,
+ ECacheLockType lock_type)
+{
+ g_return_if_fail (E_IS_CACHE (cache));
+
+ g_rec_mutex_lock (&cache->priv->lock);
+
+ cache->priv->in_transaction++;
+ g_return_if_fail (cache->priv->in_transaction > 0);
+
+ if (cache->priv->in_transaction == 1) {
+ /* It's important to make the distinction between a
+ * transaction which will read or one which will write.
+ *
+ * While it's not well documented, when receiving the SQLITE_BUSY
+ * error status, one can only safely retry at the beginning of
+ * the transaction.
+ *
+ * If a transaction is 'upgraded' to require a writer lock
+ * half way through the transaction and SQLITE_BUSY is returned,
+ * the whole transaction would need to be retried from the beginning.
+ */
+ cache->priv->lock_type = lock_type;
+
+ switch (lock_type) {
+ case E_CACHE_LOCK_READ:
+ e_cache_sqlite_exec_internal (cache, "BEGIN", NULL, NULL, NULL, NULL);
+ break;
+ case E_CACHE_LOCK_WRITE:
+ e_cache_sqlite_exec_internal (cache, "BEGIN IMMEDIATE", NULL, NULL, NULL, NULL);
+ break;
+ }
+ } else {
+ /* Warn about cases where where a read transaction might be upgraded */
+ if (lock_type == E_CACHE_LOCK_WRITE && cache->priv->lock_type == E_CACHE_LOCK_READ)
+ g_warning (
+ "A nested transaction wants to write, "
+ "but the outermost transaction was started "
+ "without a writer lock.");
+ }
+}
+
+/**
+ * e_cache_unlock:
+ * @cache: an #ECache
+ * @action: an #ECacheUnlockAction
+ *
+ * Unlocks the cache which was previouly locked with e_cache_lock().
+ * The cache locked with #E_CACHE_LOCK_WRITE should use either
+ * @action #E_CACHE_UNLOCK_COMMIT or #E_CACHE_UNLOCK_ROLLBACK,
+ * while the #E_CACHE_LOCK_READ should use #E_CACHE_UNLOCK_NONE @action.
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_unlock (ECache *cache,
+ ECacheUnlockAction action)
+{
+ g_return_if_fail (E_IS_CACHE (cache));
+ g_return_if_fail (cache->priv->in_transaction > 0);
+
+ cache->priv->in_transaction--;
+
+ if (cache->priv->in_transaction == 0) {
+ switch (action) {
+ case E_CACHE_UNLOCK_NONE:
+ case E_CACHE_UNLOCK_COMMIT:
+ e_cache_sqlite_exec_internal (cache, "COMMIT", NULL, NULL, NULL, NULL);
+ break;
+ case E_CACHE_UNLOCK_ROLLBACK:
+ e_cache_sqlite_exec_internal (cache, "ROLLBACK", NULL, NULL, NULL, NULL);
+ break;
+ }
+ }
+
+ g_rec_mutex_unlock (&cache->priv->lock);
+}
+
+/**
+ * e_cache_get_sqlitedb:
+ * @cache: an #ECache
+ *
+ * Returns: (transfer none): An SQLite3 database pointer. It is owned by the @cache.
+ *
+ * Since: 3.26
+ **/
+gpointer
+e_cache_get_sqlitedb (ECache *cache)
+{
+ g_return_val_if_fail (E_IS_CACHE (cache), NULL);
+
+ return cache->priv->db;
+}
+
+/**
+ * e_cache_sqlite_exec:
+ * @cache: an #ECache
+ * @sql_stmt: an SQLite statement to execute
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Executes an SQLite statement. Use e_cache_sqlite_select() for
+ * SELECT statements.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_sqlite_exec (ECache *cache,
+ const gchar *sql_stmt,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+
+ return e_cache_sqlite_exec_internal (cache, sql_stmt, NULL, NULL, cancellable, error);
+}
+
+/**
+ * e_cache_sqlite_select:
+ * @cache: an #ECache
+ * @sql_stmt: an SQLite SELECT statement to execute
+ * @func: an #ECacheSelectFunc function to call for each row
+ * @user_data: user data for @func
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Executes a SELECT statement @sql_stmt and calls @func for each row of the result.
+ * Use e_cache_sqlite_exec() for statements which do not return row sets.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_sqlite_select (ECache *cache,
+ const gchar *sql_stmt,
+ ECacheSelectFunc func,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+ g_return_val_if_fail (sql_stmt, FALSE);
+ g_return_val_if_fail (func, FALSE);
+
+ return e_cache_sqlite_exec_internal (cache, sql_stmt, func, user_data, cancellable, error);
+}
+
+/**
+ * e_cache_sqlite_stmt_append_printf:
+ * @stmt: a #GString statement to append to
+ * @format: a printf-like format
+ * @...: arguments for the @format
+ *
+ * Appends an SQLite statement fragment based on the @format and
+ * its arguments to the @stmt.
+ * The @format can contain any values recognized by sqlite3_mprintf().
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_sqlite_stmt_append_printf (GString *stmt,
+ const gchar *format,
+ ...)
+{
+ va_list args;
+ gchar *tmp_stmt;
+
+ g_return_if_fail (stmt != NULL);
+ g_return_if_fail (format != NULL);
+
+ va_start (args, format);
+ tmp_stmt = sqlite3_vmprintf (format, args);
+ va_end (args);
+
+ g_string_append (stmt, tmp_stmt);
+
+ sqlite3_free (tmp_stmt);
+}
+
+/**
+ * e_cache_sqlite_stmt_printf:
+ * @format: a printf-like format
+ * @...: arguments for the @format
+ *
+ * Creates an SQLite statement based on the @format and its arguments.
+ * The @format can contain any values recognized by sqlite3_mprintf().
+ *
+ * Returns: (transfer full): A new SQLite statement. Free the returned
+ * string with e_cache_sqlite_stmt_free() when no longer needed.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_cache_sqlite_stmt_printf (const gchar *format,
+ ...)
+{
+ va_list args;
+ gchar *stmt;
+
+ g_return_val_if_fail (format != NULL, NULL);
+
+ va_start (args, format);
+ stmt = sqlite3_vmprintf (format, args);
+ va_end (args);
+
+ return stmt;
+}
+
+/**
+ * e_cache_sqlite_stmt_free:
+ * @stmt: a statement to free
+ *
+ * Frees a statement previously constructed with e_cache_sqlite_stmt_printf().
+ *
+ * Since: 3.26
+ **/
+void
+e_cache_sqlite_stmt_free (gchar *stmt)
+{
+ if (stmt)
+ sqlite3_free (stmt);
+}
+
+/**
+ * e_cache_sqlite_maybe_vacuum:
+ * @cache: an #ECache
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Runs vacuum (compacts the database file), if needed.
+ *
+ * Returns: Whether succeeded. It doesn't mean that the vacuum had been run,
+ * only that no error happened during the call.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_cache_sqlite_maybe_vacuum (ECache *cache,
+ GCancellable *cancellable,
+ GError **error)
+{
+ guint64 page_count = 0, page_size = 0, freelist_count = 0;
+ gboolean success = FALSE;
+ GError *local_error = NULL;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+
+ g_rec_mutex_lock (&cache->priv->lock);
+
+ if (e_cache_sqlite_exec_internal (cache, "PRAGMA page_count;", e_cache_get_uint64_cb, &page_count,
cancellable, &local_error) &&
+ e_cache_sqlite_exec_internal (cache, "PRAGMA page_size;", e_cache_get_uint64_cb, &page_size,
cancellable, &local_error) &&
+ e_cache_sqlite_exec_internal (cache, "PRAGMA freelist_count;", e_cache_get_uint64_cb,
&freelist_count, cancellable, &local_error)) {
+ /* Vacuum, if there's more than 5% of the free pages, or when free pages use more than 10MB */
+ success = !page_count || !freelist_count ||
+ (freelist_count * page_size < 1024 * 1024 * 10 && freelist_count * 1000 / page_count
<= 50) ||
+ e_cache_sqlite_exec_internal (cache, "vacuum;", NULL, NULL, cancellable,
&local_error);
+ }
+
+ g_rec_mutex_unlock (&cache->priv->lock);
+
+ if (local_error) {
+ g_propagate_error (error, local_error);
+ success = FALSE;
+ }
+
+ return success;
+}
+
+static gboolean
+e_cache_put_locked_default (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ ECacheColumnValues *other_columns,
+ EOfflineState offline_state,
+ gboolean is_replace,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GString *statement, *other_names = NULL, *other_values = NULL;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (object != NULL, FALSE);
+
+ statement = g_string_sized_new (255);
+
+ e_cache_sqlite_stmt_append_printf (statement, "INSERT OR REPLACE INTO %Q ("
+ E_CACHE_COLUMN_UID ","
+ E_CACHE_COLUMN_REVISION ","
+ E_CACHE_COLUMN_OBJECT ","
+ E_CACHE_COLUMN_STATE,
+ E_CACHE_TABLE_OBJECTS);
+
+ if (other_columns) {
+ GHashTableIter iter;
+ gpointer key, value;
+
+ e_cache_column_values_init_iter (other_columns, &iter);
+ while (g_hash_table_iter_next (&iter, &key, &value)) {
+ if (!other_names)
+ other_names = g_string_new ("");
+ g_string_append (other_names, ",");
+
+ e_cache_sqlite_stmt_append_printf (other_names, "%Q", key);
+
+ if (!other_values)
+ other_values = g_string_new ("");
+
+ g_string_append (other_values, ",");
+ if (value) {
+ e_cache_sqlite_stmt_append_printf (other_values, "%Q", value);
+ } else {
+ g_string_append (other_values, "NULL");
+ }
+ }
+ }
+
+ if (other_names)
+ g_string_append (statement, other_names->str);
+
+ g_string_append (statement, ") VALUES (");
+
+ e_cache_sqlite_stmt_append_printf (statement, "%Q,%Q,%Q,%d", uid, revision ? revision : "", object,
offline_state);
+
+ if (other_values)
+ g_string_append (statement, other_values->str);
+
+ g_string_append (statement, ")");
+
+ success = e_cache_sqlite_exec_internal (cache, statement->str, NULL, NULL, cancellable, error);
+
+ if (other_names)
+ g_string_free (other_names, TRUE);
+ if (other_values)
+ g_string_free (other_values, TRUE);
+ g_string_free (statement, TRUE);
+
+ return success;
+}
+
+static gboolean
+e_cache_remove_locked_default (ECache *cache,
+ const gchar *uid,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+
+ g_signal_emit (cache,
+ signals[BEFORE_REMOVE],
+ 0,
+ uid, cancellable, error,
+ &success);
+
+ success = success && e_cache_sqlite_exec_printf (cache,
+ "DELETE FROM " E_CACHE_TABLE_OBJECTS " WHERE " E_CACHE_COLUMN_UID " = %Q",
+ NULL, NULL, cancellable, error,
+ uid);
+
+ return success;
+}
+
+static gboolean
+e_cache_remove_all_locked_default (ECache *cache,
+ const GSList *uids,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const GSList *link;
+ gboolean success = TRUE;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+
+ for (link = uids; link && success; link = g_slist_next (link)) {
+ const gchar *uid = link->data;
+
+ g_signal_emit (cache,
+ signals[BEFORE_REMOVE],
+ 0,
+ uid, cancellable, error,
+ &success);
+ }
+
+ if (success) {
+ success = e_cache_sqlite_exec_printf (cache,
+ "DELETE FROM " E_CACHE_TABLE_OBJECTS,
+ NULL, NULL, cancellable, error);
+ }
+
+ return success;
+}
+
+static gboolean
+e_cache_clear_offline_changes_locked_default (ECache *cache,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_CACHE (cache), FALSE);
+
+ success = e_cache_sqlite_exec_printf (cache,
+ "DELETE FROM " E_CACHE_TABLE_OBJECTS " WHERE " E_CACHE_COLUMN_STATE "=%d",
+ NULL, NULL, cancellable, error,
+ E_OFFLINE_STATE_LOCALLY_DELETED);
+
+ success = success && e_cache_sqlite_exec_printf (cache,
+ "UPDATE " E_CACHE_TABLE_OBJECTS " SET " E_CACHE_COLUMN_STATE "=%d"
+ " WHERE " E_CACHE_COLUMN_STATE "!=%d",
+ NULL, NULL, cancellable, error,
+ E_OFFLINE_STATE_SYNCED, E_OFFLINE_STATE_SYNCED);
+
+ return success;
+}
+
+static gboolean
+e_cache_signals_accumulator (GSignalInvocationHint *ihint,
+ GValue *return_accu,
+ const GValue *handler_return,
+ gpointer data)
+{
+ gboolean handler_result;
+
+ handler_result = g_value_get_boolean (handler_return);
+ g_value_set_boolean (return_accu, handler_result);
+
+ return handler_result;
+}
+
+static gboolean
+e_cache_before_put_default (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ ECacheColumnValues *other_columns,
+ gboolean is_replace,
+ GCancellable *cancellable,
+ GError **error)
+{
+ return TRUE;
+}
+
+static gboolean
+e_cache_before_remove_default (ECache *cache,
+ const gchar *uid,
+ GCancellable *cancellable,
+ GError **error)
+{
+ return TRUE;
+}
+
+static void
+e_cache_finalize (GObject *object)
+{
+ ECache *cache = E_CACHE (object);
+
+ g_free (cache->priv->filename);
+ cache->priv->filename = NULL;
+
+ if (cache->priv->db) {
+ sqlite3_close (cache->priv->db);
+ cache->priv->db = NULL;
+ }
+
+ g_rec_mutex_clear (&cache->priv->lock);
+
+ g_warn_if_fail (cache->priv->cancellable == NULL);
+ g_clear_object (&cache->priv->cancellable);
+
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_cache_parent_class)->finalize (object);
+}
+
+static void
+e_cache_class_init (ECacheClass *klass)
+{
+ GObjectClass *object_class;
+
+ g_type_class_add_private (klass, sizeof (ECachePrivate));
+
+ object_class = G_OBJECT_CLASS (klass);
+ object_class->finalize = e_cache_finalize;
+
+ klass->put_locked = e_cache_put_locked_default;
+ klass->remove_locked = e_cache_remove_locked_default;
+ klass->remove_all_locked = e_cache_remove_all_locked_default;
+ klass->clear_offline_changes_locked = e_cache_clear_offline_changes_locked_default;
+ klass->before_put = e_cache_before_put_default;
+ klass->before_remove = e_cache_before_remove_default;
+
+ signals[BEFORE_PUT] = g_signal_new (
+ "before-put",
+ G_OBJECT_CLASS_TYPE (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (ECacheClass, before_put),
+ e_cache_signals_accumulator,
+ NULL,
+ g_cclosure_marshal_generic,
+ G_TYPE_BOOLEAN, 7,
+ G_TYPE_STRING,
+ G_TYPE_STRING,
+ G_TYPE_STRING,
+ G_TYPE_HASH_TABLE,
+ G_TYPE_BOOLEAN,
+ G_TYPE_CANCELLABLE,
+ G_TYPE_POINTER);
+
+ signals[BEFORE_REMOVE] = g_signal_new (
+ "before-remove",
+ G_OBJECT_CLASS_TYPE (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (ECacheClass, before_remove),
+ e_cache_signals_accumulator,
+ NULL,
+ g_cclosure_marshal_generic,
+ G_TYPE_BOOLEAN, 3,
+ G_TYPE_STRING,
+ G_TYPE_CANCELLABLE,
+ G_TYPE_POINTER);
+
+ signals[REVISION_CHANGED] = g_signal_new (
+ "revision-changed",
+ G_OBJECT_CLASS_TYPE (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (ECacheClass, revision_changed),
+ NULL,
+ NULL,
+ g_cclosure_marshal_generic,
+ G_TYPE_NONE, 0,
+ G_TYPE_NONE);
+
+ e_sqlite3_vfs_init ();
+}
+
+static void
+e_cache_init (ECache *cache)
+{
+ cache->priv = G_TYPE_INSTANCE_GET_PRIVATE (cache, E_TYPE_CACHE, ECachePrivate);
+
+ cache->priv->filename = NULL;
+ cache->priv->db = NULL;
+ cache->priv->cancellable = NULL;
+ cache->priv->in_transaction = 0;
+ cache->priv->revision_change_frozen = 0;
+ cache->priv->revision_counter = 0;
+ cache->priv->last_revision_time = 0;
+ cache->priv->needs_revision_change = FALSE;
+
+ g_rec_mutex_init (&cache->priv->lock);
+}
diff --git a/src/libebackend/e-cache.h b/src/libebackend/e-cache.h
new file mode 100644
index 0000000..18b3ed7
--- /dev/null
+++ b/src/libebackend/e-cache.h
@@ -0,0 +1,524 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#if !defined (__LIBEBACKEND_H_INSIDE__) && !defined (LIBEBACKEND_COMPILATION)
+#error "Only <libebackend/libebackend.h> should be included directly."
+#endif
+
+#ifndef E_CACHE_H
+#define E_CACHE_H
+
+#include <glib-object.h>
+#include <gio/gio.h>
+#include <libebackend/e-backend-enums.h>
+
+/* Standard GObject macros */
+#define E_TYPE_CACHE \
+ (e_cache_get_type ())
+#define E_CACHE(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST \
+ ((obj), E_TYPE_CACHE, ECache))
+#define E_CACHE_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_CAST \
+ ((cls), E_TYPE_CACHE, ECacheClass))
+#define E_IS_CACHE(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE \
+ ((obj), E_TYPE_CACHE))
+#define E_IS_CACHE_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_TYPE \
+ ((cls), E_TYPE_CACHE))
+#define E_CACHE_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS \
+ ((obj), E_TYPE_CACHE, ECacheClass))
+
+G_BEGIN_DECLS
+
+#define E_CACHE_TABLE_OBJECTS "ECacheObjects"
+#define E_CACHE_TABLE_KEYS "ECacheKeys"
+
+#define E_CACHE_COLUMN_UID "ECacheUID"
+#define E_CACHE_COLUMN_REVISION "ECacheREV"
+#define E_CACHE_COLUMN_OBJECT "ECacheOBJ"
+#define E_CACHE_COLUMN_STATE "ECacheState"
+
+/**
+ * E_CACHE_ERROR:
+ *
+ * Error domain for #ECache operations.
+ *
+ * Since: 3.26
+ **/
+#define E_CACHE_ERROR (e_cache_error_quark ())
+
+GQuark e_cache_error_quark (void);
+
+/**
+ * ECacheError:
+ * @E_CACHE_ERROR_ENGINE: An error was reported from the SQLite engine
+ * @E_CACHE_ERROR_CONSTRAINT: The error occurred due to an explicit constraint, like
+ * when attempting to add two objects with the same UID.
+ * @E_CACHE_ERROR_NOT_FOUND: An object was not found by UID (this is
+ * different from a query that returns no results, which is not an error).
+ * @E_CACHE_ERROR_INVALID_QUERY: A query was invalid.
+ * @E_CACHE_ERROR_UNSUPPORTED_FIELD: A field requested for inclusion in summary is not supported.
+ * @E_CACHE_ERROR_UNSUPPORTED_QUERY: A query was not supported.
+ * @E_CACHE_ERROR_END_OF_LIST: An attempt was made to fetch results past the end of a the list.
+ * @E_CACHE_ERROR_LOAD: An error occured while loading or creating the database.
+ *
+ * Defines the types of possible errors reported by the #ECache
+ *
+ * Since: 3.26
+ */
+typedef enum {
+ E_CACHE_ERROR_ENGINE,
+ E_CACHE_ERROR_CONSTRAINT,
+ E_CACHE_ERROR_NOT_FOUND,
+ E_CACHE_ERROR_INVALID_QUERY,
+ E_CACHE_ERROR_UNSUPPORTED_FIELD,
+ E_CACHE_ERROR_UNSUPPORTED_QUERY,
+ E_CACHE_ERROR_END_OF_LIST,
+ E_CACHE_ERROR_LOAD
+} ECacheError;
+
+typedef struct _ECacheColumnValues ECacheColumnValues;
+
+#define E_TYPE_CACHE_COLUMN_VALUES (e_cache_column_values_get_type ())
+GType e_cache_column_values_get_type (void) G_GNUC_CONST;
+ECacheColumnValues *
+ e_cache_column_values_new (void);
+ECacheColumnValues *
+ e_cache_column_values_copy (ECacheColumnValues *other_columns);
+void e_cache_column_values_free (ECacheColumnValues *other_columns);
+void e_cache_column_values_put (ECacheColumnValues *other_columns,
+ const gchar *name,
+ const gchar *value);
+void e_cache_column_values_take_value(ECacheColumnValues *other_columns,
+ const gchar *name,
+ gchar *value);
+void e_cache_column_values_take (ECacheColumnValues *other_columns,
+ gchar *name,
+ gchar *value);
+gboolean e_cache_column_values_contains (ECacheColumnValues *other_columns,
+ const gchar *name);
+gboolean e_cache_column_values_remove (ECacheColumnValues *other_columns,
+ const gchar *name);
+void e_cache_column_values_remove_all(ECacheColumnValues *other_columns);
+const gchar * e_cache_column_values_lookup (ECacheColumnValues *other_columns,
+ const gchar *name);
+guint e_cache_column_values_get_size (ECacheColumnValues *other_columns);
+void e_cache_column_values_init_iter (ECacheColumnValues *other_columns,
+ GHashTableIter *iter);
+
+/**
+ * ECacheOfflineChange:
+ * @uid: UID of the object
+ * @revision: stored revision of the object
+ * @object: the object itself
+ * @state: an #EOfflineState of the object
+ *
+ * Holds the information about offline change for one object.
+ *
+ * Since: 3.26
+ **/
+typedef struct {
+ gchar *uid;
+ gchar *revision;
+ gchar *object;
+ EOfflineState state;
+} ECacheOfflineChange;
+
+#define E_TYPE_CACHE_OFFLINE_CHANGE (e_cache_offline_change_get_type ())
+
+GType e_cache_offline_change_get_type (void) G_GNUC_CONST;
+ECacheOfflineChange *
+ e_cache_offline_change_new (const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ EOfflineState state);
+ECacheOfflineChange *
+ e_cache_offline_change_copy (const ECacheOfflineChange *change);
+void e_cache_offline_change_free (/* ECacheOfflineChange */ gpointer change);
+
+typedef struct {
+ gchar *name;
+ gchar *type;
+ gchar *index_name;
+} ECacheColumnInfo;
+
+#define E_TYPE_CACHE_COLUMN_INFO (e_cache_column_info_get_type ())
+GType e_cache_column_info_get_type (void) G_GNUC_CONST;
+ECacheColumnInfo *
+ e_cache_column_info_new (const gchar *name,
+ const gchar *type,
+ const gchar *index_name);
+ECacheColumnInfo *
+ e_cache_column_info_copy (const ECacheColumnInfo *info);
+void e_cache_column_info_free (/* ECacheColumnInfo */ gpointer info);
+
+/**
+ * ECacheLockType:
+ * @E_CACHE_LOCK_READ: Obtain a lock for reading.
+ * @E_CACHE_LOCK_WRITE: Obtain a lock for writing. This also starts a transaction.
+ *
+ * Indicates the type of lock requested in e_cache_lock().
+ *
+ * Since: 3.26
+ **/
+typedef enum {
+ E_CACHE_LOCK_READ,
+ E_CACHE_LOCK_WRITE
+} ECacheLockType;
+
+/**
+ * ECacheUnlockAction:
+ * @E_CACHE_UNLOCK_NONE: Just unlock, this is appropriate for locks which were obtained with
%E_CACHE_LOCK_READ.
+ * @E_CACHE_UNLOCK_COMMIT: Commit any modifications which were made while the lock was held.
+ * @E_CACHE_UNLOCK_ROLLBACK: Rollback any modifications which were made while the lock was held.
+ *
+ * Indicates what type of action to take while unlocking the cache with e_cache_unlock().
+ *
+ * Since: 3.26
+ **/
+typedef enum {
+ E_CACHE_UNLOCK_NONE,
+ E_CACHE_UNLOCK_COMMIT,
+ E_CACHE_UNLOCK_ROLLBACK
+} ECacheUnlockAction;
+
+/**
+ * ECacheDeletedFlag:
+ * @E_CACHE_EXCLUDE_DELETED: Do not include locally deleted objects
+ * @E_CACHE_INCLUDE_DELETED: Include locally deleted objects
+ *
+ * Declares whether to exclude or include locally deleted objects.
+ *
+ * Since: 3.26
+ **/
+typedef enum {
+ E_CACHE_EXCLUDE_DELETED = 0,
+ E_CACHE_INCLUDE_DELETED
+} ECacheDeletedFlag;
+
+/**
+ * ECacheOfflineFlag:
+ * @E_CACHE_OFFLINE_UNKNOWN: Do not know current online/offline state
+ * @E_CACHE_IS_ONLINE: The operation is done in online
+ * @E_CACHE_IS_OFFLINE: The operation is done in offline
+ *
+ * Declares whether the operation is done in online or offline.
+ * This influences the offline state of the related objects.
+ *
+ * Since: 3.26
+ **/
+typedef enum {
+ E_CACHE_OFFLINE_UNKNOWN = -1,
+ E_CACHE_IS_ONLINE = 0,
+ E_CACHE_IS_OFFLINE
+} ECacheOfflineFlag;
+
+typedef struct _ECache ECache;
+typedef struct _ECacheClass ECacheClass;
+typedef struct _ECachePrivate ECachePrivate;
+
+/**
+ * ECacheForeachFunc:
+ * @cache: an #ECache
+ * @uid: a unique object identifier
+ * @revision: the object revision
+ * @object: the object itself
+ * @offline_state: objects offline state, one of #EOfflineState
+ * @ncols: count of columns, items in column_names and column_values
+ * @column_names: column names
+ * @column_values: column values
+ * @user_data: user data, as used in e_cache_foreach()
+ *
+ * A callback called for each object row when using e_cache_foreach() function.
+ *
+ * Returns: %TRUE to continue, %FALSE to stop walk through.
+ *
+ * Since: 3.26
+ **/
+typedef gboolean (* ECacheForeachFunc) (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ EOfflineState offline_state,
+ gint ncols,
+ const gchar *column_names[],
+ const gchar *column_values[],
+ gpointer user_data);
+
+/**
+ * ECacheUpdateFunc:
+ * @cache: an #ECache
+ * @uid: a unique object identifier
+ * @revision: the object revision
+ * @object: the object itself
+ * @offline_state: objects offline state, one of #EOfflineState
+ * @ncols: count of columns, items in column_names and column_values
+ * @column_names: column names
+ * @column_values: column values
+ * @out_revision: (out): the new object revision to set; keep it untouched to not change
+ * @out_object: (out): the new object to set; keep it untouched to not change
+ * @out_offline_state: (out): the offline state to set; the default is the same as @offline_state
+ * @out_other_columns: (out) (transfer full): an #ECacheColumnValues with other columns to set; keep it
untouched to not change any
+ * @user_data: user data, as used in e_cache_foreach_update()
+ *
+ * A callback called for each object row when using e_cache_foreach_update() function.
+ * When all out parameters are left untouched, then the row is not changed.
+ *
+ * Returns: %TRUE to continue, %FALSE to stop walk through.
+ *
+ * Since: 3.26
+ **/
+typedef gboolean (* ECacheUpdateFunc) (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ EOfflineState offline_state,
+ gint ncols,
+ const gchar *column_names[],
+ const gchar *column_values[],
+ gchar **out_revision,
+ gchar **out_object,
+ EOfflineState *out_offline_state,
+ ECacheColumnValues **out_other_columns,
+ gpointer user_data);
+
+/**
+ * ECacheSelectFunc:
+ * @cache: an #ECache
+ * @ncols: count of columns, items in column_names and column_values
+ * @column_names: column names
+ * @column_values: column values
+ * @user_data: user data, as used in e_cache_sqlite_select()
+ *
+ * A callback called for each row of a SELECT statement executed
+ * with e_cache_sqlite_select() function.
+ *
+ * Returns: %TRUE to continue, %FALSE to stop walk through.
+ *
+ * Since: 3.26
+ **/
+typedef gboolean (* ECacheSelectFunc) (ECache *cache,
+ gint ncols,
+ const gchar *column_names[],
+ const gchar *column_values[],
+ gpointer user_data);
+
+/**
+ * ECache:
+ *
+ * Contains only private data that should be read and manipulated using the
+ * functions below.
+ *
+ * Since: 3.26
+ **/
+struct _ECache {
+ /*< private >*/
+ GObject parent;
+ ECachePrivate *priv;
+};
+
+struct _ECacheClass {
+ GObjectClass parent_class;
+
+ /* Virtual methods */
+ gboolean (* put_locked) (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ ECacheColumnValues *other_columns,
+ EOfflineState offline_state,
+ gboolean is_replace,
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (* remove_locked) (ECache *cache,
+ const gchar *uid,
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (* remove_all_locked) (ECache *cache,
+ const GSList *uids, /* gchar * */
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (* clear_offline_changes_locked)
+ (ECache *cache,
+ GCancellable *cancellable,
+ GError **error);
+ void (* erase) (ECache *cache);
+
+ /* Signals */
+ gboolean (* before_put) (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ ECacheColumnValues *other_columns,
+ gboolean is_replace,
+ GCancellable *cancellable,
+ GError **error);
+ gboolean (* before_remove) (ECache *cache,
+ const gchar *uid,
+ GCancellable *cancellable,
+ GError **error);
+ void (* revision_changed) (ECache *cache);
+
+ /* Padding for future expansion */
+ gpointer reserved[10];
+};
+
+GType e_cache_get_type (void) G_GNUC_CONST;
+
+gboolean e_cache_initialize_sync (ECache *cache,
+ const gchar *filename,
+ const GSList *other_columns, /* ECacheColumnInfo * */
+ GCancellable *cancellable,
+ GError **error);
+const gchar * e_cache_get_filename (ECache *cache);
+gint e_cache_get_version (ECache *cache);
+void e_cache_set_version (ECache *cache,
+ gint version);
+gchar * e_cache_dup_revision (ECache *cache);
+void e_cache_set_revision (ECache *cache,
+ const gchar *revision);
+void e_cache_change_revision (ECache *cache);
+void e_cache_freeze_revision_change (ECache *cache);
+void e_cache_thaw_revision_change (ECache *cache);
+gboolean e_cache_is_revision_change_frozen
+ (ECache *cache);
+void e_cache_erase (ECache *cache);
+gboolean e_cache_contains (ECache *cache,
+ const gchar *uid,
+ ECacheDeletedFlag deleted_flag);
+gchar * e_cache_get (ECache *cache,
+ const gchar *uid,
+ gchar **out_revision,
+ ECacheColumnValues **out_other_columns,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cache_put (ECache *cache,
+ const gchar *uid,
+ const gchar *revision,
+ const gchar *object,
+ ECacheColumnValues *other_columns,
+ ECacheOfflineFlag offline_flag,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cache_remove (ECache *cache,
+ const gchar *uid,
+ ECacheOfflineFlag offline_flag,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cache_remove_all (ECache *cache,
+ GCancellable *cancellable,
+ GError **error);
+guint e_cache_get_count (ECache *cache,
+ ECacheDeletedFlag deleted_flag,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cache_get_uids (ECache *cache,
+ ECacheDeletedFlag deleted_flag,
+ GSList **out_uids, /* gchar * */
+ GSList **out_revisions, /* gchar * */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cache_get_objects (ECache *cache,
+ ECacheDeletedFlag deleted_flag,
+ GSList **out_objects, /* gchar * */
+ GSList **out_revisions, /* gchar * */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cache_foreach (ECache *cache,
+ ECacheDeletedFlag deleted_flag,
+ const gchar *where_clause,
+ ECacheForeachFunc func,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cache_foreach_update (ECache *cache,
+ ECacheDeletedFlag deleted_flag,
+ const gchar *where_clause,
+ ECacheUpdateFunc func,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error);
+
+/* Offline support */
+EOfflineState e_cache_get_offline_state (ECache *cache,
+ const gchar *uid,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cache_set_offline_state (ECache *cache,
+ const gchar *uid,
+ EOfflineState state,
+ GCancellable *cancellable,
+ GError **error);
+GSList * e_cache_get_offline_changes (ECache *cache,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cache_clear_offline_changes (ECache *cache,
+ GCancellable *cancellable,
+ GError **error);
+
+/* Custom keys */
+gboolean e_cache_set_key (ECache *cache,
+ const gchar *key,
+ const gchar *value,
+ GError **error);
+gchar * e_cache_dup_key (ECache *cache,
+ const gchar *key,
+ GError **error);
+gboolean e_cache_set_key_int (ECache *cache,
+ const gchar *key,
+ gint value,
+ GError **error);
+gint e_cache_get_key_int (ECache *cache,
+ const gchar *key,
+ GError **error);
+
+/* Locking */
+void e_cache_lock (ECache *cache,
+ ECacheLockType lock_type);
+void e_cache_unlock (ECache *cache,
+ ECacheUnlockAction action);
+
+/* Low-level SQLite functions */
+gpointer e_cache_get_sqlitedb (ECache *cache);
+gboolean e_cache_sqlite_exec (ECache *cache,
+ const gchar *sql_stmt,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cache_sqlite_select (ECache *cache,
+ const gchar *sql_stmt,
+ ECacheSelectFunc func,
+ gpointer user_data,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_cache_sqlite_maybe_vacuum (ECache *cache,
+ GCancellable *cancellable,
+ GError **error);
+
+void e_cache_sqlite_stmt_append_printf
+ (GString *stmt,
+ const gchar *format,
+ ...);
+gchar * e_cache_sqlite_stmt_printf (const gchar *format,
+ ...);
+void e_cache_sqlite_stmt_free (gchar *stmt);
+
+G_END_DECLS
+
+#endif /* E_CACHE_H */
diff --git a/src/libebackend/libebackend.h b/src/libebackend/libebackend.h
index c286f18..d1559fd 100644
--- a/src/libebackend/libebackend.h
+++ b/src/libebackend/libebackend.h
@@ -26,6 +26,7 @@
#include <libebackend/e-backend-enumtypes.h>
#include <libebackend/e-backend-factory.h>
#include <libebackend/e-backend.h>
+#include <libebackend/e-cache.h>
#include <libebackend/e-cache-reaper.h>
#include <libebackend/e-collection-backend-factory.h>
#include <libebackend/e-collection-backend.h>
diff --git a/src/libedataserver/CMakeLists.txt b/src/libedataserver/CMakeLists.txt
index 8e26b83..fc523ea 100644
--- a/src/libedataserver/CMakeLists.txt
+++ b/src/libedataserver/CMakeLists.txt
@@ -69,6 +69,7 @@ set(SOURCES
e-secret-store.c
e-sexp.c
e-soup-auth-bearer.c
+ e-soup-session.c
e-soup-ssl-trust.c
e-source.c
e-source-extension.c
@@ -117,9 +118,11 @@ set(SOURCES
e-uid.c
e-url.c
e-webdav-discover.c
+ e-webdav-session.c
e-data-server-util.c
- e-xml-utils.c
+ e-xml-document.c
e-xml-hash-utils.c
+ e-xml-utils.c
libedataserver-private.h
eds-version.c
${CMAKE_CURRENT_BINARY_DIR}/e-source-enumtypes.c
@@ -148,6 +151,7 @@ set(HEADERS
e-secret-store.h
e-sexp.h
e-soup-auth-bearer.h
+ e-soup-session.h
e-soup-ssl-trust.h
e-source.h
e-source-address-book.h
@@ -196,9 +200,11 @@ set(HEADERS
e-uid.h
e-url.h
e-webdav-discover.h
+ e-webdav-session.h
e-data-server-util.h
- e-xml-utils.h
+ e-xml-document.h
e-xml-hash-utils.h
+ e-xml-utils.h
${CMAKE_CURRENT_BINARY_DIR}/e-source-enumtypes.h
${CMAKE_CURRENT_BINARY_DIR}/eds-version.h
)
diff --git a/src/libedataserver/e-data-server-util.c b/src/libedataserver/e-data-server-util.c
index b68c0d1..7cdae3e 100644
--- a/src/libedataserver/e-data-server-util.c
+++ b/src/libedataserver/e-data-server-util.c
@@ -505,6 +505,52 @@ e_util_utf8_remove_accents (const gchar *str)
}
/**
+ * e_util_utf8_decompose:
+ * @text: a UTF-8 string
+ *
+ * Converts the @text into a decomposed variant and strips it, which
+ * allows also cheap case insensitive comparision afterwards. This
+ * produces an output as being used in e_util_utf8_strstrcasedecomp().
+ *
+ * Returns: (transfer full): A newly allocated string, a decomposed
+ * variant of the @text. Free with g_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_util_utf8_decompose (const gchar *text)
+{
+ gunichar unival;
+ const gchar *p;
+ gchar utf8[12];
+ GString *decomp;
+
+ if (!text)
+ return NULL;
+
+ decomp = g_string_sized_new (strlen (text) + 1);
+
+ for (p = e_util_unicode_get_utf8 (text, &unival);
+ p && unival;
+ p = e_util_unicode_get_utf8 (p, &unival)) {
+ gunichar sc;
+ sc = stripped_char (unival);
+ if (sc) {
+ gint ulen = g_unichar_to_utf8 (sc, utf8);
+ g_string_append_len (decomp, utf8, ulen);
+ }
+ }
+
+ /* NULL means there was illegal utf-8 sequence */
+ if (!p || !decomp->len) {
+ g_string_free (decomp, TRUE);
+ return NULL;
+ }
+
+ return g_string_free (decomp, FALSE);
+}
+
+/**
* e_util_utf8_make_valid:
* @str: a UTF-8 string
*
@@ -2922,3 +2968,47 @@ e_util_get_source_oauth2_access_token_sync (ESource *source,
return success;
}
+
+static gpointer
+unref_object_in_thread (gpointer ptr)
+{
+ GObject *object = ptr;
+
+ g_return_val_if_fail (object != NULL, NULL);
+
+ g_object_unref (object);
+
+ return NULL;
+}
+
+/**
+ * e_util_unref_in_thread:
+ * @object: a #GObject
+ *
+ * Unrefs the given @object in a dedicated thread. This is useful when unreffing
+ * object deep in call stack when the caller might still use the object and
+ * this being the last reference to it.
+ *
+ * Since: 3.26
+ **/
+void
+e_util_unref_in_thread (gpointer object)
+{
+ GThread *thread;
+ GError *error = NULL;
+
+ if (!object)
+ return;
+
+ g_return_if_fail (G_IS_OBJECT (object));
+
+ thread = g_thread_try_new (NULL, unref_object_in_thread, object, &error);
+ if (thread) {
+ g_thread_unref (thread);
+ } else {
+ g_warning ("%s: Failed to run thread: %s", G_STRFUNC, error ? error->message : "Unknown
error");
+ g_object_unref (object);
+ }
+
+ g_clear_error (&error);
+}
diff --git a/src/libedataserver/e-data-server-util.h b/src/libedataserver/e-data-server-util.h
index 6b26f12..9dfb801 100644
--- a/src/libedataserver/e-data-server-util.h
+++ b/src/libedataserver/e-data-server-util.h
@@ -55,6 +55,7 @@ const gchar * e_util_utf8_strstrcasedecomp (const gchar *haystack,
gint e_util_utf8_strcasecmp (const gchar *s1,
const gchar *s2);
gchar * e_util_utf8_remove_accents (const gchar *str);
+gchar * e_util_utf8_decompose (const gchar *text);
gchar * e_util_utf8_make_valid (const gchar *str);
gchar * e_util_utf8_data_make_valid (const gchar *data,
gsize data_bytes);
@@ -280,6 +281,9 @@ gboolean e_util_get_source_oauth2_access_token_sync
gint *out_expires_in_seconds,
GCancellable *cancellable,
GError **error);
+
+void e_util_unref_in_thread (gpointer object);
+
G_END_DECLS
#endif /* E_DATA_SERVER_UTIL_H */
diff --git a/src/libedataserver/e-soup-session.c b/src/libedataserver/e-soup-session.c
new file mode 100644
index 0000000..437a941
--- /dev/null
+++ b/src/libedataserver/e-soup-session.c
@@ -0,0 +1,1019 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * SECTION: e-soup-session
+ * @include: libedataserver/libedataserver.h
+ * @short_description: A SoupSession descendant
+ *
+ * The #ESoupSession is a #SoupSession descendant, which hides common
+ * tasks related to the way evolution-data-server works.
+ **/
+
+#include "evolution-data-server-config.h"
+
+#include <stdio.h>
+#include <glib/gi18n-lib.h>
+
+#include "e-soup-auth-bearer.h"
+#include "e-soup-ssl-trust.h"
+#include "e-source-authentication.h"
+#include "e-source-webdav.h"
+
+#include "e-soup-session.h"
+
+#define BUFFER_SIZE 16384
+
+struct _ESoupSessionPrivate {
+ GMutex property_lock;
+ ESource *source;
+ ENamedParameters *credentials;
+
+ gboolean ssl_info_set;
+ gchar *ssl_certificate_pem;
+ GTlsCertificateFlags ssl_certificate_errors;
+
+ SoupLoggerLogLevel log_level;
+
+ GError *bearer_auth_error;
+ ESoupAuthBearer *using_bearer_auth;
+};
+
+enum {
+ PROP_0,
+ PROP_SOURCE,
+ PROP_CREDENTIALS
+};
+
+G_DEFINE_TYPE (ESoupSession, e_soup_session, SOUP_TYPE_SESSION)
+
+static void
+e_soup_session_ensure_bearer_auth_usage (ESoupSession *session,
+ ESoupAuthBearer *bearer)
+{
+ SoupSessionFeature *feature;
+ SoupURI *soup_uri;
+ ESourceWebdav *extension;
+ ESource *source;
+
+ g_return_if_fail (E_IS_SOUP_SESSION (session));
+
+ source = e_soup_session_get_source (session);
+
+ /* Preload the SoupAuthManager with a valid "Bearer" token
+ * when using OAuth 2.0. This avoids an extra unauthorized
+ * HTTP round-trip, which apparently Google doesn't like. */
+
+ feature = soup_session_get_feature (SOUP_SESSION (session), SOUP_TYPE_AUTH_MANAGER);
+
+ if (!soup_session_feature_has_feature (feature, E_TYPE_SOUP_AUTH_BEARER)) {
+ /* Add the "Bearer" auth type to support OAuth 2.0. */
+ soup_session_feature_add_feature (feature, E_TYPE_SOUP_AUTH_BEARER);
+ }
+
+ extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+ soup_uri = e_source_webdav_dup_soup_uri (extension);
+
+ soup_auth_manager_use_auth (
+ SOUP_AUTH_MANAGER (feature),
+ soup_uri, SOUP_AUTH (bearer));
+
+ soup_uri_free (soup_uri);
+}
+
+static gboolean
+e_soup_session_setup_bearer_auth (ESoupSession *session,
+ gboolean is_in_authenticate_handler,
+ ESoupAuthBearer *bearer,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ENamedParameters *credentials;
+ ESource *source;
+ gchar *access_token = NULL;
+ gint expires_in_seconds = -1;
+ gboolean success = FALSE;
+
+ g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
+ g_return_val_if_fail (E_IS_SOUP_AUTH_BEARER (bearer), FALSE);
+
+ source = e_soup_session_get_source (session);
+ credentials = e_soup_session_dup_credentials (session);
+
+ if (!credentials || !e_named_parameters_count (credentials)) {
+ e_named_parameters_free (credentials);
+ g_set_error_literal (error, SOUP_HTTP_ERROR, SOUP_STATUS_UNAUTHORIZED, _("Credentials
required"));
+ return FALSE;
+ }
+
+ success = e_util_get_source_oauth2_access_token_sync (source, credentials,
+ &access_token, &expires_in_seconds, cancellable, error);
+
+ if (success) {
+ e_soup_auth_bearer_set_access_token (bearer, access_token, expires_in_seconds);
+
+ if (!is_in_authenticate_handler)
+ e_soup_session_ensure_bearer_auth_usage (session, bearer);
+ }
+
+ e_named_parameters_free (credentials);
+ g_free (access_token);
+
+ return success;
+}
+
+static gboolean
+e_soup_session_maybe_prepare_bearer_auth (ESoupSession *session,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ESource *source;
+ gchar *auth_method = NULL;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
+
+ source = e_soup_session_get_source (session);
+
+ if (e_source_has_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
+ ESourceAuthentication *extension;
+
+ extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
+ auth_method = e_source_authentication_dup_method (extension);
+ } else {
+ return TRUE;
+ }
+
+ if (g_strcmp0 (auth_method, "OAuth2") != 0 && g_strcmp0 (auth_method, "Google") != 0) {
+ g_free (auth_method);
+ return TRUE;
+ }
+
+ g_free (auth_method);
+
+ g_mutex_lock (&session->priv->property_lock);
+ if (session->priv->using_bearer_auth) {
+ ESoupAuthBearer *using_bearer_auth = g_object_ref (session->priv->using_bearer_auth);
+
+ g_mutex_unlock (&session->priv->property_lock);
+
+ success = e_soup_session_setup_bearer_auth (session, FALSE, using_bearer_auth, cancellable,
error);
+
+ g_clear_object (&using_bearer_auth);
+ } else {
+ ESourceWebdav *extension;
+ SoupAuth *soup_auth;
+ SoupURI *soup_uri;
+
+ g_mutex_unlock (&session->priv->property_lock);
+
+ extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+ soup_uri = e_source_webdav_dup_soup_uri (extension);
+
+ soup_auth = g_object_new (
+ E_TYPE_SOUP_AUTH_BEARER,
+ SOUP_AUTH_HOST, soup_uri->host, NULL);
+
+ success = e_soup_session_setup_bearer_auth (session, FALSE, E_SOUP_AUTH_BEARER (soup_auth),
cancellable, error);
+ if (success) {
+ g_mutex_lock (&session->priv->property_lock);
+ g_clear_object (&session->priv->using_bearer_auth);
+ session->priv->using_bearer_auth = g_object_ref (soup_auth);
+ g_mutex_unlock (&session->priv->property_lock);
+ }
+
+ g_object_unref (soup_auth);
+ soup_uri_free (soup_uri);
+ }
+
+ return success;
+}
+
+static void
+e_soup_session_authenticate_cb (SoupSession *soup_session,
+ SoupMessage *message,
+ SoupAuth *auth,
+ gboolean retrying,
+ gpointer user_data)
+{
+ ESoupSession *session;
+ const gchar *username;
+ ENamedParameters *credentials;
+ gchar *auth_user = NULL;
+
+ g_return_if_fail (E_IS_SOUP_SESSION (soup_session));
+
+ session = E_SOUP_SESSION (soup_session);
+
+ if (E_IS_SOUP_AUTH_BEARER (auth)) {
+ g_object_ref (auth);
+ g_warn_if_fail ((gpointer) session->priv->using_bearer_auth == (gpointer) auth);
+ g_clear_object (&session->priv->using_bearer_auth);
+ session->priv->using_bearer_auth = E_SOUP_AUTH_BEARER (auth);
+ }
+
+ if (retrying)
+ return;
+
+ if (session->priv->using_bearer_auth) {
+ GError *local_error = NULL;
+
+ e_soup_session_setup_bearer_auth (session, TRUE, E_SOUP_AUTH_BEARER (auth), NULL,
&local_error);
+
+ if (local_error) {
+ g_mutex_lock (&session->priv->property_lock);
+
+ /* Warn about an unclaimed error before we clear it.
+ * This is just to verify the errors we set here are
+ * actually making it back to the user. */
+ g_warn_if_fail (session->priv->bearer_auth_error == NULL);
+ g_clear_error (&session->priv->bearer_auth_error);
+
+ g_propagate_error (&session->priv->bearer_auth_error, local_error);
+
+ g_mutex_unlock (&session->priv->property_lock);
+ }
+
+ return;
+ }
+
+ credentials = e_soup_session_dup_credentials (session);
+
+ username = credentials ? e_named_parameters_get (credentials, E_SOURCE_CREDENTIAL_USERNAME) : NULL;
+ if ((!username || !*username) &&
+ e_source_has_extension (session->priv->source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
+ ESourceAuthentication *auth_extension;
+
+ auth_extension = e_source_get_extension (session->priv->source,
E_SOURCE_EXTENSION_AUTHENTICATION);
+ auth_user = e_source_authentication_dup_user (auth_extension);
+
+ username = auth_user;
+ }
+
+ if (!username || !*username || !credentials ||
+ !e_named_parameters_exists (credentials, E_SOURCE_CREDENTIAL_PASSWORD))
+ soup_message_set_status (message, SOUP_STATUS_UNAUTHORIZED);
+ else
+ soup_auth_authenticate (auth, username, e_named_parameters_get (credentials,
E_SOURCE_CREDENTIAL_PASSWORD));
+
+ e_named_parameters_free (credentials);
+ g_free (auth_user);
+}
+
+static void
+e_soup_session_set_source (ESoupSession *session,
+ ESource *source)
+{
+ g_return_if_fail (E_IS_SOUP_SESSION (session));
+ g_return_if_fail (E_IS_SOURCE (source));
+ g_return_if_fail (!session->priv->source);
+
+ session->priv->source = g_object_ref (source);
+}
+
+static void
+e_soup_session_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case PROP_SOURCE:
+ e_soup_session_set_source (
+ E_SOUP_SESSION (object),
+ g_value_get_object (value));
+ return;
+
+ case PROP_CREDENTIALS:
+ e_soup_session_set_credentials (
+ E_SOUP_SESSION (object),
+ g_value_get_boxed (value));
+ return;
+ }
+
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+e_soup_session_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case PROP_SOURCE:
+ g_value_set_object (
+ value,
+ e_soup_session_get_source (
+ E_SOUP_SESSION (object)));
+ return;
+
+ case PROP_CREDENTIALS:
+ g_value_take_boxed (
+ value,
+ e_soup_session_dup_credentials (
+ E_SOUP_SESSION (object)));
+ return;
+ }
+
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+e_soup_session_finalize (GObject *object)
+{
+ ESoupSession *session = E_SOUP_SESSION (object);
+
+ g_clear_error (&session->priv->bearer_auth_error);
+ g_clear_object (&session->priv->source);
+ g_clear_object (&session->priv->using_bearer_auth);
+ g_clear_pointer (&session->priv->credentials, e_named_parameters_free);
+ g_clear_pointer (&session->priv->ssl_certificate_pem, g_free);
+
+ g_mutex_clear (&session->priv->property_lock);
+
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_soup_session_parent_class)->finalize (object);
+}
+
+static void
+e_soup_session_class_init (ESoupSessionClass *klass)
+{
+ GObjectClass *object_class;
+
+ g_type_class_add_private (klass, sizeof (ESoupSessionPrivate));
+
+ object_class = G_OBJECT_CLASS (klass);
+ object_class->set_property = e_soup_session_set_property;
+ object_class->get_property = e_soup_session_get_property;
+ object_class->finalize = e_soup_session_finalize;
+
+ /**
+ * ESoupSession:source:
+ *
+ * The #ESource being used for this soup session.
+ *
+ * Since: 3.26
+ **/
+ g_object_class_install_property (
+ object_class,
+ PROP_SOURCE,
+ g_param_spec_object (
+ "source",
+ "Source",
+ NULL,
+ E_TYPE_SOURCE,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * ESoupSession:credentials:
+ *
+ * The #ENamedParameters containing login credentials.
+ *
+ * Since: 3.26
+ **/
+ g_object_class_install_property (
+ object_class,
+ PROP_CREDENTIALS,
+ g_param_spec_boxed (
+ "credentials",
+ "Credentials",
+ NULL,
+ E_TYPE_NAMED_PARAMETERS,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+}
+
+static void
+e_soup_session_init (ESoupSession *session)
+{
+ session->priv = G_TYPE_INSTANCE_GET_PRIVATE (session, E_TYPE_SOUP_SESSION, ESoupSessionPrivate);
+ session->priv->ssl_info_set = FALSE;
+ session->priv->log_level = SOUP_LOGGER_LOG_NONE;
+
+ g_mutex_init (&session->priv->property_lock);
+
+ g_object_set (
+ G_OBJECT (session),
+ SOUP_SESSION_TIMEOUT, 90,
+ SOUP_SESSION_SSL_STRICT, TRUE,
+ SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, TRUE,
+ SOUP_SESSION_ACCEPT_LANGUAGE_AUTO, TRUE,
+ NULL);
+
+ g_signal_connect (session, "authenticate",
+ G_CALLBACK (e_soup_session_authenticate_cb), NULL);
+}
+
+/**
+ * e_soup_session_new:
+ * @source: an #ESource
+ *
+ * Creates a new #ESoupSession associated with given @source.
+ * The @source can be used to store and read SSL trust settings, but only if
+ * it already contains an #ESourceWebdav extension. Otherwise the SSL trust
+ * settings are ignored.
+ *
+ * Returns: (transfer full): a new #ESoupSession; free it with g_object_unref(),
+ * when no longer needed.
+ *
+ * Since: 3.26
+ **/
+ESoupSession *
+e_soup_session_new (ESource *source)
+{
+ g_return_val_if_fail (E_IS_SOURCE (source), NULL);
+
+ return g_object_new (E_TYPE_SOUP_SESSION,
+ "source", source,
+ NULL);
+}
+
+/**
+ * e_soup_session_setup_logging:
+ * @session: an #ESoupSession
+ * @logging_level: (nullable): logging level to setup, or %NULL
+ *
+ * Setups logging for the @session. The @logging_level can be one of:
+ * "all" - log whole raw communication;
+ * "body" - the same as "all";
+ * "headers" - log the headers only;
+ * "min" - minimal logging;
+ * "1" - the same as "all".
+ * Any other value, including %NULL, disables logging.
+ *
+ * Use e_soup_session_get_log_level() to get current log level.
+ *
+ * Since: 3.26
+ **/
+void
+e_soup_session_setup_logging (ESoupSession *session,
+ const gchar *logging_level)
+{
+ SoupLogger *logger;
+
+ g_return_if_fail (E_IS_SOUP_SESSION (session));
+
+ soup_session_remove_feature_by_type (SOUP_SESSION (session), SOUP_TYPE_LOGGER);
+ session->priv->log_level = SOUP_LOGGER_LOG_NONE;
+
+ if (!logging_level)
+ return;
+
+ if (g_ascii_strcasecmp (logging_level, "all") == 0 ||
+ g_ascii_strcasecmp (logging_level, "body") == 0 ||
+ g_ascii_strcasecmp (logging_level, "1") == 0)
+ session->priv->log_level = SOUP_LOGGER_LOG_BODY;
+ else if (g_ascii_strcasecmp (logging_level, "headers") == 0)
+ session->priv->log_level = SOUP_LOGGER_LOG_HEADERS;
+ else if (g_ascii_strcasecmp (logging_level, "min") == 0)
+ session->priv->log_level = SOUP_LOGGER_LOG_MINIMAL;
+ else
+ return;
+
+ logger = soup_logger_new (session->priv->log_level, -1);
+ soup_session_add_feature (SOUP_SESSION (session), SOUP_SESSION_FEATURE (logger));
+ g_object_unref (logger);
+}
+
+/**
+ * e_soup_session_get_log_level:
+ * @session: an #ESoupSession
+ *
+ * Returns: Current log level, as #SoupLoggerLogLevel
+ *
+ * Since: 3.26
+ **/
+SoupLoggerLogLevel
+e_soup_session_get_log_level (ESoupSession *session)
+{
+ g_return_val_if_fail (E_IS_SOUP_SESSION (session), SOUP_LOGGER_LOG_NONE);
+
+ return session->priv->log_level;
+}
+
+/**
+ * e_soup_session_get_source:
+ * @session: an #ESoupSession
+ *
+ * Returns: (transfer none): Associated #ESource with the @session.
+ *
+ * Since: 3.26
+ **/
+ESource *
+e_soup_session_get_source (ESoupSession *session)
+{
+ g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
+
+ return session->priv->source;
+}
+
+/**
+ * e_soup_session_set_credentials:
+ * @session: an #ESoupSession
+ * @credentials: (nullable): an #ENamedParameters with credentials to use, or %NULL
+ *
+ * Sets credentials to use for connection. Using %NULL for @credentials
+ * unsets previous value.
+ *
+ * Since: 3.26
+ **/
+void
+e_soup_session_set_credentials (ESoupSession *session,
+ const ENamedParameters *credentials)
+{
+ g_return_if_fail (E_IS_SOUP_SESSION (session));
+
+ g_mutex_lock (&session->priv->property_lock);
+
+ if (credentials == session->priv->credentials) {
+ g_mutex_unlock (&session->priv->property_lock);
+ return;
+ }
+
+ e_named_parameters_free (session->priv->credentials);
+ if (credentials)
+ session->priv->credentials = e_named_parameters_new_clone (credentials);
+ else
+ session->priv->credentials = NULL;
+
+ g_mutex_unlock (&session->priv->property_lock);
+
+ g_object_notify (G_OBJECT (session), "credentials");
+}
+
+/**
+ * e_soup_session_dup_credentials:
+ * @session: an #ESoupSession
+ *
+ * Returns: (nullable) (transfer full): A copy of the credentials being
+ * previously set with e_soup_session_set_credentials(), or %NULL when
+ * none are set. Free the returned pointer with e_named_parameters_free(),
+ * when no longer needed.
+ *
+ * Since: 3.26
+ **/
+ENamedParameters *
+e_soup_session_dup_credentials (ESoupSession *session)
+{
+ ENamedParameters *credentials;
+
+ g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
+
+ g_mutex_lock (&session->priv->property_lock);
+
+ if (session->priv->credentials)
+ credentials = e_named_parameters_new_clone (session->priv->credentials);
+ else
+ credentials = NULL;
+
+ g_mutex_unlock (&session->priv->property_lock);
+
+ return credentials;
+}
+
+/**
+ * e_soup_session_get_ssl_error_details:
+ * @session: an #ESoupSession
+ * @out_certificate_pem: (out): return location for a server TLS/SSL certificate
+ * in PEM format, when the last operation failed with a TLS/SSL error
+ * @out_certificate_errors: (out): return location for a #GTlsCertificateFlags,
+ * with certificate error flags when the the operation failed with a TLS/SSL error
+ *
+ * Populates @out_certificate_pem and @out_certificate_errors with the last values
+ * returned on #SOUP_STATUS_SSL_FAILED error.
+ *
+ * Returns: Whether the information was available and set to the out parameters.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_soup_session_get_ssl_error_details (ESoupSession *session,
+ gchar **out_certificate_pem,
+ GTlsCertificateFlags *out_certificate_errors)
+{
+ g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
+ g_return_val_if_fail (out_certificate_pem != NULL, FALSE);
+ g_return_val_if_fail (out_certificate_errors != NULL, FALSE);
+
+ g_mutex_lock (&session->priv->property_lock);
+ if (!session->priv->ssl_info_set) {
+ g_mutex_unlock (&session->priv->property_lock);
+ return FALSE;
+ }
+
+ *out_certificate_pem = g_strdup (session->priv->ssl_certificate_pem);
+ *out_certificate_errors = session->priv->ssl_certificate_errors;
+
+ g_mutex_unlock (&session->priv->property_lock);
+
+ return TRUE;
+}
+
+static void
+e_soup_session_preset_request (SoupRequestHTTP *request)
+{
+ SoupMessage *message;
+
+ if (!request)
+ return;
+
+ message = soup_request_http_get_message (request);
+ if (message) {
+ soup_message_headers_append (message->request_headers, "User-Agent", "Evolution/" VERSION);
+ soup_message_headers_append (message->request_headers, "Connection", "close");
+
+ /* Disable caching for proxies (RFC 4918, section 10.4.5) */
+ soup_message_headers_append (message->request_headers, "Cache-Control", "no-cache");
+ soup_message_headers_append (message->request_headers, "Pragma", "no-cache");
+
+ g_clear_object (&message);
+ }
+}
+
+/**
+ * e_soup_session_new_request:
+ * @session: an #ESoupSession
+ * @method: an HTTP method
+ * @uri_string: a URI string to use for the request
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new #SoupRequestHTTP, similar to soup_session_request_http(),
+ * but also presets request headers with "User-Agent" to be "Evolution/version"
+ * and with "Connection" to be "close".
+ *
+ * See also e_soup_session_new_request_uri().
+ *
+ * Returns: (transfer full): a new #SoupRequestHTTP, or %NULL on error
+ *
+ * Since: 3.26
+ **/
+SoupRequestHTTP *
+e_soup_session_new_request (ESoupSession *session,
+ const gchar *method,
+ const gchar *uri_string,
+ GError **error)
+{
+ SoupRequestHTTP *request;
+
+ g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
+
+ request = soup_session_request_http (SOUP_SESSION (session), method, uri_string, error);
+ if (!request)
+ return NULL;
+
+ e_soup_session_preset_request (request);
+
+ return request;
+}
+
+/**
+ * e_soup_session_new_request:
+ * @session: an #ESoupSession
+ * @method: an HTTP method
+ * @uri: a #SoupURI to use for the request
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new #SoupRequestHTTP, similar to soup_session_request_http_uri(),
+ * but also presets request headers with "User-Agent" to be "Evolution/version"
+ * and with "Connection" to be "close".
+ *
+ * See also e_soup_session_new_request().
+ *
+ * Returns: (transfer full): a new #SoupRequestHTTP, or %NULL on error
+ *
+ * Since: 3.26
+ **/
+SoupRequestHTTP *
+e_soup_session_new_request_uri (ESoupSession *session,
+ const gchar *method,
+ SoupURI *uri,
+ GError **error)
+{
+ SoupRequestHTTP *request;
+
+ g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
+
+ request = soup_session_request_http_uri (SOUP_SESSION (session), method, uri, error);
+ if (!request)
+ return NULL;
+
+ e_soup_session_preset_request (request);
+
+ return request;
+}
+
+static void
+e_soup_session_extract_ssl_data (ESoupSession *session,
+ SoupMessage *message)
+{
+ GTlsCertificate *certificate = NULL;
+
+ g_return_if_fail (E_IS_SOUP_SESSION (session));
+ g_return_if_fail (SOUP_IS_MESSAGE (message));
+
+ g_mutex_lock (&session->priv->property_lock);
+
+ g_clear_pointer (&session->priv->ssl_certificate_pem, g_free);
+ session->priv->ssl_info_set = FALSE;
+
+ g_object_get (G_OBJECT (message),
+ "tls-certificate", &certificate,
+ "tls-errors", &session->priv->ssl_certificate_errors,
+ NULL);
+
+ if (certificate) {
+ g_object_get (certificate, "certificate-pem", &session->priv->ssl_certificate_pem, NULL);
+ session->priv->ssl_info_set = TRUE;
+
+ g_object_unref (certificate);
+ }
+
+ g_mutex_unlock (&session->priv->property_lock);
+}
+
+/**
+ * e_soup_session_check_result:
+ * @session: an #ESoupSession
+ * @request: a #SoupRequestHTTP
+ * @read_bytes: (nullable): optional bytes which had been read from the stream, or %NULL
+ * @bytes_length: how many bytes had been read; ignored when @read_bytes is %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Checks result of the @request and sets the @error if it failed.
+ * When it failed and the @read_bytes is provided, then these are
+ * set to @request's message response_body, thus it can be used
+ * later.
+ *
+ * Returns: Whether succeeded, aka %TRUE, when no error recognized
+ * and %FALSE otherwise.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_soup_session_check_result (ESoupSession *session,
+ SoupRequestHTTP *request,
+ gconstpointer read_bytes,
+ gsize bytes_length,
+ GError **error)
+{
+ SoupMessage *message;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_SOUP_SESSION (session), FALSE);
+ g_return_val_if_fail (SOUP_IS_REQUEST_HTTP (request), FALSE);
+
+ message = soup_request_http_get_message (request);
+ g_return_val_if_fail (SOUP_IS_MESSAGE (message), FALSE);
+
+ success = SOUP_STATUS_IS_SUCCESSFUL (message->status_code);
+ if (!success) {
+ if (message->status_code == SOUP_STATUS_CANCELLED) {
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_CANCELLED, _("Operation was
cancelled"));
+ } else {
+ g_set_error (error, SOUP_HTTP_ERROR, message->status_code,
+ _("Failed with HTTP error %d: %s"), message->status_code,
+ e_soup_session_util_status_to_string (message->status_code,
message->reason_phrase));
+ }
+
+ if (message->status_code == SOUP_STATUS_SSL_FAILED)
+ e_soup_session_extract_ssl_data (session, message);
+
+ if (read_bytes && bytes_length > 0) {
+ SoupBuffer *buffer;
+
+ soup_message_body_append (message->response_body, SOUP_MEMORY_COPY, read_bytes,
bytes_length);
+
+ /* This writes data to message->response_body->data */
+ buffer = soup_message_body_flatten (message->response_body);
+ if (buffer)
+ soup_buffer_free (buffer);
+ }
+ }
+
+ g_object_unref (message);
+
+ return success;
+}
+
+/**
+ * e_soup_session_send_request_sync:
+ * @session: an #ESoupSession
+ * @request: a #SoupRequestHTTP to send
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Synchronously sends prepared request and returns #GInputStream
+ * that can be used to read its contents.
+ *
+ * This calls soup_request_send() internally, but it also setups
+ * the request according to #ESoupSession:source authentication
+ * settings. It also extracts information about used certificate,
+ * in case of SOUP_STATUS_SSL_FAILED error and keeps it for later use
+ * by e_soup_session_get_ssl_error_details().
+ *
+ * Use e_soup_session_send_request_simple_sync() to read whole
+ * content into a #GByteArray.
+ *
+ * Note that SoupSession doesn't log content read from GInputStream,
+ * thus the caller may print the read content on its own when needed.
+ *
+ * Note the @request is fully filled only after there is anything
+ * read from the resulting #GInputStream, thus use
+ * e_soup_session_check_result() to verify that the receive had
+ * been finished properly.
+ *
+ * Returns: (transfer full): A newly allocated #GInputStream,
+ * that can be used to read from the URI pointed to by @request.
+ * Free it with g_object_unref(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+GInputStream *
+e_soup_session_send_request_sync (ESoupSession *session,
+ SoupRequestHTTP *request,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ESoupAuthBearer *using_bearer_auth = NULL;
+ GInputStream *input_stream;
+ SoupMessage *message;
+ GError *local_error = NULL;
+
+ g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
+ g_return_val_if_fail (SOUP_IS_REQUEST_HTTP (request), NULL);
+
+ if (!e_soup_session_maybe_prepare_bearer_auth (session, cancellable, error))
+ return NULL;
+
+ g_mutex_lock (&session->priv->property_lock);
+ g_clear_pointer (&session->priv->ssl_certificate_pem, g_free);
+ session->priv->ssl_certificate_errors = 0;
+ session->priv->ssl_info_set = FALSE;
+ if (session->priv->using_bearer_auth)
+ using_bearer_auth = g_object_ref (session->priv->using_bearer_auth);
+ g_mutex_unlock (&session->priv->property_lock);
+
+ if (session->priv->source &&
+ e_source_has_extension (session->priv->source, E_SOURCE_EXTENSION_WEBDAV_BACKEND)) {
+ message = soup_request_http_get_message (request);
+
+ e_soup_ssl_trust_connect (message, session->priv->source);
+
+ g_clear_object (&message);
+ }
+
+ if (using_bearer_auth &&
+ e_soup_auth_bearer_is_expired (using_bearer_auth)) {
+ if (!e_soup_session_setup_bearer_auth (session, FALSE, using_bearer_auth, cancellable,
&local_error)) {
+ message = soup_request_http_get_message (request);
+
+ if (local_error) {
+ soup_message_set_status_full (message, SOUP_STATUS_BAD_REQUEST,
local_error->message);
+ g_propagate_error (error, local_error);
+ } else {
+ soup_message_set_status (message, SOUP_STATUS_BAD_REQUEST);
+ }
+
+ g_object_unref (using_bearer_auth);
+ g_clear_object (&message);
+
+ return NULL;
+ }
+ }
+
+ g_clear_object (&using_bearer_auth);
+
+ input_stream = soup_request_send (SOUP_REQUEST (request), cancellable, &local_error);
+ if (input_stream)
+ return input_stream;
+
+ if (g_error_matches (local_error, SOUP_HTTP_ERROR, SOUP_STATUS_SSL_FAILED)) {
+ message = soup_request_http_get_message (request);
+
+ e_soup_session_extract_ssl_data (session, message);
+
+ g_clear_object (&message);
+ }
+
+ if (local_error)
+ g_propagate_error (error, local_error);
+
+ return NULL;
+}
+
+/**
+ * e_soup_session_send_request_simple_sync:
+ * @session: an #ESoupSession
+ * @request: a #SoupRequestHTTP to send
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Similar to e_soup_session_send_request_sync(), except it reads
+ * whole response content into memory and returns it as a #GByteArray.
+ * Use e_soup_session_send_request_sync() when you want to have
+ * more control on the content read.
+ *
+ * The function prints read content to stdout when
+ * e_soup_session_get_log_level() returns #SOUP_LOGGER_LOG_BODY.
+ *
+ * Returns: (transfer full): A newly allocated #GByteArray,
+ * which contains whole content from the URI pointed to by @request.
+ *
+ * Since: 3.26
+ **/
+GByteArray *
+e_soup_session_send_request_simple_sync (ESoupSession *session,
+ SoupRequestHTTP *request,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GInputStream *input_stream;
+ GByteArray *bytes;
+ gint expected_length;
+ gpointer buffer;
+ gsize nread = 0;
+ gboolean success = FALSE;
+
+ g_return_val_if_fail (E_IS_SOUP_SESSION (session), NULL);
+ g_return_val_if_fail (SOUP_IS_REQUEST_HTTP (request), NULL);
+
+ input_stream = e_soup_session_send_request_sync (session, request, cancellable, error);
+ if (!input_stream)
+ return NULL;
+
+ expected_length = soup_request_get_content_length (SOUP_REQUEST (request));
+ if (expected_length > 0)
+ bytes = g_byte_array_sized_new (expected_length);
+ else
+ bytes = g_byte_array_new ();
+
+ buffer = g_malloc (BUFFER_SIZE);
+
+ while (success = g_input_stream_read_all (input_stream, buffer, BUFFER_SIZE, &nread, cancellable,
error),
+ success && nread > 0) {
+ g_byte_array_append (bytes, buffer, nread);
+ }
+
+ g_free (buffer);
+ g_object_unref (input_stream);
+
+ if (bytes->len > 0 && e_soup_session_get_log_level (session) == SOUP_LOGGER_LOG_BODY) {
+ fwrite (bytes->data, 1, bytes->len, stdout);
+ fprintf (stdout, "\n");
+ fflush (stdout);
+ }
+
+ if (success)
+ success = e_soup_session_check_result (session, request, bytes->data, bytes->len, error);
+
+ if (!success) {
+ g_byte_array_free (bytes, TRUE);
+ bytes = NULL;
+ }
+
+ return bytes;
+}
+
+/**
+ * e_soup_session_util_status_to_string:
+ * @status_code: an HTTP status code
+ * @reason_phrase: (nullable): preferred string to use for the message, or %NULL
+ *
+ * Returns the @reason_phrase, if it's non-%NULL and non-empty, a static string
+ * corresponding to @status_code. In case neither that can be found a localized
+ * "Unknown error" message is returned.
+ *
+ * Returns: (transfer none): Error text based on given arguments. The returned
+ * value is valid as long as @reason_phrase is not freed.
+ *
+ * Since: 3.26
+ **/
+const gchar *
+e_soup_session_util_status_to_string (guint status_code,
+ const gchar *reason_phrase)
+{
+ if (!reason_phrase || !*reason_phrase)
+ reason_phrase = soup_status_get_phrase (status_code);
+
+ if (reason_phrase && *reason_phrase)
+ return reason_phrase;
+
+ return _("Unknown error");
+}
diff --git a/src/libedataserver/e-soup-session.h b/src/libedataserver/e-soup-session.h
new file mode 100644
index 0000000..c877829
--- /dev/null
+++ b/src/libedataserver/e-soup-session.h
@@ -0,0 +1,120 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#if !defined (__LIBEDATASERVER_H_INSIDE__) && !defined (LIBEDATASERVER_COMPILATION)
+#error "Only <libedataserver/libedataserver.h> should be included directly."
+#endif
+
+#ifndef E_SOUP_SESSION_H
+#define E_SOUP_SESSION_H
+
+#include <glib.h>
+#include <libsoup/soup.h>
+
+#include <libedataserver/e-data-server-util.h>
+#include <libedataserver/e-source.h>
+
+/* Standard GObject macros */
+#define E_TYPE_SOUP_SESSION \
+ (e_soup_session_get_type ())
+#define E_SOUP_SESSION(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST \
+ ((obj), E_TYPE_SOUP_SESSION, ESoupSession))
+#define E_SOUP_SESSION_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_CAST \
+ ((cls), E_TYPE_SOUP_SESSION, ESoupSessionClass))
+#define E_IS_SOUP_SESSION(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE \
+ ((obj), E_TYPE_SOUP_SESSION))
+#define E_IS_SOUP_SESSION_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_TYPE \
+ ((cls), E_TYPE_SOUP_SESSION))
+#define E_SOUP_SESSION_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS \
+ ((obj), E_TYPE_SOUP_SESSION, ESoupSessionClass))
+
+G_BEGIN_DECLS
+
+typedef struct _ESoupSession ESoupSession;
+typedef struct _ESoupSessionClass ESoupSessionClass;
+typedef struct _ESoupSessionPrivate ESoupSessionPrivate;
+
+/**
+ * ESoupSession:
+ *
+ * Contains only private data that should be read and manipulated using the
+ * functions below.
+ *
+ * Since: 3.26
+ **/
+struct _ESoupSession {
+ /*< private >*/
+ SoupSession parent;
+ ESoupSessionPrivate *priv;
+};
+
+struct _ESoupSessionClass {
+ SoupSessionClass parent_class;
+
+ /* Padding for future expansion */
+ gpointer reserved[10];
+};
+
+GType e_soup_session_get_type (void) G_GNUC_CONST;
+
+ESoupSession * e_soup_session_new (ESource *source);
+void e_soup_session_setup_logging (ESoupSession *session,
+ const gchar *logging_level);
+SoupLoggerLogLevel
+ e_soup_session_get_log_level (ESoupSession *session);
+ESource * e_soup_session_get_source (ESoupSession *session);
+void e_soup_session_set_credentials (ESoupSession *session,
+ const ENamedParameters *credentials);
+ENamedParameters *
+ e_soup_session_dup_credentials (ESoupSession *session);
+gboolean e_soup_session_get_ssl_error_details (ESoupSession *session,
+ gchar **out_certificate_pem,
+ GTlsCertificateFlags *out_certificate_errors);
+SoupRequestHTTP *
+ e_soup_session_new_request (ESoupSession *session,
+ const gchar *method,
+ const gchar *uri_string,
+ GError **error);
+SoupRequestHTTP *
+ e_soup_session_new_request_uri (ESoupSession *session,
+ const gchar *method,
+ SoupURI *uri,
+ GError **error);
+gboolean e_soup_session_check_result (ESoupSession *session,
+ SoupRequestHTTP *request,
+ gconstpointer read_bytes,
+ gsize bytes_length,
+ GError **error);
+GInputStream * e_soup_session_send_request_sync (ESoupSession *session,
+ SoupRequestHTTP *request,
+ GCancellable *cancellable,
+ GError **error);
+GByteArray * e_soup_session_send_request_simple_sync (ESoupSession *session,
+ SoupRequestHTTP *request,
+ GCancellable *cancellable,
+ GError **error);
+const gchar * e_soup_session_util_status_to_string (guint status_code,
+ const gchar *reason_phrase);
+
+G_END_DECLS
+
+#endif /* E_SOUP_SESSION_H */
diff --git a/src/libedataserver/e-source-credentials-provider-impl-google.c
b/src/libedataserver/e-source-credentials-provider-impl-google.c
index 80146f2..9867df4 100644
--- a/src/libedataserver/e-source-credentials-provider-impl-google.c
+++ b/src/libedataserver/e-source-credentials-provider-impl-google.c
@@ -522,6 +522,7 @@ e_source_credentials_google_get_access_token_sync (ESource *source,
GError **error)
{
ENamedParameters *tmp_credentials = NULL;
+ gboolean success;
g_return_val_if_fail (credentials != NULL, FALSE);
@@ -546,10 +547,13 @@ e_source_credentials_google_get_access_token_sync (ESource *source,
return TRUE;
}
+ /* Try to refresh the token */
+ success = e_source_credentials_google_refresh_token_sync (source, tmp_credentials ? tmp_credentials :
credentials,
+ out_access_token, out_expires_in_seconds, cancellable, error);
+
e_named_parameters_free (tmp_credentials);
- /* Try to refresh the token */
- return e_source_credentials_google_refresh_token_sync (source, credentials, out_access_token,
out_expires_in_seconds, cancellable, error);
+ return success;
}
gboolean
diff --git a/src/libedataserver/e-source-enums.h b/src/libedataserver/e-source-enums.h
index f241c9f..2c37f52 100644
--- a/src/libedataserver/e-source-enums.h
+++ b/src/libedataserver/e-source-enums.h
@@ -67,6 +67,8 @@ typedef enum {
/**
* ESourceAuthenticationResult:
+ * @E_SOURCE_AUTHENTICATION_UNKNOWN:
+ * Unknown error occurred while authenticating. Since: 3.26
* @E_SOURCE_AUTHENTICATION_ERROR:
* An error occurred while authenticating.
* @E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED:
@@ -83,6 +85,7 @@ typedef enum {
* Since: 3.6
**/
typedef enum {
+ E_SOURCE_AUTHENTICATION_UNKNOWN = -1,
E_SOURCE_AUTHENTICATION_ERROR,
E_SOURCE_AUTHENTICATION_ERROR_SSL_FAILED,
E_SOURCE_AUTHENTICATION_ACCEPTED,
diff --git a/src/libedataserver/e-webdav-discover.c b/src/libedataserver/e-webdav-discover.c
index b25577a..43eaeff 100644
--- a/src/libedataserver/e-webdav-discover.c
+++ b/src/libedataserver/e-webdav-discover.c
@@ -21,1582 +21,329 @@
#include <libsoup/soup.h>
-#include <libxml/tree.h>
-#include <libxml/xpath.h>
-#include <libxml/xpathInternals.h>
-
-#include "e-soup-auth-bearer.h"
-#include "e-soup-ssl-trust.h"
-#include "e-source-authentication.h"
-#include "e-source-credentials-provider-impl-google.h"
#include "e-source-webdav.h"
-#include "e-webdav-discover.h"
-
-#define XC(string) ((xmlChar *) string)
-
-/* Standard Namespaces */
-#define NS_WEBDAV "DAV:"
-#define NS_CALDAV "urn:ietf:params:xml:ns:caldav"
-#define NS_CARDDAV "urn:ietf:params:xml:ns:carddav"
-
-/* Application-Specific Namespaces */
-#define NS_ICAL "http://apple.com/ns/ical/"
+#include "e-webdav-session.h"
+#include "e-xml-utils.h"
-/* Mainly for readability. */
-enum {
- DEPTH_0 = 0,
- DEPTH_1 = 1
-};
+#include "e-webdav-discover.h"
-typedef struct _EWebDAVDiscoverContext {
- ESource *source;
- gchar *url_use_path;
+typedef struct _WebDAVDiscoverData {
+ GHashTable *covered_hrefs;
+ GSList *addressbooks;
+ GSList *calendars;
guint32 only_supports;
- ENamedParameters *credentials;
- gchar *out_certificate_pem;
- GTlsCertificateFlags out_certificate_errors;
- GSList *out_discovered_sources;
- GSList *out_calendar_user_addresses;
-} EWebDAVDiscoverContext;
-
-static EWebDAVDiscoverContext *
-e_webdav_discover_context_new (ESource *source,
- const gchar *url_use_path,
- guint32 only_supports,
- const ENamedParameters *credentials)
-{
- EWebDAVDiscoverContext *context;
-
- context = g_new0 (EWebDAVDiscoverContext, 1);
- context->source = g_object_ref (source);
- context->url_use_path = g_strdup (url_use_path);
- context->only_supports = only_supports;
- context->credentials = e_named_parameters_new_clone (credentials);
- context->out_certificate_pem = NULL;
- context->out_certificate_errors = 0;
- context->out_discovered_sources = NULL;
- context->out_calendar_user_addresses = NULL;
-
- return context;
-}
+ GSList **out_calendar_user_addresses;
+ GCancellable *cancellable;
+ GError **error;
+} WebDAVDiscoverData;
static void
-e_webdav_discover_context_free (gpointer ptr)
-{
- EWebDAVDiscoverContext *context = ptr;
-
- if (!context)
- return;
-
- g_clear_object (&context->source);
- g_free (context->url_use_path);
- e_named_parameters_free (context->credentials);
- g_free (context->out_certificate_pem);
- e_webdav_discover_free_discovered_sources (context->out_discovered_sources);
- g_slist_free_full (context->out_calendar_user_addresses, g_free);
- g_free (context);
-}
-
-static gchar *
-e_webdav_discover_make_href_full_uri (SoupURI *base_uri,
- const gchar *href)
+e_webdav_discover_split_resources (WebDAVDiscoverData *wdd,
+ const GSList *resources)
{
- SoupURI *soup_uri;
- gchar *full_uri;
-
- if (!base_uri || !href)
- return g_strdup (href);
-
- if (strstr (href, "://"))
- return g_strdup (href);
-
- soup_uri = soup_uri_copy (base_uri);
- soup_uri_set_path (soup_uri, href);
- soup_uri_set_user (soup_uri, NULL);
- soup_uri_set_password (soup_uri, NULL);
-
- full_uri = soup_uri_to_string (soup_uri, FALSE);
-
- soup_uri_free (soup_uri);
-
- return full_uri;
-}
-
-static void
-e_webdav_discover_redirect (SoupMessage *message,
- SoupSession *session)
-{
- SoupURI *soup_uri;
- const gchar *location;
-
- if (!SOUP_STATUS_IS_REDIRECTION (message->status_code))
- return;
-
- location = soup_message_headers_get_list (message->response_headers, "Location");
-
- if (location == NULL)
- return;
-
- soup_uri = soup_uri_new_with_base (soup_message_get_uri (message), location);
-
- if (soup_uri == NULL) {
- soup_message_set_status_full (
- message, SOUP_STATUS_MALFORMED,
- _("Invalid Redirect URL"));
- return;
- }
-
- soup_message_set_uri (message, soup_uri);
- soup_session_requeue_message (session, message);
-
- soup_uri_free (soup_uri);
-}
-
-static gconstpointer
-compat_libxml_output_buffer_get_content (xmlOutputBufferPtr buf,
- gsize *out_len)
-{
-#ifdef LIBXML2_NEW_BUFFER
- *out_len = xmlOutputBufferGetSize (buf);
- return xmlOutputBufferGetContent (buf);
-#else
- *out_len = buf->buffer->use;
- return buf->buffer->content;
-#endif
-}
-
-static G_GNUC_NULL_TERMINATED SoupMessage *
-e_webdav_discover_new_propfind (SoupSession *session,
- SoupURI *soup_uri,
- gint depth,
- ...)
-{
- GHashTable *namespaces;
- SoupMessage *message;
- xmlDocPtr doc;
- xmlNodePtr root;
- xmlNodePtr node;
- xmlNsPtr ns;
- xmlOutputBufferPtr output;
- gconstpointer content;
- gsize length;
- gpointer key;
- va_list va;
-
- /* Construct the XML content. */
-
- doc = xmlNewDoc (XC ("1.0"));
- node = xmlNewDocNode (doc, NULL, XC ("propfind"), NULL);
-
- /* Build a hash table of namespace URIs to xmlNs structs. */
- namespaces = g_hash_table_new (NULL, NULL);
-
- ns = xmlNewNs (node, XC (NS_CALDAV), XC ("C"));
- g_hash_table_insert (namespaces, (gpointer) NS_CALDAV, ns);
-
- ns = xmlNewNs (node, XC (NS_CARDDAV), XC ("A"));
- g_hash_table_insert (namespaces, (gpointer) NS_CARDDAV, ns);
-
- ns = xmlNewNs (node, XC (NS_ICAL), XC ("IC"));
- g_hash_table_insert (namespaces, (gpointer) NS_ICAL, ns);
-
- /* Add WebDAV last since we use it below. */
- ns = xmlNewNs (node, XC (NS_WEBDAV), XC ("D"));
- g_hash_table_insert (namespaces, (gpointer) NS_WEBDAV, ns);
-
- xmlSetNs (node, ns);
- xmlDocSetRootElement (doc, node);
-
- node = xmlNewTextChild (node, ns, XC ("prop"), NULL);
-
- va_start (va, depth);
- while ((key = va_arg (va, gpointer)) != NULL) {
- xmlChar *name;
-
- ns = g_hash_table_lookup (namespaces, key);
- name = va_arg (va, xmlChar *);
-
- if (ns != NULL && name != NULL)
- xmlNewTextChild (node, ns, name, NULL);
- else
- g_warn_if_reached ();
- }
- va_end (va);
-
- g_hash_table_destroy (namespaces);
-
- /* Construct the SoupMessage. */
-
- message = soup_message_new_from_uri (SOUP_METHOD_PROPFIND, soup_uri);
-
- soup_message_set_flags (message, SOUP_MESSAGE_NO_REDIRECT);
-
- soup_message_headers_append (
- message->request_headers,
- "User-Agent", "Evolution/" VERSION);
-
- soup_message_headers_append (
- message->request_headers,
- "Connection", "close");
-
- soup_message_headers_append (
- message->request_headers,
- "Depth", (depth == 0) ? "0" : "1");
-
- output = xmlAllocOutputBuffer (NULL);
-
- root = xmlDocGetRootElement (doc);
- xmlNodeDumpOutput (output, doc, root, 0, 1, NULL);
- xmlOutputBufferFlush (output);
-
- content = compat_libxml_output_buffer_get_content (output, &length);
-
- soup_message_set_request (
- message, "application/xml", SOUP_MEMORY_COPY,
- content, length);
-
- xmlOutputBufferClose (output);
- xmlFreeDoc (doc);
-
- soup_message_add_header_handler (
- message, "got-body", "Location",
- G_CALLBACK (e_webdav_discover_redirect), session);
-
- return message;
-}
-
-static xmlXPathObjectPtr
-e_webdav_discover_get_xpath (xmlXPathContextPtr xp_ctx,
- const gchar *path_format,
- ...)
-{
- xmlXPathObjectPtr xp_obj;
- va_list va;
- gchar *path;
-
- va_start (va, path_format);
- path = g_strdup_vprintf (path_format, va);
- va_end (va);
-
- xp_obj = xmlXPathEvalExpression (XC (path), xp_ctx);
-
- g_free (path);
-
- if (xp_obj == NULL)
- return NULL;
-
- if (xp_obj->type != XPATH_NODESET) {
- xmlXPathFreeObject (xp_obj);
- return NULL;
- }
-
- if (xmlXPathNodeSetGetLength (xp_obj->nodesetval) == 0) {
- xmlXPathFreeObject (xp_obj);
- return NULL;
- }
-
- return xp_obj;
-}
-
-static gchar *
-e_webdav_discover_get_xpath_string (xmlXPathContextPtr xp_ctx,
- const gchar *path_format,
- ...)
-{
- xmlXPathObjectPtr xp_obj;
- va_list va;
- gchar *path;
- gchar *expression;
- gchar *string = NULL;
-
- va_start (va, path_format);
- path = g_strdup_vprintf (path_format, va);
- va_end (va);
-
- expression = g_strdup_printf ("string(%s)", path);
- xp_obj = xmlXPathEvalExpression (XC (expression), xp_ctx);
- g_free (expression);
-
- g_free (path);
-
- if (xp_obj == NULL)
- return NULL;
-
- if (xp_obj->type == XPATH_STRING)
- string = g_strdup ((gchar *) xp_obj->stringval);
-
- /* If the string is empty, return NULL. */
- if (string != NULL && *string == '\0') {
- g_free (string);
- string = NULL;
+ const GSList *link;
+
+ g_return_if_fail (wdd != NULL);
+
+ for (link = resources; link; link = g_slist_next (link)) {
+ const EWebDAVResource *resource = link->data;
+
+ if (resource && (
+ resource->kind == E_WEBDAV_RESOURCE_KIND_ADDRESSBOOK ||
+ resource->kind == E_WEBDAV_RESOURCE_KIND_CALENDAR)) {
+ EWebDAVDiscoveredSource *discovered;
+
+ if (resource->kind == E_WEBDAV_RESOURCE_KIND_CALENDAR &&
+ wdd->only_supports != E_WEBDAV_DISCOVER_SUPPORTS_NONE &&
+ (resource->supports & wdd->only_supports) == 0)
+ continue;
+
+ discovered = g_new0 (EWebDAVDiscoveredSource, 1);
+ discovered->href = g_strdup (resource->href);
+ discovered->supports = resource->supports;
+ discovered->display_name = g_strdup (resource->display_name);
+ discovered->description = g_strdup (resource->description);
+ discovered->color = g_strdup (resource->color);
+
+ if (resource->kind == E_WEBDAV_RESOURCE_KIND_ADDRESSBOOK) {
+ wdd->addressbooks = g_slist_prepend (wdd->addressbooks, discovered);
+ } else {
+ wdd->calendars = g_slist_prepend (wdd->calendars, discovered);
+ }
+ }
}
-
- xmlXPathFreeObject (xp_obj);
-
- return string;
}
static gboolean
-e_webdav_discover_setup_bearer_auth (ESource *source,
- const ENamedParameters *credentials,
- ESoupAuthBearer *bearer,
- GCancellable *cancellable,
- GError **error)
-{
- gchar *access_token = NULL;
- gint expires_in_seconds = -1;
- gboolean success = FALSE;
-
- g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
- g_return_val_if_fail (credentials != NULL, FALSE);
-
- success = e_util_get_source_oauth2_access_token_sync (source, credentials,
- &access_token, &expires_in_seconds, cancellable, error);
-
- if (success)
- e_soup_auth_bearer_set_access_token (bearer, access_token, expires_in_seconds);
-
- g_free (access_token);
-
- return success;
-}
-
-typedef struct _AuthenticateData {
- ESource *source;
- const ENamedParameters *credentials;
-} AuthenticateData;
+e_webdav_discover_propfind_uri_sync (EWebDAVSession *webdav,
+ WebDAVDiscoverData *wdd,
+ const gchar *uri,
+ gboolean only_sets);
-static void
-e_webdav_discover_authenticate_cb (SoupSession *session,
- SoupMessage *msg,
- SoupAuth *auth,
- gboolean retrying,
- gpointer user_data)
+static gboolean
+e_webdav_discover_traverse_propfind_response_cb (EWebDAVSession *webdav,
+ xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prop_prefix,
+ const SoupURI *request_uri,
+ const gchar *href,
+ guint status_code,
+ gpointer user_data)
{
- AuthenticateData *auth_data = user_data;
-
- g_return_if_fail (auth_data != NULL);
-
- if (retrying)
- return;
-
- if (E_IS_SOUP_AUTH_BEARER (auth)) {
- GError *local_error = NULL;
-
- e_webdav_discover_setup_bearer_auth (auth_data->source, auth_data->credentials,
- E_SOUP_AUTH_BEARER (auth), NULL, &local_error);
-
- if (local_error != NULL) {
- soup_message_set_status_full (msg, SOUP_STATUS_FORBIDDEN, local_error->message);
-
- g_error_free (local_error);
- }
- } else {
- gchar *auth_user = NULL;
+ WebDAVDiscoverData *wdd = user_data;
- if (e_named_parameters_get (auth_data->credentials, E_SOURCE_CREDENTIAL_USERNAME))
- auth_user = g_strdup (e_named_parameters_get (auth_data->credentials,
E_SOURCE_CREDENTIAL_USERNAME));
-
- if (auth_user && !*auth_user) {
- g_free (auth_user);
- auth_user = NULL;
- }
+ g_return_val_if_fail (wdd != NULL, FALSE);
- if (!auth_user) {
- ESourceAuthentication *auth_extension;
-
- auth_extension = e_source_get_extension (auth_data->source,
E_SOURCE_EXTENSION_AUTHENTICATION);
- auth_user = e_source_authentication_dup_user (auth_extension);
- }
+ if (!xpath_prop_prefix) {
+ e_xml_xpath_context_register_namespaces (xpath_ctx,
+ "C", E_WEBDAV_NS_CALDAV,
+ "A", E_WEBDAV_NS_CARDDAV,
+ NULL);
+ } else if (status_code == SOUP_STATUS_OK) {
+ xmlXPathObjectPtr xpath_obj;
+ gchar *principal_href, *full_href;
- if (!auth_user || !*auth_user || !auth_data->credentials || !e_named_parameters_get
(auth_data->credentials, E_SOURCE_CREDENTIAL_PASSWORD))
- soup_message_set_status (msg, SOUP_STATUS_FORBIDDEN);
- else
- soup_auth_authenticate (auth, auth_user, e_named_parameters_get
(auth_data->credentials, E_SOURCE_CREDENTIAL_PASSWORD));
+ xpath_obj = e_xml_xpath_eval (xpath_ctx, "%s/A:addressbook-home-set", xpath_prop_prefix);
+ if (xpath_obj) {
+ gint ii, length;
- g_free (auth_user);
- }
-}
+ length = xmlXPathNodeSetGetLength (xpath_obj->nodesetval);
-static gboolean
-e_webdav_discover_check_successful (SoupMessage *message,
- gchar **out_certificate_pem,
- GTlsCertificateFlags *out_certificate_errors,
- GError **error)
-{
- GIOErrorEnum error_code;
+ for (ii = 0; ii < length; ii++) {
+ gchar *home_set_href;
- g_return_val_if_fail (message != NULL, FALSE);
+ full_href = NULL;
- /* Loosely copied from the GVFS DAV backend. */
+ home_set_href = e_xml_xpath_eval_as_string (xpath_ctx,
"%s/A:addressbook-home-set/D:href[%d]", xpath_prop_prefix, ii + 1);
+ if (home_set_href && *home_set_href) {
+ GSList *resources = NULL;
- if (SOUP_STATUS_IS_SUCCESSFUL (message->status_code))
- return TRUE;
+ full_href = e_webdav_session_ensure_full_uri (webdav, request_uri,
home_set_href);
+ if (full_href && *full_href && !g_hash_table_contains
(wdd->covered_hrefs, full_href) &&
+ e_webdav_session_list_sync (webdav, full_href,
E_WEBDAV_DEPTH_THIS_AND_CHILDREN,
+ E_WEBDAV_LIST_SUPPORTS | E_WEBDAV_LIST_DISPLAY_NAME |
E_WEBDAV_LIST_DESCRIPTION | E_WEBDAV_LIST_COLOR,
+ &resources, wdd->cancellable, wdd->error)) {
+ e_webdav_discover_split_resources (wdd, resources);
+ g_slist_free_full (resources, e_webdav_resource_free);
+ }
- switch (message->status_code) {
- case SOUP_STATUS_CANCELLED:
- error_code = G_IO_ERROR_CANCELLED;
- break;
- case SOUP_STATUS_NOT_FOUND:
- error_code = G_IO_ERROR_NOT_FOUND;
- break;
- case SOUP_STATUS_UNAUTHORIZED:
- case SOUP_STATUS_FORBIDDEN:
- g_set_error (
- error, SOUP_HTTP_ERROR, message->status_code,
- _("HTTP Error: %s"), message->reason_phrase);
- return FALSE;
- case SOUP_STATUS_PAYMENT_REQUIRED:
- error_code = G_IO_ERROR_PERMISSION_DENIED;
- break;
- case SOUP_STATUS_REQUEST_TIMEOUT:
- error_code = G_IO_ERROR_TIMED_OUT;
- break;
- case SOUP_STATUS_CANT_RESOLVE:
- error_code = G_IO_ERROR_HOST_NOT_FOUND;
- break;
- case SOUP_STATUS_NOT_IMPLEMENTED:
- error_code = G_IO_ERROR_NOT_SUPPORTED;
- break;
- case SOUP_STATUS_INSUFFICIENT_STORAGE:
- error_code = G_IO_ERROR_NO_SPACE;
- break;
- case SOUP_STATUS_SSL_FAILED:
- if (out_certificate_pem) {
- GTlsCertificate *certificate = NULL;
-
- g_free (*out_certificate_pem);
- *out_certificate_pem = NULL;
-
- g_object_get (G_OBJECT (message), "tls-certificate", &certificate, NULL);
-
- if (certificate) {
- g_object_get (certificate, "certificate-pem", out_certificate_pem,
NULL);
- g_object_unref (certificate);
+ if (full_href && *full_href)
+ g_hash_table_insert (wdd->covered_hrefs, g_strdup
(full_href), GINT_TO_POINTER (1));
}
- }
- if (out_certificate_errors) {
- *out_certificate_errors = 0;
- g_object_get (G_OBJECT (message), "tls-errors", out_certificate_errors, NULL);
+ g_free (home_set_href);
+ g_free (full_href);
}
- g_set_error (
- error, SOUP_HTTP_ERROR, message->status_code,
- _("HTTP Error: %s"), message->reason_phrase);
- return FALSE;
- default:
- error_code = G_IO_ERROR_FAILED;
- break;
- }
-
- g_set_error (
- error, G_IO_ERROR, error_code,
- _("HTTP Error: %s"), message->reason_phrase);
-
- return FALSE;
-}
-
-static xmlDocPtr
-e_webdav_discover_parse_xml (SoupMessage *message,
- const gchar *expected_name,
- gchar **out_certificate_pem,
- GTlsCertificateFlags *out_certificate_errors,
- GError **error)
-{
- xmlDocPtr doc;
- xmlNodePtr root;
-
- if (!e_webdav_discover_check_successful (message, out_certificate_pem, out_certificate_errors, error))
- return NULL;
-
- doc = xmlReadMemory (
- message->response_body->data,
- message->response_body->length,
- "response.xml", NULL,
- XML_PARSE_NONET |
- XML_PARSE_NOWARNING |
- XML_PARSE_NOCDATA |
- XML_PARSE_COMPACT);
-
- if (doc == NULL) {
- g_set_error_literal (
- error, G_IO_ERROR, G_IO_ERROR_FAILED,
- _("Could not parse response"));
- return NULL;
- }
-
- root = xmlDocGetRootElement (doc);
-
- if (root == NULL || root->children == NULL) {
- g_set_error_literal (
- error, G_IO_ERROR, G_IO_ERROR_FAILED,
- _("Empty response"));
- xmlFreeDoc (doc);
- return NULL;
- }
-
- if (g_strcmp0 ((gchar *) root->name, expected_name) != 0) {
- g_set_error_literal (
- error, G_IO_ERROR, G_IO_ERROR_FAILED,
- _("Unexpected reply from server"));
- xmlFreeDoc (doc);
- return NULL;
- }
-
- return doc;
-}
-
-static void
-e_webdav_discover_process_user_address_set (xmlXPathContextPtr xp_ctx,
- GSList **out_calendar_user_addresses)
-{
- xmlXPathObjectPtr xp_obj;
- gint ii, length;
-
- if (!out_calendar_user_addresses)
- return;
-
- xp_obj = e_webdav_discover_get_xpath (
- xp_ctx,
- "/D:multistatus"
- "/D:response"
- "/D:propstat"
- "/D:prop"
- "/C:calendar-user-address-set");
-
- if (xp_obj == NULL)
- return;
-
- length = xmlXPathNodeSetGetLength (xp_obj->nodesetval);
-
- for (ii = 0; ii < length; ii++) {
- GSList *duplicate;
- const gchar *address;
- gchar *href;
-
- href = e_webdav_discover_get_xpath_string (
- xp_ctx,
- "/D:multistatus"
- "/D:response"
- "/D:propstat"
- "/D:prop"
- "/C:calendar-user-address-set"
- "/D:href[%d]", ii + 1);
-
- if (href == NULL)
- continue;
-
- if (!g_str_has_prefix (href, "mailto:")) {
- g_free (href);
- continue;
- }
-
- /* strlen("mailto:") == 7 */
- address = href + 7;
-
- /* Avoid duplicates. */
- duplicate = g_slist_find_custom (
- *out_calendar_user_addresses,
- address, (GCompareFunc) g_ascii_strcasecmp);
-
- if (duplicate != NULL) {
- g_free (href);
- continue;
+ xmlXPathFreeObject (xpath_obj);
}
- *out_calendar_user_addresses = g_slist_prepend (
- *out_calendar_user_addresses, g_strdup (address));
-
- g_free (href);
- }
-
- xmlXPathFreeObject (xp_obj);
-}
-
-static guint32
-e_webdav_discover_get_supported_component_set (xmlXPathContextPtr xp_ctx,
- gint response_index,
- gint propstat_index)
-{
- xmlXPathObjectPtr xp_obj;
- guint32 set = 0;
- gint ii, length;
-
- xp_obj = e_webdav_discover_get_xpath (
- xp_ctx,
- "/D:multistatus"
- "/D:response[%d]"
- "/D:propstat[%d]"
- "/D:prop"
- "/C:supported-calendar-component-set"
- "/C:comp",
- response_index,
- propstat_index);
-
- /* If the property is not present, assume all component
- * types are supported. (RFC 4791, Section 5.2.3) */
- if (xp_obj == NULL)
- return E_WEBDAV_DISCOVER_SUPPORTS_EVENTS |
- E_WEBDAV_DISCOVER_SUPPORTS_MEMOS |
- E_WEBDAV_DISCOVER_SUPPORTS_TASKS;
-
- length = xmlXPathNodeSetGetLength (xp_obj->nodesetval);
-
- for (ii = 0; ii < length; ii++) {
- gchar *name;
-
- name = e_webdav_discover_get_xpath_string (
- xp_ctx,
- "/D:multistatus"
- "/D:response[%d]"
- "/D:propstat[%d]"
- "/D:prop"
- "/C:supported-calendar-component-set"
- "/C:comp[%d]"
- "/@name",
- response_index,
- propstat_index,
- ii + 1);
-
- if (name == NULL)
- continue;
-
- if (g_ascii_strcasecmp (name, "VEVENT") == 0)
- set |= E_WEBDAV_DISCOVER_SUPPORTS_EVENTS;
- else if (g_ascii_strcasecmp (name, "VJOURNAL") == 0)
- set |= E_WEBDAV_DISCOVER_SUPPORTS_MEMOS;
- else if (g_ascii_strcasecmp (name, "VTODO") == 0)
- set |= E_WEBDAV_DISCOVER_SUPPORTS_TASKS;
-
- g_free (name);
- }
-
- xmlXPathFreeObject (xp_obj);
-
- return set;
-}
-
-static void
-e_webdav_discover_process_calendar_response_propstat (SoupMessage *message,
- xmlXPathContextPtr xp_ctx,
- gint response_index,
- gint propstat_index,
- GSList **out_discovered_sources)
-{
- xmlXPathObjectPtr xp_obj;
- guint32 comp_set;
- gchar *color_spec;
- gchar *display_name;
- gchar *description;
- gchar *href_encoded;
- gchar *status_line;
- guint status;
- gboolean success;
- EWebDAVDiscoveredSource *discovered_source;
-
- if (!out_discovered_sources)
- return;
-
- status_line = e_webdav_discover_get_xpath_string (
- xp_ctx,
- "/D:multistatus"
- "/D:response[%d]"
- "/D:propstat[%d]"
- "/D:status",
- response_index,
- propstat_index);
-
- if (status_line == NULL)
- return;
-
- success = soup_headers_parse_status_line (
- status_line, NULL, &status, NULL);
-
- g_free (status_line);
+ xpath_obj = e_xml_xpath_eval (xpath_ctx, "%s/C:calendar-home-set", xpath_prop_prefix);
+ if (xpath_obj) {
+ gint ii, length;
- if (!success || status != SOUP_STATUS_OK)
- return;
+ length = xmlXPathNodeSetGetLength (xpath_obj->nodesetval);
- comp_set = e_webdav_discover_get_supported_component_set (xp_ctx, response_index, propstat_index);
- if (comp_set == E_WEBDAV_DISCOVER_SUPPORTS_NONE)
- return;
+ for (ii = 0; ii < length; ii++) {
+ gchar *home_set_href, *full_href = NULL;
- href_encoded = e_webdav_discover_get_xpath_string (
- xp_ctx,
- "/D:multistatus"
- "/D:response[%d]"
- "/D:href",
- response_index);
+ home_set_href = e_xml_xpath_eval_as_string (xpath_ctx,
"%s/C:calendar-home-set/D:href[%d]", xpath_prop_prefix, ii + 1);
+ if (home_set_href && *home_set_href) {
+ GSList *resources = NULL;
- if (href_encoded == NULL)
- return;
-
- /* Make sure the resource is a calendar. */
-
- xp_obj = e_webdav_discover_get_xpath (
- xp_ctx,
- "/D:multistatus"
- "/D:response[%d]"
- "/D:propstat[%d]"
- "/D:prop"
- "/D:resourcetype"
- "/C:calendar",
- response_index,
- propstat_index);
-
- if (xp_obj == NULL) {
- g_free (href_encoded);
- return;
- }
+ full_href = e_webdav_session_ensure_full_uri (webdav, request_uri,
home_set_href);
+ if (full_href && *full_href && !g_hash_table_contains
(wdd->covered_hrefs, full_href) &&
+ e_webdav_session_list_sync (webdav, full_href,
E_WEBDAV_DEPTH_THIS_AND_CHILDREN,
+ E_WEBDAV_LIST_SUPPORTS | E_WEBDAV_LIST_DISPLAY_NAME |
E_WEBDAV_LIST_DESCRIPTION | E_WEBDAV_LIST_COLOR,
+ &resources, wdd->cancellable, wdd->error)) {
+ e_webdav_discover_split_resources (wdd, resources);
+ g_slist_free_full (resources, e_webdav_resource_free);
+ }
- xmlXPathFreeObject (xp_obj);
-
- /* Get the display name or fall back to the href. */
-
- display_name = e_webdav_discover_get_xpath_string (
- xp_ctx,
- "/D:multistatus"
- "/D:response[%d]"
- "/D:propstat[%d]"
- "/D:prop"
- "/D:displayname",
- response_index,
- propstat_index);
-
- if (display_name == NULL) {
- gchar *href_decoded = soup_uri_decode (href_encoded);
-
- if (href_decoded) {
- gchar *cp;
-
- /* Use the last non-empty path segment. */
- while ((cp = strrchr (href_decoded, '/')) != NULL) {
- if (*(cp + 1) == '\0')
- *cp = '\0';
- else {
- display_name = g_strdup (cp + 1);
- break;
+ if (full_href && *full_href)
+ g_hash_table_insert (wdd->covered_hrefs, g_strdup
(full_href), GINT_TO_POINTER (1));
}
- }
- }
-
- g_free (href_decoded);
- }
-
- description = e_webdav_discover_get_xpath_string (
- xp_ctx,
- "/D:multistatus"
- "/D:response[%d]"
- "/D:propstat[%d]"
- "/D:prop"
- "/C:calendar-description",
- response_index,
- propstat_index);
-
- /* Get the color specification string. */
-
- color_spec = e_webdav_discover_get_xpath_string (
- xp_ctx,
- "/D:multistatus"
- "/D:response[%d]"
- "/D:propstat[%d]"
- "/D:prop"
- "/IC:calendar-color",
- response_index,
- propstat_index);
-
- discovered_source = g_new0 (EWebDAVDiscoveredSource, 1);
- discovered_source->href = e_webdav_discover_make_href_full_uri (soup_message_get_uri (message),
href_encoded);
- discovered_source->supports = comp_set;
- discovered_source->display_name = g_strdup (display_name);
- discovered_source->description = g_strdup (description);
- discovered_source->color = g_strdup (color_spec);
-
- *out_discovered_sources = g_slist_prepend (*out_discovered_sources, discovered_source);
-
- g_free (href_encoded);
- g_free (display_name);
- g_free (description);
- g_free (color_spec);
-}
-static void
-e_webdav_discover_traverse_responses (SoupMessage *message,
- xmlXPathContextPtr xp_ctx,
- GSList **out_discovered_sources,
- void (* func) (
- SoupMessage *message,
- xmlXPathContextPtr xp_ctx,
- gint response_index,
- gint propstat_index,
- GSList **out_discovered_sources))
-{
- xmlXPathObjectPtr xp_obj_response;
-
- xp_obj_response = e_webdav_discover_get_xpath (
- xp_ctx,
- "/D:multistatus"
- "/D:response");
-
- if (xp_obj_response != NULL) {
- gint response_index, response_length;
+ g_free (home_set_href);
+ g_free (full_href);
+ }
- response_length = xmlXPathNodeSetGetLength (xp_obj_response->nodesetval);
+ xmlXPathFreeObject (xpath_obj);
+ }
- for (response_index = 0; response_index < response_length; response_index++) {
- xmlXPathObjectPtr xp_obj_propstat;
+ xpath_obj = e_xml_xpath_eval (xpath_ctx, "%s/C:calendar-user-address-set", xpath_prop_prefix);
+ if (xpath_obj) {
+ gint ii, length;
- xp_obj_propstat = e_webdav_discover_get_xpath (
- xp_ctx,
- "/D:multistatus"
- "/D:response[%d]"
- "/D:propstat",
- response_index + 1);
+ length = xmlXPathNodeSetGetLength (xpath_obj->nodesetval);
- if (xp_obj_propstat != NULL) {
- gint propstat_index, propstat_length;
+ for (ii = 0; ii < length; ii++) {
+ gchar *address_href;
- propstat_length = xmlXPathNodeSetGetLength (xp_obj_propstat->nodesetval);
+ address_href = e_xml_xpath_eval_as_string (xpath_ctx,
"%s/C:calendar-user-address-set/D:href[%d]", xpath_prop_prefix, ii + 1);
+ if (address_href && g_ascii_strncasecmp (address_href, "mailto:", 7) == 0) {
+ /* Skip the "mailto:" prefix */
+ const gchar *address = address_href + 7;
- for (propstat_index = 0; propstat_index < propstat_length; propstat_index++) {
- func (message, xp_ctx, response_index + 1, propstat_index + 1,
out_discovered_sources);
+ /* Avoid duplicates and empty values */
+ if (*address &&
+ !g_slist_find_custom (*wdd->out_calendar_user_addresses, address,
(GCompareFunc) g_ascii_strcasecmp)) {
+ *wdd->out_calendar_user_addresses = g_slist_prepend (
+ *wdd->out_calendar_user_addresses, g_strdup
(address));
+ }
}
- xmlXPathFreeObject (xp_obj_propstat);
+ g_free (address_href);
}
- }
- xmlXPathFreeObject (xp_obj_response);
- }
-}
-
-static gboolean
-e_webdav_discover_get_calendar_collection_details (SoupSession *session,
- SoupMessage *message,
- const gchar *path_or_uri,
- ESource *source,
- gchar **out_certificate_pem,
- GTlsCertificateFlags *out_certificate_errors,
- GSList **out_discovered_sources,
- GCancellable *cancellable,
- GError **error)
-{
- xmlDocPtr doc;
- xmlXPathContextPtr xp_ctx;
- SoupURI *soup_uri;
- GError *local_error = NULL;
-
- if (g_cancellable_is_cancelled (cancellable))
- return FALSE;
-
- soup_uri = soup_uri_new (path_or_uri);
- if (!soup_uri ||
- !soup_uri_get_scheme (soup_uri) ||
- !soup_uri_get_host (soup_uri) ||
- !soup_uri_get_path (soup_uri) ||
- !*soup_uri_get_scheme (soup_uri) ||
- !*soup_uri_get_host (soup_uri) ||
- !*soup_uri_get_path (soup_uri)) {
- /* it's a path only, not full uri */
- if (soup_uri)
- soup_uri_free (soup_uri);
- soup_uri = soup_uri_copy (soup_message_get_uri (message));
- soup_uri_set_path (soup_uri, path_or_uri);
- }
+ xmlXPathFreeObject (xpath_obj);
+ }
- message = e_webdav_discover_new_propfind (
- session, soup_uri, DEPTH_1,
- NS_WEBDAV, XC ("displayname"),
- NS_WEBDAV, XC ("resourcetype"),
- NS_CALDAV, XC ("calendar-description"),
- NS_CALDAV, XC ("supported-calendar-component-set"),
- NS_CALDAV, XC ("calendar-user-address-set"),
- NS_ICAL, XC ("calendar-color"),
- NULL);
-
- e_soup_ssl_trust_connect (message, source);
-
- /* This takes ownership of the message. */
- soup_session_send_message (session, message);
-
- if (message->status_code == SOUP_STATUS_BAD_REQUEST) {
- g_clear_object (&message);
-
- message = e_webdav_discover_new_propfind (
- session, soup_uri, DEPTH_0,
- NS_WEBDAV, XC ("displayname"),
- NS_WEBDAV, XC ("resourcetype"),
- NS_CALDAV, XC ("calendar-description"),
- NS_CALDAV, XC ("supported-calendar-component-set"),
- NS_CALDAV, XC ("calendar-user-address-set"),
- NS_ICAL, XC ("calendar-color"),
- NULL);
+ principal_href = e_xml_xpath_eval_as_string (xpath_ctx, "%s/D:current-user-principal/D:href",
xpath_prop_prefix);
+ if (principal_href && *principal_href) {
+ full_href = e_webdav_session_ensure_full_uri (webdav, request_uri, principal_href);
- e_soup_ssl_trust_connect (message, source);
- soup_session_send_message (session, message);
- }
+ if (full_href && *full_href)
+ e_webdav_discover_propfind_uri_sync (webdav, wdd, full_href, TRUE);
- soup_uri_free (soup_uri);
-
- doc = e_webdav_discover_parse_xml (message, "multistatus", out_certificate_pem,
out_certificate_errors, &local_error);
- if (!doc) {
- g_clear_object (&message);
+ g_free (full_href);
+ g_free (principal_href);
- if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_FAILED) ||
- g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
- /* Ignore these errors, but still propagate them. */
- g_propagate_error (error, local_error);
return TRUE;
- } else if (local_error) {
- g_propagate_error (error, local_error);
}
- return FALSE;
- }
-
- xp_ctx = xmlXPathNewContext (doc);
- xmlXPathRegisterNs (xp_ctx, XC ("D"), XC (NS_WEBDAV));
- xmlXPathRegisterNs (xp_ctx, XC ("C"), XC (NS_CALDAV));
- xmlXPathRegisterNs (xp_ctx, XC ("A"), XC (NS_CARDDAV));
- xmlXPathRegisterNs (xp_ctx, XC ("IC"), XC (NS_ICAL));
-
- e_webdav_discover_traverse_responses (message, xp_ctx, out_discovered_sources,
- e_webdav_discover_process_calendar_response_propstat);
-
- xmlXPathFreeContext (xp_ctx);
- xmlFreeDoc (doc);
-
- g_clear_object (&message);
-
- return TRUE;
-}
-
-static gboolean
-e_webdav_discover_process_calendar_home_set (SoupSession *session,
- SoupMessage *message,
- ESource *source,
- gchar **out_certificate_pem,
- GTlsCertificateFlags *out_certificate_errors,
- GSList **out_discovered_sources,
- GSList **out_calendar_user_addresses,
- GCancellable *cancellable,
- GError **error)
-{
- SoupURI *soup_uri;
- xmlDocPtr doc;
- xmlXPathContextPtr xp_ctx;
- xmlXPathObjectPtr xp_obj;
- gchar *calendar_home_set;
- GError *local_error = NULL;
- gboolean success;
+ g_free (principal_href);
- g_return_val_if_fail (SOUP_IS_SESSION (session), FALSE);
- g_return_val_if_fail (SOUP_IS_MESSAGE (message), FALSE);
- g_return_val_if_fail (out_discovered_sources != NULL, FALSE);
- g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
+ principal_href = e_xml_xpath_eval_as_string (xpath_ctx, "%s/D:principal-URL/D:href",
xpath_prop_prefix);
+ if (principal_href && *principal_href) {
+ full_href = e_webdav_session_ensure_full_uri (webdav, request_uri, principal_href);
- if (g_cancellable_is_cancelled (cancellable))
- return FALSE;
+ if (full_href && *full_href)
+ e_webdav_discover_propfind_uri_sync (webdav, wdd, full_href, TRUE);
- doc = e_webdav_discover_parse_xml (message, "multistatus", out_certificate_pem,
out_certificate_errors, &local_error);
+ g_free (full_href);
+ g_free (principal_href);
- if (!doc) {
- if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_FAILED) ||
- g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
- /* Ignore these errors, but still propagate them. */
- g_propagate_error (error, local_error);
return TRUE;
- } else if (local_error) {
- g_propagate_error (error, local_error);
}
- return FALSE;
- }
+ g_free (principal_href);
- xp_ctx = xmlXPathNewContext (doc);
- xmlXPathRegisterNs (xp_ctx, XC ("D"), XC (NS_WEBDAV));
- xmlXPathRegisterNs (xp_ctx, XC ("C"), XC (NS_CALDAV));
- xmlXPathRegisterNs (xp_ctx, XC ("A"), XC (NS_CARDDAV));
-
- /* Record any "C:calendar-user-address-set" properties. */
- e_webdav_discover_process_user_address_set (xp_ctx, out_calendar_user_addresses);
-
- /* Try to find the calendar home URL using the
- * following properties in order of preference:
- *
- * "C:calendar-home-set"
- * "D:current-user-principal"
- * "D:principal-URL"
- *
- * If the second or third URL preference is used, rerun
- * the PROPFIND method on that URL at Depth=1 in hopes
- * of getting a proper "C:calendar-home-set" property.
- */
-
- /* FIXME There can be multiple "D:href" elements for a
- * "C:calendar-home-set". We're only processing
- * the first one. Need to iterate over them. */
-
- calendar_home_set = e_webdav_discover_get_xpath_string (
- xp_ctx,
- "/D:multistatus"
- "/D:response"
- "/D:propstat"
- "/D:prop"
- "/C:calendar-home-set"
- "/D:href");
-
- if (calendar_home_set != NULL)
- goto get_collection_details;
-
- g_free (calendar_home_set);
-
- calendar_home_set = e_webdav_discover_get_xpath_string (
- xp_ctx,
- "/D:multistatus"
- "/D:response"
- "/D:propstat"
- "/D:prop"
- "/D:current-user-principal"
- "/D:href");
-
- if (calendar_home_set != NULL)
- goto retry_propfind;
-
- g_free (calendar_home_set);
-
- calendar_home_set = e_webdav_discover_get_xpath_string (
- xp_ctx,
- "/D:multistatus"
- "/D:response"
- "/D:propstat"
- "/D:prop"
- "/D:principal-URL"
- "/D:href");
-
- if (calendar_home_set != NULL)
- goto retry_propfind;
-
- g_free (calendar_home_set);
- calendar_home_set = NULL;
-
- /* None of the aforementioned properties are present. If the
- * user-supplied CalDAV URL is a calendar resource, use that. */
-
- xp_obj = e_webdav_discover_get_xpath (
- xp_ctx,
- "/D:multistatus"
- "/D:response"
- "/D:propstat"
- "/D:prop"
- "/D:resourcetype"
- "/C:calendar");
-
- if (xp_obj != NULL) {
- soup_uri = soup_message_get_uri (message);
-
- if (soup_uri->path != NULL && *soup_uri->path != '\0') {
- gchar *slash;
-
- soup_uri = soup_uri_copy (soup_uri);
-
- slash = strrchr (soup_uri->path, '/');
- while (slash != NULL && slash != soup_uri->path) {
-
- if (slash[1] != '\0') {
- slash[1] = '\0';
- calendar_home_set =
- g_strdup (soup_uri->path);
- break;
- }
+ if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:resourcetype/C:calendar", xpath_prop_prefix) ||
+ e_xml_xpath_eval_exists (xpath_ctx, "%s/D:resourcetype/A:addressbook",
xpath_prop_prefix)) {
+ GSList *resources = NULL;
- slash[0] = '\0';
- slash = strrchr (soup_uri->path, '/');
+ if (!g_hash_table_contains (wdd->covered_hrefs, href) &&
+ e_webdav_session_list_sync (webdav, href, E_WEBDAV_DEPTH_THIS,
+ E_WEBDAV_LIST_SUPPORTS | E_WEBDAV_LIST_DISPLAY_NAME |
E_WEBDAV_LIST_DESCRIPTION | E_WEBDAV_LIST_COLOR,
+ &resources, wdd->cancellable, wdd->error)) {
+ e_webdav_discover_split_resources (wdd, resources);
+ g_slist_free_full (resources, e_webdav_resource_free);
}
- soup_uri_free (soup_uri);
+ g_hash_table_insert (wdd->covered_hrefs, g_strdup (href), GINT_TO_POINTER (1));
}
-
- xmlXPathFreeObject (xp_obj);
}
- if (calendar_home_set == NULL || *calendar_home_set == '\0') {
- g_free (calendar_home_set);
- xmlXPathFreeContext (xp_ctx);
- xmlFreeDoc (doc);
- return TRUE;
- }
-
- get_collection_details:
-
- xmlXPathFreeContext (xp_ctx);
- xmlFreeDoc (doc);
-
- if (!e_webdav_discover_get_calendar_collection_details (
- session, message, calendar_home_set, source,
- out_certificate_pem, out_certificate_errors, out_discovered_sources,
- cancellable, error)) {
- g_free (calendar_home_set);
- return FALSE;
- }
-
- g_free (calendar_home_set);
-
return TRUE;
-
- retry_propfind:
-
- xmlXPathFreeContext (xp_ctx);
- xmlFreeDoc (doc);
-
- soup_uri = soup_uri_copy (soup_message_get_uri (message));
- soup_uri_set_path (soup_uri, calendar_home_set);
-
- /* Note that we omit "D:resourcetype", "D:current-user-principal"
- * and "D:principal-URL" in order to short-circuit the recursion. */
- message = e_webdav_discover_new_propfind (
- session, soup_uri, DEPTH_1,
- NS_CALDAV, XC ("calendar-home-set"),
- NS_CALDAV, XC ("calendar-user-address-set"),
- NULL);
-
- e_soup_ssl_trust_connect (message, source);
-
- /* This takes ownership of the message. */
- soup_session_send_message (session, message);
-
- if (message->status_code == SOUP_STATUS_BAD_REQUEST) {
- g_clear_object (&message);
-
- message = e_webdav_discover_new_propfind (
- session, soup_uri, DEPTH_0,
- NS_CALDAV, XC ("calendar-home-set"),
- NS_CALDAV, XC ("calendar-user-address-set"),
- NULL);
-
- e_soup_ssl_trust_connect (message, source);
- soup_session_send_message (session, message);
- }
-
- soup_uri_free (soup_uri);
-
- g_free (calendar_home_set);
-
- success = e_webdav_discover_process_calendar_home_set (session, message, source,
- out_certificate_pem, out_certificate_errors, out_discovered_sources,
out_calendar_user_addresses,
- cancellable, error);
-
- g_object_unref (message);
-
- return success;
}
-static void
-e_webdav_discover_process_addressbook_response_propstat (SoupMessage *message,
- xmlXPathContextPtr xp_ctx,
- gint response_index,
- gint propstat_index,
- GSList **out_discovered_sources)
+static gboolean
+e_webdav_discover_propfind_uri_sync (EWebDAVSession *webdav,
+ WebDAVDiscoverData *wdd,
+ const gchar *uri,
+ gboolean only_sets)
{
- xmlXPathObjectPtr xp_obj;
- gchar *display_name;
- gchar *description;
- gchar *href_encoded;
- gchar *status_line;
- guint status;
+ EXmlDocument *xml;
gboolean success;
- EWebDAVDiscoveredSource *discovered_source;
-
- if (!out_discovered_sources)
- return;
-
- status_line = e_webdav_discover_get_xpath_string (
- xp_ctx,
- "/D:multistatus"
- "/D:response[%d]"
- "/D:propstat[%d]"
- "/D:status",
- response_index,
- propstat_index);
-
- if (status_line == NULL)
- return;
- success = soup_headers_parse_status_line (
- status_line, NULL, &status, NULL);
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (wdd != NULL, FALSE);
+ g_return_val_if_fail (uri && *uri, FALSE);
- g_free (status_line);
+ if (g_hash_table_contains (wdd->covered_hrefs, uri))
+ return TRUE;
- if (!success || status != SOUP_STATUS_OK)
- return;
+ g_hash_table_insert (wdd->covered_hrefs, g_strdup (uri), GINT_TO_POINTER (1));
- href_encoded = e_webdav_discover_get_xpath_string (
- xp_ctx,
- "/D:multistatus"
- "/D:response[%d]"
- "/D:href",
- response_index);
+ xml = e_xml_document_new (E_WEBDAV_NS_DAV, "propfind");
+ g_return_val_if_fail (xml != NULL, FALSE);
- if (href_encoded == NULL)
- return;
+ e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "prop");
- /* Make sure the resource is an addressbook. */
-
- xp_obj = e_webdav_discover_get_xpath (
- xp_ctx,
- "/D:multistatus"
- "/D:response[%d]"
- "/D:propstat[%d]"
- "/D:prop"
- "/D:resourcetype"
- "/A:addressbook",
- response_index,
- propstat_index);
-
- if (xp_obj == NULL) {
- g_free (href_encoded);
- return;
+ if (!only_sets) {
+ e_xml_document_add_empty_element (xml, E_WEBDAV_NS_DAV, "resourcetype");
+ e_xml_document_add_empty_element (xml, E_WEBDAV_NS_DAV, "current-user-principal");
+ e_xml_document_add_empty_element (xml, E_WEBDAV_NS_DAV, "principal-URL");
}
- xmlXPathFreeObject (xp_obj);
-
- /* Get the display name or fall back to the href. */
-
- display_name = e_webdav_discover_get_xpath_string (
- xp_ctx,
- "/D:multistatus"
- "/D:response[%d]"
- "/D:propstat[%d]"
- "/D:prop"
- "/D:displayname",
- response_index,
- propstat_index);
-
- if (display_name == NULL) {
- gchar *href_decoded = soup_uri_decode (href_encoded);
-
- if (href_decoded) {
- gchar *cp;
-
- /* Use the last non-empty path segment. */
- while ((cp = strrchr (href_decoded, '/')) != NULL) {
- if (*(cp + 1) == '\0')
- *cp = '\0';
- else {
- display_name = g_strdup (cp + 1);
- break;
- }
- }
- }
-
- g_free (href_decoded);
+ if ((wdd->only_supports == E_WEBDAV_DISCOVER_SUPPORTS_NONE ||
+ (wdd->only_supports & (E_WEBDAV_DISCOVER_SUPPORTS_EVENTS | E_WEBDAV_DISCOVER_SUPPORTS_MEMOS |
E_WEBDAV_DISCOVER_SUPPORTS_TASKS)) != 0)) {
+ e_xml_document_add_empty_element (xml, E_WEBDAV_NS_CALDAV, "calendar-home-set");
+ e_xml_document_add_empty_element (xml, E_WEBDAV_NS_CALDAV, "calendar-user-address-set");
}
- description = e_webdav_discover_get_xpath_string (
- xp_ctx,
- "/D:multistatus"
- "/D:response[%d]"
- "/D:propstat[%d]"
- "/D:prop"
- "/A:addressbook-description",
- response_index,
- propstat_index);
-
- discovered_source = g_new0 (EWebDAVDiscoveredSource, 1);
- discovered_source->href = e_webdav_discover_make_href_full_uri (soup_message_get_uri (message),
href_encoded);
- discovered_source->supports = E_WEBDAV_DISCOVER_SUPPORTS_CONTACTS;
- discovered_source->display_name = g_strdup (display_name);
- discovered_source->description = g_strdup (description);
- discovered_source->color = NULL;
-
- *out_discovered_sources = g_slist_prepend (*out_discovered_sources, discovered_source);
-
- g_free (href_encoded);
- g_free (display_name);
- g_free (description);
-}
-
-static gboolean
-e_webdav_discover_get_addressbook_collection_details (SoupSession *session,
- SoupMessage *message,
- const gchar *path_or_uri,
- ESource *source,
- gchar **out_certificate_pem,
- GTlsCertificateFlags *out_certificate_errors,
- GSList **out_discovered_sources,
- GCancellable *cancellable,
- GError **error)
-{
- xmlDocPtr doc;
- xmlXPathContextPtr xp_ctx;
- SoupURI *soup_uri;
- GError *local_error = NULL;
-
- if (g_cancellable_is_cancelled (cancellable))
- return FALSE;
-
- soup_uri = soup_uri_new (path_or_uri);
- if (!soup_uri ||
- !soup_uri_get_scheme (soup_uri) ||
- !soup_uri_get_host (soup_uri) ||
- !soup_uri_get_path (soup_uri) ||
- !*soup_uri_get_scheme (soup_uri) ||
- !*soup_uri_get_host (soup_uri) ||
- !*soup_uri_get_path (soup_uri)) {
- /* it's a path only, not full uri */
- if (soup_uri)
- soup_uri_free (soup_uri);
- soup_uri = soup_uri_copy (soup_message_get_uri (message));
- soup_uri_set_path (soup_uri, path_or_uri);
+ if ((wdd->only_supports == E_WEBDAV_DISCOVER_SUPPORTS_NONE ||
+ (wdd->only_supports & (E_WEBDAV_DISCOVER_SUPPORTS_CONTACTS)) != 0)) {
+ e_xml_document_add_empty_element (xml, E_WEBDAV_NS_CARDDAV, "addressbook-home-set");
}
- message = e_webdav_discover_new_propfind (
- session, soup_uri, DEPTH_1,
- NS_WEBDAV, XC ("displayname"),
- NS_WEBDAV, XC ("resourcetype"),
- NS_CARDDAV, XC ("addressbook-description"),
- NULL);
-
- e_soup_ssl_trust_connect (message, source);
+ e_xml_document_end_element (xml); /* prop */
- /* This takes ownership of the message. */
- soup_session_send_message (session, message);
-
- soup_uri_free (soup_uri);
-
- doc = e_webdav_discover_parse_xml (message, "multistatus", out_certificate_pem,
out_certificate_errors, &local_error);
- if (!doc) {
- g_clear_object (&message);
-
- if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_FAILED) ||
- g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
- /* Ignore these errors, but still propagate them. */
- g_propagate_error (error, local_error);
- return TRUE;
- } else if (local_error) {
- g_propagate_error (error, local_error);
- }
+ success = e_webdav_session_propfind_sync (webdav, uri, E_WEBDAV_DEPTH_THIS, xml,
+ e_webdav_discover_traverse_propfind_response_cb, wdd, wdd->cancellable, wdd->error);
- return FALSE;
- }
+ g_clear_object (&xml);
- xp_ctx = xmlXPathNewContext (doc);
- xmlXPathRegisterNs (xp_ctx, XC ("D"), XC (NS_WEBDAV));
- xmlXPathRegisterNs (xp_ctx, XC ("C"), XC (NS_CALDAV));
- xmlXPathRegisterNs (xp_ctx, XC ("A"), XC (NS_CARDDAV));
- xmlXPathRegisterNs (xp_ctx, XC ("IC"), XC (NS_ICAL));
+ return success;
+}
- e_webdav_discover_traverse_responses (message, xp_ctx, out_discovered_sources,
- e_webdav_discover_process_addressbook_response_propstat);
+typedef struct _EWebDAVDiscoverContext {
+ ESource *source;
+ gchar *url_use_path;
+ guint32 only_supports;
+ ENamedParameters *credentials;
+ gchar *out_certificate_pem;
+ GTlsCertificateFlags out_certificate_errors;
+ GSList *out_discovered_sources;
+ GSList *out_calendar_user_addresses;
+} EWebDAVDiscoverContext;
- xmlXPathFreeContext (xp_ctx);
- xmlFreeDoc (doc);
+static EWebDAVDiscoverContext *
+e_webdav_discover_context_new (ESource *source,
+ const gchar *url_use_path,
+ guint32 only_supports,
+ const ENamedParameters *credentials)
+{
+ EWebDAVDiscoverContext *context;
- g_clear_object (&message);
+ context = g_new0 (EWebDAVDiscoverContext, 1);
+ context->source = g_object_ref (source);
+ context->url_use_path = g_strdup (url_use_path);
+ context->only_supports = only_supports;
+ context->credentials = e_named_parameters_new_clone (credentials);
+ context->out_certificate_pem = NULL;
+ context->out_certificate_errors = 0;
+ context->out_discovered_sources = NULL;
+ context->out_calendar_user_addresses = NULL;
- return TRUE;
+ return context;
}
-static gboolean
-e_webdav_discover_process_addressbook_home_set (SoupSession *session,
- SoupMessage *message,
- ESource *source,
- gchar **out_certificate_pem,
- GTlsCertificateFlags *out_certificate_errors,
- GSList **out_discovered_sources,
- GCancellable *cancellable,
- GError **error)
+static void
+e_webdav_discover_context_free (gpointer ptr)
{
- SoupURI *soup_uri;
- xmlDocPtr doc;
- xmlXPathContextPtr xp_ctx;
- xmlXPathObjectPtr xp_obj;
- gchar *addressbook_home_set;
- GError *local_error = NULL;
- gboolean success;
-
- g_return_val_if_fail (SOUP_IS_SESSION (session), FALSE);
- g_return_val_if_fail (SOUP_IS_MESSAGE (message), FALSE);
- g_return_val_if_fail (out_discovered_sources != NULL, FALSE);
- g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
-
- if (g_cancellable_is_cancelled (cancellable))
- return FALSE;
-
- doc = e_webdav_discover_parse_xml (message, "multistatus", out_certificate_pem,
out_certificate_errors, &local_error);
- if (!doc) {
- if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_FAILED) ||
- g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
- /* Ignore these errors, but still propagate them. */
- g_propagate_error (error, local_error);
- return TRUE;
- } else if (local_error) {
- g_propagate_error (error, local_error);
- }
-
- return FALSE;
- }
-
- xp_ctx = xmlXPathNewContext (doc);
- xmlXPathRegisterNs (xp_ctx, XC ("D"), XC (NS_WEBDAV));
- xmlXPathRegisterNs (xp_ctx, XC ("C"), XC (NS_CALDAV));
- xmlXPathRegisterNs (xp_ctx, XC ("A"), XC (NS_CARDDAV));
-
- /* Try to find the addressbook home URL using the
- * following properties in order of preference:
- *
- * "A:addressbook-home-set"
- * "D:current-user-principal"
- * "D:principal-URL"
- *
- * If the second or third URL preference is used, rerun
- * the PROPFIND method on that URL at Depth=1 in hopes
- * of getting a proper "A:addressbook-home-set" property.
- */
-
- /* FIXME There can be multiple "D:href" elements for a
- * "A:addressbook-home-set". We're only processing
- * the first one. Need to iterate over them. */
-
- addressbook_home_set = e_webdav_discover_get_xpath_string (
- xp_ctx,
- "/D:multistatus"
- "/D:response"
- "/D:propstat"
- "/D:prop"
- "/A:addressbook-home-set"
- "/D:href");
-
- if (addressbook_home_set != NULL)
- goto get_collection_details;
-
- g_free (addressbook_home_set);
-
- addressbook_home_set = e_webdav_discover_get_xpath_string (
- xp_ctx,
- "/D:multistatus"
- "/D:response"
- "/D:propstat"
- "/D:prop"
- "/D:current-user-principal"
- "/D:href");
-
- if (addressbook_home_set != NULL)
- goto retry_propfind;
-
- g_free (addressbook_home_set);
-
- addressbook_home_set = e_webdav_discover_get_xpath_string (
- xp_ctx,
- "/D:multistatus"
- "/D:response"
- "/D:propstat"
- "/D:prop"
- "/D:principal-URL"
- "/D:href");
-
- if (addressbook_home_set != NULL)
- goto retry_propfind;
-
- g_free (addressbook_home_set);
- addressbook_home_set = NULL;
-
- /* None of the aforementioned properties are present. If the
- * user-supplied CardDAV URL is an addressbook resource, use that. */
-
- xp_obj = e_webdav_discover_get_xpath (
- xp_ctx,
- "/D:multistatus"
- "/D:response"
- "/D:propstat"
- "/D:prop"
- "/D:resourcetype"
- "/A:addressbook");
-
- if (xp_obj != NULL) {
- soup_uri = soup_message_get_uri (message);
-
- if (soup_uri->path != NULL && *soup_uri->path != '\0') {
- gchar *slash;
-
- soup_uri = soup_uri_copy (soup_uri);
-
- slash = strrchr (soup_uri->path, '/');
- while (slash != NULL && slash != soup_uri->path) {
-
- if (slash[1] != '\0') {
- slash[1] = '\0';
- addressbook_home_set =
- g_strdup (soup_uri->path);
- break;
- }
-
- slash[0] = '\0';
- slash = strrchr (soup_uri->path, '/');
- }
-
- soup_uri_free (soup_uri);
- }
-
- xmlXPathFreeObject (xp_obj);
- }
-
- if (addressbook_home_set == NULL || *addressbook_home_set == '\0') {
- g_free (addressbook_home_set);
- xmlXPathFreeContext (xp_ctx);
- xmlFreeDoc (doc);
- return TRUE;
- }
-
- get_collection_details:
-
- xmlXPathFreeContext (xp_ctx);
- xmlFreeDoc (doc);
-
- if (!e_webdav_discover_get_addressbook_collection_details (
- session, message, addressbook_home_set, source,
- out_certificate_pem, out_certificate_errors, out_discovered_sources,
- cancellable, error)) {
- g_free (addressbook_home_set);
- return FALSE;
- }
-
- g_free (addressbook_home_set);
-
- return TRUE;
-
- retry_propfind:
-
- xmlXPathFreeContext (xp_ctx);
- xmlFreeDoc (doc);
-
- soup_uri = soup_uri_copy (soup_message_get_uri (message));
- soup_uri_set_path (soup_uri, addressbook_home_set);
-
- /* Note that we omit "D:resourcetype", "D:current-user-principal"
- * and "D:principal-URL" in order to short-circuit the recursion. */
- message = e_webdav_discover_new_propfind (
- session, soup_uri, DEPTH_1,
- NS_CARDDAV, XC ("addressbook-home-set"),
- NULL);
-
- e_soup_ssl_trust_connect (message, source);
-
- /* This takes ownership of the message. */
- soup_session_send_message (session, message);
-
- soup_uri_free (soup_uri);
-
- g_free (addressbook_home_set);
-
- success = e_webdav_discover_process_addressbook_home_set (session, message, source,
- out_certificate_pem, out_certificate_errors, out_discovered_sources,
- cancellable, error);
+ EWebDAVDiscoverContext *context = ptr;
- g_object_unref (message);
+ if (!context)
+ return;
- return success;
+ g_clear_object (&context->source);
+ g_free (context->url_use_path);
+ e_named_parameters_free (context->credentials);
+ g_free (context->out_certificate_pem);
+ e_webdav_discover_free_discovered_sources (context->out_discovered_sources);
+ g_slist_free_full (context->out_calendar_user_addresses, g_free);
+ g_free (context);
}
static void
@@ -1779,13 +526,6 @@ e_webdav_discover_sources_finish (ESource *source,
return g_task_propagate_boolean (G_TASK (result), error);
}
-static void
-e_webdav_discover_cancelled_cb (GCancellable *cancellable,
- SoupSession *session)
-{
- soup_session_abort (session);
-}
-
/**
* e_webdav_discover_sources_sync:
* @source: an #ESource from which to take connection details
@@ -1843,11 +583,8 @@ e_webdav_discover_sources_sync (ESource *source,
GError **error)
{
ESourceWebdav *webdav_extension;
- AuthenticateData auth_data;
- SoupSession *session;
- SoupMessage *message;
+ EWebDAVSession *webdav;
SoupURI *soup_uri;
- gulong cancelled_handler_id = 0, authenticate_handler_id;
gboolean success;
g_return_val_if_fail (E_IS_SOURCE (source), FALSE);
@@ -1889,166 +626,76 @@ e_webdav_discover_sources_sync (ESource *source,
g_string_free (new_path, TRUE);
}
- session = soup_session_new ();
- g_object_set (
- session,
- SOUP_SESSION_ACCEPT_LANGUAGE_AUTO, TRUE,
- NULL);
-
- message = e_webdav_discover_new_propfind (
- session, soup_uri, DEPTH_0,
- NS_WEBDAV, XC ("resourcetype"),
- NS_WEBDAV, XC ("current-user-principal"),
- NS_WEBDAV, XC ("principal-URL"),
- NS_CALDAV, XC ("calendar-home-set"),
- NS_CALDAV, XC ("calendar-user-address-set"),
- NS_CARDDAV, XC ("addressbook-home-set"),
- NS_CARDDAV, XC ("principal-address"),
- NULL);
-
- if (!message) {
- soup_uri_free (soup_uri);
- g_object_unref (session);
- return FALSE;
- }
+ webdav = e_webdav_session_new (source);
+ e_soup_session_setup_logging (E_SOUP_SESSION (webdav), g_getenv ("WEBDAV_DEBUG"));
+ e_soup_session_set_credentials (E_SOUP_SESSION (webdav), credentials);
- if (g_getenv ("WEBDAV_DEBUG") != NULL) {
- SoupLogger *logger;
+ if (!g_cancellable_set_error_if_cancelled (cancellable, error)) {
+ WebDAVDiscoverData wdd;
+ gchar *uri;
+ GError *local_error = NULL;
- logger = soup_logger_new (SOUP_LOGGER_LOG_BODY, 100 * 1024 * 1024);
- soup_session_add_feature (session, SOUP_SESSION_FEATURE (logger));
- g_object_unref (logger);
- }
+ wdd.covered_hrefs = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+ wdd.addressbooks = NULL;
+ wdd.calendars = NULL;
+ wdd.only_supports = only_supports;
+ wdd.out_calendar_user_addresses = out_calendar_user_addresses;
+ wdd.cancellable = cancellable;
+ wdd.error = &local_error;
- if (e_source_has_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
- SoupSessionFeature *feature;
- ESourceAuthentication *auth_extension;
- gchar *auth_method;
+ uri = soup_uri_to_string (soup_uri, FALSE);
- feature = soup_session_get_feature (session, SOUP_TYPE_AUTH_MANAGER);
+ success = uri && *uri && e_webdav_discover_propfind_uri_sync (webdav, &wdd, uri, FALSE);
- success = TRUE;
+ g_free (uri);
- auth_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION);
- auth_method = e_source_authentication_dup_method (auth_extension);
+ if (success && !wdd.calendars && (only_supports == E_WEBDAV_DISCOVER_SUPPORTS_NONE ||
+ (only_supports & (E_WEBDAV_DISCOVER_SUPPORTS_EVENTS | E_WEBDAV_DISCOVER_SUPPORTS_MEMOS |
E_WEBDAV_DISCOVER_SUPPORTS_TASKS)) != 0) &&
+ (!soup_uri_get_path (soup_uri) || !strstr (soup_uri_get_path (soup_uri),
"/.well-known/"))) {
+ gchar *saved_path;
- if (g_strcmp0 (auth_method, "OAuth2") == 0 || g_strcmp0 (auth_method, "Google") == 0) {
- SoupAuth *soup_auth;
+ saved_path = g_strdup (soup_uri_get_path (soup_uri));
- soup_auth = g_object_new (E_TYPE_SOUP_AUTH_BEARER, SOUP_AUTH_HOST, soup_uri->host,
NULL);
+ soup_uri_set_path (soup_uri, "/.well-known/caldav");
- success = e_webdav_discover_setup_bearer_auth (source, credentials,
- E_SOUP_AUTH_BEARER (soup_auth), cancellable, error);
+ uri = soup_uri_to_string (soup_uri, FALSE);
- if (success) {
- soup_session_feature_add_feature (feature, E_TYPE_SOUP_AUTH_BEARER);
- soup_auth_manager_use_auth (
- SOUP_AUTH_MANAGER (feature),
- soup_uri, soup_auth);
- }
+ /* Ignore errors here */
+ wdd.error = NULL;
+ wdd.only_supports = E_WEBDAV_DISCOVER_SUPPORTS_EVENTS |
E_WEBDAV_DISCOVER_SUPPORTS_MEMOS | E_WEBDAV_DISCOVER_SUPPORTS_TASKS;
- g_object_unref (soup_auth);
- }
+ success = uri && *uri && e_webdav_discover_propfind_uri_sync (webdav, &wdd, uri,
FALSE);
- g_free (auth_method);
+ g_free (uri);
- if (!success) {
- soup_uri_free (soup_uri);
- g_object_unref (message);
- g_object_unref (session);
- return FALSE;
+ soup_uri_set_path (soup_uri, saved_path);
+ g_free (saved_path);
}
- }
-
- auth_data.source = source;
- auth_data.credentials = credentials;
-
- authenticate_handler_id = g_signal_connect (session, "authenticate",
- G_CALLBACK (e_webdav_discover_authenticate_cb), &auth_data);
-
- if (cancellable)
- cancelled_handler_id = g_cancellable_connect (cancellable, G_CALLBACK
(e_webdav_discover_cancelled_cb), session, NULL);
- if (!g_cancellable_set_error_if_cancelled (cancellable, error)) {
- GSList *calendars = NULL, *addressbooks = NULL;
- GError *local_error = NULL;
-
- e_soup_ssl_trust_connect (message, source);
- soup_session_send_message (session, message);
-
- success = TRUE;
-
- if (only_supports == E_WEBDAV_DISCOVER_SUPPORTS_NONE ||
- (only_supports & (E_WEBDAV_DISCOVER_SUPPORTS_EVENTS | E_WEBDAV_DISCOVER_SUPPORTS_MEMOS |
E_WEBDAV_DISCOVER_SUPPORTS_TASKS)) != 0) {
- success = e_webdav_discover_process_calendar_home_set (session, message, source,
out_certificate_pem,
- out_certificate_errors, &calendars, out_calendar_user_addresses, cancellable,
&local_error);
-
- if (!calendars && !g_cancellable_is_cancelled (cancellable) && (!soup_uri_get_path
(soup_uri) ||
- !strstr (soup_uri_get_path (soup_uri), "/.well-known/"))) {
- SoupMessage *well_known_message;
- gchar *saved_path;
-
- saved_path = g_strdup (soup_uri_get_path (soup_uri));
+ if (success && !wdd.addressbooks && (only_supports == E_WEBDAV_DISCOVER_SUPPORTS_NONE ||
+ (only_supports & (E_WEBDAV_DISCOVER_SUPPORTS_CONTACTS)) != 0) &&
+ (!soup_uri_get_path (soup_uri) || !strstr (soup_uri_get_path (soup_uri),
"/.well-known/"))) {
+ gchar *saved_path;
- soup_uri_set_path (soup_uri, "/.well-known/caldav");
+ saved_path = g_strdup (soup_uri_get_path (soup_uri));
- well_known_message = e_webdav_discover_new_propfind (
- session, soup_uri, DEPTH_0,
- NS_WEBDAV, XC ("resourcetype"),
- NS_WEBDAV, XC ("current-user-principal"),
- NS_WEBDAV, XC ("principal-URL"),
- NS_CALDAV, XC ("calendar-home-set"),
- NS_CALDAV, XC ("calendar-user-address-set"),
- NULL);
+ soup_uri_set_path (soup_uri, "/.well-known/carddav");
- soup_uri_set_path (soup_uri, saved_path);
- g_free (saved_path);
+ uri = soup_uri_to_string (soup_uri, FALSE);
- if (well_known_message) {
- e_soup_ssl_trust_connect (well_known_message, source);
- soup_session_send_message (session, well_known_message);
+ /* Ignore errors here */
+ wdd.error = NULL;
+ wdd.only_supports = E_WEBDAV_DISCOVER_SUPPORTS_CONTACTS;
- /* Ignore errors here */
- e_webdav_discover_process_calendar_home_set (session,
well_known_message, source, out_certificate_pem,
- out_certificate_errors, &calendars,
out_calendar_user_addresses, cancellable, NULL);
+ success = uri && *uri && e_webdav_discover_propfind_uri_sync (webdav, &wdd, uri,
FALSE);
- g_clear_object (&well_known_message);
- }
- }
- }
+ g_free (uri);
- if (success && (only_supports == E_WEBDAV_DISCOVER_SUPPORTS_NONE ||
- (only_supports & (E_WEBDAV_DISCOVER_SUPPORTS_CONTACTS)) != 0)) {
- success = e_webdav_discover_process_addressbook_home_set (session, message, source,
out_certificate_pem,
- out_certificate_errors, &addressbooks, cancellable, local_error ? NULL :
&local_error);
-
- if (!addressbooks && !g_cancellable_is_cancelled (cancellable) && (!soup_uri_get_path
(soup_uri) ||
- !strstr (soup_uri_get_path (soup_uri), "/.well-known/"))) {
- g_clear_object (&message);
-
- soup_uri_set_path (soup_uri, "/.well-known/carddav");
-
- message = e_webdav_discover_new_propfind (
- session, soup_uri, DEPTH_0,
- NS_WEBDAV, XC ("resourcetype"),
- NS_WEBDAV, XC ("current-user-principal"),
- NS_WEBDAV, XC ("principal-URL"),
- NS_CARDDAV, XC ("addressbook-home-set"),
- NS_CARDDAV, XC ("principal-address"),
- NULL);
-
- if (message) {
- e_soup_ssl_trust_connect (message, source);
- soup_session_send_message (session, message);
-
- /* Ignore errors here */
- e_webdav_discover_process_addressbook_home_set (session, message,
source, out_certificate_pem,
- out_certificate_errors, &addressbooks, cancellable, NULL);
- }
- }
+ soup_uri_set_path (soup_uri, saved_path);
+ g_free (saved_path);
}
- if (calendars || addressbooks) {
+ if (wdd.calendars || wdd.addressbooks) {
success = TRUE;
g_clear_error (&local_error);
} else if (local_error) {
@@ -2056,13 +703,13 @@ e_webdav_discover_sources_sync (ESource *source,
}
if (out_discovered_sources) {
- if (calendars)
- *out_discovered_sources = g_slist_concat (*out_discovered_sources, calendars);
- if (addressbooks)
- *out_discovered_sources = g_slist_concat (*out_discovered_sources,
addressbooks);
+ if (wdd.calendars)
+ *out_discovered_sources = g_slist_concat (*out_discovered_sources,
wdd.calendars);
+ if (wdd.addressbooks)
+ *out_discovered_sources = g_slist_concat (*out_discovered_sources,
wdd.addressbooks);
} else {
- e_webdav_discover_free_discovered_sources (calendars);
- e_webdav_discover_free_discovered_sources (addressbooks);
+ e_webdav_discover_free_discovered_sources (wdd.calendars);
+ e_webdav_discover_free_discovered_sources (wdd.addressbooks);
}
if (out_calendar_user_addresses && *out_calendar_user_addresses)
@@ -2070,19 +717,17 @@ e_webdav_discover_sources_sync (ESource *source,
if (out_discovered_sources && *out_discovered_sources)
*out_discovered_sources = g_slist_reverse (*out_discovered_sources);
+
+ g_hash_table_destroy (wdd.covered_hrefs);
} else {
success = FALSE;
}
- if (cancellable && cancelled_handler_id)
- g_cancellable_disconnect (cancellable, cancelled_handler_id);
-
- if (authenticate_handler_id)
- g_signal_handler_disconnect (session, authenticate_handler_id);
+ if (!success)
+ e_soup_session_get_ssl_error_details (E_SOUP_SESSION (webdav), out_certificate_pem,
out_certificate_errors);
soup_uri_free (soup_uri);
- g_clear_object (&message);
- g_object_unref (session);
+ g_object_unref (webdav);
return success;
}
diff --git a/src/libedataserver/e-webdav-session.c b/src/libedataserver/e-webdav-session.c
new file mode 100644
index 0000000..65762ca
--- /dev/null
+++ b/src/libedataserver/e-webdav-session.c
@@ -0,0 +1,4983 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * SECTION: e-webdav-session
+ * @include: libedataserver/libedataserver.h
+ * @short_description: A WebDAV, CalDAV and CardDAV session
+ *
+ * The #EWebDAVSession is a class to work with WebDAV (RFC 4918),
+ * CalDAV (RFC 4791) or CardDAV (RFC 6352) servers, providing API
+ * for common requests/responses, on top of an #ESoupSession. It
+ * supports also Access Control Protocol (RFC 3744).
+ **/
+
+#include "evolution-data-server-config.h"
+
+#include <stdio.h>
+#include <glib/gi18n-lib.h>
+
+#include "camel/camel.h"
+
+#include "e-source-authentication.h"
+#include "e-source-webdav.h"
+#include "e-xml-utils.h"
+
+#include "e-webdav-session.h"
+
+#define BUFFER_SIZE 16384
+
+struct _EWebDAVSessionPrivate {
+ gboolean dummy;
+};
+
+G_DEFINE_TYPE (EWebDAVSession, e_webdav_session, E_TYPE_SOUP_SESSION)
+
+G_DEFINE_BOXED_TYPE (EWebDAVResource, e_webdav_resource, e_webdav_resource_copy, e_webdav_resource_free)
+G_DEFINE_BOXED_TYPE (EWebDAVPropertyChange, e_webdav_property_change, e_webdav_property_change_copy,
e_webdav_property_change_free)
+G_DEFINE_BOXED_TYPE (EWebDAVPrivilege, e_webdav_privilege, e_webdav_privilege_copy, e_webdav_privilege_free)
+G_DEFINE_BOXED_TYPE (EWebDAVAccessControlEntry, e_webdav_access_control_entry,
e_webdav_access_control_entry_copy, e_webdav_access_control_entry_free)
+
+/**
+ * e_webdav_resource_new:
+ * @kind: an #EWebDAVResourceKind of the resource
+ * @supports: bit-or of #EWebDAVResourceSupports values
+ * @href: href of the resource
+ * @etag: (nullable): optional ETag of the resource, or %NULL
+ * @display_name: (nullable): optional display name of the resource, or %NULL
+ * @description: (nullable): optional description of the resource, or %NULL
+ * @color: (nullable): optional color of the resource, or %NULL
+ *
+ * Some values of the resource are not always valid, depending on the @kind,
+ * but also whether server stores such values and whether it had been asked
+ * for them to be fetched.
+ *
+ * The @etag for %E_WEBDAV_RESOURCE_KIND_COLLECTION can be a change tag instead.
+ *
+ * Returns: (transfer full): A newly created #EWebDAVResource, prefilled with
+ * given values. Free it with e_webdav_resource_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EWebDAVResource *
+e_webdav_resource_new (EWebDAVResourceKind kind,
+ guint32 supports,
+ const gchar *href,
+ const gchar *etag,
+ const gchar *display_name,
+ const gchar *content_type,
+ gsize content_length,
+ glong creation_date,
+ glong last_modified,
+ const gchar *description,
+ const gchar *color)
+{
+ EWebDAVResource *resource;
+
+ resource = g_new0 (EWebDAVResource, 1);
+ resource->kind = kind;
+ resource->supports = supports;
+ resource->href = g_strdup (href);
+ resource->etag = g_strdup (etag);
+ resource->display_name = g_strdup (display_name);
+ resource->content_type = g_strdup (content_type);
+ resource->content_length = content_length;
+ resource->creation_date = creation_date;
+ resource->last_modified = last_modified;
+ resource->description = g_strdup (description);
+ resource->color = g_strdup (color);
+
+ return resource;
+}
+
+/**
+ * e_webdav_resource_copy:
+ * @src: (nullable): an #EWebDAVResource to make a copy of
+ *
+ * Returns: (transfer full): A new #EWebDAVResource prefilled with
+ * the same values as @src, or %NULL, when @src is %NULL.
+ * Free it with e_webdav_resource_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EWebDAVResource *
+e_webdav_resource_copy (const EWebDAVResource *src)
+{
+ if (!src)
+ return NULL;
+
+ return e_webdav_resource_new (src->kind,
+ src->supports,
+ src->href,
+ src->etag,
+ src->display_name,
+ src->content_type,
+ src->content_length,
+ src->creation_date,
+ src->last_modified,
+ src->description,
+ src->color);
+}
+
+/**
+ * e_webdav_resource_free:
+ * @ptr: (nullable): an #EWebDAVResource
+ *
+ * Frees an #EWebDAVResource previously created with e_webdav_resource_new()
+ * or e_webdav_resource_copy(). The function does nothing, if @ptr is %NULL.
+ *
+ * Since: 3.26
+ **/
+void
+e_webdav_resource_free (gpointer ptr)
+{
+ EWebDAVResource *resource = ptr;
+
+ if (resource) {
+ g_free (resource->href);
+ g_free (resource->etag);
+ g_free (resource->display_name);
+ g_free (resource->content_type);
+ g_free (resource->description);
+ g_free (resource->color);
+ g_free (resource);
+ }
+}
+
+static EWebDAVPropertyChange *
+e_webdav_property_change_new (EWebDAVPropertyChangeKind kind,
+ const gchar *ns_uri,
+ const gchar *name,
+ const gchar *value)
+{
+ EWebDAVPropertyChange *change;
+
+ change = g_new0 (EWebDAVPropertyChange, 1);
+ change->kind = kind;
+ change->ns_uri = g_strdup (ns_uri);
+ change->name = g_strdup (name);
+ change->value = g_strdup (value);
+
+ return change;
+}
+
+/**
+ * e_webdav_property_change_new_set:
+ * @ns_uri: namespace URI of the property
+ * @name: name of the property
+ * @value: (nullable): value of the property, or %NULL for empty value
+ *
+ * Creates a new #EWebDAVPropertyChange of kind %E_WEBDAV_PROPERTY_SET,
+ * which is used to modify or set the property value. The @value is a string
+ * representation of the value to store. It can be %NULL, but it means
+ * an empty value, not to remove it. To remove property use
+ * e_webdav_property_change_new_remove() instead.
+ *
+ * Returns: (transfer full): A new #EWebDAVPropertyChange. Free it with
+ * e_webdav_property_change_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EWebDAVPropertyChange *
+e_webdav_property_change_new_set (const gchar *ns_uri,
+ const gchar *name,
+ const gchar *value)
+{
+ g_return_val_if_fail (ns_uri != NULL, NULL);
+ g_return_val_if_fail (name != NULL, NULL);
+
+ return e_webdav_property_change_new (E_WEBDAV_PROPERTY_SET, ns_uri, name, value);
+}
+
+/**
+ * e_webdav_property_change_new_remove:
+ * @ns_uri: namespace URI of the property
+ * @name: name of the property
+ *
+ * Creates a new #EWebDAVPropertyChange of kind %E_WEBDAV_PROPERTY_REMOVE,
+ * which is used to remove the given property. To change property value
+ * use e_webdav_property_change_new_set() instead.
+ *
+ * Returns: (transfer full): A new #EWebDAVPropertyChange. Free it with
+ * e_webdav_property_change_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EWebDAVPropertyChange *
+e_webdav_property_change_new_remove (const gchar *ns_uri,
+ const gchar *name)
+{
+ g_return_val_if_fail (ns_uri != NULL, NULL);
+ g_return_val_if_fail (name != NULL, NULL);
+
+ return e_webdav_property_change_new (E_WEBDAV_PROPERTY_REMOVE, ns_uri, name, NULL);
+}
+
+/**
+ * e_webdav_property_change_copy:
+ * @src: (nullable): an #EWebDAVPropertyChange to make a copy of
+ *
+ * Returns: (transfer full): A new #EWebDAVPropertyChange prefilled with
+ * the same values as @src, or %NULL, when @src is %NULL.
+ * Free it with e_webdav_property_change_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EWebDAVPropertyChange *
+e_webdav_property_change_copy (const EWebDAVPropertyChange *src)
+{
+ if (!src)
+ return NULL;
+
+ return e_webdav_property_change_new (
+ src->kind,
+ src->ns_uri,
+ src->name,
+ src->value);
+}
+
+/**
+ * e_webdav_property_change_free:
+ * @ptr: (nullable): an #EWebDAVPropertyChange
+ *
+ * Frees an #EWebDAVPropertyChange previously created with e_webdav_property_change_new_set(),
+ * e_webdav_property_change_new_remove() or or e_webdav_property_change_copy().
+ * The function does nothing, if @ptr is %NULL.
+ *
+ * Since: 3.26
+ **/
+void
+e_webdav_property_change_free (gpointer ptr)
+{
+ EWebDAVPropertyChange *change = ptr;
+
+ if (change) {
+ g_free (change->ns_uri);
+ g_free (change->name);
+ g_free (change->value);
+ g_free (change);
+ }
+}
+
+/**
+ * e_webdav_privilege_new:
+ * @ns_uri: (nullable): a namespace URI
+ * @name: (nullable): element name
+ * @description: (nullable): human read-able description, or %NULL
+ * @kind: an #EWebDAVPrivilegeKind
+ * @hint: an #EWebDAVPrivilegeHint
+ *
+ * Describes one privilege entry. The @hint can be %E_WEBDAV_PRIVILEGE_HINT_UNKNOWN
+ * for privileges which are not known to the #EWebDAVSession. It's possible, because
+ * the servers can define their own privileges. The hint is also tried to pair with
+ * known hnts when it's %E_WEBDAV_PRIVILEGE_HINT_UNKNOWN.
+ *
+ * The @ns_uri and @name can be %NULL only if the @hint is one of the known
+ * privileges. Otherwise it's an error to pass either of the two as %NULL.
+ *
+ * Returns: (transfer full): A newly created #EWebDAVPrivilege, prefilled with
+ * given values. Free it with e_webdav_privilege_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EWebDAVPrivilege *
+e_webdav_privilege_new (const gchar *ns_uri,
+ const gchar *name,
+ const gchar *description,
+ EWebDAVPrivilegeKind kind,
+ EWebDAVPrivilegeHint hint)
+{
+ EWebDAVPrivilege *privilege;
+
+ if ((!ns_uri || !name) && hint != E_WEBDAV_PRIVILEGE_HINT_UNKNOWN) {
+ const gchar *use_ns_uri = NULL, *use_name = NULL;
+
+ switch (hint) {
+ case E_WEBDAV_PRIVILEGE_HINT_UNKNOWN:
+ break;
+ case E_WEBDAV_PRIVILEGE_HINT_READ:
+ use_name = "read";
+ break;
+ case E_WEBDAV_PRIVILEGE_HINT_WRITE:
+ use_name = "write";
+ break;
+ case E_WEBDAV_PRIVILEGE_HINT_WRITE_PROPERTIES:
+ use_name = "write-properties";
+ break;
+ case E_WEBDAV_PRIVILEGE_HINT_WRITE_CONTENT:
+ use_name = "write-content";
+ break;
+ case E_WEBDAV_PRIVILEGE_HINT_UNLOCK:
+ use_name = "unlock";
+ break;
+ case E_WEBDAV_PRIVILEGE_HINT_READ_ACL:
+ use_name = "read-acl";
+ break;
+ case E_WEBDAV_PRIVILEGE_HINT_WRITE_ACL:
+ use_name = "write-acl";
+ break;
+ case E_WEBDAV_PRIVILEGE_HINT_READ_CURRENT_USER_PRIVILEGE_SET:
+ use_name = "read-current-user-privilege-set";
+ break;
+ case E_WEBDAV_PRIVILEGE_HINT_BIND:
+ use_name = "bind";
+ break;
+ case E_WEBDAV_PRIVILEGE_HINT_UNBIND:
+ use_name = "unbind";
+ break;
+ case E_WEBDAV_PRIVILEGE_HINT_ALL:
+ use_name = "all";
+ break;
+ case E_WEBDAV_PRIVILEGE_HINT_CALDAV_READ_FREE_BUSY:
+ use_ns_uri = E_WEBDAV_NS_CALDAV;
+ use_name = "read-free-busy";
+ break;
+ }
+
+ if (use_name) {
+ ns_uri = use_ns_uri ? use_ns_uri : E_WEBDAV_NS_DAV;
+ name = use_name;
+ }
+ }
+
+ g_return_val_if_fail (ns_uri != NULL, NULL);
+ g_return_val_if_fail (name != NULL, NULL);
+
+ if (hint == E_WEBDAV_PRIVILEGE_HINT_UNKNOWN) {
+ if (g_str_equal (ns_uri, E_WEBDAV_NS_DAV)) {
+ if (g_str_equal (name, "read"))
+ hint = E_WEBDAV_PRIVILEGE_HINT_READ;
+ else if (g_str_equal (name, "write"))
+ hint = E_WEBDAV_PRIVILEGE_HINT_WRITE;
+ else if (g_str_equal (name, "write-properties"))
+ hint = E_WEBDAV_PRIVILEGE_HINT_WRITE_PROPERTIES;
+ else if (g_str_equal (name, "write-content"))
+ hint = E_WEBDAV_PRIVILEGE_HINT_WRITE_CONTENT;
+ else if (g_str_equal (name, "unlock"))
+ hint = E_WEBDAV_PRIVILEGE_HINT_UNLOCK;
+ else if (g_str_equal (name, "read-acl"))
+ hint = E_WEBDAV_PRIVILEGE_HINT_READ_ACL;
+ else if (g_str_equal (name, "write-acl"))
+ hint = E_WEBDAV_PRIVILEGE_HINT_WRITE_ACL;
+ else if (g_str_equal (name, "read-current-user-privilege-set"))
+ hint = E_WEBDAV_PRIVILEGE_HINT_READ_CURRENT_USER_PRIVILEGE_SET;
+ else if (g_str_equal (name, "bind"))
+ hint = E_WEBDAV_PRIVILEGE_HINT_BIND;
+ else if (g_str_equal (name, "unbind"))
+ hint = E_WEBDAV_PRIVILEGE_HINT_UNBIND;
+ else if (g_str_equal (name, "all"))
+ hint = E_WEBDAV_PRIVILEGE_HINT_ALL;
+ } else if (g_str_equal (ns_uri, E_WEBDAV_NS_CALDAV)) {
+ if (g_str_equal (name, "read-free-busy"))
+ hint = E_WEBDAV_PRIVILEGE_HINT_CALDAV_READ_FREE_BUSY;
+ }
+ }
+
+ privilege = g_new (EWebDAVPrivilege, 1);
+ privilege->ns_uri = g_strdup (ns_uri);
+ privilege->name = g_strdup (name);
+ privilege->description = g_strdup (description);
+ privilege->kind = kind;
+ privilege->hint = hint;
+
+ return privilege;
+}
+
+/**
+ * e_webdav_privilege_copy:
+ * @src: (nullable): an #EWebDAVPrivilege to make a copy of
+ *
+ * Returns: (transfer full): A new #EWebDAVPrivilege prefilled with
+ * the same values as @src, or %NULL, when @src is %NULL.
+ * Free it with e_webdav_privilege_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EWebDAVPrivilege *
+e_webdav_privilege_copy (const EWebDAVPrivilege *src)
+{
+ if (!src)
+ return NULL;
+
+ return e_webdav_privilege_new (
+ src->ns_uri,
+ src->name,
+ src->description,
+ src->kind,
+ src->hint);
+}
+
+/**
+ * e_webdav_privilege_free:
+ * @ptr: (nullable): an #EWebDAVPrivilege
+ *
+ * Frees an #EWebDAVPrivilege previously created with e_webdav_privilege_new()
+ * or e_webdav_privilege_copy(). The function does nothing, if @ptr is %NULL.
+ *
+ * Since: 3.26
+ **/
+void
+e_webdav_privilege_free (gpointer ptr)
+{
+ EWebDAVPrivilege *privilege = ptr;
+
+ if (privilege) {
+ g_free (privilege->ns_uri);
+ g_free (privilege->name);
+ g_free (privilege->description);
+ g_free (privilege);
+ }
+}
+
+/**
+ * e_webdav_access_control_entry_new:
+ * @principal_kind: an #EWebDAVACEPrincipalKind
+ * @principal_href: (nullable): principal href; should be set only if @principal_kind is
@E_WEBDAV_ACE_PRINCIPAL_HREF
+ * @flags: bit-or of #EWebDAVACEFlag values
+ * @inherited_href: (nullable): href of the resource from which inherits; should be set only if @flags
contain E_WEBDAV_ACE_FLAG_INHERITED
+ *
+ * Describes one Access Control Entry (ACE).
+ *
+ * The @flags should always contain either %E_WEBDAV_ACE_FLAG_GRANT or
+ * %E_WEBDAV_ACE_FLAG_DENY value.
+ *
+ * Use e_webdav_access_control_entry_append_privilege() to add respective
+ * privileges to the entry.
+ *
+ * Returns: (transfer full): A newly created #EWebDAVAccessControlEntry, prefilled with
+ * given values. Free it with e_webdav_access_control_entry_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EWebDAVAccessControlEntry *
+e_webdav_access_control_entry_new (EWebDAVACEPrincipalKind principal_kind,
+ const gchar *principal_href,
+ guint32 flags,
+ const gchar *inherited_href)
+{
+ EWebDAVAccessControlEntry *ace;
+
+ if (principal_kind == E_WEBDAV_ACE_PRINCIPAL_HREF)
+ g_return_val_if_fail (principal_href != NULL, NULL);
+ else
+ g_return_val_if_fail (principal_href == NULL, NULL);
+
+ if ((flags & E_WEBDAV_ACE_FLAG_INHERITED) != 0)
+ g_return_val_if_fail (inherited_href != NULL, NULL);
+ else
+ g_return_val_if_fail (inherited_href == NULL, NULL);
+
+ ace = g_new0 (EWebDAVAccessControlEntry, 1);
+ ace->principal_kind = principal_kind;
+ ace->principal_href = g_strdup (principal_href);
+ ace->flags = flags;
+ ace->inherited_href = g_strdup (inherited_href);
+ ace->privileges = NULL;
+
+ return ace;
+}
+
+/**
+ * e_webdav_access_control_entry_copy:
+ * @src: (nullable): an #EWebDAVAccessControlEntry to make a copy of
+ *
+ * Returns: (transfer full): A new #EWebDAVAccessControlEntry prefilled with
+ * the same values as @src, or %NULL, when @src is %NULL.
+ * Free it with e_webdav_access_control_entry_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EWebDAVAccessControlEntry *
+e_webdav_access_control_entry_copy (const EWebDAVAccessControlEntry *src)
+{
+ EWebDAVAccessControlEntry *ace;
+ GSList *link;
+
+ if (!src)
+ return NULL;
+
+ ace = e_webdav_access_control_entry_new (
+ src->principal_kind,
+ src->principal_href,
+ src->flags,
+ src->inherited_href);
+ if (!ace)
+ return NULL;
+
+ for (link = src->privileges; link; link = g_slist_next (link)) {
+ EWebDAVPrivilege *privilege = link->data;
+
+ if (privilege)
+ ace->privileges = g_slist_prepend (ace->privileges, e_webdav_privilege_copy
(privilege));
+ }
+
+ ace->privileges = g_slist_reverse (ace->privileges);
+
+ return ace;
+}
+
+/**
+ * e_webdav_access_control_entry_free:
+ * @ptr: (nullable): an #EWebDAVAccessControlEntry
+ *
+ * Frees an #EWebDAVAccessControlEntry previously created with e_webdav_access_control_entry_new()
+ * or e_webdav_access_control_entry_copy(). The function does nothing, if @ptr is %NULL.
+ *
+ * Since: 3.26
+ **/
+void
+e_webdav_access_control_entry_free (gpointer ptr)
+{
+ EWebDAVAccessControlEntry *ace = ptr;
+
+ if (ace) {
+ g_free (ace->principal_href);
+ g_free (ace->inherited_href);
+ g_slist_free_full (ace->privileges, e_webdav_privilege_free);
+ g_free (ace);
+ }
+}
+
+/**
+ * e_webdav_access_control_entry_append_privilege:
+ * @ace: an #EWebDAVAccessControlEntry
+ * @privilege: (transfer full): an #EWebDAVPrivilege
+ *
+ * Appends a new @privilege to the list of privileges for the @ace.
+ * The function assumes ownership of the @privilege, which is freed
+ * together with the @ace.
+ *
+ * Since: 3.26
+ **/
+void
+e_webdav_access_control_entry_append_privilege (EWebDAVAccessControlEntry *ace,
+ EWebDAVPrivilege *privilege)
+{
+ g_return_if_fail (ace != NULL);
+ g_return_if_fail (privilege != NULL);
+
+ ace->privileges = g_slist_append (ace->privileges, privilege);
+}
+
+/**
+ * e_webdav_access_control_entry_get_privileges:
+ * @ace: an #EWebDAVAccessControlEntry
+ *
+ * Returns: (element-type EWebDAVPrivilege) (transfer none): A #GSList of #EWebDAVPrivilege
+ * with the list of privileges for the @ace. The reurned #GSList, together with its data
+ * is owned by the @ace.
+ *
+ * Since: 3.26
+ **/
+GSList *
+e_webdav_access_control_entry_get_privileges (EWebDAVAccessControlEntry *ace)
+{
+ g_return_val_if_fail (ace != NULL, NULL);
+
+ return ace->privileges;
+}
+
+static void
+e_webdav_session_class_init (EWebDAVSessionClass *klass)
+{
+ g_type_class_add_private (klass, sizeof (EWebDAVSessionPrivate));
+}
+
+static void
+e_webdav_session_init (EWebDAVSession *webdav)
+{
+ webdav->priv = G_TYPE_INSTANCE_GET_PRIVATE (webdav, E_TYPE_WEBDAV_SESSION, EWebDAVSessionPrivate);
+}
+
+/**
+ * e_webdav_session_new:
+ * @source: an #ESource
+ *
+ * Creates a new #EWebDAVSession associated with given @source. It's
+ * a user's error to try to create the #EWebDAVSession for a source
+ * which doesn't have #ESourceWebdav extension properly defined.
+ *
+ * Returns: (transfer full): a new #EWebDAVSession; free it with g_object_unref(),
+ * when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EWebDAVSession *
+e_webdav_session_new (ESource *source)
+{
+ g_return_val_if_fail (E_IS_SOURCE (source), NULL);
+ g_return_val_if_fail (e_source_has_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND), NULL);
+
+ return g_object_new (E_TYPE_WEBDAV_SESSION,
+ "source", source,
+ NULL);
+}
+
+/**
+ * e_webdav_session_new_request:
+ * @webdav: an #EWebDAVSession
+ * @method: an HTTP method
+ * @uri: (nullable): URI to create the request for, or %NULL to read from #ESource
+ * @error: return location for a #GError, or %NULL
+ *
+ * Returns: (transfer full): A new #SoupRequestHTTP for the given @uri, or, when %NULL,
+ * for the URI stored in the associated #ESource. Free the returned structure
+ * with g_object_unref(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+SoupRequestHTTP *
+e_webdav_session_new_request (EWebDAVSession *webdav,
+ const gchar *method,
+ const gchar *uri,
+ GError **error)
+{
+ ESoupSession *session;
+ SoupRequestHTTP *request;
+ SoupURI *soup_uri;
+ ESource *source;
+ ESourceWebdav *webdav_extension;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), NULL);
+
+ session = E_SOUP_SESSION (webdav);
+ if (uri && *uri)
+ return e_soup_session_new_request (session, method, uri, error);
+
+ source = e_soup_session_get_source (session);
+ g_return_val_if_fail (E_IS_SOURCE (source), NULL);
+
+ webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+ soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
+
+ g_return_val_if_fail (soup_uri != NULL, NULL);
+
+ request = e_soup_session_new_request_uri (session, method, soup_uri, error);
+
+ soup_uri_free (soup_uri);
+
+ return request;
+}
+
+static gboolean
+e_webdav_session_extract_propstat_error_cb (EWebDAVSession *webdav,
+ xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prop_prefix,
+ const SoupURI *request_uri,
+ const gchar *href,
+ guint status_code,
+ gpointer user_data)
+{
+ GError **error = user_data;
+
+ g_return_val_if_fail (error != NULL, FALSE);
+
+ if (!xpath_prop_prefix)
+ return TRUE;
+
+ if (status_code != SOUP_STATUS_OK && (
+ status_code != SOUP_STATUS_FAILED_DEPENDENCY ||
+ !*error)) {
+ gchar *description;
+
+ description = e_xml_xpath_eval_as_string (xpath_ctx, "%s/../D:responsedescription",
xpath_prop_prefix);
+ if (!description || !*description) {
+ g_free (description);
+
+ description = e_xml_xpath_eval_as_string (xpath_ctx,
"%s/../../D:responsedescription", xpath_prop_prefix);
+ }
+
+ g_clear_error (error);
+ g_set_error_literal (error, SOUP_HTTP_ERROR, status_code,
+ e_soup_session_util_status_to_string (status_code, description));
+
+ g_free (description);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+e_webdav_session_extract_dav_error (EWebDAVSession *webdav,
+ xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prefix,
+ gchar **out_detail_text)
+{
+ xmlXPathObjectPtr xpath_obj;
+ gchar *detail_text;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (xpath_ctx != NULL, FALSE);
+ g_return_val_if_fail (xpath_prefix != NULL, FALSE);
+ g_return_val_if_fail (out_detail_text != NULL, FALSE);
+
+ if (!e_xml_xpath_eval_exists (xpath_ctx, "%s/D:error", xpath_prefix))
+ return FALSE;
+
+ detail_text = e_xml_xpath_eval_as_string (xpath_ctx, "%s/D:error", xpath_prefix);
+
+ xpath_obj = e_xml_xpath_eval (xpath_ctx, "%s/D:error", xpath_prefix);
+ if (xpath_obj) {
+ if (xpath_obj->type == XPATH_NODESET &&
+ xpath_obj->nodesetval &&
+ xpath_obj->nodesetval->nodeNr == 1 &&
+ xpath_obj->nodesetval->nodeTab &&
+ xpath_obj->nodesetval->nodeTab[0] &&
+ xpath_obj->nodesetval->nodeTab[0]->children) {
+ GString *text = g_string_new ("");
+ xmlNodePtr node;
+
+ for (node = xpath_obj->nodesetval->nodeTab[0]->children; node; node = node->next) {
+ if (node->type == XML_ELEMENT_NODE &&
+ node->name && *(node->name))
+ g_string_append_printf (text, "[%s]", (const gchar *) node->name);
+ }
+
+ if (text->len > 0) {
+ if (detail_text) {
+ g_strstrip (detail_text);
+ if (*detail_text)
+ g_string_prepend (text, detail_text);
+ g_free (detail_text);
+ }
+
+ detail_text = g_string_free (text, FALSE);
+ } else {
+ g_string_free (text, TRUE);
+ }
+ }
+
+ xmlXPathFreeObject (xpath_obj);
+ }
+
+ *out_detail_text = detail_text;
+
+ return detail_text != NULL;
+}
+
+/**
+ * e_webdav_session_replace_with_detailed_error:
+ * @webdav: an #EWebDAVSession
+ * @request: a #SoupRequestHTTP
+ * @response_data: (nullable): received response data, or %NULL
+ * @ignore_multistatus: whether to ignore multistatus responses
+ * @prefix: (nullable): error message prefix, used when replacing, or %NULL
+ * @inout_error: (inout) (nullable) (transfer full): a #GError variable to replace content to, or %NULL
+ *
+ * Tries to read detailed error information from @response_data,
+ * if not provided, then from @request's response_body. If the detailed
+ * error cannot be found, then does nothing, otherwise frees the content
+ * of @inout_error, if any, and then populates it with an error message
+ * prefixed with @prefix.
+ *
+ * The @prefix might be of form "Failed to something", because the resulting
+ * error message will be:
+ * "Failed to something: HTTP error code XXX (reason_phrase): detailed_error".
+ * When @prefix is %NULL, the error message will be:
+ * "Failed with HTTP error code XXX (reason phrase): detailed_error".
+ *
+ * As the caller might not be interested in errors, also the @inout_error
+ * can be %NULL, in which case the function does nothing.
+ *
+ * Returns: Whether any detailed error had been recognized.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_replace_with_detailed_error (EWebDAVSession *webdav,
+ SoupRequestHTTP *request,
+ const GByteArray *response_data,
+ gboolean ignore_multistatus,
+ const gchar *prefix,
+ GError **inout_error)
+{
+ SoupMessage *message;
+ GByteArray byte_array = { 0 };
+ const gchar *content_type, *reason_phrase;
+ gchar *detail_text = NULL;
+ gchar *reason_phrase_copy = NULL;
+ gboolean error_set = FALSE;
+ guint status_code;
+ GError *local_error = NULL;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (SOUP_IS_REQUEST_HTTP (request), FALSE);
+
+ message = soup_request_http_get_message (request);
+ if (!message)
+ return FALSE;
+
+ status_code = message->status_code;
+ reason_phrase = message->reason_phrase;
+ byte_array.data = NULL;
+ byte_array.len = 0;
+
+ if (response_data && response_data->len) {
+ byte_array.data = (gpointer) response_data->data;
+ byte_array.len = response_data->len;
+ } else if (message->response_body && message->response_body->length) {
+ byte_array.data = (gpointer) message->response_body->data;
+ byte_array.len = message->response_body->length;
+ }
+
+ if (!byte_array.data || !byte_array.len)
+ goto out;
+
+ if (status_code == SOUP_STATUS_MULTI_STATUS &&
+ !ignore_multistatus &&
+ !e_webdav_session_traverse_multistatus_response (webdav, message, &byte_array,
+ e_webdav_session_extract_propstat_error_cb, &local_error, NULL)) {
+ g_clear_error (&local_error);
+ }
+
+ if (local_error) {
+ if (prefix)
+ g_prefix_error (&local_error, "%s: ", prefix);
+ g_propagate_error (inout_error, local_error);
+
+ g_object_unref (message);
+
+ return TRUE;
+ }
+
+ content_type = soup_message_headers_get_content_type (message->response_headers, NULL);
+ if (content_type && (
+ (g_ascii_strcasecmp (content_type, "application/xml") == 0 ||
+ g_ascii_strcasecmp (content_type, "text/xml") == 0))) {
+ xmlDocPtr doc;
+
+ if (status_code == SOUP_STATUS_MULTI_STATUS && ignore_multistatus)
+ doc = NULL;
+ else
+ doc = e_xml_parse_data (byte_array.data, byte_array.len);
+
+ if (doc) {
+ xmlXPathContextPtr xpath_ctx;
+
+ xpath_ctx = e_xml_new_xpath_context_with_namespaces (doc,
+ "D", E_WEBDAV_NS_DAV,
+ "C", E_WEBDAV_NS_CALDAV,
+ "A", E_WEBDAV_NS_CARDDAV,
+ NULL);
+
+ if (xpath_ctx &&
+ e_webdav_session_extract_dav_error (webdav, xpath_ctx, "", &detail_text)) {
+ /* do nothing, detail_text is set */
+ } else if (xpath_ctx) {
+ const gchar *path_prefix = NULL;
+
+ if (e_xml_xpath_eval_exists (xpath_ctx, "/D:multistatus/D:response/D:status"))
+ path_prefix = "/D:multistatus/D:response";
+ else if (e_xml_xpath_eval_exists (xpath_ctx,
"/C:mkcalendar-response/D:status"))
+ path_prefix = "/C:mkcalendar-response";
+ else if (e_xml_xpath_eval_exists (xpath_ctx, "/D:mkcol-response/D:status"))
+ path_prefix = "/D:mkcol-response";
+
+ if (path_prefix) {
+ guint parsed_status = 0;
+ gchar *status;
+
+ status = e_xml_xpath_eval_as_string (xpath_ctx, "%s/D:status",
path_prefix);
+ if (status && soup_headers_parse_status_line (status, NULL,
&parsed_status, &reason_phrase_copy) &&
+ !SOUP_STATUS_IS_SUCCESSFUL (parsed_status)) {
+ status_code = parsed_status;
+ reason_phrase = reason_phrase_copy;
+ detail_text = e_xml_xpath_eval_as_string (xpath_ctx,
"%s/D:responsedescription", path_prefix);
+
+ if (!detail_text)
+ e_webdav_session_extract_dav_error (webdav,
xpath_ctx, path_prefix, &detail_text);
+ } else {
+ e_webdav_session_extract_dav_error (webdav, xpath_ctx,
path_prefix, &detail_text);
+ }
+
+ g_free (status);
+ }
+ }
+
+ if (xpath_ctx)
+ xmlXPathFreeContext (xpath_ctx);
+ xmlFreeDoc (doc);
+ }
+ } else if (content_type &&
+ g_ascii_strcasecmp (content_type, "text/plain") == 0) {
+ detail_text = g_strndup ((const gchar *) byte_array.data, byte_array.len);
+ } else if (content_type &&
+ g_ascii_strcasecmp (content_type, "text/html") == 0) {
+ SoupURI *soup_uri;
+ gchar *uri = NULL;
+
+ soup_uri = soup_message_get_uri (message);
+ if (soup_uri) {
+ soup_uri = soup_uri_copy (soup_uri);
+ soup_uri_set_password (soup_uri, NULL);
+
+ uri = soup_uri_to_string (soup_uri, FALSE);
+
+ soup_uri_free (soup_uri);
+ }
+
+ if (uri && *uri)
+ detail_text = g_strdup_printf (_("The server responded with an HTML page, which can
mean there's an error on the server or with the client request. The used URI was: %s"), uri);
+ else
+ detail_text = g_strdup_printf (_("The server responded with an HTML page, which can
mean there's an error on the server or with the client request."));
+
+ g_free (uri);
+ }
+
+ out:
+ if (detail_text)
+ g_strstrip (detail_text);
+
+ if (detail_text && *detail_text) {
+ error_set = TRUE;
+
+ g_clear_error (inout_error);
+
+ if (prefix) {
+ g_set_error (inout_error, SOUP_HTTP_ERROR, status_code,
+ /* Translators: The first '%s' is replaced with error prefix, as provided
+ by the caller, which can be in a form: "Failed with something".
+ The '%d' is replaced with actual HTTP status code.
+ The second '%s' is replaced with a reason phrase of the error (user
readable text).
+ The last '%s' is replaced with detailed error text, as returned by the
server. */
+ _("%s: HTTP error code %d (%s): %s"), prefix, status_code,
+ e_soup_session_util_status_to_string (status_code, reason_phrase),
+ detail_text);
+ } else {
+ g_set_error (inout_error, SOUP_HTTP_ERROR, status_code,
+ /* Translators: The '%d' is replaced with actual HTTP status code.
+ The '%s' is replaced with a reason phrase of the error (user readable
text).
+ The last '%s' is replaced with detailed error text, as returned by the
server. */
+ _("Failed with HTTP error code %d (%s): %s"), status_code,
+ e_soup_session_util_status_to_string (status_code, reason_phrase),
+ detail_text);
+ }
+ } else if (status_code && !SOUP_STATUS_IS_SUCCESSFUL (status_code)) {
+ error_set = TRUE;
+
+ g_clear_error (inout_error);
+
+ if (prefix) {
+ g_set_error (inout_error, SOUP_HTTP_ERROR, status_code,
+ /* Translators: The first '%s' is replaced with error prefix, as provided
+ by the caller, which can be in a form: "Failed with something".
+ The '%d' is replaced with actual HTTP status code.
+ The second '%s' is replaced with a reason phrase of the error (user
readable text). */
+ _("%s: HTTP error code %d (%s)"), prefix, status_code,
+ e_soup_session_util_status_to_string (status_code, reason_phrase));
+ } else {
+ g_set_error (inout_error, SOUP_HTTP_ERROR, status_code,
+ /* Translators: The '%d' is replaced with actual HTTP status code.
+ The '%s' is replaced with a reason phrase of the error (user readable
text). */
+ _("Failed with HTTP error code %d (%s)"), status_code,
+ e_soup_session_util_status_to_string (status_code, reason_phrase));
+ }
+ }
+
+ g_object_unref (message);
+ g_free (reason_phrase_copy);
+ g_free (detail_text);
+
+ return error_set;
+}
+
+/**
+ * e_webdav_session_ensure_full_uri:
+ * @webdav: an #EWebDAVSession
+ * @request_uri: (nullable): a #SoupURI to which the @href belongs, or %NULL
+ * @href: a possibly path-only href
+ *
+ * Converts possibly path-only @href into a full URI under the @request_uri.
+ * When the @request_uri is %NULL, the URI defined in associated #ESource is
+ * used instead.
+ *
+ * Free the returned pointer with g_free(), when no longer needed.
+ *
+ * Returns: (transfer full): The @href as a full URI
+ *
+ * Since: 3.24
+ **/
+gchar *
+e_webdav_session_ensure_full_uri (EWebDAVSession *webdav,
+ const SoupURI *request_uri,
+ const gchar *href)
+{
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), NULL);
+ g_return_val_if_fail (href != NULL, NULL);
+
+ if (*href == '/' || !strstr (href, "://")) {
+ SoupURI *soup_uri;
+ gchar *full_uri;
+
+ if (request_uri) {
+ soup_uri = soup_uri_copy ((SoupURI *) request_uri);
+ } else {
+ ESource *source;
+ ESourceWebdav *webdav_extension;
+
+ source = e_soup_session_get_source (E_SOUP_SESSION (webdav));
+ g_return_val_if_fail (E_IS_SOURCE (source), NULL);
+
+ webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+ soup_uri = e_source_webdav_dup_soup_uri (webdav_extension);
+ }
+
+ g_return_val_if_fail (soup_uri != NULL, NULL);
+
+ soup_uri_set_path (soup_uri, href);
+ soup_uri_set_user (soup_uri, NULL);
+ soup_uri_set_password (soup_uri, NULL);
+
+ full_uri = soup_uri_to_string (soup_uri, FALSE);
+
+ soup_uri_free (soup_uri);
+
+ return full_uri;
+ }
+
+ return g_strdup (href);
+}
+
+static GHashTable *
+e_webdav_session_comma_header_to_hashtable (SoupMessageHeaders *headers,
+ const gchar *header_name)
+{
+ GHashTable *soup_params, *result;
+ GHashTableIter iter;
+ const gchar *value;
+ gpointer key;
+
+ g_return_val_if_fail (header_name != NULL, NULL);
+
+ if (!headers)
+ return NULL;
+
+ value = soup_message_headers_get_list (headers, header_name);
+ if (!value)
+ return NULL;
+
+ soup_params = soup_header_parse_param_list (value);
+ if (!soup_params)
+ return NULL;
+
+ result = g_hash_table_new_full (camel_strcase_hash, camel_strcase_equal, g_free, NULL);
+
+ g_hash_table_iter_init (&iter, soup_params);
+ while (g_hash_table_iter_next (&iter, &key, NULL)) {
+ value = key;
+
+ if (value && *value)
+ g_hash_table_insert (result, g_strdup (value), GINT_TO_POINTER (1));
+ }
+
+ soup_header_free_param_list (soup_params);
+
+ return result;
+}
+
+/**
+ * e_webdav_session_options_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @out_capabilities: (out) (transfer full): return location for DAV capabilities
+ * @out_allows: (out) (transfer full): return location for allowed operations
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Issues OPTIONS request on the provided @uri, or, in case it's %NULL, on the URI
+ * defined in associated #ESource.
+ *
+ * The @out_capabilities contains a set of returned capabilities. Some known are
+ * defined as E_WEBDAV_CAPABILITY_CLASS_1, and so on. The 'value' of the #GHashTable
+ * doesn't have any particular meaning and the strings are compared case insensitively.
+ * Free the hash table with g_hash_table_destroy(), when no longer needed. The returned
+ * value can be %NULL on success, it's when the server doesn't provide the information.
+ *
+ * The @out_allows contains a set of allowed methods returned by the server. Some known
+ * are defined as %SOUP_METHOD_OPTIONS, and so on. The 'value' of the #GHashTable
+ * doesn't have any particular meaning and the strings are compared case insensitively.
+ * Free the hash table with g_hash_table_destroy(), when no longer needed. The returned
+ * value can be %NULL on success, it's when the server doesn't provide the information.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_options_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ GHashTable **out_capabilities,
+ GHashTable **out_allows,
+ GCancellable *cancellable,
+ GError **error)
+{
+ SoupRequestHTTP *request;
+ SoupMessage *message;
+ GByteArray *bytes;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (out_capabilities != NULL, FALSE);
+ g_return_val_if_fail (out_allows != NULL, FALSE);
+
+ *out_capabilities = NULL;
+ *out_allows = NULL;
+
+ request = e_webdav_session_new_request (webdav, SOUP_METHOD_OPTIONS, uri, error);
+ if (!request)
+ return FALSE;
+
+ bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable,
error);
+
+ if (!bytes) {
+ g_object_unref (request);
+ return FALSE;
+ }
+
+ message = soup_request_http_get_message (request);
+
+ g_byte_array_free (bytes, TRUE);
+ g_object_unref (request);
+
+ g_return_val_if_fail (message != NULL, FALSE);
+
+ *out_capabilities = e_webdav_session_comma_header_to_hashtable (message->response_headers, "DAV");
+ *out_allows = e_webdav_session_comma_header_to_hashtable (message->response_headers, "Allow");
+
+ g_object_unref (message);
+
+ return TRUE;
+}
+
+/**
+ * e_webdav_session_post_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @data: data to post to the server
+ * @data_length: length of @data, or -1, when @data is NUL-terminated
+ * @out_content_type: (nullable) (transfer full): return location for response Content-Type, or %NULL
+ * @out_content: (nullable) (transfer full): return location for response content, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Issues POST request on the provided @uri, or, in case it's %NULL, on the URI
+ * defined in associated #ESource.
+ *
+ * The optional @out_content_type can be used to get content type of the response.
+ * Free it with g_free(), when no longer needed.
+ *
+ * The optional @out_content can be used to get actual result content. Free it
+ * with g_byte_array_free(), when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_post_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const gchar *data,
+ gsize data_length,
+ gchar **out_content_type,
+ GByteArray **out_content,
+ GCancellable *cancellable,
+ GError **error)
+{
+ SoupRequestHTTP *request;
+ SoupMessage *message;
+ GByteArray *bytes;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (data != NULL, FALSE);
+
+ if (out_content_type)
+ *out_content_type = NULL;
+
+ if (out_content)
+ *out_content = NULL;
+
+ if (data_length == (gsize) -1)
+ data_length = strlen (data);
+
+ request = e_webdav_session_new_request (webdav, SOUP_METHOD_POST, uri, error);
+ if (!request)
+ return FALSE;
+
+ message = soup_request_http_get_message (request);
+ if (!message) {
+ g_warn_if_fail (message != NULL);
+ g_object_unref (request);
+
+ return FALSE;
+ }
+
+ soup_message_set_request (message, E_WEBDAV_CONTENT_TYPE_XML,
+ SOUP_MEMORY_COPY, data, data_length);
+
+ bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable,
error);
+
+ success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, TRUE, _("Failed to
post data"), error) &&
+ bytes != NULL;
+
+ if (success) {
+ if (out_content_type) {
+ *out_content_type = g_strdup (soup_message_headers_get_content_type
(message->response_headers, NULL));
+ }
+
+ if (out_content) {
+ *out_content = bytes;
+ bytes = NULL;
+ }
+ }
+
+ if (bytes)
+ g_byte_array_free (bytes, TRUE);
+ g_object_unref (message);
+ g_object_unref (request);
+
+ return success;
+}
+
+/**
+ * e_webdav_session_propfind_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @depth: requested depth, can be one of %E_WEBDAV_DEPTH_THIS, %E_WEBDAV_DEPTH_THIS_AND_CHILDREN or
%E_WEBDAV_DEPTH_INFINITY
+ * @xml: (nullable): the request itself, as an #EXmlDocument, the root element should be DAV:propfind, or
%NULL
+ * @func: an #EWebDAVPropstatTraverseFunc function to call for each DAV:propstat in the multistatus response
+ * @func_user_data: user data passed to @func
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Issues PROPFIND request on the provided @uri, or, in case it's %NULL, on the URI
+ * defined in associated #ESource. On success, calls @func for each returned
+ * DAV:propstat. The provided XPath context has registered %E_WEBDAV_NS_DAV namespace
+ * with prefix "D". It doesn't have any other namespace registered.
+ *
+ * The @func is called always at least once, with %NULL xpath_prop_prefix, which
+ * is meant to let the caller setup the xpath_ctx, like to register its own namespaces
+ * to it with e_xml_xpath_context_register_namespaces(). All other invocations of @func
+ * will have xpath_prop_prefix non-%NULL.
+ *
+ * The @xml can be %NULL, in which case the server should behave like DAV:allprop request.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_propfind_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const gchar *depth,
+ const EXmlDocument *xml,
+ EWebDAVPropstatTraverseFunc func,
+ gpointer func_user_data,
+ GCancellable *cancellable,
+ GError **error)
+{
+ SoupRequestHTTP *request;
+ SoupMessage *message;
+ GByteArray *bytes;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (depth != NULL, FALSE);
+ if (xml)
+ g_return_val_if_fail (E_IS_XML_DOCUMENT (xml), FALSE);
+ g_return_val_if_fail (func != NULL, FALSE);
+
+ request = e_webdav_session_new_request (webdav, SOUP_METHOD_PROPFIND, uri, error);
+ if (!request)
+ return FALSE;
+
+ message = soup_request_http_get_message (request);
+ if (!message) {
+ g_warn_if_fail (message != NULL);
+ g_object_unref (request);
+
+ return FALSE;
+ }
+
+ soup_message_headers_replace (message->request_headers, "Depth", depth);
+
+ if (xml) {
+ gchar *content;
+ gsize content_length;
+
+ content = e_xml_document_get_content (xml, &content_length);
+ if (!content) {
+ g_object_unref (message);
+ g_object_unref (request);
+
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, _("Failed to get
input XML content"));
+
+ return FALSE;
+ }
+
+ soup_message_set_request (message, E_WEBDAV_CONTENT_TYPE_XML,
+ SOUP_MEMORY_TAKE, content, content_length);
+ }
+
+ bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable,
error);
+
+ success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, TRUE, _("Failed to
get properties"), error) &&
+ bytes != NULL;
+
+ if (success)
+ success = e_webdav_session_traverse_multistatus_response (webdav, message, bytes, func,
func_user_data, error);
+
+ if (bytes)
+ g_byte_array_free (bytes, TRUE);
+ g_object_unref (message);
+ g_object_unref (request);
+
+ return success;
+}
+
+/**
+ * e_webdav_session_proppatch_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @xml: an #EXmlDocument with request changes, its root element should be DAV:propertyupdate
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Issues PROPPATCH request on the provided @uri, or, in case it's %NULL, on the URI
+ * defined in associated #ESource, with the @changes. The order of requested changes
+ * inside @xml is significant, unlike on other places.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_proppatch_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const EXmlDocument *xml,
+ GCancellable *cancellable,
+ GError **error)
+{
+ SoupRequestHTTP *request;
+ SoupMessage *message;
+ GByteArray *bytes;
+ gchar *content;
+ gsize content_length;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (E_IS_XML_DOCUMENT (xml), FALSE);
+
+ request = e_webdav_session_new_request (webdav, SOUP_METHOD_PROPPATCH, uri, error);
+ if (!request)
+ return FALSE;
+
+ message = soup_request_http_get_message (request);
+ if (!message) {
+ g_warn_if_fail (message != NULL);
+ g_object_unref (request);
+
+ return FALSE;
+ }
+
+ content = e_xml_document_get_content (xml, &content_length);
+ if (!content) {
+ g_object_unref (message);
+ g_object_unref (request);
+
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, _("Failed to get input
XML content"));
+
+ return FALSE;
+ }
+
+ soup_message_set_request (message, E_WEBDAV_CONTENT_TYPE_XML,
+ SOUP_MEMORY_TAKE, content, content_length);
+
+ bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable,
error);
+
+ success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, FALSE, _("Failed to
update properties"), error) &&
+ bytes != NULL;
+
+ if (bytes)
+ g_byte_array_free (bytes, TRUE);
+ g_object_unref (message);
+ g_object_unref (request);
+
+ return success;
+}
+
+/**
+ * e_webdav_session_report_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @depth: (nullable): requested depth, can be %NULL, then no Depth header is sent
+ * @xml: the request itself, as an #EXmlDocument
+ * @func: (nullable): an #EWebDAVPropstatTraverseFunc function to call for each DAV:propstat in the
multistatus response, or %NULL
+ * @func_user_data: user data passed to @func
+ * @out_content_type: (nullable) (transfer full): return location for response Content-Type, or %NULL
+ * @out_content: (nullable) (transfer full): return location for response content, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Issues REPORT request on the provided @uri, or, in case it's %NULL, on the URI
+ * defined in associated #ESource. On success, calls @func for each returned
+ * DAV:propstat. The provided XPath context has registered %E_WEBDAV_NS_DAV namespace
+ * with prefix "D". It doesn't have any other namespace registered.
+ *
+ * The report can result in a multistatus response, but also to raw data. In case
+ * the @func is provided and the result is a multistatus response, then it is traversed
+ * using this @func. The @func is called always at least once, with %NULL xpath_prop_prefix,
+ * which is meant to let the caller setup the xpath_ctx, like to register its own namespaces
+ * to it with e_xml_xpath_context_register_namespaces(). All other invocations of @func
+ * will have xpath_prop_prefix non-%NULL.
+ *
+ * The optional @out_content_type can be used to get content type of the response.
+ * Free it with g_free(), when no longer needed.
+ *
+ * The optional @out_content can be used to get actual result content. Free it
+ * with g_byte_array_free(), when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_report_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const gchar *depth,
+ const EXmlDocument *xml,
+ EWebDAVPropstatTraverseFunc func,
+ gpointer func_user_data,
+ gchar **out_content_type,
+ GByteArray **out_content,
+ GCancellable *cancellable,
+ GError **error)
+{
+ SoupRequestHTTP *request;
+ SoupMessage *message;
+ GByteArray *bytes;
+ gchar *content;
+ gsize content_length;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (E_IS_XML_DOCUMENT (xml), FALSE);
+
+ if (out_content_type)
+ *out_content_type = NULL;
+
+ if (out_content)
+ *out_content = NULL;
+
+ request = e_webdav_session_new_request (webdav, "REPORT", uri, error);
+ if (!request)
+ return FALSE;
+
+ message = soup_request_http_get_message (request);
+ if (!message) {
+ g_warn_if_fail (message != NULL);
+ g_object_unref (request);
+
+ return FALSE;
+ }
+
+ if (depth)
+ soup_message_headers_replace (message->request_headers, "Depth", depth);
+
+ content = e_xml_document_get_content (xml, &content_length);
+ if (!content) {
+ g_object_unref (message);
+ g_object_unref (request);
+
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, _("Failed to get input
XML content"));
+
+ return FALSE;
+ }
+
+ soup_message_set_request (message, E_WEBDAV_CONTENT_TYPE_XML,
+ SOUP_MEMORY_TAKE, content, content_length);
+
+ bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable,
error);
+
+ success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, TRUE, _("Failed to
issue REPORT"), error) &&
+ bytes != NULL;
+
+ if (success && func && message->status_code == SOUP_STATUS_MULTI_STATUS)
+ success = e_webdav_session_traverse_multistatus_response (webdav, message, bytes, func,
func_user_data, error);
+
+ if (success) {
+ if (out_content_type) {
+ *out_content_type = g_strdup (soup_message_headers_get_content_type
(message->response_headers, NULL));
+ }
+
+ if (out_content) {
+ *out_content = bytes;
+ bytes = NULL;
+ }
+ }
+
+ if (bytes)
+ g_byte_array_free (bytes, TRUE);
+ g_object_unref (message);
+ g_object_unref (request);
+
+ return success;
+}
+
+/**
+ * e_webdav_session_mkcol_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: URI of the collection to create
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new generic collection identified by @uri on the server.
+ * To create specific collections use e_webdav_session_mkcalendar_sync()
+ * or e_webdav_session_mkcol_addressbook_sync().
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_mkcol_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ GCancellable *cancellable,
+ GError **error)
+{
+ SoupRequestHTTP *request;
+ GByteArray *bytes;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (uri != NULL, FALSE);
+
+ request = e_webdav_session_new_request (webdav, SOUP_METHOD_MKCOL, uri, error);
+ if (!request)
+ return FALSE;
+
+ bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable,
error);
+
+ success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, FALSE, _("Failed to
create collection"), error) &&
+ bytes != NULL;
+
+ if (bytes)
+ g_byte_array_free (bytes, TRUE);
+ g_object_unref (request);
+
+ return success;
+}
+
+/**
+ * e_webdav_session_mkcol_addressbook_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: URI of the collection to create
+ * @display_name: (nullable): a human-readable display name to set, or %NULL
+ * @description: (nullable): a human-readable description of the address book, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new address book collection identified by @uri on the server.
+ *
+ * Note that CardDAV RFC 6352 Section 5.2 forbids to create address book
+ * resources under other address book resources (no nested address books
+ * are allowed).
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_mkcol_addressbook_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const gchar *display_name,
+ const gchar *description,
+ GCancellable *cancellable,
+ GError **error)
+{
+ SoupRequestHTTP *request;
+ SoupMessage *message;
+ EXmlDocument *xml;
+ gchar *content;
+ gsize content_length;
+ GByteArray *bytes;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (uri != NULL, FALSE);
+
+ request = e_webdav_session_new_request (webdav, SOUP_METHOD_MKCOL, uri, error);
+ if (!request)
+ return FALSE;
+
+ message = soup_request_http_get_message (request);
+ if (!message) {
+ g_warn_if_fail (message != NULL);
+ g_object_unref (request);
+
+ return FALSE;
+ }
+
+ xml = e_xml_document_new (E_WEBDAV_NS_DAV, "mkcol");
+ e_xml_document_add_namespaces (xml, "A", E_WEBDAV_NS_CARDDAV, NULL);
+
+ e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "set");
+ e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "prop");
+ e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "resourcetype");
+ e_xml_document_add_empty_element (xml, E_WEBDAV_NS_DAV, "collection");
+ e_xml_document_add_empty_element (xml, E_WEBDAV_NS_CARDDAV, "addressbook");
+ e_xml_document_end_element (xml); /* resourcetype */
+
+ if (display_name && *display_name) {
+ e_xml_document_start_text_element (xml, E_WEBDAV_NS_DAV, "displayname");
+ e_xml_document_write_string (xml, display_name);
+ e_xml_document_end_element (xml);
+ }
+
+ if (description && *description) {
+ e_xml_document_start_text_element (xml, E_WEBDAV_NS_CARDDAV, "addressbook-description");
+ e_xml_document_write_string (xml, description);
+ e_xml_document_end_element (xml);
+ }
+
+ e_xml_document_end_element (xml); /* prop */
+ e_xml_document_end_element (xml); /* set */
+
+ content = e_xml_document_get_content (xml, &content_length);
+ if (!content) {
+ g_object_unref (message);
+ g_object_unref (request);
+ g_object_unref (xml);
+
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, _("Failed to get XML
request content"));
+
+ return FALSE;
+ }
+
+ soup_message_set_request (message, E_WEBDAV_CONTENT_TYPE_XML,
+ SOUP_MEMORY_TAKE, content, content_length);
+
+ g_object_unref (xml);
+
+ bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable,
error);
+
+ success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, FALSE, _("Failed to
create address book"), error) &&
+ bytes != NULL;
+
+ if (bytes)
+ g_byte_array_free (bytes, TRUE);
+ g_object_unref (message);
+ g_object_unref (request);
+
+ return success;
+}
+
+/**
+ * e_webdav_session_mkcalendar_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: URI of the collection to create
+ * @display_name: (nullable): a human-readable display name to set, or %NULL
+ * @description: (nullable): a human-readable description of the calendar, or %NULL
+ * @color: (nullable): a color to set, in format "#RRGGBB", or %NULL
+ * @supports: a bit-or of EWebDAVResourceSupports values
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Creates a new calendar collection identified by @uri on the server.
+ * The @supports defines what component types can be stored into
+ * the created calendar collection. Only %E_WEBDAV_RESOURCE_SUPPORTS_NONE
+ * and values related to iCalendar content can be used here.
+ * Using %E_WEBDAV_RESOURCE_SUPPORTS_NONE means that everything is supported.
+ *
+ * Note that CalDAV RFC 4791 Section 4.2 forbids to create calendar
+ * resources under other calendar resources (no nested calendars
+ * are allowed).
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_mkcalendar_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const gchar *display_name,
+ const gchar *description,
+ const gchar *color,
+ guint32 supports,
+ GCancellable *cancellable,
+ GError **error)
+{
+ SoupRequestHTTP *request;
+ SoupMessage *message;
+ GByteArray *bytes;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (uri != NULL, FALSE);
+
+ request = e_webdav_session_new_request (webdav, "MKCALENDAR", uri, error);
+ if (!request)
+ return FALSE;
+
+ message = soup_request_http_get_message (request);
+ if (!message) {
+ g_warn_if_fail (message != NULL);
+ g_object_unref (request);
+
+ return FALSE;
+ }
+
+ supports = supports & (
+ E_WEBDAV_RESOURCE_SUPPORTS_EVENTS |
+ E_WEBDAV_RESOURCE_SUPPORTS_MEMOS |
+ E_WEBDAV_RESOURCE_SUPPORTS_TASKS |
+ E_WEBDAV_RESOURCE_SUPPORTS_FREEBUSY |
+ E_WEBDAV_RESOURCE_SUPPORTS_TIMEZONE);
+
+ if ((display_name && *display_name) ||
+ (description && *description) ||
+ (color && *color) ||
+ (supports != 0)) {
+ EXmlDocument *xml;
+ gchar *content;
+ gsize content_length;
+
+ xml = e_xml_document_new (E_WEBDAV_NS_CALDAV, "mkcalendar");
+ e_xml_document_add_namespaces (xml, "D", E_WEBDAV_NS_DAV, NULL);
+
+ e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "set");
+ e_xml_document_start_element (xml, E_WEBDAV_NS_DAV, "prop");
+
+ if (display_name && *display_name) {
+ e_xml_document_start_text_element (xml, E_WEBDAV_NS_DAV, "displayname");
+ e_xml_document_write_string (xml, display_name);
+ e_xml_document_end_element (xml);
+ }
+
+ if (description && *description) {
+ e_xml_document_start_text_element (xml, E_WEBDAV_NS_CALDAV, "calendar-description");
+ e_xml_document_write_string (xml, description);
+ e_xml_document_end_element (xml);
+ }
+
+ if (color && *color) {
+ e_xml_document_add_namespaces (xml, "IC", E_WEBDAV_NS_ICAL, NULL);
+
+ e_xml_document_start_text_element (xml, E_WEBDAV_NS_ICAL, "calendar-color");
+ e_xml_document_write_string (xml, color);
+ e_xml_document_end_element (xml);
+ }
+
+ if (supports != 0) {
+ struct SupportValues {
+ guint32 mask;
+ const gchar *value;
+ } values[] = {
+ { E_WEBDAV_RESOURCE_SUPPORTS_EVENTS, "VEVENT" },
+ { E_WEBDAV_RESOURCE_SUPPORTS_MEMOS, "VJOURNAL" },
+ { E_WEBDAV_RESOURCE_SUPPORTS_TASKS, "VTODO" },
+ { E_WEBDAV_RESOURCE_SUPPORTS_FREEBUSY, "VFREEBUSY" },
+ { E_WEBDAV_RESOURCE_SUPPORTS_TIMEZONE, "TIMEZONE" }
+ };
+ gint ii;
+
+ e_xml_document_start_text_element (xml, E_WEBDAV_NS_CALDAV,
"supported-calendar-component-set");
+
+ for (ii = 0; ii < G_N_ELEMENTS (values); ii++) {
+ if ((supports & values[ii].mask) != 0) {
+ e_xml_document_start_text_element (xml, E_WEBDAV_NS_CALDAV, "comp");
+ e_xml_document_add_attribute (xml, NULL, "name", values[ii].value);
+ e_xml_document_end_element (xml); /* comp */
+ }
+ }
+
+ e_xml_document_end_element (xml); /* supported-calendar-component-set */
+ }
+
+ e_xml_document_end_element (xml); /* prop */
+ e_xml_document_end_element (xml); /* set */
+
+ content = e_xml_document_get_content (xml, &content_length);
+ if (!content) {
+ g_object_unref (message);
+ g_object_unref (request);
+ g_object_unref (xml);
+
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, _("Failed to get
XML request content"));
+
+ return FALSE;
+ }
+
+ soup_message_set_request (message, E_WEBDAV_CONTENT_TYPE_XML,
+ SOUP_MEMORY_TAKE, content, content_length);
+
+ g_object_unref (xml);
+ }
+
+ bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable,
error);
+
+ success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, FALSE, _("Failed to
create calendar"), error) &&
+ bytes != NULL;
+
+ if (bytes)
+ g_byte_array_free (bytes, TRUE);
+ g_object_unref (message);
+ g_object_unref (request);
+
+ return success;
+}
+
+static void
+e_webdav_session_extract_href_and_etag (SoupMessage *message,
+ gchar **out_href,
+ gchar **out_etag)
+{
+ g_return_if_fail (SOUP_IS_MESSAGE (message));
+
+ if (out_href) {
+ const gchar *header;
+
+ *out_href = NULL;
+
+ header = soup_message_headers_get_list (message->response_headers, "Location");
+ if (header) {
+ gchar *file = strrchr (header, '/');
+
+ if (file) {
+ gchar *decoded;
+
+ decoded = soup_uri_decode (file + 1);
+ *out_href = soup_uri_encode (decoded ? decoded : (file + 1), NULL);
+
+ g_free (decoded);
+ }
+ }
+
+ if (!*out_href)
+ *out_href = soup_uri_to_string (soup_message_get_uri (message), FALSE);
+ }
+
+ if (out_etag) {
+ const gchar *header;
+
+ *out_etag = NULL;
+
+ header = soup_message_headers_get_list (message->response_headers, "ETag");
+ if (header)
+ *out_etag = e_webdav_session_util_maybe_dequote (g_strdup (header));
+ }
+}
+
+/**
+ * e_webdav_session_get_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: URI of the resource to read
+ * @out_href: (out) (nullable) (transfer full): optional return location for href of the resource, or %NULL
+ * @out_etag: (out) (nullable) (transfer full): optional return location for etag of the resource, or %NULL
+ * @out_stream: (out) (caller-allocates): a #GOutputStream to write data to
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Reads a resource identified by @uri from the server and writes it
+ * to the @stream. The URI cannot reference a collection.
+ *
+ * Free returned pointer of @out_href and @out_etag, if not %NULL, with g_free(),
+ * when no longer needed.
+ *
+ * The e_webdav_session_get_data_sync() can be used to read the resource data
+ * directly to memory.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_get_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ gchar **out_href,
+ gchar **out_etag,
+ GOutputStream *out_stream,
+ GCancellable *cancellable,
+ GError **error)
+{
+ SoupRequestHTTP *request;
+ SoupMessage *message;
+ GInputStream *input_stream;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (uri != NULL, FALSE);
+ g_return_val_if_fail (G_IS_OUTPUT_STREAM (out_stream), FALSE);
+
+ request = e_webdav_session_new_request (webdav, SOUP_METHOD_GET, uri, error);
+ if (!request)
+ return FALSE;
+
+ message = soup_request_http_get_message (request);
+ if (!message) {
+ g_warn_if_fail (message != NULL);
+ g_object_unref (request);
+
+ return FALSE;
+ }
+
+ input_stream = e_soup_session_send_request_sync (E_SOUP_SESSION (webdav), request, cancellable,
error);
+
+ success = input_stream != NULL;
+
+ if (success) {
+ SoupLoggerLogLevel log_level = e_soup_session_get_log_level (E_SOUP_SESSION (webdav));
+ gpointer buffer;
+ gsize nread = 0, nwritten;
+ gboolean first_chunk = TRUE;
+
+ buffer = g_malloc (BUFFER_SIZE);
+
+ while (success = g_input_stream_read_all (input_stream, buffer, BUFFER_SIZE, &nread,
cancellable, error),
+ success && nread > 0) {
+ if (log_level == SOUP_LOGGER_LOG_BODY) {
+ fwrite (buffer, 1, nread, stdout);
+ fflush (stdout);
+ }
+
+ if (first_chunk) {
+ GByteArray tmp_bytes;
+
+ first_chunk = FALSE;
+
+ tmp_bytes.data = buffer;
+ tmp_bytes.len = nread;
+
+ success = !e_webdav_session_replace_with_detailed_error (webdav, request,
&tmp_bytes, FALSE, _("Failed to read resource"), error);
+ if (!success)
+ break;
+ }
+
+ success = g_output_stream_write_all (out_stream, buffer, nread, &nwritten,
cancellable, error);
+ if (!success)
+ break;
+ }
+
+ if (success && first_chunk) {
+ success = !e_webdav_session_replace_with_detailed_error (webdav, request, NULL,
FALSE, _("Failed to read resource"), error);
+ } else if (success && !first_chunk && log_level == SOUP_LOGGER_LOG_BODY) {
+ fprintf (stdout, "\n");
+ fflush (stdout);
+ }
+
+ g_free (buffer);
+ }
+
+ if (success)
+ e_webdav_session_extract_href_and_etag (message, out_href, out_etag);
+
+ g_clear_object (&input_stream);
+ g_object_unref (message);
+ g_object_unref (request);
+
+ return success;
+}
+
+/**
+ * e_webdav_session_get_data_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: URI of the resource to read
+ * @out_href: (out) (nullable) (transfer full): optional return location for href of the resource, or %NULL
+ * @out_etag: (out) (nullable) (transfer full): optional return location for etag of the resource, or %NULL
+ * @out_bytes: (out) (transfer full): return location for bytes being read
+ * @out_length: (out) (nullable): option return location for length of bytes being read, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Reads a resource identified by @uri from the server. The URI cannot
+ * reference a collection.
+ *
+ * The @out_bytes is filled by actual data being read. If not %NULL, @out_length
+ * is populated with how many bytes had been read. The @out_bytes is always
+ * NUL-terminated, while this termination byte is not part of @out_length.
+ * Free the @out_bytes with g_free(), when no longer needed.
+ *
+ * Free returned pointer of @out_href and @out_etag, if not %NULL, with g_free(),
+ * when no longer needed.
+ *
+ * To read large data use e_webdav_session_get_sync() instead.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_get_data_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ gchar **out_href,
+ gchar **out_etag,
+ gchar **out_bytes,
+ gsize *out_length,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GOutputStream *output_stream;
+ gsize bytes_written = 0;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (uri != NULL, FALSE);
+ g_return_val_if_fail (out_bytes != NULL, FALSE);
+
+ *out_bytes = NULL;
+ if (out_length)
+ *out_length = 0;
+
+ output_stream = g_memory_output_stream_new_resizable ();
+
+ success = e_webdav_session_get_sync (webdav, uri, out_href, out_etag, output_stream, cancellable,
error) &&
+ g_output_stream_write_all (output_stream, "", 1, &bytes_written, cancellable, error) &&
+ g_output_stream_close (output_stream, cancellable, error);
+
+ if (success) {
+ if (out_length)
+ *out_length = g_memory_output_stream_get_data_size (G_MEMORY_OUTPUT_STREAM
(output_stream)) - 1;
+
+ *out_bytes = g_memory_output_stream_steal_data (G_MEMORY_OUTPUT_STREAM (output_stream));
+ }
+
+ g_object_unref (output_stream);
+
+ return success;
+}
+
+typedef struct _ChunkWriteData {
+ SoupSession *session;
+ SoupLoggerLogLevel log_level;
+ GInputStream *stream;
+ goffset read_from;
+ gboolean wrote_any;
+ gsize buffer_size;
+ gpointer buffer;
+ GCancellable *cancellable;
+ GError *error;
+} ChunkWriteData;
+
+static void
+e_webdav_session_write_next_chunk (SoupMessage *message,
+ gpointer user_data)
+{
+ ChunkWriteData *cwd = user_data;
+ gsize nread;
+
+ g_return_if_fail (SOUP_IS_MESSAGE (message));
+ g_return_if_fail (cwd != NULL);
+
+ if (!g_input_stream_read_all (cwd->stream, cwd->buffer, cwd->buffer_size, &nread, cwd->cancellable,
&cwd->error)) {
+ soup_session_cancel_message (cwd->session, message, SOUP_STATUS_CANCELLED);
+ return;
+ }
+
+ if (nread == 0) {
+ soup_message_body_complete (message->request_body);
+ } else {
+ cwd->wrote_any = TRUE;
+ soup_message_body_append (message->request_body, SOUP_MEMORY_TEMPORARY, cwd->buffer, nread);
+
+ if (cwd->log_level == SOUP_LOGGER_LOG_BODY) {
+ fwrite (cwd->buffer, 1, nread, stdout);
+ fflush (stdout);
+ }
+ }
+}
+
+static void
+e_webdav_session_write_restarted (SoupMessage *message,
+ gpointer user_data)
+{
+ ChunkWriteData *cwd = user_data;
+
+ g_return_if_fail (SOUP_IS_MESSAGE (message));
+ g_return_if_fail (cwd != NULL);
+
+ /* The 302 redirect will turn it into a GET request and
+ * reset the body encoding back to "NONE". Fix that.
+ */
+ soup_message_headers_set_encoding (message->request_headers, SOUP_ENCODING_CHUNKED);
+ message->method = SOUP_METHOD_PUT;
+
+ if (cwd->wrote_any) {
+ cwd->wrote_any = FALSE;
+
+ if (!G_IS_SEEKABLE (cwd->stream) || !g_seekable_can_seek (G_SEEKABLE (cwd->stream)) ||
+ !g_seekable_seek (G_SEEKABLE (cwd->stream), cwd->read_from, G_SEEK_SET, cwd->cancellable,
&cwd->error)) {
+ if (!cwd->error)
+ g_set_error_literal (&cwd->error, G_IO_ERROR, G_IO_ERROR_PARTIAL_INPUT,
+ _("Cannot rewind input stream: Not supported"));
+
+ soup_session_cancel_message (cwd->session, message, SOUP_STATUS_CANCELLED);
+ }
+ }
+}
+
+/**
+ * e_webdav_session_put_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: URI of the resource to write
+ * @etag: (nullable): an ETag of the resource, if it's an existing resource, or %NULL
+ * @content_type: Content-Type of the @bytes to be written
+ * @stream: a #GInputStream with data to be written
+ * @out_href: (out) (nullable) (transfer full): optional return location for href of the resource, or %NULL
+ * @out_etag: (out) (nullable) (transfer full): optional return location for etag of the resource, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Writes data from @stream to a resource identified by @uri to the server.
+ * The URI cannot reference a collection.
+ *
+ * The @etag argument is used to avoid clashes when overwriting existing
+ * resources. It can contain three values:
+ * - %NULL - to write completely new resource
+ * - empty string - write new resource or overwrite any existing, regardless changes on the server
+ * - valid ETag - overwrite existing resource only if it wasn't changed on the server.
+ *
+ * Note that the actual behaviour is also influenced by #ESourceWebdav:avoid-ifmatch
+ * property of the associated #ESource.
+ *
+ * The @out_href, if provided, is filled with the resulting URI
+ * of the written resource. It can be different from the @uri when the server
+ * redirected to a different location.
+ *
+ * The @out_etag contains ETag of the resource after it had been saved.
+ *
+ * The @stream should support also #GSeekable interface, because the data
+ * send can require restart of the send due to redirect or other reasons.
+ *
+ * The e_webdav_session_put_data_sync() can be used to write data stored in memory.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_put_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const gchar *etag,
+ const gchar *content_type,
+ GInputStream *stream,
+ gchar **out_href,
+ gchar **out_etag,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ChunkWriteData cwd;
+ SoupRequestHTTP *request;
+ SoupMessage *message;
+ GByteArray *bytes;
+ gulong restarted_id, wrote_headers_id, wrote_chunk_id;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (uri != NULL, FALSE);
+ g_return_val_if_fail (content_type != NULL, FALSE);
+ g_return_val_if_fail (G_IS_INPUT_STREAM (stream), FALSE);
+
+ if (out_href)
+ *out_href = NULL;
+ if (out_etag)
+ *out_etag = NULL;
+
+ request = e_webdav_session_new_request (webdav, SOUP_METHOD_PUT, uri, error);
+ if (!request)
+ return FALSE;
+
+ message = soup_request_http_get_message (request);
+ if (!message) {
+ g_warn_if_fail (message != NULL);
+ g_object_unref (request);
+
+ return FALSE;
+ }
+
+ if (!etag || *etag) {
+ ESource *source;
+ gboolean avoid_ifmatch = FALSE;
+
+ source = e_soup_session_get_source (E_SOUP_SESSION (webdav));
+ if (source && e_source_has_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND)) {
+ ESourceWebdav *webdav_extension;
+
+ webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+ avoid_ifmatch = e_source_webdav_get_avoid_ifmatch (webdav_extension);
+ }
+
+ if (!avoid_ifmatch) {
+ if (etag) {
+ gint len = strlen (etag);
+
+ if (*etag == '\"' && len > 2 && etag[len - 1] == '\"') {
+ soup_message_headers_replace (message->request_headers, "If-Match",
etag);
+ } else {
+ gchar *quoted;
+
+ quoted = g_strconcat ("\"", etag, "\"", NULL);
+ soup_message_headers_replace (message->request_headers, "If-Match",
quoted);
+ g_free (quoted);
+ }
+ } else {
+ soup_message_headers_replace (message->request_headers, "If-None-Match", "*");
+ }
+ }
+ }
+
+ cwd.session = SOUP_SESSION (webdav);
+ cwd.log_level = e_soup_session_get_log_level (E_SOUP_SESSION (webdav));
+ cwd.stream = stream;
+ cwd.read_from = 0;
+ cwd.wrote_any = FALSE;
+ cwd.buffer_size = BUFFER_SIZE;
+ cwd.buffer = g_malloc (cwd.buffer_size);
+ cwd.cancellable = cancellable;
+ cwd.error = NULL;
+
+ if (G_IS_SEEKABLE (stream) && g_seekable_can_seek (G_SEEKABLE (stream)))
+ cwd.read_from = g_seekable_tell (G_SEEKABLE (stream));
+
+ if (content_type && *content_type)
+ soup_message_headers_replace (message->request_headers, "Content-Type", content_type);
+
+ soup_message_headers_set_encoding (message->request_headers, SOUP_ENCODING_CHUNKED);
+ soup_message_body_set_accumulate (message->request_body, FALSE);
+ soup_message_set_flags (message, SOUP_MESSAGE_CAN_REBUILD);
+
+ restarted_id = g_signal_connect (message, "restarted", G_CALLBACK (e_webdav_session_write_restarted),
&cwd);
+ wrote_headers_id = g_signal_connect (message, "wrote-headers", G_CALLBACK
(e_webdav_session_write_next_chunk), &cwd);
+ wrote_chunk_id = g_signal_connect (message, "wrote-chunk", G_CALLBACK
(e_webdav_session_write_next_chunk), &cwd);
+
+ bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable,
error);
+
+ g_signal_handler_disconnect (message, restarted_id);
+ g_signal_handler_disconnect (message, wrote_headers_id);
+ g_signal_handler_disconnect (message, wrote_chunk_id);
+
+ success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, FALSE, _("Failed to
put data"), error) &&
+ bytes != NULL;
+
+ if (cwd.wrote_any && cwd.log_level == SOUP_LOGGER_LOG_BODY) {
+ fprintf (stdout, "\n");
+ fflush (stdout);
+ }
+
+ if (cwd.error) {
+ g_clear_error (error);
+ g_propagate_error (error, cwd.error);
+ success = FALSE;
+ }
+
+ if (success) {
+ if (success && !SOUP_STATUS_IS_SUCCESSFUL (message->status_code)) {
+ success = FALSE;
+
+ g_set_error (error, SOUP_HTTP_ERROR, message->status_code,
+ _("Failed to put data to server, error code %d (%s)"), message->status_code,
+ e_soup_session_util_status_to_string (message->status_code,
message->reason_phrase));
+ }
+ }
+
+ if (success)
+ e_webdav_session_extract_href_and_etag (message, out_href, out_etag);
+
+ if (bytes)
+ g_byte_array_free (bytes, TRUE);
+ g_object_unref (message);
+ g_object_unref (request);
+ g_free (cwd.buffer);
+
+ return success;
+}
+
+/**
+ * e_webdav_session_put_data_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: URI of the resource to write
+ * @etag: (nullable): an ETag of the resource, if it's an existing resource, or %NULL
+ * @content_type: Content-Type of the @bytes to be written
+ * @bytes: actual bytes to be written
+ * @length: how many bytes to write, or -1, when the @bytes is NUL-terminated
+ * @out_href: (out) (nullable) (transfer full): optional return location for href of the resource, or %NULL
+ * @out_etag: (out) (nullable) (transfer full): optional return location for etag of the resource, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Writes data to a resource identified by @uri to the server. The URI cannot
+ * reference a collection.
+ *
+ * The @etag argument is used to avoid clashes when overwriting existing
+ * resources. It can contain three values:
+ * - %NULL - to write completely new resource
+ * - empty string - write new resource or overwrite any existing, regardless changes on the server
+ * - valid ETag - overwrite existing resource only if it wasn't changed on the server.
+ *
+ * Note that the actual usage of @etag is also influenced by #ESourceWebdav:avoid-ifmatch
+ * property of the associated #ESource.
+ *
+ * The @out_href, if provided, is filled with the resulting URI
+ * of the written resource. It can be different from the @uri when the server
+ * redirected to a different location.
+ *
+ * The @out_etag contains ETag of the resource after it had been saved.
+ *
+ * To read large data use e_webdav_session_put_sync() instead.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_put_data_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const gchar *etag,
+ const gchar *content_type,
+ const gchar *bytes,
+ gsize length,
+ gchar **out_href,
+ gchar **out_etag,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GInputStream *input_stream;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (uri != NULL, FALSE);
+ g_return_val_if_fail (content_type != NULL, FALSE);
+ g_return_val_if_fail (bytes != NULL, FALSE);
+
+ if (length == (gsize) -1)
+ length = strlen (bytes);
+
+ input_stream = g_memory_input_stream_new_from_data (bytes, length, NULL);
+
+ success = e_webdav_session_put_sync (webdav, uri, etag, content_type,
+ input_stream, out_href, out_etag, cancellable, error);
+
+ g_object_unref (input_stream);
+
+ return success;
+}
+
+/**
+ * e_webdav_session_delete_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: URI of the resource to delete
+ * @depth: (nullable): optional requested depth, can be one of %E_WEBDAV_DEPTH_THIS or
%E_WEBDAV_DEPTH_INFINITY, or %NULL
+ * @etag: (nullable): an optional ETag of the resource, or %NULL
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Deletes a resource identified by @uri on the server. The URI can
+ * reference a collection, in which case @depth should be %E_WEBDAV_DEPTH_INFINITY.
+ * Use @depth %E_WEBDAV_DEPTH_THIS when deleting a regular resource, or %NULL,
+ * to let the server use default Depth.
+ *
+ * The @etag argument is used to avoid clashes when overwriting existing resources.
+ * Use %NULL @etag when deleting collection resources or to force the deletion,
+ * otherwise provide a valid ETag of a non-collection resource to verify that
+ * the version requested to delete is the same as on the server.
+ *
+ * Note that the actual usage of @etag is also influenced by #ESourceWebdav:avoid-ifmatch
+ * property of the associated #ESource.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_delete_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const gchar *depth,
+ const gchar *etag,
+ GCancellable *cancellable,
+ GError **error)
+{
+ SoupRequestHTTP *request;
+ SoupMessage *message;
+ GByteArray *bytes;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (uri != NULL, FALSE);
+
+ request = e_webdav_session_new_request (webdav, SOUP_METHOD_DELETE, uri, error);
+ if (!request)
+ return FALSE;
+
+ message = soup_request_http_get_message (request);
+ if (!message) {
+ g_warn_if_fail (message != NULL);
+ g_object_unref (request);
+
+ return FALSE;
+ }
+
+ if (etag) {
+ ESource *source;
+ gboolean avoid_ifmatch = FALSE;
+
+ source = e_soup_session_get_source (E_SOUP_SESSION (webdav));
+ if (source && e_source_has_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND)) {
+ ESourceWebdav *webdav_extension;
+
+ webdav_extension = e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND);
+ avoid_ifmatch = e_source_webdav_get_avoid_ifmatch (webdav_extension);
+ }
+
+ if (!avoid_ifmatch) {
+ gint len = strlen (etag);
+
+ if (*etag == '\"' && len > 2 && etag[len - 1] == '\"') {
+ soup_message_headers_replace (message->request_headers, "If-Match", etag);
+ } else {
+ gchar *quoted;
+
+ quoted = g_strconcat ("\"", etag, "\"", NULL);
+ soup_message_headers_replace (message->request_headers, "If-Match", quoted);
+ g_free (quoted);
+ }
+ }
+ }
+
+ if (depth)
+ soup_message_headers_replace (message->request_headers, "Depth", depth);
+
+ bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable,
error);
+
+ success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, FALSE, _("Failed to
delete resource"), error) &&
+ bytes != NULL;
+
+ if (bytes)
+ g_byte_array_free (bytes, TRUE);
+ g_object_unref (message);
+ g_object_unref (request);
+
+ return success;
+}
+
+/**
+ * e_webdav_session_copy_sync:
+ * @webdav: an #EWebDAVSession
+ * @source_uri: URI of the resource or collection to copy
+ * @destination_uri: URI of the destination
+ * @depth: requested depth, can be one of %E_WEBDAV_DEPTH_THIS or %E_WEBDAV_DEPTH_INFINITY
+ * @can_overwrite: whether can overwrite @destination_uri, when it exists
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Copies a resource identified by @source_uri to @destination_uri on the server.
+ * The @source_uri can reference also collections, in which case the @depth influences
+ * whether only the collection itself is copied (%E_WEBDAV_DEPTH_THIS) or whether
+ * the collection with all its children is copied (%E_WEBDAV_DEPTH_INFINITY).
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_copy_sync (EWebDAVSession *webdav,
+ const gchar *source_uri,
+ const gchar *destination_uri,
+ const gchar *depth,
+ gboolean can_overwrite,
+ GCancellable *cancellable,
+ GError **error)
+{
+ SoupRequestHTTP *request;
+ SoupMessage *message;
+ GByteArray *bytes;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (source_uri != NULL, FALSE);
+ g_return_val_if_fail (destination_uri != NULL, FALSE);
+ g_return_val_if_fail (depth != NULL, FALSE);
+
+ request = e_webdav_session_new_request (webdav, SOUP_METHOD_COPY, source_uri, error);
+ if (!request)
+ return FALSE;
+
+ message = soup_request_http_get_message (request);
+ if (!message) {
+ g_warn_if_fail (message != NULL);
+ g_object_unref (request);
+
+ return FALSE;
+ }
+
+ soup_message_headers_replace (message->request_headers, "Depth", depth);
+ soup_message_headers_replace (message->request_headers, "Destination", destination_uri);
+ soup_message_headers_replace (message->request_headers, "Overwrite", can_overwrite ? "T" : "F");
+
+ bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable,
error);
+
+ success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, FALSE, _("Failed to
copy resource"), error) &&
+ bytes != NULL;
+
+ if (bytes)
+ g_byte_array_free (bytes, TRUE);
+ g_object_unref (message);
+ g_object_unref (request);
+
+ return success;
+}
+
+/**
+ * e_webdav_session_move_sync:
+ * @webdav: an #EWebDAVSession
+ * @source_uri: URI of the resource or collection to copy
+ * @destination_uri: URI of the destination
+ * @can_overwrite: whether can overwrite @destination_uri, when it exists
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Moves a resource identified by @source_uri to @destination_uri on the server.
+ * The @source_uri can reference also collections.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_move_sync (EWebDAVSession *webdav,
+ const gchar *source_uri,
+ const gchar *destination_uri,
+ gboolean can_overwrite,
+ GCancellable *cancellable,
+ GError **error)
+{
+ SoupRequestHTTP *request;
+ SoupMessage *message;
+ GByteArray *bytes;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (source_uri != NULL, FALSE);
+ g_return_val_if_fail (destination_uri != NULL, FALSE);
+
+ request = e_webdav_session_new_request (webdav, SOUP_METHOD_MOVE, source_uri, error);
+ if (!request)
+ return FALSE;
+
+ message = soup_request_http_get_message (request);
+ if (!message) {
+ g_warn_if_fail (message != NULL);
+ g_object_unref (request);
+
+ return FALSE;
+ }
+
+ soup_message_headers_replace (message->request_headers, "Depth", E_WEBDAV_DEPTH_INFINITY);
+ soup_message_headers_replace (message->request_headers, "Destination", destination_uri);
+ soup_message_headers_replace (message->request_headers, "Overwrite", can_overwrite ? "T" : "F");
+
+ bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable,
error);
+
+ success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, FALSE, _("Failed to
move resource"), error) &&
+ bytes != NULL;
+
+ if (bytes)
+ g_byte_array_free (bytes, TRUE);
+ g_object_unref (message);
+ g_object_unref (request);
+
+ return success;
+}
+
+/**
+ * e_webdav_session_lock_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to lock, or %NULL to read from #ESource
+ * @depth: requested depth, can be one of %E_WEBDAV_DEPTH_THIS or %E_WEBDAV_DEPTH_INFINITY
+ * @lock_timeout: timeout for the lock, in seconds, on 0 to infinity
+ * @xml: an XML describing the lock request, with DAV:lockinfo root element
+ * @out_lock_token: (out) (transfer full): return location of the obtained or refreshed lock token
+ * @out_xml_response: (out) (nullable) (transfer full): optional return location for the server response as
#xmlDocPtr
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Locks a resource identified by @uri, or, in case it's %NULL, on the URI
+ * defined in associated #ESource.
+ *
+ * The @out_lock_token can be refreshed with e_webdav_session_refresh_lock_sync().
+ * Release the lock with e_webdav_session_unlock_sync().
+ * Free the returned @out_lock_token with g_free(), when no longer needed.
+ *
+ * If provided, free the returned @out_xml_response with xmlFreeDoc(),
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_lock_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const gchar *depth,
+ gint32 lock_timeout,
+ const EXmlDocument *xml,
+ gchar **out_lock_token,
+ xmlDocPtr *out_xml_response,
+ GCancellable *cancellable,
+ GError **error)
+{
+ SoupRequestHTTP *request;
+ SoupMessage *message;
+ GByteArray *bytes;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (depth != NULL, FALSE);
+ g_return_val_if_fail (E_IS_XML_DOCUMENT (xml), FALSE);
+ g_return_val_if_fail (out_lock_token != NULL, FALSE);
+
+ *out_lock_token = NULL;
+
+ request = e_webdav_session_new_request (webdav, SOUP_METHOD_LOCK, uri, error);
+ if (!request)
+ return FALSE;
+
+ message = soup_request_http_get_message (request);
+ if (!message) {
+ g_warn_if_fail (message != NULL);
+ g_object_unref (request);
+
+ return FALSE;
+ }
+
+ if (depth)
+ soup_message_headers_replace (message->request_headers, "Depth", depth);
+
+ if (lock_timeout) {
+ gchar *value;
+
+ value = g_strdup_printf ("Second-%d", lock_timeout);
+ soup_message_headers_replace (message->request_headers, "Timeout", value);
+ g_free (value);
+ } else {
+ soup_message_headers_replace (message->request_headers, "Timeout", "Infinite");
+ }
+
+ if (xml) {
+ gchar *content;
+ gsize content_length;
+
+ content = e_xml_document_get_content (xml, &content_length);
+ if (!content) {
+ g_object_unref (message);
+ g_object_unref (request);
+
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, _("Failed to get
input XML content"));
+
+ return FALSE;
+ }
+
+ soup_message_set_request (message, E_WEBDAV_CONTENT_TYPE_XML,
+ SOUP_MEMORY_TAKE, content, content_length);
+ }
+
+ bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable,
error);
+
+ success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, FALSE, _("Failed to
lock resource"), error) &&
+ bytes != NULL;
+
+ if (success && out_xml_response) {
+ const gchar *content_type;
+
+ *out_xml_response = NULL;
+
+ content_type = soup_message_headers_get_content_type (message->response_headers, NULL);
+ if (!content_type ||
+ (g_ascii_strcasecmp (content_type, "application/xml") != 0 &&
+ g_ascii_strcasecmp (content_type, "text/xml") != 0)) {
+ if (!content_type) {
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
+ _("Expected application/xml response, but none returned"));
+ } else {
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
+ _("Expected application/xml response, but %s returned"),
content_type);
+ }
+
+ success = FALSE;
+ }
+
+ if (success) {
+ xmlDocPtr doc;
+
+ doc = e_xml_parse_data (bytes->data, bytes->len);
+ if (!doc) {
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
+ _("Failed to parse XML data"));
+
+ success = FALSE;
+ } else {
+ *out_xml_response = doc;
+ }
+ }
+ }
+
+ if (success)
+ *out_lock_token = g_strdup (soup_message_headers_get_list (message->response_headers,
"Lock-Token"));
+
+ if (bytes)
+ g_byte_array_free (bytes, TRUE);
+ g_object_unref (message);
+ g_object_unref (request);
+
+ return success;
+}
+
+/**
+ * e_webdav_session_refresh_lock_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to lock, or %NULL to read from #ESource
+ * @lock_token: token of an existing lock
+ * @lock_timeout: timeout for the lock, in seconds, on 0 to infinity
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Refreshes existing lock @lock_token for a resource identified by @uri,
+ * or, in case it's %NULL, on the URI defined in associated #ESource.
+ * The @lock_token is returned from e_webdav_session_lock_sync() and
+ * the @uri should be the same as that used with e_webdav_session_lock_sync().
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_refresh_lock_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const gchar *lock_token,
+ gint32 lock_timeout,
+ GCancellable *cancellable,
+ GError **error)
+{
+ SoupRequestHTTP *request;
+ SoupMessage *message;
+ GByteArray *bytes;
+ gchar *value;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (lock_token != NULL, FALSE);
+
+ request = e_webdav_session_new_request (webdav, SOUP_METHOD_LOCK, uri, error);
+ if (!request)
+ return FALSE;
+
+ message = soup_request_http_get_message (request);
+ if (!message) {
+ g_warn_if_fail (message != NULL);
+ g_object_unref (request);
+
+ return FALSE;
+ }
+
+ if (lock_timeout) {
+ value = g_strdup_printf ("Second-%d", lock_timeout);
+ soup_message_headers_replace (message->request_headers, "Timeout", value);
+ g_free (value);
+ } else {
+ soup_message_headers_replace (message->request_headers, "Timeout", "Infinite");
+ }
+
+ soup_message_headers_replace (message->request_headers, "Lock-Token", lock_token);
+
+ bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable,
error);
+
+ success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, FALSE, _("Failed to
refresh lock"), error) &&
+ bytes != NULL;
+
+ if (bytes)
+ g_byte_array_free (bytes, TRUE);
+ g_object_unref (message);
+ g_object_unref (request);
+
+ return success;
+}
+
+/**
+ * e_webdav_session_unlock_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to lock, or %NULL to read from #ESource
+ * @lock_token: token of an existing lock
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Releases (unlocks) existing lock @lock_token for a resource identified by @uri,
+ * or, in case it's %NULL, on the URI defined in associated #ESource.
+ * The @lock_token is returned from e_webdav_session_lock_sync() and
+ * the @uri should be the same as that used with e_webdav_session_lock_sync().
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_unlock_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const gchar *lock_token,
+ GCancellable *cancellable,
+ GError **error)
+{
+ SoupRequestHTTP *request;
+ SoupMessage *message;
+ GByteArray *bytes;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (lock_token != NULL, FALSE);
+
+ request = e_webdav_session_new_request (webdav, SOUP_METHOD_UNLOCK, uri, error);
+ if (!request)
+ return FALSE;
+
+ message = soup_request_http_get_message (request);
+ if (!message) {
+ g_warn_if_fail (message != NULL);
+ g_object_unref (request);
+
+ return FALSE;
+ }
+
+ soup_message_headers_replace (message->request_headers, "Lock-Token", lock_token);
+
+ bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable,
error);
+
+ success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, FALSE, _("Failed to
unlock"), error) &&
+ bytes != NULL;
+
+ if (bytes)
+ g_byte_array_free (bytes, TRUE);
+ g_object_unref (message);
+ g_object_unref (request);
+
+ return success;
+}
+
+static gboolean
+e_webdav_session_traverse_propstat_response (EWebDAVSession *webdav,
+ const SoupMessage *message,
+ const GByteArray *xml_data,
+ gboolean require_multistatus,
+ const gchar *additional_ns_prefix,
+ const gchar *additional_ns,
+ const gchar *propstat_path_prefix,
+ EWebDAVPropstatTraverseFunc func,
+ gpointer func_user_data,
+ GError **error)
+{
+ SoupURI *request_uri = NULL;
+ xmlDocPtr doc;
+ xmlXPathContextPtr xpath_ctx;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (xml_data != NULL, FALSE);
+ g_return_val_if_fail (propstat_path_prefix != NULL, FALSE);
+ g_return_val_if_fail (func != NULL, FALSE);
+
+ if (message) {
+ const gchar *content_type;
+
+ if (require_multistatus && message->status_code != SOUP_STATUS_MULTI_STATUS) {
+ g_set_error (error, SOUP_HTTP_ERROR, message->status_code,
+ _("Expected multistatus response, but %d returned (%s)"),
message->status_code,
+ e_soup_session_util_status_to_string (message->status_code,
message->reason_phrase));
+
+ return FALSE;
+ }
+
+ content_type = soup_message_headers_get_content_type (message->response_headers, NULL);
+ if (!content_type ||
+ (g_ascii_strcasecmp (content_type, "application/xml") != 0 &&
+ g_ascii_strcasecmp (content_type, "text/xml") != 0)) {
+ if (!content_type) {
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
+ _("Expected application/xml response, but none returned"));
+ } else {
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
+ _("Expected application/xml response, but %s returned"),
content_type);
+ }
+
+ return FALSE;
+ }
+
+ request_uri = soup_message_get_uri ((SoupMessage *) message);
+ }
+
+ doc = e_xml_parse_data (xml_data->data, xml_data->len);
+
+ if (!doc) {
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
+ _("Failed to parse XML data"));
+
+ return FALSE;
+ }
+
+ xpath_ctx = e_xml_new_xpath_context_with_namespaces (doc,
+ "D", E_WEBDAV_NS_DAV,
+ additional_ns_prefix, additional_ns,
+ NULL);
+
+ if (xpath_ctx &&
+ func (webdav, xpath_ctx, NULL, request_uri, NULL, SOUP_STATUS_NONE, func_user_data)) {
+ xmlXPathObjectPtr xpath_obj_response;
+
+ xpath_obj_response = e_xml_xpath_eval (xpath_ctx, "%s", propstat_path_prefix);
+
+ if (xpath_obj_response) {
+ gboolean do_stop = FALSE;
+ gint response_index, response_length;
+
+ response_length = xmlXPathNodeSetGetLength (xpath_obj_response->nodesetval);
+
+ for (response_index = 0; response_index < response_length && !do_stop;
response_index++) {
+ xmlXPathObjectPtr xpath_obj_propstat;
+
+ xpath_obj_propstat = e_xml_xpath_eval (xpath_ctx,
+ "%s[%d]/D:propstat",
+ propstat_path_prefix, response_index + 1);
+
+ if (xpath_obj_propstat) {
+ gchar *href;
+ gint propstat_index, propstat_length;
+
+ href = e_xml_xpath_eval_as_string (xpath_ctx, "%s[%d]/D:href",
propstat_path_prefix, response_index + 1);
+ if (href) {
+ gchar *full_uri;
+
+ full_uri = e_webdav_session_ensure_full_uri (webdav,
request_uri, href);
+ if (full_uri) {
+ g_free (href);
+ href = full_uri;
+ }
+ }
+
+ propstat_length = xmlXPathNodeSetGetLength
(xpath_obj_propstat->nodesetval);
+
+ for (propstat_index = 0; propstat_index < propstat_length &&
!do_stop; propstat_index++) {
+ gchar *status, *propstat_prefix;
+ guint status_code;
+
+ propstat_prefix = g_strdup_printf
("%s[%d]/D:propstat[%d]/D:prop",
+ propstat_path_prefix, response_index + 1,
propstat_index + 1);
+
+ status = e_xml_xpath_eval_as_string (xpath_ctx,
"%s/../D:status", propstat_prefix);
+ if (!status || !soup_headers_parse_status_line (status, NULL,
&status_code, NULL))
+ status_code = 0;
+ g_free (status);
+
+ do_stop = !func (webdav, xpath_ctx, propstat_prefix,
request_uri, href, status_code, func_user_data);
+
+ g_free (propstat_prefix);
+ }
+
+ xmlXPathFreeObject (xpath_obj_propstat);
+ g_free (href);
+ }
+ }
+
+ xmlXPathFreeObject (xpath_obj_response);
+ }
+ }
+
+ if (xpath_ctx)
+ xmlXPathFreeContext (xpath_ctx);
+ xmlFreeDoc (doc);
+
+ return TRUE;
+}
+
+/**
+ * e_webdav_session_traverse_multistatus_response:
+ * @webdav: an #EWebDAVSession
+ * @message: (nullable): an optional #SoupMessage corresponding to the response, or %NULL
+ * @xml_data: a #GByteArray containing DAV:multistatus response
+ * @func: an #EWebDAVPropstatTraverseFunc function to call for each DAV:propstat in the multistatus response
+ * @func_user_data: user data passed to @func
+ * @error: return location for a #GError, or %NULL
+ *
+ * Traverses a DAV:multistatus response and calls @func for each returned DAV:propstat.
+ * The provided XPath context has registered %E_WEBDAV_NS_DAV namespace with prefix "D".
+ * It doesn't have any other namespace registered.
+ *
+ * The @message, if provided, is used to verify that the response is a multi-status
+ * and that the Content-Type is properly set. It's used to get a request URI as well.
+ *
+ * The @func is called always at least once, with %NULL xpath_prop_prefix, which
+ * is meant to let the caller setup the xpath_ctx, like to register its own namespaces
+ * to it with e_xml_xpath_context_register_namespaces(). All other invocations of @func
+ * will have xpath_prop_prefix non-%NULL.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_traverse_multistatus_response (EWebDAVSession *webdav,
+ const SoupMessage *message,
+ const GByteArray *xml_data,
+ EWebDAVPropstatTraverseFunc func,
+ gpointer func_user_data,
+ GError **error)
+{
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (xml_data != NULL, FALSE);
+ g_return_val_if_fail (func != NULL, FALSE);
+
+ return e_webdav_session_traverse_propstat_response (webdav, message, xml_data, TRUE,
+ NULL, NULL, "/D:multistatus/D:response",
+ func, func_user_data, error);
+}
+
+/**
+ * e_webdav_session_traverse_mkcol_response:
+ * @webdav: an #EWebDAVSession
+ * @message: (nullable): an optional #SoupMessage corresponding to the response, or %NULL
+ * @xml_data: a #GByteArray containing DAV:mkcol-response response
+ * @func: an #EWebDAVPropstatTraverseFunc function to call for each DAV:propstat in the response
+ * @func_user_data: user data passed to @func
+ * @error: return location for a #GError, or %NULL
+ *
+ * Traverses a DAV:mkcol-response response and calls @func for each returned DAV:propstat.
+ * The provided XPath context has registered %E_WEBDAV_NS_DAV namespace with prefix "D".
+ * It doesn't have any other namespace registered.
+ *
+ * The @message, if provided, is used to verify that the response is an XML Content-Type.
+ * It's used to get the request URI as well.
+ *
+ * The @func is called always at least once, with %NULL xpath_prop_prefix, which
+ * is meant to let the caller setup the xpath_ctx, like to register its own namespaces
+ * to it with e_xml_xpath_context_register_namespaces(). All other invocations of @func
+ * will have xpath_prop_prefix non-%NULL.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_traverse_mkcol_response (EWebDAVSession *webdav,
+ const SoupMessage *message,
+ const GByteArray *xml_data,
+ EWebDAVPropstatTraverseFunc func,
+ gpointer func_user_data,
+ GError **error)
+{
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (xml_data != NULL, FALSE);
+ g_return_val_if_fail (func != NULL, FALSE);
+
+ return e_webdav_session_traverse_propstat_response (webdav, message, xml_data, FALSE,
+ NULL, NULL, "/D:mkcol-response",
+ func, func_user_data, error);
+}
+
+/**
+ * e_webdav_session_traverse_mkcalendar_response:
+ * @webdav: an #EWebDAVSession
+ * @message: (nullable): an optional #SoupMessage corresponding to the response, or %NULL
+ * @xml_data: a #GByteArray containing CALDAV:mkcalendar-response response
+ * @func: an #EWebDAVPropstatTraverseFunc function to call for each DAV:propstat in the response
+ * @func_user_data: user data passed to @func
+ * @error: return location for a #GError, or %NULL
+ *
+ * Traverses a CALDAV:mkcalendar-response response and calls @func for each returned DAV:propstat.
+ * The provided XPath context has registered %E_WEBDAV_NS_DAV namespace with prefix "D" and
+ * %E_WEBDAV_NS_CALDAV namespace with prefix "C". It doesn't have any other namespace registered.
+ *
+ * The @message, if provided, is used to verify that the response is an XML Content-Type.
+ * It's used to get the request URI as well.
+ *
+ * The @func is called always at least once, with %NULL xpath_prop_prefix, which
+ * is meant to let the caller setup the xpath_ctx, like to register its own namespaces
+ * to it with e_xml_xpath_context_register_namespaces(). All other invocations of @func
+ * will have xpath_prop_prefix non-%NULL.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_traverse_mkcalendar_response (EWebDAVSession *webdav,
+ const SoupMessage *message,
+ const GByteArray *xml_data,
+ EWebDAVPropstatTraverseFunc func,
+ gpointer func_user_data,
+ GError **error)
+{
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (xml_data != NULL, FALSE);
+ g_return_val_if_fail (func != NULL, FALSE);
+
+ return e_webdav_session_traverse_propstat_response (webdav, message, xml_data, FALSE,
+ "C", E_WEBDAV_NS_CALDAV, "/C:mkcalendar-response",
+ func, func_user_data, error);
+}
+
+static gboolean
+e_webdav_session_getctag_cb (EWebDAVSession *webdav,
+ xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prop_prefix,
+ const SoupURI *request_uri,
+ const gchar *href,
+ guint status_code,
+ gpointer user_data)
+{
+ if (!xpath_prop_prefix) {
+ e_xml_xpath_context_register_namespaces (xpath_ctx,
+ "CS", E_WEBDAV_NS_CALENDARSERVER,
+ NULL);
+
+ return TRUE;
+ }
+
+ if (status_code == SOUP_STATUS_OK) {
+ gchar **out_ctag = user_data;
+ gchar *ctag;
+
+ g_return_val_if_fail (out_ctag != NULL, FALSE);
+
+ ctag = e_xml_xpath_eval_as_string (xpath_ctx, "%s/CS:getctag", xpath_prop_prefix);
+
+ if (ctag && *ctag) {
+ *out_ctag = e_webdav_session_util_maybe_dequote (ctag);
+ } else {
+ g_free (ctag);
+ }
+ }
+
+ return FALSE;
+}
+
+/**
+ * e_webdav_session_getctag_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @out_ctag: (out) (transfer full): return location for the ctag
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Issues a getctag property request for a collection identified by @uri, or,
+ * in case it's %NULL, on the URI defined in associated #ESource. The ctag is
+ * a collection tag, which changes whenever the collection changes (similar
+ * to etag). The getctag is an extension, thus the function can fail when
+ * the server doesn't support it.
+ *
+ * Free the returned @out_ctag with g_free(), when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_getctag_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ gchar **out_ctag,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EXmlDocument *xml;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (out_ctag != NULL, FALSE);
+
+ *out_ctag = NULL;
+
+ xml = e_xml_document_new (E_WEBDAV_NS_DAV, "propfind");
+ g_return_val_if_fail (xml != NULL, FALSE);
+
+ e_xml_document_add_namespaces (xml, "CS", E_WEBDAV_NS_CALENDARSERVER, NULL);
+
+ e_xml_document_start_element (xml, NULL, "prop");
+ e_xml_document_add_empty_element (xml, E_WEBDAV_NS_CALENDARSERVER, "getctag");
+ e_xml_document_end_element (xml); /* prop */
+
+ success = e_webdav_session_propfind_sync (webdav, uri, E_WEBDAV_DEPTH_THIS, xml,
+ e_webdav_session_getctag_cb, out_ctag, cancellable, error);
+
+ g_object_unref (xml);
+
+ return success && *out_ctag != NULL;
+}
+
+static EWebDAVResourceKind
+e_webdav_session_extract_kind (xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prop_prefix)
+{
+ g_return_val_if_fail (xpath_ctx != NULL, E_WEBDAV_RESOURCE_KIND_UNKNOWN);
+ g_return_val_if_fail (xpath_prop_prefix != NULL, E_WEBDAV_RESOURCE_KIND_UNKNOWN);
+
+ if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:resourcetype/A:addressbook", xpath_prop_prefix))
+ return E_WEBDAV_RESOURCE_KIND_ADDRESSBOOK;
+
+ if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:resourcetype/C:calendar", xpath_prop_prefix))
+ return E_WEBDAV_RESOURCE_KIND_CALENDAR;
+
+ if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:resourcetype/D:principal", xpath_prop_prefix))
+ return E_WEBDAV_RESOURCE_KIND_PRINCIPAL;
+
+ if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:resourcetype/D:collection", xpath_prop_prefix))
+ return E_WEBDAV_RESOURCE_KIND_COLLECTION;
+
+ return E_WEBDAV_RESOURCE_KIND_RESOURCE;
+}
+
+static guint32
+e_webdav_session_extract_supports (xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prop_prefix)
+{
+ guint32 supports = E_WEBDAV_RESOURCE_SUPPORTS_NONE;
+
+ g_return_val_if_fail (xpath_ctx != NULL, E_WEBDAV_RESOURCE_SUPPORTS_NONE);
+ g_return_val_if_fail (xpath_prop_prefix != NULL, E_WEBDAV_RESOURCE_SUPPORTS_NONE);
+
+ if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:resourcetype/A:addressbook", xpath_prop_prefix))
+ supports = supports | E_WEBDAV_RESOURCE_SUPPORTS_CONTACTS;
+
+ if (e_xml_xpath_eval_exists (xpath_ctx, "%s/C:supported-calendar-component-set", xpath_prop_prefix)) {
+ xmlXPathObjectPtr xpath_obj;
+
+ xpath_obj = e_xml_xpath_eval (xpath_ctx, "%s/C:supported-calendar-component-set/C:comp",
xpath_prop_prefix);
+ if (xpath_obj) {
+ gint ii, length;
+
+ length = xmlXPathNodeSetGetLength (xpath_obj->nodesetval);
+
+ for (ii = 0; ii < length; ii++) {
+ gchar *name;
+
+ name = e_xml_xpath_eval_as_string (xpath_ctx,
"%s/C:supported-calendar-component-set/C:comp[%d]/@name",
+ xpath_prop_prefix, ii + 1);
+
+ if (!name)
+ continue;
+
+ if (g_ascii_strcasecmp (name, "VEVENT") == 0)
+ supports |= E_WEBDAV_RESOURCE_SUPPORTS_EVENTS;
+ else if (g_ascii_strcasecmp (name, "VJOURNAL") == 0)
+ supports |= E_WEBDAV_RESOURCE_SUPPORTS_MEMOS;
+ else if (g_ascii_strcasecmp (name, "VTODO") == 0)
+ supports |= E_WEBDAV_RESOURCE_SUPPORTS_TASKS;
+ else if (g_ascii_strcasecmp (name, "VFREEBUSY") == 0)
+ supports |= E_WEBDAV_RESOURCE_SUPPORTS_FREEBUSY;
+ else if (g_ascii_strcasecmp (name, "VTIMEZONE") == 0)
+ supports |= E_WEBDAV_RESOURCE_SUPPORTS_TIMEZONE;
+
+ g_free (name);
+ }
+
+ xmlXPathFreeObject (xpath_obj);
+ } else {
+ /* If the property is not present, assume all component
+ * types are supported. (RFC 4791, Section 5.2.3) */
+ supports = supports |
+ E_WEBDAV_RESOURCE_SUPPORTS_EVENTS |
+ E_WEBDAV_RESOURCE_SUPPORTS_MEMOS |
+ E_WEBDAV_RESOURCE_SUPPORTS_TASKS |
+ E_WEBDAV_RESOURCE_SUPPORTS_FREEBUSY |
+ E_WEBDAV_RESOURCE_SUPPORTS_TIMEZONE;
+ }
+ }
+
+ return supports;
+}
+
+static gchar *
+e_webdav_session_extract_nonempty (xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prop_prefix,
+ const gchar *prop,
+ const gchar *alternative_prop)
+{
+ gchar *value;
+
+ g_return_val_if_fail (xpath_ctx != NULL, NULL);
+ g_return_val_if_fail (xpath_prop_prefix != NULL, NULL);
+ g_return_val_if_fail (prop != NULL, NULL);
+
+ value = e_xml_xpath_eval_as_string (xpath_ctx, "%s/%s", xpath_prop_prefix, prop);
+ if (!value && alternative_prop)
+ value = e_xml_xpath_eval_as_string (xpath_ctx, "%s/%s", xpath_prop_prefix, alternative_prop);
+ if (!value)
+ return NULL;
+
+ if (!*value) {
+ g_free (value);
+ return NULL;
+ }
+
+ return e_webdav_session_util_maybe_dequote (value);
+}
+
+static gsize
+e_webdav_session_extract_content_length (xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prop_prefix)
+{
+ gchar *value;
+ gsize length;
+
+ g_return_val_if_fail (xpath_ctx != NULL, 0);
+ g_return_val_if_fail (xpath_prop_prefix != NULL, 0);
+
+ value = e_webdav_session_extract_nonempty (xpath_ctx, xpath_prop_prefix, "D:getcontentlength", NULL);
+ if (!value)
+ return 0;
+
+ length = g_ascii_strtoll (value, NULL, 10);
+
+ g_free (value);
+
+ return length;
+}
+
+static glong
+e_webdav_session_extract_datetime (xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prop_prefix,
+ const gchar *prop,
+ gboolean is_iso_property)
+{
+ gchar *value;
+ GTimeVal tv;
+
+ g_return_val_if_fail (xpath_ctx != NULL, -1);
+ g_return_val_if_fail (xpath_prop_prefix != NULL, -1);
+
+ value = e_webdav_session_extract_nonempty (xpath_ctx, xpath_prop_prefix, prop, NULL);
+ if (!value)
+ return -1;
+
+ if (is_iso_property && !g_time_val_from_iso8601 (value, &tv)) {
+ tv.tv_sec = -1;
+ } else if (!is_iso_property) {
+ tv.tv_sec = camel_header_decode_date (value, NULL);
+ }
+
+ g_free (value);
+
+ return tv.tv_sec;
+}
+
+static gboolean
+e_webdav_session_list_cb (EWebDAVSession *webdav,
+ xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prop_prefix,
+ const SoupURI *request_uri,
+ const gchar *href,
+ guint status_code,
+ gpointer user_data)
+{
+ GSList **out_resources = user_data;
+
+ g_return_val_if_fail (out_resources != NULL, FALSE);
+ g_return_val_if_fail (request_uri != NULL, FALSE);
+
+ if (!xpath_prop_prefix) {
+ e_xml_xpath_context_register_namespaces (xpath_ctx,
+ "CS", E_WEBDAV_NS_CALENDARSERVER,
+ "C", E_WEBDAV_NS_CALDAV,
+ "A", E_WEBDAV_NS_CARDDAV,
+ "IC", E_WEBDAV_NS_ICAL,
+ NULL);
+
+ return TRUE;
+ }
+
+ if (status_code == SOUP_STATUS_OK) {
+ EWebDAVResource *resource;
+ EWebDAVResourceKind kind;
+ guint32 supports;
+ gchar *etag;
+ gchar *display_name;
+ gchar *content_type;
+ gsize content_length;
+ glong creation_date;
+ glong last_modified;
+ gchar *description;
+ gchar *color;
+
+ kind = e_webdav_session_extract_kind (xpath_ctx, xpath_prop_prefix);
+ if (kind == E_WEBDAV_RESOURCE_KIND_UNKNOWN)
+ return TRUE;
+
+ supports = e_webdav_session_extract_supports (xpath_ctx, xpath_prop_prefix);
+ etag = e_webdav_session_extract_nonempty (xpath_ctx, xpath_prop_prefix, "D:getetag",
"CS:getctag");
+ display_name = e_webdav_session_extract_nonempty (xpath_ctx, xpath_prop_prefix,
"D:displayname", NULL);
+ content_type = e_webdav_session_extract_nonempty (xpath_ctx, xpath_prop_prefix,
"D:getcontenttype", NULL);
+ content_length = e_webdav_session_extract_content_length (xpath_ctx, xpath_prop_prefix);
+ creation_date = e_webdav_session_extract_datetime (xpath_ctx, xpath_prop_prefix,
"D:creationdate", TRUE);
+ last_modified = e_webdav_session_extract_datetime (xpath_ctx, xpath_prop_prefix,
"D:getlastmodified", FALSE);
+ description = e_webdav_session_extract_nonempty (xpath_ctx, xpath_prop_prefix,
"C:calendar-description", "A:addressbook-description");
+ color = e_webdav_session_extract_nonempty (xpath_ctx, xpath_prop_prefix, "IC:calendar-color",
NULL);
+
+ resource = e_webdav_resource_new (kind, supports,
+ href,
+ NULL, /* etag */
+ NULL, /* display_name */
+ NULL, /* content_type */
+ content_length,
+ creation_date,
+ last_modified,
+ NULL, /* description */
+ NULL); /* color */
+ resource->etag = etag;
+ resource->display_name = display_name;
+ resource->content_type = content_type;
+ resource->description = description;
+ resource->color = color;
+
+ *out_resources = g_slist_prepend (*out_resources, resource);
+ }
+
+ return TRUE;
+}
+
+/**
+ * e_webdav_session_list_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @depth: requested depth, can be one of %E_WEBDAV_DEPTH_THIS, %E_WEBDAV_DEPTH_THIS_AND_CHILDREN or
%E_WEBDAV_DEPTH_INFINITY
+ * @flags: a bit-or of #EWebDAVListFlags, claiming what properties to read
+ * @out_resources: (out) (transfer full) (element-type EWebDAVResource): return location for the resources
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Lists content of the @uri, or, in case it's %NULL, of the URI defined
+ * in associated #ESource, which should point to a collection. The @flags
+ * influences which properties are read for the resources.
+ *
+ * The @out_resources is in no particular order.
+ *
+ * Free the returned @out_resources with
+ * g_slist_free_full (resources, e_webdav_resource_free);
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_list_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const gchar *depth,
+ guint32 flags,
+ GSList **out_resources,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EXmlDocument *xml;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (out_resources != NULL, FALSE);
+
+ *out_resources = NULL;
+
+ xml = e_xml_document_new (E_WEBDAV_NS_DAV, "propfind");
+ g_return_val_if_fail (xml != NULL, FALSE);
+
+ e_xml_document_start_element (xml, NULL, "prop");
+
+ e_xml_document_add_empty_element (xml, NULL, "resourcetype");
+
+ if ((flags & E_WEBDAV_LIST_SUPPORTS) != 0 ||
+ (flags & E_WEBDAV_LIST_DESCRIPTION) != 0 ||
+ (flags & E_WEBDAV_LIST_COLOR) != 0) {
+ e_xml_document_add_namespaces (xml, "C", E_WEBDAV_NS_CALDAV, NULL);
+ }
+
+ if ((flags & E_WEBDAV_LIST_SUPPORTS) != 0) {
+ e_xml_document_add_empty_element (xml, E_WEBDAV_NS_CALDAV,
"supported-calendar-component-set");
+ }
+
+ if ((flags & E_WEBDAV_LIST_DISPLAY_NAME) != 0) {
+ e_xml_document_add_empty_element (xml, NULL, "displayname");
+ }
+
+ if ((flags & E_WEBDAV_LIST_ETAG) != 0) {
+ e_xml_document_add_empty_element (xml, NULL, "getetag");
+
+ e_xml_document_add_namespaces (xml, "CS", E_WEBDAV_NS_CALENDARSERVER, NULL);
+
+ e_xml_document_add_empty_element (xml, E_WEBDAV_NS_CALENDARSERVER, "getctag");
+ }
+
+ if ((flags & E_WEBDAV_LIST_CONTENT_TYPE) != 0) {
+ e_xml_document_add_empty_element (xml, NULL, "getcontenttype");
+ }
+
+ if ((flags & E_WEBDAV_LIST_CONTENT_LENGTH) != 0) {
+ e_xml_document_add_empty_element (xml, NULL, "getcontentlength");
+ }
+
+ if ((flags & E_WEBDAV_LIST_CREATION_DATE) != 0) {
+ e_xml_document_add_empty_element (xml, NULL, "creationdate");
+ }
+
+ if ((flags & E_WEBDAV_LIST_LAST_MODIFIED) != 0) {
+ e_xml_document_add_empty_element (xml, NULL, "getlastmodified");
+ }
+
+ if ((flags & E_WEBDAV_LIST_DESCRIPTION) != 0) {
+ e_xml_document_add_empty_element (xml, E_WEBDAV_NS_CALDAV, "calendar-description");
+
+ e_xml_document_add_namespaces (xml, "A", E_WEBDAV_NS_CARDDAV, NULL);
+
+ e_xml_document_add_empty_element (xml, E_WEBDAV_NS_CARDDAV, "addressbook-description");
+ }
+
+ if ((flags & E_WEBDAV_LIST_COLOR) != 0) {
+ e_xml_document_add_namespaces (xml, "IC", E_WEBDAV_NS_ICAL, NULL);
+
+ e_xml_document_add_empty_element (xml, E_WEBDAV_NS_ICAL, "calendar-color");
+ }
+
+ e_xml_document_end_element (xml); /* prop */
+
+ success = e_webdav_session_propfind_sync (webdav, uri, depth, xml,
+ e_webdav_session_list_cb, out_resources, cancellable, error);
+
+ g_object_unref (xml);
+
+ /* Ensure display name in case the resource doesn't have any */
+ if (success && (flags & E_WEBDAV_LIST_DISPLAY_NAME) != 0) {
+ GSList *link;
+
+ for (link = *out_resources; link; link = g_slist_next (link)) {
+ EWebDAVResource *resource = link->data;
+
+ if (resource && !resource->display_name && resource->href) {
+ gchar *href_decoded = soup_uri_decode (resource->href);
+
+ if (href_decoded) {
+ gchar *cp;
+
+ /* Use the last non-empty path segment. */
+ while ((cp = strrchr (href_decoded, '/')) != NULL) {
+ if (*(cp + 1) == '\0')
+ *cp = '\0';
+ else {
+ resource->display_name = g_strdup (cp + 1);
+ break;
+ }
+ }
+ }
+
+ g_free (href_decoded);
+ }
+ }
+ }
+
+ if (success) {
+ /* Honour order returned by the server, even it's not significant. */
+ *out_resources = g_slist_reverse (*out_resources);
+ }
+
+ return success;
+}
+
+/**
+ * e_webdav_session_update_properties_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @changes: (element-type EWebDAVResource): a #GSList with request changes
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Updates properties (set/remove) on the provided @uri, or, in case it's %NULL,
+ * on the URI defined in associated #ESource, with the @changes. The order
+ * of @changes is significant, unlike on other places.
+ *
+ * This function supports only flat properties, those not under other element.
+ * To support more complex property tries use e_webdav_session_proppatch_sync()
+ * directly.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_update_properties_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const GSList *changes,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EXmlDocument *xml;
+ GSList *link;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (changes != NULL, FALSE);
+
+ xml = e_xml_document_new (E_WEBDAV_NS_DAV, "propertyupdate");
+ g_return_val_if_fail (xml != NULL, FALSE);
+
+ for (link = (GSList *) changes; link; link = g_slist_next (link)) {
+ EWebDAVPropertyChange *change = link->data;
+
+ if (!change)
+ continue;
+
+ switch (change->kind) {
+ case E_WEBDAV_PROPERTY_SET:
+ e_xml_document_start_element (xml, NULL, "set");
+ e_xml_document_start_element (xml, NULL, "prop");
+ e_xml_document_start_text_element (xml, change->ns_uri, change->name);
+ if (change->value) {
+ e_xml_document_write_string (xml, change->value);
+ }
+ e_xml_document_end_element (xml); /* change->name */
+ e_xml_document_end_element (xml); /* prop */
+ e_xml_document_end_element (xml); /* set */
+ break;
+ case E_WEBDAV_PROPERTY_REMOVE:
+ e_xml_document_start_element (xml, NULL, "remove");
+ e_xml_document_start_element (xml, NULL, "prop");
+ e_xml_document_add_empty_element (xml, change->ns_uri, change->name);
+ e_xml_document_end_element (xml); /* prop */
+ e_xml_document_end_element (xml); /* remove */
+ break;
+ }
+ }
+
+ success = e_webdav_session_proppatch_sync (webdav, uri, xml, cancellable, error);
+
+ g_object_unref (xml);
+
+ return success;
+}
+
+/**
+ * e_webdav_session_lock_resource_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to lock, or %NULL to read from #ESource
+ * @lock_scope: an #EWebDAVLockScope to define the scope of the lock
+ * @lock_timeout: timeout for the lock, in seconds, on 0 to infinity
+ * @owner: (nullable): optional identificator of the owner of the lock, or %NULL
+ * @out_lock_token: (out) (transfer full): return location of the obtained or refreshed lock token
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Locks a resource identified by @uri, or, in case it's %NULL, by the URI defined
+ * in associated #ESource. It obtains a write lock with the given @lock_scope.
+ *
+ * The @owner is used to identify the lock owner. When it's an http:// or https://,
+ * then it's referenced as DAV:href, otherwise the value is treated as plain text.
+ * If it's %NULL, then the user name from the associated #ESource is used.
+ *
+ * The @out_lock_token can be refreshed with e_webdav_session_refresh_lock_sync().
+ * Release the lock with e_webdav_session_unlock_sync().
+ * Free the returned @out_lock_token with g_free(), when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_lock_resource_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ EWebDAVLockScope lock_scope,
+ gint32 lock_timeout,
+ const gchar *owner,
+ gchar **out_lock_token,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EXmlDocument *xml;
+ gchar *owner_ref;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (out_lock_token != NULL, FALSE);
+
+ xml = e_xml_document_new (E_WEBDAV_NS_DAV, "lockinfo");
+ g_return_val_if_fail (xml != NULL, FALSE);
+
+ e_xml_document_start_element (xml, NULL, "lockscope");
+ switch (lock_scope) {
+ case E_WEBDAV_LOCK_EXCLUSIVE:
+ e_xml_document_add_empty_element (xml, NULL, "exclusive");
+ break;
+ case E_WEBDAV_LOCK_SHARED:
+ e_xml_document_add_empty_element (xml, NULL, "shared");
+ break;
+ }
+ e_xml_document_end_element (xml); /* lockscope */
+
+ e_xml_document_start_element (xml, NULL, "locktype");
+ e_xml_document_add_empty_element (xml, NULL, "write");
+ e_xml_document_end_element (xml); /* locktype */
+
+ e_xml_document_start_text_element (xml, NULL, "owner");
+ if (owner) {
+ owner_ref = g_strdup (owner);
+ } else {
+ ESource *source = e_soup_session_get_source (E_SOUP_SESSION (webdav));
+
+ owner_ref = NULL;
+
+ if (e_source_has_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION)) {
+ owner_ref = e_source_authentication_dup_user (
+ e_source_get_extension (source, E_SOURCE_EXTENSION_AUTHENTICATION));
+
+ if (owner_ref && !*owner_ref)
+ g_clear_pointer (&owner_ref, g_free);
+ }
+
+ if (!owner_ref && e_source_has_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND)) {
+ owner_ref = e_source_webdav_dup_email_address (
+ e_source_get_extension (source, E_SOURCE_EXTENSION_WEBDAV_BACKEND));
+
+ if (owner_ref && !*owner_ref)
+ g_clear_pointer (&owner_ref, g_free);
+ }
+ }
+
+ if (!owner_ref)
+ owner_ref = g_strconcat (g_get_host_name (), " / ", g_get_user_name (), NULL);
+
+ if (owner_ref) {
+ if (g_str_has_prefix (owner_ref, "http://") ||
+ g_str_has_prefix (owner_ref, "https://")) {
+ e_xml_document_start_element (xml, NULL, "href");
+ e_xml_document_write_string (xml, owner_ref);
+ e_xml_document_end_element (xml); /* href */
+ } else {
+ e_xml_document_write_string (xml, owner_ref);
+ }
+ }
+
+ g_free (owner_ref);
+ e_xml_document_end_element (xml); /* owner */
+
+ success = e_webdav_session_lock_sync (webdav, uri, E_WEBDAV_DEPTH_INFINITY, lock_timeout, xml,
+ out_lock_token, NULL, cancellable, error);
+
+ g_object_unref (xml);
+
+ return success;
+}
+
+static void
+e_webdav_session_traverse_privilege_level (xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prefix,
+ GNode *parent)
+{
+ xmlXPathObjectPtr xpath_obj;
+
+ g_return_if_fail (xpath_ctx != NULL);
+ g_return_if_fail (xpath_prefix != NULL);
+ g_return_if_fail (parent != NULL);
+
+ xpath_obj = e_xml_xpath_eval (xpath_ctx, "%s/D:supported-privilege", xpath_prefix);
+
+ if (xpath_obj) {
+ gint ii, length;
+
+ length = xmlXPathNodeSetGetLength (xpath_obj->nodesetval);
+
+ for (ii = 0; ii < length; ii++) {
+ xmlXPathObjectPtr xpath_obj_privilege;
+ gchar *prefix;
+
+ prefix = g_strdup_printf ("%s/D:supported-privilege[%d]", xpath_prefix, ii + 1);
+ xpath_obj_privilege = e_xml_xpath_eval (xpath_ctx, "%s/D:privilege", prefix);
+
+ if (xpath_obj_privilege &&
+ xpath_obj_privilege->type == XPATH_NODESET &&
+ xpath_obj_privilege->nodesetval &&
+ xpath_obj_privilege->nodesetval->nodeNr == 1 &&
+ xpath_obj_privilege->nodesetval->nodeTab &&
+ xpath_obj_privilege->nodesetval->nodeTab[0] &&
+ xpath_obj_privilege->nodesetval->nodeTab[0]->children) {
+ xmlNodePtr node;
+
+ for (node = xpath_obj_privilege->nodesetval->nodeTab[0]->children; node; node
= node->next) {
+ if (node->type == XML_ELEMENT_NODE &&
+ node->name && *(node->name) &&
+ node->ns && node->ns->href && *(node->ns->href)) {
+ break;
+ }
+ }
+
+ if (node) {
+ GNode *child;
+ gchar *description;
+ EWebDAVPrivilegeKind kind = E_WEBDAV_PRIVILEGE_KIND_COMMON;
+ EWebDAVPrivilegeHint hint = E_WEBDAV_PRIVILEGE_HINT_UNKNOWN;
+ EWebDAVPrivilege *privilege;
+
+ if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:abstract", prefix))
+ kind = E_WEBDAV_PRIVILEGE_KIND_ABSTRACT;
+ else if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:aggregate",
prefix))
+ kind = E_WEBDAV_PRIVILEGE_KIND_AGGREGATE;
+
+ description = e_xml_xpath_eval_as_string (xpath_ctx,
"%s/D:description", prefix);
+ privilege = e_webdav_privilege_new ((const gchar *) node->ns->href,
(const gchar *) node->name, description, kind, hint);
+ child = g_node_new (privilege);
+ g_node_append (parent, child);
+
+ g_free (description);
+
+ if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:supported-privilege",
prefix))
+ e_webdav_session_traverse_privilege_level (xpath_ctx, prefix,
child);
+ }
+ }
+
+ if (xpath_obj_privilege)
+ xmlXPathFreeObject (xpath_obj_privilege);
+
+ g_free (prefix);
+ }
+
+ xmlXPathFreeObject (xpath_obj);
+ }
+}
+
+static gboolean
+e_webdav_session_supported_privilege_set_cb (EWebDAVSession *webdav,
+ xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prop_prefix,
+ const SoupURI *request_uri,
+ const gchar *href,
+ guint status_code,
+ gpointer user_data)
+{
+ GNode **out_privileges = user_data;
+
+ g_return_val_if_fail (out_privileges != NULL, FALSE);
+
+ if (!xpath_prop_prefix) {
+ e_xml_xpath_context_register_namespaces (xpath_ctx,
+ "C", E_WEBDAV_NS_CALDAV,
+ NULL);
+ } else if (status_code == SOUP_STATUS_OK &&
+ e_xml_xpath_eval_exists (xpath_ctx, "%s/D:supported-privilege-set/D:supported-privilege",
xpath_prop_prefix)) {
+ GNode *root;
+ gchar *prefix;
+
+ prefix = g_strconcat (xpath_prop_prefix, "/D:supported-privilege-set", NULL);
+ root = g_node_new (NULL);
+
+ e_webdav_session_traverse_privilege_level (xpath_ctx, prefix, root);
+
+ *out_privileges = root;
+
+ g_free (prefix);
+ }
+
+ return TRUE;
+}
+
+/**
+ * e_webdav_session_acl_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @xml:the request itself, as an #EXmlDocument, the root element should be DAV:acl
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Issues ACL request on the provided @uri, or, in case it's %NULL, on the URI
+ * defined in associated #ESource.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_acl_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const EXmlDocument *xml,
+ GCancellable *cancellable,
+ GError **error)
+{
+ SoupRequestHTTP *request;
+ SoupMessage *message;
+ GByteArray *bytes;
+ gchar *content;
+ gsize content_length;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (E_IS_XML_DOCUMENT (xml), FALSE);
+
+ request = e_webdav_session_new_request (webdav, "ACL", uri, error);
+ if (!request)
+ return FALSE;
+
+ message = soup_request_http_get_message (request);
+ if (!message) {
+ g_warn_if_fail (message != NULL);
+ g_object_unref (request);
+
+ return FALSE;
+ }
+
+ content = e_xml_document_get_content (xml, &content_length);
+ if (!content) {
+ g_object_unref (message);
+ g_object_unref (request);
+
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, _("Failed to get input
XML content"));
+
+ return FALSE;
+ }
+
+ soup_message_set_request (message, E_WEBDAV_CONTENT_TYPE_XML,
+ SOUP_MEMORY_TAKE, content, content_length);
+
+ bytes = e_soup_session_send_request_simple_sync (E_SOUP_SESSION (webdav), request, cancellable,
error);
+
+ success = !e_webdav_session_replace_with_detailed_error (webdav, request, bytes, TRUE, _("Failed to
get access control list"), error) &&
+ bytes != NULL;
+
+ if (bytes)
+ g_byte_array_free (bytes, TRUE);
+ g_object_unref (message);
+ g_object_unref (request);
+
+ return success;
+}
+
+/**
+ * e_webdav_session_get_supported_privilege_set_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @out_privileges: (out) (transfer full) (element-type EWebDAVPrivilege): return location for the tree of
supported privileges
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets supported privileges for the @uri, or, in case it's %NULL, for the URI
+ * defined in associated #ESource.
+ *
+ * The root node of @out_privileges has always %NULL data.
+ *
+ * Free the returned @out_privileges with e_webdav_session_util_free_privileges()
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_get_supported_privilege_set_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ GNode **out_privileges,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EXmlDocument *xml;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (out_privileges != NULL, FALSE);
+
+ *out_privileges = NULL;
+
+ xml = e_xml_document_new (E_WEBDAV_NS_DAV, "propfind");
+ g_return_val_if_fail (xml != NULL, FALSE);
+
+ e_xml_document_start_element (xml, NULL, "prop");
+ e_xml_document_add_empty_element (xml, NULL, "supported-privilege-set");
+ e_xml_document_end_element (xml); /* prop */
+
+ success = e_webdav_session_propfind_sync (webdav, uri, E_WEBDAV_DEPTH_THIS, xml,
+ e_webdav_session_supported_privilege_set_cb, out_privileges, cancellable, error);
+
+ g_object_unref (xml);
+
+ return success;
+}
+
+static EWebDAVPrivilege *
+e_webdav_session_extract_privilege_simple (xmlXPathObjectPtr xpath_obj_privilege)
+{
+ EWebDAVPrivilege *privilege = NULL;
+
+ if (xpath_obj_privilege &&
+ xpath_obj_privilege->type == XPATH_NODESET &&
+ xpath_obj_privilege->nodesetval &&
+ xpath_obj_privilege->nodesetval->nodeNr == 1 &&
+ xpath_obj_privilege->nodesetval->nodeTab &&
+ xpath_obj_privilege->nodesetval->nodeTab[0] &&
+ xpath_obj_privilege->nodesetval->nodeTab[0]->children) {
+ xmlNodePtr node;
+
+ for (node = xpath_obj_privilege->nodesetval->nodeTab[0]->children; node; node = node->next) {
+ if (node->type == XML_ELEMENT_NODE &&
+ node->name && *(node->name) &&
+ node->ns && node->ns->href && *(node->ns->href)) {
+ break;
+ }
+ }
+
+ if (node) {
+ privilege = e_webdav_privilege_new ((const gchar *) node->ns->href, (const gchar *)
node->name,
+ NULL, E_WEBDAV_PRIVILEGE_KIND_COMMON, E_WEBDAV_PRIVILEGE_HINT_UNKNOWN);
+ }
+ }
+
+ return privilege;
+}
+
+static gboolean
+e_webdav_session_current_user_privilege_set_cb (EWebDAVSession *webdav,
+ xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prop_prefix,
+ const SoupURI *request_uri,
+ const gchar *href,
+ guint status_code,
+ gpointer user_data)
+{
+ GSList **out_privileges = user_data;
+
+ g_return_val_if_fail (xpath_ctx != NULL, FALSE);
+ g_return_val_if_fail (out_privileges != NULL, FALSE);
+
+ if (!xpath_prop_prefix) {
+ e_xml_xpath_context_register_namespaces (xpath_ctx,
+ "C", E_WEBDAV_NS_CALDAV,
+ NULL);
+ } else if (status_code == SOUP_STATUS_OK &&
+ e_xml_xpath_eval_exists (xpath_ctx, "%s/D:current-user-privilege-set/D:privilege",
xpath_prop_prefix)) {
+ xmlXPathObjectPtr xpath_obj;
+
+ xpath_obj = e_xml_xpath_eval (xpath_ctx, "%s/D:current-user-privilege-set/D:privilege",
xpath_prop_prefix);
+
+ if (xpath_obj) {
+ gint ii, length;
+
+ length = xmlXPathNodeSetGetLength (xpath_obj->nodesetval);
+
+ for (ii = 0; ii < length; ii++) {
+ xmlXPathObjectPtr xpath_obj_privilege;
+
+ xpath_obj_privilege = e_xml_xpath_eval (xpath_ctx,
"%s/D:current-user-privilege-set/D:privilege[%d]", xpath_prop_prefix, ii + 1);
+
+ if (xpath_obj_privilege) {
+ EWebDAVPrivilege *privilege;
+
+ privilege = e_webdav_session_extract_privilege_simple
(xpath_obj_privilege);
+ if (privilege)
+ *out_privileges = g_slist_prepend (*out_privileges,
privilege);
+
+ xmlXPathFreeObject (xpath_obj_privilege);
+ }
+ }
+
+ xmlXPathFreeObject (xpath_obj);
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * e_webdav_session_get_current_user_privilege_set_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @out_privileges: (out) (transfer full) (element-type EWebDAVPrivilege): return location for a %GSList of
#EWebDAVPrivilege
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets current user privileges for the @uri, or, in case it's %NULL, for the URI
+ * defined in associated #ESource.
+ *
+ * Free the returned @out_privileges with
+ * g_slist_free_full (privileges, e_webdav_privilege_free);
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_get_current_user_privilege_set_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ GSList **out_privileges,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EXmlDocument *xml;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (out_privileges != NULL, FALSE);
+
+ *out_privileges = NULL;
+
+ xml = e_xml_document_new (E_WEBDAV_NS_DAV, "propfind");
+ g_return_val_if_fail (xml != NULL, FALSE);
+
+ e_xml_document_start_element (xml, NULL, "prop");
+ e_xml_document_add_empty_element (xml, NULL, "current-user-privilege-set");
+ e_xml_document_end_element (xml); /* prop */
+
+ success = e_webdav_session_propfind_sync (webdav, uri, E_WEBDAV_DEPTH_THIS, xml,
+ e_webdav_session_current_user_privilege_set_cb, out_privileges, cancellable, error);
+
+ g_object_unref (xml);
+
+ if (success)
+ *out_privileges = g_slist_reverse (*out_privileges);
+
+ return success;
+}
+
+static EWebDAVACEPrincipalKind
+e_webdav_session_extract_acl_principal (xmlXPathContextPtr xpath_ctx,
+ const gchar *principal_prefix,
+ gchar **out_principal_href,
+ GSList **out_principal_hrefs)
+{
+ g_return_val_if_fail (xpath_ctx != NULL, E_WEBDAV_ACE_PRINCIPAL_UNKNOWN);
+ g_return_val_if_fail (principal_prefix != NULL, E_WEBDAV_ACE_PRINCIPAL_UNKNOWN);
+ g_return_val_if_fail (out_principal_href != NULL || out_principal_hrefs != NULL,
E_WEBDAV_ACE_PRINCIPAL_UNKNOWN);
+
+ *out_principal_href = NULL;
+
+ if (!e_xml_xpath_eval_exists (xpath_ctx, "%s", principal_prefix))
+ return E_WEBDAV_ACE_PRINCIPAL_UNKNOWN;
+
+ if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:href", principal_prefix)) {
+ if (out_principal_href) {
+ *out_principal_href = e_xml_xpath_eval_as_string (xpath_ctx, "%s/D:href",
principal_prefix);
+ } else {
+ xmlXPathObjectPtr xpath_obj;
+
+ *out_principal_hrefs = NULL;
+
+ xpath_obj = e_xml_xpath_eval (xpath_ctx, "%s/D:href", principal_prefix);
+
+ if (xpath_obj) {
+ gint ii, length;
+
+ length = xmlXPathNodeSetGetLength (xpath_obj->nodesetval);
+
+ for (ii = 0; ii < length; ii++) {
+ gchar *href;
+
+ href = e_xml_xpath_eval_as_string (xpath_ctx, "%s/D:href[%d]",
principal_prefix, ii + 1);
+ if (href)
+ *out_principal_hrefs = g_slist_prepend (*out_principal_hrefs,
href);
+ }
+ }
+
+ *out_principal_hrefs = g_slist_reverse (*out_principal_hrefs);
+ }
+
+ return E_WEBDAV_ACE_PRINCIPAL_HREF;
+ }
+
+ if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:all", principal_prefix))
+ return E_WEBDAV_ACE_PRINCIPAL_ALL;
+
+ if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:authenticated", principal_prefix))
+ return E_WEBDAV_ACE_PRINCIPAL_AUTHENTICATED;
+
+ if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:unauthenticated", principal_prefix))
+ return E_WEBDAV_ACE_PRINCIPAL_UNAUTHENTICATED;
+
+ if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:self", principal_prefix))
+ return E_WEBDAV_ACE_PRINCIPAL_SELF;
+
+ if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:property", principal_prefix)) {
+ /* No details read about what properties */
+ EWebDAVACEPrincipalKind kind = E_WEBDAV_ACE_PRINCIPAL_PROPERTY;
+
+ /* Special-case owner */
+ if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:property/D:owner", principal_prefix)) {
+ xmlXPathObjectPtr xpath_obj_property;
+
+ xpath_obj_property = e_xml_xpath_eval (xpath_ctx, "%s/D:property", principal_prefix);
+
+ /* DAV:owner is the only child and there is only one DAV:property child of the
DAV:principal */
+ if (xpath_obj_property &&
+ xpath_obj_property->type == XPATH_NODESET &&
+ xmlXPathNodeSetGetLength (xpath_obj_property->nodesetval) == 1 &&
+ xpath_obj_property->nodesetval &&
+ xpath_obj_property->nodesetval->nodeNr == 1 &&
+ xpath_obj_property->nodesetval->nodeTab &&
+ xpath_obj_property->nodesetval->nodeTab[0] &&
+ xpath_obj_property->nodesetval->nodeTab[0]->children) {
+ xmlNodePtr node;
+ gint subelements = 0;
+
+ for (node = xpath_obj_property->nodesetval->nodeTab[0]->children; node &&
subelements <= 1; node = node->next) {
+ if (node->type == XML_ELEMENT_NODE)
+ subelements++;
+ }
+
+ if (subelements == 1)
+ kind = E_WEBDAV_ACE_PRINCIPAL_OWNER;
+ }
+
+ if (xpath_obj_property)
+ xmlXPathFreeObject (xpath_obj_property);
+ }
+
+ return kind;
+ }
+
+ return E_WEBDAV_ACE_PRINCIPAL_UNKNOWN;
+}
+
+static gboolean
+e_webdav_session_acl_cb (EWebDAVSession *webdav,
+ xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prop_prefix,
+ const SoupURI *request_uri,
+ const gchar *href,
+ guint status_code,
+ gpointer user_data)
+{
+ GSList **out_entries = user_data;
+
+ g_return_val_if_fail (xpath_ctx != NULL, FALSE);
+ g_return_val_if_fail (out_entries != NULL, FALSE);
+
+ if (!xpath_prop_prefix) {
+ } else if (status_code == SOUP_STATUS_OK &&
+ e_xml_xpath_eval_exists (xpath_ctx, "%s/D:acl/D:ace", xpath_prop_prefix)) {
+ xmlXPathObjectPtr xpath_obj_ace;
+
+ xpath_obj_ace = e_xml_xpath_eval (xpath_ctx, "%s/D:acl/D:ace", xpath_prop_prefix);
+
+ if (xpath_obj_ace) {
+ gint ii, length;
+
+ length = xmlXPathNodeSetGetLength (xpath_obj_ace->nodesetval);
+
+ for (ii = 0; ii < length; ii++) {
+ EWebDAVACEPrincipalKind principal_kind = E_WEBDAV_ACE_PRINCIPAL_UNKNOWN;
+ xmlXPathObjectPtr xpath_obj = NULL;
+ gchar *principal_href = NULL;
+ guint32 flags = E_WEBDAV_ACE_FLAG_UNKNOWN;
+ gchar *inherited_href = NULL;
+ gchar *privilege_prefix = NULL;
+ gchar *ace_prefix;
+
+ ace_prefix = g_strdup_printf ("%s/D:acl/D:ace[%d]", xpath_prop_prefix, ii +
1);
+
+ if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:invert", ace_prefix)) {
+ gchar *prefix;
+
+ flags |= E_WEBDAV_ACE_FLAG_INVERT;
+
+ prefix = g_strdup_printf ("%s/D:invert/D:principal", ace_prefix);
+ principal_kind = e_webdav_session_extract_acl_principal (xpath_ctx,
prefix, &principal_href, NULL);
+ g_free (prefix);
+ } else {
+ gchar *prefix;
+
+ prefix = g_strdup_printf ("%s/D:principal", ace_prefix);
+ principal_kind = e_webdav_session_extract_acl_principal (xpath_ctx,
prefix, &principal_href, NULL);
+ g_free (prefix);
+ }
+
+ if (principal_kind == E_WEBDAV_ACE_PRINCIPAL_UNKNOWN) {
+ g_free (ace_prefix);
+ continue;
+ }
+
+ if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:protected", ace_prefix))
+ flags |= E_WEBDAV_ACE_FLAG_PROTECTED;
+
+ if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:inherited/D:href", ace_prefix))
{
+ flags |= E_WEBDAV_ACE_FLAG_INHERITED;
+ inherited_href = e_xml_xpath_eval_as_string (xpath_ctx,
"%s/D:inherited/D:href", ace_prefix);
+ }
+
+ if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:grant", ace_prefix)) {
+ privilege_prefix = g_strdup_printf ("%s/D:grant/D:privilege",
ace_prefix);
+ flags |= E_WEBDAV_ACE_FLAG_GRANT;
+ } else if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:deny", ace_prefix)) {
+ privilege_prefix = g_strdup_printf ("%s/D:deny/D:privilege",
ace_prefix);
+ flags |= E_WEBDAV_ACE_FLAG_DENY;
+ }
+
+ if (privilege_prefix)
+ xpath_obj = e_xml_xpath_eval (xpath_ctx, "%s", privilege_prefix);
+
+ if (xpath_obj) {
+ EWebDAVAccessControlEntry *ace;
+ gint ii, length;
+
+ ace = e_webdav_access_control_entry_new (principal_kind,
principal_href, flags, inherited_href);
+ if (ace) {
+ length = xmlXPathNodeSetGetLength (xpath_obj->nodesetval);
+
+ for (ii = 0; ii < length; ii++) {
+ xmlXPathObjectPtr xpath_obj_privilege;
+
+ xpath_obj_privilege = e_xml_xpath_eval (xpath_ctx,
"%s[%d]", privilege_prefix, ii + 1);
+
+ if (xpath_obj_privilege) {
+ EWebDAVPrivilege *privilege;
+
+ privilege =
e_webdav_session_extract_privilege_simple (xpath_obj_privilege);
+ if (privilege)
+ ace->privileges = g_slist_prepend
(ace->privileges, privilege);
+
+ xmlXPathFreeObject (xpath_obj_privilege);
+ }
+ }
+
+ ace->privileges = g_slist_reverse (ace->privileges);
+
+ *out_entries = g_slist_prepend (*out_entries, ace);
+ }
+
+ xmlXPathFreeObject (xpath_obj);
+ }
+
+ g_free (principal_href);
+ g_free (inherited_href);
+ g_free (privilege_prefix);
+ g_free (ace_prefix);
+ }
+
+ xmlXPathFreeObject (xpath_obj_ace);
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * e_webdav_session_get_acl_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @out_entries: (out) (transfer full) (element-type EWebDAVAccessControlEntry): return location for a
#GSList of #EWebDAVAccessControlEntry
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets Access Control List (ACL) for the @uri, or, in case it's %NULL, for the URI
+ * defined in associated #ESource.
+ *
+ * This function doesn't read general #E_WEBDAV_ACE_PRINCIPAL_PROPERTY.
+ *
+ * Free the returned @out_entries with
+ * g_slist_free_full (entries, e_webdav_access_control_entry_free);
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_get_acl_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ GSList **out_entries,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EXmlDocument *xml;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (out_entries != NULL, FALSE);
+
+ *out_entries = NULL;
+
+ xml = e_xml_document_new (E_WEBDAV_NS_DAV, "propfind");
+ g_return_val_if_fail (xml != NULL, FALSE);
+
+ e_xml_document_start_element (xml, NULL, "prop");
+ e_xml_document_add_empty_element (xml, NULL, "acl");
+ e_xml_document_end_element (xml); /* prop */
+
+ success = e_webdav_session_propfind_sync (webdav, uri, E_WEBDAV_DEPTH_THIS, xml,
+ e_webdav_session_acl_cb, out_entries, cancellable, error);
+
+ g_object_unref (xml);
+
+ if (success)
+ *out_entries = g_slist_reverse (*out_entries);
+
+ return success;
+}
+
+typedef struct _ACLRestrictionsData {
+ guint32 *out_restrictions;
+ EWebDAVACEPrincipalKind *out_principal_kind;
+ GSList **out_principal_hrefs;
+} ACLRestrictionsData;
+
+static gboolean
+e_webdav_session_acl_restrictions_cb (EWebDAVSession *webdav,
+ xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prop_prefix,
+ const SoupURI *request_uri,
+ const gchar *href,
+ guint status_code,
+ gpointer user_data)
+{
+ ACLRestrictionsData *ard = user_data;
+
+ g_return_val_if_fail (xpath_ctx != NULL, FALSE);
+ g_return_val_if_fail (ard != NULL, FALSE);
+
+ if (!xpath_prop_prefix) {
+ } else if (status_code == SOUP_STATUS_OK &&
+ e_xml_xpath_eval_exists (xpath_ctx, "%s/D:acl-restrictions", xpath_prop_prefix)) {
+ if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:acl-restrictions/D:grant-only",
xpath_prop_prefix))
+ *ard->out_restrictions |= E_WEBDAV_ACL_RESTRICTION_GRANT_ONLY;
+
+ if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:acl-restrictions/D:no-invert",
xpath_prop_prefix))
+ *ard->out_restrictions |= E_WEBDAV_ACL_RESTRICTION_NO_INVERT;
+
+ if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:acl-restrictions/D:deny-before-grant",
xpath_prop_prefix))
+ *ard->out_restrictions |= E_WEBDAV_ACL_RESTRICTION_DENY_BEFORE_GRANT;
+
+ if (e_xml_xpath_eval_exists (xpath_ctx, "%s/D:acl-restrictions/D:required-principal",
xpath_prop_prefix)) {
+ gchar *prefix;
+
+ *ard->out_restrictions |= E_WEBDAV_ACL_RESTRICTION_REQUIRED_PRINCIPAL;
+
+ prefix = g_strdup_printf ("%s/D:acl-restrictions/D:required-principal",
xpath_prop_prefix);
+ *ard->out_principal_kind = e_webdav_session_extract_acl_principal (xpath_ctx, prefix,
NULL, ard->out_principal_hrefs);
+ g_free (prefix);
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * e_webdav_session_get_acl_restrictions_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @out_restrictions: (out): return location for bit-or of #EWebDAVACLRestrictions
+ * @out_principal_kind: (out): return location for principal kind
+ * @out_principal_hrefs: (out) (transfer full) (element-type utf8): return location for a #GSList of
principal href-s
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets Access Control List (ACL) restrictions for the @uri, or, in case it's %NULL,
+ * for the URI defined in associated #ESource. The @out_principal_kind is valid only
+ * if the @out_restrictions contains #E_WEBDAV_ACL_RESTRICTION_REQUIRED_PRINCIPAL.
+ * The @out_principal_hrefs is valid only if the @out_principal_kind is valid and when
+ * it is #E_WEBDAV_ACE_PRINCIPAL_HREF.
+ *
+ * Free the returned @out_principal_hrefs with
+ * g_slist_free_full (entries, g_free);
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_get_acl_restrictions_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ guint32 *out_restrictions,
+ EWebDAVACEPrincipalKind *out_principal_kind,
+ GSList **out_principal_hrefs,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ACLRestrictionsData ard;
+ EXmlDocument *xml;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (out_restrictions != NULL, FALSE);
+ g_return_val_if_fail (out_principal_kind != NULL, FALSE);
+ g_return_val_if_fail (out_principal_hrefs != NULL, FALSE);
+
+ *out_restrictions = E_WEBDAV_ACL_RESTRICTION_NONE;
+ *out_principal_kind = E_WEBDAV_ACE_PRINCIPAL_UNKNOWN;
+ *out_principal_hrefs = NULL;
+
+ xml = e_xml_document_new (E_WEBDAV_NS_DAV, "propfind");
+ g_return_val_if_fail (xml != NULL, FALSE);
+
+ e_xml_document_start_element (xml, NULL, "prop");
+ e_xml_document_add_empty_element (xml, NULL, "acl-restrictions");
+ e_xml_document_end_element (xml); /* prop */
+
+ ard.out_restrictions = out_restrictions;
+ ard.out_principal_kind = out_principal_kind;
+ ard.out_principal_hrefs = out_principal_hrefs;
+
+ success = e_webdav_session_propfind_sync (webdav, uri, E_WEBDAV_DEPTH_THIS, xml,
+ e_webdav_session_acl_restrictions_cb, &ard, cancellable, error);
+
+ g_object_unref (xml);
+
+ return success;
+}
+
+static gboolean
+e_webdav_session_principal_collection_set_cb (EWebDAVSession *webdav,
+ xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prop_prefix,
+ const SoupURI *request_uri,
+ const gchar *href,
+ guint status_code,
+ gpointer user_data)
+{
+ GSList **out_principal_hrefs = user_data;
+
+ g_return_val_if_fail (xpath_ctx != NULL, FALSE);
+ g_return_val_if_fail (out_principal_hrefs != NULL, FALSE);
+
+ if (!xpath_prop_prefix) {
+ } else if (status_code == SOUP_STATUS_OK &&
+ e_xml_xpath_eval_exists (xpath_ctx, "%s/D:principal-collection-set", xpath_prop_prefix)) {
+ xmlXPathObjectPtr xpath_obj;
+
+ xpath_obj = e_xml_xpath_eval (xpath_ctx, "%s/D:principal-collection-set/D:href",
xpath_prop_prefix);
+
+ if (xpath_obj) {
+ gint ii, length;
+
+ length = xmlXPathNodeSetGetLength (xpath_obj->nodesetval);
+
+ for (ii = 0; ii < length; ii++) {
+ gchar *href;
+
+ href = e_xml_xpath_eval_as_string (xpath_ctx,
"%s/D:principal-collection-set/D:href[%d]", xpath_prop_prefix, ii + 1);
+ if (href)
+ *out_principal_hrefs = g_slist_prepend (*out_principal_hrefs, href);
+ }
+
+ xmlXPathFreeObject (xpath_obj);
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * e_webdav_session_get_principal_collection_set_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @out_principal_hrefs: (out) (transfer full) (element-type utf8): return location for a #GSList of
principal href-s
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Gets list of principal collection href for the @uri, or, in case it's %NULL,
+ * for the URI defined in associated #ESource. The @out_principal_hrefs are root
+ * collections that contain the principals that are available on the server that
+ * implements this resource.
+ *
+ * Free the returned @out_principal_hrefs with
+ * g_slist_free_full (entries, g_free);
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_get_principal_collection_set_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ GSList **out_principal_hrefs, /* gchar * */
+ GCancellable *cancellable,
+ GError **error)
+{
+ EXmlDocument *xml;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (out_principal_hrefs != NULL, FALSE);
+
+ *out_principal_hrefs = NULL;
+
+ xml = e_xml_document_new (E_WEBDAV_NS_DAV, "propfind");
+ g_return_val_if_fail (xml != NULL, FALSE);
+
+ e_xml_document_start_element (xml, NULL, "prop");
+ e_xml_document_add_empty_element (xml, NULL, "principal-collection-set");
+ e_xml_document_end_element (xml); /* prop */
+
+ success = e_webdav_session_propfind_sync (webdav, uri, E_WEBDAV_DEPTH_THIS, xml,
+ e_webdav_session_principal_collection_set_cb, out_principal_hrefs, cancellable, error);
+
+ g_object_unref (xml);
+
+ if (success)
+ *out_principal_hrefs = g_slist_reverse (*out_principal_hrefs);
+
+ return success;
+}
+
+/**
+ * e_webdav_session_set_acl_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @entries: (element-type EWebDAVAccessControlEntry): entries to write
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Changes Access Control List (ACL) for the @uri, or, in case it's %NULL,
+ * for the URI defined in associated #ESource.
+ *
+ * Make sure that the @entries satisfy ACL restrictions, as returned
+ * by e_webdav_session_get_acl_restrictions_sync(). The order in the @entries
+ * is preserved. It cannot contain any %E_WEBDAV_ACE_FLAG_PROTECTED,
+ * nor @E_WEBDAV_ACE_FLAG_INHERITED, items.
+ *
+ * Use e_webdav_session_get_acl_sync() to read currently known ACL entries,
+ * remove from the list those protected and inherited, and then modify
+ * the rest with the required changed.
+ *
+ * Note this function doesn't support general %E_WEBDAV_ACE_PRINCIPAL_PROPERTY and
+ * returns %G_IO_ERROR_NOT_SUPPORTED error when any such is tried to be written.
+ *
+ * In case the returned entries contain any %E_WEBDAV_ACE_PRINCIPAL_PROPERTY,
+ * or there's a need to write such Access Control Entry, then do not use
+ * e_webdav_session_get_acl_sync(), neither e_webdav_session_set_acl_sync(),
+ * and write more generic implementation.
+ *
+ * Returns: Whether succeeded.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_set_acl_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const GSList *entries,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EXmlDocument *xml;
+ GSList *link, *plink;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (entries != NULL, FALSE);
+
+ xml = e_xml_document_new (E_WEBDAV_NS_DAV, "acl");
+ g_return_val_if_fail (xml != NULL, FALSE);
+
+ for (link = (GSList *) entries; link; link = g_slist_next (link)) {
+ EWebDAVAccessControlEntry *ace = link->data;
+
+ if (!ace) {
+ g_warn_if_fail (ace != NULL);
+ g_object_unref (xml);
+ return FALSE;
+ }
+
+ if ((ace->flags & E_WEBDAV_ACE_FLAG_PROTECTED) != 0 ||
+ (ace->flags & E_WEBDAV_ACE_FLAG_INHERITED) != 0) {
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
+ _("Cannot store protected nor inherited Access Control Entry."));
+ g_object_unref (xml);
+ return FALSE;
+ }
+
+ if (ace->principal_kind == E_WEBDAV_ACE_PRINCIPAL_UNKNOWN) {
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
+ _("Provided invalid principal kind for Access Control Entry."));
+ g_object_unref (xml);
+ return FALSE;
+ }
+
+ if (ace->principal_kind == E_WEBDAV_ACE_PRINCIPAL_PROPERTY) {
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
+ _("Cannot store property-based Access Control Entry."));
+ g_object_unref (xml);
+ return FALSE;
+ }
+
+ if ((ace->flags & (E_WEBDAV_ACE_FLAG_GRANT | E_WEBDAV_ACE_FLAG_DENY)) == 0) {
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
+ _("Access Control Entry can be only to Grant or Deny, but not None."));
+ g_object_unref (xml);
+ return FALSE;
+ }
+
+ if ((ace->flags & E_WEBDAV_ACE_FLAG_GRANT) != 0 &&
+ (ace->flags & E_WEBDAV_ACE_FLAG_DENY) != 0) {
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
+ _("Access Control Entry can be only to Grant or Deny, but not both."));
+ g_object_unref (xml);
+ return FALSE;
+ }
+
+ e_xml_document_start_element (xml, NULL, "ace");
+
+ if ((ace->flags & E_WEBDAV_ACE_FLAG_INVERT) != 0)
+ e_xml_document_start_element (xml, NULL, "invert");
+
+ e_xml_document_start_element (xml, NULL, "principal");
+ switch (ace->principal_kind) {
+ case E_WEBDAV_ACE_PRINCIPAL_UNKNOWN:
+ g_warn_if_reached ();
+ break;
+ case E_WEBDAV_ACE_PRINCIPAL_HREF:
+ e_xml_document_start_text_element (xml, NULL, "href");
+ e_xml_document_write_string (xml, ace->principal_href);
+ e_xml_document_end_element (xml);
+ break;
+ case E_WEBDAV_ACE_PRINCIPAL_ALL:
+ e_xml_document_add_empty_element (xml, NULL, "all");
+ break;
+ case E_WEBDAV_ACE_PRINCIPAL_AUTHENTICATED:
+ e_xml_document_add_empty_element (xml, NULL, "authenticated");
+ break;
+ case E_WEBDAV_ACE_PRINCIPAL_UNAUTHENTICATED:
+ e_xml_document_add_empty_element (xml, NULL, "unauthenticated");
+ break;
+ case E_WEBDAV_ACE_PRINCIPAL_PROPERTY:
+ g_warn_if_reached ();
+ break;
+ case E_WEBDAV_ACE_PRINCIPAL_SELF:
+ e_xml_document_add_empty_element (xml, NULL, "self");
+ break;
+ case E_WEBDAV_ACE_PRINCIPAL_OWNER:
+ e_xml_document_start_element (xml, NULL, "property");
+ e_xml_document_add_empty_element (xml, NULL, "owner");
+ e_xml_document_end_element (xml);
+ break;
+
+ }
+ e_xml_document_end_element (xml); /* principal */
+
+ if ((ace->flags & E_WEBDAV_ACE_FLAG_INVERT) != 0)
+ e_xml_document_end_element (xml); /* invert */
+
+ if ((ace->flags & E_WEBDAV_ACE_FLAG_GRANT) != 0)
+ e_xml_document_start_element (xml, NULL, "grant");
+ else if ((ace->flags & E_WEBDAV_ACE_FLAG_DENY) != 0)
+ e_xml_document_start_element (xml, NULL, "deny");
+ else
+ g_warn_if_reached ();
+
+ for (plink = ace->privileges; plink; plink = g_slist_next (plink)) {
+ EWebDAVPrivilege *privilege = plink->data;
+
+ if (!privilege) {
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
+ _("Access Control Entry privilege cannot be NULL."));
+ g_object_unref (xml);
+ return FALSE;
+ }
+
+ e_xml_document_start_element (xml, NULL, "privilege");
+ e_xml_document_add_empty_element (xml, privilege->ns_uri, privilege->name);
+ e_xml_document_end_element (xml); /* privilege */
+ }
+
+ e_xml_document_end_element (xml); /* grant or deny */
+
+ e_xml_document_end_element (xml); /* ace */
+ }
+
+ success = e_webdav_session_acl_sync (webdav, uri, xml, cancellable, error);
+
+ g_object_unref (xml);
+
+ return success;
+}
+
+static gboolean
+e_webdav_session_principal_property_search_cb (EWebDAVSession *webdav,
+ xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prop_prefix,
+ const SoupURI *request_uri,
+ const gchar *href,
+ guint status_code,
+ gpointer user_data)
+{
+ GSList **out_principals = user_data;
+
+ g_return_val_if_fail (out_principals != NULL, FALSE);
+
+ if (!xpath_prop_prefix) {
+ } else if (status_code == SOUP_STATUS_OK) {
+ EWebDAVResource *resource;
+ gchar *display_name;
+
+ display_name = e_webdav_session_extract_nonempty (xpath_ctx, xpath_prop_prefix,
"D:displayname", NULL);
+
+ resource = e_webdav_resource_new (
+ E_WEBDAV_RESOURCE_KIND_PRINCIPAL,
+ 0, /* supports */
+ href,
+ NULL, /* etag */
+ NULL, /* display_name */
+ NULL, /* content_type */
+ 0, /* content_length */
+ 0, /* creation_date */
+ 0, /* last_modified */
+ NULL, /* description */
+ NULL); /* color */
+ resource->display_name = display_name;
+
+ *out_principals = g_slist_prepend (*out_principals, resource);
+ }
+
+ return TRUE;
+}
+
+/**
+ * e_webdav_session_principal_property_search_sync:
+ * @webdav: an #EWebDAVSession
+ * @uri: (nullable): URI to issue the request for, or %NULL to read from #ESource
+ * @apply_to_principal_collection_set: whether to apply to principal-collection-set
+ * @match_ns_uri: (nullable): namespace URI of the property to search in, or %NULL for %E_WEBDAV_NS_DAV
+ * @match_property: name of the property to search in
+ * @match_value: a string value to search for
+ * @out_principals: (out) (transfer full) (element-type EWebDAVResource): return location for matching
principals
+ * @cancellable: optional #GCancellable object, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Issues a DAV:principal-property-search for the @uri, or, in case it's %NULL,
+ * for the URI defined in associated #ESource. The DAV:principal-property-search
+ * performs a search for all principals whose properties contain character data
+ * that matches the search criteria @match_value in @match_property property
+ * of namespace @match_ns_uri.
+ *
+ * By default, the function searches all members (at any depth) of the collection
+ * identified by the @uri. If @apply_to_principal_collection_set is set to %TRUE,
+ * the search is applied instead to each collection returned by
+ * e_webdav_session_get_principal_collection_set_sync() for the @uri.
+ *
+ * The @out_principals is a #GSList of #EWebDAVResource, where the kind
+ * is set to %E_WEBDAV_RESOURCE_KIND_PRINCIPAL and only href with displayname
+ * are filled. All other members of #EWebDAVResource are not set.
+ *
+ * Free the returned @out_principals with
+ * g_slist_free_full (principals, e_webdav_resource_free);
+ * when no longer needed.
+ *
+ * Returns: Whether succeeded. Note it can report success also when no matching
+ * principal had been found.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_webdav_session_principal_property_search_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ gboolean apply_to_principal_collection_set,
+ const gchar *match_ns_uri,
+ const gchar *match_property,
+ const gchar *match_value,
+ GSList **out_principals,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EXmlDocument *xml;
+ gboolean success;
+
+ g_return_val_if_fail (E_IS_WEBDAV_SESSION (webdav), FALSE);
+ g_return_val_if_fail (match_property != NULL, FALSE);
+ g_return_val_if_fail (match_value != NULL, FALSE);
+ g_return_val_if_fail (out_principals != NULL, FALSE);
+
+ *out_principals = NULL;
+
+ xml = e_xml_document_new (E_WEBDAV_NS_DAV, "principal-property-search");
+ g_return_val_if_fail (xml != NULL, FALSE);
+
+ if (apply_to_principal_collection_set) {
+ e_xml_document_add_empty_element (xml, NULL, "apply-to-principal-collection-set");
+ }
+
+ e_xml_document_start_element (xml, NULL, "property-search");
+ e_xml_document_start_element (xml, NULL, "prop");
+ e_xml_document_add_empty_element (xml, match_ns_uri, match_property);
+ e_xml_document_end_element (xml); /* prop */
+ e_xml_document_start_text_element (xml, NULL, "match");
+ e_xml_document_write_string (xml, match_value);
+ e_xml_document_end_element (xml); /* match */
+ e_xml_document_end_element (xml); /* property-search */
+
+ e_xml_document_start_element (xml, NULL, "prop");
+ e_xml_document_add_empty_element (xml, NULL, "displayname");
+ e_xml_document_end_element (xml); /* prop */
+
+ success = e_webdav_session_report_sync (webdav, uri, E_WEBDAV_DEPTH_THIS, xml,
+ e_webdav_session_principal_property_search_cb, out_principals, NULL, NULL, cancellable,
error);
+
+ g_object_unref (xml);
+
+ if (success)
+ *out_principals = g_slist_reverse (*out_principals);
+
+ return success;
+}
+
+/**
+ * e_webdav_session_util_maybe_dequote:
+ * @text: (inout) (nullable): text to dequote
+ *
+ * Dequotes @text, if it's enclosed in double-quotes. The function
+ * changes @text, it doesn't allocate new string. The function does
+ * nothing when the @text is not enclosed in double-quotes.
+ *
+ * Returns: possibly dequoted @text
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_webdav_session_util_maybe_dequote (gchar *text)
+{
+ gint len;
+
+ if (!text || *text != '\"')
+ return text;
+
+ len = strlen (text);
+
+ if (len < 2 || text[len - 1] != '\"')
+ return text;
+
+ memmove (text, text + 1, len - 2);
+ text[len - 2] = '\0';
+
+ return text;
+}
+
+static gboolean
+e_webdav_session_free_in_traverse_cb (GNode *node,
+ gpointer user_data)
+{
+ if (node) {
+ e_webdav_privilege_free (node->data);
+ node->data = NULL;
+ }
+
+ return FALSE;
+}
+
+/**
+ * e_webdav_session_util_free_privileges:
+ * @privileges: (nullable): a tree of #EWebDAVPrivilege structures
+ *
+ * Frees @privileges returned by e_webdav_session_get_supported_privilege_set_sync().
+ * The function does nothing, if @privileges is %NULL.
+ *
+ * Since: 3.26
+ **/
+void
+e_webdav_session_util_free_privileges (GNode *privileges)
+{
+ if (!privileges)
+ return;
+
+ g_node_traverse (privileges, G_PRE_ORDER, G_TRAVERSE_ALL, -1, e_webdav_session_free_in_traverse_cb,
NULL);
+ g_node_destroy (privileges);
+}
diff --git a/src/libedataserver/e-webdav-session.h b/src/libedataserver/e-webdav-session.h
new file mode 100644
index 0000000..8d15080
--- /dev/null
+++ b/src/libedataserver/e-webdav-session.h
@@ -0,0 +1,582 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#if !defined (__LIBEDATASERVER_H_INSIDE__) && !defined (LIBEDATASERVER_COMPILATION)
+#error "Only <libedataserver/libedataserver.h> should be included directly."
+#endif
+
+#ifndef E_WEBDAV_SESSION_H
+#define E_WEBDAV_SESSION_H
+
+#include <glib.h>
+#include <libxml/xpath.h>
+
+#include <libedataserver/e-data-server-util.h>
+#include <libedataserver/e-soup-session.h>
+#include <libedataserver/e-source.h>
+#include <libedataserver/e-xml-document.h>
+
+/* Standard GObject macros */
+#define E_TYPE_WEBDAV_SESSION \
+ (e_webdav_session_get_type ())
+#define E_WEBDAV_SESSION(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST \
+ ((obj), E_TYPE_WEBDAV_SESSION, EWebDAVSession))
+#define E_WEBDAV_SESSION_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_CAST \
+ ((cls), E_TYPE_WEBDAV_SESSION, EWebDAVSessionClass))
+#define E_IS_WEBDAV_SESSION(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE \
+ ((obj), E_TYPE_WEBDAV_SESSION))
+#define E_IS_WEBDAV_SESSION_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_TYPE \
+ ((cls), E_TYPE_WEBDAV_SESSION))
+#define E_WEBDAV_SESSION_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS \
+ ((obj), E_TYPE_WEBDAV_SESSION, EWebDAVSessionClass))
+
+G_BEGIN_DECLS
+
+#define E_WEBDAV_CAPABILITY_CLASS_1 "1"
+#define E_WEBDAV_CAPABILITY_CLASS_2 "2"
+#define E_WEBDAV_CAPABILITY_CLASS_3 "3"
+#define E_WEBDAV_CAPABILITY_ACCESS_CONTROL "access-control"
+#define E_WEBDAV_CAPABILITY_BIND "bind"
+#define E_WEBDAV_CAPABILITY_EXTENDED_MKCOL "extended-mkcol"
+#define E_WEBDAV_CAPABILITY_ADDRESSBOOK "addressbook"
+#define E_WEBDAV_CAPABILITY_CALENDAR_ACCESS "calendar-access"
+#define E_WEBDAV_CAPABILITY_CALENDAR_SCHEDULE "calendar-schedule"
+#define E_WEBDAV_CAPABILITY_CALENDAR_AUTO_SCHEDULE "calendar-auto-schedule"
+#define E_WEBDAV_CAPABILITY_CALENDAR_PROXY "calendar-proxy"
+
+#define E_WEBDAV_DEPTH_THIS "0"
+#define E_WEBDAV_DEPTH_THIS_AND_CHILDREN "1"
+#define E_WEBDAV_DEPTH_INFINITY "infinity"
+
+#define E_WEBDAV_CONTENT_TYPE_XML "application/xml; charset=\"utf-8\""
+#define E_WEBDAV_CONTENT_TYPE_CALENDAR "text/calendar; charset=\"utf-8\""
+#define E_WEBDAV_CONTENT_TYPE_VCARD "text/vcard; charset=\"utf-8\""
+
+#define E_WEBDAV_NS_DAV "DAV:"
+#define E_WEBDAV_NS_CALDAV "urn:ietf:params:xml:ns:caldav"
+#define E_WEBDAV_NS_CARDDAV "urn:ietf:params:xml:ns:carddav"
+#define E_WEBDAV_NS_CALENDARSERVER "http://calendarserver.org/ns/"
+#define E_WEBDAV_NS_ICAL "http://apple.com/ns/ical/"
+
+typedef struct _EWebDAVSession EWebDAVSession;
+typedef struct _EWebDAVSessionClass EWebDAVSessionClass;
+typedef struct _EWebDAVSessionPrivate EWebDAVSessionPrivate;
+
+typedef enum {
+ E_WEBDAV_RESOURCE_KIND_UNKNOWN,
+ E_WEBDAV_RESOURCE_KIND_ADDRESSBOOK,
+ E_WEBDAV_RESOURCE_KIND_CALENDAR,
+ E_WEBDAV_RESOURCE_KIND_PRINCIPAL,
+ E_WEBDAV_RESOURCE_KIND_COLLECTION,
+ E_WEBDAV_RESOURCE_KIND_RESOURCE
+} EWebDAVResourceKind;
+
+typedef enum {
+ E_WEBDAV_RESOURCE_SUPPORTS_NONE = 0,
+ E_WEBDAV_RESOURCE_SUPPORTS_CONTACTS = 1 << 0,
+ E_WEBDAV_RESOURCE_SUPPORTS_EVENTS = 1 << 1,
+ E_WEBDAV_RESOURCE_SUPPORTS_MEMOS = 1 << 2,
+ E_WEBDAV_RESOURCE_SUPPORTS_TASKS = 1 << 3,
+ E_WEBDAV_RESOURCE_SUPPORTS_FREEBUSY = 1 << 4,
+ E_WEBDAV_RESOURCE_SUPPORTS_TIMEZONE = 1 << 5
+} EWebDAVResourceSupports;
+
+typedef struct _EWebDAVResource {
+ EWebDAVResourceKind kind;
+ guint32 supports;
+ gchar *href;
+ gchar *etag;
+ gchar *display_name;
+ gchar *content_type;
+ gsize content_length;
+ glong creation_date;
+ glong last_modified;
+ gchar *description;
+ gchar *color;
+} EWebDAVResource;
+
+GType e_webdav_resource_get_type (void) G_GNUC_CONST;
+EWebDAVResource *
+ e_webdav_resource_new (EWebDAVResourceKind kind,
+ guint32 supports,
+ const gchar *href,
+ const gchar *etag,
+ const gchar *display_name,
+ const gchar *content_type,
+ gsize content_length,
+ glong creation_date,
+ glong last_modified,
+ const gchar *description,
+ const gchar *color);
+EWebDAVResource *
+ e_webdav_resource_copy (const EWebDAVResource *src);
+void e_webdav_resource_free (gpointer ptr /* EWebDAVResource * */);
+
+typedef enum {
+ E_WEBDAV_LIST_ALL = 0xFFFFFFFF,
+ E_WEBDAV_LIST_NONE = 0,
+ E_WEBDAV_LIST_SUPPORTS = 1 << 0,
+ E_WEBDAV_LIST_ETAG = 1 << 1,
+ E_WEBDAV_LIST_DISPLAY_NAME = 1 << 2,
+ E_WEBDAV_LIST_CONTENT_TYPE = 1 << 3,
+ E_WEBDAV_LIST_CONTENT_LENGTH = 1 << 4,
+ E_WEBDAV_LIST_CREATION_DATE = 1 << 5,
+ E_WEBDAV_LIST_LAST_MODIFIED = 1 << 6,
+ E_WEBDAV_LIST_DESCRIPTION = 1 << 7,
+ E_WEBDAV_LIST_COLOR = 1 << 8
+} EWebDAVListFlags;
+
+/**
+ * EWebDAVPropstatTraverseFunc:
+ * @webdav: an #EWebDAVSession
+ * @xpath_ctx: an #xmlXPathContextPtr
+ * @xpath_prop_prefix: (nullable): an XPath prefix for the current prop element, without trailing forward
slash
+ * @request_uri: a #SoupURI, containing the request URI, maybe redirected by the server
+ * @href: (nullable): a full URI to which the property belongs, or %NULL, when not found
+ * @status_code: an HTTP status code for this property
+ * @user_data: user data, as passed to e_webdav_session_propfind_sync()
+ *
+ * A callback function for e_webdav_session_propfind_sync(),
+ * e_webdav_session_report_sync() and other XML response with DAV:propstat
+ * elements traversal functions.
+ *
+ * The @xpath_prop_prefix can be %NULL only once, for the first time,
+ * which is meant to let the caller setup the @xpath_ctx, like to register
+ * its own namespaces to it with e_xml_xpath_context_register_namespaces().
+ * All other invocations of the function will have @xpath_prop_prefix non-%NULL.
+ *
+ * Returns: %TRUE to continue traversal of the returned response, %FALSE otherwise.
+ *
+ * Since: 3.26
+ **/
+typedef gboolean (* EWebDAVPropstatTraverseFunc) (EWebDAVSession *webdav,
+ xmlXPathContextPtr xpath_ctx,
+ const gchar *xpath_prop_prefix,
+ const SoupURI *request_uri,
+ const gchar *href,
+ guint status_code,
+ gpointer user_data);
+
+typedef enum {
+ E_WEBDAV_PROPERTY_SET,
+ E_WEBDAV_PROPERTY_REMOVE
+} EWebDAVPropertyChangeKind;
+
+typedef struct _EWebDAVPropertyChange {
+ EWebDAVPropertyChangeKind kind;
+ gchar *ns_uri;
+ gchar *name;
+ gchar *value;
+} EWebDAVPropertyChange;
+
+GType e_webdav_property_change_get_type (void) G_GNUC_CONST;
+EWebDAVPropertyChange *
+ e_webdav_property_change_new_set (const gchar *ns_uri,
+ const gchar *name,
+ const gchar *value);
+EWebDAVPropertyChange *
+ e_webdav_property_change_new_remove (const gchar *ns_uri,
+ const gchar *name);
+EWebDAVPropertyChange *
+ e_webdav_property_change_copy (const EWebDAVPropertyChange *src);
+void e_webdav_property_change_free (gpointer ptr); /* EWebDAVPropertyChange * */
+
+typedef enum {
+ E_WEBDAV_LOCK_EXCLUSIVE,
+ E_WEBDAV_LOCK_SHARED
+} EWebDAVLockScope;
+
+#define E_WEBDAV_COLLATION_ASCII_NUMERIC_SUFFIX "ascii-numeric"
+#define E_WEBDAV_COLLATION_ASCII_NUMERIC "i;" E_WEBDAV_COLLATION_ASCII_NUMERIC_SUFFIX
+
+#define E_WEBDAV_COLLATION_ASCII_CASEMAP_SUFFIX "ascii-casemap"
+#define E_WEBDAV_COLLATION_ASCII_CASEMAP "i;" E_WEBDAV_COLLATION_ASCII_CASEMAP_SUFFIX
+
+#define E_WEBDAV_COLLATION_OCTET_SUFFIX "octet"
+#define E_WEBDAV_COLLATION_OCTET "i;" E_WEBDAV_COLLATION_OCTET_SUFFIX
+
+#define E_WEBDAV_COLLATION_UNICODE_CASEMAP_SUFFIX "unicode-casemap"
+#define E_WEBDAV_COLLATION_UNICODE_CASEMAP "i;" E_WEBDAV_COLLATION_UNICODE_CASEMAP_SUFFIX
+
+typedef enum {
+ E_WEBDAV_PRIVILEGE_KIND_UNKNOWN = 0,
+ E_WEBDAV_PRIVILEGE_KIND_ABSTRACT,
+ E_WEBDAV_PRIVILEGE_KIND_AGGREGATE,
+ E_WEBDAV_PRIVILEGE_KIND_COMMON
+} EWebDAVPrivilegeKind;
+
+typedef enum {
+ E_WEBDAV_PRIVILEGE_HINT_UNKNOWN = 0,
+ E_WEBDAV_PRIVILEGE_HINT_READ,
+ E_WEBDAV_PRIVILEGE_HINT_WRITE,
+ E_WEBDAV_PRIVILEGE_HINT_WRITE_PROPERTIES,
+ E_WEBDAV_PRIVILEGE_HINT_WRITE_CONTENT,
+ E_WEBDAV_PRIVILEGE_HINT_UNLOCK,
+ E_WEBDAV_PRIVILEGE_HINT_READ_ACL,
+ E_WEBDAV_PRIVILEGE_HINT_WRITE_ACL,
+ E_WEBDAV_PRIVILEGE_HINT_READ_CURRENT_USER_PRIVILEGE_SET,
+ E_WEBDAV_PRIVILEGE_HINT_BIND,
+ E_WEBDAV_PRIVILEGE_HINT_UNBIND,
+ E_WEBDAV_PRIVILEGE_HINT_ALL,
+ E_WEBDAV_PRIVILEGE_HINT_CALDAV_READ_FREE_BUSY
+} EWebDAVPrivilegeHint;
+
+typedef struct _EWebDAVPrivilege {
+ gchar *ns_uri;
+ gchar *name;
+ gchar *description;
+ EWebDAVPrivilegeKind kind;
+ EWebDAVPrivilegeHint hint;
+} EWebDAVPrivilege;
+
+GType e_webdav_privilege_get_type (void) G_GNUC_CONST;
+EWebDAVPrivilege *
+ e_webdav_privilege_new (const gchar *ns_uri,
+ const gchar *name,
+ const gchar *description,
+ EWebDAVPrivilegeKind kind,
+ EWebDAVPrivilegeHint hint);
+EWebDAVPrivilege *
+ e_webdav_privilege_copy (const EWebDAVPrivilege *src);
+void e_webdav_privilege_free (gpointer ptr); /* EWebDAVPrivilege * */
+
+typedef enum {
+ E_WEBDAV_ACE_PRINCIPAL_UNKNOWN = 0,
+ E_WEBDAV_ACE_PRINCIPAL_HREF,
+ E_WEBDAV_ACE_PRINCIPAL_ALL,
+ E_WEBDAV_ACE_PRINCIPAL_AUTHENTICATED,
+ E_WEBDAV_ACE_PRINCIPAL_UNAUTHENTICATED,
+ E_WEBDAV_ACE_PRINCIPAL_PROPERTY,
+ E_WEBDAV_ACE_PRINCIPAL_SELF,
+ E_WEBDAV_ACE_PRINCIPAL_OWNER /* special-case, 'property' with only 'DAV:owner' child */
+} EWebDAVACEPrincipalKind;
+
+typedef enum {
+ E_WEBDAV_ACE_FLAG_UNKNOWN = 0,
+ E_WEBDAV_ACE_FLAG_GRANT = 1 << 0,
+ E_WEBDAV_ACE_FLAG_DENY = 1 << 1,
+ E_WEBDAV_ACE_FLAG_INVERT = 1 << 2,
+ E_WEBDAV_ACE_FLAG_PROTECTED = 1 << 3,
+ E_WEBDAV_ACE_FLAG_INHERITED = 1 << 4
+} EWebDAVACEFlag;
+
+typedef struct _EWebDAVAccessControlEntry {
+ EWebDAVACEPrincipalKind principal_kind;
+ gchar *principal_href; /* valid onyl if principal_kind is E_WEBDAV_ACE_PRINCIPAL_HREF */
+ guint32 flags; /* bit-or of EWebDAVACEFlag */
+ gchar *inherited_href; /* valid only if flags contain E_WEBDAV_ACE_INHERITED */
+ GSList *privileges; /* EWebDAVPrivilege * */
+} EWebDAVAccessControlEntry;
+
+GType e_webdav_access_control_entry_get_type (void) G_GNUC_CONST;
+EWebDAVAccessControlEntry *
+ e_webdav_access_control_entry_new (EWebDAVACEPrincipalKind principal_kind,
+ const gchar *principal_href,
+ guint32 flags, /* bit-or of EWebDAVACEFlag */
+ const gchar *inherited_href);
+EWebDAVAccessControlEntry *
+ e_webdav_access_control_entry_copy (const EWebDAVAccessControlEntry *src);
+void e_webdav_access_control_entry_free (gpointer ptr); /* EWebDAVAccessControlEntry * */
+void e_webdav_access_control_entry_append_privilege
+ (EWebDAVAccessControlEntry *ace,
+ EWebDAVPrivilege *privilege);
+GSList * e_webdav_access_control_entry_get_privileges
+ (EWebDAVAccessControlEntry *ace); /* EWebDAVPrivilege
* */
+
+typedef enum {
+ E_WEBDAV_ACL_RESTRICTION_NONE = 0,
+ E_WEBDAV_ACL_RESTRICTION_GRANT_ONLY = 1 << 0,
+ E_WEBDAV_ACL_RESTRICTION_NO_INVERT = 1 << 1,
+ E_WEBDAV_ACL_RESTRICTION_DENY_BEFORE_GRANT = 1 << 2,
+ E_WEBDAV_ACL_RESTRICTION_REQUIRED_PRINCIPAL = 1 << 3
+} EWebDAVACLRestrictions;
+
+/**
+ * EWebDAVSession:
+ *
+ * Contains only private data that should be read and manipulated using the
+ * functions below.
+ *
+ * Since: 3.26
+ **/
+struct _EWebDAVSession {
+ /*< private >*/
+ ESoupSession parent;
+ EWebDAVSessionPrivate *priv;
+};
+
+struct _EWebDAVSessionClass {
+ ESoupSessionClass parent_class;
+
+ /* Padding for future expansion */
+ gpointer reserved[10];
+};
+
+GType e_webdav_session_get_type (void) G_GNUC_CONST;
+
+EWebDAVSession *e_webdav_session_new (ESource *source);
+SoupRequestHTTP *
+ e_webdav_session_new_request (EWebDAVSession *webdav,
+ const gchar *method,
+ const gchar *uri,
+ GError **error);
+gboolean e_webdav_session_replace_with_detailed_error
+ (EWebDAVSession *webdav,
+ SoupRequestHTTP *request,
+ const GByteArray *response_data,
+ gboolean ignore_multistatus,
+ const gchar *prefix,
+ GError **inout_error);
+gchar * e_webdav_session_ensure_full_uri (EWebDAVSession *webdav,
+ const SoupURI *request_uri,
+ const gchar *href);
+gboolean e_webdav_session_options_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ GHashTable **out_capabilities,
+ GHashTable **out_allows,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_post_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const gchar *data,
+ gsize data_length,
+ gchar **out_content_type,
+ GByteArray **out_content,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_propfind_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const gchar *depth,
+ const EXmlDocument *xml,
+ EWebDAVPropstatTraverseFunc func,
+ gpointer func_user_data,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_proppatch_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const EXmlDocument *xml,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_report_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const gchar *depth,
+ const EXmlDocument *xml,
+ EWebDAVPropstatTraverseFunc func,
+ gpointer func_user_data,
+ gchar **out_content_type,
+ GByteArray **out_content,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_mkcol_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_mkcol_addressbook_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const gchar *display_name,
+ const gchar *description,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_mkcalendar_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const gchar *display_name,
+ const gchar *description,
+ const gchar *color,
+ guint32 supports, /* bit-or of
EWebDAVResourceSupports */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_get_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ gchar **out_href,
+ gchar **out_etag,
+ GOutputStream *out_stream,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_get_data_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ gchar **out_href,
+ gchar **out_etag,
+ gchar **out_bytes,
+ gsize *out_length,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_put_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const gchar *etag,
+ const gchar *content_type,
+ GInputStream *stream,
+ gchar **out_href,
+ gchar **out_etag,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_put_data_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const gchar *etag,
+ const gchar *content_type,
+ const gchar *bytes,
+ gsize length,
+ gchar **out_href,
+ gchar **out_etag,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_delete_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const gchar *depth,
+ const gchar *etag,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_copy_sync (EWebDAVSession *webdav,
+ const gchar *source_uri,
+ const gchar *destination_uri,
+ const gchar *depth,
+ gboolean can_overwrite,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_move_sync (EWebDAVSession *webdav,
+ const gchar *source_uri,
+ const gchar *destination_uri,
+ gboolean can_overwrite,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_lock_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const gchar *depth,
+ gint32 lock_timeout,
+ const EXmlDocument *xml,
+ gchar **out_lock_token,
+ xmlDocPtr *out_xml_response,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_refresh_lock_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const gchar *lock_token,
+ gint32 lock_timeout,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_unlock_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const gchar *lock_token,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_traverse_multistatus_response
+ (EWebDAVSession *webdav,
+ const SoupMessage *message,
+ const GByteArray *xml_data,
+ EWebDAVPropstatTraverseFunc func,
+ gpointer func_user_data,
+ GError **error);
+gboolean e_webdav_session_traverse_mkcol_response
+ (EWebDAVSession *webdav,
+ const SoupMessage *message,
+ const GByteArray *xml_data,
+ EWebDAVPropstatTraverseFunc func,
+ gpointer func_user_data,
+ GError **error);
+gboolean e_webdav_session_traverse_mkcalendar_response
+ (EWebDAVSession *webdav,
+ const SoupMessage *message,
+ const GByteArray *xml_data,
+ EWebDAVPropstatTraverseFunc func,
+ gpointer func_user_data,
+ GError **error);
+gboolean e_webdav_session_getctag_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ gchar **out_ctag,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_list_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const gchar *depth,
+ guint32 flags, /* bit-or of EWebDAVListFlags */
+ GSList **out_resources, /* EWebDAVResource * */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_update_properties_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const GSList *changes, /* EWebDAVPropertyChange * */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_lock_resource_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ EWebDAVLockScope lock_scope,
+ gint32 lock_timeout,
+ const gchar *owner,
+ gchar **out_lock_token,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_acl_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const EXmlDocument *xml,
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_get_supported_privilege_set_sync
+ (EWebDAVSession *webdav,
+ const gchar *uri,
+ GNode **out_privileges, /* EWebDAVPrivilege * */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_get_current_user_privilege_set_sync
+ (EWebDAVSession *webdav,
+ const gchar *uri,
+ GSList **out_privileges, /* EWebDAVPrivilege * */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_get_acl_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ GSList **out_entries, /* EWebDAVAccessControlEntry *
*/
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_get_acl_restrictions_sync
+ (EWebDAVSession *webdav,
+ const gchar *uri,
+ guint32 *out_restrictions, /* bit-or of
EWebDAVACLRestrictions */
+ EWebDAVACEPrincipalKind *out_principal_kind,
+ GSList **out_principal_hrefs, /* gchar * */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_get_principal_collection_set_sync
+ (EWebDAVSession *webdav,
+ const gchar *uri,
+ GSList **out_principal_hrefs, /* gchar * */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_set_acl_sync (EWebDAVSession *webdav,
+ const gchar *uri,
+ const GSList *entries, /* EWebDAVAccessControlEntry
* */
+ GCancellable *cancellable,
+ GError **error);
+gboolean e_webdav_session_principal_property_search_sync
+ (EWebDAVSession *webdav,
+ const gchar *uri,
+ gboolean apply_to_principal_collection_set,
+ const gchar *match_ns_uri,
+ const gchar *match_property,
+ const gchar *match_value,
+ GSList **out_principals, /* EWebDAVResource * */
+ GCancellable *cancellable,
+ GError **error);
+gchar * e_webdav_session_util_maybe_dequote (gchar *text);
+void e_webdav_session_util_free_privileges (GNode *privileges); /* EWebDAVPrivilege * */
+
+G_END_DECLS
+
+#endif /* E_WEBDAV_SESSION_H */
diff --git a/src/libedataserver/e-xml-document.c b/src/libedataserver/e-xml-document.c
new file mode 100644
index 0000000..c195699
--- /dev/null
+++ b/src/libedataserver/e-xml-document.c
@@ -0,0 +1,727 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * SECTION: e-xml-document
+ * @include: libedataserver/libedataserver.h
+ * @short_description: An XML document wrapper
+ *
+ * The #EXmlDocument class wraps creation of XML documents.
+ **/
+
+#include "evolution-data-server-config.h"
+
+#include <string.h>
+
+#include "e-xml-document.h"
+
+struct _EXmlDocumentPrivate {
+ xmlDocPtr doc;
+ xmlNodePtr root;
+ xmlNodePtr current_element;
+
+ GHashTable *namespaces_by_href; /* gchar *ns_href ~> xmlNsPtr */
+};
+
+G_DEFINE_TYPE (EXmlDocument, e_xml_document, G_TYPE_OBJECT)
+
+static void
+e_xml_document_finalize (GObject *object)
+{
+ EXmlDocument *xml = E_XML_DOCUMENT (object);
+
+ if (xml->priv->doc) {
+ xmlFreeDoc (xml->priv->doc);
+ xml->priv->doc = NULL;
+ }
+
+ xml->priv->root = NULL;
+ xml->priv->current_element = NULL;
+
+ if (xml->priv->namespaces_by_href) {
+ g_hash_table_destroy (xml->priv->namespaces_by_href);
+ xml->priv->namespaces_by_href = NULL;
+ }
+
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_xml_document_parent_class)->finalize (object);
+}
+
+static void
+e_xml_document_class_init (EXmlDocumentClass *klass)
+{
+ GObjectClass *object_class;
+
+ g_type_class_add_private (klass, sizeof (EXmlDocumentPrivate));
+
+ object_class = G_OBJECT_CLASS (klass);
+ object_class->finalize = e_xml_document_finalize;
+}
+
+static void
+e_xml_document_init (EXmlDocument *xml)
+{
+ xml->priv = G_TYPE_INSTANCE_GET_PRIVATE (xml, E_TYPE_XML_DOCUMENT, EXmlDocumentPrivate);
+
+ xml->priv->doc = xmlNewDoc ((const xmlChar *) "1.0");
+ g_return_if_fail (xml->priv->doc != NULL);
+
+ xml->priv->doc->encoding = xmlCharStrdup ("UTF-8");
+
+ xml->priv->namespaces_by_href = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+}
+
+/**
+ * e_xml_document_new:
+ * @ns_href: (nullable): default namespace href to use, or %NULL
+ * @root_element: root element name
+ *
+ * Creates a new #EXmlDocument with root element @root_element and optionally
+ * also with set default namespace @ns_href.
+ *
+ * Returns: (transfer full): a new #EXmlDocument; free it with g_object_unref(),
+ * when no longer needed.
+ *
+ * Since: 3.26
+ **/
+EXmlDocument *
+e_xml_document_new (const gchar *ns_href,
+ const gchar *root_element)
+{
+ EXmlDocument *xml;
+
+ g_return_val_if_fail (root_element != NULL, NULL);
+ g_return_val_if_fail (*root_element, NULL);
+
+ xml = g_object_new (E_TYPE_XML_DOCUMENT, NULL);
+
+ xml->priv->root = xmlNewDocNode (xml->priv->doc, NULL, (const xmlChar *) root_element, NULL);
+ if (ns_href) {
+ xmlNsPtr ns;
+
+ ns = xmlNewNs (xml->priv->root, (const xmlChar *) ns_href, NULL);
+ g_warn_if_fail (ns != NULL);
+
+ xmlSetNs (xml->priv->root, ns);
+
+ if (ns)
+ g_hash_table_insert (xml->priv->namespaces_by_href, g_strdup (ns_href), ns);
+ }
+
+ xmlDocSetRootElement (xml->priv->doc, xml->priv->root);
+
+ xml->priv->current_element = xml->priv->root;
+
+ return xml;
+}
+
+/**
+ * e_xml_document_get_xmldoc:
+ * @xml: an #EXmlDocument
+ *
+ * Returns: (transfer none): Underlying #xmlDocPtr.
+ *
+ * Since: 3.26
+ **/
+xmlDocPtr
+e_xml_document_get_xmldoc (EXmlDocument *xml)
+{
+ g_return_val_if_fail (E_IS_XML_DOCUMENT (xml), NULL);
+
+ return xml->priv->doc;
+}
+
+/**
+ * e_xml_document_get_content:
+ * @xml: an #EXmlDocument
+ * @out_length: (out) (nullable): optional return location for length of the content, or %NULL
+ *
+ * Gets content of the @xml as string. The string is nul-terminated, but
+ * if @out_length is also provided, then it doesn't contain this additional
+ * nul character.
+ *
+ * Returns: (transfer full): Content of the @xml as newly allocated string.
+ * Free it with g_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_xml_document_get_content (const EXmlDocument *xml,
+ gsize *out_length)
+{
+ xmlOutputBufferPtr xmlbuffer;
+ gsize length;
+ gchar *text;
+
+ g_return_val_if_fail (E_IS_XML_DOCUMENT (xml), NULL);
+
+ xmlbuffer = xmlAllocOutputBuffer (NULL);
+ xmlNodeDumpOutput (xmlbuffer, xml->priv->doc, xml->priv->root, 0, 1, NULL);
+ xmlOutputBufferFlush (xmlbuffer);
+
+#ifdef LIBXML2_NEW_BUFFER
+ length = xmlOutputBufferGetSize (xmlbuffer);
+ text = g_strndup ((const gchar *) xmlOutputBufferGetContent (xmlbuffer), length);
+#else
+ length = xmlbuffer->buffer->use;
+ text = g_strndup ((const gchar *) xmlbuffer->buffer->content, length);
+#endif
+
+ xmlOutputBufferClose (xmlbuffer);
+
+ if (out_length)
+ *out_length = length;
+
+ return text;
+}
+
+/**
+ * e_xml_document_add_namespaces:
+ * @xml: an #EXmlDocument
+ * @ns_prefix: namespace prefix to use for this namespace
+ * @ns_href: namespace href
+ * @...: %NULL-terminated pairs of (ns_prefix, ns_href)
+ *
+ * Adds one or more namespaces to @xml, which can be referenced
+ * later by @ns_href. The caller should take care that neither
+ * used @ns_prefix, nor @ns_href, is already used by @xml.
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_add_namespaces (EXmlDocument *xml,
+ const gchar *ns_prefix,
+ const gchar *ns_href,
+ ...)
+{
+ xmlNsPtr ns;
+ va_list va;
+
+ g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+ g_return_if_fail (ns_prefix != NULL);
+ g_return_if_fail (xml->priv->root != NULL);
+
+ if (!ns_href)
+ ns_href = "";
+
+ if (!g_hash_table_contains (xml->priv->namespaces_by_href, ns_href)) {
+ ns = xmlNewNs (xml->priv->root, (const xmlChar *) ns_href, (const xmlChar *) ns_prefix);
+ g_return_if_fail (ns != NULL);
+
+ g_hash_table_insert (xml->priv->namespaces_by_href, g_strdup (ns_href), ns);
+ }
+
+ va_start (va, ns_href);
+
+ while (ns_prefix = va_arg (va, const gchar *), ns_prefix) {
+ ns_href = va_arg (va, const gchar *);
+ if (!ns_href)
+ ns_href = "";
+
+ if (!g_hash_table_contains (xml->priv->namespaces_by_href, ns_href)) {
+ ns = xmlNewNs (xml->priv->root, (const xmlChar *) ns_href, (const xmlChar *)
ns_prefix);
+ g_return_if_fail (ns != NULL);
+
+ g_hash_table_insert (xml->priv->namespaces_by_href, g_strdup (ns_href), ns);
+ }
+ }
+
+ va_end (va);
+}
+
+static gchar *
+e_xml_document_number_to_alpha (gint number)
+{
+ GString *alpha;
+
+ g_return_val_if_fail (number >= 0, NULL);
+
+ alpha = g_string_new ("");
+ g_string_append_c (alpha, 'A' + (number % 26));
+
+ while (number = number / 26, number > 0) {
+ g_string_prepend_c (alpha, 'A' + (number % 26));
+ }
+
+ return g_string_free (alpha, FALSE);
+}
+
+static gchar *
+e_xml_document_gen_ns_prefix (EXmlDocument *xml,
+ const gchar *ns_href)
+{
+ GHashTable *prefixes;
+ GHashTableIter iter;
+ gpointer value;
+ gchar *new_prefix = NULL;
+ const gchar *ptr;
+ gint counter = 0, n_prefixes;
+
+ g_return_val_if_fail (E_IS_XML_DOCUMENT (xml), NULL);
+ g_return_val_if_fail (ns_href && *ns_href, NULL);
+
+ if (!ns_href)
+ return NULL;
+
+ prefixes = g_hash_table_new (g_str_hash, g_str_equal);
+
+ g_hash_table_iter_init (&iter, xml->priv->namespaces_by_href);
+ while (g_hash_table_iter_next (&iter, NULL, &value)) {
+ xmlNsPtr ns = value;
+
+ if (ns && ns->prefix)
+ g_hash_table_insert (prefixes, (gpointer) ns->prefix, NULL);
+ }
+
+ ptr = strrchr (ns_href, ':');
+
+ /* the ns_href ends with ':' */
+ if (ptr && !ptr[1] && g_ascii_isalpha (ns_href[0])) {
+ new_prefix = g_strndup (ns_href, 1);
+ } else if (ptr && strchr (ns_href, ':') < ptr && g_ascii_isalpha (ptr[1])) {
+ new_prefix = g_strndup (ptr + 1, 1);
+ } else if (g_str_has_prefix (ns_href, "http://") &&
+ g_ascii_isalpha (ns_href[7])) {
+ new_prefix = g_strndup (ns_href + 7, 1);
+ }
+
+ n_prefixes = g_hash_table_size (prefixes);
+
+ while (!new_prefix || g_hash_table_contains (prefixes, new_prefix)) {
+ g_free (new_prefix);
+
+ if (counter > n_prefixes + 2) {
+ new_prefix = NULL;
+ break;
+ }
+
+ new_prefix = e_xml_document_number_to_alpha (counter);
+ counter++;
+ }
+
+ g_hash_table_destroy (prefixes);
+
+ return new_prefix;
+}
+
+static xmlNsPtr
+e_xml_document_ensure_namespace (EXmlDocument *xml,
+ const gchar *ns_href)
+{
+ xmlNsPtr ns;
+ gchar *ns_prefix;
+
+ g_return_val_if_fail (E_IS_XML_DOCUMENT (xml), NULL);
+
+ if (!ns_href)
+ return NULL;
+
+ ns = g_hash_table_lookup (xml->priv->namespaces_by_href, ns_href);
+ if (ns || !*ns_href)
+ return ns;
+
+ ns_prefix = e_xml_document_gen_ns_prefix (xml, ns_href);
+
+ e_xml_document_add_namespaces (xml, ns_prefix, ns_href, NULL);
+
+ g_free (ns_prefix);
+
+ return g_hash_table_lookup (xml->priv->namespaces_by_href, ns_href);
+}
+
+/**
+ * e_xml_document_start_element:
+ * @xml: an #EXmlDocument
+ * @ns_href: (nullable): optional namespace href for the new element, or %NULL
+ * @name: name of the new element
+ *
+ * Starts a new non-text element as a child of the current element.
+ * Each such call should be ended with corresponding e_xml_document_end_element().
+ * Use %NULL @ns_href, to use the default namespace, otherwise either previously
+ * added namespace with the same href from e_xml_document_add_namespaces() is picked,
+ * or a new namespace with generated prefix is added.
+ *
+ * To start a text node use e_xml_document_start_text_element().
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_start_element (EXmlDocument *xml,
+ const gchar *ns_href,
+ const gchar *name)
+{
+ g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+ g_return_if_fail (name != NULL);
+ g_return_if_fail (*name);
+ g_return_if_fail (xml->priv->current_element != NULL);
+
+ xml->priv->current_element = xmlNewChild (xml->priv->current_element,
+ e_xml_document_ensure_namespace (xml, ns_href), (const xmlChar *) name, NULL);
+}
+
+/**
+ * e_xml_document_start_text_element:
+ * @xml: an #EXmlDocument
+ * @ns_href: (nullable): optional namespace href for the new element, or %NULL
+ * @name: name of the new element
+ *
+ * Starts a new text element as a child of the current element.
+ * Each such call should be ended with corresponding e_xml_document_end_element().
+ * Use %NULL @ns_href, to use the default namespace, otherwise either previously
+ * added namespace with the same href from e_xml_document_add_namespaces() is picked,
+ * or a new namespace with generated prefix is added.
+ *
+ * To start a non-text node use e_xml_document_start_element().
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_start_text_element (EXmlDocument *xml,
+ const gchar *ns_href,
+ const gchar *name)
+{
+ g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+ g_return_if_fail (name != NULL);
+ g_return_if_fail (*name);
+ g_return_if_fail (xml->priv->current_element != NULL);
+
+ xml->priv->current_element = xmlNewTextChild (xml->priv->current_element,
+ e_xml_document_ensure_namespace (xml, ns_href), (const xmlChar *) name, NULL);
+}
+
+/**
+ * e_xml_document_end_element:
+ * @xml: an #EXmlDocument
+ *
+ * This is a pair function for e_xml_document_start_element() and
+ * e_xml_document_start_text_element(), which changes current
+ * element to the parent of that element.
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_end_element (EXmlDocument *xml)
+{
+ g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+ g_return_if_fail (xml->priv->current_element != NULL);
+ g_return_if_fail (xml->priv->current_element != xml->priv->root);
+
+ xml->priv->current_element = xml->priv->current_element->parent;
+}
+
+/**
+ * e_xml_document_add_empty_element:
+ * @xml: an #EXmlDocument
+ * @ns_href: (nullable): optional namespace href for the new element, or %NULL
+ * @name: name of the new element
+ *
+ * Adds an empty element, which is an element with no attribute and no value.
+ *
+ * It's the same as calling e_xml_document_start_element() immediately
+ * followed by e_xml_document_end_element().
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_add_empty_element (EXmlDocument *xml,
+ const gchar *ns_href,
+ const gchar *name)
+{
+ g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+ g_return_if_fail (name != NULL);
+ g_return_if_fail (*name);
+ g_return_if_fail (xml->priv->current_element != NULL);
+
+ e_xml_document_start_element (xml, ns_href, name);
+ e_xml_document_end_element (xml);
+}
+
+/**
+ * e_xml_document_add_attribute:
+ * @xml: an #EXmlDocument
+ * @ns_href: (nullable): optional namespace href for the new attribute, or %NULL
+ * @name: name of the attribute
+ * @value: value of the attribute
+ *
+ * Adds a new attribute to the current element.
+ * Use %NULL @ns_href, to use the default namespace, otherwise either previously
+ * added namespace with the same href from e_xml_document_add_namespaces() is picked,
+ * or a new namespace with generated prefix is added.
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_add_attribute (EXmlDocument *xml,
+ const gchar *ns_href,
+ const gchar *name,
+ const gchar *value)
+{
+ g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+ g_return_if_fail (xml->priv->current_element != NULL);
+ g_return_if_fail (name != NULL);
+ g_return_if_fail (value != NULL);
+
+ xmlNewNsProp (
+ xml->priv->current_element,
+ e_xml_document_ensure_namespace (xml, ns_href),
+ (const xmlChar *) name,
+ (const xmlChar *) value);
+}
+
+/**
+ * e_xml_document_add_attribute_int:
+ * @xml: an #EXmlDocument
+ * @ns_href: (nullable): optional namespace href for the new attribute, or %NULL
+ * @name: name of the attribute
+ * @value: integer value of the attribute
+ *
+ * Adds a new attribute with an integer value to the current element.
+ * Use %NULL @ns_href, to use the default namespace, otherwise either previously
+ * added namespace with the same href from e_xml_document_add_namespaces() is picked,
+ * or a new namespace with generated prefix is added.
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_add_attribute_int (EXmlDocument *xml,
+ const gchar *ns_href,
+ const gchar *name,
+ gint64 value)
+{
+ gchar *strvalue;
+
+ g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+ g_return_if_fail (xml->priv->current_element != NULL);
+ g_return_if_fail (name != NULL);
+
+ strvalue = g_strdup_printf ("%" G_GINT64_FORMAT, value);
+ e_xml_document_add_attribute (xml, ns_href, name, strvalue);
+ g_free (strvalue);
+}
+
+/**
+ * e_xml_document_add_attribute_double:
+ * @xml: an #EXmlDocument
+ * @ns_href: (nullable): optional namespace href for the new attribute, or %NULL
+ * @name: name of the attribute
+ * @value: double value of the attribute
+ *
+ * Adds a new attribute with a double value to the current element.
+ * Use %NULL @ns_href, to use the default namespace, otherwise either previously
+ * added namespace with the same href from e_xml_document_add_namespaces() is picked,
+ * or a new namespace with generated prefix is added.
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_add_attribute_double (EXmlDocument *xml,
+ const gchar *ns_href,
+ const gchar *name,
+ gdouble value)
+{
+ gchar *strvalue;
+
+ g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+ g_return_if_fail (xml->priv->current_element != NULL);
+ g_return_if_fail (name != NULL);
+
+ strvalue = g_strdup_printf ("%f", value);
+ e_xml_document_add_attribute (xml, ns_href, name, strvalue);
+ g_free (strvalue);
+}
+
+/**
+ * e_xml_document_add_attribute_time:
+ * @xml: an #EXmlDocument
+ * @ns_href: (nullable): optional namespace href for the new attribute, or %NULL
+ * @name: name of the attribute
+ * @value: time_t value of the attribute
+ *
+ * Adds a new attribute with a time_t value in ISO 8601 format to the current element.
+ * The format is "YYYY-MM-DDTHH:MM:SSZ".
+ * Use %NULL @ns_href, to use the default namespace, otherwise either previously
+ * added namespace with the same href from e_xml_document_add_namespaces() is picked,
+ * or a new namespace with generated prefix is added.
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_add_attribute_time (EXmlDocument *xml,
+ const gchar *ns_href,
+ const gchar *name,
+ time_t value)
+{
+ GTimeVal tv;
+ gchar *strvalue;
+
+ g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+ g_return_if_fail (xml->priv->current_element != NULL);
+ g_return_if_fail (name != NULL);
+
+ tv.tv_usec = 0;
+ tv.tv_sec = value;
+
+ strvalue = g_time_val_to_iso8601 (&tv);
+ e_xml_document_add_attribute (xml, ns_href, name, strvalue);
+ g_free (strvalue);
+}
+
+/**
+ * e_xml_document_write_int:
+ * @xml: an #EXmlDocument
+ * @value: value to write as the content
+ *
+ * Writes @value as content of the current element.
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_write_int (EXmlDocument *xml,
+ gint64 value)
+{
+ gchar *strvalue;
+
+ g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+ g_return_if_fail (xml->priv->current_element != NULL);
+
+ strvalue = g_strdup_printf ("%" G_GINT64_FORMAT, value);
+ e_xml_document_write_string (xml, strvalue);
+ g_free (strvalue);
+}
+
+/**
+ * e_xml_document_write_double:
+ * @xml: an #EXmlDocument
+ * @value: value to write as the content
+ *
+ * Writes @value as content of the current element.
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_write_double (EXmlDocument *xml,
+ gdouble value)
+{
+ gchar *strvalue;
+
+ g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+ g_return_if_fail (xml->priv->current_element != NULL);
+
+ strvalue = g_strdup_printf ("%f", value);
+ e_xml_document_write_string (xml, strvalue);
+ g_free (strvalue);
+}
+
+/**
+ * e_xml_document_write_base64:
+ * @xml: an #EXmlDocument
+ * @value: value to write as the content
+ * @len: length of @value
+ *
+ * Writes @value of length @len, encoded to base64, as content of the current element.
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_write_base64 (EXmlDocument *xml,
+ const gchar *value,
+ gint len)
+{
+ gchar *strvalue;
+
+ g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+ g_return_if_fail (xml->priv->current_element != NULL);
+ g_return_if_fail (value != NULL);
+
+ strvalue = g_base64_encode ((const guchar *) value, len);
+ e_xml_document_write_string (xml, strvalue);
+ g_free (strvalue);
+}
+
+/**
+ * e_xml_document_write_time:
+ * @xml: an #EXmlDocument
+ * @value: value to write as the content
+ *
+ * Writes @value in ISO 8601 format as content of the current element.
+ * The format is "YYYY-MM-DDTHH:MM:SSZ".
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_write_time (EXmlDocument *xml,
+ time_t value)
+{
+ GTimeVal tv;
+ gchar *strvalue;
+
+ g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+ g_return_if_fail (xml->priv->current_element != NULL);
+
+ tv.tv_usec = 0;
+ tv.tv_sec = value;
+
+ strvalue = g_time_val_to_iso8601 (&tv);
+ e_xml_document_write_string (xml, strvalue);
+ g_free (strvalue);
+}
+
+/**
+ * e_xml_document_write_string:
+ * @xml: an #EXmlDocument
+ * @value: value to write as the content
+ *
+ * Writes @value as content of the current element.
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_write_string (EXmlDocument *xml,
+ const gchar *value)
+{
+ g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+ g_return_if_fail (xml->priv->current_element != NULL);
+ g_return_if_fail (value != NULL);
+
+ xmlNodeAddContent (
+ xml->priv->current_element,
+ (const xmlChar *) value);
+}
+
+/**
+ * e_xml_document_write_buffer:
+ * @xml: an #EXmlDocument
+ * @value: value to write as the content
+ * @len: length of @value
+ *
+ * Writes @value of length @len as content of the current element.
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_document_write_buffer (EXmlDocument *xml,
+ const gchar *value,
+ gint len)
+{
+ g_return_if_fail (E_IS_XML_DOCUMENT (xml));
+ g_return_if_fail (xml->priv->current_element != NULL);
+ g_return_if_fail (value != NULL);
+
+ xmlNodeAddContentLen (
+ xml->priv->current_element,
+ (const xmlChar *) value, len);
+}
diff --git a/src/libedataserver/e-xml-document.h b/src/libedataserver/e-xml-document.h
new file mode 100644
index 0000000..e1fcf94
--- /dev/null
+++ b/src/libedataserver/e-xml-document.h
@@ -0,0 +1,134 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#if !defined (__LIBEDATASERVER_H_INSIDE__) && !defined (LIBEDATASERVER_COMPILATION)
+#error "Only <libedataserver/libedataserver.h> should be included directly."
+#endif
+
+#ifndef E_XML_DOCUMENT_H
+#define E_XML_DOCUMENT_H
+
+#include <glib.h>
+#include <glib-object.h>
+#include <libxml/parser.h>
+
+/* Standard GObject macros */
+#define E_TYPE_XML_DOCUMENT \
+ (e_xml_document_get_type ())
+#define E_XML_DOCUMENT(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST \
+ ((obj), E_TYPE_XML_DOCUMENT, EXmlDocument))
+#define E_XML_DOCUMENT_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_CAST \
+ ((cls), E_TYPE_XML_DOCUMENT, EXmlDocumentClass))
+#define E_IS_XML_DOCUMENT(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE \
+ ((obj), E_TYPE_XML_DOCUMENT))
+#define E_IS_XML_DOCUMENT_CLASS(cls) \
+ (G_TYPE_CHECK_CLASS_TYPE \
+ ((cls), E_TYPE_XML_DOCUMENT))
+#define E_XML_DOCUMENT_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS \
+ ((obj), E_TYPE_XML_DOCUMENT, EXmlDocumentClass))
+
+G_BEGIN_DECLS
+
+typedef struct _EXmlDocument EXmlDocument;
+typedef struct _EXmlDocumentClass EXmlDocumentClass;
+typedef struct _EXmlDocumentPrivate EXmlDocumentPrivate;
+
+/**
+ * EXmlDocument:
+ *
+ * Contains only private data that should be read and manipulated using the
+ * functions below.
+ *
+ * Since: 3.26
+ **/
+struct _EXmlDocument {
+ /*< private >*/
+ GObject parent;
+ EXmlDocumentPrivate *priv;
+};
+
+struct _EXmlDocumentClass {
+ GObjectClass parent_class;
+
+ /* Padding for future expansion */
+ gpointer reserved[10];
+};
+
+GType e_xml_document_get_type (void) G_GNUC_CONST;
+
+EXmlDocument * e_xml_document_new (const gchar *ns_href,
+ const gchar *root_element);
+xmlDocPtr e_xml_document_get_xmldoc (EXmlDocument *xml);
+gchar * e_xml_document_get_content (const EXmlDocument *xml,
+ gsize *out_length);
+void e_xml_document_add_namespaces (EXmlDocument *xml,
+ const gchar *ns_prefix,
+ const gchar *ns_href,
+ ...) G_GNUC_NULL_TERMINATED;
+void e_xml_document_start_element (EXmlDocument *xml,
+ const gchar *ns_href,
+ const gchar *name);
+void e_xml_document_start_text_element
+ (EXmlDocument *xml,
+ const gchar *ns_href,
+ const gchar *name);
+void e_xml_document_end_element (EXmlDocument *xml);
+void e_xml_document_add_empty_element
+ (EXmlDocument *xml,
+ const gchar *ns_href,
+ const gchar *name);
+void e_xml_document_add_attribute (EXmlDocument *xml,
+ const gchar *ns_href,
+ const gchar *name,
+ const gchar *value);
+void e_xml_document_add_attribute_int
+ (EXmlDocument *xml,
+ const gchar *ns_href,
+ const gchar *name,
+ gint64 value);
+void e_xml_document_add_attribute_double
+ (EXmlDocument *xml,
+ const gchar *ns_href,
+ const gchar *name,
+ gdouble value);
+void e_xml_document_add_attribute_time
+ (EXmlDocument *xml,
+ const gchar *ns_href,
+ const gchar *name,
+ time_t value);
+void e_xml_document_write_int (EXmlDocument *xml,
+ gint64 value);
+void e_xml_document_write_double (EXmlDocument *xml,
+ gdouble value);
+void e_xml_document_write_base64 (EXmlDocument *xml,
+ const gchar *value,
+ gint len);
+void e_xml_document_write_time (EXmlDocument *xml,
+ time_t value);
+void e_xml_document_write_string (EXmlDocument *xml,
+ const gchar *value);
+void e_xml_document_write_buffer (EXmlDocument *xml,
+ const gchar *value,
+ gint len);
+
+G_END_DECLS
+
+#endif /* E_XML_DOCUMENT_H */
diff --git a/src/libedataserver/e-xml-utils.c b/src/libedataserver/e-xml-utils.c
index 56f0f66..4b7a267 100644
--- a/src/libedataserver/e-xml-utils.c
+++ b/src/libedataserver/e-xml-utils.c
@@ -28,6 +28,7 @@
#include <libxml/parser.h>
#include <libxml/tree.h>
+#include <libxml/xpathInternals.h>
#include <glib/gstdio.h>
@@ -177,3 +178,267 @@ e_xml_get_child_by_name (const xmlNode *parent,
return NULL;
}
+/**
+ * e_xml_parse_data:
+ * @data: an XML data
+ * @length: (length-of data): length of data, should be greated than zero
+ *
+ * Parses XML data into an #xmlDocPtr. Free returned pointer
+ * with xmlFreeDoc(), when no longer needed.
+ *
+ * Returns: (nullable) (transfer full): a new #xmlDocPtr with parsed @data,
+ * or %NULL on error.
+ *
+ * Since: 3.26
+ **/
+xmlDocPtr
+e_xml_parse_data (gconstpointer data,
+ gsize length)
+{
+ g_return_val_if_fail (data != NULL, NULL);
+ g_return_val_if_fail (length > 0, NULL);
+
+ return xmlReadMemory (data, length, "data.xml", NULL, 0);
+}
+
+/**
+ * e_xml_new_xpath_context_with_namespaces:
+ * @doc: an #xmlDocPtr
+ * @...: %NULL-terminated list of pairs (prefix, href) with namespaces
+ *
+ * Creates a new #xmlXPathContextPtr on @doc with preregistered
+ * namespaces. The namepsaces are pair of (prefix, href), terminated
+ * by %NULL.
+ *
+ * Returns: (transfer full): a new #xmlXPathContextPtr. Free the returned
+ * pointer with xmlXPathFreeContext() when no longer needed.
+ *
+ * Since: 3.26
+ **/
+xmlXPathContextPtr
+e_xml_new_xpath_context_with_namespaces (xmlDocPtr doc,
+ ...)
+{
+ xmlXPathContextPtr xpath_ctx;
+ va_list va;
+ const gchar *prefix;
+
+ g_return_val_if_fail (doc != NULL, NULL);
+
+ xpath_ctx = xmlXPathNewContext (doc);
+ g_return_val_if_fail (xpath_ctx != NULL, NULL);
+
+ va_start (va, doc);
+
+ while (prefix = va_arg (va, const gchar *), prefix) {
+ const gchar *href = va_arg (va, const gchar *);
+
+ if (!href) {
+ g_warn_if_fail (href != NULL);
+ break;
+ }
+
+ xmlXPathRegisterNs (xpath_ctx, (const xmlChar *) prefix, (const xmlChar *) href);
+ }
+
+ va_end (va);
+
+ return xpath_ctx;
+}
+
+/**
+ * e_xml_xpath_context_register_namespaces:
+ * @xpath_ctx: an #xmlXPathContextPtr
+ * @prefix: namespace prefix
+ * @href: namespace href
+ * @...: %NULL-terminated list of pairs (prefix, href) with additional namespaces
+ *
+ * Registers one or more additional namespaces. It's a caller's error
+ * to try to register a namespace with the same prefix again, unless
+ * the prefix uses the same namespace href.
+ *
+ * Since: 3.26
+ **/
+void
+e_xml_xpath_context_register_namespaces (xmlXPathContextPtr xpath_ctx,
+ const gchar *prefix,
+ const gchar *href,
+ ...)
+{
+ va_list va;
+ const gchar *used_href;
+
+ g_return_if_fail (xpath_ctx != NULL);
+ g_return_if_fail (prefix != NULL);
+ g_return_if_fail (href != NULL);
+
+ used_href = (const gchar *) xmlXPathNsLookup (xpath_ctx, (const xmlChar *) prefix);
+ if (used_href && g_strcmp0 (used_href, href) != 0) {
+ g_warning ("%s: Trying to register prefix '%s' with href '%s', but it already points to '%s'",
+ G_STRFUNC, prefix, href, used_href);
+ } else if (!used_href) {
+ xmlXPathRegisterNs (xpath_ctx, (const xmlChar *) prefix, (const xmlChar *) href);
+ }
+
+ va_start (va, href);
+
+ while (prefix = va_arg (va, const gchar *), prefix) {
+ href = va_arg (va, const gchar *);
+
+ if (!href) {
+ g_warn_if_fail (href != NULL);
+ break;
+ }
+
+ used_href = (const gchar *) xmlXPathNsLookup (xpath_ctx, (const xmlChar *) prefix);
+ if (used_href && g_strcmp0 (used_href, href) != 0) {
+ g_warning ("%s: Trying to register prefix '%s' with href '%s', but it already points
to '%s'",
+ G_STRFUNC, prefix, href, used_href);
+ } else if (!used_href) {
+ xmlXPathRegisterNs (xpath_ctx, (const xmlChar *) prefix, (const xmlChar *) href);
+ }
+ }
+
+ va_end (va);
+}
+
+/**
+ * e_xml_xpath_eval:
+ * @xpath_ctx: an #xmlXPathContextPtr
+ * @format: printf-like format specifier of path to evaluate
+ * @...: arguments for the @format
+ *
+ * Evaluates path specified by @format and returns its #xmlXPathObjectPtr,
+ * in case the path evaluates to a non-empty node set. See also
+ * e_xml_xpath_eval_as_string() which evaluates the path to string.
+ *
+ * Returns: (nullable) (transfer full): a new #xmlXPathObjectPtr which
+ * references given path, or %NULL if path cannot be found or when
+ * it evaluates to an empty nodeset. Free returned pointer with
+ * xmlXPathFreeObject(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+xmlXPathObjectPtr
+e_xml_xpath_eval (xmlXPathContextPtr xpath_ctx,
+ const gchar *format,
+ ...)
+{
+ xmlXPathObjectPtr object;
+ va_list va;
+ gchar *expr;
+
+ g_return_val_if_fail (xpath_ctx != NULL, NULL);
+ g_return_val_if_fail (format != NULL, NULL);
+
+ va_start (va, format);
+ expr = g_strdup_vprintf (format, va);
+ va_end (va);
+
+ object = xmlXPathEvalExpression ((const xmlChar *) expr, xpath_ctx);
+ g_free (expr);
+
+ if (!object)
+ return NULL;
+
+ if (object->type == XPATH_NODESET &&
+ xmlXPathNodeSetIsEmpty (object->nodesetval)) {
+ xmlXPathFreeObject (object);
+ return NULL;
+ }
+
+ return object;
+}
+
+/**
+ * e_xml_xpath_eval_as_string:
+ * @xpath_ctx: an #xmlXPathContextPtr
+ * @format: printf-like format specifier of path to evaluate
+ * @...: arguments for the @format
+ *
+ * Evaluates path specified by @format and returns its result as string,
+ * in case the path evaluates to a non-empty node set. See also
+ * e_xml_xpath_eval() which evaluates the path to an #xmlXPathObjectPtr.
+ *
+ * Returns: (nullable) (transfer full): a new string which contains value
+ * of the given path, or %NULL if path cannot be found or when
+ * it evaluates to an empty nodeset. Free returned pointer with
+ * g_free(), when no longer needed.
+ *
+ * Since: 3.26
+ **/
+gchar *
+e_xml_xpath_eval_as_string (xmlXPathContextPtr xpath_ctx,
+ const gchar *format,
+ ...)
+{
+ xmlXPathObjectPtr object;
+ va_list va;
+ gchar *expr, *value;
+
+ g_return_val_if_fail (xpath_ctx != NULL, NULL);
+ g_return_val_if_fail (format != NULL, NULL);
+
+ va_start (va, format);
+ expr = g_strdup_vprintf (format, va);
+ va_end (va);
+
+ if (!g_str_has_prefix (format, "string(")) {
+ gchar *tmp = expr;
+
+ expr = g_strconcat ("string(", expr, ")", NULL);
+
+ g_free (tmp);
+ }
+
+ object = e_xml_xpath_eval (xpath_ctx, "%s", expr);
+ if (!object)
+ return NULL;
+
+ if (object->type == XPATH_STRING &&
+ *object->stringval)
+ value = g_strdup ((const gchar *) object->stringval);
+ else
+ value = NULL;
+
+ xmlXPathFreeObject (object);
+
+ return value;
+}
+
+/**
+ * e_xml_xpath_eval_exists:
+ * @xpath_ctx: an #xmlXPathContextPtr
+ * @format: printf-like format specifier of path to evaluate
+ * @...: arguments for the @format
+ *
+ * Evaluates path specified by @format and returns whether it exists.
+ *
+ * Returns: %TRUE, when the given XPath exists, %FALSE otherwise.
+ *
+ * Since: 3.26
+ **/
+gboolean
+e_xml_xpath_eval_exists (xmlXPathContextPtr xpath_ctx,
+ const gchar *format,
+ ...)
+{
+ xmlXPathObjectPtr object;
+ va_list va;
+ gchar *expr;
+
+ g_return_val_if_fail (xpath_ctx != NULL, FALSE);
+ g_return_val_if_fail (format != NULL, FALSE);
+
+ va_start (va, format);
+ expr = g_strdup_vprintf (format, va);
+ va_end (va);
+
+ object = e_xml_xpath_eval (xpath_ctx, "%s", expr);
+ if (!object)
+ return FALSE;
+
+ xmlXPathFreeObject (object);
+
+ return TRUE;
+}
diff --git a/src/libedataserver/e-xml-utils.h b/src/libedataserver/e-xml-utils.h
index de3cebf..5222792 100644
--- a/src/libedataserver/e-xml-utils.h
+++ b/src/libedataserver/e-xml-utils.h
@@ -25,6 +25,7 @@
#include <glib.h>
#include <libxml/parser.h>
+#include <libxml/xpath.h>
G_BEGIN_DECLS
@@ -34,7 +35,28 @@ gint e_xml_save_file (const gchar *filename,
xmlNode * e_xml_get_child_by_name (const xmlNode *parent,
const xmlChar *child_name);
+xmlDocPtr e_xml_parse_data (gconstpointer data,
+ gsize length);
+xmlXPathContextPtr
+ e_xml_new_xpath_context_with_namespaces
+ (xmlDocPtr doc,
+ ...) G_GNUC_NULL_TERMINATED;
+void e_xml_xpath_context_register_namespaces
+ (xmlXPathContextPtr xpath_ctx,
+ const gchar *prefix,
+ const gchar *href,
+ ...) G_GNUC_NULL_TERMINATED;
+xmlXPathObjectPtr
+ e_xml_xpath_eval (xmlXPathContextPtr xpath_ctx,
+ const gchar *format,
+ ...) G_GNUC_PRINTF (2, 3);
+gchar * e_xml_xpath_eval_as_string (xmlXPathContextPtr xpath_ctx,
+ const gchar *format,
+ ...) G_GNUC_PRINTF (2, 3);
+gboolean e_xml_xpath_eval_exists (xmlXPathContextPtr xpath_ctx,
+ const gchar *format,
+ ...) G_GNUC_PRINTF (2, 3);
+
G_END_DECLS
#endif /* E_XML_UTILS_H */
-
diff --git a/src/libedataserver/libedataserver.h b/src/libedataserver/libedataserver.h
index 47b6304..b11f8f8 100644
--- a/src/libedataserver/libedataserver.h
+++ b/src/libedataserver/libedataserver.h
@@ -43,6 +43,7 @@
#include <libedataserver/e-secret-store.h>
#include <libedataserver/e-sexp.h>
#include <libedataserver/e-soup-auth-bearer.h>
+#include <libedataserver/e-soup-session.h>
#include <libedataserver/e-soup-ssl-trust.h>
#include <libedataserver/e-source-address-book.h>
#include <libedataserver/e-source-alarms.h>
@@ -91,6 +92,8 @@
#include <libedataserver/e-uid.h>
#include <libedataserver/e-url.h>
#include <libedataserver/e-webdav-discover.h>
+#include <libedataserver/e-webdav-session.h>
+#include <libedataserver/e-xml-document.h>
#include <libedataserver/e-xml-hash-utils.h>
#include <libedataserver/e-xml-utils.h>
#include <libedataserver/eds-version.h>
diff --git a/tests/libebook/client/test-book-client-view-operations.c
b/tests/libebook/client/test-book-client-view-operations.c
index e126750..7ad3db4 100644
--- a/tests/libebook/client/test-book-client-view-operations.c
+++ b/tests/libebook/client/test-book-client-view-operations.c
@@ -31,6 +31,7 @@ static ETestServerClosure book_closure_direct_async = { E_TEST_SERVER_DIRECT_ADD
#define N_CONTACTS 5
typedef struct {
+ ESourceRegistry *registry;
ETestServerClosure *closure;
GThread *thread;
const gchar *book_uid;
@@ -179,7 +180,6 @@ static gpointer
test_view_thread_async (ThreadData *data)
{
GMainContext *context;
- ESourceRegistry *registry;
ESource *source;
GError *error = NULL;
@@ -188,18 +188,14 @@ test_view_thread_async (ThreadData *data)
g_main_context_push_thread_default (context);
/* Open the test book client in this thread */
- registry = e_source_registry_new_sync (NULL, &error);
- if (!registry)
- g_error ("Unable to create the registry: %s", error->message);
-
- source = e_source_registry_ref_source (registry, data->book_uid);
+ source = e_source_registry_ref_source (data->registry, data->book_uid);
if (!source)
g_error ("Unable to fetch source uid '%s' from the registry", data->book_uid);
if (data->closure->type == E_TEST_SERVER_DIRECT_ADDRESS_BOOK) {
/* There is no Async API to open a direct book for now, let's stick with the sync API
*/
- data->client = (EBookClient *) e_book_client_connect_direct_sync (registry, source, (guint32)
-1, NULL, &error);
+ data->client = (EBookClient *) e_book_client_connect_direct_sync (data->registry, source,
(guint32) -1, NULL, &error);
if (!data->client)
g_error ("Unable to create EBookClient for uid '%s': %s", data->book_uid,
error->message);
@@ -215,7 +211,6 @@ test_view_thread_async (ThreadData *data)
g_main_loop_run (data->loop);
g_object_unref (source);
- g_object_unref (registry);
g_object_unref (data->client);
g_main_context_pop_thread_default (context);
@@ -263,7 +258,6 @@ static gpointer
test_view_thread_sync (ThreadData *data)
{
GMainContext *context;
- ESourceRegistry *registry;
ESource *source;
GError *error = NULL;
@@ -272,16 +266,12 @@ test_view_thread_sync (ThreadData *data)
g_main_context_push_thread_default (context);
/* Open the test book client in this thread */
- registry = e_source_registry_new_sync (NULL, &error);
- if (!registry)
- g_error ("Unable to create the registry: %s", error->message);
-
- source = e_source_registry_ref_source (registry, data->book_uid);
+ source = e_source_registry_ref_source (data->registry, data->book_uid);
if (!source)
g_error ("Unable to fetch source uid '%s' from the registry", data->book_uid);
if (data->closure->type == E_TEST_SERVER_DIRECT_ADDRESS_BOOK)
- data->client = (EBookClient *) e_book_client_connect_direct_sync (registry, source, (guint32)
-1, NULL, &error);
+ data->client = (EBookClient *) e_book_client_connect_direct_sync (data->registry, source,
(guint32) -1, NULL, &error);
else
data->client = (EBookClient *) e_book_client_connect_sync (source, (guint32) -1, NULL,
&error);
@@ -293,7 +283,6 @@ test_view_thread_sync (ThreadData *data)
g_main_loop_run (data->loop);
g_object_unref (source);
- g_object_unref (registry);
g_object_unref (data->client);
g_main_context_pop_thread_default (context);
@@ -305,12 +294,16 @@ test_view_thread_sync (ThreadData *data)
static ThreadData *
create_test_thread (const gchar *book_uid,
+ ESourceRegistry *registry,
gconstpointer user_data,
gboolean sync)
{
ThreadData *data = g_slice_new0 (ThreadData);
+ g_assert_nonnull (registry);
+
data->book_uid = book_uid;
+ data->registry = registry;
data->closure = (ETestServerClosure *) user_data;
g_mutex_init (&data->complete_mutex);
@@ -352,7 +345,7 @@ test_concurrent_views (ETestServerFixture *fixture,
/* Create all concurrent threads accessing the same addressbook */
tests = g_new0 (ThreadData *, N_THREADS);
for (i = 0; i < N_THREADS; i++)
- tests[i] = create_test_thread (book_uid, user_data, sync);
+ tests[i] = create_test_thread (book_uid, fixture->registry, user_data, sync);
/* Wait for all threads to receive the complete signal */
for (i = 0; i < N_THREADS; i++) {
diff --git a/tests/libebook/client/test-book-client-write-write.c
b/tests/libebook/client/test-book-client-write-write.c
index ee888e3..00e6cae 100644
--- a/tests/libebook/client/test-book-client-write-write.c
+++ b/tests/libebook/client/test-book-client-write-write.c
@@ -45,6 +45,7 @@ typedef struct {
} TestData;
typedef struct {
+ ESourceRegistry *registry;
GThread *thread;
const gchar *book_uid;
const gchar *contact_uid;
@@ -182,7 +183,6 @@ static gpointer
test_write_thread (ThreadData *data)
{
GMainContext *context;
- ESourceRegistry *registry;
GSource *gsource;
ESource *source;
GError *error = NULL;
@@ -192,11 +192,7 @@ test_write_thread (ThreadData *data)
g_main_context_push_thread_default (context);
/* Open the test book client in this thread */
- registry = e_source_registry_new_sync (NULL, &error);
- if (!registry)
- g_error ("Unable to create the registry: %s", error->message);
-
- source = e_source_registry_ref_source (registry, data->book_uid);
+ source = e_source_registry_ref_source (data->registry, data->book_uid);
if (!source)
g_error ("Unable to fetch source uid '%s' from the registry", data->book_uid);
@@ -212,7 +208,6 @@ test_write_thread (ThreadData *data)
g_main_loop_run (data->loop);
g_object_unref (source);
- g_object_unref (registry);
g_object_unref (data->client);
g_main_context_pop_thread_default (context);
@@ -223,7 +218,8 @@ test_write_thread (ThreadData *data)
}
static ThreadData *
-create_test_thread (const gchar *book_uid,
+create_test_thread (ESourceRegistry *registry,
+ const gchar *book_uid,
const gchar *contact_uid,
EContactField field,
const gchar *value)
@@ -231,6 +227,9 @@ create_test_thread (const gchar *book_uid,
ThreadData *data = g_slice_new0 (ThreadData);
const gchar *name = e_contact_field_name (field);
+ g_assert_nonnull (registry);
+
+ data->registry = registry;
data->book_uid = book_uid;
data->contact_uid = contact_uid;
data->field = field;
@@ -276,6 +275,7 @@ test_concurrent_writes (ETestServerFixture *fixture,
tests = g_new0 (ThreadData *, G_N_ELEMENTS (field_tests));
for (i = 0; i < G_N_ELEMENTS (field_tests); i++)
tests[i] = create_test_thread (
+ fixture->registry,
book_uid, contact_uid,
field_tests[i].field,
field_tests[i].value);
diff --git a/tests/libebook/data/vcards/.gitattributes b/tests/libebook/data/vcards/.gitattributes
new file mode 100644
index 0000000..e668d08
--- /dev/null
+++ b/tests/libebook/data/vcards/.gitattributes
@@ -0,0 +1 @@
+*.vcf eol=crlf
diff --git a/tests/libebook/data/vcards/custom-1.vcf b/tests/libebook/data/vcards/custom-1.vcf
index ae8dcfe..ea3aaef 100644
--- a/tests/libebook/data/vcards/custom-1.vcf
+++ b/tests/libebook/data/vcards/custom-1.vcf
@@ -1,5 +1,6 @@
BEGIN:VCARD
UID:custom-1
+REV:0
FN:Micheal Jackson
TEL;HOME:+1-221-5423789
EMAIL;TYPE=home,work:micheal jackson com
diff --git a/tests/libebook/data/vcards/custom-2.vcf b/tests/libebook/data/vcards/custom-2.vcf
index c7a1f50..50b208b 100644
--- a/tests/libebook/data/vcards/custom-2.vcf
+++ b/tests/libebook/data/vcards/custom-2.vcf
@@ -1,5 +1,6 @@
BEGIN:VCARD
UID:custom-2
+REV:0
FN:Janet Jackson
N:Janet
TEL;HOME:7654321
diff --git a/tests/libebook/data/vcards/custom-3.vcf b/tests/libebook/data/vcards/custom-3.vcf
index c7fb251..cd8a26d 100644
--- a/tests/libebook/data/vcards/custom-3.vcf
+++ b/tests/libebook/data/vcards/custom-3.vcf
@@ -1,5 +1,6 @@
BEGIN:VCARD
UID:custom-3
+REV:0
FN:Bobby Brown
TEL;HOME:+9999999
EMAIL;TYPE=work:bobby brown org
diff --git a/tests/libebook/data/vcards/custom-4.vcf b/tests/libebook/data/vcards/custom-4.vcf
index 77e2990..3f1b3b4 100644
--- a/tests/libebook/data/vcards/custom-4.vcf
+++ b/tests/libebook/data/vcards/custom-4.vcf
@@ -1,5 +1,6 @@
BEGIN:VCARD
UID:Custom-4
+REV:0
FN:Big Bobby Brown
TEL;TYPE=work,pref:+9999999
EMAIL:big bobby brown org
diff --git a/tests/libebook/data/vcards/custom-5.vcf b/tests/libebook/data/vcards/custom-5.vcf
index 9ade0a4..c28bae9 100644
--- a/tests/libebook/data/vcards/custom-5.vcf
+++ b/tests/libebook/data/vcards/custom-5.vcf
@@ -1,5 +1,6 @@
BEGIN:VCARD
UID:custom-5
+REV:0
FN:James Brown
TEL;HOME:+6666666
EMAIL;TYPE=home,work:james brown com
diff --git a/tests/libebook/data/vcards/custom-6.vcf b/tests/libebook/data/vcards/custom-6.vcf
index a43da5e..cba585d 100644
--- a/tests/libebook/data/vcards/custom-6.vcf
+++ b/tests/libebook/data/vcards/custom-6.vcf
@@ -1,5 +1,6 @@
BEGIN:VCARD
UID:custom-6
+REV:0
TEL;HOME:ask Jenny for Lisa's number
FN:%Stran_ge Name
END:VCARD
diff --git a/tests/libebook/data/vcards/custom-7.vcf b/tests/libebook/data/vcards/custom-7.vcf
index dae210a..66ff9cb 100644
--- a/tests/libebook/data/vcards/custom-7.vcf
+++ b/tests/libebook/data/vcards/custom-7.vcf
@@ -1,5 +1,6 @@
BEGIN:VCARD
UID:custom-7
+REV:0
FN:Purple Goose
TEL;HOME:+49-89-7888 99
KEY;ENCODING=b;TYPE=X509:AA==
diff --git a/tests/libebook/data/vcards/custom-8.vcf b/tests/libebook/data/vcards/custom-8.vcf
index 5306ad0..462a3b8 100644
--- a/tests/libebook/data/vcards/custom-8.vcf
+++ b/tests/libebook/data/vcards/custom-8.vcf
@@ -1,5 +1,6 @@
BEGIN:VCARD
UID:custom-8
+REV:0
FN:Purple Pony
TEL;HOME:+31-221-5423789
EMAIL;TYPE=home,work:purple pony com
diff --git a/tests/libebook/data/vcards/custom-9.vcf b/tests/libebook/data/vcards/custom-9.vcf
index c7a57dd..f07e9f3 100644
--- a/tests/libebook/data/vcards/custom-9.vcf
+++ b/tests/libebook/data/vcards/custom-9.vcf
@@ -1,5 +1,6 @@
BEGIN:VCARD
UID:custom-9
+REV:0
FN:Pink Pony
TEL;HOME:514-845-8436
EMAIL;TYPE=home,work:pink pony com
diff --git a/tests/libebook/data/vcards/logo-1.vcf b/tests/libebook/data/vcards/logo-1.vcf
new file mode 100644
index 0000000..ad1de8c
--- /dev/null
+++ b/tests/libebook/data/vcards/logo-1.vcf
@@ -0,0 +1,21 @@
+BEGIN:VCARD
+VERSION:3.0
+UID:logo-1
+FN:logo
+N:;logo;;;
+LOGO;TYPE="X-EVOLUTION-UNKNOWN";ENCODING=b:/9j/4AAQSkZJRgABAQEASABIAAD/2wB
+ DABYPEBMQDhYTEhMYFxYaIDYjIB4eIEIvMic2TkVSUU1FTEpWYXxpVlx1XUpMbJNtdYCEi4yLV
+ GiZo5eHonyIi4b/2wBDARcYGCAcID8jIz+GWUxZhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoa
+ GhoaGhoaGhoaGhoaGhoaGhoaGhoaGhob/wgARCABAAEADAREAAhEBAxEB/8QAFgABAQEAAAAAA
+ AAAAAAAAAAAAgEE/8QAFwEBAQEBAAAAAAAAAAAAAAAAAAECBP/aAAwDAQACEAMQAAABXP2RDRD
+ ZAhs2Y2UlENkCGzZjZSUQ2QNFNmNlJRDZA0U2Y2UlENkDRTZjZSUQ2QNFNmNlJRDZA0U2Y2UlE
+ NkDRT//xAAUEAEAAAAAAAAAAAAAAAAAAABg/9oACAEBAAEFAgH/xAAUEQEAAAAAAAAAAAAAAAA
+ AAABg/9oACAEDAQE/AQH/xAAUEQEAAAAAAAAAAAAAAAAAAABg/9oACAECAQE/AQH/xAAUEAEAA
+ AAAAAAAAAAAAAAAAABg/9oACAEBAAY/AgH/xAAWEAADAAAAAAAAAAAAAAAAAAABIFD/2gAIAQE
+ AAT8hQ3P/2gAMAwEAAgADAAAAEIzqtZzqlZzqhZzqhZzqhZyqhZyqhZyqhf/EABQRAQAAAAAAA
+ AAAAAAAAAAAAGD/2gAIAQMBAT8QAf/EABQRAQAAAAAAAAAAAAAAAAAAAGD/2gAIAQIBAT8QAf/
+ EABYQAQEBAAAAAAAAAAAAAAAAAAEAEP/aAAgBAQABPxBmZwzMzMzMzMzMzMzMzMzMzMzMzMzMz
+ MzMzMzMzMzMzf/Z
+REV:2017-05-10T14:36:01Z(2)
+EMAIL;TYPE=WORK:logo@no.where
+END:VCARD
\ No newline at end of file
diff --git a/tests/libebook/data/vcards/photo-1.vcf b/tests/libebook/data/vcards/photo-1.vcf
new file mode 100644
index 0000000..4bf5bce
--- /dev/null
+++ b/tests/libebook/data/vcards/photo-1.vcf
@@ -0,0 +1,59 @@
+BEGIN:VCARD
+VERSION:3.0
+UID:photo-1
+FN:photo
+N:;photo;;;
+PHOTO;TYPE="X-EVOLUTION-UNKNOWN";ENCODING=b:iVBORw0KGgoAAAANSUhEUgAAAEAAAAB
+ ACAIAAAAlC+aJAAAKo0lEQVRo3n1aW7LcuA4j6OzkLujufxOzgu5gPsQHIHdNqpKc03bbEh8gC
+ Ar//98/D74Pvonvk98nvw++mZ8Hf5/8PPk38XnwffLz4Jv5/ZN+Jz5P/n3wOR/q1cSnn/b9U8/
+ /PPlNfP9gb8j8ngU8+X3wyX7yfPd8+Of83DdnnlV9E0QQgQBi/oD9ExlxLiACCJCBiGAE++Pg+
+ TbqQl/a2xgR0Esg55HngYhAUBbB4DxmV7BPqwVn/Xb+zteBehUC8bdWBkYQCNbLIoI4//XiSMz
+ r58GYGyLifCH6+STQ1xjYrR+TEmGbYOz+z/6yHj7WYahtQ/fXNgXLGNRnIxgEOMvDOI+13vJWk
+ DHb6LftdiLo4YAIQmwwtg1E5ImLCJLol8i3O5DYNu+nHStD9kHUM6McgbaSuvdcIDo+y0VAhQh
+ PWHECkDxvqd3vV84KElznjmM3D6AxSIlsRuDYGxUVx3mMDmpaUJZLNLdCdkFW/EGiAbXQE6Zio
+ Fj/5URZXay9mmfbYBIx0L2KiWfFJ91IC7EKwVnZiRAy2CjSWwc8OCsMy7PcKM9KAEhoiek3lSG
+ JUTdhgoUWnpi4t0RiG4nrTfSdKAeIkzdj9clUXzKQtVeu1UKTNxpkCBSMKUYqbtCRsvOv4avAg
+ /rwY0oQDWaGZAh1IGaFjbwRAaZEz946KdObsMisxQJmc4a+b5If5gkAZ59sNEOA4IKSoCfVuI1
+ bgrokiBNCtpTGo4bLdYpiPDf2OqwNwvp1kigdRrLNBRDuvQx0kBiQMMqD8/AIJg54U6orFMIOW
+ LIdsxmDwXsEScYFWmO3jo628zG7xFi/nuI8qWDs2HS2UKmXt4Fm24dewFLP0ny5AjfQOkrDDSz
+ lg78WGcCWZAQpZAZbCaUEHITDoRKahUAHnsDT/AAtZwPVqNAk+33siregPw6rbayblo0gJt2x1
+ QNV5SvPuM5DRELsKbQmoOyBoNC6QGjBONawWC9KAa2ZBR77lCFn62OKgTUwFnhPvnFsilxmVyZ
+ WzgH2N6r0yOtjI4K1UsBf3TuD4L2gOql4Wp63r2vJlXji8A4yN+6JYLHlqXVotkPeUSt5j96Mw
+ igHdKlMQpGi6PkG1rhsaQPGV02XsLcdMidkdO6afK8bMZykw3FyYQy4HIQoWhlGnSFQHFJr4ra
+ 5ErCmEVQmJnmVtCiE1fKtu8PP1uvQzIzoZoSNkrxiWVhjvIHIAmfgunF/YWgCvCMugd4er36q6
+ xU9OwdFsdbjWKjQiHfxa8oq7UaRCzh+nMQocgHrCzQLhs/mMlvxHadBxMu/y6UWm9FbBSU8YSX
+ NP+bAsBpXgp4n9ZbKFxOFxSQjt4/Q9o3UfWnKWruLWgCNuQvIH2syMDi7YCjZ14lGy6piSuvKN
+ 4QgUjj49iXUMiCdlPZ6Bxl46uYw2WMJCGGvoIM1gnMHA1Z0ebcP2ntSuXDZKOubVqwHx+70gnU
+ 55eY1GCQEpeQrQa5yxZEigryaDRBT+PhqmSYS65n5RoZu3GkMY2QHWWcRXqh08ILFlV60EQhrD
+ KraFNO2vgD0duXud9KqjkQjHMW3/G0asOGyQIyWHW0iLwihzfysDp1PNJpqtHf7Jyom5A8HhLH
+ BFXesTbQi24HKqzYoK2Ros3koGOFGfjXaNLubKlB/U00P0SJwdfGjbGydOBx/u7u7366s53TUX
+ KJOCvcxCr4AdYL3LnHjobP7hCwmRFnb5rHKUmlLcEBSXY3anE0nwnBUG4AeelW/S2ZVUwbr3Kb
+ TlW0zkotaV9gsXGN8866mu6XJHhohP2DOoYdQ4MRymLiUvuG5LrJyfEsNIayZtlrhWEFVtFvnC
+ NWi4NR9qAyr0GFQdchnV0N4Q7HqmCmlWFF2gD5/cGMhTwo40KYcyypK6Vy9NUQsgxJQYDwi+Wa
+ CU8ksy3qmXeFUOk6nVjnQHAUTs1bDsOwi0JoFl4xOCDHurjuCKkzwJZuNeIANO4Eyo5cSUlilP
+ 5s4FisQ0JRkxEruA9kuPmy5RThv1458Uobq3rvvAqRGmEVQ2FHePmRuOxd6y6BUHl2Et4/a5u0
+ 8VbQA0djDSAQCP7pG9BtMvyMsFlbBHIlnyJwBMJ35wHg9hffuQokXaRFMkCIPEdRxF/qfastMS
+ jgyYb+7fs9f44Alq1aV9AX4pYEobYTmB0e1QrQ3J8cpxevYDBRTUgcDryb/eMCk4kt2iztkgZW
+ 2MLOYu2SKVhDaWc0HvwjfyXcKf+OoghfBmGVn4cpC74olnE5XpIELJOLdDR0h29qJQX5lKAwHf
+ 2tZVkwAhBh0970J3pXYRH2ZMYBcnApihljKPV99J0ftLNnWG9EefsKnRZQKKsMA5b3Cqcriicu
+ fq8qUFId7HAeFhbu/UDOw40JHDlt3DqptRkHUEZo1A9QhkY0GM+It5ZUg9xLjAF1HiM5milsnC
+ uwapFXoT8iLA8DZ+egiWMn5RBa6O8sLQd513QrhzTDm5WouazxbMgJFeNRyj5Wk2Vr4KIi4h2a
+ kRBuDkZgefhJFtLjl1+Vu/hCiAOoEk+9umss9IGIJJfZWOotp0wd1RLaCCD/TUqKQUHYZFKELW
+ 2tAG0bpFGPaAe5wcuunyBDOeVXnUuLHHmBX7fCMn/jI1+GCka+HBbb3Xx3fTQlc599WD/dkxbp
+ lyECO+6oTU7iUHRqv3SQmtf0cOcAHXJAKrrPSe3iN1eurMNHliLGIHm5gUVcOt+iB3rBuLt0ea
+ MlRly65wyBo5yry76uiSpJeYIBr2jRr6rI+LI+mstkAITrHuSINItWkwrRIF3HoLWmFBX523DJ
+ lHLpnBxWoOu01s2Go3GqaLm69vuR1xiuhwmVnrfFqBgqPW/Ecry1ZuPFMhne4DA5mai1v2UF5n
+ 6UdIcIWlJtx4QYhKh9WVD8kS8W/kempieRzfOzsddpF1kANPu6B9/TTCslhghMlaSm6skgXb53
+ Zj0hRsIsjLeI109IJcLxOUdySk0u/0HBVKRf8QWYQCZftBBD2sNMUkq79LTEi5KSR0kLuzLUBe
+ gVp29WxvjGpq6dB/BClx1RJ13tsPC0jX8xpC6JZ+ujnEPUesOGGcMOA5YhOFLz7xMzq7FAPtJ0
+ aVEj8x0xzM0mEUVxa4NieQlaOaansw0d8XZlvsk/pfHdM3+xYhYmK4QwpHMQ1IQPxRia8lFbYS
+ /V0AR02pKbInA2yGZ3LD/N9nYLZtnxOLV76qs0y5iiZTSzVlMD7gIRqUg2UOqPZFOB1tuoamM6
+ sWrjVQkcOBYdwPRklz0k8oUt2uM5Yj2iw0FnLSDNKoWDHdLRnJnAPXnWsGyILJKhjXZsSj+YLu
+ GANutANHxxiaixsbKnijoHVKINdS0b7NqFmRaEYnTSSkNr+FmT2iOT2KlCzN12b2QGwhzy53by
+ UAVxngsPneTwKt9kSdLKxM5OcirRMkxSpcQ/sanTDz8n5kUoYntGl2vuEoLahnWT06qi8jLe2m
+ gXrLVRMFzBdFuQMMW08JU4F34QdLa+QK0zjNYuZ7pl21nKAcPusPmRndDdBGwnBWS/u4wzYhME
+ ew9N6W3PGHcICYvwdQjbvoh2a3Z/QdZQ6zBy6hOkH5oSWjuOMM9mhSkpI86cUB2IVzAg5ZGVCE
+ F5nVDCI3L0ktGde0ZgyjP0XqSHx2GyNb9QAAAAASUVORK5CYII=
+EMAIL;TYPE=WORK:photo@no.where
+REV:2017-05-10T14:34:50Z(0)
+END:VCARD
diff --git a/tests/libebook/vcard/.gitattributes b/tests/libebook/vcard/.gitattributes
new file mode 100644
index 0000000..e668d08
--- /dev/null
+++ b/tests/libebook/vcard/.gitattributes
@@ -0,0 +1 @@
+*.vcf eol=crlf
diff --git a/tests/libebook/vcard/11.vcf b/tests/libebook/vcard/11.vcf
index e56c60d..d385d99 100644
--- a/tests/libebook/vcard/11.vcf
+++ b/tests/libebook/vcard/11.vcf
@@ -1,9 +1,9 @@
-BEGIN:VCARD
-VERSION:3.0
-X-EVOLUTION-FILE-AS:AttributeParam\, Invalid
-FN:Invalid AttributeParam
-N:AttributeParam;Invalid;;;
-NOPARAM;:pas-id-4094434A00000001
-TAIL;PARAM=value;:pas-id-4094434A00000001
-MIDDLE;PARAM=value;;PARAM2=value2:pas-id-4094434A00000001
+BEGIN:VCARD
+VERSION:3.0
+X-EVOLUTION-FILE-AS:AttributeParam\, Invalid
+FN:Invalid AttributeParam
+N:AttributeParam;Invalid;;;
+NOPARAM;:pas-id-4094434A00000001
+TAIL;PARAM=value;:pas-id-4094434A00000001
+MIDDLE;PARAM=value;;PARAM2=value2:pas-id-4094434A00000001
END:VCARD
\ No newline at end of file
diff --git a/tests/libebook/vcard/12.vcf b/tests/libebook/vcard/12.vcf
index d1c0598..7f1e6ec 100644
--- a/tests/libebook/vcard/12.vcf
+++ b/tests/libebook/vcard/12.vcf
@@ -1,23 +1,23 @@
-BEGIN:VCARD
-VERSION:2.1
-N;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=
-Br=C3=BCning;=
-Michael
-FN;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=
-Michael Br=C3=BCning
-TEL;CELL;WORK:+491622433834
-TEL;CELL;HOME:017623384942
-TEL;WORK:+4973117546691
-EMAIL;WORK:ext-Michael Bruning nokia com
-ADR;HOME;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=
-;=
-;=
-M=C3=BCnsterplatz 21;=
-Ulm;=
-Baden-W=C3=BCrttemberg;=
-89073;=
-
-LABEL;HOME;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=
-M=C3=BCnsterplatz 21=0D=0A89073 Ulm, Baden-W=C3=BCrttemberg
-BDAY:19781229
-END:VCARD
+BEGIN:VCARD
+VERSION:2.1
+N;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=
+Br=C3=BCning;=
+Michael
+FN;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=
+Michael Br=C3=BCning
+TEL;CELL;WORK:+491622433834
+TEL;CELL;HOME:017623384942
+TEL;WORK:+4973117546691
+EMAIL;WORK:ext-Michael Bruning nokia com
+ADR;HOME;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=
+;=
+;=
+M=C3=BCnsterplatz 21;=
+Ulm;=
+Baden-W=C3=BCrttemberg;=
+89073;=
+
+LABEL;HOME;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=
+M=C3=BCnsterplatz 21=0D=0A89073 Ulm, Baden-W=C3=BCrttemberg
+BDAY:19781229
+END:VCARD
diff --git a/tests/libebook/vcard/3.vcf b/tests/libebook/vcard/3.vcf
index 0428d6a..3058716 100644
--- a/tests/libebook/vcard/3.vcf
+++ b/tests/libebook/vcard/3.vcf
@@ -1,13 +1,13 @@
-BEGIN:VCARD
-VERSION:2.1
-X-EVOLUTION-FILE-AS:Friedman, Nat
-FN:Nat
-N:Friedman;Nat;D;Mr.
-ADR;POSTAL;WORK:P.O. Box 101;;;Any Town;CA;91921-1234
-TEL;WORK:617 679 1984
-TEL;CELL:123 456 7890
-EMAIL;INTERNET:nat nat org
-EMAIL;INTERNET:nat ximian com
-BDAY:1977-08-06
-UID:pas-id-3E65886900000002
-END:VCARD
+BEGIN:VCARD
+VERSION:2.1
+X-EVOLUTION-FILE-AS:Friedman, Nat
+FN:Nat
+N:Friedman;Nat;D;Mr.
+ADR;POSTAL;WORK:P.O. Box 101;;;Any Town;CA;91921-1234
+TEL;WORK:617 679 1984
+TEL;CELL:123 456 7890
+EMAIL;INTERNET:nat nat org
+EMAIL;INTERNET:nat ximian com
+BDAY:1977-08-06
+UID:pas-id-3E65886900000002
+END:VCARD
diff --git a/tests/libebook/vcard/4.vcf b/tests/libebook/vcard/4.vcf
index 305088a..46878f7 100644
--- a/tests/libebook/vcard/4.vcf
+++ b/tests/libebook/vcard/4.vcf
@@ -1,11 +1,11 @@
-BEGIN:VCARD
-VERSION:2.1
-X-EVOLUTION-FILE-AS:address, canada
-FN:canada address
-N:address;canada
-ADR;WORK:;;92 Main St. N.;Newmarket;ON;L3Y 4A1;Canada
-ADR;HOME;PREF:;;92 Main St. N.;Newmarket;ON;L3Y 4A1;Canada
-LABEL;QUOTED-PRINTABLE;WORK:92 Main St. N.=0ANewmarket, ON L3Y 4A1=0ACanada
-LABEL;QUOTED-PRINTABLE;HOME;PREF:92 Main St. N.=0ANewmarket, ON L3Y 4A1=0ACanada
-UID:pas-id-3E84C16E00000001
-END:VCARD
+BEGIN:VCARD
+VERSION:2.1
+X-EVOLUTION-FILE-AS:address, canada
+FN:canada address
+N:address;canada
+ADR;WORK:;;92 Main St. N.;Newmarket;ON;L3Y 4A1;Canada
+ADR;HOME;PREF:;;92 Main St. N.;Newmarket;ON;L3Y 4A1;Canada
+LABEL;QUOTED-PRINTABLE;WORK:92 Main St. N.=0ANewmarket, ON L3Y 4A1=0ACanada
+LABEL;QUOTED-PRINTABLE;HOME;PREF:92 Main St. N.=0ANewmarket, ON L3Y 4A1=0ACanada
+UID:pas-id-3E84C16E00000001
+END:VCARD
diff --git a/tests/libebook/vcard/5.vcf b/tests/libebook/vcard/5.vcf
index f424330..51bbe20 100644
--- a/tests/libebook/vcard/5.vcf
+++ b/tests/libebook/vcard/5.vcf
@@ -1,32 +1,32 @@
-BEGIN:VCARD
-VERSION:2.1
-X-EVOLUTION-FILE-AS;CHARSET=UTF-8;QUOTED-PRINTABLE:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
-=BA=A7
-FN;CHARSET=UTF-8;QUOTED-PRINTABLE:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
-=BA=A7
-N;CHARSET=UTF-8:;十城目管理大型知座
-ADR;WORK;PREF;QUOTED-PRINTABLE;CHARSET=UTF-8:;=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
-=BA=A7=0A=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=
-=9F=A5=E5=BA=A7=0A=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=
-=9E=8B=E7=9F=A5=E5=BA=A7;=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
-=BA=A7
-LABEL;CHARSET=UTF-8;QUOTED-PRINTABLE;WORK;PREF:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
-=BA=A7=0A=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=
-=9F=A5=E5=BA=A7=0A=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=
-=9E=8B=E7=9F=A5=E5=BA=A7=0A=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=
-=A4=A7=E5=9E=8B=E7=9F=A5=E5=BA=A7
-TEL;CHARSET=UTF-8;QUOTED-PRINTABLE;WORK;VOICE:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
-=BA=A7
-TEL;CHARSET=UTF-8;QUOTED-PRINTABLE;WORK;FAX:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
-=BA=A7
-TEL;CHARSET=UTF-8;QUOTED-PRINTABLE;HOME:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
-=BA=A7
-TEL;CHARSET=UTF-8;QUOTED-PRINTABLE;CELL:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
-=BA=A7
-EMAIL;INTERNET:weird weird com
-ORG;CHARSET=UTF-8:十城目管理大型知座
-TITLE;CHARSET=UTF-8;QUOTED-PRINTABLE:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
-=BA=A7
-UID:pas-id-3E52FE2E00000000
-END:VCARD
-
+BEGIN:VCARD
+VERSION:2.1
+X-EVOLUTION-FILE-AS;CHARSET=UTF-8;QUOTED-PRINTABLE:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
+=BA=A7
+FN;CHARSET=UTF-8;QUOTED-PRINTABLE:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
+=BA=A7
+N;CHARSET=UTF-8:;十城目管理大型知座
+ADR;WORK;PREF;QUOTED-PRINTABLE;CHARSET=UTF-8:;=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
+=BA=A7=0A=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=
+=9F=A5=E5=BA=A7=0A=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=
+=9E=8B=E7=9F=A5=E5=BA=A7;=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
+=BA=A7
+LABEL;CHARSET=UTF-8;QUOTED-PRINTABLE;WORK;PREF:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
+=BA=A7=0A=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=
+=9F=A5=E5=BA=A7=0A=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=
+=9E=8B=E7=9F=A5=E5=BA=A7=0A=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=
+=A4=A7=E5=9E=8B=E7=9F=A5=E5=BA=A7
+TEL;CHARSET=UTF-8;QUOTED-PRINTABLE;WORK;VOICE:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
+=BA=A7
+TEL;CHARSET=UTF-8;QUOTED-PRINTABLE;WORK;FAX:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
+=BA=A7
+TEL;CHARSET=UTF-8;QUOTED-PRINTABLE;HOME:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
+=BA=A7
+TEL;CHARSET=UTF-8;QUOTED-PRINTABLE;CELL:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
+=BA=A7
+EMAIL;INTERNET:weird weird com
+ORG;CHARSET=UTF-8:十城目管理大型知座
+TITLE;CHARSET=UTF-8;QUOTED-PRINTABLE:=E5=8D=81=E5=9F=8E=E7=9B=AE=E7=AE=A1=E7=90=86=E5=A4=A7=E5=9E=8B=E7=9F=A5=E5=
+=BA=A7
+UID:pas-id-3E52FE2E00000000
+END:VCARD
+
diff --git a/tests/libedata-book/CMakeLists.txt b/tests/libedata-book/CMakeLists.txt
index 347196d..8bcc6c1 100644
--- a/tests/libedata-book/CMakeLists.txt
+++ b/tests/libedata-book/CMakeLists.txt
@@ -37,29 +37,31 @@ set(extra_ldflags
set(SOURCES
data-test-utils.c
data-test-utils.h
+ test-book-cache-utils.c
+ test-book-cache-utils.h
)
-add_library(data-test-utils STATIC
+add_library(data-book-test-utils STATIC
${SOURCES}
)
-add_dependencies(data-test-utils
+add_dependencies(data-book-test-utils
edataserver
${extra_deps}
)
-target_compile_definitions(data-test-utils PRIVATE
- -DG_LOG_DOMAIN=\"data-test-utils\"
+target_compile_definitions(data-book-test-utils PRIVATE
+ -DG_LOG_DOMAIN=\"data-book-test-utils\"
${extra_defines}
)
-target_compile_options(data-test-utils PUBLIC
+target_compile_options(data-book-test-utils PUBLIC
${BACKEND_CFLAGS}
${DATA_SERVER_CFLAGS}
${extra_cflags}
)
-target_include_directories(data-test-utils PUBLIC
+target_include_directories(data-book-test-utils PUBLIC
${CMAKE_BINARY_DIR}
${CMAKE_BINARY_DIR}/src
${CMAKE_SOURCE_DIR}/src
@@ -68,7 +70,7 @@ target_include_directories(data-test-utils PUBLIC
${extra_incdirs}
)
-target_link_libraries(data-test-utils
+target_link_libraries(data-book-test-utils
edataserver
${extra_deps}
${BACKEND_LDFLAGS}
@@ -79,7 +81,7 @@ target_link_libraries(data-test-utils
set(extra_deps
ebook
ebook-contacts
- data-test-utils
+ data-book-test-utils
)
set(extra_defines)
@@ -94,6 +96,18 @@ set(extra_defines)
# This is because each migrated test changes the
# locale and reloads the same addressbook of the previous test.
set(TESTS
+ test-book-cache-get-contact
+ test-book-cache-create-cursor
+ test-book-cache-cursor-move-by-posix
+ test-book-cache-cursor-move-by-en-US
+ test-book-cache-cursor-move-by-fr-CA
+ test-book-cache-cursor-move-by-de-DE
+ test-book-cache-cursor-set-target
+ test-book-cache-cursor-calculate
+ test-book-cache-cursor-set-sexp
+ test-book-cache-cursor-change-locale
+ test-book-cache-offline
+ test-book-meta-backend
test-sqlite-get-contact
test-sqlite-create-cursor
test-sqlite-cursor-move-by-posix
diff --git a/tests/libedata-book/data-test-utils.h b/tests/libedata-book/data-test-utils.h
index b72822e..e415101 100644
--- a/tests/libedata-book/data-test-utils.h
+++ b/tests/libedata-book/data-test-utils.h
@@ -22,6 +22,8 @@
#include <libedata-book/libedata-book.h>
+G_BEGIN_DECLS
+
/* This legend shows the add order, and various sort order of the sorted
* vcards. The UIDs of these contacts are formed as 'sorted-1', 'sorted-2' etc
* and the numbering of the contacts is according to the 'N' column in the
@@ -174,4 +176,6 @@ StepData *step_test_new_full (const gchar *test_prefix,
void step_test_add (StepData *data,
gboolean filtered);
+G_END_DECLS
+
#endif /* DATA_TEST_UTILS_H */
diff --git a/tests/libedata-book/test-book-cache-create-cursor.c
b/tests/libedata-book/test-book-cache-create-cursor.c
new file mode 100644
index 0000000..c4cc6e1
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-create-cursor.c
@@ -0,0 +1,125 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libebook/libebook.h>
+
+#include "test-book-cache-utils.h"
+
+static TCUClosure closure = { NULL };
+
+static void
+test_create_cursor_empty_query (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ EBookCacheCursor *cursor;
+ EContactField sort_fields[] = { E_CONTACT_FAMILY_NAME, E_CONTACT_GIVEN_NAME };
+ EBookCursorSortType sort_types[] = { E_BOOK_CURSOR_SORT_ASCENDING, E_BOOK_CURSOR_SORT_ASCENDING };
+ GError *error = NULL;
+
+ cursor = e_book_cache_cursor_new (
+ fixture->book_cache, NULL,
+ sort_fields, sort_types, 2, &error);
+
+ g_assert (cursor != NULL);
+ e_book_cache_cursor_free (fixture->book_cache, cursor);
+}
+
+static void
+test_create_cursor_valid_query (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ EBookCacheCursor *cursor;
+ EContactField sort_fields[] = { E_CONTACT_FAMILY_NAME, E_CONTACT_GIVEN_NAME };
+ EBookCursorSortType sort_types[] = { E_BOOK_CURSOR_SORT_ASCENDING, E_BOOK_CURSOR_SORT_ASCENDING };
+ EBookQuery *query;
+ gchar *sexp;
+ GError *error = NULL;
+
+ query = e_book_query_field_test (E_CONTACT_FULL_NAME, E_BOOK_QUERY_IS, "James Brown");
+ sexp = e_book_query_to_string (query);
+
+ cursor = e_book_cache_cursor_new (
+ fixture->book_cache, sexp,
+ sort_fields, sort_types, 2, &error);
+
+ g_assert (cursor != NULL);
+ e_book_cache_cursor_free (fixture->book_cache, cursor);
+ g_free (sexp);
+ e_book_query_unref (query);
+}
+
+static void
+test_create_cursor_invalid_sort (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ EBookCacheCursor *cursor;
+ EContactField sort_fields[] = { E_CONTACT_TEL };
+ EBookCursorSortType sort_types[] = { E_BOOK_CURSOR_SORT_ASCENDING };
+ GError *error = NULL;
+
+ cursor = e_book_cache_cursor_new (
+ fixture->book_cache, NULL,
+ sort_fields, sort_types, 1, &error);
+
+ g_assert (cursor == NULL);
+ g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY);
+ g_clear_error (&error);
+}
+
+static void
+test_create_cursor_missing_sort (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ EBookCacheCursor *cursor;
+ GError *error = NULL;
+
+ cursor = e_book_cache_cursor_new (fixture->book_cache, NULL, NULL, NULL, 0, &error);
+
+ g_assert (cursor == NULL);
+ g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY);
+ g_clear_error (&error);
+}
+
+gint
+main (gint argc,
+ gchar **argv)
+{
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+ g_type_init ();
+#endif
+ g_test_init (&argc, &argv, NULL);
+
+ /* Ensure that the client and server get the same locale */
+ g_assert (g_setenv ("LC_ALL", "en_US.UTF-8", TRUE));
+ setlocale (LC_ALL, "");
+
+ g_test_add (
+ "/EBookCacheCursor/Create/EmptyQuery", TCUFixture, &closure,
+ tcu_fixture_setup, test_create_cursor_empty_query, tcu_fixture_teardown);
+ g_test_add (
+ "/EBookCacheCursor/Create/ValidQuery", TCUFixture, &closure,
+ tcu_fixture_setup, test_create_cursor_valid_query, tcu_fixture_teardown);
+ g_test_add (
+ "/EBookCacheCursor/Create/InvalidSort", TCUFixture, &closure,
+ tcu_fixture_setup, test_create_cursor_invalid_sort, tcu_fixture_teardown);
+ g_test_add (
+ "/EBookCacheCursor/Create/MissingSort", TCUFixture, &closure,
+ tcu_fixture_setup, test_create_cursor_missing_sort, tcu_fixture_teardown);
+
+ return g_test_run ();
+}
diff --git a/tests/libedata-book/test-book-cache-cursor-calculate.c
b/tests/libedata-book/test-book-cache-cursor-calculate.c
new file mode 100644
index 0000000..522ca6f
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-cursor-calculate.c
@@ -0,0 +1,695 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libebook/libebook.h>
+
+#include "test-book-cache-utils.h"
+
+static TCUCursorClosure ascending_closure = {
+ { NULL },
+ NULL, E_BOOK_CURSOR_SORT_ASCENDING
+};
+
+static TCUCursorClosure descending_closure = {
+ { NULL },
+ NULL, E_BOOK_CURSOR_SORT_DESCENDING
+};
+
+static void
+test_cursor_calculate_initial (TCUCursorFixture *fixture,
+ gconstpointer user_data)
+{
+ GError *error = NULL;
+ gint position = 0, total = 0;
+
+ if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, &total, &position, NULL, &error))
+ g_error ("Error calculating cursor: %s", error->message);
+
+ g_assert_cmpint (position, ==, 0);
+ g_assert_cmpint (total, ==, 20);
+}
+
+static void
+test_cursor_calculate_move_forward (TCUCursorFixture *fixture,
+ gconstpointer user_data)
+{
+ GSList *results = NULL;
+ GError *error = NULL;
+ gint position = 0, total = 0;
+
+ /* Move cursor */
+ if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor,
+ E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+ E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT,
+ 5,
+ &results, NULL, &error) < 0)
+ g_error ("Error fetching cursor results: %s", error->message);
+
+ /* Assert the first 5 contacts in en_US order */
+ g_assert_cmpint (g_slist_length (results), ==, 5);
+ tcu_assert_contacts_order (
+ results,
+ "sorted-11",
+ "sorted-1",
+ "sorted-2",
+ "sorted-5",
+ "sorted-6",
+ NULL);
+ g_slist_free_full (results, e_book_cache_search_data_free);
+
+ /* Check new position */
+ if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, &total, &position, NULL, &error))
+ g_error ("Error calculating cursor: %s", error->message);
+
+ /* results 0 + 5 = position 5, result index 4 (results[0, 1, 2, 3, 4]) */
+ g_assert_cmpint (position, ==, 5);
+ g_assert_cmpint (total, ==, 20);
+}
+
+static void
+test_cursor_calculate_move_backwards (TCUCursorFixture *fixture,
+ gconstpointer user_data)
+{
+ GSList *results = NULL;
+ GError *error = NULL;
+ gint position = 0, total = 0;
+
+ /* Move cursor */
+ if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor,
+ E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+ E_BOOK_CACHE_CURSOR_ORIGIN_END,
+ -5,
+ &results, NULL, &error) < 0)
+ g_error ("Error fetching cursor results: %s", error->message);
+
+ /* Assert the last 5 contacts in en_US order */
+ g_assert_cmpint (g_slist_length (results), ==, 5);
+ tcu_assert_contacts_order (
+ results,
+ "sorted-20",
+ "sorted-19",
+ "sorted-9",
+ "sorted-13",
+ "sorted-12",
+ NULL);
+ g_slist_free_full (results, e_book_cache_search_data_free);
+
+ /* Check new position */
+ if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, &total, &position, NULL, &error))
+ g_error ("Error calculating cursor: %s", error->message);
+
+ /* results 20 - 5 = position 16 result index 15 (results[20, 19, 18, 17, 16]) */
+ g_assert_cmpint (position, ==, 16);
+ g_assert_cmpint (total, ==, 20);
+}
+
+static void
+test_cursor_calculate_back_and_forth (TCUCursorFixture *fixture,
+ gconstpointer user_data)
+{
+ GSList *results = NULL;
+ GError *error = NULL;
+ gint position = 0, total = 0;
+
+ /* Move cursor */
+ if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor,
+ E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+ E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN,
+ 7,
+ &results, NULL, &error) < 0)
+ g_error ("Error fetching cursor results: %s", error->message);
+
+ g_assert_cmpint (g_slist_length (results), ==, 7);
+ g_slist_free_full (results, e_book_cache_search_data_free);
+ results = NULL;
+
+ /* Check new position */
+ if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, &total, &position, NULL, &error))
+ g_error ("Error calculating cursor: %s", error->message);
+
+ /* results 0 + 7 = position 7 result index 6 (results[0, 1, 2, 3, 4, 5, 6]) */
+ g_assert_cmpint (position, ==, 7);
+ g_assert_cmpint (total, ==, 20);
+
+ /* Move cursor */
+ if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor,
+ E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+ E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT,
+ -4,
+ &results, NULL, &error) < 0)
+ g_error ("Error fetching cursor results: %s", error->message);
+
+ g_assert_cmpint (g_slist_length (results), ==, 4);
+ g_slist_free_full (results, e_book_cache_search_data_free);
+ results = NULL;
+
+ /* Check new position */
+ if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, &total, &position, NULL, &error))
+ g_error ("Error calculating cursor: %s", error->message);
+
+ /* results 7 - 4 = position 3 result index 2 (results[5, 4, 3, 2]) */
+ g_assert_cmpint (position, ==, 3);
+ g_assert_cmpint (total, ==, 20);
+
+ /* Move cursor */
+ if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor,
+ E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+ E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT,
+ 5,
+ &results, NULL, &error) < 0)
+ g_error ("Error fetching cursor results: %s", error->message);
+
+ g_assert_cmpint (g_slist_length (results), ==, 5);
+ g_slist_free_full (results, e_book_cache_search_data_free);
+ results = NULL;
+
+ /* Check new position */
+ if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, &total, &position, NULL, &error))
+ g_error ("Error calculating cursor: %s", error->message);
+
+ /* results 3 + 5 = position 8 result index 7 (results[3, 4, 5, 6, 7]) */
+ g_assert_cmpint (position, ==, 8);
+ g_assert_cmpint (total, ==, 20);
+}
+
+static void
+test_cursor_calculate_partial_target (TCUCursorFixture *fixture,
+ gconstpointer user_data)
+{
+ GError *error = NULL;
+ gint position = 0, total = 0;
+ ECollator *collator;
+ gint n_labels;
+ const gchar *const *labels;
+
+ /* First verify our test... in en_US locale the label 'C' should exist with the index 3 */
+ collator = e_book_cache_ref_collator (((TCUFixture *) fixture)->book_cache);
+ labels = e_collator_get_index_labels (collator, &n_labels, NULL, NULL, NULL);
+ g_assert_cmpstr (labels[3], ==, "C");
+ e_collator_unref (collator);
+
+ /* Set the cursor at the start of family names beginning with 'C' */
+ e_book_cache_cursor_set_target_alphabetic_index (
+ ((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, 3);
+
+ /* Check new position */
+ if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, &total, &position, NULL, &error))
+ g_error ("Error calculating cursor: %s", error->message);
+
+ /* Position is 13, there are 13 contacts before the letter 'C' in en_US locale */
+ g_assert_cmpint (position, ==, 13);
+ g_assert_cmpint (total, ==, 20);
+}
+
+static void
+test_cursor_calculate_after_modification (TCUCursorFixture *fixture,
+ gconstpointer user_data)
+{
+ GError *error = NULL;
+ gint position = 0, total = 0;
+
+ /* Set the cursor to point exactly 'blackbird' (which is the 12th contact) */
+ if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor,
+ E_BOOK_CACHE_CURSOR_STEP_MOVE,
+ E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT,
+ 12, NULL, NULL, &error) < 0)
+ g_error ("Error fetching cursor results: %s", error->message);
+
+ /* Check new position */
+ if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, &total, &position, NULL, &error))
+ g_error ("Error calculating cursor: %s", error->message);
+
+ /* blackbird is at position 12 in en_US locale */
+ g_assert_cmpint (position, ==, 12);
+ g_assert_cmpint (total, ==, 20);
+
+ /* Rename Muffler -> Jacob Appelbaum */
+ e_contact_set (fixture->contacts[19 - 1], E_CONTACT_FAMILY_NAME, "Appelbaum");
+ e_contact_set (fixture->contacts[19 - 1], E_CONTACT_GIVEN_NAME, "Jacob");
+ if (!e_book_cache_put_contact (((TCUFixture *) fixture)->book_cache,
+ fixture->contacts[19 - 1],
+ e_contact_get_const (fixture->contacts[19 - 1], E_CONTACT_UID),
+ E_CACHE_IS_ONLINE, NULL, &error))
+ g_error ("Failed to modify contact: %s", error->message);
+
+ /* Rename Müller -> Sade Adu */
+ e_contact_set (fixture->contacts[20 - 1], E_CONTACT_FAMILY_NAME, "Adu");
+ e_contact_set (fixture->contacts[20 - 1], E_CONTACT_GIVEN_NAME, "Sade");
+ if (!e_book_cache_put_contact (((TCUFixture *) fixture)->book_cache,
+ fixture->contacts[20 - 1],
+ e_contact_get_const (fixture->contacts[20 - 1], E_CONTACT_UID),
+ E_CACHE_IS_ONLINE, NULL, &error))
+ g_error ("Failed to modify contact: %s", error->message);
+
+ /* Check new position */
+ if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, &total, &position, NULL, &error))
+ g_error ("Error calculating cursor: %s", error->message);
+
+ /* blackbird is now at position 14 after moving 2 later contacts to begin with 'A' */
+ g_assert_cmpint (position, ==, 14);
+ g_assert_cmpint (total, ==, 20);
+}
+
+static void
+test_cursor_calculate_filtered_initial (TCUCursorFixture *fixture,
+ gconstpointer user_data)
+{
+ GError *error = NULL;
+ gint position = 0, total = 0;
+
+ if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, &total, &position, NULL, &error))
+ g_error ("Error calculating cursor: %s", error->message);
+
+ g_assert_cmpint (position, ==, 0);
+ g_assert_cmpint (total, ==, 13);
+}
+
+static void
+test_cursor_calculate_filtered_move_forward (TCUCursorFixture *fixture,
+ gconstpointer user_data)
+{
+ GSList *results = NULL;
+ GError *error = NULL;
+ gint position = 0, total = 0;
+
+ /* Move cursor */
+ if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor,
+ E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+ E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT,
+ 5, &results, NULL, &error) < 0)
+ g_error ("Error fetching cursor results: %s", error->message);
+
+ g_assert_cmpint (g_slist_length (results), ==, 5);
+ g_slist_free_full (results, e_book_cache_search_data_free);
+ results = NULL;
+
+ /* Check new position */
+ if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, &total, &position, NULL, &error))
+ g_error ("Error calculating cursor: %s", error->message);
+
+ /* results 0 + 5 = position 5, result index 4 (results[0, 1, 2, 3, 4]) */
+ g_assert_cmpint (position, ==, 5);
+ g_assert_cmpint (total, ==, 13);
+}
+
+static void
+test_cursor_calculate_filtered_move_backwards (TCUCursorFixture *fixture,
+ gconstpointer user_data)
+{
+ GSList *results = NULL;
+ GError *error = NULL;
+ gint position = 0, total = 0;
+
+ /* Move cursor */
+ if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor,
+ E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+ E_BOOK_CACHE_CURSOR_ORIGIN_END,
+ -5,
+ &results, NULL, &error) < 0)
+ g_error ("Error fetching cursor results: %s", error->message);
+
+ g_assert_cmpint (g_slist_length (results), ==, 5);
+ g_slist_free_full (results, e_book_cache_search_data_free);
+ results = NULL;
+
+ /* Check new position */
+ if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, &total, &position, NULL, &error))
+ g_error ("Error calculating cursor: %s", error->message);
+
+ /* results 13 - 5 = position 9 (results[13, 12, 11, 10, 9]) */
+ g_assert_cmpint (position, ==, 9);
+ g_assert_cmpint (total, ==, 13);
+}
+
+static void
+test_cursor_calculate_filtered_partial_target (TCUCursorFixture *fixture,
+ gconstpointer user_data)
+{
+ GError *error = NULL;
+ gint position = 0, total = 0;
+ ECollator *collator;
+ gint n_labels;
+ const gchar *const *labels;
+
+ /* First verify our test... in en_US locale the label 'C' should exist with the index 3 */
+ collator = e_book_cache_ref_collator (((TCUFixture *) fixture)->book_cache);
+ labels = e_collator_get_index_labels (collator, &n_labels, NULL, NULL, NULL);
+ g_assert_cmpstr (labels[3], ==, "C");
+ e_collator_unref (collator);
+
+ /* Set the cursor at the start of family names beginning with 'C' */
+ e_book_cache_cursor_set_target_alphabetic_index (
+ ((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, 3);
+
+ /* Check new position */
+ if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, &total, &position, NULL, &error))
+ g_error ("Error calculating cursor: %s", error->message);
+
+ /* There are 9 contacts before the letter 'C' in the en_US locale */
+ g_assert_cmpint (position, ==, 9);
+ g_assert_cmpint (total, ==, 13);
+}
+
+static void
+test_cursor_calculate_filtered_after_modification (TCUCursorFixture *fixture,
+ gconstpointer user_data)
+{
+ GError *error = NULL;
+ gint position = 0, total = 0;
+
+ /* Set the cursor to point exactly 'blackbird' (which is the 8th contact when filtered) */
+ if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor,
+ E_BOOK_CACHE_CURSOR_STEP_MOVE,
+ E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN,
+ 8, NULL, NULL, &error) < 0)
+ g_error ("Error fetching cursor results: %s", error->message);
+
+ /* 'blackbirds' -> Jacob Appelbaum */
+ e_contact_set (fixture->contacts[18 - 1], E_CONTACT_FAMILY_NAME, "Appelbaum");
+ e_contact_set (fixture->contacts[18 - 1], E_CONTACT_GIVEN_NAME, "Jacob");
+ if (!e_book_cache_put_contact (((TCUFixture *) fixture)->book_cache,
+ fixture->contacts[18 - 1],
+ e_contact_get_const (fixture->contacts[18 - 1], E_CONTACT_UID),
+ E_CACHE_IS_ONLINE, NULL, &error))
+ g_error ("Failed to modify contact: %s", error->message);
+
+ /* 'black-birds' -> Sade Adu */
+ e_contact_set (fixture->contacts[17 - 1], E_CONTACT_FAMILY_NAME, "Adu");
+ e_contact_set (fixture->contacts[17 - 1], E_CONTACT_GIVEN_NAME, "Sade");
+ if (!e_book_cache_put_contact (((TCUFixture *) fixture)->book_cache,
+ fixture->contacts[17 - 1],
+ e_contact_get_const (fixture->contacts[17 - 1], E_CONTACT_UID),
+ E_CACHE_IS_ONLINE, NULL, &error))
+ g_error ("Failed to modify contact: %s", error->message);
+
+ /* Check new position */
+ if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, &total, &position, NULL, &error))
+ g_error ("Error calculating cursor: %s", error->message);
+
+ /* blackbird is now at position 11 after moving 2 later contacts to begin with 'A' */
+ g_assert_cmpint (position, ==, 9);
+ g_assert_cmpint (total, ==, 13);
+}
+
+static void
+test_cursor_calculate_descending_move_forward (TCUCursorFixture *fixture,
+ gconstpointer user_data)
+{
+ GSList *results = NULL;
+ GError *error = NULL;
+ gint position = 0, total = 0;
+
+ /* Move cursor */
+ if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor,
+ E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+ E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN,
+ 5,
+ &results, NULL, &error) < 0)
+ g_error ("Error fetching cursor results: %s", error->message);
+
+ /* Assert the first 5 contacts in en_US order */
+ g_assert_cmpint (g_slist_length (results), ==, 5);
+ tcu_assert_contacts_order (
+ results,
+ "sorted-20",
+ "sorted-19",
+ "sorted-9",
+ "sorted-13",
+ "sorted-12",
+ NULL);
+ g_slist_free_full (results, e_book_cache_search_data_free);
+ results = NULL;
+
+ /* Check new position */
+ if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, &total, &position, NULL, &error))
+ g_error ("Error calculating cursor: %s", error->message);
+
+ /* results 0 + 5 = position 5, result index 4 (results[0, 1, 2, 3, 4]) */
+ g_assert_cmpint (position, ==, 5);
+ g_assert_cmpint (total, ==, 20);
+}
+
+static void
+test_cursor_calculate_descending_move_backwards (TCUCursorFixture *fixture,
+ gconstpointer user_data)
+{
+ GSList *results = NULL;
+ GError *error = NULL;
+ gint position = 0, total = 0;
+
+ /* Move cursor */
+ if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor,
+ E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+ E_BOOK_CACHE_CURSOR_ORIGIN_END,
+ -5, &results, NULL, &error) < 0)
+ g_error ("Error fetching cursor results: %s", error->message);
+
+ /* Assert the last 5 contacts in en_US order */
+ g_assert_cmpint (g_slist_length (results), ==, 5);
+ tcu_assert_contacts_order (
+ results,
+ "sorted-11",
+ "sorted-1",
+ "sorted-2",
+ "sorted-5",
+ "sorted-6",
+ NULL);
+ g_slist_free_full (results, e_book_cache_search_data_free);
+ results = NULL;
+
+ /* Check new position */
+ if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, &total, &position, NULL, &error))
+ g_error ("Error calculating cursor: %s", error->message);
+
+ /* results 20 - 5 = position 16 result index 15 (results[20, 19, 18, 17, 16]) */
+ g_assert_cmpint (position, ==, 16);
+ g_assert_cmpint (total, ==, 20);
+}
+
+static void
+test_cursor_calculate_descending_partial_target (TCUCursorFixture *fixture,
+ gconstpointer user_data)
+{
+ GError *error = NULL;
+ gint position = 0, total = 0;
+ ECollator *collator;
+ gint n_labels;
+ const gchar *const *labels;
+
+ /* First verify our test... in en_US locale the label 'C' should exist with the index 3 */
+ collator = e_book_cache_ref_collator (((TCUFixture *) fixture)->book_cache);
+ labels = e_collator_get_index_labels (collator, &n_labels, NULL, NULL, NULL);
+ g_assert_cmpstr (labels[3], ==, "C");
+ e_collator_unref (collator);
+
+ /* Set the cursor at the start of family names beginning with 'C' */
+ e_book_cache_cursor_set_target_alphabetic_index (
+ ((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, 3);
+
+ /* Check new position */
+ if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, &total, &position, NULL, &error))
+ g_error ("Error calculating cursor: %s", error->message);
+
+ /* Position is 7, there are 7 contacts leading up to the last 'C' in en_US locale
+ * (when sorting in descending order) */
+ g_assert_cmpint (position, ==, 7);
+ g_assert_cmpint (total, ==, 20);
+}
+
+static void
+test_cursor_calculate_descending_after_modification (TCUCursorFixture *fixture,
+ gconstpointer user_data)
+{
+ GError *error = NULL;
+ gint position = 0, total = 0;
+
+ /* Set the cursor to point exactly 'Bät' (which is the 12th contact in descending order) */
+ if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor,
+ E_BOOK_CACHE_CURSOR_STEP_MOVE,
+ E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN,
+ 12, NULL, NULL, &error) < 0)
+ g_error ("Error fetching cursor results: %s", error->message);
+
+ /* Check new position */
+ if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, &total, &position, NULL, &error))
+ g_error ("Error calculating cursor: %s", error->message);
+
+ /* 'Bät' is at position 12 in en_US locale (descending order) */
+ g_assert_cmpint (position, ==, 12);
+ g_assert_cmpint (total, ==, 20);
+
+ /* Rename Muffler -> Jacob Appelbaum */
+ e_contact_set (fixture->contacts[19 - 1], E_CONTACT_FAMILY_NAME, "Appelbaum");
+ e_contact_set (fixture->contacts[19 - 1], E_CONTACT_GIVEN_NAME, "Jacob");
+ if (!e_book_cache_put_contact (((TCUFixture *) fixture)->book_cache,
+ fixture->contacts[19 - 1],
+ e_contact_get_const (fixture->contacts[19 - 1], E_CONTACT_UID),
+ E_CACHE_IS_ONLINE, NULL, &error))
+ g_error ("Failed to modify contact: %s", error->message);
+
+ /* Rename Müller -> Sade Adu */
+ e_contact_set (fixture->contacts[20 - 1], E_CONTACT_FAMILY_NAME, "Adu");
+ e_contact_set (fixture->contacts[20 - 1], E_CONTACT_GIVEN_NAME, "Sade");
+ if (!e_book_cache_put_contact (((TCUFixture *) fixture)->book_cache,
+ fixture->contacts[20 - 1],
+ e_contact_get_const (fixture->contacts[20 - 1], E_CONTACT_UID),
+ E_CACHE_IS_ONLINE, NULL, &error))
+ g_error ("Failed to modify contact: %s", error->message);
+
+ /* Check new position */
+ if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, &total, &position, NULL, &error))
+ g_error ("Error calculating cursor: %s", error->message);
+
+ /* 'Bät' is now at position 10 in descending order after moving 2 contacts to begin with 'A' */
+ g_assert_cmpint (position, ==, 10);
+ g_assert_cmpint (total, ==, 20);
+}
+
+gint
+main (gint argc,
+ gchar **argv)
+{
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+ g_type_init ();
+#endif
+ g_test_init (&argc, &argv, NULL);
+
+ g_test_add (
+ "/EBookCacheCursor/Calculate/Initial", TCUCursorFixture, &ascending_closure,
+ tcu_cursor_fixture_setup,
+ test_cursor_calculate_initial,
+ tcu_cursor_fixture_teardown);
+ g_test_add (
+ "/EBookCacheCursor/Calculate/MoveForward", TCUCursorFixture, &ascending_closure,
+ tcu_cursor_fixture_setup,
+ test_cursor_calculate_move_forward,
+ tcu_cursor_fixture_teardown);
+ g_test_add (
+ "/EBookCacheCursor/Calculate/MoveBackwards", TCUCursorFixture, &ascending_closure,
+ tcu_cursor_fixture_setup,
+ test_cursor_calculate_move_backwards,
+ tcu_cursor_fixture_teardown);
+ g_test_add (
+ "/EBookCacheCursor/Calculate/BackAndForth", TCUCursorFixture, &ascending_closure,
+ tcu_cursor_fixture_setup,
+ test_cursor_calculate_back_and_forth,
+ tcu_cursor_fixture_teardown);
+ g_test_add (
+ "/EBookCacheCursor/Calculate/AlphabeticTarget", TCUCursorFixture, &ascending_closure,
+ tcu_cursor_fixture_setup,
+ test_cursor_calculate_partial_target,
+ tcu_cursor_fixture_teardown);
+ g_test_add (
+ "/EBookCacheCursor/Calculate/AfterModification", TCUCursorFixture, &ascending_closure,
+ tcu_cursor_fixture_setup,
+ test_cursor_calculate_after_modification,
+ tcu_cursor_fixture_teardown);
+
+ g_test_add (
+ "/EBookCacheCursor/Calculate/Filtered/Initial", TCUCursorFixture, &ascending_closure,
+ tcu_cursor_fixture_filtered_setup,
+ test_cursor_calculate_filtered_initial,
+ tcu_cursor_fixture_teardown);
+ g_test_add (
+ "/EBookCacheCursor/Calculate/Filtered/MoveForward", TCUCursorFixture, &ascending_closure,
+ tcu_cursor_fixture_filtered_setup,
+ test_cursor_calculate_filtered_move_forward,
+ tcu_cursor_fixture_teardown);
+ g_test_add (
+ "/EBookCacheCursor/Calculate/Filtered/MoveBackwards", TCUCursorFixture, &ascending_closure,
+ tcu_cursor_fixture_filtered_setup,
+ test_cursor_calculate_filtered_move_backwards,
+ tcu_cursor_fixture_teardown);
+ g_test_add (
+ "/EBookCacheCursor/Calculate/Filtered/AlphabeticTarget", TCUCursorFixture, &ascending_closure,
+ tcu_cursor_fixture_filtered_setup,
+ test_cursor_calculate_filtered_partial_target,
+ tcu_cursor_fixture_teardown);
+ g_test_add (
+ "/EBookCacheCursor/Calculate/Filtered/AfterModification", TCUCursorFixture,
&ascending_closure,
+ tcu_cursor_fixture_filtered_setup,
+ test_cursor_calculate_filtered_after_modification,
+ tcu_cursor_fixture_teardown);
+
+ g_test_add (
+ "/EBookCacheCursor/Calculate/Descending/Initial", TCUCursorFixture, &descending_closure,
+ tcu_cursor_fixture_setup,
+ test_cursor_calculate_initial,
+ tcu_cursor_fixture_teardown);
+ g_test_add (
+ "/EBookCacheCursor/Calculate/Descending/MoveForward", TCUCursorFixture, &descending_closure,
+ tcu_cursor_fixture_setup,
+ test_cursor_calculate_descending_move_forward,
+ tcu_cursor_fixture_teardown);
+ g_test_add (
+ "/EBookCacheCursor/Calculate/Descending/MoveBackwards", TCUCursorFixture, &descending_closure,
+ tcu_cursor_fixture_setup,
+ test_cursor_calculate_descending_move_backwards,
+ tcu_cursor_fixture_teardown);
+ g_test_add (
+ "/EBookCacheCursor/Calculate/Descending/BackAndForth", TCUCursorFixture, &descending_closure,
+ tcu_cursor_fixture_setup,
+ test_cursor_calculate_back_and_forth,
+ tcu_cursor_fixture_teardown);
+ g_test_add (
+ "/EBookCacheCursor/Calculate/Descending/AlphabeticTarget", TCUCursorFixture,
&descending_closure,
+ tcu_cursor_fixture_setup,
+ test_cursor_calculate_descending_partial_target,
+ tcu_cursor_fixture_teardown);
+ g_test_add (
+ "/EBookCacheCursor/Calculate/Descending/AfterModification", TCUCursorFixture,
&descending_closure,
+ tcu_cursor_fixture_setup,
+ test_cursor_calculate_descending_after_modification,
+ tcu_cursor_fixture_teardown);
+
+ return g_test_run ();
+}
diff --git a/tests/libedata-book/test-book-cache-cursor-change-locale.c
b/tests/libedata-book/test-book-cache-cursor-change-locale.c
new file mode 100644
index 0000000..f95a901
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-cursor-change-locale.c
@@ -0,0 +1,102 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libebook/libebook.h>
+
+#include "test-book-cache-utils.h"
+
+struct {
+ gboolean empty_book;
+ const gchar *path;
+} params[] = {
+ { FALSE, "/EBookCacheCursor/DefaultSummary" },
+ { TRUE, "/EBookCacheCursor/EmptySummary" }
+};
+
+gint
+main (gint argc,
+ gchar **argv)
+{
+ TCUStepData *data;
+ gint ii;
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+ g_type_init ();
+#endif
+ g_test_init (&argc, &argv, NULL);
+
+ for (ii = 0; ii < G_N_ELEMENTS (params); ii++) {
+
+ data = tcu_step_test_new (
+ params[ii].path, "/ChangeLocale/POSIX/en_US", "POSIX",
+ params[ii].empty_book);
+ tcu_step_test_add_assertion (data, 5, 11, 2, 6, 3, 8);
+ tcu_step_test_add_assertion (data, 5, 1, 5, 4, 7, 15);
+ tcu_step_test_add_assertion (data, 5, 17, 16, 18, 10, 14);
+ tcu_step_test_add_assertion (data, 5, 12, 13, 9, 19, 20);
+
+ tcu_step_test_change_locale (data, "en_US.UTF-8", 0);
+ tcu_step_test_add_assertion (data, 5, 11, 1, 2, 5, 6);
+ tcu_step_test_add_assertion (data, 5, 4, 3, 7, 8, 15);
+ tcu_step_test_add_assertion (data, 5, 17, 16, 18, 10, 14);
+ tcu_step_test_add_assertion (data, 5, 12, 13, 9, 19, 20);
+ tcu_step_test_add (data, FALSE);
+
+ data = tcu_step_test_new (
+ params[ii].path, "/ChangeLocale/en_US/fr_CA", "en_US.UTF-8",
+ params[ii].empty_book);
+ tcu_step_test_add_assertion (data, 5, 11, 1, 2, 5, 6);
+ tcu_step_test_add_assertion (data, 5, 4, 3, 7, 8, 15);
+ tcu_step_test_add_assertion (data, 5, 17, 16, 18, 10, 14);
+ tcu_step_test_add_assertion (data, 5, 12, 13, 9, 19, 20);
+
+ tcu_step_test_change_locale (data, "fr_CA.UTF-8", 0);
+ tcu_step_test_add_assertion (data, 5, 11, 1, 2, 5, 6);
+ tcu_step_test_add_assertion (data, 5, 4, 3, 7, 8, 15);
+ tcu_step_test_add_assertion (data, 5, 17, 16, 18, 10, 14);
+ tcu_step_test_add_assertion (data, 5, 13, 12, 9, 19, 20);
+ tcu_step_test_add (data, FALSE);
+
+ data = tcu_step_test_new (
+ params[ii].path, "/ChangeLocale/fr_CA/de_DE", "fr_CA.UTF-8",
+ params[ii].empty_book);
+ tcu_step_test_add_assertion (data, 5, 11, 1, 2, 5, 6);
+ tcu_step_test_add_assertion (data, 5, 4, 3, 7, 8, 15);
+ tcu_step_test_add_assertion (data, 5, 17, 16, 18, 10, 14);
+ tcu_step_test_add_assertion (data, 5, 13, 12, 9, 19, 20);
+
+ /* When changing from fr_CA to de_DE, two numbers change:
+ *
+ * sorted-5:
+ * 049-2459-4393 is now parsed with the national number as 4924594393
+ *
+ * sorted-4:
+ * 12 245999 is now parsed with national number 12245999 instead of 2245999
+ *
+ */
+ tcu_step_test_change_locale (data, "de_DE.UTF-8", 2);
+ tcu_step_test_add_assertion (data, 5, 11, 1, 2, 5, 6);
+ tcu_step_test_add_assertion (data, 5, 7, 8, 4, 3, 15);
+ tcu_step_test_add_assertion (data, 5, 17, 16, 18, 10, 14);
+ tcu_step_test_add_assertion (data, 5, 12, 13, 9, 20, 19);
+ tcu_step_test_add (data, FALSE);
+ }
+
+ /* On this case, we want to delete the work directory and start fresh */
+ return g_test_run ();
+}
diff --git a/tests/libedata-book/test-book-cache-cursor-move-by-de-DE.c
b/tests/libedata-book/test-book-cache-cursor-move-by-de-DE.c
new file mode 100644
index 0000000..8432f03
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-cursor-move-by-de-DE.c
@@ -0,0 +1,82 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libebook/libebook.h>
+
+#include "test-book-cache-utils.h"
+
+struct {
+ gboolean empty_book;
+ const gchar *path;
+} params[] = {
+ { FALSE, "/EBookCacheCursor/DefaultSummary" },
+ { TRUE, "/EBookCacheCursor/EmptySummary" }
+};
+
+gint
+main (gint argc,
+ gchar **argv)
+{
+ TCUStepData *data;
+ gint ii;
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+ g_type_init ();
+#endif
+ g_test_init (&argc, &argv, NULL);
+
+ for (ii = 0; ii < G_N_ELEMENTS (params); ii++) {
+
+ data = tcu_step_test_new (
+ params[ii].path, "/de_DE/Move/Forward", "de_DE.UTF-8",
+ params[ii].empty_book);
+ tcu_step_test_add_assertion (data, 5, 11, 1, 2, 5, 6);
+ tcu_step_test_add_assertion (data, 6, 7, 8, 4, 3, 15, 17);
+ tcu_step_test_add (data, FALSE);
+
+ data = tcu_step_test_new (
+ params[ii].path, "/de_DE/Move/ForwardOnNameless", "de_DE.UTF-8",
+ params[ii].empty_book);
+ tcu_step_test_add_assertion (data, 1, 11);
+ tcu_step_test_add_assertion (data, 3, 1, 2, 5);
+ tcu_step_test_add (data, FALSE);
+
+ data = tcu_step_test_new (
+ params[ii].path, "/de_DE/Move/Backwards", "de_DE.UTF-8",
+ params[ii].empty_book);
+ tcu_step_test_add_assertion (data, -5, 19, 20, 9, 13, 12);
+ tcu_step_test_add_assertion (data, -8, 14, 10, 18, 16, 17, 15, 3, 4);
+ tcu_step_test_add (data, FALSE);
+
+ data = tcu_step_test_new (
+ params[ii].path, "/de_DE/Filtered/Move/Forward", "de_DE.UTF-8",
+ params[ii].empty_book);
+ tcu_step_test_add_assertion (data, 5, 11, 1, 2, 5, 8);
+ tcu_step_test_add_assertion (data, 8, 3, 17, 16, 18, 10, 14, 12, 9);
+ tcu_step_test_add (data, TRUE);
+
+ data = tcu_step_test_new (
+ params[ii].path, "/de_DE/Filtered/Move/Backwards", "de_DE.UTF-8",
+ params[ii].empty_book);
+ tcu_step_test_add_assertion (data, -5, 9, 12, 14, 10, 18);
+ tcu_step_test_add_assertion (data, -8, 16, 17, 3, 8, 5, 2, 1, 11);
+ tcu_step_test_add (data, TRUE);
+ }
+
+ return g_test_run ();
+}
diff --git a/tests/libedata-book/test-book-cache-cursor-move-by-en-US.c
b/tests/libedata-book/test-book-cache-cursor-move-by-en-US.c
new file mode 100644
index 0000000..94955d5
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-cursor-move-by-en-US.c
@@ -0,0 +1,100 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libebook/libebook.h>
+
+#include "test-book-cache-utils.h"
+
+struct {
+ gboolean empty_book;
+ const gchar *path;
+} params[] = {
+ { FALSE, "/EBookCacheCursor/DefaultSummary" },
+ { TRUE, "/EBookCacheCursor/EmptySummary" }
+};
+
+gint
+main (gint argc,
+ gchar **argv)
+{
+ TCUStepData *data;
+ gint ii;
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+ g_type_init ();
+#endif
+ g_test_init (&argc, &argv, NULL);
+
+ for (ii = 0; ii < G_N_ELEMENTS (params); ii++) {
+
+ data = tcu_step_test_new (
+ params[ii].path, "/en_US/Move/Forward", "en_US.UTF-8",
+ params[ii].empty_book);
+ tcu_step_test_add_assertion (data, 5, 11, 1, 2, 5, 6);
+ tcu_step_test_add_assertion (data, 6, 4, 3, 7, 8, 15, 17);
+ tcu_step_test_add (data, FALSE);
+
+ data = tcu_step_test_new (
+ params[ii].path, "/en_US/Move/ForwardOnNameless", "en_US.UTF-8",
+ params[ii].empty_book);
+ tcu_step_test_add_assertion (data, 1, 11);
+ tcu_step_test_add_assertion (data, 3, 1, 2, 5);
+ tcu_step_test_add (data, FALSE);
+
+ data = tcu_step_test_new (
+ params[ii].path, "/en_US/Move/Backwards", "en_US.UTF-8",
+ params[ii].empty_book);
+ tcu_step_test_add_assertion (data, -5, 20, 19, 9, 13, 12);
+ tcu_step_test_add_assertion (data, -8, 14, 10, 18, 16, 17, 15, 8, 7);
+ tcu_step_test_add (data, FALSE);
+
+ data = tcu_step_test_new (
+ params[ii].path, "/en_US/Filtered/Move/Forward", "en_US.UTF-8",
+ params[ii].empty_book);
+ tcu_step_test_add_assertion (data, 5, 11, 1, 2, 5, 3);
+ tcu_step_test_add_assertion (data, 8, 8, 17, 16, 18, 10, 14, 12, 9);
+ tcu_step_test_add (data, TRUE);
+
+ data = tcu_step_test_new (
+ params[ii].path, "/en_US/Filtered/Move/Backwards", "en_US.UTF-8",
+ params[ii].empty_book);
+ tcu_step_test_add_assertion (data, -5, 9, 12, 14, 10, 18);
+ tcu_step_test_add_assertion (data, -8, 16, 17, 8, 3, 5, 2, 1, 11);
+ tcu_step_test_add (data, TRUE);
+
+ data = tcu_step_test_new_full (
+ params[ii].path, "/en_US/Move/Descending/Forward", "en_US.UTF-8",
+ params[ii].empty_book,
+ E_BOOK_CURSOR_SORT_DESCENDING);
+ tcu_step_test_add_assertion (data, 5, 20, 19, 9, 13, 12);
+ tcu_step_test_add_assertion (data, 5, 14, 10, 18, 16, 17);
+ tcu_step_test_add_assertion (data, 5, 15, 8, 7, 3, 4);
+ tcu_step_test_add_assertion (data, 5, 6, 5, 2, 1, 11);
+ tcu_step_test_add (data, FALSE);
+
+ data = tcu_step_test_new_full (
+ params[ii].path, "/en_US/Move/Descending/Backwards", "en_US.UTF-8",
+ params[ii].empty_book,
+ E_BOOK_CURSOR_SORT_DESCENDING);
+ tcu_step_test_add_assertion (data, -10, 11, 1, 2, 5, 6, 4, 3, 7, 8, 15);
+ tcu_step_test_add_assertion (data, -10, 17, 16, 18, 10, 14, 12, 13, 9, 19, 20);
+ tcu_step_test_add (data, FALSE);
+ }
+
+ return g_test_run ();
+}
diff --git a/tests/libedata-book/test-book-cache-cursor-move-by-fr-CA.c
b/tests/libedata-book/test-book-cache-cursor-move-by-fr-CA.c
new file mode 100644
index 0000000..8d71e27
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-cursor-move-by-fr-CA.c
@@ -0,0 +1,82 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libebook/libebook.h>
+
+#include "test-book-cache-utils.h"
+
+struct {
+ gboolean empty_book;
+ const gchar *path;
+} params[] = {
+ { FALSE, "/EBookCacheCursor/DefaultSummary" },
+ { TRUE, "/EBookCacheCursor/EmptySummary" }
+};
+
+gint
+main (gint argc,
+ gchar **argv)
+{
+ TCUStepData *data;
+ gint ii;
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+ g_type_init ();
+#endif
+ g_test_init (&argc, &argv, NULL);
+
+ for (ii = 0; ii < G_N_ELEMENTS (params); ii++) {
+
+ data = tcu_step_test_new (
+ params[ii].path, "/fr_CA/Move/Forward", "fr_CA.UTF-8",
+ params[ii].empty_book);
+ tcu_step_test_add_assertion (data, 5, 11, 1, 2, 5, 6);
+ tcu_step_test_add_assertion (data, 6, 4, 3, 7, 8, 15, 17);
+ tcu_step_test_add (data, FALSE);
+
+ data = tcu_step_test_new (
+ params[ii].path, "/fr_CA/Move/ForwardOnNameless", "fr_CA.UTF-8",
+ params[ii].empty_book);
+ tcu_step_test_add_assertion (data, 1, 11);
+ tcu_step_test_add_assertion (data, 3, 1, 2, 5);
+ tcu_step_test_add (data, FALSE);
+
+ data = tcu_step_test_new (
+ params[ii].path, "/fr_CA/Move/Backwards", "fr_CA.UTF-8",
+ params[ii].empty_book);
+ tcu_step_test_add_assertion (data, -5, 20, 19, 9, 12, 13);
+ tcu_step_test_add_assertion (data, -8, 14, 10, 18, 16, 17, 15, 8, 7);
+ tcu_step_test_add (data, FALSE);
+
+ data = tcu_step_test_new (
+ params[ii].path, "/fr_CA/Filtered/Move/Forward", "fr_CA.UTF-8",
+ params[ii].empty_book);
+ tcu_step_test_add_assertion (data, 5, 11, 1, 2, 5, 3);
+ tcu_step_test_add_assertion (data, 8, 8, 17, 16, 18, 10, 14, 12, 9);
+ tcu_step_test_add (data, TRUE);
+
+ data = tcu_step_test_new (
+ params[ii].path, "/fr_CA/Filtered/Move/Backwards", "fr_CA.UTF-8",
+ params[ii].empty_book);
+ tcu_step_test_add_assertion (data, -5, 9, 12, 14, 10, 18);
+ tcu_step_test_add_assertion (data, -8, 16, 17, 8, 3, 5, 2, 1, 11);
+ tcu_step_test_add (data, TRUE);
+ }
+
+ return g_test_run ();
+}
diff --git a/tests/libedata-book/test-book-cache-cursor-move-by-posix.c
b/tests/libedata-book/test-book-cache-cursor-move-by-posix.c
new file mode 100644
index 0000000..110cd6c
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-cursor-move-by-posix.c
@@ -0,0 +1,82 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libebook/libebook.h>
+
+#include "test-book-cache-utils.h"
+
+struct {
+ gboolean empty_book;
+ const gchar *path;
+} params[] = {
+ { FALSE, "/EBookCacheCursor/DefaultSummary" },
+ { TRUE, "/EBookCacheCursor/EmptySummary" }
+};
+
+gint
+main (gint argc,
+ gchar **argv)
+{
+ TCUStepData *data;
+ gint ii;
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+ g_type_init ();
+#endif
+ g_test_init (&argc, &argv, NULL);
+
+ for (ii = 0; ii < G_N_ELEMENTS (params); ii++) {
+
+ data = tcu_step_test_new (
+ params[ii].path, "/POSIX/Move/Forward", "POSIX",
+ params[ii].empty_book);
+ tcu_step_test_add_assertion (data, 5, 11, 2, 6, 3, 8);
+ tcu_step_test_add_assertion (data, 6, 1, 5, 4, 7, 15, 17);
+ tcu_step_test_add (data, FALSE);
+
+ data = tcu_step_test_new (
+ params[ii].path, "/POSIX/Move/ForwardOnNameless", "POSIX",
+ params[ii].empty_book);
+ tcu_step_test_add_assertion (data, 1, 11);
+ tcu_step_test_add_assertion (data, 3, 2, 6, 3);
+ tcu_step_test_add (data, FALSE);
+
+ data = tcu_step_test_new (
+ params[ii].path, "/POSIX/Move/Backwards", "POSIX",
+ params[ii].empty_book);
+ tcu_step_test_add_assertion (data, -5, 20, 19, 9, 13, 12);
+ tcu_step_test_add_assertion (data, -12, 14, 10, 18, 16, 17, 15, 7, 4, 5, 1, 8, 3);
+ tcu_step_test_add (data, FALSE);
+
+ data = tcu_step_test_new (
+ params[ii].path, "/POSIX/Filtered/Move/Forward", "POSIX",
+ params[ii].empty_book);
+ tcu_step_test_add_assertion (data, 5, 11, 2, 3, 8, 1);
+ tcu_step_test_add_assertion (data, 8, 5, 17, 16, 18, 10, 14, 12, 9);
+ tcu_step_test_add (data, TRUE);
+
+ data = tcu_step_test_new (
+ params[ii].path, "/POSIX/Filtered/Move/Backwards", "POSIX",
+ params[ii].empty_book);
+ tcu_step_test_add_assertion (data, -5, 9, 12, 14, 10, 18);
+ tcu_step_test_add_assertion (data, -8, 16, 17, 5, 1, 8, 3, 2, 11);
+ tcu_step_test_add (data, TRUE);
+ }
+
+ return g_test_run ();
+}
diff --git a/tests/libedata-book/test-book-cache-cursor-set-sexp.c
b/tests/libedata-book/test-book-cache-cursor-set-sexp.c
new file mode 100644
index 0000000..88da7de
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-cursor-set-sexp.c
@@ -0,0 +1,155 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libebook/libebook.h>
+
+#include "test-book-cache-utils.h"
+
+static TCUCursorClosure book_closure = { { NULL }, NULL, E_BOOK_CURSOR_SORT_ASCENDING };
+
+static void
+test_cursor_sexp_calculate_position (TCUCursorFixture *fixture,
+ gconstpointer user_data)
+{
+ GError *error = NULL;
+ EBookQuery *query;
+ gint position = 0, total = 0;
+ gchar *sexp = NULL;
+ GSList *results = NULL, *node;
+ EBookCacheSearchData *data;
+
+ /* Set the cursor to point exactly to 'blackbirds', which is the 12th contact in en_US */
+ if (!e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor,
+ E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+ E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN,
+ 12, &results, NULL, &error))
+ g_error ("Error fetching cursor results: %s", error->message);
+
+ /* Ensure we moved to the right contact */
+ node = g_slist_last (results);
+ g_assert (node);
+ data = node->data;
+ g_assert_cmpstr (data->uid, ==, "sorted-16");
+ g_slist_free_full (results, e_book_cache_search_data_free);
+
+ /* Check position */
+ if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, &total, &position, NULL, &error))
+ g_error ("Error calculating cursor: %s", error->message);
+
+ /* blackbird is at position 12 in an unfiltered en_US locale */
+ g_assert_cmpint (position, ==, 12);
+ g_assert_cmpint (total, ==, 20);
+
+ /* Set new sexp, only contacts with .com email addresses */
+ query = e_book_query_field_test (E_CONTACT_EMAIL, E_BOOK_QUERY_ENDS_WITH, ".com");
+ sexp = e_book_query_to_string (query);
+ e_book_query_unref (query);
+
+ if (!e_book_cache_cursor_set_sexp (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, sexp, &error))
+ g_error ("Failed to set sexp: %s", error->message);
+
+ /* Check new position after modified sexp */
+ if (!e_book_cache_cursor_calculate (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, &total, &position, NULL, &error))
+ g_error ("Error calculating cursor: %s", error->message);
+
+ /* 'blackbird' is now at position 8 out of 13, with a filtered set of contacts in en_US locale */
+ g_assert_cmpint (position, ==, 8);
+ g_assert_cmpint (total, ==, 13);
+
+ g_free (sexp);
+}
+
+static void
+test_cursor_sexp_and_step (TCUCursorFixture *fixture,
+ gconstpointer user_data)
+{
+ GError *error = NULL;
+ EBookQuery *query;
+ gchar *sexp = NULL;
+ GSList *results = NULL, *node;
+ EBookCacheSearchData *data;
+
+ /* Set new sexp, only contacts with .com email addresses */
+ query = e_book_query_field_test (E_CONTACT_EMAIL, E_BOOK_QUERY_ENDS_WITH, ".com");
+ sexp = e_book_query_to_string (query);
+ e_book_query_unref (query);
+
+ if (!e_book_cache_cursor_set_sexp (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, sexp, &error))
+ g_error ("Failed to set sexp: %s", error->message);
+
+ /* Step 6 results from the beginning of the filtered list, gets up to contact 'sorted-8' */
+ if (!e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor,
+ E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+ E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN,
+ 6, &results, NULL, &error))
+ g_error ("Error fetching cursor results: %s", error->message);
+
+ /* Ensure we moved to the right contact */
+ node = g_slist_last (results);
+ g_assert (node);
+ data = node->data;
+ g_assert_cmpstr (data->uid, ==, "sorted-8");
+ g_slist_free_full (results, e_book_cache_search_data_free);
+ results = NULL;
+
+ /* Step 6 results more, gets up to contact 'sorted-12' */
+ if (!e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor,
+ E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+ E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT,
+ 6, &results, NULL, &error))
+ g_error ("Error fetching cursor results: %s", error->message);
+
+ /* Ensure we moved to the right contact */
+ node = g_slist_last (results);
+ g_assert (node);
+ data = node->data;
+ g_assert_cmpstr (data->uid, ==, "sorted-12");
+ g_slist_free_full (results, e_book_cache_search_data_free);
+
+ g_free (sexp);
+}
+
+gint
+main (gint argc,
+ gchar **argv)
+{
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+ g_type_init ();
+#endif
+ g_test_init (&argc, &argv, NULL);
+
+ g_test_add (
+ "/EBookCacheCursor/SetSexp/CalculatePosition", TCUCursorFixture, &book_closure,
+ tcu_cursor_fixture_setup,
+ test_cursor_sexp_calculate_position,
+ tcu_cursor_fixture_teardown);
+ g_test_add (
+ "/EBookCacheCursor/SetSexp/Step", TCUCursorFixture, &book_closure,
+ tcu_cursor_fixture_setup,
+ test_cursor_sexp_and_step,
+ tcu_cursor_fixture_teardown);
+
+ return g_test_run ();
+}
diff --git a/tests/libedata-book/test-book-cache-cursor-set-target.c
b/tests/libedata-book/test-book-cache-cursor-set-target.c
new file mode 100644
index 0000000..f230634
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-cursor-set-target.c
@@ -0,0 +1,225 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libebook/libebook.h>
+
+#include "test-book-cache-utils.h"
+
+/*****************************************************
+ * Expect the same results twice *
+ *****************************************************/
+static void
+test_cursor_set_target_reset_cursor (TCUCursorFixture *fixture,
+ gconstpointer user_data)
+{
+ GSList *results = NULL;
+ GError *error = NULL;
+
+ /* First batch */
+ if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor,
+ E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+ E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN,
+ 5, &results, NULL, &error) < 0)
+ g_error ("Error fetching cursor results: %s", error->message);
+
+ tcu_print_results (results);
+
+ /* Assert the first 5 contacts in en_US order */
+ g_assert_cmpint (g_slist_length (results), ==, 5);
+ tcu_assert_contacts_order (
+ results,
+ "sorted-11",
+ "sorted-1",
+ "sorted-2",
+ "sorted-5",
+ "sorted-6",
+ NULL);
+
+ g_slist_free_full (results, e_book_cache_search_data_free);
+ results = NULL;
+
+ /* Second batch reset (same results) */
+ if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor,
+ E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+ E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN,
+ 5, &results, NULL, &error) < 0)
+ g_error ("Error fetching cursor results: %s", error->message);
+
+ tcu_print_results (results);
+
+ /* Assert the first 5 contacts in en_US order again */
+ g_assert_cmpint (g_slist_length (results), ==, 5);
+ tcu_assert_contacts_order (
+ results,
+ "sorted-11",
+ "sorted-1",
+ "sorted-2",
+ "sorted-5",
+ "sorted-6",
+ NULL);
+
+ g_slist_free_full (results, e_book_cache_search_data_free);
+}
+
+/*****************************************************
+ * Expect results with family name starting with 'C' *
+ *****************************************************/
+static void
+test_cursor_set_target_c_next_results (TCUCursorFixture *fixture,
+ gconstpointer user_data)
+{
+ GSList *results = NULL;
+ GError *error = NULL;
+ ECollator *collator;
+ gint n_labels;
+ const gchar *const *labels;
+
+ /* First verify our test... in en_US locale the label 'C' should exist with the index 3 */
+ collator = e_book_cache_ref_collator (((TCUFixture *) fixture)->book_cache);
+ labels = e_collator_get_index_labels (collator, &n_labels, NULL, NULL, NULL);
+ g_assert_cmpstr (labels[3], ==, "C");
+ e_collator_unref (collator);
+
+ /* Set the cursor at the start of family names beginning with 'C' */
+ e_book_cache_cursor_set_target_alphabetic_index (
+ ((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, 3);
+
+ if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor,
+ E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+ E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT,
+ 5, &results, NULL, &error) < 0)
+ g_error ("Error fetching cursor results: %s", error->message);
+
+ tcu_print_results (results);
+
+ /* Assert that we got the results starting at C */
+ g_assert_cmpint (g_slist_length (results), ==, 5);
+ tcu_assert_contacts_order (
+ results,
+ "sorted-10",
+ "sorted-14",
+ "sorted-12",
+ "sorted-13",
+ "sorted-9",
+ NULL);
+
+ g_slist_free_full (results, e_book_cache_search_data_free);
+}
+
+/*****************************************************
+ * Expect results before the letter 'C' *
+ *****************************************************/
+static void
+test_cursor_set_target_c_prev_results (TCUCursorFixture *fixture,
+ gconstpointer user_data)
+{
+ GSList *results = NULL;
+ GError *error = NULL;
+ ECollator *collator;
+ gint n_labels;
+ const gchar *const *labels;
+
+ /* First verify our test... in en_US locale the label 'C' should exist with the index 3 */
+ collator = e_book_cache_ref_collator (((TCUFixture *) fixture)->book_cache);
+ labels = e_collator_get_index_labels (collator, &n_labels, NULL, NULL, NULL);
+ g_assert_cmpstr (labels[3], ==, "C");
+ e_collator_unref (collator);
+
+ /* Set the cursor at the start of family names beginning with 'C' */
+ e_book_cache_cursor_set_target_alphabetic_index (
+ ((TCUFixture *) fixture)->book_cache,
+ fixture->cursor, 3);
+
+ if (e_book_cache_cursor_step (((TCUFixture *) fixture)->book_cache,
+ fixture->cursor,
+ E_BOOK_CACHE_CURSOR_STEP_MOVE | E_BOOK_CACHE_CURSOR_STEP_FETCH,
+ E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT,
+ -5, &results, NULL, &error) < 0)
+ g_error ("Error fetching cursor results: %s", error->message);
+
+ tcu_print_results (results);
+
+ /* Assert that we got the results before C */
+ g_assert_cmpint (g_slist_length (results), ==, 5);
+ tcu_assert_contacts_order (
+ results,
+ "sorted-18",
+ "sorted-16",
+ "sorted-17",
+ "sorted-15",
+ "sorted-8",
+ NULL);
+
+ g_slist_free_full (results, e_book_cache_search_data_free);
+}
+
+static TCUCursorClosure closures[] = {
+ { { NULL }, NULL, E_BOOK_CURSOR_SORT_ASCENDING },
+ { { tcu_setup_empty_book }, NULL, E_BOOK_CURSOR_SORT_ASCENDING }
+};
+
+static const gchar *prefixes[] = {
+ "/EBookCache/DefaultSummary",
+ "/EBookCache/EmptySummary"
+};
+
+gint
+main (gint argc,
+ gchar **argv)
+{
+ gint ii;
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+ g_type_init ();
+#endif
+ g_test_init (&argc, &argv, NULL);
+
+ for (ii = 0; ii < G_N_ELEMENTS (closures); ii++) {
+ gchar *path;
+
+ path = g_strconcat (prefixes[ii], "/SetTarget/ResetCursor", NULL);
+ g_test_add (
+ path, TCUCursorFixture, &closures[ii],
+ tcu_cursor_fixture_setup,
+ test_cursor_set_target_reset_cursor,
+ tcu_cursor_fixture_teardown);
+ g_free (path);
+
+ path = g_strconcat (prefixes[ii], "/SetTarget/Alphabetic/C/NextResults", NULL);
+ g_test_add (
+ path, TCUCursorFixture, &closures[ii],
+ tcu_cursor_fixture_setup,
+ test_cursor_set_target_c_next_results,
+ tcu_cursor_fixture_teardown);
+ g_free (path);
+
+ path = g_strconcat (prefixes[ii], "/SetTarget/Alphabetic/C/PreviousResults", NULL);
+ g_test_add (
+ path, TCUCursorFixture, &closures[ii],
+ tcu_cursor_fixture_setup,
+ test_cursor_set_target_c_prev_results,
+ tcu_cursor_fixture_teardown);
+ g_free (path);
+ }
+
+ return g_test_run ();
+}
diff --git a/tests/libedata-book/test-book-cache-get-contact.c
b/tests/libedata-book/test-book-cache-get-contact.c
new file mode 100644
index 0000000..c214d4f
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-get-contact.c
@@ -0,0 +1,78 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libebook/libebook.h>
+
+#include "test-book-cache-utils.h"
+
+static void
+test_get_contact (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ EContact *contact = NULL;
+ EContact *other = NULL;
+ GError *error = NULL;
+
+ tcu_add_contact_from_test_case (fixture, "simple-1", &contact);
+
+ if (!e_book_cache_get_contact (fixture->book_cache,
+ (const gchar *) e_contact_get_const (contact, E_CONTACT_UID),
+ FALSE, &other, NULL, &error)) {
+ g_error (
+ "Failed to get contact with uid '%s': %s",
+ (const gchar *) e_contact_get_const (contact, E_CONTACT_UID),
+ error->message);
+ }
+
+ g_object_unref (contact);
+ g_object_unref (other);
+}
+
+static TCUClosure closures[] = {
+ { NULL },
+ { tcu_setup_empty_book }
+};
+
+static const gchar *paths[] = {
+ "/EBookCache/DefaultSummary/GetContact",
+ "/EBookCache/EmptySummary/GetContact",
+};
+
+gint
+main (gint argc,
+ gchar **argv)
+{
+ gint ii;
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+ g_type_init ();
+#endif
+ g_test_init (&argc, &argv, NULL);
+
+ /* Ensure that the client and server get the same locale */
+ g_assert (g_setenv ("LC_ALL", "en_US.UTF-8", TRUE));
+ setlocale (LC_ALL, "");
+
+ for (ii = 0; ii < G_N_ELEMENTS (closures); ii++) {
+ g_test_add (
+ paths[ii], TCUFixture, &closures[ii],
+ tcu_fixture_setup, test_get_contact, tcu_fixture_teardown);
+ }
+
+ return g_test_run ();
+}
diff --git a/tests/libedata-book/test-book-cache-offline.c b/tests/libedata-book/test-book-cache-offline.c
new file mode 100644
index 0000000..b6d4f1e
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-offline.c
@@ -0,0 +1,1138 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libebook/libebook.h>
+
+#include "test-book-cache-utils.h"
+
+static void
+test_fill_cache (TCUFixture *fixture,
+ EContact **out_contact)
+{
+ tcu_add_contact_from_test_case (fixture, "custom-1", out_contact);
+ tcu_add_contact_from_test_case (fixture, "custom-3", NULL);
+ tcu_add_contact_from_test_case (fixture, "custom-9", NULL);
+}
+
+enum {
+ EXPECT_DEFAULT = (0),
+ EXPECT_CUSTOM_1 = (1 << 0),
+ EXPECT_CUSTOM_9 = (1 << 1),
+ EXPECT_SIMPLE_1 = (1 << 2),
+ EXPECT_SIMPLE_2 = (1 << 3),
+ HAS_SEARCH_DATA = (1 << 4),
+ HAS_META_CONTACTS = (1 << 5),
+ SKIP_CONTACT_PUT = (1 << 6)
+};
+
+static void
+test_check_search_result (const GSList *list,
+ guint32 flags)
+{
+ gboolean expect_custom_1 = (flags & EXPECT_CUSTOM_1) != 0;
+ gboolean expect_custom_9 = (flags & EXPECT_CUSTOM_9) != 0;
+ gboolean expect_simple_1 = (flags & EXPECT_SIMPLE_1) != 0;
+ gboolean expect_simple_2 = (flags & EXPECT_SIMPLE_2) != 0;
+ gboolean has_search_data = (flags & HAS_SEARCH_DATA) != 0;
+ gboolean has_meta_contacts = (flags & HAS_META_CONTACTS) != 0;
+ gboolean have_custom_1 = FALSE;
+ gboolean have_custom_3 = FALSE;
+ gboolean have_custom_9 = FALSE;
+ gboolean have_simple_1 = FALSE;
+ gboolean have_simple_2 = FALSE;
+ const GSList *link;
+
+ for (link = list; link; link = g_slist_next (link)) {
+ const gchar *uid;
+
+ if (has_search_data) {
+ EBookCacheSearchData *sd = link->data;
+ EContact *contact;
+
+ g_assert (sd != NULL);
+ g_assert (sd->uid != NULL);
+ g_assert (sd->vcard != NULL);
+
+ uid = sd->uid;
+
+ contact = e_contact_new_from_vcard (sd->vcard);
+ g_assert (E_IS_CONTACT (contact));
+ g_assert_cmpstr (uid, ==, e_contact_get_const (contact, E_CONTACT_UID));
+
+ if (has_meta_contacts) {
+ g_assert_nonnull (e_contact_get_const (contact, E_CONTACT_REV));
+ g_assert_null (e_contact_get_const (contact, E_CONTACT_EMAIL_1));
+ } else {
+ g_assert_nonnull (e_contact_get_const (contact, E_CONTACT_EMAIL_1));
+ }
+
+ g_clear_object (&contact);
+ } else {
+ uid = link->data;
+ }
+
+ g_assert_nonnull (uid);
+
+ if (g_str_equal (uid, "custom-1")) {
+ g_assert (expect_custom_1);
+ g_assert (!have_custom_1);
+ have_custom_1 = TRUE;
+ } else if (g_str_equal (uid, "custom-3")) {
+ g_assert (!have_custom_3);
+ have_custom_3 = TRUE;
+ } else if (g_str_equal (uid, "custom-9")) {
+ g_assert (expect_custom_9);
+ g_assert (!have_custom_9);
+ have_custom_9 = TRUE;
+ } else if (g_str_equal (uid, "simple-1")) {
+ g_assert (expect_simple_1);
+ g_assert (!have_simple_1);
+ have_simple_1 = TRUE;
+ } else if (g_str_equal (uid, "simple-2")) {
+ g_assert (expect_simple_2);
+ g_assert (!have_simple_2);
+ have_simple_2 = TRUE;
+ } else {
+ /* It's not supposed to be NULL, but it will print the value of 'uid' */
+ g_assert_cmpstr (uid, ==, NULL);
+ }
+ }
+
+ g_assert ((expect_custom_1 && have_custom_1) || (!expect_custom_1 && !have_custom_1));
+ g_assert ((expect_custom_9 && have_custom_9) || (!expect_custom_9 && !have_custom_9));
+ g_assert ((expect_simple_1 && have_simple_1) || (!expect_simple_1 && !have_simple_1));
+ g_assert ((expect_simple_2 && have_simple_2) || (!expect_simple_2 && !have_simple_2));
+ g_assert (have_custom_3);
+}
+
+static void
+test_basic_cursor (TCUFixture *fixture,
+ guint32 flags,
+ const gchar *sexp)
+{
+ EContactField sort_fields[] = { E_CONTACT_FAMILY_NAME, E_CONTACT_GIVEN_NAME };
+ EBookCursorSortType sort_types[] = { E_BOOK_CURSOR_SORT_ASCENDING, E_BOOK_CURSOR_SORT_ASCENDING };
+ EBookCacheCursor *cursor;
+ gint total = -1, position = -1, expect_total;
+ GSList *list;
+ GError *error = NULL;
+
+ expect_total = 1 +
+ (((flags & EXPECT_CUSTOM_1) != 0) ? 1 : 0) +
+ (((flags & EXPECT_CUSTOM_9) != 0) ? 1 : 0) +
+ (((flags & EXPECT_SIMPLE_1) != 0) ? 1 : 0) +
+ (((flags & EXPECT_SIMPLE_2) != 0) ? 1 : 0);
+
+ cursor = e_book_cache_cursor_new (fixture->book_cache, sexp, sort_fields, sort_types, 2, &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (cursor);
+
+ g_assert (e_book_cache_cursor_calculate (fixture->book_cache, cursor, &total, &position, NULL,
&error));
+ g_assert_no_error (error);
+ g_assert_cmpint (total, ==, expect_total);
+ g_assert_cmpint (position, ==, 0);
+
+ g_assert_cmpint (e_book_cache_cursor_step (fixture->book_cache, cursor,
E_BOOK_CACHE_CURSOR_STEP_FETCH,
+ E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT, total, &list, NULL, &error), ==, total);
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (list), ==, total);
+
+ test_check_search_result (list, flags | HAS_SEARCH_DATA);
+
+ g_slist_free_full (list, e_book_cache_search_data_free);
+ e_book_cache_cursor_free (fixture->book_cache, cursor);
+}
+
+static void
+test_basic_search (TCUFixture *fixture,
+ guint32 flags)
+{
+ EBookQuery *query;
+ GSList *list = NULL;
+ gchar *sexp;
+ gint expect_total;
+ GError *error = NULL;
+
+ expect_total = 2 +
+ ((flags & EXPECT_CUSTOM_1) != 0 ? 1 : 0) +
+ ((flags & EXPECT_SIMPLE_1) != 0 ? 1 : 0) +
+ ((flags & EXPECT_SIMPLE_2) != 0 ? 1 : 0);
+
+ /* All contacts first */
+ g_assert (e_book_cache_search (fixture->book_cache, NULL, FALSE, &list, NULL, &error));
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (list), ==, expect_total);
+ test_check_search_result (list, flags | EXPECT_CUSTOM_9 | HAS_SEARCH_DATA);
+ g_slist_free_full (list, e_book_cache_search_data_free);
+ list = NULL;
+
+ g_assert (e_book_cache_search (fixture->book_cache, NULL, TRUE, &list, NULL, &error));
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (list), ==, expect_total);
+ test_check_search_result (list, flags | EXPECT_CUSTOM_9 | HAS_SEARCH_DATA | HAS_META_CONTACTS);
+ g_slist_free_full (list, e_book_cache_search_data_free);
+ list = NULL;
+
+ g_assert (e_book_cache_search_uids (fixture->book_cache, NULL, &list, NULL, &error));
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (list), ==, expect_total);
+ test_check_search_result (list, flags | EXPECT_CUSTOM_9);
+ g_slist_free_full (list, g_free);
+ list = NULL;
+
+ test_basic_cursor (fixture, flags | EXPECT_CUSTOM_9, NULL);
+
+ /* Only Brown, aka custom-3, as an autocomplete query */
+ query = e_book_query_field_test (E_CONTACT_FULL_NAME, E_BOOK_QUERY_CONTAINS, "Brown");
+ sexp = e_book_query_to_string (query);
+ e_book_query_unref (query);
+
+ g_assert (e_book_cache_search (fixture->book_cache, sexp, FALSE, &list, NULL, &error));
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (list), ==, 1);
+ test_check_search_result (list, HAS_SEARCH_DATA);
+ g_slist_free_full (list, e_book_cache_search_data_free);
+ list = NULL;
+
+ g_assert (e_book_cache_search (fixture->book_cache, sexp, TRUE, &list, NULL, &error));
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (list), ==, 1);
+ test_check_search_result (list, HAS_SEARCH_DATA | HAS_META_CONTACTS);
+ g_slist_free_full (list, e_book_cache_search_data_free);
+ list = NULL;
+
+ g_assert (e_book_cache_search_uids (fixture->book_cache, sexp, &list, NULL, &error));
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (list), ==, 1);
+ test_check_search_result (list, EXPECT_DEFAULT);
+ g_slist_free_full (list, g_free);
+ list = NULL;
+
+ test_basic_cursor (fixture, EXPECT_DEFAULT, sexp);
+
+ g_free (sexp);
+
+ /* Only Brown, aka custom-3, as a regular query */
+ query = e_book_query_field_test (E_CONTACT_EMAIL, E_BOOK_QUERY_CONTAINS, "brown");
+ sexp = e_book_query_to_string (query);
+ e_book_query_unref (query);
+
+ g_assert (e_book_cache_search (fixture->book_cache, sexp, FALSE, &list, NULL, &error));
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (list), ==, 1);
+ test_check_search_result (list, HAS_SEARCH_DATA);
+ g_slist_free_full (list, e_book_cache_search_data_free);
+ list = NULL;
+
+ g_assert (e_book_cache_search (fixture->book_cache, sexp, TRUE, &list, NULL, &error));
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (list), ==, 1);
+ test_check_search_result (list, HAS_SEARCH_DATA | HAS_META_CONTACTS);
+ g_slist_free_full (list, e_book_cache_search_data_free);
+ list = NULL;
+
+ g_assert (e_book_cache_search_uids (fixture->book_cache, sexp, &list, NULL, &error));
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (list), ==, 1);
+ test_check_search_result (list, EXPECT_DEFAULT);
+ g_slist_free_full (list, g_free);
+ list = NULL;
+
+ test_basic_cursor (fixture, EXPECT_DEFAULT, sexp);
+
+ g_free (sexp);
+
+ /* Invalid expression */
+ g_assert (!e_book_cache_search (fixture->book_cache, "invalid expression here", TRUE, &list, NULL,
&error));
+ g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY);
+ g_assert_null (list);
+ g_clear_error (&error);
+
+ g_assert (!e_book_cache_search_uids (fixture->book_cache, "invalid expression here", &list, NULL,
&error));
+ g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY);
+ g_assert_null (list);
+ g_clear_error (&error);
+}
+
+/* Expects pairs of UID (gchar *) and EOfflineState (gint), terminated by NULL */
+static void
+test_check_offline_changes (TCUFixture *fixture,
+ ...) G_GNUC_NULL_TERMINATED;
+
+static void
+test_check_offline_changes (TCUFixture *fixture,
+ ...)
+{
+ GSList *changes, *link;
+ va_list args;
+ GHashTable *expects;
+ const gchar *uid;
+ GError *error = NULL;
+
+ changes = e_cache_get_offline_changes (E_CACHE (fixture->book_cache), NULL, &error);
+
+ g_assert_no_error (error);
+
+ expects = g_hash_table_new (g_str_hash, g_str_equal);
+
+ va_start (args, fixture);
+ uid = va_arg (args, const gchar *);
+ while (uid) {
+ gint state = va_arg (args, gint);
+
+ g_hash_table_insert (expects, (gpointer) uid, GINT_TO_POINTER (state));
+ uid = va_arg (args, const gchar *);
+ }
+ va_end (args);
+
+ g_assert_cmpint (g_slist_length (changes), ==, g_hash_table_size (expects));
+
+ for (link = changes; link; link = g_slist_next (link)) {
+ ECacheOfflineChange *change = link->data;
+ gint expect_state;
+
+ g_assert_nonnull (change);
+ g_assert (g_hash_table_contains (expects, change->uid));
+
+ expect_state = GPOINTER_TO_INT (g_hash_table_lookup (expects, change->uid));
+ g_assert_cmpint (expect_state, ==, change->state);
+ }
+
+ g_slist_free_full (changes, e_cache_offline_change_free);
+ g_hash_table_destroy (expects);
+}
+
+static EOfflineState
+test_check_offline_state (TCUFixture *fixture,
+ const gchar *uid,
+ EOfflineState expect_offline_state)
+{
+ EOfflineState offline_state;
+ GError *error = NULL;
+
+ offline_state = e_cache_get_offline_state (E_CACHE (fixture->book_cache), uid, NULL, &error);
+ g_assert_cmpint (offline_state, ==, expect_offline_state);
+
+ if (offline_state == E_OFFLINE_STATE_UNKNOWN) {
+ g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+ g_clear_error (&error);
+ } else {
+ g_assert_no_error (error);
+ }
+
+ return offline_state;
+}
+
+static void
+test_check_edit_saved (TCUFixture *fixture,
+ const gchar *uid,
+ const gchar *rev_value)
+{
+ EContact *contact = NULL;
+ GError *error = NULL;
+
+ g_assert (e_book_cache_get_contact (fixture->book_cache, uid, FALSE, &contact, NULL, &error));
+ g_assert_no_error (error);
+ g_assert_nonnull (contact);
+ g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_REV), ==, rev_value);
+
+ g_clear_object (&contact);
+
+ g_assert (e_book_cache_get_contact (fixture->book_cache, uid, TRUE, &contact, NULL, &error));
+ g_assert_no_error (error);
+ g_assert_nonnull (contact);
+ g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_REV), ==, rev_value);
+
+ g_clear_object (&contact);
+}
+
+static void
+test_verify_storage (TCUFixture *fixture,
+ const gchar *uid,
+ const gchar *expect_rev,
+ const gchar *expect_extra,
+ EOfflineState expect_offline_state)
+{
+ EContact *contact = NULL;
+ EOfflineState offline_state;
+ gchar *vcard, *saved_rev = NULL, *saved_extra = NULL;
+ GError *error = NULL;
+
+ if (expect_offline_state == E_OFFLINE_STATE_LOCALLY_DELETED ||
+ expect_offline_state == E_OFFLINE_STATE_UNKNOWN) {
+ g_assert (!e_book_cache_get_contact (fixture->book_cache, uid, FALSE, &contact, NULL,
&error));
+ g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+ g_assert_null (contact);
+
+ g_clear_error (&error);
+ } else {
+ g_assert (e_book_cache_get_contact (fixture->book_cache, uid, FALSE, &contact, NULL, &error));
+ g_assert_no_error (error);
+ g_assert_nonnull (contact);
+ }
+
+ offline_state = test_check_offline_state (fixture, uid, expect_offline_state);
+
+ if (offline_state == E_OFFLINE_STATE_UNKNOWN) {
+ g_assert (!e_cache_contains (E_CACHE (fixture->book_cache), uid, E_CACHE_EXCLUDE_DELETED));
+ g_assert (!e_cache_contains (E_CACHE (fixture->book_cache), uid, E_CACHE_INCLUDE_DELETED));
+ test_check_offline_changes (fixture, NULL);
+ return;
+ }
+
+ g_assert (e_book_cache_get_contact_extra (fixture->book_cache, uid, &saved_extra, NULL, &error));
+ g_assert_no_error (error);
+
+ g_assert_cmpstr (saved_extra, ==, expect_extra);
+ g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_REV), ==, expect_rev);
+
+ g_clear_object (&contact);
+
+ vcard = e_cache_get (E_CACHE (fixture->book_cache), uid, &saved_rev, NULL, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (vcard);
+ g_assert_nonnull (saved_rev);
+
+ g_assert_cmpstr (saved_rev, ==, expect_rev);
+
+ g_free (vcard);
+ g_free (saved_rev);
+ g_free (saved_extra);
+
+ if (expect_offline_state == E_OFFLINE_STATE_SYNCED)
+ test_check_offline_changes (fixture, NULL);
+ else
+ test_check_offline_changes (fixture, uid, expect_offline_state, NULL);
+}
+
+static void
+test_offline_basics (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ EOfflineState states[] = {
+ E_OFFLINE_STATE_LOCALLY_CREATED,
+ E_OFFLINE_STATE_LOCALLY_MODIFIED,
+ E_OFFLINE_STATE_LOCALLY_DELETED,
+ E_OFFLINE_STATE_SYNCED
+ };
+ EContact *contact = NULL;
+ gint ii;
+ const gchar *uid;
+ gchar *saved_extra = NULL, *tmp;
+ GError *error = NULL;
+
+ /* Basic ECache stuff */
+ e_cache_set_version (E_CACHE (fixture->book_cache), 123);
+ g_assert_cmpint (e_cache_get_version (E_CACHE (fixture->book_cache)), ==, 123);
+
+ e_cache_set_revision (E_CACHE (fixture->book_cache), "rev-321");
+ tmp = e_cache_dup_revision (E_CACHE (fixture->book_cache));
+ g_assert_cmpstr ("rev-321", ==, tmp);
+ g_free (tmp);
+
+ g_assert (e_cache_set_key (E_CACHE (fixture->book_cache), "my-key-str", "key-str-value", &error));
+ g_assert_no_error (error);
+
+ tmp = e_cache_dup_key (E_CACHE (fixture->book_cache), "my-key-str", &error);
+ g_assert_no_error (error);
+ g_assert_cmpstr ("key-str-value", ==, tmp);
+ g_free (tmp);
+
+ g_assert (e_cache_set_key_int (E_CACHE (fixture->book_cache), "version", 567, &error));
+ g_assert_no_error (error);
+
+ g_assert_cmpint (e_cache_get_key_int (E_CACHE (fixture->book_cache), "version", &error), ==, 567);
+ g_assert_no_error (error);
+
+ g_assert_cmpint (e_cache_get_version (E_CACHE (fixture->book_cache)), ==, 123);
+
+ /* Add in online */
+ test_fill_cache (fixture, &contact);
+ g_assert_nonnull (contact);
+
+ uid = e_contact_get_const (contact, E_CONTACT_UID);
+ g_assert_nonnull (uid);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ g_assert (e_book_cache_set_contact_extra (fixture->book_cache, uid, "extra-0", NULL, &error));
+ g_assert_no_error (error);
+
+ g_assert (e_book_cache_get_contact_extra (fixture->book_cache, uid, &saved_extra, NULL, &error));
+ g_assert_no_error (error);
+ g_assert_cmpstr (saved_extra, ==, "extra-0");
+
+ g_free (saved_extra);
+ saved_extra = NULL;
+
+ e_contact_set (contact, E_CONTACT_REV, "rev-0");
+
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+
+ test_check_offline_changes (fixture, NULL);
+
+ /* Try change status */
+ for (ii = 0; ii < G_N_ELEMENTS (states); ii++) {
+ g_assert (e_cache_set_offline_state (E_CACHE (fixture->book_cache), uid, states[ii], NULL,
&error));
+ g_assert_no_error (error);
+
+ test_check_offline_state (fixture, uid, states[ii]);
+
+ if (states[ii] != E_OFFLINE_STATE_SYNCED)
+ test_check_offline_changes (fixture, uid, states[ii], NULL);
+
+ if (states[ii] == E_OFFLINE_STATE_LOCALLY_DELETED) {
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache),
E_CACHE_EXCLUDE_DELETED, NULL, &error), ==, 2);
+ g_assert_no_error (error);
+
+ g_assert (!e_cache_contains (E_CACHE (fixture->book_cache), uid,
E_CACHE_EXCLUDE_DELETED));
+
+ g_assert (e_book_cache_set_contact_extra (fixture->book_cache, uid, "extra-1", NULL,
&error));
+ g_assert_no_error (error);
+
+ g_assert (e_book_cache_get_contact_extra (fixture->book_cache, uid, &saved_extra,
NULL, &error));
+ g_assert_no_error (error);
+ g_assert_cmpstr (saved_extra, ==, "extra-1");
+
+ g_free (saved_extra);
+ saved_extra = NULL;
+
+ /* Search when locally deleted */
+ test_basic_search (fixture, EXPECT_DEFAULT);
+ } else {
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache),
E_CACHE_EXCLUDE_DELETED, NULL, &error), ==, 3);
+ g_assert_no_error (error);
+
+ g_assert (e_cache_contains (E_CACHE (fixture->book_cache), uid,
E_CACHE_EXCLUDE_DELETED));
+
+ /* Search when locally available */
+ test_basic_search (fixture, EXPECT_CUSTOM_1);
+ }
+
+ g_assert (e_cache_contains (E_CACHE (fixture->book_cache), uid, E_CACHE_INCLUDE_DELETED));
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_INCLUDE_DELETED,
NULL, &error), ==, 3);
+ g_assert_no_error (error);
+ }
+
+ test_check_offline_changes (fixture, NULL);
+
+ /* Edit in online */
+ e_contact_set (contact, E_CONTACT_REV, "rev-1");
+
+ g_assert (e_book_cache_put_contact (fixture->book_cache, contact, NULL, E_CACHE_IS_ONLINE, NULL,
&error));
+ g_assert_no_error (error);
+
+ test_verify_storage (fixture, uid, "rev-1", NULL, E_OFFLINE_STATE_SYNCED);
+ test_check_offline_changes (fixture, NULL);
+
+ e_contact_set (contact, E_CONTACT_REV, "rev-2");
+
+ g_assert (e_book_cache_put_contact (fixture->book_cache, contact, "extra-2", E_CACHE_IS_ONLINE, NULL,
&error));
+ g_assert_no_error (error);
+
+ test_verify_storage (fixture, uid, "rev-2", "extra-2", E_OFFLINE_STATE_SYNCED);
+ test_check_offline_changes (fixture, NULL);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ /* Search before delete */
+ test_basic_search (fixture, EXPECT_CUSTOM_1);
+
+ /* Delete in online */
+ g_assert (e_book_cache_remove_contact (fixture->book_cache, uid, E_CACHE_IS_ONLINE, NULL, &error));
+ g_assert_no_error (error);
+
+ g_assert (!e_cache_set_offline_state (E_CACHE (fixture->book_cache), uid,
E_OFFLINE_STATE_LOCALLY_MODIFIED, NULL, &error));
+ g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+ g_clear_error (&error);
+
+ test_verify_storage (fixture, uid, NULL, NULL, E_OFFLINE_STATE_UNKNOWN);
+ test_check_offline_changes (fixture, NULL);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 2);
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_INCLUDE_DELETED, NULL,
&error), ==, 2);
+ g_assert_no_error (error);
+
+ g_assert (!e_book_cache_set_contact_extra (fixture->book_cache, uid, "extra-3", NULL, &error));
+ g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+ g_clear_error (&error);
+
+ g_assert (!e_book_cache_get_contact_extra (fixture->book_cache, uid, &saved_extra, NULL, &error));
+ g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+ g_assert_null (saved_extra);
+ g_clear_error (&error);
+
+ g_clear_object (&contact);
+
+ /* Search after delete */
+ test_basic_search (fixture, EXPECT_DEFAULT);
+}
+
+static void
+test_offline_add_one (TCUFixture *fixture,
+ const gchar *case_name,
+ gint expect_total,
+ guint32 flags,
+ EContact **out_contact)
+{
+ EContact *contact = NULL;
+ const gchar *uid;
+ GError *error = NULL;
+
+ if (!(flags & SKIP_CONTACT_PUT)) {
+ contact = tcu_new_contact_from_test_case (case_name);
+ g_assert_nonnull (contact);
+
+ uid = e_contact_get_const (contact, E_CONTACT_UID);
+ g_assert_nonnull (uid);
+
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_UNKNOWN);
+
+ /* Add a contact in offline */
+ g_assert (e_book_cache_put_contact (fixture->book_cache, contact, NULL, E_CACHE_IS_OFFLINE,
NULL, &error));
+ g_assert_no_error (error);
+ } else {
+ uid = case_name;
+ }
+
+ if ((flags & EXPECT_SIMPLE_1) != 0) {
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_CREATED);
+ } else {
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_UNKNOWN);
+ }
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, expect_total);
+ g_assert_no_error (error);
+
+ test_basic_search (fixture, flags);
+
+ if (out_contact)
+ *out_contact = contact;
+ else
+ g_clear_object (&contact);
+}
+
+static void
+test_offline_add (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ GError *error = NULL;
+
+ /* Add in online */
+ test_fill_cache (fixture, NULL);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_check_offline_changes (fixture, NULL);
+
+ /* Add the first in offline */
+ test_offline_add_one (fixture, "simple-1", 4, EXPECT_SIMPLE_1 | EXPECT_CUSTOM_1, NULL);
+
+ test_check_offline_changes (fixture,
+ "simple-1", E_OFFLINE_STATE_LOCALLY_CREATED,
+ NULL);
+
+ /* Add the second in offline */
+ test_offline_add_one (fixture, "simple-2", 5, EXPECT_SIMPLE_1 | EXPECT_SIMPLE_2 | EXPECT_CUSTOM_1,
NULL);
+
+ test_check_offline_changes (fixture,
+ "simple-1", E_OFFLINE_STATE_LOCALLY_CREATED,
+ "simple-2", E_OFFLINE_STATE_LOCALLY_CREATED,
+ NULL);
+}
+
+static void
+test_offline_add_edit (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ EContact *contact = NULL;
+ GError *error = NULL;
+
+ /* Add in online */
+ test_fill_cache (fixture, NULL);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_check_offline_changes (fixture, NULL);
+
+ /* Add in offline */
+ test_offline_add_one (fixture, "simple-1", 4, EXPECT_SIMPLE_1 | EXPECT_CUSTOM_1, &contact);
+ g_assert_nonnull (contact);
+
+ test_check_offline_changes (fixture,
+ "simple-1", E_OFFLINE_STATE_LOCALLY_CREATED,
+ NULL);
+
+ /* Modify added in offline */
+ e_contact_set (contact, E_CONTACT_REV, "rev-2");
+
+ g_assert (e_book_cache_put_contact (fixture->book_cache, contact, NULL, E_CACHE_IS_OFFLINE, NULL,
&error));
+ g_assert_no_error (error);
+
+ test_offline_add_one (fixture, "simple-1", 4, EXPECT_SIMPLE_1 | EXPECT_CUSTOM_1 | SKIP_CONTACT_PUT,
NULL);
+
+ test_check_offline_changes (fixture,
+ "simple-1", E_OFFLINE_STATE_LOCALLY_CREATED,
+ NULL);
+
+ test_check_edit_saved (fixture, "simple-1", "rev-2");
+
+ g_clear_object (&contact);
+}
+
+static void
+test_offline_add_delete (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ EContact *contact = NULL;
+ const gchar *uid;
+ GError *error = NULL;
+
+ /* Add in online */
+ test_fill_cache (fixture, NULL);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_check_offline_changes (fixture, NULL);
+
+ /* Add in offline */
+ test_offline_add_one (fixture, "simple-1", 4, EXPECT_SIMPLE_1 | EXPECT_CUSTOM_1, &contact);
+ g_assert_nonnull (contact);
+
+ test_check_offline_changes (fixture,
+ "simple-1", E_OFFLINE_STATE_LOCALLY_CREATED,
+ NULL);
+
+ uid = e_contact_get_const (contact, E_CONTACT_UID);
+ g_assert_nonnull (uid);
+
+ /* Delete added in offline */
+
+ g_assert (e_book_cache_remove_contact (fixture->book_cache, uid, E_CACHE_IS_OFFLINE, NULL, &error));
+ g_assert_no_error (error);
+
+ test_offline_add_one (fixture, "simple-1", 3, EXPECT_CUSTOM_1 | SKIP_CONTACT_PUT, NULL);
+
+ test_check_offline_changes (fixture, NULL);
+
+ g_clear_object (&contact);
+}
+
+static void
+test_offline_add_delete_add (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ EContact *contact = NULL;
+ const gchar *uid;
+ GError *error = NULL;
+
+ /* Add in online */
+ test_fill_cache (fixture, NULL);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_check_offline_changes (fixture, NULL);
+
+ /* Add in offline */
+ test_offline_add_one (fixture, "simple-1", 4, EXPECT_SIMPLE_1 | EXPECT_CUSTOM_1, &contact);
+ g_assert_nonnull (contact);
+
+ test_check_offline_changes (fixture,
+ "simple-1", E_OFFLINE_STATE_LOCALLY_CREATED,
+ NULL);
+
+ uid = e_contact_get_const (contact, E_CONTACT_UID);
+ g_assert_nonnull (uid);
+
+ /* Delete added in offline */
+ g_assert (e_book_cache_remove_contact (fixture->book_cache, uid, E_CACHE_IS_OFFLINE, NULL, &error));
+ g_assert_no_error (error);
+
+ test_offline_add_one (fixture, "simple-1", 3, EXPECT_CUSTOM_1 | SKIP_CONTACT_PUT, NULL);
+
+ test_check_offline_changes (fixture, NULL);
+
+ g_clear_object (&contact);
+
+ /* Add in offline again */
+ test_offline_add_one (fixture, "simple-1", 4, EXPECT_SIMPLE_1 | EXPECT_CUSTOM_1, NULL);
+
+ test_check_offline_changes (fixture,
+ "simple-1", E_OFFLINE_STATE_LOCALLY_CREATED,
+ NULL);
+}
+
+static void
+test_offline_add_resync (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ GError *error = NULL;
+
+ /* Add in online */
+ test_fill_cache (fixture, NULL);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_check_offline_changes (fixture, NULL);
+
+ /* Add in offline */
+ test_offline_add_one (fixture, "simple-1", 4, EXPECT_SIMPLE_1 | EXPECT_CUSTOM_1, NULL);
+
+ test_check_offline_changes (fixture,
+ "simple-1", E_OFFLINE_STATE_LOCALLY_CREATED,
+ NULL);
+
+ /* Resync all offline changes */
+ g_assert (e_cache_clear_offline_changes (E_CACHE (fixture->book_cache), NULL, &error));
+ g_assert_no_error (error);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 4);
+ g_assert_no_error (error);
+
+ test_basic_search (fixture, EXPECT_SIMPLE_1 | EXPECT_CUSTOM_1);
+ test_check_offline_changes (fixture, NULL);
+ test_check_offline_state (fixture, "simple-1", E_OFFLINE_STATE_SYNCED);
+}
+
+static void
+test_offline_edit_common (TCUFixture *fixture,
+ gchar **out_uid)
+{
+ EContact *contact = NULL;
+ const gchar *uid;
+ GError *error = NULL;
+
+ /* Add in online */
+ test_fill_cache (fixture, &contact);
+ g_assert_nonnull (contact);
+
+ uid = e_contact_get_const (contact, E_CONTACT_UID);
+ g_assert_nonnull (uid);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_check_offline_changes (fixture, NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+
+ /* Modify in offline */
+ e_contact_set (contact, E_CONTACT_REV, "rev-2");
+
+ g_assert (e_book_cache_put_contact (fixture->book_cache, contact, NULL, E_CACHE_IS_OFFLINE, NULL,
&error));
+ g_assert_no_error (error);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_check_edit_saved (fixture, uid, "rev-2");
+
+ test_basic_search (fixture, EXPECT_CUSTOM_1);
+ test_check_offline_changes (fixture,
+ uid, E_OFFLINE_STATE_LOCALLY_MODIFIED,
+ NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_MODIFIED);
+
+ if (out_uid)
+ *out_uid = g_strdup (uid);
+
+ g_clear_object (&contact);
+}
+
+static void
+test_offline_edit (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ test_offline_edit_common (fixture, NULL);
+}
+
+static void
+test_offline_edit_delete (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ EContact *contact = NULL;
+ gchar *uid = NULL;
+ GError *error = NULL;
+
+ test_offline_edit_common (fixture, &uid);
+
+ /* Delete the modified contact in offline */
+ g_assert (e_book_cache_remove_contact (fixture->book_cache, uid, E_CACHE_IS_OFFLINE, NULL, &error));
+ g_assert_no_error (error);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 2);
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_INCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_basic_search (fixture, EXPECT_DEFAULT);
+ test_check_offline_changes (fixture,
+ uid, E_OFFLINE_STATE_LOCALLY_DELETED,
+ NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_DELETED);
+
+ g_assert (!e_book_cache_get_contact (fixture->book_cache, uid, FALSE, &contact, NULL, &error));
+ g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+ g_assert_null (contact);
+
+ g_clear_error (&error);
+ g_free (uid);
+}
+
+static void
+test_offline_edit_resync (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ gchar *uid = NULL;
+ GError *error = NULL;
+
+ test_offline_edit_common (fixture, &uid);
+
+ /* Resync all offline changes */
+ g_assert (e_cache_clear_offline_changes (E_CACHE (fixture->book_cache), NULL, &error));
+ g_assert_no_error (error);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_basic_search (fixture, EXPECT_CUSTOM_1);
+ test_check_offline_changes (fixture, NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+
+ g_free (uid);
+}
+
+static void
+test_offline_delete (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ EContact *contact = NULL;
+ const gchar *uid;
+ GError *error = NULL;
+
+ /* Add in online */
+ test_fill_cache (fixture, &contact);
+ g_assert_nonnull (contact);
+
+ uid = e_contact_get_const (contact, E_CONTACT_UID);
+ g_assert_nonnull (uid);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_check_offline_changes (fixture, NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+
+ /* Delete in offline */
+ g_assert (e_book_cache_remove_contact (fixture->book_cache, uid, E_CACHE_IS_OFFLINE, NULL, &error));
+ g_assert_no_error (error);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 2);
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_INCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_basic_search (fixture, EXPECT_DEFAULT);
+ test_check_offline_changes (fixture,
+ uid, E_OFFLINE_STATE_LOCALLY_DELETED,
+ NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_DELETED);
+
+ g_clear_object (&contact);
+}
+
+static void
+test_offline_delete_add (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ EContact *contact = NULL;
+ const gchar *uid;
+ GError *error = NULL;
+
+ /* Add in online */
+ test_fill_cache (fixture, &contact);
+ g_assert_nonnull (contact);
+
+ uid = e_contact_get_const (contact, E_CONTACT_UID);
+ g_assert_nonnull (uid);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_check_offline_changes (fixture, NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+
+ /* Delete locally created in offline */
+ test_offline_add_one (fixture, "simple-1", 4, EXPECT_SIMPLE_1 | EXPECT_CUSTOM_1, NULL);
+ g_assert (e_book_cache_remove_contact (fixture->book_cache, "simple-1", E_CACHE_IS_OFFLINE, NULL,
&error));
+ g_assert_no_error (error);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_INCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_basic_search (fixture, EXPECT_CUSTOM_1);
+ test_check_offline_changes (fixture, NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+ test_check_offline_state (fixture, "simple-1", E_OFFLINE_STATE_UNKNOWN);
+
+ /* Delete synced in offline */
+ g_assert (e_book_cache_remove_contact (fixture->book_cache, uid, E_CACHE_IS_OFFLINE, NULL, &error));
+ g_assert_no_error (error);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 2);
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_INCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_basic_search (fixture, EXPECT_DEFAULT);
+ test_check_offline_changes (fixture,
+ uid, E_OFFLINE_STATE_LOCALLY_DELETED,
+ NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_DELETED);
+
+ /* Add one in offline */
+ test_offline_add_one (fixture, "simple-1", 3, EXPECT_SIMPLE_1, NULL);
+
+ test_check_offline_changes (fixture,
+ uid, E_OFFLINE_STATE_LOCALLY_DELETED,
+ "simple-1", E_OFFLINE_STATE_LOCALLY_CREATED,
+ NULL);
+
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_DELETED);
+ test_check_offline_state (fixture, "simple-1", E_OFFLINE_STATE_LOCALLY_CREATED);
+
+ /* Modify the previous contact and add it again */
+ e_contact_set (contact, E_CONTACT_REV, "rev-3");
+
+ g_assert (e_book_cache_put_contact (fixture->book_cache, contact, NULL, E_CACHE_IS_OFFLINE, NULL,
&error));
+ g_assert_no_error (error);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 4);
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_INCLUDE_DELETED, NULL,
&error), ==, 4);
+ g_assert_no_error (error);
+
+ test_check_edit_saved (fixture, uid, "rev-3");
+
+ test_basic_search (fixture, EXPECT_CUSTOM_1 | EXPECT_SIMPLE_1);
+ test_check_offline_changes (fixture,
+ uid, E_OFFLINE_STATE_LOCALLY_MODIFIED,
+ "simple-1", E_OFFLINE_STATE_LOCALLY_CREATED,
+ NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_MODIFIED);
+ test_check_offline_state (fixture, "simple-1", E_OFFLINE_STATE_LOCALLY_CREATED);
+
+ g_clear_object (&contact);
+}
+
+static void
+test_offline_delete_resync (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ EContact *contact = NULL;
+ const gchar *uid;
+ GError *error = NULL;
+
+ /* Add in online */
+ test_fill_cache (fixture, &contact);
+ g_assert_nonnull (contact);
+
+ uid = e_contact_get_const (contact, E_CONTACT_UID);
+ g_assert_nonnull (uid);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_check_offline_changes (fixture, NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+
+ /* Delete in offline */
+ g_assert (e_book_cache_remove_contact (fixture->book_cache, uid, E_CACHE_IS_OFFLINE, NULL, &error));
+ g_assert_no_error (error);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 2);
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_INCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_basic_search (fixture, EXPECT_DEFAULT);
+ test_check_offline_changes (fixture,
+ uid, E_OFFLINE_STATE_LOCALLY_DELETED,
+ NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_DELETED);
+
+ /* Resync all offline changes */
+ e_cache_clear_offline_changes (E_CACHE (fixture->book_cache), NULL, &error);
+ g_assert_no_error (error);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 2);
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_INCLUDE_DELETED, NULL,
&error), ==, 2);
+ g_assert_no_error (error);
+
+ test_basic_search (fixture, EXPECT_DEFAULT);
+ test_check_offline_changes (fixture, NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_UNKNOWN);
+
+ g_clear_object (&contact);
+}
+
+gint
+main (gint argc,
+ gchar **argv)
+{
+ TCUClosure closure = { NULL };
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+ g_type_init ();
+#endif
+ g_test_init (&argc, &argv, NULL);
+
+ /* Ensure that the client and server get the same locale */
+ g_assert (g_setenv ("LC_ALL", "en_US.UTF-8", TRUE));
+ setlocale (LC_ALL, "");
+
+ g_test_add ("/EBookCache/Offline/Basics", TCUFixture, &closure,
+ tcu_fixture_setup, test_offline_basics, tcu_fixture_teardown);
+ g_test_add ("/EBookCache/Offline/Add", TCUFixture, &closure,
+ tcu_fixture_setup, test_offline_add, tcu_fixture_teardown);
+ g_test_add ("/EBookCache/Offline/AddEdit", TCUFixture, &closure,
+ tcu_fixture_setup, test_offline_add_edit, tcu_fixture_teardown);
+ g_test_add ("/EBookCache/Offline/AddDelete", TCUFixture, &closure,
+ tcu_fixture_setup, test_offline_add_delete, tcu_fixture_teardown);
+ g_test_add ("/EBookCache/Offline/AddDeleteAdd", TCUFixture, &closure,
+ tcu_fixture_setup, test_offline_add_delete_add, tcu_fixture_teardown);
+ g_test_add ("/EBookCache/Offline/AddResync", TCUFixture, &closure,
+ tcu_fixture_setup, test_offline_add_resync, tcu_fixture_teardown);
+ g_test_add ("/EBookCache/Offline/Edit", TCUFixture, &closure,
+ tcu_fixture_setup, test_offline_edit, tcu_fixture_teardown);
+ g_test_add ("/EBookCache/Offline/EditDelete", TCUFixture, &closure,
+ tcu_fixture_setup, test_offline_edit_delete, tcu_fixture_teardown);
+ g_test_add ("/EBookCache/Offline/EditResync", TCUFixture, &closure,
+ tcu_fixture_setup, test_offline_edit_resync, tcu_fixture_teardown);
+ g_test_add ("/EBookCache/Offline/Delete", TCUFixture, &closure,
+ tcu_fixture_setup, test_offline_delete, tcu_fixture_teardown);
+ g_test_add ("/EBookCache/Offline/DeleteAdd", TCUFixture, &closure,
+ tcu_fixture_setup, test_offline_delete_add, tcu_fixture_teardown);
+ g_test_add ("/EBookCache/Offline/DeleteResync", TCUFixture, &closure,
+ tcu_fixture_setup, test_offline_delete_resync, tcu_fixture_teardown);
+
+ return g_test_run ();
+}
diff --git a/tests/libedata-book/test-book-cache-utils.c b/tests/libedata-book/test-book-cache-utils.c
new file mode 100644
index 0000000..5db1684
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-utils.c
@@ -0,0 +1,695 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2013, Openismus GmbH
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Tristan Van Berkom <tristanvb openismus com>
+ */
+
+#include "evolution-data-server-config.h"
+
+#ifdef HAVE_SYS_WAIT_H
+#include <sys/wait.h>
+#endif
+
+#include <locale.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+
+#include "test-book-cache-utils.h"
+
+gchar *
+tcu_new_vcard_from_test_case (const gchar *case_name)
+{
+ gchar *filename;
+ gchar *case_filename;
+ GFile * file;
+ GError *error = NULL;
+ gchar *vcard;
+
+ case_filename = g_strdup_printf ("%s.vcf", case_name);
+
+ /* In the case of installed tests, they run in ${pkglibexecdir}/installed-tests
+ * and the vcards are installed in ${pkglibexecdir}/installed-tests/vcards
+ */
+ if (g_getenv ("TEST_INSTALLED_SERVICES") != NULL)
+ filename = g_build_filename (INSTALLED_TEST_DIR, "vcards", case_filename, NULL);
+ else
+ filename = g_build_filename (SRCDIR, "..", "libebook", "data", "vcards", case_filename, NULL);
+
+ file = g_file_new_for_path (filename);
+ if (!g_file_load_contents (file, NULL, &vcard, NULL, NULL, &error))
+ g_error (
+ "failed to read test contact file '%s': %s",
+ filename, error->message);
+
+ g_free (case_filename);
+ g_free (filename);
+ g_object_unref (file);
+
+ return vcard;
+}
+
+EContact *
+tcu_new_contact_from_test_case (const gchar *case_name)
+{
+ gchar *vcard;
+ EContact *contact = NULL;
+
+ vcard = tcu_new_vcard_from_test_case (case_name);
+ if (vcard)
+ contact = e_contact_new_from_vcard (vcard);
+ g_free (vcard);
+
+ if (!contact)
+ g_error (
+ "failed to construct contact from test case '%s'",
+ case_name);
+
+ return contact;
+}
+
+void
+tcu_add_contact_from_test_case (TCUFixture *fixture,
+ const gchar *case_name,
+ EContact **ret_contact)
+{
+ EContact *contact;
+ GError *error = NULL;
+
+ contact = tcu_new_contact_from_test_case (case_name);
+
+ if (!e_book_cache_put_contact (fixture->book_cache, contact, case_name, E_CACHE_IS_ONLINE, NULL,
&error))
+ g_error ("Failed to add contact: %s", error->message);
+
+ if (ret_contact)
+ *ret_contact = g_object_ref (contact);
+
+ g_clear_object (&contact);
+}
+
+static void
+delete_work_directory (const gchar *filename)
+{
+ /* XXX Instead of complex error checking here, we should ideally use
+ * a recursive GDir / g_unlink() function.
+ *
+ * We cannot use GFile and the recursive delete function without
+ * corrupting our contained D-Bus environment with service files
+ * from the OS.
+ */
+ const gchar *argv[] = { "/bin/rm", "-rf", filename, NULL };
+ gboolean spawn_succeeded;
+ gint exit_status;
+
+ spawn_succeeded = g_spawn_sync (
+ NULL, (gchar **) argv, NULL, 0, NULL, NULL,
+ NULL, NULL, &exit_status, NULL);
+
+ g_assert (spawn_succeeded);
+ #ifndef G_OS_WIN32
+ g_assert (WIFEXITED (exit_status));
+ g_assert_cmpint (WEXITSTATUS (exit_status), ==, 0);
+ #else
+ g_assert_cmpint (exit_status, ==, 0);
+ #endif
+}
+
+ESourceBackendSummarySetup *
+tcu_setup_empty_book (void)
+{
+ ESourceBackendSummarySetup *setup;
+ ESource *scratch;
+ GError *error = NULL;
+
+ scratch = e_source_new_with_uid ("test-source", NULL, &error);
+ if (!scratch)
+ g_error ("Error creating scratch source: %s", error ? error->message : "Unknown error");
+
+ /* This is a bit of a cheat */
+ setup = g_object_new (E_TYPE_SOURCE_BACKEND_SUMMARY_SETUP, "source", scratch, NULL);
+ e_source_backend_summary_setup_set_summary_fields (
+ setup,
+ /* We don't use this field in our tests anyway */
+ E_CONTACT_FILE_AS,
+ 0);
+
+ g_object_unref (scratch);
+
+ return setup;
+}
+
+static void
+e164_changed_cb (EBookCache *book_cache,
+ EContact *contact,
+ gboolean is_replace,
+ gpointer user_data)
+{
+ TCUFixture *fixture = user_data;
+
+ if (is_replace)
+ fixture->n_locale_changes++;
+ else
+ fixture->n_add_changes++;
+}
+
+void
+tcu_fixture_setup (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ TCUClosure *closure = (TCUClosure *) user_data;
+ ESourceBackendSummarySetup *setup = NULL;
+ gchar *filename, *directory;
+ GError *error = NULL;
+
+ if (!g_file_test (CAMEL_PROVIDERDIR, G_FILE_TEST_IS_DIR | G_FILE_TEST_EXISTS)) {
+ if (g_mkdir_with_parents (CAMEL_PROVIDERDIR, 0700) == -1)
+ g_warning ("%s: Failed to create folder '%s': %s\n", G_STRFUNC, CAMEL_PROVIDERDIR,
g_strerror (errno));
+ }
+
+ /* Cleanup from last test */
+ directory = g_build_filename (g_get_tmp_dir (), "test-book-cache", NULL);
+ delete_work_directory (directory);
+ g_free (directory);
+ filename = g_build_filename (g_get_tmp_dir (), "test-book-cache", "cache.db", NULL);
+
+ if (closure->setup_summary)
+ setup = closure->setup_summary ();
+
+ fixture->book_cache = e_book_cache_new_full (filename, NULL, setup, NULL, &error);
+
+ g_clear_object (&setup);
+
+ if (!fixture->book_cache)
+ g_error ("Failed to create the EBookCache: %s", error->message);
+
+ g_free (filename);
+
+ g_signal_connect (fixture->book_cache, "e164-changed",
+ G_CALLBACK (e164_changed_cb), fixture);
+}
+
+void
+tcu_fixture_teardown (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ g_object_unref (fixture->book_cache);
+}
+
+void
+tcu_cursor_fixture_setup (TCUCursorFixture *fixture,
+ gconstpointer user_data)
+{
+ TCUFixture *base_fixture = (TCUFixture *) fixture;
+ TCUCursorClosure *data = (TCUCursorClosure *) user_data;
+ EContactField sort_fields[] = { E_CONTACT_FAMILY_NAME, E_CONTACT_GIVEN_NAME };
+ EBookCursorSortType sort_types[] = { data->sort_type, data->sort_type };
+ GSList *contacts = NULL;
+ GSList *extra_list = NULL;
+ GError *error = NULL;
+ gint ii;
+ gchar *sexp = NULL;
+
+ tcu_fixture_setup (base_fixture, user_data);
+
+ if (data->locale)
+ tcu_cursor_fixture_set_locale (fixture, data->locale);
+ else
+ tcu_cursor_fixture_set_locale (fixture, "en_US.UTF-8");
+
+ for (ii = 0; ii < N_SORTED_CONTACTS; ii++) {
+ gchar *case_name = g_strdup_printf ("sorted-%d", ii + 1);
+ gchar *vcard;
+ EContact *contact;
+
+ vcard = tcu_new_vcard_from_test_case (case_name);
+ contact = e_contact_new_from_vcard (vcard);
+ contacts = g_slist_prepend (contacts, contact);
+ extra_list = g_slist_prepend (extra_list, case_name);
+
+ g_free (vcard);
+
+ fixture->contacts[ii] = g_object_ref (contact);
+ }
+
+ if (!e_book_cache_put_contacts (base_fixture->book_cache, contacts, extra_list, E_CACHE_IS_ONLINE,
NULL, &error)) {
+ /* Dont complain here, we re-use the same addressbook for multiple tests
+ * and we can't add the same contacts twice
+ */
+ if (g_error_matches (error, E_CACHE_ERROR, E_CACHE_ERROR_CONSTRAINT))
+ g_clear_error (&error);
+ else
+ g_error ("Failed to add test contacts: %s", error->message);
+ }
+
+ g_slist_free_full (contacts, g_object_unref);
+ g_slist_free_full (extra_list, g_free);
+
+ /* Allow a surrounding fixture setup to add a query here */
+ if (fixture->query) {
+ sexp = e_book_query_to_string (fixture->query);
+ e_book_query_unref (fixture->query);
+ fixture->query = NULL;
+ }
+
+ fixture->cursor = e_book_cache_cursor_new (
+ base_fixture->book_cache, sexp,
+ sort_fields, sort_types, 2, &error);
+
+ if (!fixture->cursor)
+ g_error ("Failed to create cursor: %s\n", error->message);
+
+ g_free (sexp);
+}
+
+void
+tcu_cursor_fixture_filtered_setup (TCUCursorFixture *fixture,
+ gconstpointer user_data)
+{
+ fixture->query = e_book_query_field_test (E_CONTACT_EMAIL, E_BOOK_QUERY_ENDS_WITH, ".com");
+
+ tcu_cursor_fixture_setup (fixture, user_data);
+}
+
+void
+tcu_cursor_fixture_teardown (TCUCursorFixture *fixture,
+ gconstpointer user_data)
+{
+ TCUFixture *base_fixture = (TCUFixture *) fixture;
+ gint ii;
+
+ for (ii = 0; ii < N_SORTED_CONTACTS; ii++) {
+ if (fixture->contacts[ii])
+ g_object_unref (fixture->contacts[ii]);
+ }
+
+ e_book_cache_cursor_free (base_fixture->book_cache, fixture->cursor);
+ tcu_fixture_teardown (base_fixture, user_data);
+}
+
+void
+tcu_cursor_fixture_set_locale (TCUCursorFixture *fixture,
+ const gchar *locale)
+{
+ TCUFixture *base_fixture = (TCUFixture *) fixture;
+ GError *error = NULL;
+
+ if (!e_book_cache_set_locale (base_fixture->book_cache, locale, NULL, &error))
+ g_error ("Failed to set locale: %s", error->message);
+}
+
+static gint
+find_contact_data (EBookCacheSearchData *data,
+ const gchar *uid)
+{
+ return g_strcmp0 (data->uid, uid);
+}
+
+void
+tcu_assert_contacts_order_slist (GSList *results,
+ GSList *uids)
+{
+ gint position = -1;
+ GSList *link, *l;
+
+ /* Assert that all passed UIDs are found in the
+ * results, and that those UIDs are in the
+ * specified order.
+ */
+ for (l = uids; l; l = l->next) {
+ const gchar *uid = l->data;
+ gint new_position;
+
+ link = g_slist_find_custom (results, uid, (GCompareFunc) find_contact_data);
+ if (!link)
+ g_error ("Specified uid '%s' was not found in results", uid);
+
+ new_position = g_slist_position (results, link);
+ g_assert_cmpint (new_position, >, position);
+ position = new_position;
+ }
+}
+
+void
+tcu_assert_contacts_order (GSList *results,
+ const gchar *first_uid,
+ ...)
+{
+ GSList *uids = NULL;
+ gchar *uid;
+ va_list args;
+
+ g_assert (first_uid);
+
+ uids = g_slist_append (uids, (gpointer) first_uid);
+
+ va_start (args, first_uid);
+ uid = va_arg (args, gchar *);
+ while (uid) {
+ uids = g_slist_append (uids, uid);
+ uid = va_arg (args, gchar *);
+ }
+ va_end (args);
+
+ tcu_assert_contacts_order_slist (results, uids);
+ g_slist_free (uids);
+}
+
+void
+tcu_print_results (const GSList *results)
+{
+ const GSList *link;
+
+ if (g_getenv ("TEST_DEBUG") == NULL)
+ return;
+
+ g_print ("\nPRINTING RESULTS:\n");
+
+ for (link = results; link; link = link->next) {
+ EBookCacheSearchData *data = link->data;
+
+ g_print ("\n%s\n", data->vcard);
+ }
+
+ g_print ("\nRESULT LIST_FINISHED\n");
+}
+
+/********************************************
+ * Move By Test Helpers
+ ********************************************/
+#define DEBUG_FIXTURE 0
+
+static TCUStepData *
+step_test_new_internal (const gchar *test_path,
+ const gchar *locale,
+ gboolean empty_book)
+{
+ TCUStepData *data;
+
+ data = g_slice_new0 (TCUStepData);
+
+ data->parent.locale = g_strdup (locale);
+ data->parent.sort_type = E_BOOK_CURSOR_SORT_ASCENDING;
+
+ if (empty_book)
+ data->parent.parent.setup_summary = tcu_setup_empty_book;
+
+ data->path = g_strdup (test_path);
+
+ return data;
+}
+
+static void
+step_test_free (TCUStepData *data)
+{
+ GList *l;
+
+ g_free (data->path);
+ g_free ((gchar *) data->parent.locale);
+
+ for (l = data->assertions; l; l = l->next) {
+ TCUStepAssertion *assertion = l->data;
+
+ g_free (assertion->locale);
+ g_slice_free (TCUStepAssertion, assertion);
+ }
+
+ g_list_free (data->assertions);
+
+ g_slice_free (TCUStepData, data);
+}
+
+TCUStepData *
+tcu_step_test_new (const gchar *test_prefix,
+ const gchar *test_path,
+ const gchar *locale,
+ gboolean empty_book)
+{
+ TCUStepData *data;
+ gchar *path;
+
+ path = g_strconcat (test_prefix, test_path, NULL);
+ data = step_test_new_internal (path, locale, empty_book);
+ g_free (path);
+
+ return data;
+}
+
+TCUStepData *
+tcu_step_test_new_full (const gchar *test_prefix,
+ const gchar *test_path,
+ const gchar *locale,
+ gboolean empty_book,
+ EBookCursorSortType sort_type)
+{
+ TCUStepData *data;
+ gchar *path;
+
+ path = g_strconcat (test_prefix, test_path, NULL);
+ data = step_test_new_internal (path, locale, empty_book);
+ data->parent.sort_type = sort_type;
+ g_free (path);
+
+ return data;
+}
+
+static void
+test_cursor_move_teardown (TCUCursorFixture *fixture,
+ gconstpointer user_data)
+{
+ TCUStepData *data = (TCUStepData *) user_data;
+
+ tcu_cursor_fixture_teardown (fixture, user_data);
+ step_test_free (data);
+}
+
+static void
+assert_step (TCUCursorFixture *fixture,
+ TCUStepData *data,
+ TCUStepAssertion *assertion,
+ GSList *results,
+ gint n_results,
+ gboolean expect_results)
+{
+ GSList *uids = NULL;
+ gint ii, expected = 0;
+
+ /* Count the number of really expected results */
+ for (ii = 0; ii < ABS (assertion->count); ii++) {
+ gint index = assertion->expected[ii];
+
+ if (index < 0)
+ break;
+
+ expected++;
+ }
+
+ g_assert_cmpint (n_results, ==, expected);
+ if (!expect_results) {
+ g_assert_cmpint (g_slist_length (results), ==, 0);
+ return;
+ }
+
+ /* Assert the exact amount of requested results */
+ g_assert_cmpint (g_slist_length (results), ==, expected);
+
+#if DEBUG_FIXTURE
+ g_print (
+ "%s: Constructing expected result list for a fetch of %d: ",
+ data->path, assertion->count);
+#endif
+ for (ii = 0; ii < ABS (assertion->count); ii++) {
+ gint index = assertion->expected[ii];
+ gchar *uid;
+
+ if (index < 0)
+ break;
+
+ uid = (gchar *) e_contact_get_const (fixture->contacts[index], E_CONTACT_UID);
+ uids = g_slist_append (uids, uid);
+
+#if DEBUG_FIXTURE
+ g_print ("%s ", uid);
+#endif
+
+ }
+#if DEBUG_FIXTURE
+ g_print ("\n");
+#endif
+
+ tcu_assert_contacts_order_slist (results, uids);
+ g_slist_free (uids);
+}
+
+static void
+test_step (TCUCursorFixture *fixture,
+ gconstpointer user_data)
+{
+ TCUFixture *base_fixture = (TCUFixture *) fixture;
+ TCUStepData *data = (TCUStepData *) user_data;
+ GSList *results = NULL;
+ GError *error = NULL;
+ gint n_results;
+ EBookCacheCursorOrigin origin;
+ GList *l;
+ gboolean reset = TRUE;
+
+ for (l = data->assertions; l; l = l->next) {
+ TCUStepAssertion *assertion = l->data;
+
+ if (assertion->locale) {
+ gint n_locale_changes = base_fixture->n_locale_changes;
+
+ if (!e_book_cache_set_locale (base_fixture->book_cache, assertion->locale, NULL,
&error))
+ g_error ("Failed to set locale: %s", error->message);
+
+ n_locale_changes = (base_fixture->n_locale_changes - n_locale_changes);
+
+ /* Only check for contact changes is phone numbers are supported,
+ * contact changes only happen because of e164 number interpretations.
+ */
+ if (e_phone_number_is_supported () &&
+ assertion->count != n_locale_changes)
+ g_error ("Expected %d e164 numbers to change, %d actually changed.",
+ assertion->count, n_locale_changes);
+
+ reset = TRUE;
+ continue;
+ }
+
+ /* For the first call to e_book_cache_cursor_step(),
+ * or the first reset after locale change, set the origin accordingly.
+ */
+ if (reset) {
+ if (assertion->count < 0)
+ origin = E_BOOK_CACHE_CURSOR_ORIGIN_END;
+ else
+ origin = E_BOOK_CACHE_CURSOR_ORIGIN_BEGIN;
+
+ reset = FALSE;
+ } else {
+ origin = E_BOOK_CACHE_CURSOR_ORIGIN_CURRENT;
+ }
+
+ /* Try only fetching the contacts but not moving the cursor */
+ n_results = e_book_cache_cursor_step (
+ base_fixture->book_cache,
+ fixture->cursor,
+ E_BOOK_CACHE_CURSOR_STEP_FETCH,
+ origin,
+ assertion->count,
+ &results,
+ NULL, &error);
+ if (n_results < 0)
+ g_error ("Error fetching cursor results: %s", error->message);
+
+ tcu_print_results (results);
+ assert_step (fixture, data, assertion, results, n_results, TRUE);
+ g_slist_free_full (results, e_book_cache_search_data_free);
+ results = NULL;
+
+ /* Do it again, this time only moving the cursor */
+ n_results = e_book_cache_cursor_step (
+ base_fixture->book_cache,
+ fixture->cursor,
+ E_BOOK_CACHE_CURSOR_STEP_MOVE,
+ origin,
+ assertion->count,
+ &results,
+ NULL, &error);
+ if (n_results < 0)
+ g_error ("Error fetching cursor results: %s", error->message);
+
+ tcu_print_results (results);
+ assert_step (fixture, data, assertion, results, n_results, FALSE);
+ g_slist_free_full (results, e_book_cache_search_data_free);
+ results = NULL;
+ }
+}
+
+static void
+step_test_add_assertion_va_list (TCUStepData *data,
+ gint count,
+ va_list args)
+{
+ TCUStepAssertion *assertion = g_slice_new0 (TCUStepAssertion);
+ gint expected, ii = 0;
+
+ assertion->count = count;
+
+#if DEBUG_FIXTURE
+ g_print ("Adding assertion to test %d: %s\n", ii + 1, data->path);
+ g_print (" Test will move by %d and expect: ", count);
+#endif
+ for (ii = 0; ii < ABS (count); ii++) {
+ expected = va_arg (args, gint);
+
+#if DEBUG_FIXTURE
+ g_print ("%d ", expected);
+#endif
+ assertion->expected[ii] = expected - 1;
+ }
+#if DEBUG_FIXTURE
+ g_print ("\n");
+#endif
+
+ data->assertions = g_list_append (data->assertions, assertion);
+}
+
+/* A positive of negative 'count' value
+ * followed by ABS (count) UID indexes.
+ *
+ * The indexes start at 1 so that they
+ * are easier to match up with the chart
+ * in data-test-utils.h
+ */
+void
+tcu_step_test_add_assertion (TCUStepData *data,
+ gint count,
+ ...)
+{
+ va_list args;
+
+ va_start (args, count);
+ step_test_add_assertion_va_list (data, count, args);
+ va_end (args);
+}
+
+void
+tcu_step_test_change_locale (TCUStepData *data,
+ const gchar *locale,
+ gint expected_changes)
+{
+ TCUStepAssertion *assertion = g_slice_new0 (TCUStepAssertion);
+
+ assertion->locale = g_strdup (locale);
+ assertion->count = expected_changes;
+ data->assertions = g_list_append (data->assertions, assertion);
+}
+
+void
+tcu_step_test_add (TCUStepData *data,
+ gboolean filtered)
+{
+ data->filtered = filtered;
+
+ g_test_add (
+ data->path, TCUCursorFixture, data,
+ filtered ?
+ tcu_cursor_fixture_filtered_setup :
+ tcu_cursor_fixture_setup,
+ test_step,
+ test_cursor_move_teardown);
+}
diff --git a/tests/libedata-book/test-book-cache-utils.h b/tests/libedata-book/test-book-cache-utils.h
new file mode 100644
index 0000000..4718665
--- /dev/null
+++ b/tests/libedata-book/test-book-cache-utils.h
@@ -0,0 +1,178 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2013, Openismus GmbH
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Tristan Van Berkom <tristanvb openismus com>
+ */
+
+#ifndef TEST_BOOK_CACHE_UTILS_H
+#define TEST_BOOK_CACHE_UTILS_H
+
+#include <libedata-book/libedata-book.h>
+
+G_BEGIN_DECLS
+
+/* This legend shows the add order, and various sort order of the sorted
+ * vcards. The UIDs of these contacts are formed as 'sorted-1', 'sorted-2' etc
+ * and the numbering of the contacts is according to the 'N' column in the
+ * following legend.
+ *
+ * The Email column indicates whether the contact has a .com email address
+ * (in order to test filtered cursor results) and corresponds to the natural
+ * order in the 'N' column.
+ *
+ * +-----------------------------------------------------------------------------------------------+
+ * | N | Email | Last Name | en_US_POSIX | en_US / de_DE | fr_CA | de_DE |
+ * | | | | | | | (phonebook) |
+ * +-----------------------------------------------------------------------------------------------+
+ * | 1 | Yes | bad | 11 | 11 | 11 | 11 |
+ * | 2 | Yes | Bad | Bad 2 | bad 1 | bad 1 | bad 1 |
+ * | 3 | Yes | Bat | Bäd 6 | Bad 2 | Bad 2 | Bad 2 |
+ * | 4 | No | bat | Bat 3 | bäd 5 | bäd 5 | bäd 5 |
+ * | 5 | Yes | bäd | Bät 8 | Bäd 6 | Bäd 6 | Bäd 6 |
+ * | 6 | No | Bäd | bad 1 | bat 4 | bat 4 | bät 7 |
+ * | 7 | No | bät | bäd 5 | Bat 3 | Bat 3 | Bät 8 |
+ * | 8 | Yes | Bät | bat 4 | bät 7 | bät 7 | bat 4 |
+ * | 9 | Yes | côté | bät 7 | Bät 8 | Bät 8 | Bat 3 |
+ * | 10 | Yes | C | black-bird 15 | black-bird 15 | black-bird 15 | black-bird 15 |
+ * | 11 | Yes | | black-birds 17 | black-birds 17 | black-birds 17 | black-birds 17 |
+ * | 12 | Yes | coté | blackbird 16 | blackbird 16 | blackbird 16 | blackbird 16 |
+ * | 13 | No | côte | blackbirds 18 | blackbirds 18 | blackbirds 18 | blackbirds 18 |
+ * | 14 | Yes | cote | C 10 | C 10 | C 10 | C 10 |
+ * | 15 | No | black-bird | cote 14 | cote 14 | cote 14 | cote 14 |
+ * | 16 | Yes | blackbird | coté 12 | coté 12 | côte 13 | coté 12 |
+ * | 17 | Yes | black-birds | côte 13 | côte 13 | coté 12 | côte 13 |
+ * | 18 | Yes | blackbirds | côté 9 | côté 9 | côté 9 | côté 9 |
+ * | 19 | No | Muffler | Muffler 19 | Muffler 19 | Muffler 19 | Müller 20 |
+ * | 20 | No | Müller | Müller 20 | Müller 20 | Müller 20 | Muffler 19 |
+ * +-----------------------------------------------------------------------------------------------+
+ *
+ * See this ICU demo to check additional sort ordering by ICU in various locales:
+ * http://demo.icu-project.org/icu-bin/locexp?_=en_US&d_=en&x=col
+ */
+
+/* 13 contacts in the test data have an email address ending with ".com" */
+#define N_FILTERED_CONTACTS 13
+#define N_SORTED_CONTACTS 20
+
+typedef ESourceBackendSummarySetup * (* TCUSetupSummaryFunc) (void);
+
+typedef struct {
+ EBookCache *book_cache;
+
+ gint n_add_changes;
+ gint n_locale_changes;
+} TCUFixture;
+
+typedef struct {
+ TCUSetupSummaryFunc setup_summary;
+} TCUClosure;
+
+typedef struct {
+ TCUFixture parent_fixture;
+
+ EBookCacheCursor *cursor;
+ EContact *contacts[N_SORTED_CONTACTS];
+ EBookQuery *query;
+
+ guint own_id;
+} TCUCursorFixture;
+
+typedef struct {
+ TCUClosure parent;
+
+ const gchar *locale;
+ EBookCursorSortType sort_type;
+} TCUCursorClosure;
+
+typedef struct {
+ /* A locale change */
+ gchar *locale;
+
+ /* count argument for move */
+ gint count;
+
+ /* An array of 'ABS (counts[i])' expected contacts */
+ gint expected[N_SORTED_CONTACTS];
+} TCUStepAssertion;
+
+typedef struct {
+ TCUCursorClosure parent;
+ gchar *path;
+
+ GList *assertions;
+
+ /* Whether this is a filtered test */
+ gboolean filtered;
+} TCUStepData;
+
+/* Base fixture */
+void tcu_fixture_setup (TCUFixture *fixture,
+ gconstpointer user_data);
+void tcu_fixture_teardown (TCUFixture *fixture,
+ gconstpointer user_data);
+ESourceBackendSummarySetup *
+ tcu_setup_empty_book (void);
+
+/* Cursor fixture */
+void tcu_cursor_fixture_setup (TCUCursorFixture *fixture,
+ gconstpointer user_data);
+void tcu_cursor_fixture_teardown (TCUCursorFixture *fixture,
+ gconstpointer user_data);
+void tcu_cursor_fixture_set_locale (TCUCursorFixture *fixture,
+ const gchar *locale);
+
+/* Filters contacts with E_CONTACT_EMAIL ending with '.com' */
+void tcu_cursor_fixture_filtered_setup (TCUCursorFixture *fixture,
+ gconstpointer user_data);
+
+gchar * tcu_new_vcard_from_test_case (const gchar *case_name);
+EContact * tcu_new_contact_from_test_case (const gchar *case_name);
+
+void tcu_add_contact_from_test_case (TCUFixture *fixture,
+ const gchar *case_name,
+ EContact **ret_contact);
+void tcu_assert_contacts_order_slist (GSList *results,
+ GSList *uids);
+void tcu_assert_contacts_order (GSList *results,
+ const gchar *first_uid,
+ ...) G_GNUC_NULL_TERMINATED;
+
+void tcu_print_results (const GSList *results);
+
+/* Step test helpers */
+void tcu_step_test_add_assertion (TCUStepData *data,
+ gint count,
+ ...);
+void tcu_step_test_change_locale (TCUStepData *data,
+ const gchar *locale,
+ gint expected_changes);
+
+TCUStepData * tcu_step_test_new (const gchar *test_prefix,
+ const gchar *test_path,
+ const gchar *locale,
+ gboolean empty_book);
+TCUStepData * tcu_step_test_new_full (const gchar *test_prefix,
+ const gchar *test_path,
+ const gchar *locale,
+ gboolean empty_book,
+ EBookCursorSortType sort_type);
+
+void tcu_step_test_add (TCUStepData *data,
+ gboolean filtered);
+
+G_END_DECLS
+
+#endif /* TEST_BOOK_CACHE_UTILS_H */
diff --git a/tests/libedata-book/test-book-meta-backend.c b/tests/libedata-book/test-book-meta-backend.c
new file mode 100644
index 0000000..331032c
--- /dev/null
+++ b/tests/libedata-book/test-book-meta-backend.c
@@ -0,0 +1,1718 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "evolution-data-server-config.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <locale.h>
+
+#include "libebook-contacts/libebook-contacts.h"
+
+#include "e-test-server-utils.h"
+#include "test-book-cache-utils.h"
+
+#define REMOTE_URL "https://www.gnome.org/wp-content/themes/gnome-grass/images/gnome-logo.svg"
+#define MODIFIED_FN_STR "Modified FN"
+
+typedef struct _EBookMetaBackendTest {
+ EBookMetaBackend parent;
+
+ GHashTable *contacts;
+
+ gint sync_tag_index;
+ gboolean can_connect;
+ gboolean is_connected;
+ gint connect_count;
+ gint list_count;
+ gint save_count;
+ gint load_count;
+ gint remove_count;
+} EBookMetaBackendTest;
+
+typedef struct _EBookMetaBackendTestClass {
+ EBookMetaBackendClass parent_class;
+} EBookMetaBackendTestClass;
+
+#define E_TYPE_BOOK_META_BACKEND_TEST (e_book_meta_backend_test_get_type ())
+#define E_BOOK_META_BACKEND_TEST(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST \
+ ((obj), E_TYPE_BOOK_META_BACKEND_TEST, EBookMetaBackendTest))
+#define E_IS_BOOK_META_BACKEND_TEST(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE \
+ ((obj), E_TYPE_BOOK_META_BACKEND_TEST))
+
+GType e_book_meta_backend_test_get_type (void) G_GNUC_CONST;
+
+G_DEFINE_TYPE (EBookMetaBackendTest, e_book_meta_backend_test, E_TYPE_BOOK_META_BACKEND)
+
+static void
+ebmb_test_add_test_case (EBookMetaBackendTest *test_backend,
+ const gchar *case_name)
+{
+ EContact *contact;
+
+ g_assert_nonnull (test_backend);
+ g_assert_nonnull (case_name);
+
+ contact = tcu_new_contact_from_test_case (case_name);
+ g_assert_nonnull (contact);
+
+ g_hash_table_insert (test_backend->contacts, e_contact_get (contact, E_CONTACT_UID), contact);
+}
+
+static void
+ebmb_test_remove_component (EBookMetaBackendTest *test_backend,
+ const gchar *uid)
+{
+ g_assert_nonnull (test_backend);
+ g_assert_nonnull (uid);
+
+ g_hash_table_remove (test_backend->contacts, uid);
+}
+
+static GHashTable * /* gchar * ~> NULL */
+ebmb_test_gather_uids (va_list args)
+{
+ GHashTable *expects;
+ const gchar *uid;
+
+ expects = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+ uid = va_arg (args, const gchar *);
+ while (uid) {
+ g_hash_table_insert (expects, g_strdup (uid), NULL);
+ uid = va_arg (args, const gchar *);
+ }
+
+ return expects;
+}
+
+static void
+ebmb_test_hash_contains (GHashTable *contacts, /* gchar *uid ~> EContact * */
+ gboolean negate,
+ gboolean exact,
+ ...) /* uid-s, ended with NULL */
+{
+ va_list args;
+ GHashTable *expects;
+ GHashTableIter iter;
+ gpointer uid;
+ guint ntotal;
+
+ g_return_if_fail (contacts != NULL);
+
+ va_start (args, exact);
+ expects = ebmb_test_gather_uids (args);
+ va_end (args);
+
+ ntotal = g_hash_table_size (expects);
+
+ g_hash_table_iter_init (&iter, contacts);
+ while (g_hash_table_iter_next (&iter, &uid, NULL)) {
+ if (exact) {
+ if (negate)
+ g_assert (!g_hash_table_remove (expects, uid));
+ else
+ g_assert (g_hash_table_remove (expects, uid));
+ } else {
+ g_hash_table_remove (expects, uid);
+ }
+ }
+
+ if (negate)
+ g_assert_cmpint (g_hash_table_size (expects), ==, ntotal);
+ else
+ g_assert_cmpint (g_hash_table_size (expects), ==, 0);
+
+ g_hash_table_destroy (expects);
+}
+
+static void
+ebmb_test_cache_contains (EBookCache *book_cache,
+ gboolean negate,
+ gboolean exact,
+ ...) /* uid-s, ended with NULL */
+{
+ va_list args;
+ GHashTable *expects;
+ GHashTableIter iter;
+ ECache *cache;
+ gpointer key;
+ gint found = 0;
+
+ g_return_if_fail (E_IS_BOOK_CACHE (book_cache));
+
+ va_start (args, exact);
+ expects = ebmb_test_gather_uids (args);
+ va_end (args);
+
+ cache = E_CACHE (book_cache);
+
+ g_hash_table_iter_init (&iter, expects);
+ while (g_hash_table_iter_next (&iter, &key, NULL)) {
+ const gchar *uid = key;
+
+ g_assert_nonnull (uid);
+
+ if (e_cache_contains (cache, uid, E_CACHE_EXCLUDE_DELETED))
+ found++;
+ }
+
+ if (negate)
+ g_assert_cmpint (0, ==, found);
+ else
+ g_assert_cmpint (g_hash_table_size (expects), ==, found);
+
+ g_hash_table_destroy (expects);
+
+ if (exact && !negate)
+ g_assert_cmpint (e_cache_get_count (cache, E_CACHE_EXCLUDE_DELETED, NULL, NULL), ==, found);
+}
+
+static void
+ebmb_test_cache_and_server_equal (EBookCache *book_cache,
+ GHashTable *contacts,
+ ECacheDeletedFlag deleted_flag)
+{
+ ECache *cache;
+ GHashTableIter iter;
+ gpointer uid;
+
+ g_return_if_fail (E_IS_BOOK_CACHE (book_cache));
+ g_return_if_fail (contacts != NULL);
+
+ cache = E_CACHE (book_cache);
+
+ g_assert_cmpint (e_cache_get_count (cache, deleted_flag, NULL, NULL), ==,
+ g_hash_table_size (contacts));
+
+ g_hash_table_iter_init (&iter, contacts);
+ while (g_hash_table_iter_next (&iter, &uid, NULL)) {
+ g_assert (e_cache_contains (cache, uid, deleted_flag));
+ }
+}
+
+static gchar *
+e_book_meta_backend_test_get_backend_property (EBookBackend *book_backend,
+ const gchar *prop_name)
+{
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND_TEST (book_backend), NULL);
+ g_return_val_if_fail (prop_name != NULL, NULL);
+
+ if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
+ return g_strjoin (",",
+ e_book_meta_backend_get_capabilities (E_BOOK_META_BACKEND (book_backend)),
+ "local",
+ "contact-lists",
+ NULL);
+ }
+
+ /* Chain up to parent's method. */
+ return E_BOOK_BACKEND_CLASS (e_book_meta_backend_test_parent_class)->get_backend_property
(book_backend, prop_name);
+}
+
+static gboolean
+e_book_meta_backend_test_connect_sync (EBookMetaBackend *meta_backend,
+ const ENamedParameters *credentials,
+ ESourceAuthenticationResult *out_auth_result,
+ gchar **out_certificate_pem,
+ GTlsCertificateFlags *out_certificate_errors,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackendTest *test_backend;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND_TEST (meta_backend), FALSE);
+
+ test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+
+ if (test_backend->is_connected)
+ return TRUE;
+
+ test_backend->connect_count++;
+
+ if (test_backend->can_connect) {
+ test_backend->is_connected = TRUE;
+ return TRUE;
+ }
+
+ g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_REPOSITORY_OFFLINE,
+ e_client_error_to_string (E_CLIENT_ERROR_REPOSITORY_OFFLINE));
+
+ return FALSE;
+}
+
+static gboolean
+e_book_meta_backend_test_disconnect_sync (EBookMetaBackend *meta_backend,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackendTest *test_backend;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND_TEST (meta_backend), FALSE);
+
+ test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+ test_backend->is_connected = FALSE;
+
+ return TRUE;
+}
+
+static gboolean
+e_book_meta_backend_test_get_changes_sync (EBookMetaBackend *meta_backend,
+ const gchar *last_sync_tag,
+ gboolean is_repeat,
+ gchar **out_new_sync_tag,
+ gboolean *out_repeat,
+ GSList **out_created_objects,
+ GSList **out_modified_objects,
+ GSList **out_removed_objects,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackendTest *test_backend;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND_TEST (meta_backend), FALSE);
+ g_return_val_if_fail (out_new_sync_tag != NULL, FALSE);
+ g_return_val_if_fail (out_repeat != NULL, FALSE);
+
+ test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+
+ if (!test_backend->sync_tag_index) {
+ g_assert_null (last_sync_tag);
+ } else {
+ g_assert_nonnull (last_sync_tag);
+ g_assert_cmpint (atoi (last_sync_tag), ==, test_backend->sync_tag_index);
+
+ test_backend->sync_tag_index++;
+ *out_new_sync_tag = g_strdup_printf ("%d", test_backend->sync_tag_index);
+
+ if (test_backend->sync_tag_index == 2)
+ *out_repeat = TRUE;
+ else if (test_backend->sync_tag_index == 3)
+ return TRUE;
+ }
+
+ /* Nothing to do here at the moment, left the work to the parent class,
+ which calls list_existing_sync() internally. */
+ return E_BOOK_META_BACKEND_CLASS (e_book_meta_backend_test_parent_class)->get_changes_sync
(meta_backend,
+ last_sync_tag, is_repeat, out_new_sync_tag, out_repeat, out_created_objects,
+ out_modified_objects, out_removed_objects, cancellable, error);
+}
+
+static gboolean
+e_book_meta_backend_test_list_existing_sync (EBookMetaBackend *meta_backend,
+ gchar **out_new_sync_tag,
+ GSList **out_existing_objects,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackendTest *test_backend;
+ GHashTableIter iter;
+ gpointer key, value;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND_TEST (meta_backend), FALSE);
+ g_return_val_if_fail (out_new_sync_tag, FALSE);
+ g_return_val_if_fail (out_existing_objects, FALSE);
+
+ test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+ test_backend->list_count++;
+
+ g_assert (test_backend->is_connected);
+
+ *out_existing_objects = NULL;
+
+ g_hash_table_iter_init (&iter, test_backend->contacts);
+ while (g_hash_table_iter_next (&iter, &key, &value)) {
+ const gchar *uid;
+ gchar *revision;
+ EBookMetaBackendInfo *nfo;
+
+ uid = key;
+ revision = e_contact_get (value, E_CONTACT_REV);
+
+ nfo = e_book_meta_backend_info_new (uid, revision, NULL, NULL);
+ *out_existing_objects = g_slist_prepend (*out_existing_objects, nfo);
+
+ g_free (revision);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+e_book_meta_backend_test_save_contact_sync (EBookMetaBackend *meta_backend,
+ gboolean overwrite_existing,
+ EConflictResolution conflict_resolution,
+ EContact *contact,
+ const gchar *extra,
+ gchar **out_new_uid,
+ gchar **out_new_extra,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackendTest *test_backend;
+ const gchar *uid;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND_TEST (meta_backend), FALSE);
+ g_return_val_if_fail (E_IS_CONTACT (contact), FALSE);
+ g_return_val_if_fail (out_new_uid != NULL, FALSE);
+
+ test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+ test_backend->save_count++;
+
+ g_assert (test_backend->is_connected);
+
+ uid = e_contact_get_const (contact, E_CONTACT_UID);
+ g_assert_nonnull (uid);
+
+ if (g_hash_table_contains (test_backend->contacts, uid)) {
+ if (!overwrite_existing) {
+ g_propagate_error (error, e_data_book_create_error
(E_DATA_BOOK_STATUS_CONTACTID_ALREADY_EXISTS, NULL));
+ return FALSE;
+ }
+
+ g_hash_table_remove (test_backend->contacts, uid);
+ }
+
+ /* Intentionally do not add a referenced 'contact', thus any later changes
+ on it are not "propagated" into the test_backend's content. */
+ g_hash_table_insert (test_backend->contacts, g_strdup (uid), e_contact_duplicate (contact));
+
+ *out_new_uid = g_strdup (uid);
+
+ return TRUE;
+}
+
+static gboolean
+e_book_meta_backend_test_load_contact_sync (EBookMetaBackend *meta_backend,
+ const gchar *uid,
+ const gchar *extra,
+ EContact **out_contact,
+ gchar **out_extra,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackendTest *test_backend;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND_TEST (meta_backend), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (out_contact != NULL, FALSE);
+ g_return_val_if_fail (out_extra != NULL, FALSE);
+
+ test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+ test_backend->load_count++;
+
+ g_assert (test_backend->is_connected);
+
+ *out_contact = g_hash_table_lookup (test_backend->contacts, uid);
+
+ if (*out_contact) {
+ *out_contact = e_contact_duplicate (*out_contact);
+ *out_extra = g_strconcat ("extra for ", uid, NULL);
+ return TRUE;
+ } else {
+ g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_CONTACT_NOT_FOUND,
NULL));
+ }
+
+ return FALSE;
+}
+
+static gboolean
+e_book_meta_backend_test_remove_contact_sync (EBookMetaBackend *meta_backend,
+ EConflictResolution conflict_resolution,
+ const gchar *uid,
+ const gchar *extra,
+ const gchar *object,
+ GCancellable *cancellable,
+ GError **error)
+{
+ EBookMetaBackendTest *test_backend;
+ gboolean success = FALSE;
+
+ g_return_val_if_fail (E_IS_BOOK_META_BACKEND_TEST (meta_backend), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (extra != NULL, FALSE);
+
+ test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+ test_backend->remove_count++;
+
+ g_assert (test_backend->is_connected);
+
+ success = g_hash_table_remove (test_backend->contacts, uid);
+ if (success) {
+ gchar *expected_extra;
+
+ expected_extra = g_strconcat ("extra for ", uid, NULL);
+ g_assert_cmpstr (expected_extra, ==, extra);
+ g_free (expected_extra);
+ } else {
+ g_propagate_error (error, e_data_book_create_error (E_DATA_BOOK_STATUS_CONTACT_NOT_FOUND,
NULL));
+ }
+
+ return success;
+}
+
+static void
+e_book_meta_backend_test_reset_counters (EBookMetaBackendTest *test_backend)
+{
+ g_return_if_fail (E_IS_BOOK_META_BACKEND_TEST (test_backend));
+
+ test_backend->connect_count = 0;
+ test_backend->list_count = 0;
+ test_backend->save_count = 0;
+ test_backend->load_count = 0;
+ test_backend->remove_count = 0;
+}
+
+static EBookCache *glob_use_cache = NULL;
+
+static void
+e_book_meta_backend_test_constructed (GObject *object)
+{
+ EBookMetaBackendTest *test_backend = E_BOOK_META_BACKEND_TEST (object);
+
+ g_assert_nonnull (glob_use_cache);
+
+ /* Set it before EBookMetaBackend::constucted() creates its own cache */
+ e_book_meta_backend_set_cache (E_BOOK_META_BACKEND (test_backend), glob_use_cache);
+
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_book_meta_backend_test_parent_class)->constructed (object);
+}
+
+static void
+e_book_meta_backend_test_finalize (GObject *object)
+{
+ EBookMetaBackendTest *test_backend = E_BOOK_META_BACKEND_TEST (object);
+
+ g_assert_nonnull (test_backend->contacts);
+
+ g_hash_table_destroy (test_backend->contacts);
+
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_book_meta_backend_test_parent_class)->finalize (object);
+}
+
+static void
+e_book_meta_backend_test_class_init (EBookMetaBackendTestClass *klass)
+{
+ EBookMetaBackendClass *book_meta_backend_class;
+ EBookBackendClass *book_backend_class;
+ GObjectClass *object_class;
+
+ book_meta_backend_class = E_BOOK_META_BACKEND_CLASS (klass);
+ book_meta_backend_class->connect_sync = e_book_meta_backend_test_connect_sync;
+ book_meta_backend_class->disconnect_sync = e_book_meta_backend_test_disconnect_sync;
+ book_meta_backend_class->get_changes_sync = e_book_meta_backend_test_get_changes_sync;
+ book_meta_backend_class->list_existing_sync = e_book_meta_backend_test_list_existing_sync;
+ book_meta_backend_class->save_contact_sync = e_book_meta_backend_test_save_contact_sync;
+ book_meta_backend_class->load_contact_sync = e_book_meta_backend_test_load_contact_sync;
+ book_meta_backend_class->remove_contact_sync = e_book_meta_backend_test_remove_contact_sync;
+
+ book_backend_class = E_BOOK_BACKEND_CLASS (klass);
+ book_backend_class->get_backend_property = e_book_meta_backend_test_get_backend_property;
+
+ object_class = G_OBJECT_CLASS (klass);
+ object_class->constructed = e_book_meta_backend_test_constructed;
+ object_class->finalize = e_book_meta_backend_test_finalize;
+}
+
+static void
+e_book_meta_backend_test_init (EBookMetaBackendTest *test_backend)
+{
+ test_backend->sync_tag_index = 0;
+ test_backend->is_connected = FALSE;
+ test_backend->can_connect = TRUE;
+ test_backend->contacts = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
+
+ ebmb_test_add_test_case (test_backend, "custom-1");
+ ebmb_test_add_test_case (test_backend, "custom-2");
+ ebmb_test_add_test_case (test_backend, "custom-3");
+ ebmb_test_add_test_case (test_backend, "custom-5");
+ ebmb_test_add_test_case (test_backend, "custom-6");
+
+ e_book_meta_backend_test_reset_counters (test_backend);
+
+ e_backend_set_online (E_BACKEND (test_backend), TRUE);
+ e_book_backend_set_writable (E_BOOK_BACKEND (test_backend), TRUE);
+}
+
+static ESourceRegistry *glob_registry = NULL;
+
+static EBookMetaBackend *
+e_book_meta_backend_test_new (EBookCache *cache)
+{
+ EBookMetaBackend *meta_backend;
+ GHashTableIter iter;
+ ESource *scratch;
+ gpointer contact;
+ gboolean success;
+ GError *error = NULL;
+
+ g_assert (E_IS_BOOK_CACHE (cache));
+
+ g_assert_nonnull (glob_registry);
+ g_assert_null (glob_use_cache);
+
+ glob_use_cache = cache;
+
+ scratch = e_source_new_with_uid ("test-source", NULL, &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (scratch);
+
+ meta_backend = g_object_new (E_TYPE_BOOK_META_BACKEND_TEST,
+ "source", scratch,
+ "registry", glob_registry,
+ NULL);
+ g_assert_nonnull (meta_backend);
+
+ g_assert (glob_use_cache == cache);
+ glob_use_cache = NULL;
+
+ g_object_unref (scratch);
+
+ g_hash_table_iter_init (&iter, E_BOOK_META_BACKEND_TEST (meta_backend)->contacts);
+ while (g_hash_table_iter_next (&iter, NULL, &contact)) {
+ gchar *extra;
+
+ extra = g_strconcat ("extra for ", e_contact_get_const (contact, E_CONTACT_UID), NULL);
+ success = e_book_cache_put_contact (cache, contact, extra, E_CACHE_IS_ONLINE, NULL, &error);
+ g_free (extra);
+
+ g_assert_no_error (error);
+ g_assert (success);
+ }
+
+ return meta_backend;
+}
+
+static void
+e_book_meta_backend_test_change_online (EBookMetaBackend *meta_backend,
+ gboolean is_online)
+{
+ EFlag *flag;
+ gulong handler_id;
+
+ if (!is_online) {
+ e_backend_set_online (E_BACKEND (meta_backend), FALSE);
+ return;
+ }
+
+ if (e_backend_get_online (E_BACKEND (meta_backend)))
+ return;
+
+ flag = e_flag_new ();
+
+ handler_id = g_signal_connect_swapped (meta_backend, "refresh-completed",
+ G_CALLBACK (e_flag_set), flag);
+
+ /* Going online triggers refresh, thus wait for it */
+ e_backend_set_online (E_BACKEND (meta_backend), TRUE);
+
+ e_flag_wait (flag);
+ e_flag_free (flag);
+
+ g_signal_handler_disconnect (meta_backend, handler_id);
+}
+
+static void
+e_book_meta_backend_test_call_refresh (EBookMetaBackend *meta_backend)
+{
+ EFlag *flag;
+ gulong handler_id;
+ gboolean success;
+ GError *error = NULL;
+
+ if (!e_backend_get_online (E_BACKEND (meta_backend)))
+ return;
+
+ flag = e_flag_new ();
+
+ handler_id = g_signal_connect_swapped (meta_backend, "refresh-completed",
+ G_CALLBACK (e_flag_set), flag);
+
+ success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->refresh_sync (E_BOOK_BACKEND (meta_backend), NULL,
&error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ e_flag_wait (flag);
+ e_flag_free (flag);
+
+ g_signal_handler_disconnect (meta_backend, handler_id);
+}
+
+static void
+test_one_photo (EBookMetaBackend *meta_backend,
+ const gchar *test_case,
+ EContactField field)
+{
+ EContact *contact;
+ EContactPhoto *photo;
+ guchar *orig_content;
+ gchar *new_content = NULL;
+ gsize orig_len = 0, new_len = 0;
+ gchar *filename;
+ gboolean success;
+ GError *error = NULL;
+
+ g_assert (E_IS_BOOK_META_BACKEND (meta_backend));
+ g_assert_nonnull (test_case);
+ g_assert (field == E_CONTACT_PHOTO || field == E_CONTACT_LOGO);
+
+ contact = tcu_new_contact_from_test_case (test_case);
+ g_assert_nonnull (contact);
+
+ photo = e_contact_get (contact, field);
+ g_assert_nonnull (photo);
+ g_assert_cmpint (photo->type, ==, E_CONTACT_PHOTO_TYPE_INLINED);
+
+ orig_content = (guchar *) e_contact_photo_get_inlined (photo, &orig_len);
+ g_assert_nonnull (orig_content);
+ g_assert_cmpint (orig_len, >, 0);
+
+ orig_content = g_memdup (orig_content, (guint) orig_len);
+
+ e_contact_photo_free (photo);
+
+ success = e_book_meta_backend_store_inline_photos_sync (meta_backend, contact, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ photo = e_contact_get (contact, field);
+ g_assert_nonnull (photo);
+ g_assert_cmpint (photo->type, ==, E_CONTACT_PHOTO_TYPE_URI);
+ g_assert_nonnull (e_contact_photo_get_uri (photo));
+
+ filename = g_filename_from_uri (e_contact_photo_get_uri (photo), NULL, &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (filename);
+
+ success = g_file_get_contents (filename, &new_content, &new_len, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_nonnull (new_content);
+ g_assert_cmpmem (orig_content, orig_len, new_content, new_len);
+
+ g_free (new_content);
+ g_free (filename);
+
+ e_contact_photo_free (photo);
+
+ success = e_book_meta_backend_inline_local_photos_sync (meta_backend, contact, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ photo = e_contact_get (contact, field);
+ g_assert_nonnull (photo);
+ g_assert_cmpint (photo->type, ==, E_CONTACT_PHOTO_TYPE_INLINED);
+
+ new_content = (gchar *) e_contact_photo_get_inlined (photo, &new_len);
+ g_assert_nonnull (new_content);
+ g_assert_cmpmem (orig_content, orig_len, new_content, new_len);
+
+ e_contact_photo_free (photo);
+ g_free (orig_content);
+
+ /* Also try with remote URI, which should be left as is */
+ photo = e_contact_photo_new ();
+ g_assert_nonnull (photo);
+
+ photo->type = E_CONTACT_PHOTO_TYPE_URI;
+ e_contact_photo_set_uri (photo, REMOTE_URL);
+ e_contact_set (contact, field, photo);
+ e_contact_photo_free (photo);
+
+ photo = e_contact_get (contact, field);
+ g_assert_nonnull (photo);
+ g_assert_cmpint (photo->type, ==, E_CONTACT_PHOTO_TYPE_URI);
+ g_assert_cmpstr (e_contact_photo_get_uri (photo), ==, REMOTE_URL);
+ e_contact_photo_free (photo);
+
+ success = e_book_meta_backend_store_inline_photos_sync (meta_backend, contact, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ photo = e_contact_get (contact, field);
+ g_assert_nonnull (photo);
+ g_assert_cmpint (photo->type, ==, E_CONTACT_PHOTO_TYPE_URI);
+ g_assert_cmpstr (e_contact_photo_get_uri (photo), ==, REMOTE_URL);
+ e_contact_photo_free (photo);
+
+ success = e_book_meta_backend_inline_local_photos_sync (meta_backend, contact, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ photo = e_contact_get (contact, field);
+ g_assert_nonnull (photo);
+ g_assert_cmpint (photo->type, ==, E_CONTACT_PHOTO_TYPE_URI);
+ g_assert_cmpstr (e_contact_photo_get_uri (photo), ==, REMOTE_URL);
+ e_contact_photo_free (photo);
+
+ g_object_unref (contact);
+}
+
+static void
+test_photos (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ EBookMetaBackend *meta_backend;
+
+ meta_backend = e_book_meta_backend_test_new (fixture->book_cache);
+ g_assert_nonnull (meta_backend);
+
+ test_one_photo (meta_backend, "photo-1", E_CONTACT_PHOTO);
+ test_one_photo (meta_backend, "logo-1", E_CONTACT_LOGO);
+
+ g_object_unref (meta_backend);
+}
+
+static void
+test_empty_cache (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ EBookMetaBackend *meta_backend;
+ EBookMetaBackendTest *test_backend;
+ GSList *uids;
+ gboolean success;
+ GError *error = NULL;
+
+ meta_backend = e_book_meta_backend_test_new (fixture->book_cache);
+ g_assert_nonnull (meta_backend);
+
+ test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+ g_assert_nonnull (test_backend);
+
+ uids = NULL;
+ success = e_book_cache_search_uids (fixture->book_cache, NULL, &uids, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpint (g_slist_length (uids), >, 0);
+ g_slist_free_full (uids, g_free);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_INCLUDE_DELETED, NULL,
&error), >, 0);
+ g_assert_no_error (error);
+
+ /* Empty the cache */
+ success = e_book_meta_backend_empty_cache_sync (meta_backend, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ /* Verify the cache is truly empty */
+ uids = NULL;
+ success = e_book_cache_search_uids (fixture->book_cache, NULL, &uids, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpint (g_slist_length (uids), ==, 0);
+ g_slist_free_full (uids, g_free);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->book_cache), E_CACHE_INCLUDE_DELETED, NULL,
&error), ==, 0);
+ g_assert_no_error (error);
+
+ g_object_unref (meta_backend);
+}
+
+static void
+test_create_contacts (EBookMetaBackend *meta_backend)
+{
+ EBookMetaBackendTest *test_backend;
+ EBookCache *book_cache;
+ GSList *offline_changes;
+ gchar *vcards[2] = { NULL, NULL }, *tmp;
+ GQueue new_contacts = G_QUEUE_INIT;
+ gboolean success;
+ GError *error = NULL;
+
+ g_assert_nonnull (meta_backend);
+
+ test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
+ g_assert_nonnull (book_cache);
+
+ ebmb_test_cache_and_server_equal (book_cache, test_backend->contacts, E_CACHE_INCLUDE_DELETED);
+
+ /* Try to add existing contact, it should fail */
+ vcards[0] = tcu_new_vcard_from_test_case ("custom-1");
+
+ success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->create_contacts_sync (E_BOOK_BACKEND
(meta_backend),
+ (const gchar * const *) vcards, &new_contacts, NULL, &error);
+ g_assert_error (error, E_DATA_BOOK_ERROR, E_DATA_BOOK_STATUS_CONTACTID_ALREADY_EXISTS);
+ g_assert (!success);
+ g_assert_cmpint (g_queue_get_length (&new_contacts), ==, 0);
+ g_clear_error (&error);
+ g_free (vcards[0]);
+
+ e_book_meta_backend_test_reset_counters (test_backend);
+
+ /* Try to add new contact */
+ vcards[0] = tcu_new_vcard_from_test_case ("custom-7");
+
+ success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->create_contacts_sync (E_BOOK_BACKEND
(meta_backend),
+ (const gchar * const *) vcards, &new_contacts, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpint (g_queue_get_length (&new_contacts), ==, 1);
+ g_assert_cmpstr (e_contact_get_const (g_queue_peek_head (&new_contacts), E_CONTACT_UID), ==,
"custom-7");
+ g_assert_cmpint (test_backend->connect_count, ==, 1);
+ g_assert_cmpint (test_backend->list_count, ==, 0);
+ g_assert_cmpint (test_backend->load_count, ==, 1);
+ g_assert_cmpint (test_backend->save_count, ==, 1);
+
+ g_queue_foreach (&new_contacts, (GFunc) g_object_unref, NULL);
+ g_queue_clear (&new_contacts);
+ g_free (vcards[0]);
+
+ ebmb_test_cache_and_server_equal (book_cache, test_backend->contacts, E_CACHE_INCLUDE_DELETED);
+
+ /* Going offline */
+ e_book_meta_backend_test_change_online (meta_backend, FALSE);
+
+ e_book_meta_backend_test_reset_counters (test_backend);
+
+ /* Try to add existing contact, it should fail */
+ vcards[0] = tcu_new_vcard_from_test_case ("custom-7");
+
+ success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->create_contacts_sync (E_BOOK_BACKEND
(meta_backend),
+ (const gchar * const *) vcards, &new_contacts, NULL, &error);
+ g_assert_error (error, E_DATA_BOOK_ERROR, E_DATA_BOOK_STATUS_CONTACTID_ALREADY_EXISTS);
+ g_assert (!success);
+ g_assert_cmpint (g_queue_get_length (&new_contacts), ==, 0);
+ g_clear_error (&error);
+ g_free (vcards[0]);
+ g_assert_cmpint (test_backend->load_count, ==, 0);
+ g_assert_cmpint (test_backend->save_count, ==, 0);
+
+ /* Try to add new contact */
+ vcards[0] = tcu_new_vcard_from_test_case ("custom-8");
+
+ success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->create_contacts_sync (E_BOOK_BACKEND
(meta_backend),
+ (const gchar * const *) vcards, &new_contacts, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpint (g_queue_get_length (&new_contacts), ==, 1);
+ g_assert_cmpstr (e_contact_get_const (g_queue_peek_head (&new_contacts), E_CONTACT_UID), ==,
"custom-8");
+ g_assert_cmpint (test_backend->connect_count, ==, 0);
+ g_assert_cmpint (test_backend->load_count, ==, 0);
+ g_assert_cmpint (test_backend->save_count, ==, 0);
+
+ g_queue_foreach (&new_contacts, (GFunc) g_object_unref, NULL);
+ g_queue_clear (&new_contacts);
+ g_free (vcards[0]);
+
+ ebmb_test_hash_contains (test_backend->contacts, TRUE, FALSE,
+ "custom-8", NULL, NULL);
+ ebmb_test_cache_contains (book_cache, FALSE, FALSE,
+ "custom-8", NULL, NULL);
+
+ /* Going online */
+ e_book_meta_backend_test_change_online (meta_backend, TRUE);
+
+ g_assert_cmpint (test_backend->connect_count, ==, 1);
+ g_assert_cmpint (test_backend->load_count, ==, 1);
+ g_assert_cmpint (test_backend->save_count, ==, 1);
+
+ ebmb_test_cache_and_server_equal (book_cache, test_backend->contacts, E_CACHE_INCLUDE_DELETED);
+
+ offline_changes = e_cache_get_offline_changes (E_CACHE (book_cache), NULL, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (0, ==, g_slist_length (offline_changes));
+
+ /* Add contact without UID */
+ vcards[0] = tcu_new_vcard_from_test_case ("custom-9");
+ g_assert_nonnull (vcards[0]);
+ tmp = strstr (vcards[0], "UID:custom-9\r\n");
+ g_assert_nonnull (tmp);
+ strncpy (tmp, "X-TEST:*007*", 12);
+
+ success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->create_contacts_sync (E_BOOK_BACKEND
(meta_backend),
+ (const gchar * const *) vcards, &new_contacts, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpint (g_queue_get_length (&new_contacts), ==, 1);
+ g_assert_cmpstr (e_contact_get_const (g_queue_peek_head (&new_contacts), E_CONTACT_UID), !=,
"custom-9");
+ g_assert_cmpint (test_backend->connect_count, ==, 1);
+ g_assert_cmpint (test_backend->list_count, ==, 1);
+ g_assert_cmpint (test_backend->load_count, ==, 2);
+ g_assert_cmpint (test_backend->save_count, ==, 2);
+
+ tmp = e_vcard_to_string (E_VCARD (g_queue_peek_head (&new_contacts)), EVC_FORMAT_VCARD_30);
+ g_assert_nonnull (tmp);
+ g_assert_nonnull (strstr (tmp, "X-TEST:*007*\r\n"));
+ g_assert_nonnull (strstr (tmp, e_contact_get_const (g_queue_peek_head (&new_contacts),
E_CONTACT_UID)));
+ g_free (tmp);
+
+ g_queue_foreach (&new_contacts, (GFunc) g_object_unref, NULL);
+ g_queue_clear (&new_contacts);
+ g_free (vcards[0]);
+
+ ebmb_test_cache_and_server_equal (book_cache, test_backend->contacts, E_CACHE_INCLUDE_DELETED);
+
+ g_object_unref (book_cache);
+}
+
+static gchar *
+ebmb_test_modify_case (const gchar *case_name)
+{
+ gchar *vcard, *tmp;
+ const gchar *rev;
+ EContact *contact;
+
+ g_assert_nonnull (case_name);
+
+ contact = tcu_new_contact_from_test_case (case_name);
+ g_assert_nonnull (contact);
+
+ e_contact_set (contact, E_CONTACT_FULL_NAME, MODIFIED_FN_STR);
+
+ rev = e_contact_get_const (contact, E_CONTACT_REV);
+ if (!rev)
+ tmp = g_strdup ("0");
+ else
+ tmp = g_strdup_printf ("%d", atoi (rev) + 1);
+ e_contact_set (contact, E_CONTACT_REV, tmp);
+ g_free (tmp);
+
+ vcard = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
+ g_object_unref (contact);
+
+ return vcard;
+}
+
+static void
+test_modify_contacts (EBookMetaBackend *meta_backend)
+{
+ EBookMetaBackendTest *test_backend;
+ EBookCache *book_cache;
+ EContact *contact;
+ GSList *offline_changes;
+ gchar *vcards[2] = { NULL, NULL }, *tmp;
+ GQueue new_contacts = G_QUEUE_INIT;
+ gint old_rev, new_rev;
+ gboolean success;
+ GError *error = NULL;
+
+ g_assert_nonnull (meta_backend);
+
+ test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
+ g_assert_nonnull (book_cache);
+
+ /* Modify non-existing contact */
+ vcards[0] = tcu_new_vcard_from_test_case ("custom-1");
+ g_assert_nonnull (vcards[0]);
+ tmp = strstr (vcards[0], "UID:custom-1");
+ g_assert_nonnull (tmp);
+ strncpy (tmp + 4, "unknown", 7);
+
+ success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->modify_contacts_sync (E_BOOK_BACKEND
(meta_backend),
+ (const gchar * const *) vcards, &new_contacts, NULL, &error);
+ g_assert_error (error, E_DATA_BOOK_ERROR, E_DATA_BOOK_STATUS_CONTACT_NOT_FOUND);
+ g_assert (!success);
+ g_assert_cmpint (g_queue_get_length (&new_contacts), ==, 0);
+ g_clear_error (&error);
+ g_free (vcards[0]);
+
+ /* Modify existing contact */
+ vcards[0] = ebmb_test_modify_case ("custom-1");
+
+ success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->modify_contacts_sync (E_BOOK_BACKEND
(meta_backend),
+ (const gchar * const *) vcards, &new_contacts, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpint (g_queue_get_length (&new_contacts), ==, 1);
+ g_assert_cmpint (test_backend->load_count, ==, 1);
+ g_assert_cmpint (test_backend->save_count, ==, 1);
+
+ contact = tcu_new_contact_from_test_case ("custom-1");
+ g_assert_nonnull (contact);
+ g_assert_nonnull (e_contact_get_const (contact, E_CONTACT_REV));
+ g_assert_nonnull (e_contact_get_const (contact, E_CONTACT_FULL_NAME));
+
+ old_rev = atoi (e_contact_get_const (contact, E_CONTACT_REV));
+ g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_FULL_NAME), !=, MODIFIED_FN_STR);
+ g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_UID), ==, "custom-1");
+
+ g_object_unref (contact);
+
+ contact = g_queue_peek_head (&new_contacts);
+ g_assert_nonnull (contact);
+ g_assert_nonnull (e_contact_get_const (contact, E_CONTACT_REV));
+ g_assert_nonnull (e_contact_get_const (contact, E_CONTACT_FULL_NAME));
+
+ new_rev = atoi (e_contact_get_const (contact, E_CONTACT_REV));
+ g_assert_cmpint (old_rev + 1, ==, new_rev);
+ g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_FULL_NAME), ==, MODIFIED_FN_STR);
+ g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_UID), ==, "custom-1");
+
+ g_queue_foreach (&new_contacts, (GFunc) g_object_unref, NULL);
+ g_queue_clear (&new_contacts);
+ g_free (vcards[0]);
+
+ /* Going offline */
+ e_book_meta_backend_test_change_online (meta_backend, FALSE);
+
+ e_book_meta_backend_test_reset_counters (test_backend);
+
+ /* Modify custom-2 */
+ vcards[0] = ebmb_test_modify_case ("custom-2");
+
+ success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->modify_contacts_sync (E_BOOK_BACKEND
(meta_backend),
+ (const gchar * const *) vcards, &new_contacts, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpint (g_queue_get_length (&new_contacts), ==, 1);
+ g_assert_cmpint (test_backend->load_count, ==, 0);
+ g_assert_cmpint (test_backend->save_count, ==, 0);
+
+ contact = tcu_new_contact_from_test_case ("custom-2");
+ g_assert_nonnull (contact);
+ g_assert_nonnull (e_contact_get_const (contact, E_CONTACT_REV));
+ g_assert_nonnull (e_contact_get_const (contact, E_CONTACT_FULL_NAME));
+
+ old_rev = atoi (e_contact_get_const (contact, E_CONTACT_REV));
+ g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_FULL_NAME), !=, MODIFIED_FN_STR);
+ g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_UID), ==, "custom-2");
+
+ g_object_unref (contact);
+
+ contact = g_queue_peek_head (&new_contacts);
+ g_assert_nonnull (contact);
+ g_assert_nonnull (e_contact_get_const (contact, E_CONTACT_REV));
+ g_assert_nonnull (e_contact_get_const (contact, E_CONTACT_FULL_NAME));
+
+ new_rev = atoi (e_contact_get_const (contact, E_CONTACT_REV));
+ g_assert_cmpint (old_rev + 1, ==, new_rev);
+ g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_FULL_NAME), ==, MODIFIED_FN_STR);
+ g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_UID), ==, "custom-2");
+
+ g_queue_foreach (&new_contacts, (GFunc) g_object_unref, NULL);
+ g_queue_clear (&new_contacts);
+ g_free (vcards[0]);
+
+ /* Going online */
+ e_book_meta_backend_test_change_online (meta_backend, TRUE);
+
+ g_assert_cmpint (test_backend->load_count, ==, 1);
+ g_assert_cmpint (test_backend->save_count, ==, 1);
+
+ ebmb_test_cache_and_server_equal (book_cache, test_backend->contacts, E_CACHE_INCLUDE_DELETED);
+
+ offline_changes = e_cache_get_offline_changes (E_CACHE (book_cache), NULL, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (0, ==, g_slist_length (offline_changes));
+
+ g_object_unref (book_cache);
+}
+
+static void
+test_remove_contacts (EBookMetaBackend *meta_backend)
+{
+ EBookMetaBackendTest *test_backend;
+ EBookCache *book_cache;
+ const gchar *uids[2] = { NULL, NULL };
+ GSList *offline_changes;
+ gboolean success;
+ GError *error = NULL;
+
+ g_assert_nonnull (meta_backend);
+
+ test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
+ g_assert_nonnull (book_cache);
+
+ /* Remove non-existing contact */
+ uids[0] = "unknown-contact";
+
+ success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->remove_contacts_sync (E_BOOK_BACKEND
(meta_backend),
+ (const gchar * const *) uids, NULL, &error);
+ g_assert_error (error, E_DATA_BOOK_ERROR, E_DATA_BOOK_STATUS_CONTACT_NOT_FOUND);
+ g_assert (!success);
+ g_clear_error (&error);
+
+ /* Remove existing contact */
+ uids[0] = "custom-1";
+
+ success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->remove_contacts_sync (E_BOOK_BACKEND
(meta_backend),
+ (const gchar * const *) uids, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpint (test_backend->load_count, ==, 0);
+ g_assert_cmpint (test_backend->save_count, ==, 0);
+ g_assert_cmpint (test_backend->remove_count, ==, 1);
+
+ ebmb_test_hash_contains (test_backend->contacts, TRUE, FALSE,
+ "custom-1", NULL,
+ NULL);
+
+ /* Going offline */
+ e_book_meta_backend_test_change_online (meta_backend, FALSE);
+
+ e_book_meta_backend_test_reset_counters (test_backend);
+
+ /* Remove existing contact */
+ uids[0] = "custom-3";
+
+ success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->remove_contacts_sync (E_BOOK_BACKEND
(meta_backend),
+ (const gchar * const *) uids, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpint (test_backend->load_count, ==, 0);
+ g_assert_cmpint (test_backend->save_count, ==, 0);
+ g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+ ebmb_test_hash_contains (test_backend->contacts, FALSE, FALSE,
+ "custom-3", NULL,
+ NULL);
+ ebmb_test_cache_contains (book_cache, TRUE, FALSE,
+ "custom-3", NULL,
+ NULL);
+
+ /* Going online */
+ e_book_meta_backend_test_change_online (meta_backend, TRUE);
+
+ g_assert_cmpint (test_backend->load_count, ==, 0);
+ g_assert_cmpint (test_backend->save_count, ==, 0);
+ g_assert_cmpint (test_backend->remove_count, ==, 1);
+
+ ebmb_test_hash_contains (test_backend->contacts, TRUE, FALSE,
+ "custom-3", NULL,
+ NULL);
+ ebmb_test_cache_contains (book_cache, TRUE, FALSE,
+ "custom-3", NULL,
+ NULL);
+
+ ebmb_test_cache_and_server_equal (book_cache, test_backend->contacts, E_CACHE_INCLUDE_DELETED);
+
+ offline_changes = e_cache_get_offline_changes (E_CACHE (book_cache), NULL, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (0, ==, g_slist_length (offline_changes));
+
+ g_object_unref (book_cache);
+}
+
+static void
+test_get_contact (EBookMetaBackend *meta_backend)
+{
+ EBookMetaBackendTest *test_backend;
+ EBookCache *book_cache;
+ EContact *contact;
+ GError *error = NULL;
+
+ g_assert_nonnull (meta_backend);
+
+ test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
+ g_assert_nonnull (book_cache);
+
+ e_book_cache_remove_contact (book_cache, "custom-5", E_CACHE_IS_ONLINE, NULL, &error);
+ g_assert_no_error (error);
+ e_book_cache_remove_contact (book_cache, "custom-6", E_CACHE_IS_ONLINE, NULL, &error);
+ g_assert_no_error (error);
+
+ /* Non-existing */
+ contact = E_BOOK_BACKEND_GET_CLASS (meta_backend)->get_contact_sync (E_BOOK_BACKEND (meta_backend),
+ "unknown-contact", NULL, &error);
+ g_assert_error (error, E_DATA_BOOK_ERROR, E_DATA_BOOK_STATUS_CONTACT_NOT_FOUND);
+ g_assert_null (contact);
+ g_clear_error (&error);
+
+ /* Existing */
+ contact = E_BOOK_BACKEND_GET_CLASS (meta_backend)->get_contact_sync (E_BOOK_BACKEND (meta_backend),
+ "custom-1", NULL, &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (contact);
+ g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_UID), ==, "custom-1");
+ g_object_unref (contact);
+
+ /* Going offline */
+ e_book_meta_backend_test_change_online (meta_backend, FALSE);
+
+ g_assert (!e_cache_contains (E_CACHE (book_cache), "custom-5", E_CACHE_EXCLUDE_DELETED));
+
+ e_book_meta_backend_test_reset_counters (test_backend);
+
+ contact = E_BOOK_BACKEND_GET_CLASS (meta_backend)->get_contact_sync (E_BOOK_BACKEND (meta_backend),
+ "custom-5", NULL, &error);
+ g_assert_error (error, E_DATA_BOOK_ERROR, E_DATA_BOOK_STATUS_CONTACT_NOT_FOUND);
+ g_assert_null (contact);
+ g_clear_error (&error);
+ g_assert_cmpint (test_backend->connect_count, ==, 0);
+ g_assert_cmpint (test_backend->list_count, ==, 0);
+ g_assert_cmpint (test_backend->load_count, ==, 0);
+
+ /* Going online */
+ e_book_meta_backend_test_change_online (meta_backend, TRUE);
+
+ g_assert (e_cache_contains (E_CACHE (book_cache), "custom-5", E_CACHE_EXCLUDE_DELETED));
+
+ /* Remove it from the cache, thus it's loaded from the "server" on demand */
+ e_book_cache_remove_contact (book_cache, "custom-5", E_CACHE_IS_ONLINE, NULL, &error);
+ g_assert_no_error (error);
+
+ g_assert_cmpint (test_backend->connect_count, ==, 1);
+ e_book_meta_backend_test_reset_counters (test_backend);
+ g_assert_cmpint (test_backend->connect_count, ==, 0);
+
+ contact = E_BOOK_BACKEND_GET_CLASS (meta_backend)->get_contact_sync (E_BOOK_BACKEND (meta_backend),
+ "custom-5", NULL, &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (contact);
+ g_assert_cmpint (test_backend->connect_count, ==, 0);
+ g_assert_cmpint (test_backend->list_count, ==, 0);
+ g_assert_cmpint (test_backend->load_count, ==, 1);
+ g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_UID), ==, "custom-5");
+ g_object_unref (contact);
+
+ g_assert (e_cache_contains (E_CACHE (book_cache), "custom-5", E_CACHE_EXCLUDE_DELETED));
+
+ g_object_unref (book_cache);
+}
+
+static void
+test_get_contact_list (EBookMetaBackend *meta_backend)
+{
+ GQueue contacts = G_QUEUE_INIT;
+ EContact *contact;
+ gboolean success;
+ GError *error = NULL;
+
+ g_assert_nonnull (meta_backend);
+
+ success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->get_contact_list_sync (E_BOOK_BACKEND
(meta_backend),
+ "(is \"uid\" \"unknown-contact\")", &contacts, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpint (g_queue_get_length (&contacts), ==, 0);
+
+ success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->get_contact_list_sync (E_BOOK_BACKEND
(meta_backend),
+ "(is \"uid\" \"custom-3\")", &contacts, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (g_queue_get_length (&contacts), ==, 1);
+ contact = g_queue_peek_head (&contacts);
+ g_assert_nonnull (contact);
+ g_assert_cmpstr (e_contact_get_const (contact, E_CONTACT_UID), ==, "custom-3");
+ g_queue_foreach (&contacts, (GFunc) g_object_unref, NULL);
+ g_queue_clear (&contacts);
+}
+
+static void
+test_get_contact_list_uids (EBookMetaBackend *meta_backend)
+{
+ GQueue uids = G_QUEUE_INIT;
+ gboolean success;
+ GError *error = NULL;
+
+ g_assert_nonnull (meta_backend);
+
+ success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->get_contact_list_uids_sync (E_BOOK_BACKEND
(meta_backend),
+ "(is \"uid\" \"unknown-contact\")", &uids, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpint (g_queue_get_length (&uids), ==, 0);
+
+ success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->get_contact_list_uids_sync (E_BOOK_BACKEND
(meta_backend),
+ "(is \"uid\" \"custom-3\")", &uids, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (g_queue_get_length (&uids), ==, 1);
+ g_assert_nonnull (g_queue_peek_head (&uids));
+ g_assert_cmpstr (g_queue_peek_head (&uids), ==, "custom-3");
+ g_queue_foreach (&uids, (GFunc) g_free, NULL);
+ g_queue_clear (&uids);
+}
+
+static void
+test_refresh (EBookMetaBackend *meta_backend)
+{
+ EBookMetaBackendTest *test_backend;
+ EBookCache *book_cache;
+ ECache *cache;
+ guint count;
+ EContact *contact;
+ GError *error = NULL;
+
+ g_assert_nonnull (meta_backend);
+
+ test_backend = E_BOOK_META_BACKEND_TEST (meta_backend);
+ book_cache = e_book_meta_backend_ref_cache (meta_backend);
+ g_assert_nonnull (book_cache);
+
+ cache = E_CACHE (book_cache);
+
+ /* Empty local cache */
+ e_cache_remove_all (cache, NULL, &error);
+ g_assert_no_error (error);
+
+ count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (count, ==, 0);
+
+ e_book_meta_backend_test_reset_counters (test_backend);
+
+ ebmb_test_remove_component (test_backend, "custom-5");
+ ebmb_test_remove_component (test_backend, "custom-6");
+
+ /* Sync with server content */
+ e_book_meta_backend_test_call_refresh (meta_backend);
+
+ g_assert_cmpint (test_backend->list_count, ==, 1);
+ g_assert_cmpint (test_backend->save_count, ==, 0);
+ g_assert_cmpint (test_backend->load_count, ==, 3);
+ g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+ count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (count, ==, 3);
+
+ ebmb_test_cache_and_server_equal (book_cache, test_backend->contacts, E_CACHE_INCLUDE_DELETED);
+
+ /* Add new contact */
+ ebmb_test_add_test_case (test_backend, "custom-5");
+
+ /* Sync with server content */
+ e_book_meta_backend_test_call_refresh (meta_backend);
+
+ g_assert_cmpint (test_backend->list_count, ==, 2);
+ g_assert_cmpint (test_backend->save_count, ==, 0);
+ g_assert_cmpint (test_backend->load_count, ==, 4);
+ g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+ count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (count, ==, 4);
+
+ ebmb_test_hash_contains (test_backend->contacts, FALSE, TRUE,
+ "custom-1",
+ "custom-2",
+ "custom-3",
+ "custom-5",
+ NULL);
+
+ ebmb_test_cache_contains (book_cache, FALSE, TRUE,
+ "custom-1",
+ "custom-2",
+ "custom-3",
+ "custom-5",
+ NULL);
+
+ /* Sync with server content */
+ e_book_meta_backend_test_call_refresh (meta_backend);
+
+ g_assert_cmpint (test_backend->list_count, ==, 3);
+ g_assert_cmpint (test_backend->save_count, ==, 0);
+ g_assert_cmpint (test_backend->load_count, ==, 4);
+ g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+ count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (count, ==, 4);
+
+ ebmb_test_cache_and_server_equal (book_cache, test_backend->contacts, E_CACHE_INCLUDE_DELETED);
+
+ /* Add some more contacts */
+ ebmb_test_add_test_case (test_backend, "custom-6");
+ ebmb_test_add_test_case (test_backend, "custom-7");
+
+ /* Sync with server content */
+ e_book_meta_backend_test_call_refresh (meta_backend);
+
+ g_assert_cmpint (test_backend->list_count, ==, 4);
+ g_assert_cmpint (test_backend->save_count, ==, 0);
+ g_assert_cmpint (test_backend->load_count, ==, 6);
+ g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+ count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (count, ==, 6);
+
+ ebmb_test_cache_and_server_equal (book_cache, test_backend->contacts, E_CACHE_INCLUDE_DELETED);
+
+ /* Remove two contacts */
+ ebmb_test_remove_component (test_backend, "custom-2");
+ ebmb_test_remove_component (test_backend, "custom-5");
+
+ /* Sync with server content */
+ e_book_meta_backend_test_call_refresh (meta_backend);
+
+ g_assert_cmpint (test_backend->list_count, ==, 5);
+ g_assert_cmpint (test_backend->save_count, ==, 0);
+ g_assert_cmpint (test_backend->load_count, ==, 6);
+ g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+ count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (count, ==, 4);
+
+ ebmb_test_cache_and_server_equal (book_cache, test_backend->contacts, E_CACHE_INCLUDE_DELETED);
+
+ /* Mix add/remove/modify */
+ ebmb_test_add_test_case (test_backend, "custom-8");
+
+ ebmb_test_remove_component (test_backend, "custom-3");
+ ebmb_test_remove_component (test_backend, "custom-6");
+
+ contact = g_hash_table_lookup (test_backend->contacts, "custom-1");
+ g_assert_nonnull (contact);
+ e_contact_set (contact, E_CONTACT_REV, "changed");
+
+ contact = g_hash_table_lookup (test_backend->contacts, "custom-7");
+ g_assert_nonnull (contact);
+ e_contact_set (contact, E_CONTACT_REV, "changed");
+
+ /* Sync with server content */
+ e_book_meta_backend_test_call_refresh (meta_backend);
+
+ g_assert_cmpint (test_backend->list_count, ==, 6);
+ g_assert_cmpint (test_backend->save_count, ==, 0);
+ g_assert_cmpint (test_backend->load_count, ==, 9);
+ g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+ count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (count, ==, 3);
+
+ ebmb_test_cache_and_server_equal (book_cache, test_backend->contacts, E_CACHE_INCLUDE_DELETED);
+
+ g_object_unref (book_cache);
+}
+
+static void
+test_cursor (EBookMetaBackend *meta_backend)
+{
+ EDataBookCursor *cursor;
+ EContactField sort_fields[] = { E_CONTACT_FULL_NAME };
+ EBookCursorSortType sort_types[] = { E_BOOK_CURSOR_SORT_ASCENDING };
+ GQueue contacts = G_QUEUE_INIT;
+ gchar *vcards[2] = { NULL, NULL };
+ const gchar *uids[2] = { NULL, NULL };
+ gint traversed;
+ gboolean success;
+ GError *error = NULL;
+
+ g_assert_nonnull (meta_backend);
+
+ /* Create the cursor */
+ cursor = E_BOOK_BACKEND_GET_CLASS (meta_backend)->create_cursor (E_BOOK_BACKEND (meta_backend),
+ sort_fields, sort_types, 1, &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (cursor);
+ g_assert_cmpint (e_data_book_cursor_get_total (cursor), ==, 5);
+ g_assert_cmpint (e_data_book_cursor_get_position (cursor), ==, 0);
+
+ traversed = e_data_book_cursor_step (cursor, NULL, E_BOOK_CURSOR_STEP_MOVE,
E_BOOK_CURSOR_ORIGIN_CURRENT, 3, NULL, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (traversed, ==, 3);
+ g_assert_cmpint (e_data_book_cursor_get_total (cursor), ==, 5);
+ g_assert_cmpint (e_data_book_cursor_get_position (cursor), ==, 3);
+
+ /* Create */
+ vcards[0] = tcu_new_vcard_from_test_case ("custom-7");
+ success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->create_contacts_sync (E_BOOK_BACKEND
(meta_backend),
+ (const gchar * const *) vcards, &contacts, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpint (g_queue_get_length (&contacts), ==, 1);
+ g_queue_foreach (&contacts, (GFunc) g_object_unref, NULL);
+ g_queue_clear (&contacts);
+ g_free (vcards[0]);
+
+ g_assert_cmpint (e_data_book_cursor_get_total (cursor), ==, 6);
+ g_assert_cmpint (e_data_book_cursor_get_position (cursor), ==, 3);
+
+ /* Modify */
+ vcards[0] = ebmb_test_modify_case ("custom-2");
+ success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->modify_contacts_sync (E_BOOK_BACKEND
(meta_backend),
+ (const gchar * const *) vcards, &contacts, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpint (g_queue_get_length (&contacts), ==, 1);
+ g_queue_foreach (&contacts, (GFunc) g_object_unref, NULL);
+ g_queue_clear (&contacts);
+ g_free (vcards[0]);
+
+ g_assert_cmpint (e_data_book_cursor_get_total (cursor), ==, 6);
+ g_assert_cmpint (e_data_book_cursor_get_position (cursor), ==, 3);
+
+ /* Remove */
+ uids[0] = "custom-3";
+ success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->remove_contacts_sync (E_BOOK_BACKEND
(meta_backend),
+ (const gchar * const *) uids, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ g_assert_cmpint (e_data_book_cursor_get_total (cursor), ==, 5);
+ g_assert_cmpint (e_data_book_cursor_get_position (cursor), ==, 2);
+
+ /* Free the cursor */
+ success = E_BOOK_BACKEND_GET_CLASS (meta_backend)->delete_cursor (E_BOOK_BACKEND (meta_backend),
cursor, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+}
+
+typedef void (* TestWithMainLoopFunc) (EBookMetaBackend *meta_backend);
+
+typedef struct _MainLoopThreadData {
+ TestWithMainLoopFunc func;
+ EBookMetaBackend *meta_backend;
+ GMainLoop *main_loop;
+} MainLoopThreadData;
+
+static gpointer
+test_with_main_loop_thread (gpointer user_data)
+{
+ MainLoopThreadData *mlt = user_data;
+
+ g_assert_nonnull (mlt);
+ g_assert_nonnull (mlt->func);
+ g_assert_nonnull (mlt->meta_backend);
+
+ mlt->func (mlt->meta_backend);
+
+ g_main_loop_quit (mlt->main_loop);
+
+ return NULL;
+}
+
+static gboolean
+quit_test_with_mainloop_cb (gpointer user_data)
+{
+ GMainLoop *main_loop = user_data;
+
+ g_assert_nonnull (main_loop);
+
+ g_main_loop_quit (main_loop);
+
+ g_assert_not_reached ();
+
+ return FALSE;
+}
+
+static gboolean
+test_with_mainloop_run_thread_idle (gpointer user_data)
+{
+ GThread *thread;
+
+ g_assert_nonnull (user_data);
+
+ thread = g_thread_new (NULL, test_with_main_loop_thread, user_data);
+ g_thread_unref (thread);
+
+ return FALSE;
+}
+
+static void
+test_with_main_loop (EBookCache *book_cache,
+ TestWithMainLoopFunc func)
+{
+ MainLoopThreadData mlt;
+ EBookMetaBackend *meta_backend;
+ guint timeout_id;
+
+ g_assert_nonnull (book_cache);
+ g_assert_nonnull (func);
+
+ meta_backend = e_book_meta_backend_test_new (book_cache);
+ g_assert_nonnull (meta_backend);
+
+ mlt.func = func;
+ mlt.meta_backend = meta_backend;
+ mlt.main_loop = g_main_loop_new (NULL, FALSE);
+
+ g_idle_add (test_with_mainloop_run_thread_idle, &mlt);
+ timeout_id = g_timeout_add_seconds (10, quit_test_with_mainloop_cb, mlt.main_loop);
+
+ g_main_loop_run (mlt.main_loop);
+
+ g_source_remove (timeout_id);
+ g_main_loop_unref (mlt.main_loop);
+ g_clear_object (&mlt.meta_backend);
+}
+
+#define main_loop_wrapper(_func) \
+static void \
+_func ## _tcu (TCUFixture *fixture, \
+ gconstpointer user_data) \
+{ \
+ test_with_main_loop (fixture->book_cache, _func); \
+}
+
+main_loop_wrapper (test_create_contacts)
+main_loop_wrapper (test_modify_contacts)
+main_loop_wrapper (test_remove_contacts)
+main_loop_wrapper (test_get_contact)
+main_loop_wrapper (test_get_contact_list)
+main_loop_wrapper (test_get_contact_list_uids)
+main_loop_wrapper (test_refresh)
+main_loop_wrapper (test_cursor)
+
+#undef main_loop_wrapper
+
+gint
+main (gint argc,
+ gchar **argv)
+{
+ ETestServerClosure tsclosure = {
+ E_TEST_SERVER_NONE,
+ NULL, /* Source customization function */
+ 0, /* Calendar Type */
+ TRUE, /* Keep the working sandbox after the test, don't remove it */
+ NULL, /* Destroy Notify function */
+ };
+ ETestServerFixture tsfixture = { 0 };
+ TCUClosure closure = { 0 };
+ gint res;
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+ g_type_init ();
+#endif
+ g_test_init (&argc, &argv, NULL);
+
+ /* Ensure that the client and server get the same locale */
+ g_assert (g_setenv ("LC_ALL", "en_US.UTF-8", TRUE));
+ setlocale (LC_ALL, "");
+
+ e_test_server_utils_setup (&tsfixture, &tsclosure);
+
+ glob_registry = tsfixture.registry;
+ g_assert_nonnull (glob_registry);
+
+ g_test_add ("/EBookMetaBackend/Photos", TCUFixture, &closure,
+ tcu_fixture_setup, test_photos, tcu_fixture_teardown);
+ g_test_add ("/EBookMetaBackend/EmptyCache", TCUFixture, &closure,
+ tcu_fixture_setup, test_empty_cache, tcu_fixture_teardown);
+ g_test_add ("/EBookMetaBackend/CreateContacts", TCUFixture, &closure,
+ tcu_fixture_setup, test_create_contacts_tcu, tcu_fixture_teardown);
+ g_test_add ("/EBookMetaBackend/ModifyContacts", TCUFixture, &closure,
+ tcu_fixture_setup, test_modify_contacts_tcu, tcu_fixture_teardown);
+ g_test_add ("/EBookMetaBackend/RemoveContacts", TCUFixture, &closure,
+ tcu_fixture_setup, test_remove_contacts_tcu, tcu_fixture_teardown);
+ g_test_add ("/EBookMetaBackend/GetContact", TCUFixture, &closure,
+ tcu_fixture_setup, test_get_contact_tcu, tcu_fixture_teardown);
+ g_test_add ("/EBookMetaBackend/GetContactList", TCUFixture, &closure,
+ tcu_fixture_setup, test_get_contact_list_tcu, tcu_fixture_teardown);
+ g_test_add ("/EBookMetaBackend/GetContactListUids", TCUFixture, &closure,
+ tcu_fixture_setup, test_get_contact_list_uids_tcu, tcu_fixture_teardown);
+ g_test_add ("/EBookMetaBackend/Refresh", TCUFixture, &closure,
+ tcu_fixture_setup, test_refresh_tcu, tcu_fixture_teardown);
+ g_test_add ("/EBookMetaBackend/Cursor", TCUFixture, &closure,
+ tcu_fixture_setup, test_cursor_tcu, tcu_fixture_teardown);
+
+ res = g_test_run ();
+
+ e_test_server_utils_teardown (&tsfixture, &tsclosure);
+
+ return res;
+}
diff --git a/tests/libedata-book/test-sqlite-create-cursor.c b/tests/libedata-book/test-sqlite-create-cursor.c
index 0357d47..8ad9012 100644
--- a/tests/libedata-book/test-sqlite-create-cursor.c
+++ b/tests/libedata-book/test-sqlite-create-cursor.c
@@ -77,8 +77,8 @@ test_create_cursor_invalid_sort (EbSqlFixture *fixture,
sort_fields, sort_types, 1, &error);
g_assert (cursor == NULL);
- g_assert (error);
- g_assert (g_error_matches (error, E_BOOK_SQLITE_ERROR, E_BOOK_SQLITE_ERROR_INVALID_QUERY));
+ g_assert_error (error, E_BOOK_SQLITE_ERROR, E_BOOK_SQLITE_ERROR_INVALID_QUERY);
+ g_clear_error (&error);
}
static void
@@ -91,8 +91,8 @@ test_create_cursor_missing_sort (EbSqlFixture *fixture,
cursor = e_book_sqlite_cursor_new (fixture->ebsql, NULL, NULL, NULL, 0, &error);
g_assert (cursor == NULL);
- g_assert (error);
- g_assert (g_error_matches (error, E_BOOK_SQLITE_ERROR, E_BOOK_SQLITE_ERROR_INVALID_QUERY));
+ g_assert_error (error, E_BOOK_SQLITE_ERROR, E_BOOK_SQLITE_ERROR_INVALID_QUERY);
+ g_clear_error (&error);
}
gint
diff --git a/tests/libedata-cal/CMakeLists.txt b/tests/libedata-cal/CMakeLists.txt
index d42a64b..d5f6145 100644
--- a/tests/libedata-cal/CMakeLists.txt
+++ b/tests/libedata-cal/CMakeLists.txt
@@ -4,7 +4,14 @@ set(extra_deps
edata-cal
)
-set(extra_defines)
+set(extra_defines
+ -DSRCDIR=\"${CMAKE_CURRENT_SOURCE_DIR}\"
+ -DINSTALLED_TEST_DIR=\"${INSTALLED_TESTS_EXEC_DIR}\"
+ -DBACKENDDIR=\"${ecal_backenddir}\"
+ -DDATADIR=\"${SHARE_INSTALL_PREFIX}\"
+ -DBUILDDIR=\"${CAMKE_BINARY_DIR}\"
+ -DCAMEL_PROVIDERDIR=\"${camel_providerdir}\"
+)
set(extra_cflags
${CALENDAR_CFLAGS}
@@ -18,10 +25,64 @@ set(extra_ldflags
${CALENDAR_LDFLAGS}
)
+set(SOURCES
+ test-cal-cache-utils.c
+ test-cal-cache-utils.h
+)
+
+add_library(data-cal-test-utils STATIC
+ ${SOURCES}
+)
+
+add_dependencies(data-cal-test-utils
+ edataserver
+ ${extra_deps}
+)
+target_compile_definitions(data-cal-test-utils PRIVATE
+ -DG_LOG_DOMAIN=\"data-cal-test-utils\"
+ ${extra_defines}
+)
+
+target_compile_options(data-cal-test-utils PUBLIC
+ ${BACKEND_CFLAGS}
+ ${DATA_SERVER_CFLAGS}
+ ${extra_cflags}
+)
+
+target_include_directories(data-cal-test-utils PUBLIC
+ ${CMAKE_BINARY_DIR}
+ ${CMAKE_BINARY_DIR}/src
+ ${CMAKE_SOURCE_DIR}/src
+ ${BACKEND_INCLUDE_DIRS}
+ ${DATA_SERVER_INCLUDE_DIRS}
+ ${extra_incdirs}
+)
+
+target_link_libraries(data-cal-test-utils
+ edataserver
+ ${extra_deps}
+ ${BACKEND_LDFLAGS}
+ ${DATA_SERVER_LDFLAGS}
+ ${extra_ldflags}
+)
+
+set(extra_deps
+ ecal
+ edata-cal
+ data-cal-test-utils
+)
+
+set(extra_defines)
+
# Should be kept ordered approximately from least to most difficult/complex
set(TESTS
test-cal-backend-sexp
test-intervaltree
+ test-cal-cache-getters
+ test-cal-cache-intervals
+ test-cal-cache-offline
+ test-cal-cache-search
+ test-cal-meta-backend
)
foreach(_test ${TESTS})
@@ -38,3 +99,11 @@ foreach(_test ${TESTS})
"TEST_INSTALLED_SERVICES=1"
)
endforeach(_test)
+
+if(ENABLE_INSTALLED_TESTS)
+ file(GLOB COMPONENTS ${CMAKE_CURRENT_SOURCE_DIR}/components/*.ics)
+
+ install(FILES ${COMPONENTS}
+ DESTINATION ${INSTALLED_TESTS_EXEC_DIR}/components
+ )
+endif(ENABLE_INSTALLED_TESTS)
diff --git a/tests/libedata-cal/components/.gitattributes b/tests/libedata-cal/components/.gitattributes
new file mode 100644
index 0000000..11bcb63
--- /dev/null
+++ b/tests/libedata-cal/components/.gitattributes
@@ -0,0 +1 @@
+*.ics eol=crlf
diff --git a/tests/libedata-cal/components/event-1.ics b/tests/libedata-cal/components/event-1.ics
new file mode 100644
index 0000000..39eaa58
--- /dev/null
+++ b/tests/libedata-cal/components/event-1.ics
@@ -0,0 +1,18 @@
+BEGIN:VEVENT
+UID:event-1
+DTSTAMP:20170130T000000Z
+CREATED:20170216T155507Z
+LAST-MODIFIED:20170216T155543Z
+SEQUENCE:1
+DTSTART:20170209T013000Z
+DTEND:20170209T030000Z
+SUMMARY:Alarmed
+DESCRIPTION:Event with alarm
+CLASS:PUBLIC
+TRANSP:OPACHE
+BEGIN:VALARM
+TRIGGER:-PT30M
+ACTION:DISPLAY
+DESCRIPTION:Reminder
+END:VALARM
+END:VEVENT
diff --git a/tests/libedata-cal/components/event-2.ics b/tests/libedata-cal/components/event-2.ics
new file mode 100644
index 0000000..ce980be
--- /dev/null
+++ b/tests/libedata-cal/components/event-2.ics
@@ -0,0 +1,14 @@
+BEGIN:VEVENT
+UID:event-2
+DTSTAMP:20170103T070000Z
+CREATED:20170216T155507Z
+LAST-MODIFIED:20170216T155543Z
+SEQUENCE:1
+DTSTART:20170103T080000Z
+DTEND:20170103T083000Z
+SUMMARY:First working day
+DESCRIPTION:Multiline\n
+ description text
+CLASS:CONFIDENTIAL
+CATEGORIES:Work,Hard
+END:VEVENT
diff --git a/tests/libedata-cal/components/event-3.ics b/tests/libedata-cal/components/event-3.ics
new file mode 100644
index 0000000..4fcf5bf
--- /dev/null
+++ b/tests/libedata-cal/components/event-3.ics
@@ -0,0 +1,12 @@
+BEGIN:VEVENT
+UID:event-3
+DTSTAMP:20170103T070000Z
+CREATED:20170216T155507Z
+LAST-MODIFIED:20170216T155543Z
+SEQUENCE:3
+DTSTART:20170104T100000Z
+DTEND:20170110T120000Z
+SUMMARY:Lunch prepare
+LOCATION:Kitchen
+CLASS:PRIVATE
+END:VEVENT
diff --git a/tests/libedata-cal/components/event-4.ics b/tests/libedata-cal/components/event-4.ics
new file mode 100644
index 0000000..2fde425
--- /dev/null
+++ b/tests/libedata-cal/components/event-4.ics
@@ -0,0 +1,12 @@
+BEGIN:VEVENT
+UID:event-4
+DTSTAMP:20170102T000000Z
+CREATED:20170216T155507Z
+LAST-MODIFIED:20170216T155543Z
+SEQUENCE:3
+DTSTART:20170102T100000Z
+DTEND:20170102T180000Z
+SUMMARY:After-party clean up
+LOCATION:All around
+CLASS:PUBLIC
+END:VEVENT
diff --git a/tests/libedata-cal/components/event-5.ics b/tests/libedata-cal/components/event-5.ics
new file mode 100644
index 0000000..a12a488
--- /dev/null
+++ b/tests/libedata-cal/components/event-5.ics
@@ -0,0 +1,18 @@
+BEGIN:VEVENT
+UID:event-5
+DTSTAMP:20101231T000000Z
+CREATED:20170216T155507Z
+LAST-MODIFIED:20170216T155543Z
+SEQUENCE:1
+DTSTART:20091231T000000Z
+DTEND:20100101T235959Z
+SUMMARY:New Year Party
+LOCATION:All around
+CATEGORIES:Holiday,International
+RRULE:FREQ=YEARLY
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER;RELATED=START:-P1D
+DESCRIPTION:New Year Party
+END:VALARM
+END:VEVENT
diff --git a/tests/libedata-cal/components/event-6-a.ics b/tests/libedata-cal/components/event-6-a.ics
new file mode 100644
index 0000000..404b51e
--- /dev/null
+++ b/tests/libedata-cal/components/event-6-a.ics
@@ -0,0 +1,13 @@
+BEGIN:VEVENT
+UID:event-6
+DTSTAMP:20170221T121736Z
+DTSTART;TZID=America/New_York:20170225T150000
+DTEND;TZID=America/New_York:20170225T160000
+SEQUENCE:2
+SUMMARY:Recurring with detached instance (3rd instance)
+TRANSP:OPAQUE
+CLASS:PUBLIC
+CREATED:20170221T125024Z
+LAST-MODIFIED:20170221T125341Z
+RECURRENCE-ID;TZID=America/New_York:20170225T134900
+END:VEVENT
diff --git a/tests/libedata-cal/components/event-6.ics b/tests/libedata-cal/components/event-6.ics
new file mode 100644
index 0000000..ee85a01
--- /dev/null
+++ b/tests/libedata-cal/components/event-6.ics
@@ -0,0 +1,12 @@
+BEGIN:VEVENT
+UID:event-6
+DTSTAMP:20170221T121736Z
+DTSTART;TZID=America/New_York:20170221T134900
+DTEND;TZID=America/New_York:20170221T144900
+SEQUENCE:1
+SUMMARY:Recurring with detached instance
+RRULE:FREQ=DAILY;COUNT=5;INTERVAL=2
+CLASS:PUBLIC
+CREATED:20170221T125024Z
+LAST-MODIFIED:20170221T125024Z
+END:VEVENT
diff --git a/tests/libedata-cal/components/event-7.ics b/tests/libedata-cal/components/event-7.ics
new file mode 100644
index 0000000..b08d392
--- /dev/null
+++ b/tests/libedata-cal/components/event-7.ics
@@ -0,0 +1,14 @@
+BEGIN:VEVENT
+UID:event-7
+DTSTAMP:20170221T121736Z
+DTSTART;TZID=/freeassociation.sourceforge.net/America/New_York:20170221T135000
+DTEND;TZID=/freeassociation.sourceforge.net/America/New_York:20170221T145000
+SEQUENCE:1
+SUMMARY:With attachment
+TRANSP:OPAQUE
+ATTACH:file:///usr/share/icons/hicolor/48x48/apps/evolution.png
+CLASS:PUBLIC
+CREATED:20170221T125054Z
+LAST-MODIFIED:20170221T125054Z
+CATEGORIES:Holiday,Work
+END:VEVENT
diff --git a/tests/libedata-cal/components/event-8.ics b/tests/libedata-cal/components/event-8.ics
new file mode 100644
index 0000000..35887ad
--- /dev/null
+++ b/tests/libedata-cal/components/event-8.ics
@@ -0,0 +1,16 @@
+BEGIN:VEVENT
+UID:event-8
+DTSTAMP:20170221T121736Z
+DTSTART:20170225T160000
+DTEND:20170225T160500
+SEQUENCE:2
+ORGANIZER;CN=Bob:MAILTO:bob@no.where
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;
+ RSVP=TRUE;LANGUAGE=en:MAILTO:alice@no.where
+SUMMARY:Meet Alice (Floating time)
+COMMENT:User commentary text
+TRANSP:OPAQUE
+CLASS:PUBLIC
+CREATED:20170221T131322Z
+LAST-MODIFIED:20170221T131322Z
+END:VEVENT
diff --git a/tests/libedata-cal/components/event-9.ics b/tests/libedata-cal/components/event-9.ics
new file mode 100644
index 0000000..f0edf3c
--- /dev/null
+++ b/tests/libedata-cal/components/event-9.ics
@@ -0,0 +1,17 @@
+BEGIN:VEVENT
+UID:event-9
+DTSTAMP:20170221T121736Z
+DTSTART;TZID=America/New_York:20170225T160000
+DTEND;TZID=America/New_York:20170225T170000
+SEQUENCE:2
+ORGANIZER;CN=Alice:MAILTO:alice@no.where
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;
+ RSVP=TRUE;CN=Bob;LANGUAGE=en:MAILTO:bob@no.where
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;
+ RSVP=TRUE;CN=Charlie;LANGUAGE=en:MAILTO:charlie@no.where
+SUMMARY:2-on-1
+TRANSP:OPAQUE
+CLASS:PUBLIC
+CREATED:20170221T131421Z
+LAST-MODIFIED:20170221T131421Z
+END:VEVENT
diff --git a/tests/libedata-cal/components/invite-1.ics b/tests/libedata-cal/components/invite-1.ics
new file mode 100644
index 0000000..32267b6
--- /dev/null
+++ b/tests/libedata-cal/components/invite-1.ics
@@ -0,0 +1,19 @@
+BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Ximian//NONSGML Evolution Calendar//EN
+VERSION:2.0
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:invite
+DTSTAMP:20170308T175957Z
+DTSTART:20170321T120000Z
+DTEND:20170321T130000Z
+SEQUENCE:1
+ORGANIZER;CN=Organizer:MAILTO:organizer@no.where
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;
+ RSVP=TRUE;CN=User;LANGUAGE=en:MAILTO:user@no.where
+SUMMARY:Invite
+TRANSP:OPAQUE
+CLASS:PUBLIC
+END:VEVENT
+END:VCALENDAR
diff --git a/tests/libedata-cal/components/invite-2.ics b/tests/libedata-cal/components/invite-2.ics
new file mode 100644
index 0000000..bf8423e
--- /dev/null
+++ b/tests/libedata-cal/components/invite-2.ics
@@ -0,0 +1,19 @@
+BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Ximian//NONSGML Evolution Calendar//EN
+VERSION:2.0
+METHOD:REPLY
+BEGIN:VEVENT
+UID:invite
+DTSTAMP:20170308T181958Z
+DTSTART:20170321T120000Z
+DTEND:20170321T130000Z
+SEQUENCE:1
+ORGANIZER;CN=Organizer:MAILTO:organizer@no.where
+ATTENDEE;ROLE=OPT-PARTICIPANT;PARTSTAT=ACCEPTED:MAILTO:user@no.where
+SUMMARY:Invite
+TRANSP:OPAQUE
+CLASS:PUBLIC
+COMMENT:See you there
+END:VEVENT
+END:VCALENDAR
diff --git a/tests/libedata-cal/components/invite-3.ics b/tests/libedata-cal/components/invite-3.ics
new file mode 100644
index 0000000..80b65d2
--- /dev/null
+++ b/tests/libedata-cal/components/invite-3.ics
@@ -0,0 +1,21 @@
+BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Ximian//NONSGML Evolution Calendar//EN
+VERSION:2.0
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:invite
+DTSTAMP:20170308T180058Z
+DTSTART:20170321T130000Z
+DTEND:20170321T140000Z
+SEQUENCE:2
+ORGANIZER;CN=Organizer:MAILTO:organizer@no.where
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;
+ RSVP=TRUE;CN=User;LANGUAGE=en:MAILTO:user@no.where
+SUMMARY:Invite (modified)
+TRANSP:OPAQUE
+CLASS:PUBLIC
+CREATED:20170308T180033Z
+LAST-MODIFIED:20170308T180033Z
+END:VEVENT
+END:VCALENDAR
diff --git a/tests/libedata-cal/components/invite-4.ics b/tests/libedata-cal/components/invite-4.ics
new file mode 100644
index 0000000..ab1092b
--- /dev/null
+++ b/tests/libedata-cal/components/invite-4.ics
@@ -0,0 +1,21 @@
+BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+PRODID:-//Ximian//NONSGML Evolution Calendar//EN
+VERSION:2.0
+METHOD:CANCEL
+BEGIN:VEVENT
+UID:invite
+DTSTAMP:20170308T180113Z
+DTSTART:20170321T130000Z
+DTEND:20170321T140000Z
+SEQUENCE:2
+ORGANIZER;CN=Organizer:MAILTO:organizer@no.where
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;
+ RSVP=TRUE;CN=User;LANGUAGE=en:MAILTO:user@no.where
+SUMMARY:Invite (modified)
+TRANSP:OPAQUE
+CLASS:PUBLIC
+CREATED:20170308T180033Z
+LAST-MODIFIED:20170308T180058Z
+END:VEVENT
+END:VCALENDAR
diff --git a/tests/libedata-cal/components/task-1.ics b/tests/libedata-cal/components/task-1.ics
new file mode 100644
index 0000000..f64663c
--- /dev/null
+++ b/tests/libedata-cal/components/task-1.ics
@@ -0,0 +1,9 @@
+BEGIN:VTODO
+UID:task-1
+DTSTAMP:20170221T130109Z
+SUMMARY:Simple task
+CLASS:PUBLIC
+SEQUENCE:1
+CREATED:20170221T130123Z
+LAST-MODIFIED:20170221T130123Z
+END:VTODO
diff --git a/tests/libedata-cal/components/task-2.ics b/tests/libedata-cal/components/task-2.ics
new file mode 100644
index 0000000..64b83ab
--- /dev/null
+++ b/tests/libedata-cal/components/task-2.ics
@@ -0,0 +1,11 @@
+BEGIN:VTODO
+UID:task-2
+DTSTAMP:20170221T130109Z
+SUMMARY:With Due date
+DUE;TZID=America/New_York:20170313T000000
+PERCENT-COMPLETE:0
+CLASS:PUBLIC
+SEQUENCE:1
+CREATED:20170221T130208Z
+LAST-MODIFIED:20170221T130208Z
+END:VTODO
diff --git a/tests/libedata-cal/components/task-3.ics b/tests/libedata-cal/components/task-3.ics
new file mode 100644
index 0000000..e54e4fc
--- /dev/null
+++ b/tests/libedata-cal/components/task-3.ics
@@ -0,0 +1,13 @@
+BEGIN:VTODO
+UID:task-3
+DTSTAMP:20170221T130109Z
+SUMMARY:With completed
+STATUS:COMPLETED
+COMPLETED;America/New_York:20170221T000000
+PERCENT-COMPLETE:100
+CLASS:PUBLIC
+DESCRIPTION:Task having _with_ in description
+SEQUENCE:1
+CREATED:20170221T130319Z
+LAST-MODIFIED:20170221T130319Z
+END:VTODO
diff --git a/tests/libedata-cal/components/task-4.ics b/tests/libedata-cal/components/task-4.ics
new file mode 100644
index 0000000..82c36aa
--- /dev/null
+++ b/tests/libedata-cal/components/task-4.ics
@@ -0,0 +1,13 @@
+BEGIN:VTODO
+UID:task-4
+DTSTAMP:20170221T130109Z
+SUMMARY:With completed (2nd) and Due
+STATUS:COMPLETED
+DUE;TZID=America/New_York:20170301T000000
+COMPLETED;TZID=America/New_York:20170221T000000
+PERCENT-COMPLETE:100
+CLASS:PUBLIC
+SEQUENCE:1
+CREATED:20170221T130339Z
+LAST-MODIFIED:20170221T130339Z
+END:VTODO
diff --git a/tests/libedata-cal/components/task-5.ics b/tests/libedata-cal/components/task-5.ics
new file mode 100644
index 0000000..b813194
--- /dev/null
+++ b/tests/libedata-cal/components/task-5.ics
@@ -0,0 +1,13 @@
+BEGIN:VTODO
+UID:task-5
+DTSTAMP:20170221T130109Z
+SUMMARY:20% complete
+STATUS:IN-PROCESS
+PERCENT-COMPLETE:20
+CLASS:PUBLIC
+SEQUENCE:1
+PRIORITY:7
+CREATED:20170221T130411Z
+LAST-MODIFIED:20170221T130411Z
+LOCATION:Kitchen
+END:VTODO
diff --git a/tests/libedata-cal/components/task-6.ics b/tests/libedata-cal/components/task-6.ics
new file mode 100644
index 0000000..1ea7777
--- /dev/null
+++ b/tests/libedata-cal/components/task-6.ics
@@ -0,0 +1,14 @@
+BEGIN:VTODO
+UID:task-6
+DTSTAMP:20170221T130109Z
+SUMMARY:90% complete (Confidential)
+STATUS:IN-PROCESS
+PRIORITY:5
+PERCENT-COMPLETE:90
+CLASS:CONFIDENTIAL
+COMMENT:User commentary text
+SEQUENCE:2
+DTSTART;TZID=America/New_York:20131213T131313
+CREATED:20170221T130512Z
+LAST-MODIFIED:20170221T130610Z
+END:VTODO
diff --git a/tests/libedata-cal/components/task-7.ics b/tests/libedata-cal/components/task-7.ics
new file mode 100644
index 0000000..bd01312
--- /dev/null
+++ b/tests/libedata-cal/components/task-7.ics
@@ -0,0 +1,17 @@
+BEGIN:VTODO
+UID:task-7
+DTSTAMP:20170221T130109Z
+SUMMARY:55% complete (Private)
+STATUS:IN-PROCESS
+PERCENT-COMPLETE:55
+CLASS:PRIVATE
+SEQUENCE:3
+PRIORITY:2
+CREATED:20170221T130447Z
+LAST-MODIFIED:20170221T130618Z
+BEGIN:VALARM
+TRIGGER:-PT30M
+ACTION:DISPLAY
+DESCRIPTION:Reminder
+END:VALARM
+END:VTODO
diff --git a/tests/libedata-cal/components/task-8.ics b/tests/libedata-cal/components/task-8.ics
new file mode 100644
index 0000000..f3ca458
--- /dev/null
+++ b/tests/libedata-cal/components/task-8.ics
@@ -0,0 +1,11 @@
+BEGIN:VTODO
+UID:task-8
+DTSTAMP:20170221T130109Z
+SUMMARY:Status - Cancelled
+STATUS:CANCELLED
+PERCENT-COMPLETE:33
+CLASS:PUBLIC
+SEQUENCE:1
+CREATED:20170221T130727Z
+LAST-MODIFIED:20170221T130727Z
+END:VTODO
diff --git a/tests/libedata-cal/components/task-9.ics b/tests/libedata-cal/components/task-9.ics
new file mode 100644
index 0000000..af66b26
--- /dev/null
+++ b/tests/libedata-cal/components/task-9.ics
@@ -0,0 +1,11 @@
+BEGIN:VTODO
+UID:task-9
+DTSTAMP:20170221T130109Z
+SUMMARY:With start date
+DTSTART;TZID=America/New_York:20170213T000000
+PERCENT-COMPLETE:0
+CLASS:PUBLIC
+SEQUENCE:1
+CREATED:20170221T132838Z
+LAST-MODIFIED:20170221T132838Z
+END:VTODO
diff --git a/tests/libedata-cal/test-cal-cache-getters.c b/tests/libedata-cal/test-cal-cache-getters.c
new file mode 100644
index 0000000..4c05413
--- /dev/null
+++ b/tests/libedata-cal/test-cal-cache-getters.c
@@ -0,0 +1,247 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libecal/libecal.h>
+
+#include "test-cal-cache-utils.h"
+
+static ECalComponentId *
+extract_id_from_component (ECalComponent *component)
+{
+ ECalComponentId *id;
+
+ g_assert (component != NULL);
+
+ id = e_cal_component_get_id (component);
+ g_assert (id != NULL);
+ g_assert (id->uid != NULL);
+
+ return id;
+}
+
+static ECalComponentId *
+extract_id_from_string (const gchar *icalstring)
+{
+ ECalComponent *component;
+ ECalComponentId *id;
+
+ g_assert (icalstring != NULL);
+
+ component = e_cal_component_new_from_string (icalstring);
+ g_assert (component != NULL);
+
+ id = extract_id_from_component (component);
+
+ g_object_unref (component);
+
+ return id;
+}
+
+static void
+test_get_one (ECalCache *cal_cache,
+ const gchar *uid,
+ const gchar *rid,
+ gboolean expect_failure)
+{
+ ECalComponent *component = NULL;
+ ECalComponentId *id;
+ gchar *icalstring = NULL;
+ gboolean success;
+ GError *error = NULL;
+
+ success = e_cal_cache_get_component (cal_cache, uid, rid, &component, NULL, &error);
+ if (expect_failure) {
+ g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+ g_assert (!success);
+ g_assert (!component);
+
+ g_clear_error (&error);
+ } else {
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_nonnull (component);
+
+ id = extract_id_from_component (component);
+
+ g_assert_cmpstr (id->uid, ==, uid);
+ g_assert_cmpstr (id->rid, ==, rid && *rid ? rid : NULL);
+
+ e_cal_component_free_id (id);
+ g_object_unref (component);
+ }
+
+ success = e_cal_cache_get_component_as_string (cal_cache, uid, rid, &icalstring, NULL, &error);
+ if (expect_failure) {
+ g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+ g_assert (!success);
+ g_assert (!icalstring);
+
+ g_clear_error (&error);
+ } else {
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_nonnull (icalstring);
+
+ id = extract_id_from_string (icalstring);
+
+ g_assert_cmpstr (id->uid, ==, uid);
+ g_assert_cmpstr (id->rid, ==, rid && *rid ? rid : NULL);
+
+ e_cal_component_free_id (id);
+ g_free (icalstring);
+ }
+}
+
+static void
+test_getters_one (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ test_get_one (fixture->cal_cache, "unexistent-event", NULL, TRUE);
+ test_get_one (fixture->cal_cache, "unexistent-event", "", TRUE);
+ test_get_one (fixture->cal_cache, "event-2", NULL, FALSE);
+ test_get_one (fixture->cal_cache, "event-2", "", FALSE);
+ test_get_one (fixture->cal_cache, "event-5", NULL, FALSE);
+ test_get_one (fixture->cal_cache, "event-5", "", FALSE);
+ test_get_one (fixture->cal_cache, "event-5", "20131231T000000Z", TRUE);
+ test_get_one (fixture->cal_cache, "event-6", NULL, FALSE);
+ test_get_one (fixture->cal_cache, "event-6", "", FALSE);
+ test_get_one (fixture->cal_cache, "event-6", "20170225T134900", FALSE);
+}
+
+/* NULL-terminated list of pairs <uid, rid>, what to expect */
+static void
+test_get_all (ECalCache *cal_cache,
+ const gchar *uid,
+ ...)
+{
+ ECalComponentId *id;
+ GSList *items, *link;
+ va_list va;
+ const gchar *tmp;
+ GHashTable *expects;
+ gboolean success;
+ GError *error = NULL;
+
+ expects = g_hash_table_new_full ((GHashFunc) e_cal_component_id_hash, (GEqualFunc)
e_cal_component_id_equal,
+ (GDestroyNotify) e_cal_component_free_id, NULL);
+
+ va_start (va, uid);
+ tmp = va_arg (va, const gchar *);
+ while (tmp) {
+ const gchar *rid = va_arg (va, const gchar *);
+ id = e_cal_component_id_new (tmp, rid);
+
+ g_hash_table_insert (expects, id, NULL);
+
+ tmp = va_arg (va, const gchar *);
+ }
+ va_end (va);
+
+ items = NULL;
+
+ success = e_cal_cache_get_components_by_uid (cal_cache, uid, &items, NULL, &error);
+ if (!g_hash_table_size (expects)) {
+ g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+ g_assert (!success);
+ g_assert (!items);
+
+ g_clear_error (&error);
+ } else {
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_nonnull (items);
+
+ g_assert_cmpint (g_hash_table_size (expects), ==, g_slist_length (items));
+
+ for (link = items; link; link = g_slist_next (link)) {
+ id = extract_id_from_component (link->data);
+
+ g_assert_cmpstr (id->uid, ==, uid);
+ g_assert (g_hash_table_contains (expects, id));
+
+ e_cal_component_free_id (id);
+ }
+
+ g_slist_free_full (items, g_object_unref);
+ }
+
+ items = NULL;
+
+ success = e_cal_cache_get_components_by_uid_as_string (cal_cache, uid, &items, NULL, &error);
+ if (!g_hash_table_size (expects)) {
+ g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+ g_assert (!success);
+ g_assert (!items);
+
+ g_clear_error (&error);
+ } else {
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_nonnull (items);
+
+ g_assert_cmpint (g_hash_table_size (expects), ==, g_slist_length (items));
+
+ for (link = items; link; link = g_slist_next (link)) {
+ id = extract_id_from_string (link->data);
+
+ g_assert_cmpstr (id->uid, ==, uid);
+ g_assert (g_hash_table_contains (expects, id));
+
+ e_cal_component_free_id (id);
+ }
+
+ g_slist_free_full (items, g_free);
+ }
+
+ g_hash_table_destroy (expects);
+}
+
+static void
+test_getters_all (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ test_get_all (fixture->cal_cache, "unexistent-event", NULL);
+ test_get_all (fixture->cal_cache, "unexistent-event", NULL);
+ test_get_all (fixture->cal_cache, "event-2", "event-2", NULL, NULL);
+ test_get_all (fixture->cal_cache, "event-5", "event-5", NULL, NULL);
+ test_get_all (fixture->cal_cache, "event-6", "event-6", NULL, "event-6", "20170225T134900", NULL);
+}
+
+gint
+main (gint argc,
+ gchar **argv)
+{
+ TCUClosure closure_events = { TCU_LOAD_COMPONENT_SET_EVENTS };
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+ g_type_init ();
+#endif
+ g_test_init (&argc, &argv, NULL);
+
+ /* Ensure that the client and server get the same locale */
+ g_assert (g_setenv ("LC_ALL", "en_US.UTF-8", TRUE));
+ setlocale (LC_ALL, "");
+
+ g_test_add ("/ECalCache/Getters/One", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_getters_one, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Getters/All", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_getters_all, tcu_fixture_teardown);
+
+ return g_test_run ();
+}
diff --git a/tests/libedata-cal/test-cal-cache-intervals.c b/tests/libedata-cal/test-cal-cache-intervals.c
new file mode 100644
index 0000000..7d32622
--- /dev/null
+++ b/tests/libedata-cal/test-cal-cache-intervals.c
@@ -0,0 +1,344 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libecal/libecal.h>
+
+#include "test-cal-cache-utils.h"
+
+#define NUM_INTERVALS_CLOSED 100
+#define NUM_INTERVALS_OPEN 100
+#define NUM_SEARCHES 500
+#define DELETE_PROBABILITY 0.3
+#define _TIME_MIN ((time_t) 0) /* Min valid time_t */
+#define _TIME_MAX ((time_t) INT_MAX) /* Max valid time_t */
+
+typedef struct _IntervalData {
+ gint start;
+ gint end;
+ ECalComponent * comp;
+} IntervalData;
+
+static void
+interval_data_free (gpointer ptr)
+{
+ IntervalData *id = ptr;
+
+ if (id) {
+ g_object_unref (id->comp);
+ g_free (id);
+ }
+}
+
+static gint
+compare_intervals (time_t x_start,
+ time_t x_end,
+ time_t y_start,
+ time_t y_end)
+{
+ /* assumption: x_start <= x_end */
+ /* assumption: y_start <= y_end */
+
+ /* x is left of y */
+ if (x_end < y_start)
+ return -1;
+
+ /* x is right of y */
+ if (y_end < x_start)
+ return 1;
+
+ /* x and y overlap */
+ return 0;
+}
+
+static GHashTable *
+search_in_intervals (ETimezoneCache *zone_cache,
+ GSList *intervals,
+ time_t start,
+ time_t end)
+{
+ ECalBackendSExp *sexp;
+ struct icaltimetype itt_start, itt_end;
+ gchar *expr;
+ GSList *link;
+ GHashTable *res;
+
+ itt_start = icaltime_from_timet_with_zone (start, FALSE, NULL);
+ itt_end = icaltime_from_timet_with_zone (end, FALSE, NULL);
+
+ expr = g_strdup_printf ("(occur-in-time-range? (make-time \"%04d%02d%02dT%02d%02d%02dZ\") (make-time
\"%04d%02d%02dT%02d%02d%02dZ\"))",
+ itt_start.year, itt_start.month, itt_start.day, itt_start.hour, itt_start.minute,
itt_start.second,
+ itt_end.year, itt_end.month, itt_end.day, itt_end.hour, itt_end.minute, itt_end.second);
+
+ sexp = e_cal_backend_sexp_new (expr);
+
+ g_free (expr);
+
+ g_assert_nonnull (sexp);
+
+ res = g_hash_table_new_full ((GHashFunc) e_cal_component_id_hash, (GEqualFunc)
e_cal_component_id_equal,
+ (GDestroyNotify) e_cal_component_free_id, g_object_unref);
+
+ for (link = intervals; link; link = g_slist_next (link)) {
+ IntervalData *data = link->data;
+
+ if (compare_intervals (start, end, data->start, data->end) == 0 &&
+ e_cal_backend_sexp_match_comp (sexp, data->comp, zone_cache)) {
+ ECalComponentId *id = NULL;
+
+ id = e_cal_component_get_id (data->comp);
+ g_assert_nonnull (id);
+
+ g_hash_table_insert (res, id, g_object_ref (data->comp));
+ }
+ }
+
+ g_object_unref (sexp);
+
+ return res;
+}
+
+static void
+check_search_results (GSList *ecalcomps,
+ GHashTable *from_intervals)
+{
+ GSList *link;
+
+ g_assert_cmpint (g_slist_length (ecalcomps), ==, g_hash_table_size (from_intervals));
+
+ for (link = ecalcomps; link; link = g_slist_next (link)) {
+ ECalComponent *comp = link->data;
+ ECalComponentId *id = NULL;
+
+ id = e_cal_component_get_id (comp);
+ g_assert_nonnull (id);
+
+ g_assert (g_hash_table_contains (from_intervals, id));
+
+ e_cal_component_free_id (id);
+ }
+}
+
+static ECalComponent *
+create_test_component (time_t start,
+ time_t end)
+{
+ ECalComponent *comp;
+ ECalComponentText summary;
+ struct icaltimetype current, ittstart, ittend;
+
+ comp = e_cal_component_new ();
+
+ e_cal_component_set_new_vtype (comp, E_CAL_COMPONENT_EVENT);
+
+ ittstart = icaltime_from_timet_with_zone (start, 0, NULL);
+ ittend = icaltime_from_timet_with_zone (end, 0, NULL);
+
+ icalcomponent_set_dtstart (e_cal_component_get_icalcomponent (comp), ittstart);
+ if (end != _TIME_MAX)
+ icalcomponent_set_dtend (e_cal_component_get_icalcomponent (comp), ittend);
+
+ summary.value = g_strdup_printf ("%s - %s", icaltime_as_ical_string (ittstart),
icaltime_as_ical_string (ittend));
+ summary.altrep = NULL;
+
+ e_cal_component_set_summary (comp, &summary);
+
+ g_free ((gchar *) summary.value);
+
+ current = icaltime_from_timet_with_zone (time (NULL), 0, NULL);
+ e_cal_component_set_created (comp, ¤t);
+ e_cal_component_set_last_modified (comp, ¤t);
+
+ e_cal_component_rescan (comp);
+
+ return comp;
+}
+
+static void
+test_intervals (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ /*
+ * outline:
+ * 1. create new tree and empty list of intervals
+ * 2. insert some intervals into tree and list
+ * 3. do various searches, compare results of both structures
+ * 4. delete some intervals
+ * 5. do various searches, compare results of both structures
+ * 6. free memory
+ */
+ GRand *myrand;
+ IntervalData *interval;
+ ECalComponent *comp;
+ ETimezoneCache *zone_cache;
+ GSList *l1, *intervals = NULL;
+ GHashTable *from_intervals;
+ gint num_deleted = 0;
+ gint ii, start, end;
+ gboolean success;
+ GError *error = NULL;
+
+ zone_cache = E_TIMEZONE_CACHE (fixture->cal_cache);
+
+ myrand = g_rand_new ();
+
+ for (ii = 0; ii < NUM_INTERVALS_CLOSED; ii++) {
+ start = g_rand_int_range (myrand, 0, 1000);
+ end = g_rand_int_range (myrand, start, 2000);
+ comp = create_test_component (start, end);
+ g_assert (comp != NULL);
+
+ interval = g_new (IntervalData, 1);
+ interval->start = start;
+ interval->end = end;
+ interval->comp = comp;
+
+ intervals = g_slist_prepend (intervals, interval);
+
+ success = e_cal_cache_put_component (fixture->cal_cache, comp, NULL, E_OFFLINE_STATE_SYNCED,
NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ }
+
+ end = _TIME_MAX;
+
+ /* insert open ended intervals */
+ for (ii = 0; ii < NUM_INTERVALS_OPEN; ii++) {
+ start = g_rand_int_range (myrand, 0, 1000);
+ comp = create_test_component (start, end);
+ g_assert (comp != NULL);
+
+ interval = g_new (IntervalData, 1);
+ interval->start = start;
+ interval->end = end;
+ interval->comp = comp;
+
+ intervals = g_slist_prepend (intervals, interval);
+
+ success = e_cal_cache_put_component (fixture->cal_cache, comp, NULL, E_OFFLINE_STATE_SYNCED,
NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ }
+
+ for (ii = 0; ii < NUM_SEARCHES; ii++) {
+ start = g_rand_int_range (myrand, 0, 1000);
+ end = g_rand_int_range (myrand, 2000, _TIME_MAX);
+
+ l1 = NULL;
+
+ success = e_cal_cache_get_components_in_range (fixture->cal_cache, start, end, &l1, NULL,
&error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ from_intervals = search_in_intervals (zone_cache, intervals, start, end);
+
+ check_search_results (l1, from_intervals);
+
+ g_slist_free_full (l1, g_object_unref);
+ g_hash_table_destroy (from_intervals);
+ }
+
+ /* open-ended intervals */
+ for (ii = 0; ii < 20; ii++) {
+ start = g_rand_int_range (myrand, 0, 1000);
+ end = _TIME_MAX;
+
+ l1 = NULL;
+
+ success = e_cal_cache_get_components_in_range (fixture->cal_cache, start, end, &l1, NULL,
&error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ from_intervals = search_in_intervals (zone_cache, intervals, start, end);
+
+ check_search_results (l1, from_intervals);
+
+ g_slist_free_full (l1, g_object_unref);
+ g_hash_table_destroy (from_intervals);
+ }
+
+ l1 = intervals;
+
+ while (l1) {
+ /* perhaps we will delete l1 */
+ GSList *next = l1->next;
+
+ if (g_rand_double (myrand) < DELETE_PROBABILITY) {
+ ECalComponent *comp;
+ ECalComponentId *id;
+
+ interval = l1->data;
+ comp = interval->comp;
+
+ id = e_cal_component_get_id (comp);
+ g_assert (id != NULL);
+
+ success = e_cal_cache_remove_component (fixture->cal_cache, id->uid, id->rid,
E_OFFLINE_STATE_SYNCED, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ e_cal_component_free_id (id);
+
+ interval_data_free (interval);
+ intervals = g_slist_delete_link (intervals, l1);
+
+ num_deleted++;
+ }
+
+ l1 = next;
+ }
+
+ for (ii = 0; ii < NUM_SEARCHES; ii++) {
+ start = g_rand_int_range (myrand, 0, 1000);
+ end = g_rand_int_range (myrand, start + 1, 2000);
+
+ l1 = NULL;
+
+ success = e_cal_cache_get_components_in_range (fixture->cal_cache, start, end, &l1, NULL,
&error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ from_intervals = search_in_intervals (zone_cache, intervals, start, end);
+
+ check_search_results (l1, from_intervals);
+
+ g_slist_free_full (l1, g_object_unref);
+ g_hash_table_destroy (from_intervals);
+ }
+
+ g_slist_free_full (intervals, interval_data_free);
+ g_rand_free (myrand);
+}
+
+gint
+main (gint argc,
+ gchar **argv)
+{
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+ g_type_init ();
+#endif
+ g_test_init (&argc, &argv, NULL);
+
+ /* Ensure that the client and server get the same locale */
+ g_assert (g_setenv ("LC_ALL", "en_US.UTF-8", TRUE));
+ setlocale (LC_ALL, "");
+
+ g_test_add ("/ECalCache/Intervals", TCUFixture, NULL,
+ tcu_fixture_setup, test_intervals, tcu_fixture_teardown);
+
+ return g_test_run ();
+}
diff --git a/tests/libedata-cal/test-cal-cache-offline.c b/tests/libedata-cal/test-cal-cache-offline.c
new file mode 100644
index 0000000..a831157
--- /dev/null
+++ b/tests/libedata-cal/test-cal-cache-offline.c
@@ -0,0 +1,1043 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libecal/libecal.h>
+
+#include "test-cal-cache-utils.h"
+
+static void
+test_fill_cache (TCUFixture *fixture,
+ ECalComponent **out_component)
+{
+ tcu_add_component_from_test_case (fixture, "event-1", out_component);
+ tcu_add_component_from_test_case (fixture, "event-2", NULL);
+ tcu_add_component_from_test_case (fixture, "event-5", NULL);
+}
+
+enum {
+ EXPECT_DEFAULT = (0),
+ EXPECT_EVENT_1 = (1 << 0),
+ EXPECT_EVENT_2 = (1 << 1),
+ EXPECT_EVENT_3 = (1 << 2),
+ EXPECT_EVENT_4 = (1 << 3),
+ HAS_SEARCH_DATA = (1 << 4),
+ SKIP_COMPONENT_PUT = (1 << 5)
+};
+
+static void
+test_check_search_result (const GSList *list,
+ guint32 flags)
+{
+ gboolean expect_event_1 = (flags & EXPECT_EVENT_1) != 0;
+ gboolean expect_event_2 = (flags & EXPECT_EVENT_2) != 0;
+ gboolean expect_event_3 = (flags & EXPECT_EVENT_3) != 0;
+ gboolean expect_event_4 = (flags & EXPECT_EVENT_4) != 0;
+ gboolean has_search_data = (flags & HAS_SEARCH_DATA) != 0;
+ gboolean have_event_1 = FALSE;
+ gboolean have_event_2 = FALSE;
+ gboolean have_event_3 = FALSE;
+ gboolean have_event_4 = FALSE;
+ gboolean have_event_5 = FALSE;
+ const GSList *link;
+
+ for (link = list; link; link = g_slist_next (link)) {
+ const gchar *uid;
+
+ if (has_search_data) {
+ ECalCacheSearchData *sd = link->data;
+ ECalComponent *component;
+
+ g_assert (sd != NULL);
+ g_assert (sd->uid != NULL);
+ g_assert (sd->object != NULL);
+
+ uid = sd->uid;
+
+ component = e_cal_component_new_from_string (sd->object);
+ g_assert (E_IS_CAL_COMPONENT (component));
+ g_assert_cmpstr (uid, ==, icalcomponent_get_uid (e_cal_component_get_icalcomponent
(component)));
+ g_assert_nonnull (icalcomponent_get_summary (e_cal_component_get_icalcomponent
(component)));
+
+ g_clear_object (&component);
+ } else {
+ const ECalComponentId *id = link->data;
+
+ g_assert (id != NULL);
+ g_assert (id->uid != NULL);
+
+ uid = id->uid;
+ }
+
+ g_assert_nonnull (uid);
+
+ if (g_str_equal (uid, "event-1")) {
+ g_assert (expect_event_1);
+ g_assert (!have_event_1);
+ have_event_1 = TRUE;
+ } else if (g_str_equal (uid, "event-2")) {
+ g_assert (!have_event_2);
+ have_event_2 = TRUE;
+ } else if (g_str_equal (uid, "event-3")) {
+ g_assert (expect_event_3);
+ g_assert (!have_event_3);
+ have_event_3 = TRUE;
+ } else if (g_str_equal (uid, "event-4")) {
+ g_assert (expect_event_4);
+ g_assert (!have_event_4);
+ have_event_4 = TRUE;
+ } else if (g_str_equal (uid, "event-5")) {
+ g_assert (!have_event_5);
+ have_event_5 = TRUE;
+ } else {
+ /* It's not supposed to be NULL, but it will print the value of 'uid' */
+ g_assert_cmpstr (uid, ==, NULL);
+ }
+ }
+
+ g_assert ((expect_event_1 && have_event_1) || (!expect_event_1 && !have_event_1));
+ g_assert ((expect_event_2 && have_event_2) || (!expect_event_2 && !have_event_2));
+ g_assert ((expect_event_3 && have_event_3) || (!expect_event_3 && !have_event_3));
+ g_assert ((expect_event_4 && have_event_4) || (!expect_event_4 && !have_event_4));
+ g_assert (have_event_5);
+}
+
+static void
+test_basic_search (TCUFixture *fixture,
+ guint32 flags)
+{
+ GSList *list = NULL;
+ const gchar *sexp;
+ gint expect_total;
+ GError *error = NULL;
+
+ expect_total = 2 +
+ ((flags & EXPECT_EVENT_1) != 0 ? 1 : 0) +
+ ((flags & EXPECT_EVENT_3) != 0 ? 1 : 0) +
+ ((flags & EXPECT_EVENT_4) != 0 ? 1 : 0);
+
+ /* All components first */
+ g_assert (e_cal_cache_search (fixture->cal_cache, NULL, &list, NULL, &error));
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (list), ==, expect_total);
+ test_check_search_result (list, flags | EXPECT_EVENT_2 | HAS_SEARCH_DATA);
+ g_slist_free_full (list, e_cal_cache_search_data_free);
+ list = NULL;
+
+ g_assert (e_cal_cache_search_ids (fixture->cal_cache, NULL, &list, NULL, &error));
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (list), ==, expect_total);
+ test_check_search_result (list, flags | EXPECT_EVENT_2);
+ g_slist_free_full (list, (GDestroyNotify) e_cal_component_free_id);
+ list = NULL;
+
+ /* Only Party, aka event-5, as an in-summary query */
+ sexp = "(has-categories? \"Holiday\")";
+
+ g_assert (e_cal_cache_search (fixture->cal_cache, sexp, &list, NULL, &error));
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (list), ==, 1);
+ test_check_search_result (list, HAS_SEARCH_DATA);
+ g_slist_free_full (list, e_cal_cache_search_data_free);
+ list = NULL;
+
+ g_assert (e_cal_cache_search_ids (fixture->cal_cache, sexp, &list, NULL, &error));
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (list), ==, 1);
+ test_check_search_result (list, EXPECT_DEFAULT);
+ g_slist_free_full (list, (GDestroyNotify) e_cal_component_free_id);
+ list = NULL;
+
+ /* Only Party, aka event-5, as a non-summarised query */
+ sexp = "(has-alarms-in-range? (make-time \"20091229T230000Z\") (make-time \"20091231T010000Z\"))";
+
+ g_assert (e_cal_cache_search (fixture->cal_cache, sexp, &list, NULL, &error));
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (list), ==, 1);
+ test_check_search_result (list, HAS_SEARCH_DATA);
+ g_slist_free_full (list, e_cal_cache_search_data_free);
+ list = NULL;
+
+ g_assert (e_cal_cache_search_ids (fixture->cal_cache, sexp, &list, NULL, &error));
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (list), ==, 1);
+ test_check_search_result (list, EXPECT_DEFAULT);
+ g_slist_free_full (list, (GDestroyNotify) e_cal_component_free_id);
+ list = NULL;
+
+ /* Invalid expression */
+ g_assert (!e_cal_cache_search (fixture->cal_cache, "invalid expression here", &list, NULL, &error));
+ g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY);
+ g_assert_null (list);
+ g_clear_error (&error);
+
+ g_assert (!e_cal_cache_search_ids (fixture->cal_cache, "invalid expression here", &list, NULL,
&error));
+ g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_INVALID_QUERY);
+ g_assert_null (list);
+ g_clear_error (&error);
+}
+
+/* Expects pairs of UID (gchar *) and EOfflineState (gint), terminated by NULL */
+static void
+test_check_offline_changes (TCUFixture *fixture,
+ ...) G_GNUC_NULL_TERMINATED;
+
+static void
+test_check_offline_changes (TCUFixture *fixture,
+ ...)
+{
+ GSList *changes, *link;
+ va_list args;
+ GHashTable *expects;
+ const gchar *uid;
+ GError *error = NULL;
+
+ changes = e_cache_get_offline_changes (E_CACHE (fixture->cal_cache), NULL, &error);
+
+ g_assert_no_error (error);
+
+ expects = g_hash_table_new (g_str_hash, g_str_equal);
+
+ va_start (args, fixture);
+ uid = va_arg (args, const gchar *);
+ while (uid) {
+ gint state = va_arg (args, gint);
+
+ g_hash_table_insert (expects, (gpointer) uid, GINT_TO_POINTER (state));
+ uid = va_arg (args, const gchar *);
+ }
+ va_end (args);
+
+ g_assert_cmpint (g_slist_length (changes), ==, g_hash_table_size (expects));
+
+ for (link = changes; link; link = g_slist_next (link)) {
+ ECacheOfflineChange *change = link->data;
+ gint expect_state;
+
+ g_assert_nonnull (change);
+ g_assert (g_hash_table_contains (expects, change->uid));
+
+ expect_state = GPOINTER_TO_INT (g_hash_table_lookup (expects, change->uid));
+ g_assert_cmpint (expect_state, ==, change->state);
+ }
+
+ g_slist_free_full (changes, e_cache_offline_change_free);
+ g_hash_table_destroy (expects);
+}
+
+static EOfflineState
+test_check_offline_state (TCUFixture *fixture,
+ const gchar *uid,
+ EOfflineState expect_offline_state)
+{
+ EOfflineState offline_state;
+ GError *error = NULL;
+
+ offline_state = e_cache_get_offline_state (E_CACHE (fixture->cal_cache), uid, NULL, &error);
+ g_assert_cmpint (offline_state, ==, expect_offline_state);
+
+ if (offline_state == E_OFFLINE_STATE_UNKNOWN) {
+ g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+ g_clear_error (&error);
+ } else {
+ g_assert_no_error (error);
+ }
+
+ return offline_state;
+}
+
+static void
+test_check_edit_saved (TCUFixture *fixture,
+ const gchar *uid,
+ const gchar *summ_value)
+{
+ ECalComponent *component = NULL;
+ GError *error = NULL;
+
+ g_assert (e_cal_cache_get_component (fixture->cal_cache, uid, NULL, &component, NULL, &error));
+ g_assert_no_error (error);
+ g_assert_nonnull (component);
+ g_assert_cmpstr (icalcomponent_get_summary (e_cal_component_get_icalcomponent (component)), ==,
summ_value);
+
+ g_clear_object (&component);
+}
+
+static void
+test_verify_storage (TCUFixture *fixture,
+ const gchar *uid,
+ const gchar *expect_summ,
+ const gchar *expect_extra,
+ EOfflineState expect_offline_state)
+{
+ ECalComponent *component = NULL;
+ EOfflineState offline_state;
+ gchar *saved_extra = NULL;
+ GError *error = NULL;
+
+ if (expect_offline_state == E_OFFLINE_STATE_LOCALLY_DELETED ||
+ expect_offline_state == E_OFFLINE_STATE_UNKNOWN) {
+ g_assert (!e_cal_cache_get_component (fixture->cal_cache, uid, NULL, &component, NULL,
&error));
+ g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+ g_assert_null (component);
+
+ g_clear_error (&error);
+ } else {
+ g_assert (e_cal_cache_get_component (fixture->cal_cache, uid, NULL, &component, NULL,
&error));
+ g_assert_no_error (error);
+ g_assert_nonnull (component);
+ }
+
+ offline_state = test_check_offline_state (fixture, uid, expect_offline_state);
+
+ if (offline_state == E_OFFLINE_STATE_UNKNOWN) {
+ g_assert (!e_cal_cache_contains (fixture->cal_cache, uid, NULL, E_CACHE_EXCLUDE_DELETED));
+ g_assert (!e_cal_cache_contains (fixture->cal_cache, uid, NULL, E_CACHE_INCLUDE_DELETED));
+ test_check_offline_changes (fixture, NULL);
+ return;
+ }
+
+ g_assert (e_cal_cache_get_component_extra (fixture->cal_cache, uid, NULL, &saved_extra, NULL,
&error));
+ g_assert_no_error (error);
+
+ g_assert_cmpstr (saved_extra, ==, expect_extra);
+ g_assert_cmpstr (icalcomponent_get_summary (e_cal_component_get_icalcomponent (component)), ==,
expect_summ);
+
+ g_clear_object (&component);
+ g_free (saved_extra);
+
+ if (expect_offline_state == E_OFFLINE_STATE_SYNCED)
+ test_check_offline_changes (fixture, NULL);
+ else
+ test_check_offline_changes (fixture, uid, expect_offline_state, NULL);
+}
+
+static void
+test_offline_basics (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ EOfflineState states[] = {
+ E_OFFLINE_STATE_LOCALLY_CREATED,
+ E_OFFLINE_STATE_LOCALLY_MODIFIED,
+ E_OFFLINE_STATE_LOCALLY_DELETED,
+ E_OFFLINE_STATE_SYNCED
+ };
+ ECalComponent *component = NULL;
+ gint ii;
+ const gchar *uid;
+ gchar *saved_extra = NULL, *tmp;
+ GError *error = NULL;
+
+ /* Basic ECache stuff */
+ e_cache_set_version (E_CACHE (fixture->cal_cache), 123);
+ g_assert_cmpint (e_cache_get_version (E_CACHE (fixture->cal_cache)), ==, 123);
+
+ e_cache_set_revision (E_CACHE (fixture->cal_cache), "rev-321");
+ tmp = e_cache_dup_revision (E_CACHE (fixture->cal_cache));
+ g_assert_cmpstr ("rev-321", ==, tmp);
+ g_free (tmp);
+
+ g_assert (e_cache_set_key (E_CACHE (fixture->cal_cache), "my-key-str", "key-str-value", &error));
+ g_assert_no_error (error);
+
+ tmp = e_cache_dup_key (E_CACHE (fixture->cal_cache), "my-key-str", &error);
+ g_assert_no_error (error);
+ g_assert_cmpstr ("key-str-value", ==, tmp);
+ g_free (tmp);
+
+ g_assert (e_cache_set_key_int (E_CACHE (fixture->cal_cache), "version", 567, &error));
+ g_assert_no_error (error);
+
+ g_assert_cmpint (e_cache_get_key_int (E_CACHE (fixture->cal_cache), "version", &error), ==, 567);
+ g_assert_no_error (error);
+
+ g_assert_cmpint (e_cache_get_version (E_CACHE (fixture->cal_cache)), ==, 123);
+
+ /* Add in online */
+ test_fill_cache (fixture, &component);
+ g_assert_nonnull (component);
+
+ uid = icalcomponent_get_uid (e_cal_component_get_icalcomponent (component));
+ g_assert_nonnull (uid);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ g_assert (e_cal_cache_set_component_extra (fixture->cal_cache, uid, NULL, "extra-0", NULL, &error));
+ g_assert_no_error (error);
+
+ g_assert (e_cal_cache_get_component_extra (fixture->cal_cache, uid, NULL, &saved_extra, NULL,
&error));
+ g_assert_no_error (error);
+ g_assert_cmpstr (saved_extra, ==, "extra-0");
+
+ g_free (saved_extra);
+ saved_extra = NULL;
+
+ icalcomponent_set_summary (e_cal_component_get_icalcomponent (component), "summ-0");
+
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+
+ test_check_offline_changes (fixture, NULL);
+
+ /* Try change status */
+ for (ii = 0; ii < G_N_ELEMENTS (states); ii++) {
+ g_assert (e_cache_set_offline_state (E_CACHE (fixture->cal_cache), uid, states[ii], NULL,
&error));
+ g_assert_no_error (error);
+
+ test_check_offline_state (fixture, uid, states[ii]);
+
+ if (states[ii] != E_OFFLINE_STATE_SYNCED)
+ test_check_offline_changes (fixture, uid, states[ii], NULL);
+
+ if (states[ii] == E_OFFLINE_STATE_LOCALLY_DELETED) {
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache),
E_CACHE_EXCLUDE_DELETED, NULL, &error), ==, 2);
+ g_assert_no_error (error);
+
+ g_assert (!e_cal_cache_contains (fixture->cal_cache, uid, NULL,
E_CACHE_EXCLUDE_DELETED));
+
+ g_assert (e_cal_cache_set_component_extra (fixture->cal_cache, uid, NULL, "extra-1",
NULL, &error));
+ g_assert_no_error (error);
+
+ g_assert (e_cal_cache_get_component_extra (fixture->cal_cache, uid, NULL,
&saved_extra, NULL, &error));
+ g_assert_no_error (error);
+ g_assert_cmpstr (saved_extra, ==, "extra-1");
+
+ g_free (saved_extra);
+ saved_extra = NULL;
+
+ /* Search when locally deleted */
+ test_basic_search (fixture, EXPECT_DEFAULT);
+ } else {
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache),
E_CACHE_EXCLUDE_DELETED, NULL, &error), ==, 3);
+ g_assert_no_error (error);
+
+ g_assert (e_cal_cache_contains (fixture->cal_cache, uid, NULL,
E_CACHE_EXCLUDE_DELETED));
+
+ /* Search when locally available */
+ test_basic_search (fixture, EXPECT_EVENT_1);
+ }
+
+ g_assert (e_cal_cache_contains (fixture->cal_cache, uid, NULL, E_CACHE_INCLUDE_DELETED));
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_INCLUDE_DELETED,
NULL, &error), ==, 3);
+ g_assert_no_error (error);
+ }
+
+ test_check_offline_changes (fixture, NULL);
+
+ /* Edit in online */
+ icalcomponent_set_summary (e_cal_component_get_icalcomponent (component), "summ-1");
+
+ g_assert (e_cal_cache_put_component (fixture->cal_cache, component, NULL, E_CACHE_IS_ONLINE, NULL,
&error));
+ g_assert_no_error (error);
+
+ test_verify_storage (fixture, uid, "summ-1", NULL, E_OFFLINE_STATE_SYNCED);
+ test_check_offline_changes (fixture, NULL);
+
+ icalcomponent_set_summary (e_cal_component_get_icalcomponent (component), "summ-2");
+
+ g_assert (e_cal_cache_put_component (fixture->cal_cache, component, "extra-2", E_CACHE_IS_ONLINE,
NULL, &error));
+ g_assert_no_error (error);
+
+ test_verify_storage (fixture, uid, "summ-2", "extra-2", E_OFFLINE_STATE_SYNCED);
+ test_check_offline_changes (fixture, NULL);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ /* Search before delete */
+ test_basic_search (fixture, EXPECT_EVENT_1);
+
+ /* Delete in online */
+ g_assert (e_cal_cache_remove_component (fixture->cal_cache, uid, NULL, E_CACHE_IS_ONLINE, NULL,
&error));
+ g_assert_no_error (error);
+
+ g_assert (!e_cache_set_offline_state (E_CACHE (fixture->cal_cache), uid,
E_OFFLINE_STATE_LOCALLY_MODIFIED, NULL, &error));
+ g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+ g_clear_error (&error);
+
+ test_verify_storage (fixture, uid, NULL, NULL, E_OFFLINE_STATE_UNKNOWN);
+ test_check_offline_changes (fixture, NULL);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 2);
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_INCLUDE_DELETED, NULL,
&error), ==, 2);
+ g_assert_no_error (error);
+
+ g_assert (!e_cal_cache_set_component_extra (fixture->cal_cache, uid, NULL, "extra-3", NULL, &error));
+ g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+ g_clear_error (&error);
+
+ g_assert (!e_cal_cache_get_component_extra (fixture->cal_cache, uid, NULL, &saved_extra, NULL,
&error));
+ g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+ g_assert_null (saved_extra);
+ g_clear_error (&error);
+
+ g_clear_object (&component);
+
+ /* Search after delete */
+ test_basic_search (fixture, EXPECT_DEFAULT);
+}
+
+static void
+test_offline_add_one (TCUFixture *fixture,
+ const gchar *case_name,
+ gint expect_total,
+ guint32 flags,
+ ECalComponent **out_component)
+{
+ ECalComponent *component = NULL;
+ const gchar *uid;
+ GError *error = NULL;
+
+ if (!(flags & SKIP_COMPONENT_PUT)) {
+ component = tcu_new_component_from_test_case (case_name);
+ g_assert_nonnull (component);
+
+ uid = icalcomponent_get_uid (e_cal_component_get_icalcomponent (component));
+ g_assert_nonnull (uid);
+
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_UNKNOWN);
+
+ /* Add a component in offline */
+ g_assert (e_cal_cache_put_component (fixture->cal_cache, component, NULL, E_CACHE_IS_OFFLINE,
NULL, &error));
+ g_assert_no_error (error);
+ } else {
+ uid = case_name;
+ }
+
+ if ((flags & EXPECT_EVENT_3) != 0) {
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_CREATED);
+ } else {
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_UNKNOWN);
+ }
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, expect_total);
+ g_assert_no_error (error);
+
+ test_basic_search (fixture, flags);
+
+ if (out_component)
+ *out_component = component;
+ else
+ g_clear_object (&component);
+}
+
+static void
+test_offline_add (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ GError *error = NULL;
+
+ /* Add in online */
+ test_fill_cache (fixture, NULL);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_check_offline_changes (fixture, NULL);
+
+ /* Add the first in offline */
+ test_offline_add_one (fixture, "event-3", 4, EXPECT_EVENT_3 | EXPECT_EVENT_1, NULL);
+
+ test_check_offline_changes (fixture,
+ "event-3", E_OFFLINE_STATE_LOCALLY_CREATED,
+ NULL);
+
+ /* Add the second in offline */
+ test_offline_add_one (fixture, "event-4", 5, EXPECT_EVENT_3 | EXPECT_EVENT_4 | EXPECT_EVENT_1, NULL);
+
+ test_check_offline_changes (fixture,
+ "event-3", E_OFFLINE_STATE_LOCALLY_CREATED,
+ "event-4", E_OFFLINE_STATE_LOCALLY_CREATED,
+ NULL);
+}
+
+static void
+test_offline_add_edit (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ ECalComponent *component = NULL;
+ GError *error = NULL;
+
+ /* Add in online */
+ test_fill_cache (fixture, NULL);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_check_offline_changes (fixture, NULL);
+
+ /* Add in offline */
+ test_offline_add_one (fixture, "event-3", 4, EXPECT_EVENT_3 | EXPECT_EVENT_1, &component);
+ g_assert_nonnull (component);
+
+ test_check_offline_changes (fixture,
+ "event-3", E_OFFLINE_STATE_LOCALLY_CREATED,
+ NULL);
+
+ /* Modify added in offline */
+ icalcomponent_set_summary (e_cal_component_get_icalcomponent (component), "summ-2");
+
+ g_assert (e_cal_cache_put_component (fixture->cal_cache, component, NULL, E_CACHE_IS_OFFLINE, NULL,
&error));
+ g_assert_no_error (error);
+
+ test_offline_add_one (fixture, "event-3", 4, EXPECT_EVENT_3 | EXPECT_EVENT_1 | SKIP_COMPONENT_PUT,
NULL);
+
+ test_check_offline_changes (fixture,
+ "event-3", E_OFFLINE_STATE_LOCALLY_CREATED,
+ NULL);
+
+ test_check_edit_saved (fixture, "event-3", "summ-2");
+
+ g_clear_object (&component);
+}
+
+static void
+test_offline_add_delete (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ ECalComponent *component = NULL;
+ const gchar *uid;
+ GError *error = NULL;
+
+ /* Add in online */
+ test_fill_cache (fixture, NULL);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_check_offline_changes (fixture, NULL);
+
+ /* Add in offline */
+ test_offline_add_one (fixture, "event-3", 4, EXPECT_EVENT_3 | EXPECT_EVENT_1, &component);
+ g_assert_nonnull (component);
+
+ test_check_offline_changes (fixture,
+ "event-3", E_OFFLINE_STATE_LOCALLY_CREATED,
+ NULL);
+
+ uid = icalcomponent_get_uid (e_cal_component_get_icalcomponent (component));
+ g_assert_nonnull (uid);
+
+ /* Delete added in offline */
+
+ g_assert (e_cal_cache_remove_component (fixture->cal_cache, uid, NULL, E_CACHE_IS_OFFLINE, NULL,
&error));
+ g_assert_no_error (error);
+
+ test_offline_add_one (fixture, "event-3", 3, EXPECT_EVENT_1 | SKIP_COMPONENT_PUT, NULL);
+
+ test_check_offline_changes (fixture, NULL);
+
+ g_clear_object (&component);
+}
+
+static void
+test_offline_add_delete_add (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ ECalComponent *component = NULL;
+ const gchar *uid;
+ GError *error = NULL;
+
+ /* Add in online */
+ test_fill_cache (fixture, NULL);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_check_offline_changes (fixture, NULL);
+
+ /* Add in offline */
+ test_offline_add_one (fixture, "event-3", 4, EXPECT_EVENT_3 | EXPECT_EVENT_1, &component);
+ g_assert_nonnull (component);
+
+ test_check_offline_changes (fixture,
+ "event-3", E_OFFLINE_STATE_LOCALLY_CREATED,
+ NULL);
+
+ uid = icalcomponent_get_uid (e_cal_component_get_icalcomponent (component));
+ g_assert_nonnull (uid);
+
+ /* Delete added in offline */
+ g_assert (e_cal_cache_remove_component (fixture->cal_cache, uid, NULL, E_CACHE_IS_OFFLINE, NULL,
&error));
+ g_assert_no_error (error);
+
+ test_offline_add_one (fixture, "event-3", 3, EXPECT_EVENT_1 | SKIP_COMPONENT_PUT, NULL);
+
+ test_check_offline_changes (fixture, NULL);
+
+ g_clear_object (&component);
+
+ /* Add in offline again */
+ test_offline_add_one (fixture, "event-3", 4, EXPECT_EVENT_3 | EXPECT_EVENT_1, NULL);
+
+ test_check_offline_changes (fixture,
+ "event-3", E_OFFLINE_STATE_LOCALLY_CREATED,
+ NULL);
+}
+
+static void
+test_offline_add_resync (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ GError *error = NULL;
+
+ /* Add in online */
+ test_fill_cache (fixture, NULL);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_check_offline_changes (fixture, NULL);
+
+ /* Add in offline */
+ test_offline_add_one (fixture, "event-3", 4, EXPECT_EVENT_3 | EXPECT_EVENT_1, NULL);
+
+ test_check_offline_changes (fixture,
+ "event-3", E_OFFLINE_STATE_LOCALLY_CREATED,
+ NULL);
+
+ /* Resync all offline changes */
+ g_assert (e_cache_clear_offline_changes (E_CACHE (fixture->cal_cache), NULL, &error));
+ g_assert_no_error (error);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 4);
+ g_assert_no_error (error);
+
+ test_basic_search (fixture, EXPECT_EVENT_3 | EXPECT_EVENT_1);
+ test_check_offline_changes (fixture, NULL);
+ test_check_offline_state (fixture, "event-3", E_OFFLINE_STATE_SYNCED);
+}
+
+static void
+test_offline_edit_common (TCUFixture *fixture,
+ gchar **out_uid)
+{
+ ECalComponent *component = NULL;
+ const gchar *uid;
+ GError *error = NULL;
+
+ /* Add in online */
+ test_fill_cache (fixture, &component);
+ g_assert_nonnull (component);
+
+ uid = icalcomponent_get_uid (e_cal_component_get_icalcomponent (component));
+ g_assert_nonnull (uid);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_check_offline_changes (fixture, NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+
+ /* Modify in offline */
+ icalcomponent_set_summary (e_cal_component_get_icalcomponent (component), "summ-2");
+
+ g_assert (e_cal_cache_put_component (fixture->cal_cache, component, NULL, E_CACHE_IS_OFFLINE, NULL,
&error));
+ g_assert_no_error (error);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_check_edit_saved (fixture, uid, "summ-2");
+
+ test_basic_search (fixture, EXPECT_EVENT_1);
+ test_check_offline_changes (fixture,
+ uid, E_OFFLINE_STATE_LOCALLY_MODIFIED,
+ NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_MODIFIED);
+
+ if (out_uid)
+ *out_uid = g_strdup (uid);
+
+ g_clear_object (&component);
+}
+
+static void
+test_offline_edit (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ test_offline_edit_common (fixture, NULL);
+}
+
+static void
+test_offline_edit_delete (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ ECalComponent *component = NULL;
+ gchar *uid = NULL;
+ GError *error = NULL;
+
+ test_offline_edit_common (fixture, &uid);
+
+ /* Delete the modified component in offline */
+ g_assert (e_cal_cache_remove_component (fixture->cal_cache, uid, NULL, E_CACHE_IS_OFFLINE, NULL,
&error));
+ g_assert_no_error (error);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 2);
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_INCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_basic_search (fixture, EXPECT_DEFAULT);
+ test_check_offline_changes (fixture,
+ uid, E_OFFLINE_STATE_LOCALLY_DELETED,
+ NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_DELETED);
+
+ g_assert (!e_cal_cache_get_component (fixture->cal_cache, uid, FALSE, &component, NULL, &error));
+ g_assert_error (error, E_CACHE_ERROR, E_CACHE_ERROR_NOT_FOUND);
+ g_assert_null (component);
+
+ g_clear_error (&error);
+ g_free (uid);
+}
+
+static void
+test_offline_edit_resync (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ gchar *uid = NULL;
+ GError *error = NULL;
+
+ test_offline_edit_common (fixture, &uid);
+
+ /* Resync all offline changes */
+ g_assert (e_cache_clear_offline_changes (E_CACHE (fixture->cal_cache), NULL, &error));
+ g_assert_no_error (error);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_basic_search (fixture, EXPECT_EVENT_1);
+ test_check_offline_changes (fixture, NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+
+ g_free (uid);
+}
+
+static void
+test_offline_delete (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ ECalComponent *component = NULL;
+ const gchar *uid;
+ GError *error = NULL;
+
+ /* Add in online */
+ test_fill_cache (fixture, &component);
+ g_assert_nonnull (component);
+
+ uid = icalcomponent_get_uid (e_cal_component_get_icalcomponent (component));
+ g_assert_nonnull (uid);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_check_offline_changes (fixture, NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+
+ /* Delete in offline */
+ g_assert (e_cal_cache_remove_component (fixture->cal_cache, uid, NULL, E_CACHE_IS_OFFLINE, NULL,
&error));
+ g_assert_no_error (error);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 2);
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_INCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_basic_search (fixture, EXPECT_DEFAULT);
+ test_check_offline_changes (fixture,
+ uid, E_OFFLINE_STATE_LOCALLY_DELETED,
+ NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_DELETED);
+
+ g_clear_object (&component);
+}
+
+static void
+test_offline_delete_add (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ ECalComponent *component = NULL;
+ const gchar *uid;
+ GError *error = NULL;
+
+ /* Add in online */
+ test_fill_cache (fixture, &component);
+ g_assert_nonnull (component);
+
+ uid = icalcomponent_get_uid (e_cal_component_get_icalcomponent (component));
+ g_assert_nonnull (uid);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_check_offline_changes (fixture, NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+
+ /* Delete locally created in offline */
+ test_offline_add_one (fixture, "event-3", 4, EXPECT_EVENT_3 | EXPECT_EVENT_1, NULL);
+ g_assert (e_cal_cache_remove_component (fixture->cal_cache, "event-3", NULL, E_CACHE_IS_OFFLINE,
NULL, &error));
+ g_assert_no_error (error);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_INCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_basic_search (fixture, EXPECT_EVENT_1);
+ test_check_offline_changes (fixture, NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+ test_check_offline_state (fixture, "event-3", E_OFFLINE_STATE_UNKNOWN);
+
+ /* Delete synced in offline */
+ g_assert (e_cal_cache_remove_component (fixture->cal_cache, uid, NULL, E_CACHE_IS_OFFLINE, NULL,
&error));
+ g_assert_no_error (error);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 2);
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_INCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_basic_search (fixture, EXPECT_DEFAULT);
+ test_check_offline_changes (fixture,
+ uid, E_OFFLINE_STATE_LOCALLY_DELETED,
+ NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_DELETED);
+
+ /* Add one in offline */
+ test_offline_add_one (fixture, "event-3", 3, EXPECT_EVENT_3, NULL);
+
+ test_check_offline_changes (fixture,
+ uid, E_OFFLINE_STATE_LOCALLY_DELETED,
+ "event-3", E_OFFLINE_STATE_LOCALLY_CREATED,
+ NULL);
+
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_DELETED);
+ test_check_offline_state (fixture, "event-3", E_OFFLINE_STATE_LOCALLY_CREATED);
+
+ /* Modify the previous component and add it again */
+ icalcomponent_set_summary (e_cal_component_get_icalcomponent (component), "summ-3");
+
+ g_assert (e_cal_cache_put_component (fixture->cal_cache, component, NULL, E_CACHE_IS_OFFLINE, NULL,
&error));
+ g_assert_no_error (error);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 4);
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_INCLUDE_DELETED, NULL,
&error), ==, 4);
+ g_assert_no_error (error);
+
+ test_check_edit_saved (fixture, uid, "summ-3");
+
+ test_basic_search (fixture, EXPECT_EVENT_1 | EXPECT_EVENT_3);
+ test_check_offline_changes (fixture,
+ uid, E_OFFLINE_STATE_LOCALLY_MODIFIED,
+ "event-3", E_OFFLINE_STATE_LOCALLY_CREATED,
+ NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_MODIFIED);
+ test_check_offline_state (fixture, "event-3", E_OFFLINE_STATE_LOCALLY_CREATED);
+
+ g_clear_object (&component);
+}
+
+static void
+test_offline_delete_resync (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ ECalComponent *component = NULL;
+ const gchar *uid;
+ GError *error = NULL;
+
+ /* Add in online */
+ test_fill_cache (fixture, &component);
+ g_assert_nonnull (component);
+
+ uid = icalcomponent_get_uid (e_cal_component_get_icalcomponent (component));
+ g_assert_nonnull (uid);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_check_offline_changes (fixture, NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_SYNCED);
+
+ /* Delete in offline */
+ g_assert (e_cal_cache_remove_component (fixture->cal_cache, uid, NULL, E_CACHE_IS_OFFLINE, NULL,
&error));
+ g_assert_no_error (error);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 2);
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_INCLUDE_DELETED, NULL,
&error), ==, 3);
+ g_assert_no_error (error);
+
+ test_basic_search (fixture, EXPECT_DEFAULT);
+ test_check_offline_changes (fixture,
+ uid, E_OFFLINE_STATE_LOCALLY_DELETED,
+ NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_LOCALLY_DELETED);
+
+ /* Resync all offline changes */
+ e_cache_clear_offline_changes (E_CACHE (fixture->cal_cache), NULL, &error);
+ g_assert_no_error (error);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_EXCLUDE_DELETED, NULL,
&error), ==, 2);
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_INCLUDE_DELETED, NULL,
&error), ==, 2);
+ g_assert_no_error (error);
+
+ test_basic_search (fixture, EXPECT_DEFAULT);
+ test_check_offline_changes (fixture, NULL);
+ test_check_offline_state (fixture, uid, E_OFFLINE_STATE_UNKNOWN);
+
+ g_clear_object (&component);
+}
+
+gint
+main (gint argc,
+ gchar **argv)
+{
+ TCUClosure closure = { TCU_LOAD_COMPONENT_SET_NONE };
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+ g_type_init ();
+#endif
+ g_test_init (&argc, &argv, NULL);
+
+ /* Ensure that the client and server get the same locale */
+ g_assert (g_setenv ("LC_ALL", "en_US.UTF-8", TRUE));
+ setlocale (LC_ALL, "");
+
+ g_test_add ("/ECalCache/Offline/Basics", TCUFixture, &closure,
+ tcu_fixture_setup, test_offline_basics, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Offline/Add", TCUFixture, &closure,
+ tcu_fixture_setup, test_offline_add, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Offline/AddEdit", TCUFixture, &closure,
+ tcu_fixture_setup, test_offline_add_edit, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Offline/AddDelete", TCUFixture, &closure,
+ tcu_fixture_setup, test_offline_add_delete, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Offline/AddDeleteAdd", TCUFixture, &closure,
+ tcu_fixture_setup, test_offline_add_delete_add, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Offline/AddResync", TCUFixture, &closure,
+ tcu_fixture_setup, test_offline_add_resync, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Offline/Edit", TCUFixture, &closure,
+ tcu_fixture_setup, test_offline_edit, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Offline/EditDelete", TCUFixture, &closure,
+ tcu_fixture_setup, test_offline_edit_delete, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Offline/EditResync", TCUFixture, &closure,
+ tcu_fixture_setup, test_offline_edit_resync, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Offline/Delete", TCUFixture, &closure,
+ tcu_fixture_setup, test_offline_delete, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Offline/DeleteAdd", TCUFixture, &closure,
+ tcu_fixture_setup, test_offline_delete_add, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Offline/DeleteResync", TCUFixture, &closure,
+ tcu_fixture_setup, test_offline_delete_resync, tcu_fixture_teardown);
+
+ return g_test_run ();
+}
diff --git a/tests/libedata-cal/test-cal-cache-search.c b/tests/libedata-cal/test-cal-cache-search.c
new file mode 100644
index 0000000..ac662b7
--- /dev/null
+++ b/tests/libedata-cal/test-cal-cache-search.c
@@ -0,0 +1,473 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <locale.h>
+#include <libecal/libecal.h>
+
+#include "test-cal-cache-utils.h"
+
+#define dd(x)
+
+static GHashTable *
+test_search_manual (ECalCache *cal_cache,
+ const gchar *expr)
+{
+ GSList *components = NULL, *link;
+ GHashTable *res;
+ ECalBackendSExp *sexp;
+ ETimezoneCache *zone_cache;
+ gboolean success;
+ GError *error = NULL;
+
+ res = g_hash_table_new_full ((GHashFunc) e_cal_component_id_hash, (GEqualFunc)
e_cal_component_id_equal,
+ (GDestroyNotify) e_cal_component_free_id, g_object_unref);
+
+ zone_cache = E_TIMEZONE_CACHE (cal_cache);
+
+ /* Get all the components stored in the summary. */
+ success = e_cal_cache_search_components (cal_cache, NULL, &components, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_nonnull (components);
+
+ sexp = e_cal_backend_sexp_new (expr);
+ g_assert (sexp != NULL);
+
+ for (link = components; link; link = g_slist_next (link)) {
+ ECalComponent *comp = link->data;
+
+ if (e_cal_backend_sexp_match_comp (sexp, comp, zone_cache)) {
+ ECalComponentId *id = NULL;
+
+ id = e_cal_component_get_id (comp);
+ g_assert_nonnull (id);
+
+ g_hash_table_insert (res, id, g_object_ref (comp));
+ }
+ }
+
+ g_slist_free_full (components, g_object_unref);
+ g_object_unref (sexp);
+
+ return res;
+}
+
+#if dd(1)+0
+static void
+test_search_dump_results (GSList *search_data,
+ GHashTable *should_be)
+{
+ GSList *link;
+ GHashTableIter iter;
+ gpointer key;
+ gint ii;
+
+ printf (" Found %d in ECalCache:\n", g_slist_length (search_data));
+ for (ii = 0, link = search_data; link; link = g_slist_next (link), ii++) {
+ ECalCacheSearchData *sd = link->data;
+
+ printf (" [%d]: %s%s%s\n", ii, sd->uid, sd->rid ? ", " : "", sd->rid ? sd->rid : "");
+ }
+
+ printf ("\n");
+ printf (" Found %d in traverse:\n", g_hash_table_size (should_be));
+
+ ii = 0;
+ g_hash_table_iter_init (&iter, should_be);
+ while (g_hash_table_iter_next (&iter, &key, NULL)) {
+ ECalComponentId *id = key;
+
+ printf (" [%d]: %s%s%s\n", ii, id->uid, id->rid ? ", " : "", id->rid ? id->rid : "");
+ ii++;
+ }
+
+ printf ("\n");
+}
+#endif
+
+static void
+test_search_result_equal (GSList *items,
+ GHashTable *should_be,
+ gboolean (* check_cb) (GHashTable *should_be, gpointer item_data))
+{
+ GSList *link;
+
+ g_assert_cmpint (g_slist_length (items), ==, g_hash_table_size (should_be));
+
+ for (link = items; link; link = g_slist_next (link)) {
+ g_assert (check_cb (should_be, link->data));
+ }
+}
+
+static gboolean
+search_data_check_cb (GHashTable *should_be,
+ gpointer item_data)
+{
+ ECalCacheSearchData *sd = item_data;
+ ECalComponentId id;
+
+ g_assert (sd != NULL);
+ g_assert (sd->uid != NULL);
+
+ id.uid = sd->uid;
+ id.rid = sd->rid;
+
+ return g_hash_table_contains (should_be, &id);
+}
+
+static gboolean
+component_check_cb (GHashTable *should_be,
+ gpointer item_data)
+{
+ ECalComponent *comp = item_data;
+ ECalComponentId *id;
+ gboolean contains;
+
+ g_assert (comp != NULL);
+
+ id = e_cal_component_get_id (comp);
+
+ g_assert (id != NULL);
+ g_assert (id->uid != NULL);
+
+ contains = g_hash_table_contains (should_be, id);
+
+ e_cal_component_free_id (id);
+
+ return contains;
+}
+
+static gboolean
+id_check_cb (GHashTable *should_be,
+ gpointer item_data)
+{
+ ECalComponentId *id = item_data;
+
+ g_assert (id != NULL);
+ g_assert (id->uid != NULL);
+
+ return g_hash_table_contains (should_be, id);
+}
+
+static void
+test_search_expr (TCUFixture *fixture,
+ const gchar *expr,
+ const gchar *expects)
+{
+ GSList *items = NULL;
+ GHashTable *should_be;
+ gboolean success;
+ GError *error = NULL;
+
+ should_be = test_search_manual (fixture->cal_cache, expr);
+ g_assert_nonnull (should_be);
+
+ success = e_cal_cache_search (fixture->cal_cache, expr, &items, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ dd (test_search_dump_results (items, should_be));
+
+ test_search_result_equal (items, should_be, search_data_check_cb);
+
+ g_slist_free_full (items, e_cal_cache_search_data_free);
+ items = NULL;
+
+ success = e_cal_cache_search_components (fixture->cal_cache, expr, &items, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ test_search_result_equal (items, should_be, component_check_cb);
+
+ g_slist_free_full (items, g_object_unref);
+ items = NULL;
+
+ success = e_cal_cache_search_ids (fixture->cal_cache, expr, &items, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ if (expects) {
+ GSList *link;
+ gboolean negate = *expects == '!';
+
+ if (negate)
+ expects++;
+
+ for (link = items; link; link = g_slist_next (link)) {
+ ECalComponentId *id = link->data;
+
+ if (g_strcmp0 (id->uid, expects) == 0)
+ break;
+ }
+
+ if (link && negate)
+ g_error ("Found '%s' in result of '%s', though it should not be there", expects,
expr);
+ else if (!link && !negate)
+ g_error ("Not found '%s' in result of '%s', though it should be there", expects,
expr);
+ }
+
+ test_search_result_equal (items, should_be, id_check_cb);
+
+ g_slist_free_full (items, (GDestroyNotify) e_cal_component_free_id);
+
+ g_hash_table_destroy (should_be);
+}
+
+static void
+test_search (TCUFixture *fixture,
+ const gchar *expr,
+ const gchar *expects)
+{
+ gchar *not_expr;
+
+ test_search_expr (fixture, expr, expects);
+
+ not_expr = g_strdup_printf ("(not (%s))", expr);
+ test_search_expr (fixture, not_expr, NULL);
+ g_free (not_expr);
+}
+
+static void
+test_search_uid (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ test_search (fixture, "(uid? \"event-3\")", "event-3");
+ test_search (fixture, "(uid? \"event-6\")", "event-6");
+ test_search (fixture, "(or (uid? \"event-3\") (uid? \"event-6\"))", "event-3");
+ test_search (fixture, "(and (uid? \"event-3\") (uid? \"event-6\"))", "!event-3");
+}
+
+static void
+test_search_occur_in_time_range (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ test_search (fixture, "(occur-in-time-range? (make-time \"20010101T000000Z\") (make-time
\"20010101T010000Z\"))", "!event-1");
+ test_search (fixture, "(occur-in-time-range? (make-time \"20170209T000000Z\") (make-time
\"20170210T000000Z\"))", "event-1");
+ test_search (fixture, "(occur-in-time-range? (make-time \"20170209T020000Z\") (make-time
\"20170209T023000Z\"))", "event-1");
+ test_search (fixture, "(occur-in-time-range? (make-time \"20111231T000000Z\") (make-time
\"20111231T595959Z\"))", "event-5");
+ test_search (fixture, "(occur-in-time-range? (make-time \"20170225T210100Z\") (make-time
\"20170225T210200Z\") \"America/New_York\")", "event-8");
+ test_search (fixture, "(occur-in-time-range? (make-time \"20170225T150100Z\") (make-time
\"20170225T150200Z\") \"Europe/Berlin\")", "event-8");
+ test_search (fixture, "(occur-in-time-range? (make-time \"20170225T160100Z\") (make-time
\"20170225T160200Z\") \"UTC\")", "event-8");
+
+ /* event-6 */
+ test_search (fixture, "(occur-in-time-range? (make-time \"20170221T180000Z\") (make-time
\"20170221T190000Z\"))", "event-6");
+ test_search (fixture, "(occur-in-time-range? (make-time \"20170221T180000Z\") (make-time
\"20170221T190000Z\") \"America/New_York\")", "event-6");
+ test_search (fixture, "(occur-in-time-range? (make-time \"20170221T200000Z\") (make-time
\"20170221T210000Z\") \"Europe/Berlin\")", "!event-6");
+ test_search (fixture, "(occur-in-time-range? (make-time \"20170221T180000Z\") (make-time
\"20170221T190000Z\") \"Europe/Berlin\")", "event-6");
+}
+
+static void
+test_search_due_in_time_range (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ test_search (fixture, "(due-in-time-range? (make-time \"20170101T000000Z\") (make-time
\"20170101T010000Z\"))", "!task-4");
+ test_search (fixture, "(due-in-time-range? (make-time \"20170228T000000Z\") (make-time
\"20170302T000000Z\"))", "task-4");
+}
+
+static void
+test_search_contains (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ const TCUClosure *closure = user_data;
+ gboolean searches_events = closure && closure->load_set == TCU_LOAD_COMPONENT_SET_EVENTS;
+
+ test_search (fixture, "(contains? \"any\" \"party\")", searches_events ? "event-5" : NULL);
+ test_search (fixture, "(contains? \"comment\" \"mentar\")", searches_events ? "event-8" : "task-6");
+ test_search (fixture, "(contains? \"description\" \"with\")", searches_events ? "event-1" : "task-3");
+ test_search (fixture, "(contains? \"summary\" \"meet\")", searches_events ? "event-8" : NULL);
+ test_search (fixture, "(contains? \"location\" \"kitchen\")", searches_events ? "event-3" : "task-5");
+ test_search (fixture, "(contains? \"attendee\" \"CharLie\")", searches_events ? "event-9" : NULL);
+ test_search (fixture, "(contains? \"organizer\" \"bOb\")", searches_events ? "event-8" : NULL);
+ test_search (fixture, "(contains? \"classification\" \"Public\")", searches_events ? "event-4" :
"task-4");
+ test_search (fixture, "(contains? \"classification\" \"Private\")", searches_events ? "event-3" :
"task-7");
+ test_search (fixture, "(contains? \"classification\" \"Confidential\")", searches_events ? "event-2"
: "task-6");
+ test_search (fixture, "(contains? \"status\" \"NOT STARTED\")", searches_events ? NULL : "task-1");
+ test_search (fixture, "(contains? \"status\" \"COMPLETED\")", searches_events ? NULL : "task-4");
+ test_search (fixture, "(contains? \"status\" \"CANCELLED\")", searches_events ? NULL : "task-8");
+ test_search (fixture, "(contains? \"status\" \"IN PROGRESS\")", searches_events ? NULL : "task-7");
+ test_search (fixture, "(contains? \"priority\" \"HIGH\")", searches_events ? NULL : "task-7");
+ test_search (fixture, "(contains? \"priority\" \"NORMAL\")", searches_events ? NULL : "task-6");
+ test_search (fixture, "(contains? \"priority\" \"LOW\")", searches_events ? NULL : "task-5");
+ test_search (fixture, "(contains? \"priority\" \"UNDEFINED\")", searches_events ? NULL : "task-1");
+}
+
+static void
+test_search_has_start (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ const TCUClosure *closure = user_data;
+ gboolean searches_events = closure && closure->load_set == TCU_LOAD_COMPONENT_SET_EVENTS;
+
+ test_search (fixture, "(has-start?)", searches_events ? "event-1" : "task-9");
+ test_search (fixture, "(has-start?)", searches_events ? "event-1" : "!task-8");
+ test_search (fixture, "(not (has-start?))", searches_events ? "!event-1" : "!task-9");
+ test_search (fixture, "(not (has-start?))", searches_events ? "!event-1" : "task-8");
+}
+
+static void
+test_search_has_alarms (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ const TCUClosure *closure = user_data;
+ gboolean searches_events = closure && closure->load_set == TCU_LOAD_COMPONENT_SET_EVENTS;
+
+ test_search (fixture, "(has-alarms?)", searches_events ? "event-1" : "task-7");
+ test_search (fixture, "(has-alarms?)", searches_events ? "event-1" : "!task-6");
+ test_search (fixture, "(not (has-alarms?))", searches_events ? "!event-1" : "!task-7");
+ test_search (fixture, "(not (has-alarms?))", searches_events ? "!event-1" : "task-6");
+}
+
+static void
+test_search_has_alarms_in_range (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ const TCUClosure *closure = user_data;
+ gboolean searches_events = closure && closure->load_set == TCU_LOAD_COMPONENT_SET_EVENTS;
+
+ test_search (fixture, "(has-alarms-in-range? (make-time \"20091229T230000Z\") (make-time
\"20091231T010000Z\"))",
+ searches_events ? "event-5" : "!task-7");
+}
+
+static void
+test_search_has_recurrences (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ test_search (fixture, "(has-recurrences?)", "event-6");
+ test_search (fixture, "(not (has-recurrences?))", "!event-6");
+}
+
+static void
+test_search_has_categories (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ test_search (fixture, "(has-categories? #f)", "!event-2");
+ test_search (fixture, "(has-categories? \"Holiday\")", "event-7");
+ test_search (fixture, "(has-categories? \"Hard\" \"Work\")", "event-2");
+ test_search (fixture, "(has-categories? \"Hard\" \"Work\")", "!event-4");
+}
+
+static void
+test_search_is_completed (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ test_search (fixture, "(is-completed?)", "task-4");
+ test_search (fixture, "(is-completed?)", "!task-5");
+ test_search (fixture, "(not (is-completed?))", "!task-4");
+}
+
+static void
+test_search_completed_before (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ test_search (fixture, "(completed-before? (make-time \"20170221T000000Z\"))", "!task-4");
+ test_search (fixture, "(completed-before? (make-time \"20170222T000000Z\"))", "task-4");
+}
+
+static void
+test_search_has_attachments (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ test_search (fixture, "(has-attachments?)", "event-7");
+ test_search (fixture, "(not (has-attachments?))", "!event-7");
+}
+
+static void
+test_search_percent_complete (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ test_search (fixture, "(< (percent-complete?) 30)", "task-5");
+ test_search (fixture, "(< (percent-complete?) 30)", "!task-7");
+}
+
+static void
+test_search_occurrences_count (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ test_search (fixture, "(and (= (occurrences-count?) 1) (occur-in-time-range? (make-time
\"20170209T000000Z\") (make-time \"20170210T000000Z\")))", "event-1");
+ test_search (fixture, "(= (occurrences-count? (make-time \"20170209T000000Z\") (make-time
\"20170210T000000Z\")) 1)", "event-1");
+}
+
+static void
+test_search_complex (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ test_search (fixture,
+ "(or "
+ "(and (not (is-completed?)) (has-start?) (not (has-alarms?)))"
+ "(contains? \"summary\" \"-on-\")"
+ "(has-attachments?)"
+ ")", "event-3");
+}
+
+gint
+main (gint argc,
+ gchar **argv)
+{
+ TCUClosure closure_events = { TCU_LOAD_COMPONENT_SET_EVENTS };
+ TCUClosure closure_tasks = { TCU_LOAD_COMPONENT_SET_TASKS };
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+ g_type_init ();
+#endif
+ g_test_init (&argc, &argv, NULL);
+
+ /* Ensure that the client and server get the same locale */
+ g_assert (g_setenv ("LC_ALL", "en_US.UTF-8", TRUE));
+ setlocale (LC_ALL, "");
+
+ g_test_add ("/ECalCache/Search/Uid", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_search_uid, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Search/OccurInTimeRange", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_search_occur_in_time_range, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Search/DueInTimeRange", TCUFixture, &closure_tasks,
+ tcu_fixture_setup, test_search_due_in_time_range, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Search/Contains/Events", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_search_contains, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Search/Contains/Tasks", TCUFixture, &closure_tasks,
+ tcu_fixture_setup, test_search_contains, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Search/HasStart/Events", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_search_has_start, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Search/HasStart/Tasks", TCUFixture, &closure_tasks,
+ tcu_fixture_setup, test_search_has_start, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Search/HasAlarms/Events", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_search_has_alarms, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Search/HasAlarms/Tasks", TCUFixture, &closure_tasks,
+ tcu_fixture_setup, test_search_has_alarms, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Search/HasAlarmsInRange/Events", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_search_has_alarms_in_range, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Search/HasAlarmsInRange/Tasks", TCUFixture, &closure_tasks,
+ tcu_fixture_setup, test_search_has_alarms_in_range, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Search/HasRecurrences", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_search_has_recurrences, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Search/HasCategories", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_search_has_categories, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Search/IsCompleted", TCUFixture, &closure_tasks,
+ tcu_fixture_setup, test_search_is_completed, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Search/CompletedBefore", TCUFixture, &closure_tasks,
+ tcu_fixture_setup, test_search_completed_before, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Search/HasAttachments", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_search_has_attachments, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Search/PercentComplete", TCUFixture, &closure_tasks,
+ tcu_fixture_setup, test_search_percent_complete, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Search/OccurrencesCount", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_search_occurrences_count, tcu_fixture_teardown);
+ g_test_add ("/ECalCache/Search/Complex", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_search_complex, tcu_fixture_teardown);
+
+ return g_test_run ();
+}
diff --git a/tests/libedata-cal/test-cal-cache-utils.c b/tests/libedata-cal/test-cal-cache-utils.c
new file mode 100644
index 0000000..c109466
--- /dev/null
+++ b/tests/libedata-cal/test-cal-cache-utils.c
@@ -0,0 +1,180 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "evolution-data-server-config.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+
+#include "test-cal-cache-utils.h"
+
+static void
+delete_work_directory (const gchar *filename)
+{
+ /* XXX Instead of complex error checking here, we should ideally use
+ * a recursive GDir / g_unlink() function.
+ *
+ * We cannot use GFile and the recursive delete function without
+ * corrupting our contained D-Bus environment with service files
+ * from the OS.
+ */
+ const gchar *argv[] = { "/bin/rm", "-rf", filename, NULL };
+ gboolean spawn_succeeded;
+ gint exit_status;
+
+ spawn_succeeded = g_spawn_sync (
+ NULL, (gchar **) argv, NULL, 0, NULL, NULL,
+ NULL, NULL, &exit_status, NULL);
+
+ g_assert (spawn_succeeded);
+ #ifndef G_OS_WIN32
+ g_assert (WIFEXITED (exit_status));
+ g_assert_cmpint (WEXITSTATUS (exit_status), ==, 0);
+ #else
+ g_assert_cmpint (exit_status, ==, 0);
+ #endif
+}
+
+void
+tcu_fixture_setup (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ const TCUClosure *closure = user_data;
+ gchar *filename, *directory;
+ GError *error = NULL;
+
+ if (!g_file_test (CAMEL_PROVIDERDIR, G_FILE_TEST_IS_DIR | G_FILE_TEST_EXISTS)) {
+ if (g_mkdir_with_parents (CAMEL_PROVIDERDIR, 0700) == -1)
+ g_warning ("%s: Failed to create folder '%s': %s\n", G_STRFUNC, CAMEL_PROVIDERDIR,
g_strerror (errno));
+ }
+
+ /* Cleanup from last test */
+ directory = g_build_filename (g_get_tmp_dir (), "test-cal-cache", NULL);
+ delete_work_directory (directory);
+ g_free (directory);
+ filename = g_build_filename (g_get_tmp_dir (), "test-cal-cache", "cache.db", NULL);
+
+ fixture->cal_cache = e_cal_cache_new (filename, NULL, &error);
+
+ if (!fixture->cal_cache)
+ g_error ("Failed to create the ECalCache: %s", error->message);
+
+ g_free (filename);
+
+ if (closure) {
+ if (closure->load_set == TCU_LOAD_COMPONENT_SET_EVENTS) {
+ tcu_add_component_from_test_case (fixture, "event-1", NULL);
+ tcu_add_component_from_test_case (fixture, "event-2", NULL);
+ tcu_add_component_from_test_case (fixture, "event-3", NULL);
+ tcu_add_component_from_test_case (fixture, "event-4", NULL);
+ tcu_add_component_from_test_case (fixture, "event-5", NULL);
+ tcu_add_component_from_test_case (fixture, "event-6", NULL);
+ tcu_add_component_from_test_case (fixture, "event-6-a", NULL);
+ tcu_add_component_from_test_case (fixture, "event-7", NULL);
+ tcu_add_component_from_test_case (fixture, "event-8", NULL);
+ tcu_add_component_from_test_case (fixture, "event-9", NULL);
+ } else if (closure->load_set == TCU_LOAD_COMPONENT_SET_TASKS) {
+ tcu_add_component_from_test_case (fixture, "task-1", NULL);
+ tcu_add_component_from_test_case (fixture, "task-2", NULL);
+ tcu_add_component_from_test_case (fixture, "task-3", NULL);
+ tcu_add_component_from_test_case (fixture, "task-4", NULL);
+ tcu_add_component_from_test_case (fixture, "task-5", NULL);
+ tcu_add_component_from_test_case (fixture, "task-6", NULL);
+ tcu_add_component_from_test_case (fixture, "task-7", NULL);
+ tcu_add_component_from_test_case (fixture, "task-8", NULL);
+ tcu_add_component_from_test_case (fixture, "task-9", NULL);
+ }
+ }
+}
+
+void
+tcu_fixture_teardown (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ g_object_unref (fixture->cal_cache);
+}
+
+gchar *
+tcu_new_icalstring_from_test_case (const gchar *case_name)
+{
+ gchar *filename;
+ gchar *case_filename;
+ GFile * file;
+ GError *error = NULL;
+ gchar *icalstring = NULL;
+
+ case_filename = g_strdup_printf ("%s.ics", case_name);
+
+ /* In the case of installed tests, they run in ${pkglibexecdir}/installed-tests
+ * and the components are installed in ${pkglibexecdir}/installed-tests/components
+ */
+ if (g_getenv ("TEST_INSTALLED_SERVICES") != NULL)
+ filename = g_build_filename (INSTALLED_TEST_DIR, "components", case_filename, NULL);
+ else
+ filename = g_build_filename (SRCDIR, "..", "libedata-cal", "components", case_filename, NULL);
+
+ file = g_file_new_for_path (filename);
+ if (!g_file_load_contents (file, NULL, &icalstring, NULL, NULL, &error))
+ g_error (
+ "Failed to read test iCal string file '%s': %s",
+ filename, error->message);
+
+ g_free (case_filename);
+ g_free (filename);
+ g_object_unref (file);
+
+ return icalstring;
+}
+
+ECalComponent *
+tcu_new_component_from_test_case (const gchar *case_name)
+{
+ gchar *icalstring;
+ ECalComponent *component = NULL;
+
+ icalstring = tcu_new_icalstring_from_test_case (case_name);
+ if (icalstring)
+ component = e_cal_component_new_from_string (icalstring);
+ g_free (icalstring);
+
+ if (!component)
+ g_error (
+ "Failed to construct component from test case '%s'",
+ case_name);
+
+ return component;
+}
+
+void
+tcu_add_component_from_test_case (TCUFixture *fixture,
+ const gchar *case_name,
+ ECalComponent **out_component)
+{
+ ECalComponent *component;
+ GError *error = NULL;
+
+ component = tcu_new_component_from_test_case (case_name);
+
+ if (!e_cal_cache_put_component (fixture->cal_cache, component, case_name, E_CACHE_IS_ONLINE, NULL,
&error))
+ g_error ("Failed to add component: %s", error->message);
+
+ if (out_component)
+ *out_component = g_object_ref (component);
+
+ g_clear_object (&component);
+}
diff --git a/tests/libedata-cal/test-cal-cache-utils.h b/tests/libedata-cal/test-cal-cache-utils.h
new file mode 100644
index 0000000..d3616fc
--- /dev/null
+++ b/tests/libedata-cal/test-cal-cache-utils.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef TEST_CACHE_UTILS_H
+#define TEST_CACHE_UTILS_H
+
+#include <libedata-cal/libedata-cal.h>
+
+G_BEGIN_DECLS
+
+typedef enum {
+ TCU_LOAD_COMPONENT_SET_NONE,
+ TCU_LOAD_COMPONENT_SET_EVENTS,
+ TCU_LOAD_COMPONENT_SET_TASKS
+} TCULoadComponentSet;
+
+typedef struct {
+ ECalCache *cal_cache;
+} TCUFixture;
+
+typedef struct {
+ TCULoadComponentSet load_set;
+} TCUClosure;
+
+void tcu_fixture_setup (TCUFixture *fixture,
+ gconstpointer user_data);
+void tcu_fixture_teardown (TCUFixture *fixture,
+ gconstpointer user_data);
+
+gchar * tcu_new_icalstring_from_test_case (const gchar *case_name);
+ECalComponent * tcu_new_component_from_test_case (const gchar *case_name);
+void tcu_add_component_from_test_case (TCUFixture *fixture,
+ const gchar *case_name,
+ ECalComponent **out_component);
+
+G_END_DECLS
+
+#endif /* TEST_CACHE_UTILS_H */
diff --git a/tests/libedata-cal/test-cal-meta-backend.c b/tests/libedata-cal/test-cal-meta-backend.c
new file mode 100644
index 0000000..c931f01
--- /dev/null
+++ b/tests/libedata-cal/test-cal-meta-backend.c
@@ -0,0 +1,2723 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2017 Red Hat, Inc. (www.redhat.com)
+ *
+ * This library is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation.
+ *
+ * This library is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "evolution-data-server-config.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <locale.h>
+
+#include "libecal/libecal.h"
+
+#include "e-test-server-utils.h"
+#include "test-cal-cache-utils.h"
+
+void _e_cal_cache_remove_loaded_timezones (ECalCache *cal_cache); /* e-cal-cache.c, private function */
+
+#define EXPECTED_TZID "/freeassociation.sourceforge.net/America/New_York"
+#define EXPECTED_LOCATION "America/New_York"
+#define REMOTE_URL "https://www.gnome.org/wp-content/themes/gnome-grass/images/gnome-logo.svg"
+#define MODIFIED_SUMMARY_STR "Modified summary"
+
+typedef struct _ECalMetaBackendTest {
+ ECalMetaBackend parent;
+
+ icalcomponent *vcalendar;
+
+ gint sync_tag_index;
+ gboolean can_connect;
+ gboolean is_connected;
+ gint connect_count;
+ gint list_count;
+ gint save_count;
+ gint load_count;
+ gint remove_count;
+} ECalMetaBackendTest;
+
+typedef struct _ECalMetaBackendTestClass {
+ ECalMetaBackendClass parent_class;
+} ECalMetaBackendTestClass;
+
+#define E_TYPE_CAL_META_BACKEND_TEST (e_cal_meta_backend_test_get_type ())
+#define E_CAL_META_BACKEND_TEST(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST \
+ ((obj), E_TYPE_CAL_META_BACKEND_TEST, ECalMetaBackendTest))
+#define E_IS_CAL_META_BACKEND_TEST(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE \
+ ((obj), E_TYPE_CAL_META_BACKEND_TEST))
+
+GType e_cal_meta_backend_test_get_type (void) G_GNUC_CONST;
+
+G_DEFINE_TYPE (ECalMetaBackendTest, e_cal_meta_backend_test, E_TYPE_CAL_META_BACKEND)
+
+static void
+ecmb_test_add_test_case (ECalMetaBackendTest *test_backend,
+ const gchar *case_name)
+{
+ gchar *icalstr;
+ icalcomponent *icalcomp;
+
+ g_assert_nonnull (test_backend);
+ g_assert_nonnull (case_name);
+
+ icalstr = tcu_new_icalstring_from_test_case (case_name);
+ g_assert_nonnull (icalstr);
+
+ icalcomp = icalcomponent_new_from_string (icalstr);
+ g_assert_nonnull (icalcomp);
+ g_free (icalstr);
+
+ icalcomponent_add_component (test_backend->vcalendar, icalcomp);
+}
+
+static void
+ecmb_test_remove_component (ECalMetaBackendTest *test_backend,
+ const gchar *uid,
+ const gchar *rid)
+{
+ icalcomponent *icalcomp;
+
+ g_assert_nonnull (test_backend);
+ g_assert_nonnull (uid);
+
+ if (rid && !*rid)
+ rid = NULL;
+
+ for (icalcomp = icalcomponent_get_first_component (test_backend->vcalendar, ICAL_VEVENT_COMPONENT);
+ icalcomp;) {
+ const gchar *server_uid;
+
+ server_uid = icalcomponent_get_uid (icalcomp);
+ g_assert_nonnull (server_uid);
+
+ if (g_str_equal (server_uid, uid) && (!rid || !*rid ||
+ (icalcomponent_get_first_property (icalcomp, ICAL_RECURRENCEID_PROPERTY) &&
+ g_str_equal (rid, icaltime_as_ical_string (icalcomponent_get_recurrenceid (icalcomp))))))
{
+ icalcomponent_remove_component (test_backend->vcalendar, icalcomp);
+ icalcomponent_free (icalcomp);
+
+ icalcomp = icalcomponent_get_first_component (test_backend->vcalendar,
ICAL_VEVENT_COMPONENT);
+ } else {
+ icalcomp = icalcomponent_get_next_component (test_backend->vcalendar,
ICAL_VEVENT_COMPONENT);
+ }
+ }
+}
+
+static GHashTable * /* ECalComponentId * ~> NULL */
+ecmb_test_gather_ids (va_list args)
+{
+ GHashTable *expects;
+ const gchar *uid, *rid;
+
+ expects = g_hash_table_new_full ((GHashFunc) e_cal_component_id_hash, (GEqualFunc)
e_cal_component_id_equal,
+ (GDestroyNotify) e_cal_component_free_id, NULL);
+
+ uid = va_arg (args, const gchar *);
+ while (uid) {
+ rid = va_arg (args, const gchar *);
+
+ g_hash_table_insert (expects, e_cal_component_id_new (uid, rid), NULL);
+ uid = va_arg (args, const gchar *);
+ }
+
+ return expects;
+}
+
+static void
+ecmb_test_vcalendar_contains (icalcomponent *vcalendar,
+ gboolean negate,
+ gboolean exact,
+ ...) /* <uid, rid> pairs, ended with NULL */
+{
+ va_list args;
+ GHashTable *expects;
+ icalcomponent *icalcomp;
+ guint ntotal;
+
+ g_return_if_fail (vcalendar != NULL);
+ g_return_if_fail (icalcomponent_isa (vcalendar) == ICAL_VCALENDAR_COMPONENT);
+
+ va_start (args, exact);
+ expects = ecmb_test_gather_ids (args);
+ va_end (args);
+
+ ntotal = g_hash_table_size (expects);
+
+ for (icalcomp = icalcomponent_get_first_component (vcalendar, ICAL_VEVENT_COMPONENT);
+ icalcomp;
+ icalcomp = icalcomponent_get_next_component (vcalendar, ICAL_VEVENT_COMPONENT)) {
+ ECalComponentId id;
+
+ id.uid = (gpointer) icalcomponent_get_uid (icalcomp);
+ if (icalcomponent_get_first_property (icalcomp, ICAL_RECURRENCEID_PROPERTY))
+ id.rid = (gpointer) icaltime_as_ical_string (icalcomponent_get_recurrenceid
(icalcomp));
+ else
+ id.rid = NULL;
+
+ if (exact) {
+ if (negate)
+ g_assert (!g_hash_table_remove (expects, &id));
+ else
+ g_assert (g_hash_table_remove (expects, &id));
+ } else {
+ g_hash_table_remove (expects, &id);
+ }
+ }
+
+ if (negate)
+ g_assert_cmpint (g_hash_table_size (expects), ==, ntotal);
+ else
+ g_assert_cmpint (g_hash_table_size (expects), ==, 0);
+
+ g_hash_table_destroy (expects);
+}
+
+static void
+ecmb_test_cache_contains (ECalCache *cal_cache,
+ gboolean negate,
+ gboolean exact,
+ ...) /* <uid, rid> pairs, ended with NULL */
+{
+ va_list args;
+ GHashTable *expects;
+ GHashTableIter iter;
+ gpointer key;
+ gint found = 0;
+
+ g_return_if_fail (E_IS_CAL_CACHE (cal_cache));
+
+ va_start (args, exact);
+ expects = ecmb_test_gather_ids (args);
+ va_end (args);
+
+ g_hash_table_iter_init (&iter, expects);
+ while (g_hash_table_iter_next (&iter, &key, NULL)) {
+ ECalComponentId *id = key;
+
+ g_assert_nonnull (id);
+
+ if (e_cal_cache_contains (cal_cache, id->uid, id->rid, E_CACHE_EXCLUDE_DELETED))
+ found++;
+ }
+
+ if (negate)
+ g_assert_cmpint (0, ==, found);
+ else
+ g_assert_cmpint (g_hash_table_size (expects), ==, found);
+
+ g_hash_table_destroy (expects);
+
+ if (exact && !negate)
+ g_assert_cmpint (e_cache_get_count (E_CACHE (cal_cache), E_CACHE_EXCLUDE_DELETED, NULL,
NULL), ==, found);
+}
+
+static void
+ecmb_test_cache_and_server_equal (ECalCache *cal_cache,
+ icalcomponent *vcalendar,
+ ECacheDeletedFlag deleted_flag)
+{
+ icalcomponent *icalcomp;
+
+ g_return_if_fail (E_IS_CAL_CACHE (cal_cache));
+ g_return_if_fail (vcalendar != NULL);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (cal_cache), deleted_flag, NULL, NULL), ==,
+ icalcomponent_count_components (vcalendar, ICAL_VEVENT_COMPONENT));
+
+ for (icalcomp = icalcomponent_get_first_component (vcalendar, ICAL_VEVENT_COMPONENT);
+ icalcomp;
+ icalcomp = icalcomponent_get_next_component (vcalendar, ICAL_VEVENT_COMPONENT)) {
+ const gchar *uid, *rid = NULL;
+
+ uid = icalcomponent_get_uid (icalcomp);
+ if (icalcomponent_get_first_property (icalcomp, ICAL_RECURRENCEID_PROPERTY))
+ rid = icaltime_as_ical_string (icalcomponent_get_recurrenceid (icalcomp));
+
+ g_assert (e_cal_cache_contains (cal_cache, uid, rid, deleted_flag));
+ }
+}
+
+static gchar *
+e_cal_meta_backend_test_get_backend_property (ECalBackend *cal_backend,
+ const gchar *prop_name)
+{
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND_TEST (cal_backend), NULL);
+ g_return_val_if_fail (prop_name != NULL, NULL);
+
+ if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
+ return g_strjoin (",",
+ e_cal_meta_backend_get_capabilities (E_CAL_META_BACKEND (cal_backend)),
+ CAL_STATIC_CAPABILITY_ALARM_DESCRIPTION,
+ NULL);
+ } else if (g_str_equal (prop_name, CAL_BACKEND_PROPERTY_CAL_EMAIL_ADDRESS)) {
+ return g_strdup ("user@no.where");
+ }
+
+ /* Chain up to parent's method. */
+ return E_CAL_BACKEND_CLASS (e_cal_meta_backend_test_parent_class)->get_backend_property (cal_backend,
prop_name);
+}
+
+static gboolean
+e_cal_meta_backend_test_connect_sync (ECalMetaBackend *meta_backend,
+ const ENamedParameters *credentials,
+ ESourceAuthenticationResult *out_auth_result,
+ gchar **out_certificate_pem,
+ GTlsCertificateFlags *out_certificate_errors,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalMetaBackendTest *test_backend;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND_TEST (meta_backend), FALSE);
+
+ test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+
+ if (test_backend->is_connected)
+ return TRUE;
+
+ test_backend->connect_count++;
+
+ if (test_backend->can_connect) {
+ test_backend->is_connected = TRUE;
+ return TRUE;
+ }
+
+ g_set_error_literal (error, E_CLIENT_ERROR, E_CLIENT_ERROR_REPOSITORY_OFFLINE,
+ e_client_error_to_string (E_CLIENT_ERROR_REPOSITORY_OFFLINE));
+
+ return FALSE;
+}
+
+static gboolean
+e_cal_meta_backend_test_disconnect_sync (ECalMetaBackend *meta_backend,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalMetaBackendTest *test_backend;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND_TEST (meta_backend), FALSE);
+
+ test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+ test_backend->is_connected = FALSE;
+
+ return TRUE;
+}
+
+static gboolean
+e_cal_meta_backend_test_get_changes_sync (ECalMetaBackend *meta_backend,
+ const gchar *last_sync_tag,
+ gboolean is_repeat,
+ gchar **out_new_sync_tag,
+ gboolean *out_repeat,
+ GSList **out_created_objects,
+ GSList **out_modified_objects,
+ GSList **out_removed_objects,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalMetaBackendTest *test_backend;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND_TEST (meta_backend), FALSE);
+ g_return_val_if_fail (out_new_sync_tag != NULL, FALSE);
+ g_return_val_if_fail (out_repeat != NULL, FALSE);
+
+ test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+
+ if (!test_backend->sync_tag_index) {
+ g_assert_null (last_sync_tag);
+ } else {
+ g_assert_nonnull (last_sync_tag);
+ g_assert_cmpint (atoi (last_sync_tag), ==, test_backend->sync_tag_index);
+
+ test_backend->sync_tag_index++;
+ *out_new_sync_tag = g_strdup_printf ("%d", test_backend->sync_tag_index);
+
+ if (test_backend->sync_tag_index == 2)
+ *out_repeat = TRUE;
+ else if (test_backend->sync_tag_index == 3)
+ return TRUE;
+ }
+
+ /* Nothing to do here at the moment, left the work to the parent class,
+ which calls list_existing_sync() internally. */
+ return E_CAL_META_BACKEND_CLASS (e_cal_meta_backend_test_parent_class)->get_changes_sync
(meta_backend,
+ last_sync_tag, is_repeat, out_new_sync_tag, out_repeat, out_created_objects,
+ out_modified_objects, out_removed_objects, cancellable, error);
+}
+
+static gboolean
+e_cal_meta_backend_test_list_existing_sync (ECalMetaBackend *meta_backend,
+ gchar **out_new_sync_tag,
+ GSList **out_existing_objects,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalMetaBackendTest *test_backend;
+ ECalCache *cal_cache;
+ icalcomponent *icalcomp;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND_TEST (meta_backend), FALSE);
+ g_return_val_if_fail (out_new_sync_tag, FALSE);
+ g_return_val_if_fail (out_existing_objects, FALSE);
+
+ test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+ test_backend->list_count++;
+
+ g_assert (test_backend->is_connected);
+
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ g_assert_nonnull (cal_cache);
+
+ *out_existing_objects = NULL;
+
+ for (icalcomp = icalcomponent_get_first_component (test_backend->vcalendar, ICAL_VEVENT_COMPONENT);
+ icalcomp;
+ icalcomp = icalcomponent_get_next_component (test_backend->vcalendar, ICAL_VEVENT_COMPONENT)) {
+ const gchar *uid;
+ gchar *revision;
+ ECalMetaBackendInfo *nfo;
+
+ /* Detached instances are stored together with the master object */
+ if (icalcomponent_get_first_property (icalcomp, ICAL_RECURRENCEID_PROPERTY))
+ continue;
+
+ uid = icalcomponent_get_uid (icalcomp);
+ revision = e_cal_cache_dup_component_revision (cal_cache, icalcomp);
+
+ nfo = e_cal_meta_backend_info_new (uid, revision, NULL, NULL);
+ *out_existing_objects = g_slist_prepend (*out_existing_objects, nfo);
+
+ g_free (revision);
+ }
+
+ g_object_unref (cal_cache);
+
+ return TRUE;
+}
+
+static gboolean
+e_cal_meta_backend_test_save_component_sync (ECalMetaBackend *meta_backend,
+ gboolean overwrite_existing,
+ EConflictResolution conflict_resolution,
+ const GSList *instances,
+ const gchar *extra,
+ gchar **out_new_uid,
+ gchar **out_new_extra,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalMetaBackendTest *test_backend;
+ icalcomponent *icalcomp;
+ const gchar *uid;
+ GSList *link;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND_TEST (meta_backend), FALSE);
+ g_return_val_if_fail (instances != NULL, FALSE);
+ g_return_val_if_fail (out_new_uid != NULL, FALSE);
+
+ test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+ test_backend->save_count++;
+
+ g_assert (test_backend->is_connected);
+
+ uid = icalcomponent_get_uid (e_cal_component_get_icalcomponent (instances->data));
+ g_assert_nonnull (uid);
+
+ for (icalcomp = icalcomponent_get_first_component (test_backend->vcalendar, ICAL_VEVENT_COMPONENT);
+ icalcomp;) {
+ const gchar *server_uid;
+
+ server_uid = icalcomponent_get_uid (icalcomp);
+ g_assert_nonnull (server_uid);
+
+ if (g_str_equal (server_uid, uid)) {
+ if (!overwrite_existing) {
+ g_propagate_error (error, e_data_cal_create_error (ObjectIdAlreadyExists,
NULL));
+ return FALSE;
+ }
+
+ icalcomponent_remove_component (test_backend->vcalendar, icalcomp);
+ icalcomponent_free (icalcomp);
+
+ icalcomp = icalcomponent_get_first_component (test_backend->vcalendar,
ICAL_VEVENT_COMPONENT);
+ } else {
+ icalcomp = icalcomponent_get_next_component (test_backend->vcalendar,
ICAL_VEVENT_COMPONENT);
+ }
+ }
+
+ for (link = (GSList *) instances; link; link = g_slist_next (link)) {
+ ECalComponent *comp = link->data;
+ const gchar *comp_uid;
+
+ icalcomp = e_cal_component_get_icalcomponent (comp);
+ g_assert_nonnull (icalcomp);
+
+ comp_uid = icalcomponent_get_uid (icalcomp);
+ g_assert_cmpstr (uid, ==, comp_uid);
+
+ icalcomponent_add_component (test_backend->vcalendar, icalcomponent_new_clone (icalcomp));
+ }
+
+ *out_new_uid = g_strdup (uid);
+
+ return TRUE;
+}
+
+static gboolean
+e_cal_meta_backend_test_load_component_sync (ECalMetaBackend *meta_backend,
+ const gchar *uid,
+ const gchar *extra,
+ icalcomponent **out_instances,
+ gchar **out_extra,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalMetaBackendTest *test_backend;
+ icalcomponent *icalcomp;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND_TEST (meta_backend), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (out_instances != NULL, FALSE);
+ g_return_val_if_fail (out_extra != NULL, FALSE);
+
+ test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+ test_backend->load_count++;
+
+ g_assert (test_backend->is_connected);
+
+ *out_instances = NULL;
+
+ for (icalcomp = icalcomponent_get_first_component (test_backend->vcalendar, ICAL_VEVENT_COMPONENT);
+ icalcomp;
+ icalcomp = icalcomponent_get_next_component (test_backend->vcalendar, ICAL_VEVENT_COMPONENT)) {
+ const gchar *server_uid;
+
+ server_uid = icalcomponent_get_uid (icalcomp);
+ g_assert_nonnull (server_uid);
+
+ if (g_str_equal (server_uid, uid)) {
+ if (!*out_instances)
+ *out_instances = e_cal_util_new_top_level ();
+
+ icalcomponent_add_component (*out_instances, icalcomponent_new_clone (icalcomp));
+ }
+ }
+
+ if (*out_instances) {
+ *out_extra = g_strconcat ("extra for ", uid, NULL);
+ return TRUE;
+ } else {
+ g_propagate_error (error, e_data_cal_create_error (ObjectNotFound, NULL));
+ }
+
+ return FALSE;
+}
+
+static gboolean
+e_cal_meta_backend_test_remove_component_sync (ECalMetaBackend *meta_backend,
+ EConflictResolution conflict_resolution,
+ const gchar *uid,
+ const gchar *extra,
+ const gchar *object,
+ GCancellable *cancellable,
+ GError **error)
+{
+ ECalMetaBackendTest *test_backend;
+ icalcomponent *icalcomp;
+ gboolean success = FALSE;
+
+ g_return_val_if_fail (E_IS_CAL_META_BACKEND_TEST (meta_backend), FALSE);
+ g_return_val_if_fail (uid != NULL, FALSE);
+ g_return_val_if_fail (extra != NULL, FALSE);
+
+ test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+ test_backend->remove_count++;
+
+ g_assert (test_backend->is_connected);
+
+ for (icalcomp = icalcomponent_get_first_component (test_backend->vcalendar, ICAL_VEVENT_COMPONENT);
+ icalcomp;) {
+ const gchar *server_uid;
+
+ server_uid = icalcomponent_get_uid (icalcomp);
+ g_assert_nonnull (server_uid);
+
+ if (g_str_equal (server_uid, uid)) {
+ if (!success) {
+ gchar *expected_extra;
+
+ expected_extra = g_strconcat ("extra for ", uid, NULL);
+ g_assert_cmpstr (expected_extra, ==, extra);
+ g_free (expected_extra);
+ }
+
+ success = TRUE;
+
+ icalcomponent_remove_component (test_backend->vcalendar, icalcomp);
+ icalcomponent_free (icalcomp);
+
+ icalcomp = icalcomponent_get_first_component (test_backend->vcalendar,
ICAL_VEVENT_COMPONENT);
+ } else {
+ icalcomp = icalcomponent_get_next_component (test_backend->vcalendar,
ICAL_VEVENT_COMPONENT);
+ }
+ }
+
+ if (!success)
+ g_propagate_error (error, e_data_cal_create_error (ObjectNotFound, NULL));
+
+ return success;
+}
+
+static void
+e_cal_meta_backend_test_reset_counters (ECalMetaBackendTest *test_backend)
+{
+ g_return_if_fail (E_IS_CAL_META_BACKEND_TEST (test_backend));
+
+ test_backend->connect_count = 0;
+ test_backend->list_count = 0;
+ test_backend->save_count = 0;
+ test_backend->load_count = 0;
+ test_backend->remove_count = 0;
+}
+
+static ECalCache *glob_use_cache = NULL;
+
+static void
+e_cal_meta_backend_test_constructed (GObject *object)
+{
+ ECalMetaBackendTest *test_backend = E_CAL_META_BACKEND_TEST (object);
+
+ g_assert_nonnull (glob_use_cache);
+
+ /* Set it before ECalMetaBackend::constucted() creates its own cache */
+ e_cal_meta_backend_set_cache (E_CAL_META_BACKEND (test_backend), glob_use_cache);
+
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_cal_meta_backend_test_parent_class)->constructed (object);
+}
+
+static void
+e_cal_meta_backend_test_finalize (GObject *object)
+{
+ ECalMetaBackendTest *test_backend = E_CAL_META_BACKEND_TEST (object);
+
+ g_assert_nonnull (test_backend->vcalendar);
+
+ icalcomponent_free (test_backend->vcalendar);
+
+ /* Chain up to parent's method. */
+ G_OBJECT_CLASS (e_cal_meta_backend_test_parent_class)->finalize (object);
+}
+
+static void
+e_cal_meta_backend_test_class_init (ECalMetaBackendTestClass *klass)
+{
+ ECalMetaBackendClass *cal_meta_backend_class;
+ ECalBackendClass *cal_backend_class;
+ GObjectClass *object_class;
+
+ cal_meta_backend_class = E_CAL_META_BACKEND_CLASS (klass);
+ cal_meta_backend_class->connect_sync = e_cal_meta_backend_test_connect_sync;
+ cal_meta_backend_class->disconnect_sync = e_cal_meta_backend_test_disconnect_sync;
+ cal_meta_backend_class->get_changes_sync = e_cal_meta_backend_test_get_changes_sync;
+ cal_meta_backend_class->list_existing_sync = e_cal_meta_backend_test_list_existing_sync;
+ cal_meta_backend_class->save_component_sync = e_cal_meta_backend_test_save_component_sync;
+ cal_meta_backend_class->load_component_sync = e_cal_meta_backend_test_load_component_sync;
+ cal_meta_backend_class->remove_component_sync = e_cal_meta_backend_test_remove_component_sync;
+
+ cal_backend_class = E_CAL_BACKEND_CLASS (klass);
+ cal_backend_class->get_backend_property = e_cal_meta_backend_test_get_backend_property;
+
+ object_class = G_OBJECT_CLASS (klass);
+ object_class->constructed = e_cal_meta_backend_test_constructed;
+ object_class->finalize = e_cal_meta_backend_test_finalize;
+}
+
+static void
+e_cal_meta_backend_test_init (ECalMetaBackendTest *test_backend)
+{
+ test_backend->sync_tag_index = 0;
+ test_backend->is_connected = FALSE;
+ test_backend->can_connect = TRUE;
+ test_backend->vcalendar = e_cal_util_new_top_level ();
+
+ e_cal_meta_backend_test_reset_counters (test_backend);
+
+ e_backend_set_online (E_BACKEND (test_backend), TRUE);
+ e_cal_backend_set_writable (E_CAL_BACKEND (test_backend), TRUE);
+
+ ecmb_test_add_test_case (test_backend, "event-1");
+ ecmb_test_add_test_case (test_backend, "event-2");
+ ecmb_test_add_test_case (test_backend, "event-3");
+ ecmb_test_add_test_case (test_backend, "event-4");
+ ecmb_test_add_test_case (test_backend, "event-5");
+ ecmb_test_add_test_case (test_backend, "event-6");
+ ecmb_test_add_test_case (test_backend, "event-6-a");
+ ecmb_test_add_test_case (test_backend, "event-7");
+ ecmb_test_add_test_case (test_backend, "event-8");
+ ecmb_test_add_test_case (test_backend, "event-9");
+}
+
+static ESourceRegistry *glob_registry = NULL;
+
+static ECalMetaBackend *
+e_cal_meta_backend_test_new (ECalCache *cache)
+{
+ ECalMetaBackend *meta_backend;
+ ESource *scratch;
+ gboolean success;
+ GError *error = NULL;
+
+ g_assert (E_IS_CAL_CACHE (cache));
+
+ g_assert_nonnull (glob_registry);
+ g_assert_null (glob_use_cache);
+
+ glob_use_cache = cache;
+
+ scratch = e_source_new_with_uid ("test-source", NULL, &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (scratch);
+
+ meta_backend = g_object_new (E_TYPE_CAL_META_BACKEND_TEST,
+ "source", scratch,
+ "registry", glob_registry,
+ "kind", ICAL_VEVENT_COMPONENT,
+ NULL);
+ g_assert_nonnull (meta_backend);
+
+ g_assert (glob_use_cache == cache);
+ glob_use_cache = NULL;
+
+ g_object_unref (scratch);
+
+ e_cal_meta_backend_set_cache (meta_backend, cache);
+
+ #define set_extra_data(_uid, _rid) \
+ success = e_cal_cache_set_component_extra (cache, _uid, _rid, "extra for " _uid, NULL,
&error); \
+ g_assert_no_error (error); \
+ g_assert (success);
+
+ set_extra_data ("event-1", NULL);
+ set_extra_data ("event-2", NULL);
+ set_extra_data ("event-3", NULL);
+ set_extra_data ("event-4", NULL);
+ set_extra_data ("event-5", NULL);
+ set_extra_data ("event-6", NULL);
+ set_extra_data ("event-6", "20170225T134900");
+ set_extra_data ("event-7", NULL);
+ set_extra_data ("event-8", NULL);
+ set_extra_data ("event-9", NULL);
+
+ #undef set_extra_data
+
+ return meta_backend;
+}
+
+static void
+e_cal_meta_backend_test_change_online (ECalMetaBackend *meta_backend,
+ gboolean is_online)
+{
+ EFlag *flag;
+ gulong handler_id;
+
+ if (!is_online) {
+ e_backend_set_online (E_BACKEND (meta_backend), FALSE);
+ return;
+ }
+
+ if (e_backend_get_online (E_BACKEND (meta_backend)))
+ return;
+
+ flag = e_flag_new ();
+
+ handler_id = g_signal_connect_swapped (meta_backend, "refresh-completed",
+ G_CALLBACK (e_flag_set), flag);
+
+ /* Going online triggers refresh, thus wait for it */
+ e_backend_set_online (E_BACKEND (meta_backend), TRUE);
+
+ e_flag_wait (flag);
+ e_flag_free (flag);
+
+ g_signal_handler_disconnect (meta_backend, handler_id);
+}
+
+static void
+e_cal_meta_backend_test_call_refresh (ECalMetaBackend *meta_backend)
+{
+ EFlag *flag;
+ gulong handler_id;
+ GError *error = NULL;
+
+ if (!e_backend_get_online (E_BACKEND (meta_backend)))
+ return;
+
+ flag = e_flag_new ();
+
+ handler_id = g_signal_connect_swapped (meta_backend, "refresh-completed",
+ G_CALLBACK (e_flag_set), flag);
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->refresh_sync (E_CAL_BACKEND_SYNC (meta_backend), NULL,
NULL, &error);
+ g_assert_no_error (error);
+
+ e_flag_wait (flag);
+ e_flag_free (flag);
+
+ g_signal_handler_disconnect (meta_backend, handler_id);
+}
+
+static void
+assert_tzid_matches_cb (icalparameter *param,
+ gpointer user_data)
+{
+ const gchar *expected_tzid = user_data;
+
+ g_assert_cmpstr (icalparameter_get_tzid (param), ==, expected_tzid);
+}
+
+static void
+test_merge_instances (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ ECalMetaBackend *meta_backend;
+ GSList *instances = NULL;
+ icalcomponent *icalcomp, *subcomp;
+ icalproperty *prop;
+ gboolean success;
+ GError *error = NULL;
+
+ meta_backend = e_cal_meta_backend_test_new (fixture->cal_cache);
+ g_assert_nonnull (meta_backend);
+
+ /* event-1 has only UTC times, with no TZID */
+ success = e_cal_cache_get_components_by_uid (fixture->cal_cache, "event-1", &instances, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_nonnull (instances);
+
+ /* TZID as is */
+ icalcomp = e_cal_meta_backend_merge_instances (meta_backend, instances, FALSE);
+ g_assert_nonnull (icalcomp);
+ g_assert_cmpint (icalcomponent_isa (icalcomp), ==, ICAL_VCALENDAR_COMPONENT);
+ g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_ANY_COMPONENT), ==, 1);
+
+ subcomp = icalcomponent_get_first_component (icalcomp, ICAL_ANY_COMPONENT);
+ g_assert_nonnull (subcomp);
+ g_assert_cmpint (icalcomponent_isa (subcomp), ==, ICAL_VEVENT_COMPONENT);
+
+ icalcomponent_free (icalcomp);
+
+ /* TZID as location */
+ icalcomp = e_cal_meta_backend_merge_instances (meta_backend, instances, TRUE);
+ g_assert_nonnull (icalcomp);
+ g_assert_cmpint (icalcomponent_isa (icalcomp), ==, ICAL_VCALENDAR_COMPONENT);
+ g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_ANY_COMPONENT), ==, 1);
+
+ subcomp = icalcomponent_get_first_component (icalcomp, ICAL_ANY_COMPONENT);
+ g_assert_nonnull (subcomp);
+ g_assert_cmpint (icalcomponent_isa (subcomp), ==, ICAL_VEVENT_COMPONENT);
+
+ icalcomponent_free (icalcomp);
+
+ g_slist_free_full (instances, g_object_unref);
+ instances = NULL;
+
+ /* event-7 has built-in TZID */
+ success = e_cal_cache_get_components_by_uid (fixture->cal_cache, "event-7", &instances, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_nonnull (instances);
+
+ /* TZID as is */
+ icalcomp = e_cal_meta_backend_merge_instances (meta_backend, instances, FALSE);
+ g_assert_nonnull (icalcomp);
+ g_assert_cmpint (icalcomponent_isa (icalcomp), ==, ICAL_VCALENDAR_COMPONENT);
+ g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_ANY_COMPONENT), ==, 2);
+ g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_VTIMEZONE_COMPONENT), ==, 1);
+ g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_VEVENT_COMPONENT), ==, 1);
+
+ subcomp = icalcomponent_get_first_component (icalcomp, ICAL_VTIMEZONE_COMPONENT);
+ g_assert_nonnull (subcomp);
+ g_assert_cmpint (icalcomponent_isa (subcomp), ==, ICAL_VTIMEZONE_COMPONENT);
+
+ prop = icalcomponent_get_first_property (subcomp, ICAL_TZID_PROPERTY);
+ g_assert_nonnull (prop);
+ g_assert_cmpstr (icalproperty_get_tzid (prop), ==, EXPECTED_TZID);
+
+ subcomp = icalcomponent_get_first_component (icalcomp, ICAL_VEVENT_COMPONENT);
+ g_assert_nonnull (subcomp);
+ icalcomponent_foreach_tzid (subcomp, assert_tzid_matches_cb, (gpointer) icalproperty_get_tzid (prop));
+
+ icalcomponent_free (icalcomp);
+
+ /* TZID to location */
+ icalcomp = e_cal_meta_backend_merge_instances (meta_backend, instances, TRUE);
+ g_assert_nonnull (icalcomp);
+ g_assert_cmpint (icalcomponent_isa (icalcomp), ==, ICAL_VCALENDAR_COMPONENT);
+ g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_ANY_COMPONENT), ==, 2);
+ g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_VTIMEZONE_COMPONENT), ==, 1);
+ g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_VEVENT_COMPONENT), ==, 1);
+
+ subcomp = icalcomponent_get_first_component (icalcomp, ICAL_VTIMEZONE_COMPONENT);
+ g_assert_nonnull (subcomp);
+ g_assert_cmpint (icalcomponent_isa (subcomp), ==, ICAL_VTIMEZONE_COMPONENT);
+
+ prop = icalcomponent_get_first_property (subcomp, ICAL_TZID_PROPERTY);
+ g_assert_nonnull (prop);
+ g_assert_cmpstr (icalproperty_get_tzid (prop), ==, EXPECTED_LOCATION);
+
+ subcomp = icalcomponent_get_first_component (icalcomp, ICAL_VEVENT_COMPONENT);
+ g_assert_nonnull (subcomp);
+ icalcomponent_foreach_tzid (subcomp, assert_tzid_matches_cb, (gpointer) icalproperty_get_tzid (prop));
+
+ icalcomponent_free (icalcomp);
+ g_slist_free_full (instances, g_object_unref);
+ instances = NULL;
+
+ /* event-6 has TZID-s as locations already and a detached instance */
+ success = e_cal_cache_get_components_by_uid (fixture->cal_cache, "event-6", &instances, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_nonnull (instances);
+
+ /* TZID as is */
+ icalcomp = e_cal_meta_backend_merge_instances (meta_backend, instances, FALSE);
+ g_assert_nonnull (icalcomp);
+ g_assert_cmpint (icalcomponent_isa (icalcomp), ==, ICAL_VCALENDAR_COMPONENT);
+ g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_ANY_COMPONENT), ==, 3);
+ g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_VTIMEZONE_COMPONENT), ==, 1);
+ g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_VEVENT_COMPONENT), ==, 2);
+
+ subcomp = icalcomponent_get_first_component (icalcomp, ICAL_VTIMEZONE_COMPONENT);
+ g_assert_nonnull (subcomp);
+ g_assert_cmpint (icalcomponent_isa (subcomp), ==, ICAL_VTIMEZONE_COMPONENT);
+
+ prop = icalcomponent_get_first_property (subcomp, ICAL_TZID_PROPERTY);
+ g_assert_nonnull (prop);
+ g_assert_cmpstr (icalproperty_get_tzid (prop), ==, EXPECTED_LOCATION);
+
+ subcomp = icalcomponent_get_first_component (icalcomp, ICAL_VEVENT_COMPONENT);
+ g_assert_nonnull (subcomp);
+ icalcomponent_foreach_tzid (subcomp, assert_tzid_matches_cb, (gpointer) icalproperty_get_tzid (prop));
+
+ subcomp = icalcomponent_get_next_component (icalcomp, ICAL_VEVENT_COMPONENT);
+ g_assert_nonnull (subcomp);
+ icalcomponent_foreach_tzid (subcomp, assert_tzid_matches_cb, (gpointer) icalproperty_get_tzid (prop));
+
+ icalcomponent_free (icalcomp);
+
+ /* TZID to location */
+ icalcomp = e_cal_meta_backend_merge_instances (meta_backend, instances, TRUE);
+ g_assert_nonnull (icalcomp);
+ g_assert_cmpint (icalcomponent_isa (icalcomp), ==, ICAL_VCALENDAR_COMPONENT);
+ g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_ANY_COMPONENT), ==, 3);
+ g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_VTIMEZONE_COMPONENT), ==, 1);
+ g_assert_cmpint (icalcomponent_count_components (icalcomp, ICAL_VEVENT_COMPONENT), ==, 2);
+
+ subcomp = icalcomponent_get_first_component (icalcomp, ICAL_VTIMEZONE_COMPONENT);
+ g_assert_nonnull (subcomp);
+ g_assert_cmpint (icalcomponent_isa (subcomp), ==, ICAL_VTIMEZONE_COMPONENT);
+
+ prop = icalcomponent_get_first_property (subcomp, ICAL_TZID_PROPERTY);
+ g_assert_nonnull (prop);
+ g_assert_cmpstr (icalproperty_get_tzid (prop), ==, EXPECTED_LOCATION);
+
+ subcomp = icalcomponent_get_first_component (icalcomp, ICAL_VEVENT_COMPONENT);
+ g_assert_nonnull (subcomp);
+ icalcomponent_foreach_tzid (subcomp, assert_tzid_matches_cb, (gpointer) icalproperty_get_tzid (prop));
+
+ subcomp = icalcomponent_get_next_component (icalcomp, ICAL_VEVENT_COMPONENT);
+ g_assert_nonnull (subcomp);
+ icalcomponent_foreach_tzid (subcomp, assert_tzid_matches_cb, (gpointer) icalproperty_get_tzid (prop));
+
+ icalcomponent_free (icalcomp);
+ g_slist_free_full (instances, g_object_unref);
+
+ g_object_unref (meta_backend);
+}
+
+static void
+check_attachment_content (icalattach *attach,
+ const gchar *expected_content,
+ gsize expected_content_len)
+{
+ g_assert_nonnull (attach);
+ g_assert_nonnull (expected_content);
+ g_assert_cmpint (expected_content_len, >, 0);
+
+ if (icalattach_get_is_url (attach)) {
+ const gchar *url;
+ gboolean success;
+ gchar *filename;
+ gchar *content = NULL;
+ gsize content_len = -1;
+ GError *error = NULL;
+
+ url = icalattach_get_url (attach);
+ g_assert_nonnull (url);
+ g_assert (g_str_has_prefix (url, "file://"));
+
+ filename = g_filename_from_uri (icalattach_get_url (attach), NULL, &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (filename);
+
+ success = g_file_get_contents (filename, &content, &content_len, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_nonnull (content);
+ g_assert_cmpint (content_len, >, 0);
+
+ g_assert_cmpmem (content, content_len, expected_content, expected_content_len);
+
+ g_free (filename);
+ g_free (content);
+ } else {
+ guchar *base64;
+ gsize base64_len;
+
+ base64 = g_base64_decode ((const gchar *) icalattach_get_data (attach), &base64_len);
+ g_assert_nonnull (base64);
+ g_assert_cmpmem (base64, base64_len, expected_content, expected_content_len);
+
+ g_free (base64);
+ }
+}
+
+static void
+test_attachments (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ ECalMetaBackend *meta_backend;
+ gchar *content = NULL;
+ gsize content_len = 0;
+ ECalComponent *comp = NULL;
+ icalcomponent *icalcomp;
+ icalproperty *prop;
+ icalparameter *param;
+ icalattach *attach;
+ gchar *filename;
+ const gchar *basename;
+ gboolean success;
+ GError *error = NULL;
+
+ meta_backend = e_cal_meta_backend_test_new (fixture->cal_cache);
+ g_assert_nonnull (meta_backend);
+
+ /* It has a URL attachment */
+ success = e_cal_cache_get_component (fixture->cal_cache, "event-7", NULL, &comp, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_nonnull (comp);
+
+ icalcomp = icalcomponent_new_clone (e_cal_component_get_icalcomponent (comp));
+ g_assert_nonnull (icalcomp);
+ g_assert_cmpint (icalcomponent_count_properties (icalcomp, ICAL_ATTACH_PROPERTY), ==, 1);
+
+ prop = icalcomponent_get_first_property (icalcomp, ICAL_ATTACH_PROPERTY);
+ g_assert_nonnull (prop);
+ g_assert_null (icalproperty_get_first_parameter (prop, ICAL_FILENAME_PARAMETER));
+
+ attach = icalproperty_get_attach (prop);
+ g_assert_nonnull (attach);
+ g_assert (icalattach_get_is_url (attach));
+
+ filename = g_filename_from_uri (icalattach_get_url (attach), NULL, &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (filename);
+
+ basename = strrchr (filename, '/');
+ g_assert_nonnull (basename);
+ basename++;
+
+ success = g_file_get_contents (filename, &content, &content_len, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_nonnull (content);
+ g_assert_cmpint (content_len, >, 0);
+
+ success = e_cal_meta_backend_inline_local_attachments_sync (meta_backend, icalcomp, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpint (icalcomponent_count_properties (icalcomp, ICAL_ATTACH_PROPERTY), ==, 1);
+
+ prop = icalcomponent_get_first_property (icalcomp, ICAL_ATTACH_PROPERTY);
+ g_assert_nonnull (prop);
+ g_assert_nonnull (icalproperty_get_first_parameter (prop, ICAL_FILENAME_PARAMETER));
+ g_assert_nonnull (icalproperty_get_first_parameter (prop, ICAL_VALUE_PARAMETER));
+ g_assert_nonnull (icalproperty_get_first_parameter (prop, ICAL_ENCODING_PARAMETER));
+
+ param = icalproperty_get_first_parameter (prop, ICAL_FILENAME_PARAMETER);
+ g_assert_cmpstr (icalparameter_get_filename (param), ==, basename);
+
+ attach = icalproperty_get_attach (prop);
+ g_assert_nonnull (attach);
+ g_assert (!icalattach_get_is_url (attach));
+
+ check_attachment_content (attach, content, content_len);
+
+ success = e_cal_meta_backend_store_inline_attachments_sync (meta_backend, icalcomp, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpint (icalcomponent_count_properties (icalcomp, ICAL_ATTACH_PROPERTY), ==, 1);
+
+ prop = icalcomponent_get_first_property (icalcomp, ICAL_ATTACH_PROPERTY);
+ g_assert_nonnull (prop);
+ g_assert_nonnull (icalproperty_get_first_parameter (prop, ICAL_FILENAME_PARAMETER));
+ g_assert_null (icalproperty_get_first_parameter (prop, ICAL_VALUE_PARAMETER));
+ g_assert_null (icalproperty_get_first_parameter (prop, ICAL_ENCODING_PARAMETER));
+
+ param = icalproperty_get_first_parameter (prop, ICAL_FILENAME_PARAMETER);
+ g_assert_cmpstr (icalparameter_get_filename (param), ==, basename);
+
+ attach = icalproperty_get_attach (prop);
+ g_assert_nonnull (attach);
+ g_assert (icalattach_get_is_url (attach));
+
+ check_attachment_content (attach, content, content_len);
+
+ /* Add a URL attachment which is not pointing to a local file */
+ attach = icalattach_new_from_url (REMOTE_URL);
+ prop = icalproperty_new_attach (attach);
+ icalattach_unref (attach);
+ icalcomponent_add_property (icalcomp, prop);
+
+ g_assert_cmpint (icalcomponent_count_properties (icalcomp, ICAL_ATTACH_PROPERTY), ==, 2);
+
+ success = e_cal_meta_backend_inline_local_attachments_sync (meta_backend, icalcomp, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpint (icalcomponent_count_properties (icalcomp, ICAL_ATTACH_PROPERTY), ==, 2);
+
+ prop = icalcomponent_get_first_property (icalcomp, ICAL_ATTACH_PROPERTY);
+ g_assert_nonnull (prop);
+ g_assert_nonnull (icalproperty_get_first_parameter (prop, ICAL_FILENAME_PARAMETER));
+ g_assert_nonnull (icalproperty_get_first_parameter (prop, ICAL_VALUE_PARAMETER));
+ g_assert_nonnull (icalproperty_get_first_parameter (prop, ICAL_ENCODING_PARAMETER));
+
+ param = icalproperty_get_first_parameter (prop, ICAL_FILENAME_PARAMETER);
+ g_assert_cmpstr (icalparameter_get_filename (param), ==, basename);
+
+ attach = icalproperty_get_attach (prop);
+ g_assert_nonnull (attach);
+ g_assert (!icalattach_get_is_url (attach));
+
+ check_attachment_content (attach, content, content_len);
+
+ /* Verify the remote URL did not change */
+ prop = icalcomponent_get_next_property (icalcomp, ICAL_ATTACH_PROPERTY);
+ g_assert_nonnull (prop);
+ g_assert_null (icalproperty_get_first_parameter (prop, ICAL_FILENAME_PARAMETER));
+
+ attach = icalproperty_get_attach (prop);
+ g_assert_nonnull (attach);
+ g_assert (icalattach_get_is_url (attach));
+ g_assert_cmpstr (icalattach_get_url (attach), ==, REMOTE_URL);
+
+ success = e_cal_meta_backend_store_inline_attachments_sync (meta_backend, icalcomp, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpint (icalcomponent_count_properties (icalcomp, ICAL_ATTACH_PROPERTY), ==, 2);
+
+ prop = icalcomponent_get_first_property (icalcomp, ICAL_ATTACH_PROPERTY);
+ g_assert_nonnull (prop);
+ g_assert_nonnull (icalproperty_get_first_parameter (prop, ICAL_FILENAME_PARAMETER));
+ g_assert_null (icalproperty_get_first_parameter (prop, ICAL_VALUE_PARAMETER));
+ g_assert_null (icalproperty_get_first_parameter (prop, ICAL_ENCODING_PARAMETER));
+
+ param = icalproperty_get_first_parameter (prop, ICAL_FILENAME_PARAMETER);
+ g_assert_cmpstr (icalparameter_get_filename (param), ==, basename);
+
+ attach = icalproperty_get_attach (prop);
+ g_assert_nonnull (attach);
+ g_assert (icalattach_get_is_url (attach));
+
+ check_attachment_content (attach, content, content_len);
+
+ /* Verify the remote URL did not change */
+ prop = icalcomponent_get_next_property (icalcomp, ICAL_ATTACH_PROPERTY);
+ g_assert_nonnull (prop);
+ g_assert_null (icalproperty_get_first_parameter (prop, ICAL_FILENAME_PARAMETER));
+
+ attach = icalproperty_get_attach (prop);
+ g_assert_nonnull (attach);
+ g_assert (icalattach_get_is_url (attach));
+ g_assert_cmpstr (icalattach_get_url (attach), ==, REMOTE_URL);
+
+ icalcomponent_free (icalcomp);
+ g_object_unref (meta_backend);
+ g_object_unref (comp);
+ g_free (filename);
+ g_free (content);
+}
+
+static void
+test_empty_cache (TCUFixture *fixture,
+ gconstpointer user_data)
+{
+ #define TZID "/meta/backend/test/timezone"
+ #define TZLOC "test/timezone"
+
+ const gchar *in_tzobj =
+ "BEGIN:VTIMEZONE\r\n"
+ "TZID:" TZID "\r\n"
+ "X-LIC-LOCATION:" TZLOC "\r\n"
+ "BEGIN:STANDARD\r\n"
+ "TZNAME:Test-ST\r\n"
+ "DTSTART:19701106T020000\r\n"
+ "RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11\r\n"
+ "TZOFFSETFROM:-0400\r\n"
+ "TZOFFSETTO:-0500\r\n"
+ "END:STANDARD\r\n"
+ "BEGIN:DAYLIGHT\r\n"
+ "TZNAME:Test-DT\r\n"
+ "DTSTART:19700313T020000\r\n"
+ "RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3\r\n"
+ "TZOFFSETFROM:-0500\r\n"
+ "TZOFFSETTO:-0400\r\n"
+ "END:DAYLIGHT\r\n"
+ "END:VTIMEZONE\r\n";
+ ECalMetaBackend *meta_backend;
+ GList *zones;
+ gboolean success;
+ GError *error = NULL;
+
+ meta_backend = e_cal_meta_backend_test_new (fixture->cal_cache);
+ g_assert_nonnull (meta_backend);
+
+ /* Add timezone to the cache */
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->add_timezone_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, in_tzobj, &error);
+ g_assert_no_error (error);
+
+ zones = NULL;
+ success = e_cal_cache_list_timezones (fixture->cal_cache, &zones, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpint (g_list_length (zones), ==, 1);
+ g_list_free (zones);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_INCLUDE_DELETED, NULL,
&error), >, 0);
+ g_assert_no_error (error);
+
+ /* Empty the cache */
+ success = e_cal_meta_backend_empty_cache_sync (meta_backend, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ /* Verify the cache is truly empty */
+ zones = NULL;
+ success = e_cal_cache_list_timezones (fixture->cal_cache, &zones, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpint (g_list_length (zones), ==, 0);
+ g_list_free (zones);
+
+ g_assert_cmpint (e_cache_get_count (E_CACHE (fixture->cal_cache), E_CACHE_INCLUDE_DELETED, NULL,
&error), ==, 0);
+ g_assert_no_error (error);
+
+ g_object_unref (meta_backend);
+
+ #undef TZID
+ #undef TZLOC
+}
+
+static void
+test_send_objects (ECalMetaBackend *meta_backend)
+{
+ GSList *users = NULL;
+ const gchar *calobj = "fake-iCalendar-object";
+ gchar *modified_calobj = NULL;
+ GError *error = NULL;
+
+ g_assert_nonnull (meta_backend);
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->send_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, calobj, &users, &modified_calobj, &error);
+
+ g_assert_no_error (error);
+ g_assert_null (users);
+ g_assert_cmpstr (calobj, ==, modified_calobj);
+
+ g_free (modified_calobj);
+}
+
+static void
+test_get_attachment_uris (ECalMetaBackend *meta_backend)
+{
+ GSList *uris = NULL;
+ GError *error = NULL;
+
+ g_assert_nonnull (meta_backend);
+
+ /* non-existent event */
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_attachment_uris_sync (E_CAL_BACKEND_SYNC
(meta_backend),
+ NULL, NULL, "unknown-event", NULL, &uris, &error);
+
+ g_assert_error (error, E_DATA_CAL_ERROR, ObjectNotFound);
+ g_assert_null (uris);
+
+ g_clear_error (&error);
+
+ /* existent event, but with no attachments */
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_attachment_uris_sync (E_CAL_BACKEND_SYNC
(meta_backend),
+ NULL, NULL, "event-1", NULL, &uris, &error);
+
+ g_assert_no_error (error);
+ g_assert_null (uris);
+
+ /* event with attachments */
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_attachment_uris_sync (E_CAL_BACKEND_SYNC
(meta_backend),
+ NULL, NULL, "event-7", NULL, &uris, &error);
+
+ g_assert_no_error (error);
+ g_assert_nonnull (uris);
+ g_assert_cmpint (g_slist_length (uris), ==, 1);
+ g_assert_cmpstr (uris->data, ==, "file:///usr/share/icons/hicolor/48x48/apps/evolution.png");
+
+ g_slist_free_full (uris, g_free);
+}
+
+static void
+test_discard_alarm (ECalMetaBackend *meta_backend)
+{
+ GError *error = NULL;
+
+ g_assert_nonnull (meta_backend);
+
+ /* Not implemented */
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->discard_alarm_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, "unknown-event", NULL, NULL, &error);
+
+ g_assert_error (error, E_CLIENT_ERROR, E_CLIENT_ERROR_NOT_SUPPORTED);
+
+ g_clear_error (&error);
+}
+
+static void
+test_timezones (ECalMetaBackend *meta_backend)
+{
+ #define TZID "/meta/backend/test/timezone"
+ #define TZLOC "test/timezone"
+
+ const gchar *in_tzobj =
+ "BEGIN:VTIMEZONE\r\n"
+ "TZID:" TZID "\r\n"
+ "X-LIC-LOCATION:" TZLOC "\r\n"
+ "BEGIN:STANDARD\r\n"
+ "TZNAME:Test-ST\r\n"
+ "DTSTART:19701106T020000\r\n"
+ "RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11\r\n"
+ "TZOFFSETFROM:-0400\r\n"
+ "TZOFFSETTO:-0500\r\n"
+ "END:STANDARD\r\n"
+ "BEGIN:DAYLIGHT\r\n"
+ "TZNAME:Test-DT\r\n"
+ "DTSTART:19700313T020000\r\n"
+ "RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3\r\n"
+ "TZOFFSETFROM:-0500\r\n"
+ "TZOFFSETTO:-0400\r\n"
+ "END:DAYLIGHT\r\n"
+ "END:VTIMEZONE\r\n";
+ ECalCache *cal_cache;
+ icalcomponent *vcalendar;
+ gchar *tzobj = NULL;
+ GList *zones;
+ gboolean success;
+ GError *error = NULL;
+
+ g_assert_nonnull (meta_backend);
+
+ /* Verify neither TZID, not LOCATION is in the timezone cache */
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_timezone_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, TZID, &tzobj, &error);
+ g_assert_error (error, E_DATA_CAL_ERROR, ObjectNotFound);
+ g_assert_null (tzobj);
+ g_clear_error (&error);
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_timezone_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, TZLOC, &tzobj, &error);
+ g_assert_error (error, E_DATA_CAL_ERROR, ObjectNotFound);
+ g_assert_null (tzobj);
+ g_clear_error (&error);
+
+ /* Add it to the cache */
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->add_timezone_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, in_tzobj, &error);
+ g_assert_no_error (error);
+
+ /* Read it back */
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_timezone_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, TZID, &tzobj, &error);
+ g_assert_no_error (error);
+ g_assert_cmpstr (tzobj, ==, in_tzobj);
+ g_free (tzobj);
+ tzobj = NULL;
+
+ /* As a non-built-in timezone it cannot be read with location, only with TZID */
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_timezone_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, TZLOC, &tzobj, &error);
+ g_assert_error (error, E_DATA_CAL_ERROR, ObjectNotFound);
+ g_assert_null (tzobj);
+ g_clear_error (&error);
+
+ /* Try also internal timezone, which will be renamed and added to the cache too */
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_timezone_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, "America/New_York", &tzobj, &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (tzobj);
+ g_assert (strstr (tzobj, "America/New_York") != NULL);
+ g_free (tzobj);
+ tzobj = NULL;
+
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ g_assert_nonnull (cal_cache);
+
+ vcalendar = icalcomponent_new_from_string (
+ "BEGIN:VCALENDAR\r\n"
+ "BEGIN:VTIMEZONE\r\n"
+ "TZID:tzid1\r\n"
+ "X-LIC-LOCATION:tzid/1\r\n"
+ "BEGIN:STANDARD\r\n"
+ "TZNAME:Test-ST\r\n"
+ "DTSTART:19701106T020000\r\n"
+ "RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11\r\n"
+ "TZOFFSETFROM:-0400\r\n"
+ "TZOFFSETTO:-0500\r\n"
+ "END:STANDARD\r\n"
+ "BEGIN:DAYLIGHT\r\n"
+ "TZNAME:Test-DT\r\n"
+ "DTSTART:19700313T020000\r\n"
+ "RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3\r\n"
+ "TZOFFSETFROM:-0500\r\n"
+ "TZOFFSETTO:-0400\r\n"
+ "END:DAYLIGHT\r\n"
+ "END:VTIMEZONE\r\n"
+ "BEGIN:VTIMEZONE\r\n"
+ "TZID:tzid2\r\n"
+ "X-LIC-LOCATION:tzid/2\r\n"
+ "BEGIN:STANDARD\r\n"
+ "TZNAME:Test-ST\r\n"
+ "DTSTART:19701106T020000\r\n"
+ "RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11\r\n"
+ "TZOFFSETFROM:-0400\r\n"
+ "TZOFFSETTO:-0500\r\n"
+ "END:STANDARD\r\n"
+ "BEGIN:DAYLIGHT\r\n"
+ "TZNAME:Test-DT\r\n"
+ "DTSTART:19700313T020000\r\n"
+ "RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3\r\n"
+ "TZOFFSETFROM:-0500\r\n"
+ "TZOFFSETTO:-0400\r\n"
+ "END:DAYLIGHT\r\n"
+ "END:VTIMEZONE\r\n"
+ "BEGIN:VEVENT\r\n"
+ "UID:test-event\r\n"
+ "DTSTAMP:20170130T000000Z\r\n"
+ "CREATED:20170216T155507Z\r\n"
+ "LAST-MODIFIED:20170216T155543Z\r\n"
+ "SEQUENCE:1\r\n"
+ "DTSTART:20170209T013000Z\r\n"
+ "DTEND:20170209T030000Z\r\n"
+ "SUMMARY:Test Event\r\n"
+ "END:VEVENT\r\n"
+ "END:VCALENDAR\r\n");
+ g_assert_nonnull (vcalendar);
+
+ zones = NULL;
+ success = e_cal_cache_list_timezones (cal_cache, &zones, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpint (g_list_length (zones), ==, 2);
+ g_list_free (zones);
+
+ /* Merge with existing */
+ success = e_cal_meta_backend_gather_timezones_sync (meta_backend, vcalendar, FALSE, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ zones = NULL;
+ success = e_cal_cache_list_timezones (cal_cache, &zones, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpint (g_list_length (zones), ==, 4);
+ g_list_free (zones);
+
+ success = e_cal_cache_remove_timezones (cal_cache, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ _e_cal_cache_remove_loaded_timezones (cal_cache);
+
+ zones = NULL;
+ success = e_cal_cache_list_timezones (cal_cache, &zones, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpint (g_list_length (zones), ==, 0);
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->add_timezone_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, in_tzobj, &error);
+ g_assert_no_error (error);
+
+ zones = NULL;
+ success = e_cal_cache_list_timezones (cal_cache, &zones, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpint (g_list_length (zones), ==, 1);
+ g_list_free (zones);
+
+ /* Remove existing and add the new */
+ success = e_cal_meta_backend_gather_timezones_sync (meta_backend, vcalendar, TRUE, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ _e_cal_cache_remove_loaded_timezones (cal_cache);
+
+ zones = NULL;
+ success = e_cal_cache_list_timezones (cal_cache, &zones, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpint (g_list_length (zones), ==, 2);
+ g_list_free (zones);
+
+ icalcomponent_free (vcalendar);
+ g_object_unref (cal_cache);
+
+ #undef TZLOC
+ #undef TZID
+}
+
+static void
+test_get_free_busy (ECalMetaBackend *meta_backend)
+{
+ const gchar *expected_fbobj =
+ "BEGIN:VFREEBUSY\r\n"
+ "ORGANIZER:mailto:user@no.where\r\n"
+ "DTSTART:20170102T080000Z\r\n"
+ "DTEND:20170102T200000Z\r\n"
+ "FREEBUSY;FBTYPE=BUSY;X-SUMMARY=After-party clean up;X-LOCATION=All around:\r\n"
+ " 20170102T100000Z/20170102T180000Z\r\n"
+ "END:VFREEBUSY\r\n";
+ GSList *users, *objects = NULL;
+ time_t start, end;
+ GError *error = NULL;
+
+ g_assert_nonnull (meta_backend);
+
+ users = g_slist_prepend (NULL, (gpointer) "user@no.where");
+ users = g_slist_prepend (users, (gpointer) "unknown@no.where");
+
+ start = icaltime_as_timet (icaltime_from_string ("20170102T080000Z"));
+ end = icaltime_as_timet (icaltime_from_string ("20170102T200000Z"));
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_free_busy_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, users, start, end, &objects, &error);
+
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (objects), ==, 1);
+ g_assert_cmpstr (objects->data, ==, expected_fbobj);
+
+ g_slist_free_full (objects, g_free);
+ g_slist_free (users);
+}
+
+static void
+test_create_objects (ECalMetaBackend *meta_backend)
+{
+ ECalMetaBackendTest *test_backend;
+ ECalCache *cal_cache;
+ GSList *objects, *uids = NULL, *new_components = NULL, *offline_changes;
+ gchar *calobj, *tmp;
+ GError *error = NULL;
+
+ g_assert_nonnull (meta_backend);
+
+ test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ g_assert_nonnull (cal_cache);
+
+ /* Prepare cache and server content */
+ e_cal_cache_remove_component (cal_cache, "event-7", NULL, E_CACHE_IS_ONLINE, NULL, &error);
+ g_assert_no_error (error);
+ e_cal_cache_remove_component (cal_cache, "event-8", NULL, E_CACHE_IS_ONLINE, NULL, &error);
+ g_assert_no_error (error);
+ e_cal_cache_remove_component (cal_cache, "event-9", NULL, E_CACHE_IS_ONLINE, NULL, &error);
+ g_assert_no_error (error);
+
+ ecmb_test_remove_component (test_backend, "event-7", NULL);
+ ecmb_test_remove_component (test_backend, "event-8", NULL);
+ ecmb_test_remove_component (test_backend, "event-9", NULL);
+
+ ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+ /* Try to add existing event, it should fail */
+ objects = g_slist_prepend (NULL, tcu_new_icalstring_from_test_case ("event-1"));
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->create_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, objects, &uids, &new_components, &error);
+ g_assert_error (error, E_DATA_CAL_ERROR, ObjectIdAlreadyExists);
+ g_assert_null (uids);
+ g_assert_null (new_components);
+ g_clear_error (&error);
+ g_slist_free_full (objects, g_free);
+
+ e_cal_meta_backend_test_reset_counters (test_backend);
+
+ /* Try to add new event */
+ objects = g_slist_prepend (NULL, tcu_new_icalstring_from_test_case ("event-7"));
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->create_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, objects, &uids, &new_components, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (uids), ==, 1);
+ g_assert_cmpstr (uids->data, ==, "event-7");
+ g_assert_cmpint (g_slist_length (new_components), ==, 1);
+ g_assert_cmpint (test_backend->connect_count, ==, 1);
+ g_assert_cmpint (test_backend->list_count, ==, 0);
+ g_assert_cmpint (test_backend->load_count, ==, 1);
+ g_assert_cmpint (test_backend->save_count, ==, 1);
+
+ g_slist_free_full (uids, g_free);
+ g_slist_free_full (new_components, g_object_unref);
+ g_slist_free_full (objects, g_free);
+ uids = NULL;
+ new_components = NULL;
+
+ ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+ /* Going offline */
+ e_cal_meta_backend_test_change_online (meta_backend, FALSE);
+
+ e_cal_meta_backend_test_reset_counters (test_backend);
+
+ /* Try to add existing event, it should fail */
+ objects = g_slist_prepend (NULL, tcu_new_icalstring_from_test_case ("event-7"));
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->create_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, objects, &uids, &new_components, &error);
+ g_assert_error (error, E_DATA_CAL_ERROR, ObjectIdAlreadyExists);
+ g_assert_null (uids);
+ g_assert_null (new_components);
+ g_clear_error (&error);
+ g_slist_free_full (objects, g_free);
+ g_assert_cmpint (test_backend->load_count, ==, 0);
+ g_assert_cmpint (test_backend->save_count, ==, 0);
+
+ /* Try to add new event */
+ objects = g_slist_prepend (NULL, tcu_new_icalstring_from_test_case ("event-8"));
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->create_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, objects, &uids, &new_components, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (uids), ==, 1);
+ g_assert_cmpstr (uids->data, ==, "event-8");
+ g_assert_cmpint (g_slist_length (new_components), ==, 1);
+ g_assert_cmpint (test_backend->connect_count, ==, 0);
+ g_assert_cmpint (test_backend->load_count, ==, 0);
+ g_assert_cmpint (test_backend->save_count, ==, 0);
+
+ g_slist_free_full (uids, g_free);
+ g_slist_free_full (new_components, g_object_unref);
+ g_slist_free_full (objects, g_free);
+ uids = NULL;
+ new_components = NULL;
+
+ ecmb_test_vcalendar_contains (test_backend->vcalendar, TRUE, FALSE,
+ "event-8", NULL, NULL);
+ ecmb_test_cache_contains (cal_cache, FALSE, FALSE,
+ "event-8", NULL, NULL);
+
+ /* Going online */
+ e_cal_meta_backend_test_change_online (meta_backend, TRUE);
+
+ g_assert_cmpint (test_backend->connect_count, ==, 1);
+ g_assert_cmpint (test_backend->load_count, ==, 1);
+ g_assert_cmpint (test_backend->save_count, ==, 1);
+
+ ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+ offline_changes = e_cal_cache_get_offline_changes (cal_cache, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (0, ==, g_slist_length (offline_changes));
+
+ /* Add event without UID */
+ calobj = tcu_new_icalstring_from_test_case ("event-9");
+ g_assert_nonnull (calobj);
+ tmp = strstr (calobj, "UID:event-9\r\n");
+ g_assert_nonnull (tmp);
+ strncpy (tmp, "X-TEST:*007", 11);
+
+ objects = g_slist_prepend (NULL, calobj);
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->create_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, objects, &uids, &new_components, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (uids), ==, 1);
+ g_assert_cmpstr (uids->data, !=, "event-9");
+ g_assert_cmpint (g_slist_length (new_components), ==, 1);
+ g_assert_cmpint (test_backend->connect_count, ==, 1);
+ g_assert_cmpint (test_backend->list_count, ==, 1);
+ g_assert_cmpint (test_backend->load_count, ==, 2);
+ g_assert_cmpint (test_backend->save_count, ==, 2);
+
+ calobj = e_cal_component_get_as_string (new_components->data);
+ g_assert_nonnull (calobj);
+ g_assert_nonnull (strstr (calobj, "X-TEST:*007\r\n"));
+ g_assert_nonnull (strstr (calobj, uids->data));
+
+ g_slist_free_full (uids, g_free);
+ g_slist_free_full (new_components, g_object_unref);
+ g_slist_free_full (objects, g_free);
+ g_free (calobj);
+ uids = NULL;
+ new_components = NULL;
+
+ ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+ g_object_unref (cal_cache);
+}
+
+static gchar *
+ecmb_test_modify_case (const gchar *case_name,
+ const gchar *ridstr)
+{
+ gchar *calobj;
+ icalcomponent *icalcomp;
+
+ g_assert_nonnull (case_name);
+
+ calobj = tcu_new_icalstring_from_test_case (case_name);
+ g_assert_nonnull (calobj);
+ icalcomp = icalcomponent_new_from_string (calobj);
+ g_assert_nonnull (icalcomp);
+ g_free (calobj);
+
+ icalcomponent_set_summary (icalcomp, MODIFIED_SUMMARY_STR);
+ icalcomponent_set_sequence (icalcomp, icalcomponent_get_sequence (icalcomp) + 1);
+
+ if (ridstr)
+ icalcomponent_set_recurrenceid (icalcomp, icaltime_from_string (ridstr));
+
+ calobj = icalcomponent_as_ical_string_r (icalcomp);
+ icalcomponent_free (icalcomp);
+
+ return calobj;
+}
+
+static void
+test_modify_objects (ECalMetaBackend *meta_backend)
+{
+ ECalMetaBackendTest *test_backend;
+ ECalCache *cal_cache;
+ GSList *objects, *old_components = NULL, *new_components = NULL, *offline_changes;
+ gchar *calobj, *tmp;
+ icalcomponent *icalcomp;
+ gint old_sequence;
+ GError *error = NULL;
+
+ g_assert_nonnull (meta_backend);
+
+ test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ g_assert_nonnull (cal_cache);
+
+ /* Modify non-existing event */
+ calobj = tcu_new_icalstring_from_test_case ("event-1");
+ g_assert_nonnull (calobj);
+ tmp = strstr (calobj, "UID:event-1");
+ g_assert_nonnull (tmp);
+ strncpy (tmp + 4, "unknown", 7);
+
+ objects = g_slist_prepend (NULL, calobj);
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->modify_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, objects, E_CAL_OBJ_MOD_ALL, &old_components, &new_components, &error);
+ g_assert_error (error, E_DATA_CAL_ERROR, ObjectNotFound);
+ g_assert_null (old_components);
+ g_assert_null (new_components);
+ g_clear_error (&error);
+ g_slist_free_full (objects, g_free);
+
+ /* Modify existing event */
+ objects = g_slist_prepend (NULL, ecmb_test_modify_case ("event-1", NULL));
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->modify_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, objects, E_CAL_OBJ_MOD_ALL, &old_components, &new_components, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (old_components), ==, 1);
+ g_assert_cmpint (g_slist_length (new_components), ==, 1);
+ g_assert_cmpint (test_backend->load_count, ==, 1);
+ g_assert_cmpint (test_backend->save_count, ==, 1);
+
+ icalcomp = e_cal_component_get_icalcomponent (old_components->data);
+ old_sequence = icalcomponent_get_sequence (icalcomp);
+ g_assert_cmpstr (icalcomponent_get_summary (icalcomp), !=, MODIFIED_SUMMARY_STR);
+ g_assert_cmpstr (icalcomponent_get_uid (icalcomp), ==, "event-1");
+
+ icalcomp = e_cal_component_get_icalcomponent (new_components->data);
+ g_assert_cmpint (old_sequence + 1, ==, icalcomponent_get_sequence (icalcomp));
+ g_assert_cmpstr (icalcomponent_get_summary (icalcomp), ==, MODIFIED_SUMMARY_STR);
+ g_assert_cmpstr (icalcomponent_get_uid (icalcomp), ==, "event-1");
+
+ g_slist_free_full (old_components, g_object_unref);
+ g_slist_free_full (new_components, g_object_unref);
+ g_slist_free_full (objects, g_free);
+ old_components = NULL;
+ new_components = NULL;
+
+ /* Going offline */
+ e_cal_meta_backend_test_change_online (meta_backend, FALSE);
+
+ e_cal_meta_backend_test_reset_counters (test_backend);
+
+ /* Modify event-2 */
+ objects = g_slist_prepend (NULL, ecmb_test_modify_case ("event-2", NULL));
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->modify_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, objects, E_CAL_OBJ_MOD_ALL, &old_components, &new_components, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (old_components), ==, 1);
+ g_assert_cmpint (g_slist_length (new_components), ==, 1);
+ g_assert_cmpint (test_backend->load_count, ==, 0);
+ g_assert_cmpint (test_backend->save_count, ==, 0);
+
+ icalcomp = e_cal_component_get_icalcomponent (old_components->data);
+ old_sequence = icalcomponent_get_sequence (icalcomp);
+ g_assert_cmpstr (icalcomponent_get_summary (icalcomp), !=, MODIFIED_SUMMARY_STR);
+ g_assert_cmpstr (icalcomponent_get_uid (icalcomp), ==, "event-2");
+
+ icalcomp = e_cal_component_get_icalcomponent (new_components->data);
+ g_assert_cmpint (old_sequence + 1, ==, icalcomponent_get_sequence (icalcomp));
+ g_assert_cmpstr (icalcomponent_get_summary (icalcomp), ==, MODIFIED_SUMMARY_STR);
+ g_assert_cmpstr (icalcomponent_get_uid (icalcomp), ==, "event-2");
+
+ g_slist_free_full (old_components, g_object_unref);
+ g_slist_free_full (new_components, g_object_unref);
+ g_slist_free_full (objects, g_free);
+ old_components = NULL;
+ new_components = NULL;
+
+ /* Going online */
+ e_cal_meta_backend_test_change_online (meta_backend, TRUE);
+
+ g_assert_cmpint (test_backend->load_count, ==, 1);
+ g_assert_cmpint (test_backend->save_count, ==, 1);
+
+ ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+ offline_changes = e_cal_cache_get_offline_changes (cal_cache, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (0, ==, g_slist_length (offline_changes));
+
+ /* Modify non-recurring with THIS */
+ objects = g_slist_prepend (NULL, ecmb_test_modify_case ("event-4", NULL));
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->modify_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, objects, E_CAL_OBJ_MOD_THIS, &old_components, &new_components, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (old_components), ==, 1);
+ g_assert_cmpint (g_slist_length (new_components), ==, 1);
+ g_assert_cmpint (test_backend->load_count, ==, 2);
+ g_assert_cmpint (test_backend->save_count, ==, 2);
+
+ icalcomp = e_cal_component_get_icalcomponent (old_components->data);
+ old_sequence = icalcomponent_get_sequence (icalcomp);
+ g_assert_cmpstr (icalcomponent_get_summary (icalcomp), !=, MODIFIED_SUMMARY_STR);
+ g_assert_cmpstr (icalcomponent_get_uid (icalcomp), ==, "event-4");
+
+ icalcomp = e_cal_component_get_icalcomponent (new_components->data);
+ g_assert_cmpint (old_sequence + 1, ==, icalcomponent_get_sequence (icalcomp));
+ g_assert_cmpstr (icalcomponent_get_summary (icalcomp), ==, MODIFIED_SUMMARY_STR);
+ g_assert_cmpstr (icalcomponent_get_uid (icalcomp), ==, "event-4");
+
+ g_slist_free_full (old_components, g_object_unref);
+ g_slist_free_full (new_components, g_object_unref);
+ g_slist_free_full (objects, g_free);
+ old_components = NULL;
+ new_components = NULL;
+
+ /* Modify non-detached recurring instance with ONLY_THIS */
+ objects = g_slist_prepend (NULL, ecmb_test_modify_case ("event-6", "20170227T134900"));
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->modify_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, objects, E_CAL_OBJ_MOD_ONLY_THIS, &old_components, &new_components, &error);
+ g_assert_error (error, E_DATA_CAL_ERROR, ObjectNotFound);
+ g_assert_null (old_components);
+ g_assert_null (new_components);
+ g_assert_cmpint (test_backend->load_count, ==, 2);
+ g_assert_cmpint (test_backend->save_count, ==, 2);
+ g_clear_error (&error);
+ g_slist_free_full (objects, g_free);
+
+ /* Modify detached recurring instance with ONLY_THIS */
+ objects = g_slist_prepend (NULL, ecmb_test_modify_case ("event-6-a", NULL));
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->modify_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, objects, E_CAL_OBJ_MOD_ONLY_THIS, &old_components, &new_components, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (old_components), ==, 1);
+ g_assert_cmpint (g_slist_length (new_components), ==, 1);
+ g_assert_cmpint (test_backend->load_count, ==, 3);
+ g_assert_cmpint (test_backend->save_count, ==, 3);
+
+ icalcomp = e_cal_component_get_icalcomponent (old_components->data);
+ old_sequence = icalcomponent_get_sequence (icalcomp);
+ g_assert_cmpstr (icalcomponent_get_uid (icalcomp), ==, "event-6");
+ g_assert_cmpstr (icalcomponent_get_summary (icalcomp), !=, MODIFIED_SUMMARY_STR);
+
+ icalcomp = e_cal_component_get_icalcomponent (new_components->data);
+ g_assert_cmpstr (icalcomponent_get_uid (icalcomp), ==, "event-6");
+ g_assert_cmpstr (icalcomponent_get_summary (icalcomp), ==, MODIFIED_SUMMARY_STR);
+ g_assert_cmpint (old_sequence + 1, ==, icalcomponent_get_sequence (icalcomp));
+
+ g_slist_free_full (old_components, g_object_unref);
+ g_slist_free_full (new_components, g_object_unref);
+ g_slist_free_full (objects, g_free);
+ old_components = NULL;
+ new_components = NULL;
+
+ ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+ g_object_unref (cal_cache);
+}
+
+static void
+test_remove_objects (ECalMetaBackend *meta_backend)
+{
+ ECalMetaBackendTest *test_backend;
+ ECalCache *cal_cache;
+ GSList *ids, *old_components = NULL, *new_components = NULL, *offline_changes;
+ GError *error = NULL;
+
+ g_assert_nonnull (meta_backend);
+
+ test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ g_assert_nonnull (cal_cache);
+
+ /* Remove non-existing event */
+ ids = g_slist_prepend (NULL, e_cal_component_id_new ("unknown-event", NULL));
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->remove_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, ids, E_CAL_OBJ_MOD_ALL, &old_components, &new_components, &error);
+ g_assert_error (error, E_DATA_CAL_ERROR, ObjectNotFound);
+ g_assert_null (old_components);
+ g_assert_null (new_components);
+ g_clear_error (&error);
+ g_slist_free_full (ids, (GDestroyNotify) e_cal_component_free_id);
+
+ /* Remove existing event */
+ ids = g_slist_prepend (NULL, e_cal_component_id_new ("event-1", NULL));
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->remove_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, ids, E_CAL_OBJ_MOD_ALL, &old_components, &new_components, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (old_components), ==, 1);
+ g_assert_cmpint (g_slist_length (new_components), ==, 1);
+ g_assert_null (new_components->data);
+ g_assert_cmpint (test_backend->load_count, ==, 0);
+ g_assert_cmpint (test_backend->save_count, ==, 0);
+ g_assert_cmpint (test_backend->remove_count, ==, 1);
+
+ ecmb_test_vcalendar_contains (test_backend->vcalendar, TRUE, FALSE,
+ "event-1", NULL,
+ NULL);
+
+ g_slist_free_full (old_components, g_object_unref);
+ g_slist_free (new_components);
+ g_slist_free_full (ids, (GDestroyNotify) e_cal_component_free_id);
+ old_components = NULL;
+ new_components = NULL;
+
+ /* Remove existing detached instance */
+ ecmb_test_vcalendar_contains (test_backend->vcalendar, FALSE, FALSE,
+ "event-6", "20170225T134900",
+ NULL);
+
+ ids = g_slist_prepend (NULL, e_cal_component_id_new ("event-6", "20170225T134900"));
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->remove_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, ids, E_CAL_OBJ_MOD_THIS, &old_components, &new_components, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (old_components), ==, 1);
+ g_assert_cmpint (g_slist_length (new_components), ==, 1);
+ g_assert_cmpint (test_backend->load_count, ==, 1);
+ g_assert_cmpint (test_backend->save_count, ==, 1);
+ g_assert_cmpint (test_backend->remove_count, ==, 1);
+
+ /* Master object is there */
+ ecmb_test_vcalendar_contains (test_backend->vcalendar, FALSE, FALSE,
+ "event-6", NULL,
+ NULL);
+ /* Just-removed detached instance is not there */
+ ecmb_test_vcalendar_contains (test_backend->vcalendar, TRUE, FALSE,
+ "event-6", "20170225T134900",
+ NULL);
+
+ g_slist_free_full (old_components, g_object_unref);
+ g_slist_free_full (new_components, g_object_unref);
+ g_slist_free_full (ids, (GDestroyNotify) e_cal_component_free_id);
+ old_components = NULL;
+ new_components = NULL;
+
+ /* Remove non-existing detached instance with ONLY_THIS - fails */
+ ecmb_test_vcalendar_contains (test_backend->vcalendar, TRUE, FALSE,
+ "event-6", "20170227T134900",
+ NULL);
+
+ ids = g_slist_prepend (NULL, e_cal_component_id_new ("event-6", "20170227T134900"));
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->remove_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, ids, E_CAL_OBJ_MOD_ONLY_THIS, &old_components, &new_components, &error);
+ g_assert_error (error, E_DATA_CAL_ERROR, ObjectNotFound);
+ g_assert_null (old_components);
+ g_assert_null (new_components);
+ g_clear_error (&error);
+ g_slist_free_full (ids, (GDestroyNotify) e_cal_component_free_id);
+
+ /* Remove non-existing detached instance with THIS - changes master object */
+ ids = g_slist_prepend (NULL, e_cal_component_id_new ("event-6", "20170227T134900"));
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->remove_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, ids, E_CAL_OBJ_MOD_THIS, &old_components, &new_components, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (old_components), ==, 1);
+ g_assert_cmpint (g_slist_length (new_components), ==, 1);
+ g_assert_cmpint (test_backend->load_count, ==, 2);
+ g_assert_cmpint (test_backend->save_count, ==, 2);
+ g_assert_cmpint (test_backend->remove_count, ==, 1);
+
+ /* Master object is there */
+ ecmb_test_vcalendar_contains (test_backend->vcalendar, FALSE, FALSE,
+ "event-6", NULL,
+ NULL);
+ /* Just-removed detached instance is not there */
+ ecmb_test_vcalendar_contains (test_backend->vcalendar, TRUE, FALSE,
+ "event-6", "20170227T134900",
+ NULL);
+
+ g_slist_free_full (old_components, g_object_unref);
+ g_slist_free_full (new_components, g_object_unref);
+ g_slist_free_full (ids, (GDestroyNotify) e_cal_component_free_id);
+ old_components = NULL;
+ new_components = NULL;
+
+ /* Going offline */
+ e_cal_meta_backend_test_change_online (meta_backend, FALSE);
+
+ e_cal_meta_backend_test_reset_counters (test_backend);
+
+ /* Remove existing event */
+ ids = g_slist_prepend (NULL, e_cal_component_id_new ("event-3", NULL));
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->remove_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, ids, E_CAL_OBJ_MOD_ONLY_THIS, &old_components, &new_components, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (old_components), ==, 1);
+ g_assert_cmpint (g_slist_length (new_components), ==, 1);
+ g_assert_null (new_components->data);
+ g_assert_cmpint (test_backend->load_count, ==, 0);
+ g_assert_cmpint (test_backend->save_count, ==, 0);
+ g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+ ecmb_test_vcalendar_contains (test_backend->vcalendar, FALSE, FALSE,
+ "event-3", NULL,
+ NULL);
+ ecmb_test_cache_contains (cal_cache, TRUE, FALSE,
+ "event-3", NULL,
+ NULL);
+
+ g_slist_free_full (old_components, g_object_unref);
+ g_slist_free (new_components);
+ g_slist_free_full (ids, (GDestroyNotify) e_cal_component_free_id);
+ old_components = NULL;
+ new_components = NULL;
+
+ /* Going online */
+ e_cal_meta_backend_test_change_online (meta_backend, TRUE);
+
+ g_assert_cmpint (test_backend->load_count, ==, 0);
+ g_assert_cmpint (test_backend->save_count, ==, 0);
+ g_assert_cmpint (test_backend->remove_count, ==, 1);
+
+ ecmb_test_vcalendar_contains (test_backend->vcalendar, TRUE, FALSE,
+ "event-3", NULL,
+ NULL);
+ ecmb_test_cache_contains (cal_cache, TRUE, FALSE,
+ "event-3", NULL,
+ NULL);
+
+ ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+ offline_changes = e_cal_cache_get_offline_changes (cal_cache, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (0, ==, g_slist_length (offline_changes));
+
+ g_object_unref (cal_cache);
+}
+
+static void
+test_receive_objects (ECalMetaBackend *meta_backend)
+{
+ ECalMetaBackendTest *test_backend;
+ ECalCache *cal_cache;
+ gchar *calobj;
+ icalcomponent *icalcomp;
+ GSList *ids, *old_components = NULL, *new_components = NULL;
+ GError *error = NULL;
+
+ g_assert_nonnull (meta_backend);
+
+ test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ g_assert_nonnull (cal_cache);
+
+ /* Organizer side - receives reply from an attendee */
+ calobj = tcu_new_icalstring_from_test_case ("invite-1");
+ g_assert_nonnull (calobj);
+
+ icalcomp = icalcomponent_new_from_string (calobj);
+ g_assert_nonnull (icalcomp);
+ g_assert_nonnull (icalcomponent_get_first_component (icalcomp, ICAL_VEVENT_COMPONENT));
+ g_free (calobj);
+
+ icalcomponent_add_component (test_backend->vcalendar, icalcomponent_new_clone
(icalcomponent_get_first_component (icalcomp, ICAL_VEVENT_COMPONENT)));
+
+ icalcomponent_free (icalcomp);
+
+ /* To get the 'invite' component into local cache */
+ e_cal_meta_backend_test_call_refresh (meta_backend);
+
+ ecmb_test_vcalendar_contains (test_backend->vcalendar, FALSE, FALSE,
+ "invite", NULL,
+ NULL);
+ ecmb_test_cache_contains (cal_cache, FALSE, FALSE,
+ "invite", NULL,
+ NULL);
+
+ calobj = NULL;
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_object_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, "invite", NULL, &calobj, &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (calobj);
+ g_assert_nonnull (strstr (calobj, "PARTSTAT=NEEDS-ACTION"));
+ g_assert_null (strstr (calobj, "PARTSTAT=ACCEPTED"));
+ g_free (calobj);
+
+ calobj = tcu_new_icalstring_from_test_case ("invite-2");
+ g_assert_nonnull (calobj);
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->receive_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, calobj, &error);
+ g_assert_no_error (error);
+ g_free (calobj);
+
+ g_assert_cmpint (test_backend->load_count, ==, 2);
+ g_assert_cmpint (test_backend->save_count, ==, 1);
+ g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+ calobj = NULL;
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_object_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, "invite", NULL, &calobj, &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (calobj);
+ g_assert_null (strstr (calobj, "PARTSTAT=NEEDS-ACTION"));
+ g_assert_nonnull (strstr (calobj, "PARTSTAT=ACCEPTED"));
+ g_free (calobj);
+
+ ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+ /* Remove the 'invite' component, to test also user side */
+ ids = g_slist_prepend (NULL, e_cal_component_id_new ("invite", NULL));
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->remove_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, ids, E_CAL_OBJ_MOD_ALL, &old_components, &new_components, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (g_slist_length (old_components), ==, 1);
+ g_assert_cmpint (g_slist_length (new_components), ==, 1);
+ g_assert_null (new_components->data);
+ g_assert_cmpint (test_backend->load_count, ==, 2);
+ g_assert_cmpint (test_backend->save_count, ==, 1);
+ g_assert_cmpint (test_backend->remove_count, ==, 1);
+
+ g_slist_free_full (old_components, g_object_unref);
+ g_slist_free (new_components);
+ g_slist_free_full (ids, (GDestroyNotify) e_cal_component_free_id);
+ old_components = NULL;
+ new_components = NULL;
+
+ ecmb_test_vcalendar_contains (test_backend->vcalendar, TRUE, FALSE,
+ "invite", NULL,
+ NULL);
+ ecmb_test_cache_contains (cal_cache, TRUE, FALSE,
+ "invite", NULL,
+ NULL);
+
+ ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+ /* User side - receives invitation */
+ calobj = tcu_new_icalstring_from_test_case ("invite-1");
+ g_assert_nonnull (calobj);
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->receive_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, calobj, &error);
+ g_assert_no_error (error);
+ g_free (calobj);
+
+ g_assert_cmpint (test_backend->load_count, ==, 3);
+ g_assert_cmpint (test_backend->save_count, ==, 2);
+ g_assert_cmpint (test_backend->remove_count, ==, 1);
+
+ ecmb_test_vcalendar_contains (test_backend->vcalendar, FALSE, FALSE,
+ "invite", NULL,
+ NULL);
+ ecmb_test_cache_contains (cal_cache, FALSE, FALSE,
+ "invite", NULL,
+ NULL);
+
+ ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+ calobj = NULL;
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_object_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, "invite", NULL, &calobj, &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (calobj);
+ g_assert_nonnull (strstr (calobj, "SUMMARY:Invite\r\n"));
+ g_assert_null (strstr (calobj, "SUMMARY:Invite (modified)\r\n"));
+ g_free (calobj);
+
+ /* Receives update from organizer */
+ calobj = tcu_new_icalstring_from_test_case ("invite-3");
+ g_assert_nonnull (calobj);
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->receive_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, calobj, &error);
+ g_assert_no_error (error);
+ g_free (calobj);
+
+ g_assert_cmpint (test_backend->load_count, ==, 4);
+ g_assert_cmpint (test_backend->save_count, ==, 3);
+ g_assert_cmpint (test_backend->remove_count, ==, 1);
+
+ ecmb_test_vcalendar_contains (test_backend->vcalendar, FALSE, FALSE,
+ "invite", NULL,
+ NULL);
+ ecmb_test_cache_contains (cal_cache, FALSE, FALSE,
+ "invite", NULL,
+ NULL);
+
+ ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+ calobj = NULL;
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_object_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, "invite", NULL, &calobj, &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (calobj);
+ g_assert_null (strstr (calobj, "SUMMARY:Invite\r\n"));
+ g_assert_nonnull (strstr (calobj, "SUMMARY:Invite (modified)\r\n"));
+ g_free (calobj);
+
+ /* Receives cancellation from organizer */
+ calobj = tcu_new_icalstring_from_test_case ("invite-4");
+ g_assert_nonnull (calobj);
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->receive_objects_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, calobj, &error);
+ g_assert_no_error (error);
+ g_free (calobj);
+
+ g_assert_cmpint (test_backend->load_count, ==, 4);
+ g_assert_cmpint (test_backend->save_count, ==, 3);
+ g_assert_cmpint (test_backend->remove_count, ==, 2);
+
+ ecmb_test_vcalendar_contains (test_backend->vcalendar, TRUE, FALSE,
+ "invite", NULL,
+ NULL);
+ ecmb_test_cache_contains (cal_cache, TRUE, FALSE,
+ "invite", NULL,
+ NULL);
+
+ ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+ g_object_unref (cal_cache);
+}
+
+static void
+test_get_object (ECalMetaBackend *meta_backend)
+{
+ ECalMetaBackendTest *test_backend;
+ ECalCache *cal_cache;
+ gchar *calobj = NULL;
+ GError *error = NULL;
+
+ g_assert_nonnull (meta_backend);
+
+ test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ g_assert_nonnull (cal_cache);
+
+ e_cal_cache_remove_component (cal_cache, "event-7", NULL, E_CACHE_IS_ONLINE, NULL, &error);
+ g_assert_no_error (error);
+ e_cal_cache_remove_component (cal_cache, "event-8", NULL, E_CACHE_IS_ONLINE, NULL, &error);
+ g_assert_no_error (error);
+ e_cal_cache_remove_component (cal_cache, "event-9", NULL, E_CACHE_IS_ONLINE, NULL, &error);
+ g_assert_no_error (error);
+
+ /* Master object */
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_object_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, "event-6", NULL, &calobj, &error);
+
+ g_assert_no_error (error);
+ g_assert_nonnull (calobj);
+ g_assert (strstr (calobj, "UID:event-6"));
+ g_assert (!strstr (calobj, "RECURRENCE-ID;TZID=America/New_York:20170225T134900"));
+ g_free (calobj);
+ calobj = NULL;
+
+ /* Detached instance */
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_object_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, "event-6", "20170225T134900", &calobj, &error);
+
+ g_assert_no_error (error);
+ g_assert_nonnull (calobj);
+ g_assert (strstr (calobj, "UID:event-6"));
+ g_assert (strstr (calobj, "RECURRENCE-ID;TZID=America/New_York:20170225T134900"));
+ g_free (calobj);
+ calobj = NULL;
+
+ /* Going offline */
+ e_cal_meta_backend_test_change_online (meta_backend, FALSE);
+
+ g_assert (!e_cal_cache_contains (cal_cache, "event-7", NULL, E_CACHE_EXCLUDE_DELETED));
+
+ e_cal_meta_backend_test_reset_counters (test_backend);
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_object_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, "event-7", NULL, &calobj, &error);
+ g_assert_error (error, E_DATA_CAL_ERROR, ObjectNotFound);
+ g_assert_null (calobj);
+ g_clear_error (&error);
+ g_assert_cmpint (test_backend->connect_count, ==, 0);
+ g_assert_cmpint (test_backend->list_count, ==, 0);
+ g_assert_cmpint (test_backend->load_count, ==, 0);
+
+ /* Going online */
+ e_cal_meta_backend_test_change_online (meta_backend, TRUE);
+
+ g_assert (e_cal_cache_contains (cal_cache, "event-7", NULL, E_CACHE_EXCLUDE_DELETED));
+
+ /* Remove it from the cache, thus it's loaded from the "server" on demand */
+ e_cal_cache_remove_component (cal_cache, "event-7", NULL, E_CACHE_IS_ONLINE, NULL, &error);
+ g_assert_no_error (error);
+
+ g_assert_cmpint (test_backend->connect_count, ==, 1);
+ e_cal_meta_backend_test_reset_counters (test_backend);
+ g_assert_cmpint (test_backend->connect_count, ==, 0);
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_object_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, "event-7", NULL, &calobj, &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (calobj);
+ g_assert_cmpint (test_backend->connect_count, ==, 0);
+ g_assert_cmpint (test_backend->list_count, ==, 0);
+ g_assert_cmpint (test_backend->load_count, ==, 1);
+ g_assert_nonnull (strstr (calobj, "UID:event-7"));
+ g_free (calobj);
+
+ g_assert (e_cal_cache_contains (cal_cache, "event-7", NULL, E_CACHE_EXCLUDE_DELETED));
+
+ g_object_unref (cal_cache);
+}
+
+static void
+test_get_object_list (ECalMetaBackend *meta_backend)
+{
+ GSList *calobjs = NULL;
+ GError *error = NULL;
+
+ g_assert_nonnull (meta_backend);
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_object_list_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, "(uid? \"unknown-event\")", &calobjs, &error);
+ g_assert_no_error (error);
+ g_assert_null (calobjs);
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_object_list_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, "(uid? \"event-3\")", &calobjs, &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (calobjs);
+ g_assert_cmpint (g_slist_length (calobjs), ==, 1);
+ g_assert (strstr (calobjs->data, "UID:event-3"));
+ g_slist_free_full (calobjs, g_free);
+ calobjs = NULL;
+
+ E_CAL_BACKEND_SYNC_GET_CLASS (meta_backend)->get_object_list_sync (E_CAL_BACKEND_SYNC (meta_backend),
+ NULL, NULL, "(uid? \"event-6\")", &calobjs, &error);
+ g_assert_no_error (error);
+ g_assert_nonnull (calobjs);
+ g_assert_cmpint (g_slist_length (calobjs), ==, 2);
+ g_assert (strstr (calobjs->data, "UID:event-6"));
+ g_assert (strstr (calobjs->next->data, "UID:event-6"));
+ g_assert_cmpstr (calobjs->data, !=, calobjs->next->data);
+ g_slist_free_full (calobjs, g_free);
+}
+
+static void
+test_refresh (ECalMetaBackend *meta_backend)
+{
+ ECalMetaBackendTest *test_backend;
+ ECalCache *cal_cache;
+ ECache *cache;
+ guint count;
+ icalcomponent *icalcomp;
+ GError *error = NULL;
+
+ g_assert_nonnull (meta_backend);
+
+ test_backend = E_CAL_META_BACKEND_TEST (meta_backend);
+ cal_cache = e_cal_meta_backend_ref_cache (meta_backend);
+ g_assert_nonnull (cal_cache);
+
+ cache = E_CACHE (cal_cache);
+
+ /* Empty local cache */
+ e_cache_remove_all (cache, NULL, &error);
+ g_assert_no_error (error);
+
+ count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (count, ==, 0);
+
+ e_cal_meta_backend_test_reset_counters (test_backend);
+
+ ecmb_test_remove_component (test_backend, "event-6", "20170225T134900");
+ ecmb_test_remove_component (test_backend, "event-7", NULL);
+ ecmb_test_remove_component (test_backend, "event-8", NULL);
+ ecmb_test_remove_component (test_backend, "event-9", NULL);
+
+ /* Sync with server content */
+ e_cal_meta_backend_test_call_refresh (meta_backend);
+
+ g_assert_cmpint (test_backend->list_count, ==, 1);
+ g_assert_cmpint (test_backend->save_count, ==, 0);
+ g_assert_cmpint (test_backend->load_count, ==, 6);
+ g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+ count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (count, ==, 6);
+
+ ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+ /* Add detached instance, but do not modify the master object, thus it looks like unchanged */
+ ecmb_test_add_test_case (test_backend, "event-6-a");
+
+ /* Sync with server content */
+ e_cal_meta_backend_test_call_refresh (meta_backend);
+
+ g_assert_cmpint (test_backend->list_count, ==, 2);
+ g_assert_cmpint (test_backend->save_count, ==, 0);
+ g_assert_cmpint (test_backend->load_count, ==, 6);
+ g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+ count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (count, ==, 6);
+
+ ecmb_test_vcalendar_contains (test_backend->vcalendar, FALSE, TRUE,
+ "event-1", NULL,
+ "event-2", NULL,
+ "event-3", NULL,
+ "event-4", NULL,
+ "event-5", NULL,
+ "event-6", NULL,
+ "event-6", "20170225T134900",
+ NULL);
+
+ ecmb_test_cache_contains (cal_cache, FALSE, TRUE,
+ "event-1", NULL,
+ "event-2", NULL,
+ "event-3", NULL,
+ "event-4", NULL,
+ "event-5", NULL,
+ "event-6", NULL,
+ NULL);
+
+ /* Modify the master object, thus the detached instance will be recognized */
+ for (icalcomp = icalcomponent_get_first_component (test_backend->vcalendar, ICAL_VEVENT_COMPONENT);
+ icalcomp;
+ icalcomp = icalcomponent_get_next_component (test_backend->vcalendar, ICAL_VEVENT_COMPONENT)) {
+ if (g_strcmp0 ("event-6", icalcomponent_get_uid (icalcomp)) == 0) {
+ icalcomponent_set_sequence (icalcomp, icalcomponent_get_sequence (icalcomp) + 1);
+ }
+ }
+
+ /* Sync with server content */
+ e_cal_meta_backend_test_call_refresh (meta_backend);
+
+ g_assert_cmpint (test_backend->list_count, ==, 3);
+ g_assert_cmpint (test_backend->save_count, ==, 0);
+ g_assert_cmpint (test_backend->load_count, ==, 7);
+ g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+ count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (count, ==, 7);
+
+ ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+ /* Add some more events */
+ ecmb_test_add_test_case (test_backend, "event-7");
+ ecmb_test_add_test_case (test_backend, "event-9");
+
+ /* Sync with server content */
+ e_cal_meta_backend_test_call_refresh (meta_backend);
+
+ g_assert_cmpint (test_backend->list_count, ==, 4);
+ g_assert_cmpint (test_backend->save_count, ==, 0);
+ g_assert_cmpint (test_backend->load_count, ==, 9);
+ g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+ count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (count, ==, 9);
+
+ ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+ /* Remove two events */
+ ecmb_test_remove_component (test_backend, "event-2", NULL);
+ ecmb_test_remove_component (test_backend, "event-4", NULL);
+
+ /* Sync with server content */
+ e_cal_meta_backend_test_call_refresh (meta_backend);
+
+ g_assert_cmpint (test_backend->list_count, ==, 5);
+ g_assert_cmpint (test_backend->save_count, ==, 0);
+ g_assert_cmpint (test_backend->load_count, ==, 9);
+ g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+ count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (count, ==, 7);
+
+ ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+ /* Mix add/remove/modify */
+ ecmb_test_add_test_case (test_backend, "event-8");
+
+ ecmb_test_remove_component (test_backend, "event-3", NULL);
+ ecmb_test_remove_component (test_backend, "event-6", NULL);
+ ecmb_test_remove_component (test_backend, "event-6", "20170225T134900");
+
+ for (icalcomp = icalcomponent_get_first_component (test_backend->vcalendar, ICAL_VEVENT_COMPONENT);
+ icalcomp;
+ icalcomp = icalcomponent_get_next_component (test_backend->vcalendar, ICAL_VEVENT_COMPONENT)) {
+ if (g_strcmp0 ("event-5", icalcomponent_get_uid (icalcomp)) == 0 ||
+ g_strcmp0 ("event-9", icalcomponent_get_uid (icalcomp)) == 0) {
+ icalcomponent_set_sequence (icalcomp, icalcomponent_get_sequence (icalcomp) + 1);
+ }
+ }
+
+ /* Sync with server content */
+ e_cal_meta_backend_test_call_refresh (meta_backend);
+
+ g_assert_cmpint (test_backend->list_count, ==, 6);
+ g_assert_cmpint (test_backend->save_count, ==, 0);
+ g_assert_cmpint (test_backend->load_count, ==, 12);
+ g_assert_cmpint (test_backend->remove_count, ==, 0);
+
+ count = e_cache_get_count (cache, E_CACHE_INCLUDE_DELETED, NULL, &error);
+ g_assert_no_error (error);
+ g_assert_cmpint (count, ==, 5);
+
+ ecmb_test_cache_and_server_equal (cal_cache, test_backend->vcalendar, E_CACHE_INCLUDE_DELETED);
+
+ g_object_unref (cal_cache);
+}
+
+typedef void (* TestWithMainLoopFunc) (ECalMetaBackend *meta_backend);
+
+typedef struct _MainLoopThreadData {
+ TestWithMainLoopFunc func;
+ ECalMetaBackend *meta_backend;
+ GMainLoop *main_loop;
+} MainLoopThreadData;
+
+static gpointer
+test_with_main_loop_thread (gpointer user_data)
+{
+ MainLoopThreadData *mlt = user_data;
+
+ g_assert_nonnull (mlt);
+ g_assert_nonnull (mlt->func);
+ g_assert_nonnull (mlt->meta_backend);
+
+ mlt->func (mlt->meta_backend);
+
+ g_main_loop_quit (mlt->main_loop);
+
+ return NULL;
+}
+
+static gboolean
+quit_test_with_mainloop_cb (gpointer user_data)
+{
+ GMainLoop *main_loop = user_data;
+
+ g_assert_nonnull (main_loop);
+
+ g_main_loop_quit (main_loop);
+
+ g_assert_not_reached ();
+
+ return FALSE;
+}
+
+static gboolean
+test_with_mainloop_run_thread_idle (gpointer user_data)
+{
+ GThread *thread;
+
+ g_assert_nonnull (user_data);
+
+ thread = g_thread_new (NULL, test_with_main_loop_thread, user_data);
+ g_thread_unref (thread);
+
+ return FALSE;
+}
+
+static void
+test_with_main_loop (ECalCache *cal_cache,
+ TestWithMainLoopFunc func)
+{
+ MainLoopThreadData mlt;
+ ECalMetaBackend *meta_backend;
+ guint timeout_id;
+
+ g_assert_nonnull (cal_cache);
+ g_assert_nonnull (func);
+
+ meta_backend = e_cal_meta_backend_test_new (cal_cache);
+ g_assert_nonnull (meta_backend);
+
+ mlt.func = func;
+ mlt.meta_backend = meta_backend;
+ mlt.main_loop = g_main_loop_new (NULL, FALSE);
+
+ g_idle_add (test_with_mainloop_run_thread_idle, &mlt);
+ timeout_id = g_timeout_add_seconds (10, quit_test_with_mainloop_cb, mlt.main_loop);
+
+ g_main_loop_run (mlt.main_loop);
+
+ g_source_remove (timeout_id);
+ g_main_loop_unref (mlt.main_loop);
+ g_clear_object (&mlt.meta_backend);
+}
+
+#define main_loop_wrapper(_func) \
+static void \
+_func ## _tcu (TCUFixture *fixture, \
+ gconstpointer user_data) \
+{ \
+ test_with_main_loop (fixture->cal_cache, _func); \
+}
+
+main_loop_wrapper (test_send_objects)
+main_loop_wrapper (test_get_attachment_uris)
+main_loop_wrapper (test_discard_alarm)
+main_loop_wrapper (test_timezones)
+main_loop_wrapper (test_get_free_busy)
+main_loop_wrapper (test_create_objects)
+main_loop_wrapper (test_modify_objects)
+main_loop_wrapper (test_remove_objects)
+main_loop_wrapper (test_receive_objects)
+main_loop_wrapper (test_get_object)
+main_loop_wrapper (test_get_object_list)
+main_loop_wrapper (test_refresh)
+
+#undef main_loop_wrapper
+
+gint
+main (gint argc,
+ gchar **argv)
+{
+ ETestServerClosure tsclosure = {
+ E_TEST_SERVER_NONE,
+ NULL, /* Source customization function */
+ 0, /* Calendar Type */
+ TRUE, /* Keep the working sandbox after the test, don't remove it */
+ NULL, /* Destroy Notify function */
+ };
+ ETestServerFixture tsfixture = { 0 };
+ TCUClosure closure_events = { TCU_LOAD_COMPONENT_SET_EVENTS };
+ gint res;
+
+#if !GLIB_CHECK_VERSION (2, 35, 1)
+ g_type_init ();
+#endif
+ g_test_init (&argc, &argv, NULL);
+
+ /* Ensure that the client and server get the same locale */
+ g_assert (g_setenv ("LC_ALL", "en_US.UTF-8", TRUE));
+ setlocale (LC_ALL, "");
+
+#ifdef HAVE_ICALTZUTIL_SET_EXACT_VTIMEZONES_SUPPORT
+ icaltzutil_set_exact_vtimezones_support (0);
+#endif
+
+ e_test_server_utils_setup (&tsfixture, &tsclosure);
+
+ glob_registry = tsfixture.registry;
+ g_assert_nonnull (glob_registry);
+
+ g_test_add ("/ECalMetaBackend/MergeInstances", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_merge_instances, tcu_fixture_teardown);
+ g_test_add ("/ECalMetaBackend/Attachments", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_attachments, tcu_fixture_teardown);
+ g_test_add ("/ECalMetaBackend/EmptyCache", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_empty_cache, tcu_fixture_teardown);
+ g_test_add ("/ECalMetaBackend/SendObjects", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_send_objects_tcu, tcu_fixture_teardown);
+ g_test_add ("/ECalMetaBackend/GetAttachmentUris", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_get_attachment_uris_tcu, tcu_fixture_teardown);
+ g_test_add ("/ECalMetaBackend/DiscardAlarm", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_discard_alarm_tcu, tcu_fixture_teardown);
+ g_test_add ("/ECalMetaBackend/Timezones", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_timezones_tcu, tcu_fixture_teardown);
+ g_test_add ("/ECalMetaBackend/GetFreeBusy", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_get_free_busy_tcu, tcu_fixture_teardown);
+ g_test_add ("/ECalMetaBackend/CreateObjects", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_create_objects_tcu, tcu_fixture_teardown);
+ g_test_add ("/ECalMetaBackend/ModifyObjects", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_modify_objects_tcu, tcu_fixture_teardown);
+ g_test_add ("/ECalMetaBackend/RemoveObjects", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_remove_objects_tcu, tcu_fixture_teardown);
+ g_test_add ("/ECalMetaBackend/ReceiveObjects", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_receive_objects_tcu, tcu_fixture_teardown);
+ g_test_add ("/ECalMetaBackend/GetObject", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_get_object_tcu, tcu_fixture_teardown);
+ g_test_add ("/ECalMetaBackend/GetObjectList", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_get_object_list_tcu, tcu_fixture_teardown);
+ g_test_add ("/ECalMetaBackend/Refresh", TCUFixture, &closure_events,
+ tcu_fixture_setup, test_refresh_tcu, tcu_fixture_teardown);
+
+ res = g_test_run ();
+
+ e_test_server_utils_teardown (&tsfixture, &tsclosure);
+
+ return res;
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]