From 1b01ac337fe368d6759759553e88d7e3823903af Mon Sep 17 00:00:00 2001 From: Patrick Ohly Date: Sun, 22 Jun 2008 13:12:20 +0000 Subject: [PATCH] bug #52890: improved time zone support - merged 9022:9026 from gnome-2-22 branch svn path=/trunk/; revision=9028 --- ChangeLog | 17 + calendar/backends/file/e-cal-backend-file.c | 22 ++ calendar/libecal/Makefile.am | 2 + calendar/libecal/e-cal-check-timezones.c | 490 ++++++++++++++++++++++++++++ calendar/libecal/e-cal-check-timezones.h | 48 +++ calendar/libecal/e-cal.c | 82 +++-- configure.in | 4 +- 7 files changed, 643 insertions(+), 22 deletions(-) create mode 100644 calendar/libecal/e-cal-check-timezones.c create mode 100644 calendar/libecal/e-cal-check-timezones.h diff --git a/ChangeLog b/ChangeLog index b708ebf..bd45992 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,20 @@ +2008-06-22 Patrick Ohly + + * configure.in, calendar/libecal/Makefile.am, + calendar/libecal/e-cal-check-timezones.c, + calendar/libecal/e-cal-check-timezones.h: added + e_cal_check_timezones() which matches time zone definitions to + system time zones and resolves conflicting definitions; bumped + version and age to matched the extended libecal API (bug #52890) + + * calendar/backends/file/e-cal-backend-file.c: use + e_cal_check_timezones() to improve time zone handling (bug #52890) + + * calendar/libecal/e-cal.c: use current system time zone + definitions instead of possibly out-dated custom definitions when + exporting events or retrieving the time zone information for an + event (bug #52890) + 2008-06-17 Johnny Jacob * configure.in (eds_micro_version): Bumped to 2.23.5. diff --git a/calendar/backends/file/e-cal-backend-file.c b/calendar/backends/file/e-cal-backend-file.c index ec83eb4..f1ae222 100644 --- a/calendar/backends/file/e-cal-backend-file.c +++ b/calendar/backends/file/e-cal-backend-file.c @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include "e-cal-backend-file-events.h" @@ -2626,6 +2627,27 @@ e_cal_backend_file_receive_objects (ECalBackendSync *backend, EDataCal *cal, con g_list_free (del_comps); + /* check and patch timezones */ + { + GError *error = NULL; + if (!e_cal_check_timezones(toplevel_comp, + NULL, + e_cal_tzlookup_icomp, + priv->icalcomp, + &error)) { + /* + * This makes assumptions about what kind of + * errors can occur inside e_cal_check_timezones(). + * We control it, so that should be safe, but + * is the code really identical with the calendar + * status? + */ + status = error->code; + g_clear_error(&error); + goto error; + } + } + /* Merge the iCalendar components with our existing VCALENDAR, resolving any conflicting TZIDs. */ icalcomponent_merge_component (priv->icalcomp, toplevel_comp); diff --git a/calendar/libecal/Makefile.am b/calendar/libecal/Makefile.am index 428426d..82d5ef4 100644 --- a/calendar/libecal/Makefile.am +++ b/calendar/libecal/Makefile.am @@ -48,6 +48,7 @@ libecal_1_2_la_SOURCES = \ e-cal-listener.h \ e-cal-recur.c \ e-cal-time-util.c \ + e-cal-check-timezones.c \ e-cal-util.c \ e-cal-view.c \ e-cal-view-listener.c \ @@ -71,6 +72,7 @@ libecalinclude_HEADERS = \ e-cal-component.h \ e-cal-recur.h \ e-cal-time-util.h \ + e-cal-check-timezones.h \ e-cal-types.h \ e-cal-util.h \ e-cal-view.h diff --git a/calendar/libecal/e-cal-check-timezones.c b/calendar/libecal/e-cal-check-timezones.c new file mode 100644 index 0000000..02791ab --- /dev/null +++ b/calendar/libecal/e-cal-check-timezones.c @@ -0,0 +1,490 @@ +/* + * Copyright (C) 2008 Novell, Inc. + * + * Authors: Patrick Ohly + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef HANDLE_LIBICAL_MEMORY +# define HANDLE_LIBICAL_MEMORY 1 +#endif +#include + +#ifdef LIBICAL_MEMFIXES +/* avoid dependency on icalstrdup.h, works when compiling as part of EDS >= 2.22 */ +# define ical_strdup(_x) (_x) +#else +/* use icalstrdup.h to get runtime detection of memory fix patch */ +# include "libical/icalstrdup.h" +#endif + +#include "e-cal-check-timezones.h" +#include +#include +#include + +/** + * Matches a location to a system timezone definition via a fuzzy + * search and returns the matching TZID, or NULL if none found. + * + * Currently simply strips a suffix introduced by a hyphen, + * as in "America/Denver-(Standard)". + */ +static const char *e_cal_match_location(const char *location) +{ + icaltimezone *icomp; + const char *tail; + size_t len; + char *buffer; + + icomp = icaltimezone_get_builtin_timezone (location); + if (icomp) { + return icaltimezone_get_tzid(icomp); + } + + /* try a bit harder by stripping trailing suffix */ + tail = strrchr(location, '-'); + len = tail ? (tail - location) : strlen(location); + buffer = g_malloc(len + 1); + + if (buffer) { + memcpy(buffer, location, len); + buffer[len] = 0; + icomp = icaltimezone_get_builtin_timezone (buffer); + g_free(buffer); + if (icomp) { + return icaltimezone_get_tzid(icomp); + } + } + + return NULL; +} + +/** + * e_cal_match_tzid: + * matches a TZID against the system timezone definitions + * and returns the matching TZID, or NULL if none found + */ +const char *e_cal_match_tzid(const char *tzid) +{ + const char *location; + const char *systzid; + size_t len = strlen(tzid); + ssize_t eostr; + + /* + * Try without any trailing spaces/digits: they might have been added + * by e_cal_check_timezones() in order to distinguish between + * different incompatible definitions. At that time mapping + * to system time zones must have failed, but perhaps now + * we have better code and it succeeds... + */ + eostr = len - 1; + while (eostr >= 0 && + isdigit(tzid[eostr])) { + eostr--; + } + while (eostr >= 0 && + isspace(tzid[eostr])) { + eostr--; + } + if (eostr + 1 < len) { + char *strippedtzid = g_strndup(tzid, eostr + 1); + if (strippedtzid) { + systzid = e_cal_match_tzid(strippedtzid); + g_free(strippedtzid); + if (systzid) { + return systzid; + } + } + } + + /* + * old-style Evolution: /softwarestudio.org/Olson_20011030_5/America/Denver + * + * jump from one slash to the next and check whether the remainder + * is a known location; start with the whole string (just in case) + */ + for (location = tzid; + location && location[0]; + location = strchr(location + 1, '/')) { + systzid = e_cal_match_location(location[0] == '/' ? + location + 1 : + location); + if (systzid) { + return systzid; + } + } + + /* TODO: lookup table for Exchange TZIDs */ + + return NULL; +} + +static void patch_tzids(icalcomponent *subcomp, + GHashTable *mapping) +{ + char *tzid = NULL; + + if (icalcomponent_isa(subcomp) != ICAL_VTIMEZONE_COMPONENT) { + icalproperty *prop = icalcomponent_get_first_property(subcomp, + ICAL_ANY_PROPERTY); + while (prop) { + icalparameter *param = icalproperty_get_first_parameter(prop, + ICAL_TZID_PARAMETER); + while (param) { + const char *oldtzid; + const char *newtzid; + + g_free(tzid); + tzid = g_strdup(icalparameter_get_tzid(param)); + + if (!g_hash_table_lookup_extended(mapping, + tzid, + (gpointer *)&oldtzid, + (gpointer *)&newtzid)) { + /* Corresponding VTIMEZONE not seen before! */ + newtzid = e_cal_match_tzid(tzid); + } + if (newtzid) { + icalparameter_set_tzid(param, newtzid); + } + param = icalproperty_get_next_parameter(prop, + ICAL_TZID_PARAMETER); + } + prop = icalcomponent_get_next_property(subcomp, + ICAL_ANY_PROPERTY); + } + } + + g_free(tzid); +} + +static void addsystemtz(gpointer key, + gpointer value, + gpointer user_data) +{ + const char *tzid = key; + icalcomponent *comp = user_data; + icaltimezone *zone; + + zone = icaltimezone_get_builtin_timezone_from_tzid(tzid); + if (zone) { + icalcomponent_add_component(comp, + icalcomponent_new_clone(icaltimezone_get_component(zone))); + } +} + +/** + * e_cal_check_timezones: + * @comp: a VCALENDAR containing a list of + * VTIMEZONE and arbitrary other components, in + * arbitrary order: these other components are + * modified by this call + * @comps: a list of icalcomponent instances which + * also have to be patched; may be NULL + * @tzlookup: a callback function which is called to retrieve + * a calendar's VTIMEZONE definition; the returned + * definition is *not* freed by e_cal_check_timezones() + * (to be compatible with e_cal_get_timezone()); + * NULL indicates that no such timezone exists + * or an error occurred + * @custom: an arbitrary pointer which is passed through to + * the tzlookup function + * @error: an error description in case of a failure + * + * This function cleans up VEVENT, VJOURNAL, VTODO and VTIMEZONE + * items which are to be imported into Evolution. + * + * Using VTIMEZONE definitions is problematic because they cannot be + * updated properly when timezone definitions change. They are also + * incomplete (for compatibility reason only one set of rules for + * summer saving changes can be included, even if different rules + * apply in different years). This function looks for matches of the + * used TZIDs against system timezones and replaces such TZIDs with + * the corresponding system timezone. This works for TZIDs containing + * a location (found via a fuzzy string search) and for Outlook TZIDs + * (via a hard-coded lookup table). + * + * Some programs generate broken meeting invitations with TZID, but + * without including the corresponding VTIMEZONE. Importing such + * invitations unchanged causes problems later on (meeting displayed + * incorrectly, #e_cal_get_component_as_string fails). The situation + * where this occurred in the past (found by a SyncEvolution user) is + * now handled via the location based mapping. + * + * If this mapping fails, this function also deals with VTIMEZONE + * conflicts: such conflicts occur when the calendar already contains + * an old VTIMEZONE definition with the same TZID, but different + * summer saving rules. Replacing the VTIMEZONE potentially breaks + * displaying of old events, whereas not replacing it breaks the new + * events (the behavior in Evolution <= 2.22.1). + * + * The way this problem is resolved is by renaming the new VTIMEZONE + * definition until the TZID is unique. A running count is appended to + * the TZID. All items referencing the renamed TZID are adapted + * accordingly. + * + * Return value: TRUE if successful, FALSE otherwise. + */ +gboolean e_cal_check_timezones(icalcomponent *comp, + GList *comps, + icaltimezone *(*tzlookup)(const char *tzid, + const void *custom, + GError **error), + const void *custom, + GError **error) +{ + gboolean success = TRUE; + icalcomponent *subcomp = NULL; + icaltimezone *zone = icaltimezone_new(); + char *key = NULL, *value = NULL; + char *buffer = NULL; + char *zonestr = NULL; + char *tzid = NULL; + GList *l; + + /** a hash from old to new tzid; strings dynamically allocated */ + GHashTable *mapping = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); + + /** a hash of all system time zone IDs which have to be added; strings are shared with mapping hash */ + GHashTable *systemtzids = g_hash_table_new(g_str_hash, g_str_equal); + + *error = NULL; + + if (!mapping || !zone) { + goto nomem; + } + + /* iterate over all VTIMEZONE definitions */ + subcomp = icalcomponent_get_first_component(comp, + ICAL_VTIMEZONE_COMPONENT); + while (subcomp) { + if (icaltimezone_set_component(zone, subcomp)) { + g_free(tzid); + tzid = g_strdup(icaltimezone_get_tzid(zone)); + if (tzid) { + const char *newtzid = e_cal_match_tzid(tzid); + if (newtzid) { + /* matched against system time zone */ + g_free(key); + key = g_strdup(tzid); + if (!key) { + goto nomem; + } + + g_free(value); + value = g_strdup(newtzid); + if (!value) { + goto nomem; + } + + g_hash_table_insert(mapping, key, value); + g_hash_table_insert(systemtzids, value, NULL); + key = + value = NULL; + } else { + zonestr = ical_strdup(icalcomponent_as_ical_string(subcomp)); + + /* check for collisions with existing timezones */ + int counter; + for (counter = 0; + counter < 100 /* sanity limit */; + counter++) { + icaltimezone *existing_zone; + + if (counter) { + g_free(value); + value = g_strdup_printf("%s %d", tzid, counter); + } + existing_zone = tzlookup(counter ? value : tzid, + custom, + error); + if (!existing_zone) { + if (*error) { + goto failed; + } else { + break; + } + } + g_free(buffer); + buffer = ical_strdup(icalcomponent_as_ical_string(icaltimezone_get_component(existing_zone))); + + if (counter) { + char *fulltzid = g_strdup_printf("TZID:%s", value); + size_t baselen = strlen("TZID:") + strlen(tzid); + size_t fulllen = strlen(fulltzid); + char *tzidprop; + /* + * Map TZID with counter suffix back to basename. + */ + tzidprop = strstr(buffer, fulltzid); + if (tzidprop) { + memmove(tzidprop + baselen, + tzidprop + fulllen, + strlen(tzidprop + fulllen) + 1); + } + g_free(fulltzid); + } + + + /* + * If the strings are identical, then the + * VTIMEZONE definitions are identical. If + * they are not identical, then VTIMEZONE + * definitions might still be semantically + * correct and we waste some space by + * needlesly duplicating the VTIMEZONE. This + * is expected to occur rarely (if at all) in + * practice. + */ + if (!strcmp(zonestr, buffer)) { + break; + } + } + + if (!counter) { + /* does not exist, nothing to do */ + } else { + /* timezone renamed */ + icalproperty *prop = icalcomponent_get_first_property(subcomp, + ICAL_TZID_PROPERTY); + while (prop) { + icalproperty_set_value_from_string(prop, value, "NO"); + prop = icalcomponent_get_next_property(subcomp, + ICAL_ANY_PROPERTY); + } + g_free(key); + key = g_strdup(tzid); + g_hash_table_insert(mapping, key, value); + key = + value = NULL; + } + } + } + } + + subcomp = icalcomponent_get_next_component(comp, + ICAL_VTIMEZONE_COMPONENT); + } + + /* + * now replace all TZID parameters in place + */ + subcomp = icalcomponent_get_first_component(comp, + ICAL_ANY_COMPONENT); + while (subcomp) { + /* + * Leave VTIMEZONE unchanged, iterate over properties of + * everything else. + * + * Note that no attempt is made to remove unused VTIMEZONE + * definitions. That would just make the code more complex for + * little additional gain. However, newly used time zones are + * added below. + */ + patch_tzids (subcomp, mapping); + subcomp = icalcomponent_get_next_component(comp, + ICAL_ANY_COMPONENT); + } + + for (l = comps; l; l = l->next) { + patch_tzids (l->data, mapping); + } + + /* + * add system time zones that we mapped to: adding them ensures + * that the VCALENDAR remains consistent + */ + g_hash_table_foreach(systemtzids, addsystemtz, comp); + + goto done; + nomem: + /* set gerror for "out of memory" if possible, otherwise abort via g_error() */ + *error = g_error_new(E_CALENDAR_ERROR, E_CALENDAR_STATUS_OTHER_ERROR, "out of memory"); + if (!*error) { + g_error("e_cal_check_timezones(): out of memory, cannot proceed - sorry!"); + } + failed: + /* gerror should have been set already */ + success = FALSE; + done: + if (mapping) { + g_hash_table_destroy(mapping); + } + if (systemtzids) { + g_hash_table_destroy(systemtzids); + } + if (zone) { + icaltimezone_free(zone, 1); + } + g_free(tzid); + g_free(zonestr); + g_free(buffer); + g_free(key); + g_free(value); + + return success; +} + +/** + * e_cal_tzlookup_ecal: + * @custom: must be a valid ECal pointer + * + * An implementation of the tzlookup callback which clients + * can use. Calls #e_cal_get_timezone. + */ +icaltimezone *e_cal_tzlookup_ecal(const char *tzid, + const void *custom, + GError **error) +{ + ECal *ecal = (ECal *)custom; + icaltimezone *zone = NULL; + + if (e_cal_get_timezone(ecal, tzid, &zone, error)) { + g_assert(*error == NULL); + return zone; + } else { + g_assert(*error); + if ((*error)->domain == E_CALENDAR_ERROR && + (*error)->code == E_CALENDAR_STATUS_OBJECT_NOT_FOUND) { + /* + * we had to trigger this error to check for the timezone existance, + * clear it and return NULL + */ + g_clear_error(error); + } + return NULL; + } +} + +/** + * e_cal_tzlookup_icomp: + * @custom: must be a icalcomponent pointer which contains + * either a VCALENDAR with VTIMEZONEs or VTIMEZONES + * directly + * + * An implementation of the tzlookup callback which backends + * like the file backend can use. Searches for the timezone + * in the component list. + */ +icaltimezone *e_cal_tzlookup_icomp(const char *tzid, + const void *custom, + GError **error) +{ + icalcomponent *icomp = (icalcomponent *)custom; + + return icalcomponent_get_timezone(icomp, (char *)tzid); +} diff --git a/calendar/libecal/e-cal-check-timezones.h b/calendar/libecal/e-cal-check-timezones.h new file mode 100644 index 0000000..a19f644 --- /dev/null +++ b/calendar/libecal/e-cal-check-timezones.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2008 Novell, Inc. + * + * Authors: Patrick Ohly + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef E_CAL_CHECK_TIMEZONES_H +#define E_CAL_CHECK_TIMEZONES_H + +#include +#include + +G_BEGIN_DECLS + +gboolean e_cal_check_timezones(icalcomponent *comp, + GList *comps, + icaltimezone *(*tzlookup)(const char *tzid, + const void *custom, + GError **error), + const void *custom, + GError **error); + +icaltimezone *e_cal_tzlookup_ecal(const char *tzid, + const void *custom, + GError **error); + +icaltimezone *e_cal_tzlookup_icomp(const char *tzid, + const void *custom, + GError **error); + +const char *e_cal_match_tzid(const char *tzid); + +G_END_DECLS + +#endif /* E_CAL_CHECK_TIMEZONES_H */ diff --git a/calendar/libecal/e-cal.c b/calendar/libecal/e-cal.c index ab8db26..389006e 100644 --- a/calendar/libecal/e-cal.c +++ b/calendar/libecal/e-cal.c @@ -31,6 +31,7 @@ #include #include +#include "libecal/e-cal-check-timezones.h" #include "libedataserver/e-component-listener.h" #include "libedataserver/e-flag.h" #include "libedataserver/e-url.h" @@ -4634,9 +4635,10 @@ e_cal_get_timezone (ECal *ecal, const char *tzid, icaltimezone **zone, GError ** { ECalPrivate *priv; CORBA_Environment ev; - ECalendarStatus status; + ECalendarStatus status = E_CALENDAR_STATUS_OK; ECalendarOp *our_op; icalcomponent *icalcomp; + const char *systzid; e_return_error_if_fail (ecal && E_IS_CAL (ecal), E_CALENDAR_STATUS_INVALID_ARG); e_return_error_if_fail (zone, E_CALENDAR_STATUS_INVALID_ARG); @@ -4687,34 +4689,74 @@ e_cal_get_timezone (ECal *ecal, const char *tzid, icaltimezone **zone, GError ** E_CALENDAR_CHECK_STATUS (E_CALENDAR_STATUS_OK, error); } - /* call the backend */ - CORBA_exception_init (&ev); + /* + * Try to replace the original time zone with a more complete + * and/or potentially updated system time zone. Note that this + * also applies to TZIDs which match system time zones exactly: + * they are extracted via icaltimezone_get_builtin_timezone_from_tzid() + * below without a roundtrip to the backend. + */ + systzid = e_cal_match_tzid (tzid); + if (!systzid) { + /* call the backend */ + CORBA_exception_init (&ev); - GNOME_Evolution_Calendar_Cal_getTimezone (priv->cal, tzid, &ev); - if (BONOBO_EX (&ev)) { - e_calendar_remove_op (ecal, our_op); - e_calendar_free_op (our_op); + GNOME_Evolution_Calendar_Cal_getTimezone (priv->cal, tzid, &ev); + if (BONOBO_EX (&ev)) { + e_calendar_remove_op (ecal, our_op); + e_calendar_free_op (our_op); - CORBA_exception_free (&ev); + CORBA_exception_free (&ev); - g_warning (G_STRLOC ": Unable to contact backend"); + g_warning (G_STRLOC ": Unable to contact backend"); - E_CALENDAR_CHECK_STATUS (E_CALENDAR_STATUS_CORBA_EXCEPTION, error); - } + E_CALENDAR_CHECK_STATUS (E_CALENDAR_STATUS_CORBA_EXCEPTION, error); + } - CORBA_exception_free (&ev); + CORBA_exception_free (&ev); - e_flag_wait (our_op->done); + e_flag_wait (our_op->done); - status = our_op->status; - if (status != E_CALENDAR_STATUS_OK){ - icalcomp = NULL; - } else { - icalcomp = icalparser_parse_string (our_op->string); - if (!icalcomp) + status = our_op->status; + if (status != E_CALENDAR_STATUS_OK){ + icalcomp = NULL; + } else { + icalcomp = icalparser_parse_string (our_op->string); + if (!icalcomp) + status = E_CALENDAR_STATUS_INVALID_OBJECT; + } + g_free (our_op->string); + } else { + /* + * Use built-in time zone *and* rename it: + * if the caller is asking for a TZID=FOO, + * then likely because it has an event with + * such a TZID. Returning a different TZID + * would lead to broken VCALENDARs in the + * caller. + */ + icaltimezone *syszone = icaltimezone_get_builtin_timezone_from_tzid (systzid); + g_assert (syszone); + if (syszone) { + gboolean found = FALSE; + icalproperty *prop; + + icalcomp = icalcomponent_new_clone (icaltimezone_get_component (syszone)); + prop = icalcomponent_get_first_property(icalcomp, + ICAL_ANY_PROPERTY); + while (!found && prop) { + if (icalproperty_isa(prop) == ICAL_TZID_PROPERTY) { + icalproperty_set_value_from_string(prop, tzid, "NO"); + found = TRUE; + } + prop = icalcomponent_get_next_property(icalcomp, + ICAL_ANY_PROPERTY); + } + g_assert (found); + } else { status = E_CALENDAR_STATUS_INVALID_OBJECT; + } } - g_free (our_op->string); if (!icalcomp) { e_calendar_remove_op (ecal, our_op); diff --git a/configure.in b/configure.in index 7307533..180364b 100644 --- a/configure.in +++ b/configure.in @@ -59,9 +59,9 @@ LIBEDATASERVERUI_CURRENT=9 LIBEDATASERVERUI_REVISION=0 LIBEDATASERVERUI_AGE=1 -LIBECAL_CURRENT=8 +LIBECAL_CURRENT=9 LIBECAL_REVISION=1 -LIBECAL_AGE=1 +LIBECAL_AGE=2 LIBEDATACAL_CURRENT=6 LIBEDATACAL_REVISION=2 -- 2.7.4