2 * Copyright (C) 2008 Novell, Inc.
3 * Copyright (C) 2009 Patrick Ohly <patrick.ohly@gmx.de>
5 * Authors: Patrick Ohly <patrick.ohly@gmx.de>
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU Lesser General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Lesser General Public License for more details.
17 * You should have received a copy of the GNU Lesser General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22 #ifndef HANDLE_LIBICAL_MEMORY
23 # define HANDLE_LIBICAL_MEMORY 1
25 #include <libical/ical.h>
27 #ifdef LIBICAL_MEMFIXES
28 /* avoid dependency on icalstrdup.h, works when compiling as part of EDS >= 2.22 */
29 # define ical_strdup(_x) (_x)
31 /* use icalstrdup.h to get runtime detection of memory fix patch */
32 # include "libical/icalstrdup.h"
35 #include "e-cal-check-timezones.h"
36 #include <libecal/e-cal.h>
41 * Matches a location to a system timezone definition via a fuzzy
42 * search and returns the matching TZID, or NULL if none found.
44 * Currently simply strips a suffix introduced by a hyphen,
45 * as in "America/Denver-(Standard)".
47 static const char *e_cal_match_location(const char *location)
54 icomp = icaltimezone_get_builtin_timezone (location);
56 return icaltimezone_get_tzid(icomp);
59 /* try a bit harder by stripping trailing suffix */
60 tail = strrchr(location, '-');
61 len = tail ? (tail - location) : strlen(location);
62 buffer = g_malloc(len + 1);
65 memcpy(buffer, location, len);
67 icomp = icaltimezone_get_builtin_timezone (buffer);
70 return icaltimezone_get_tzid(icomp);
79 * matches a TZID against the system timezone definitions
80 * and returns the matching TZID, or NULL if none found
82 const char *e_cal_match_tzid(const char *tzid)
86 size_t len = strlen(tzid);
90 * Try without any trailing spaces/digits: they might have been added
91 * by e_cal_check_timezones() in order to distinguish between
92 * different incompatible definitions. At that time mapping
93 * to system time zones must have failed, but perhaps now
94 * we have better code and it succeeds...
98 isdigit(tzid[eostr])) {
102 isspace(tzid[eostr])) {
105 if (eostr + 1 < len) {
106 char *strippedtzid = g_strndup(tzid, eostr + 1);
108 systzid = e_cal_match_tzid(strippedtzid);
109 g_free(strippedtzid);
117 * old-style Evolution: /softwarestudio.org/Olson_20011030_5/America/Denver
119 * jump from one slash to the next and check whether the remainder
120 * is a known location; start with the whole string (just in case)
122 for (location = tzid;
123 location && location[0];
124 location = strchr(location + 1, '/')) {
125 systzid = e_cal_match_location(location[0] == '/' ?
133 /* TODO: lookup table for Exchange TZIDs */
138 static void patch_tzids(icalcomponent *subcomp,
143 if (icalcomponent_isa(subcomp) != ICAL_VTIMEZONE_COMPONENT) {
144 icalproperty *prop = icalcomponent_get_first_property(subcomp,
147 icalparameter *param = icalproperty_get_first_parameter(prop,
148 ICAL_TZID_PARAMETER);
154 tzid = g_strdup(icalparameter_get_tzid(param));
156 if (!g_hash_table_lookup_extended(mapping,
158 (gpointer *)&oldtzid,
159 (gpointer *)&newtzid)) {
160 /* Corresponding VTIMEZONE not seen before! */
161 newtzid = e_cal_match_tzid(tzid);
164 icalparameter_set_tzid(param, newtzid);
166 param = icalproperty_get_next_parameter(prop,
167 ICAL_TZID_PARAMETER);
169 prop = icalcomponent_get_next_property(subcomp,
177 static void addsystemtz(gpointer key,
181 const char *tzid = key;
182 icalcomponent *comp = user_data;
185 zone = icaltimezone_get_builtin_timezone_from_tzid(tzid);
187 icalcomponent_add_component(comp,
188 icalcomponent_new_clone(icaltimezone_get_component(zone)));
193 * e_cal_check_timezones:
194 * @comp: a VCALENDAR containing a list of
195 * VTIMEZONE and arbitrary other components, in
196 * arbitrary order: these other components are
197 * modified by this call
198 * @comps: a list of icalcomponent instances which
199 * also have to be patched; may be NULL
200 * @tzlookup: a callback function which is called to retrieve
201 * a calendar's VTIMEZONE definition; the returned
202 * definition is *not* freed by e_cal_check_timezones()
203 * (to be compatible with e_cal_get_timezone());
204 * NULL indicates that no such timezone exists
205 * or an error occurred
206 * @custom: an arbitrary pointer which is passed through to
207 * the tzlookup function
208 * @error: an error description in case of a failure
210 * This function cleans up VEVENT, VJOURNAL, VTODO and VTIMEZONE
211 * items which are to be imported into Evolution.
213 * Using VTIMEZONE definitions is problematic because they cannot be
214 * updated properly when timezone definitions change. They are also
215 * incomplete (for compatibility reason only one set of rules for
216 * summer saving changes can be included, even if different rules
217 * apply in different years). This function looks for matches of the
218 * used TZIDs against system timezones and replaces such TZIDs with
219 * the corresponding system timezone. This works for TZIDs containing
220 * a location (found via a fuzzy string search) and for Outlook TZIDs
221 * (via a hard-coded lookup table).
223 * Some programs generate broken meeting invitations with TZID, but
224 * without including the corresponding VTIMEZONE. Importing such
225 * invitations unchanged causes problems later on (meeting displayed
226 * incorrectly, #e_cal_get_component_as_string fails). The situation
227 * where this occurred in the past (found by a SyncEvolution user) is
228 * now handled via the location based mapping.
230 * If this mapping fails, this function also deals with VTIMEZONE
231 * conflicts: such conflicts occur when the calendar already contains
232 * an old VTIMEZONE definition with the same TZID, but different
233 * summer saving rules. Replacing the VTIMEZONE potentially breaks
234 * displaying of old events, whereas not replacing it breaks the new
235 * events (the behavior in Evolution <= 2.22.1).
237 * The way this problem is resolved is by renaming the new VTIMEZONE
238 * definition until the TZID is unique. A running count is appended to
239 * the TZID. All items referencing the renamed TZID are adapted
242 * Return value: TRUE if successful, FALSE otherwise.
244 gboolean e_cal_check_timezones(icalcomponent *comp,
246 icaltimezone *(*tzlookup)(const char *tzid,
252 gboolean success = TRUE;
253 icalcomponent *subcomp = NULL;
254 icaltimezone *zone = icaltimezone_new();
255 char *key = NULL, *value = NULL;
257 char *zonestr = NULL;
261 /** a hash from old to new tzid; strings dynamically allocated */
262 GHashTable *mapping = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
264 /** a hash of all system time zone IDs which have to be added; strings are shared with mapping hash */
265 GHashTable *systemtzids = g_hash_table_new(g_str_hash, g_str_equal);
269 if (!mapping || !zone) {
273 /* iterate over all VTIMEZONE definitions */
274 subcomp = icalcomponent_get_first_component(comp,
275 ICAL_VTIMEZONE_COMPONENT);
277 if (icaltimezone_set_component(zone, subcomp)) {
279 tzid = g_strdup(icaltimezone_get_tzid(zone));
281 const char *newtzid = e_cal_match_tzid(tzid);
283 /* matched against system time zone */
285 key = g_strdup(tzid);
291 value = g_strdup(newtzid);
296 g_hash_table_insert(mapping, key, value);
297 g_hash_table_insert(systemtzids, value, NULL);
301 zonestr = ical_strdup(icalcomponent_as_ical_string(subcomp));
303 /* check for collisions with existing timezones */
306 counter < 100 /* sanity limit */;
308 icaltimezone *existing_zone;
312 value = g_strdup_printf("%s %d", tzid, counter);
314 existing_zone = tzlookup(counter ? value : tzid,
317 if (!existing_zone) {
325 buffer = ical_strdup(icalcomponent_as_ical_string(icaltimezone_get_component(existing_zone)));
328 char *fulltzid = g_strdup_printf("TZID:%s", value);
329 size_t baselen = strlen("TZID:") + strlen(tzid);
330 size_t fulllen = strlen(fulltzid);
333 * Map TZID with counter suffix back to basename.
335 tzidprop = strstr(buffer, fulltzid);
337 memmove(tzidprop + baselen,
339 strlen(tzidprop + fulllen) + 1);
346 * If the strings are identical, then the
347 * VTIMEZONE definitions are identical. If
348 * they are not identical, then VTIMEZONE
349 * definitions might still be semantically
350 * correct and we waste some space by
351 * needlesly duplicating the VTIMEZONE. This
352 * is expected to occur rarely (if at all) in
355 if (!strcmp(zonestr, buffer)) {
361 /* does not exist, nothing to do */
363 /* timezone renamed */
364 icalproperty *prop = icalcomponent_get_first_property(subcomp,
367 icalproperty_set_value_from_string(prop, value, "NO");
368 prop = icalcomponent_get_next_property(subcomp,
372 key = g_strdup(tzid);
373 g_hash_table_insert(mapping, key, value);
381 subcomp = icalcomponent_get_next_component(comp,
382 ICAL_VTIMEZONE_COMPONENT);
386 * now replace all TZID parameters in place
388 subcomp = icalcomponent_get_first_component(comp,
392 * Leave VTIMEZONE unchanged, iterate over properties of
395 * Note that no attempt is made to remove unused VTIMEZONE
396 * definitions. That would just make the code more complex for
397 * little additional gain. However, newly used time zones are
400 patch_tzids (subcomp, mapping);
401 subcomp = icalcomponent_get_next_component(comp,
405 for (l = comps; l; l = l->next) {
406 patch_tzids (l->data, mapping);
410 * add system time zones that we mapped to: adding them ensures
411 * that the VCALENDAR remains consistent
413 g_hash_table_foreach(systemtzids, addsystemtz, comp);
417 /* set gerror for "out of memory" if possible, otherwise abort via g_error() */
418 *error = g_error_new(E_CALENDAR_ERROR, E_CALENDAR_STATUS_OTHER_ERROR, "out of memory");
420 g_error("e_cal_check_timezones(): out of memory, cannot proceed - sorry!");
423 /* gerror should have been set already */
427 g_hash_table_destroy(mapping);
430 g_hash_table_destroy(systemtzids);
433 icaltimezone_free(zone, 1);
445 * e_cal_tzlookup_ecal:
446 * @custom: must be a valid ECal pointer
448 * An implementation of the tzlookup callback which clients
449 * can use. Calls #e_cal_get_timezone.
451 icaltimezone *e_cal_tzlookup_ecal(const char *tzid,
455 ECal *ecal = (ECal *)custom;
456 icaltimezone *zone = NULL;
458 if (e_cal_get_timezone(ecal, tzid, &zone, error)) {
459 g_assert(*error == NULL);
463 if ((*error)->domain == E_CALENDAR_ERROR &&
464 ((*error)->code == E_CALENDAR_STATUS_OBJECT_NOT_FOUND /* EDS < 2.30 */ ||
465 (*error)->code == E_CALENDAR_STATUS_INVALID_OBJECT /* EDS >= 2.30 */ )) {
467 * we had to trigger this error to check for the timezone existance,
468 * clear it and return NULL
470 g_clear_error(error);
477 * e_cal_tzlookup_icomp:
478 * @custom: must be a icalcomponent pointer which contains
479 * either a VCALENDAR with VTIMEZONEs or VTIMEZONES
482 * An implementation of the tzlookup callback which backends
483 * like the file backend can use. Searches for the timezone
484 * in the component list.
486 icaltimezone *e_cal_tzlookup_icomp(const char *tzid,
490 icalcomponent *icomp = (icalcomponent *)custom;
492 return icalcomponent_get_timezone(icomp, (char *)tzid);