--- /dev/null
+/*
+ * Copyright (C) 2008 Novell, Inc.
+ *
+ * Authors: Patrick Ohly <patrick.ohly@gmx.de>
+ *
+ * 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 <libical/ical.h>
+
+#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 <libecal/e-cal.h>
+#include <string.h>
+#include <ctype.h>
+
+/**
+ * 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);
+}
#include <bonobo/bonobo-exception.h>
#include <bonobo/bonobo-main.h>
+#include "libecal/e-cal-check-timezones.h"
#include "libedataserver/e-component-listener.h"
#include "libedataserver/e-flag.h"
#include "libedataserver/e-url.h"
{
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);
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);