/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ /* * Author: * Chris Toshok (toshok@ximian.com) * * Copyright (C) 2003, Ximian, Inc. */ #include "config.h" #include "pas-backend-vcf.h" #include "pas-backend-card-sexp.h" #include "pas-book.h" #include "pas-book-view.h" #include #include #include #include #include #include #include #include #include #include #include #define PAS_ID_PREFIX "pas-id-" #define FILE_FLUSH_TIMEOUT 5000 static PASBackendSyncClass *pas_backend_vcf_parent_class; typedef struct _PASBackendVCFBookView PASBackendVCFBookView; typedef struct _PASBackendVCFSearchContext PASBackendVCFSearchContext; struct _PASBackendVCFPrivate { char *uri; char *filename; GHashTable *contacts; gboolean dirty; int flush_timeout_tag; }; static char * pas_backend_vcf_create_unique_id () { /* use a 32 counter and the 32 bit timestamp to make an id. it's doubtful 2^32 id's will be created in a second, so we should be okay. */ static guint c = 0; return g_strdup_printf (PAS_ID_PREFIX "%08lX%08X", time(NULL), c++); } typedef struct { PASBackendVCF *bvcf; PASBook *book; PASBookView *view; } VCFBackendSearchClosure; static void free_search_closure (VCFBackendSearchClosure *closure) { g_free (closure); } static void foreach_search_compare (char *id, char *vcard_string, VCFBackendSearchClosure *closure) { EContact *contact; contact = e_contact_new_from_vcard (vcard_string); pas_book_view_notify_update (closure->view, contact); g_object_unref (contact); } static gboolean pas_backend_vcf_search_timeout (gpointer data) { VCFBackendSearchClosure *closure = data; g_hash_table_foreach (closure->bvcf->priv->contacts, (GHFunc)foreach_search_compare, closure); pas_book_view_notify_complete (closure->view, GNOME_Evolution_Addressbook_Success); free_search_closure (closure); return FALSE; } static void pas_backend_vcf_search (PASBackendVCF *bvcf, PASBookView *book_view) { const char *query = pas_book_view_get_card_query (book_view); VCFBackendSearchClosure *closure = g_new0 (VCFBackendSearchClosure, 1); if ( ! strcmp (query, "(contains \"x-evolution-any-field\" \"\")")) pas_book_view_notify_status_message (book_view, _("Loading...")); else pas_book_view_notify_status_message (book_view, _("Searching...")); closure->view = book_view; closure->bvcf = bvcf; g_idle_add (pas_backend_vcf_search_timeout, closure); } static void insert_contact (PASBackendVCF *vcf, char *vcard) { EContact *contact = e_contact_new_from_vcard (vcard); char *id; id = e_contact_get (contact, E_CONTACT_UID); if (id) g_hash_table_insert (vcf->priv->contacts, id, e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30)); } static void load_file (PASBackendVCF *vcf) { FILE *fp; GString *str; char buf[1024]; fp = fopen (vcf->priv->filename, "r"); if (!fp) { g_warning ("failed to open `%s' for reading", vcf->priv->filename); return; } str = g_string_new (""); while (fgets (buf, sizeof (buf), fp)) { if (!strcmp (buf, "\r\n")) { /* if the string has accumulated some stuff, create a contact for it and start over */ if (str->len) { insert_contact (vcf, str->str); g_string_assign (str, ""); } } else { g_string_append (str, buf); } } if (str->len) { insert_contact (vcf, str->str); } g_string_free (str, TRUE); fclose (fp); } static void foreach_build_list (char *id, char *vcard_string, GList **list) { *list = g_list_append (*list, e_contact_new_from_vcard (vcard_string)); } static gboolean save_file (PASBackendVCF *vcf) { GList *contacts = NULL; GList *l; char *new_path; int fd, rv; g_hash_table_foreach (vcf->priv->contacts, (GHFunc)foreach_build_list, &contacts); new_path = g_strdup_printf ("%s.new", vcf->priv->filename); fd = open (new_path, O_CREAT | O_TRUNC | O_WRONLY, 0666); for (l = contacts; l; l = l->next) { EContact *contact = l->data; char *vcard_str = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30); int len = strlen (vcard_str); rv = write (fd, vcard_str, len); g_free (vcard_str); if (rv < len) { /* XXX */ g_warning ("write failed. we need to handle short writes\n"); close (fd); unlink (new_path); return FALSE; } rv = write (fd, "\r\n", 2); if (rv < 2) { /* XXX */ g_warning ("write failed. we need to handle short writes\n"); close (fd); unlink (new_path); return FALSE; } } if (0 > rename (new_path, vcf->priv->filename)) { g_warning ("Failed to rename %s: %s\n", vcf->priv->filename, strerror(errno)); unlink (new_path); return FALSE; } g_list_foreach (contacts, (GFunc)g_object_unref, NULL); g_list_free (contacts); g_free (new_path); vcf->priv->dirty = FALSE; return TRUE; } static gboolean vcf_flush_file (gpointer data) { PASBackendVCF *bvcf = PAS_BACKEND_VCF (data); if (!bvcf->priv->dirty) { bvcf->priv->flush_timeout_tag = 0; return FALSE; } if (!save_file (bvcf)) { g_warning ("failed to flush the .vcf file to disk, will try again next timeout"); return TRUE; } bvcf->priv->flush_timeout_tag = 0; return FALSE; } static EContact * do_create(PASBackendVCF *bvcf, const char *vcard_req, gboolean dirty_the_file) { char *id; EContact *contact; char *vcard; id = pas_backend_vcf_create_unique_id (); contact = e_contact_new_from_vcard (vcard_req); e_contact_set(contact, E_CONTACT_UID, id); vcard = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30); g_hash_table_insert (bvcf->priv->contacts, id, vcard); if (dirty_the_file) { bvcf->priv->dirty = TRUE; if (!bvcf->priv->flush_timeout_tag) bvcf->priv->flush_timeout_tag = g_timeout_add (FILE_FLUSH_TIMEOUT, vcf_flush_file, bvcf); } return contact; } static PASBackendSyncStatus pas_backend_vcf_process_create_contact (PASBackendSync *backend, PASBook *book, const char *vcard, EContact **contact) { PASBackendVCF *bvcf = PAS_BACKEND_VCF (backend); *contact = do_create(bvcf, vcard, TRUE); if (*contact) { return GNOME_Evolution_Addressbook_Success; } else { /* XXX need a different call status for this case, i think */ return GNOME_Evolution_Addressbook_ContactNotFound; } } static PASBackendSyncStatus pas_backend_vcf_process_remove_contacts (PASBackendSync *backend, PASBook *book, GList *id_list, GList **ids) { /* FIXME: make this handle bulk deletes like the file backend does */ PASBackendVCF *bvcf = PAS_BACKEND_VCF (backend); char *id = id_list->data; if (!g_hash_table_remove (bvcf->priv->contacts, id)) { return GNOME_Evolution_Addressbook_ContactNotFound; } else { bvcf->priv->dirty = TRUE; if (!bvcf->priv->flush_timeout_tag) bvcf->priv->flush_timeout_tag = g_timeout_add (FILE_FLUSH_TIMEOUT, vcf_flush_file, bvcf); *ids = g_list_append (*ids, id); return GNOME_Evolution_Addressbook_Success; } } static PASBackendSyncStatus pas_backend_vcf_process_modify_contact (PASBackendSync *backend, PASBook *book, const char *vcard, EContact **contact) { PASBackendVCF *bvcf = PAS_BACKEND_VCF (backend); char *old_id, *old_vcard_string; const char *id; /* create a new ecard from the request data */ *contact = e_contact_new_from_vcard (vcard); id = e_contact_get_const (*contact, E_CONTACT_UID); if (!g_hash_table_lookup_extended (bvcf->priv->contacts, id, (gpointer)&old_id, (gpointer)&old_vcard_string)) { return GNOME_Evolution_Addressbook_ContactNotFound; } else { g_hash_table_insert (bvcf->priv->contacts, old_id, g_strdup (vcard)); g_free (old_vcard_string); return GNOME_Evolution_Addressbook_Success; } } static PASBackendSyncStatus pas_backend_vcf_process_get_contact (PASBackendSync *backend, PASBook *book, const char *id, char **vcard) { PASBackendVCF *bvcf = PAS_BACKEND_VCF (backend); char *v; v = g_hash_table_lookup (bvcf->priv->contacts, id); if (v) { *vcard = g_strdup (v); return GNOME_Evolution_Addressbook_Success; } else { *vcard = g_strdup (""); return GNOME_Evolution_Addressbook_ContactNotFound; } } typedef struct { PASBackendVCF *bvcf; gboolean search_needed; PASBackendCardSExp *card_sexp; GList *list; } GetContactListClosure; static void foreach_get_contact_compare (char *id, char *vcard_string, GetContactListClosure *closure) { if ((!closure->search_needed) || pas_backend_card_sexp_match_vcard (closure->card_sexp, vcard_string)) { closure->list = g_list_append (closure->list, g_strdup (vcard_string)); } } static PASBackendSyncStatus pas_backend_vcf_process_get_contact_list (PASBackendSync *backend, PASBook *book, const char *query, GList **contacts) { PASBackendVCF *bvcf = PAS_BACKEND_VCF (backend); const char *search = query; GetContactListClosure closure; closure.bvcf = bvcf; closure.search_needed = strcmp (search, "(contains \"x-evolution-any-field\" \"\")"); closure.card_sexp = pas_backend_card_sexp_new (search); closure.list = NULL; g_hash_table_foreach (bvcf->priv->contacts, (GHFunc)foreach_get_contact_compare, &closure); g_object_unref (closure.card_sexp); *contacts = closure.list; return GNOME_Evolution_Addressbook_Success; } static void pas_backend_vcf_start_book_view (PASBackend *backend, PASBookView *book_view) { pas_backend_vcf_search (PAS_BACKEND_VCF (backend), book_view); } static char * pas_backend_vcf_extract_path_from_uri (const char *uri) { g_assert (strncasecmp (uri, "vcf://", 6) == 0); return g_strdup (uri + 6); } static PASBackendSyncStatus pas_backend_vcf_process_authenticate_user (PASBackendSync *backend, PASBook *book, const char *user, const char *passwd, const char *auth_method) { return GNOME_Evolution_Addressbook_Success; } static PASBackendSyncStatus pas_backend_vcf_process_get_supported_fields (PASBackendSync *backend, PASBook *book, GList **fields_out) { GList *fields = NULL; int i; /* XXX we need a way to say "we support everything", since the vcf backend does */ for (i = 0; i < E_CONTACT_FIELD_LAST; i ++) fields = g_list_append (fields, (char*)e_contact_field_name (i)); *fields_out = fields; return GNOME_Evolution_Addressbook_Success; } #include "ximian-vcard.h" static GNOME_Evolution_Addressbook_CallStatus pas_backend_vcf_load_uri (PASBackend *backend, const char *uri, gboolean only_if_exists) { PASBackendVCF *bvcf = PAS_BACKEND_VCF (backend); char *filename; gboolean writable = FALSE; int fd; g_free(bvcf->priv->uri); bvcf->priv->uri = g_strdup (uri); bvcf->priv->filename = filename = pas_backend_vcf_extract_path_from_uri (uri); fd = open (filename, O_RDWR); bvcf->priv->contacts = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); if (fd != -1) { writable = TRUE; } else { fd = open (filename, O_RDONLY); if (fd == -1) { fd = open (filename, O_CREAT, 0666); if (fd != -1 && !only_if_exists) { EContact *contact; contact = do_create(bvcf, XIMIAN_VCARD, FALSE); save_file (bvcf); /* XXX check errors here */ g_object_unref (contact); writable = TRUE; } } } if (fd == -1) { g_warning ("Failed to open addressbook at uri `%s'", uri); g_warning ("error == %s", strerror(errno)); return GNOME_Evolution_Addressbook_OtherError; } close (fd); /* XXX ugh */ load_file (bvcf); pas_backend_set_is_loaded (backend, TRUE); pas_backend_set_is_writable (backend, writable); return GNOME_Evolution_Addressbook_Success; } static char * pas_backend_vcf_get_static_capabilities (PASBackend *backend) { return g_strdup("local,do-initial-query"); } static GNOME_Evolution_Addressbook_CallStatus pas_backend_vcf_cancel_operation (PASBackend *backend, PASBook *book) { return GNOME_Evolution_Addressbook_CouldNotCancel; } static gboolean pas_backend_vcf_construct (PASBackendVCF *backend) { g_assert (backend != NULL); g_assert (PAS_IS_BACKEND_VCF (backend)); if (! pas_backend_construct (PAS_BACKEND (backend))) return FALSE; return TRUE; } /** * pas_backend_vcf_new: */ PASBackend * pas_backend_vcf_new (void) { PASBackendVCF *backend; backend = g_object_new (PAS_TYPE_BACKEND_VCF, NULL); if (! pas_backend_vcf_construct (backend)) { g_object_unref (backend); return NULL; } return PAS_BACKEND (backend); } static void pas_backend_vcf_dispose (GObject *object) { PASBackendVCF *bvcf; bvcf = PAS_BACKEND_VCF (object); if (bvcf->priv) { if (bvcf->priv->dirty) save_file (bvcf); if (bvcf->priv->flush_timeout_tag) { g_source_remove (bvcf->priv->flush_timeout_tag); bvcf->priv->flush_timeout_tag = 0; } g_free (bvcf->priv->uri); g_free (bvcf->priv->filename); g_free (bvcf->priv); bvcf->priv = NULL; } G_OBJECT_CLASS (pas_backend_vcf_parent_class)->dispose (object); } static void pas_backend_vcf_class_init (PASBackendVCFClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); PASBackendSyncClass *sync_class; PASBackendClass *backend_class; pas_backend_vcf_parent_class = g_type_class_peek_parent (klass); sync_class = PAS_BACKEND_SYNC_CLASS (klass); backend_class = PAS_BACKEND_CLASS (klass); /* Set the virtual methods. */ backend_class->load_uri = pas_backend_vcf_load_uri; backend_class->get_static_capabilities = pas_backend_vcf_get_static_capabilities; backend_class->start_book_view = pas_backend_vcf_start_book_view; backend_class->cancel_operation = pas_backend_vcf_cancel_operation; sync_class->create_contact_sync = pas_backend_vcf_process_create_contact; sync_class->remove_contacts_sync = pas_backend_vcf_process_remove_contacts; sync_class->modify_contact_sync = pas_backend_vcf_process_modify_contact; sync_class->get_contact_sync = pas_backend_vcf_process_get_contact; sync_class->get_contact_list_sync = pas_backend_vcf_process_get_contact_list; sync_class->authenticate_user_sync = pas_backend_vcf_process_authenticate_user; sync_class->get_supported_fields_sync = pas_backend_vcf_process_get_supported_fields; object_class->dispose = pas_backend_vcf_dispose; } static void pas_backend_vcf_init (PASBackendVCF *backend) { PASBackendVCFPrivate *priv; priv = g_new0 (PASBackendVCFPrivate, 1); priv->uri = NULL; backend->priv = priv; } /** * pas_backend_vcf_get_type: */ GType pas_backend_vcf_get_type (void) { static GType type = 0; if (! type) { GTypeInfo info = { sizeof (PASBackendVCFClass), NULL, /* base_class_init */ NULL, /* base_class_finalize */ (GClassInitFunc) pas_backend_vcf_class_init, NULL, /* class_finalize */ NULL, /* class_data */ sizeof (PASBackendVCF), 0, /* n_preallocs */ (GInstanceInitFunc) pas_backend_vcf_init }; type = g_type_register_static (PAS_TYPE_BACKEND_SYNC, "PASBackendVCF", &info, 0); } return type; }