Imported Upstream version 1.0beta3
[platform/upstream/syncevolution.git] / src / backends / evolution / e-cal-check-timezones.c
1 /*
2  * Copyright (C) 2008 Novell, Inc.
3  * Copyright (C) 2009 Patrick Ohly <patrick.ohly@gmx.de>
4  *
5  * Authors: Patrick Ohly <patrick.ohly@gmx.de>
6  *
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.
11  *
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.
16  *
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.
20  */
21
22 #ifndef HANDLE_LIBICAL_MEMORY
23 # define HANDLE_LIBICAL_MEMORY 1
24 #endif
25 #include <libical/ical.h>
26
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)
30 #else
31 /* use icalstrdup.h to get runtime detection of memory fix patch */
32 # include "libical/icalstrdup.h"
33 #endif
34
35 #include "e-cal-check-timezones.h"
36 #include <libecal/e-cal.h>
37 #include <string.h>
38 #include <ctype.h>
39
40 /**
41  * Matches a location to a system timezone definition via a fuzzy
42  * search and returns the matching TZID, or NULL if none found.
43  *
44  * Currently simply strips a suffix introduced by a hyphen,
45  * as in "America/Denver-(Standard)".
46  */
47 static const char *e_cal_match_location(const char *location)
48 {
49     icaltimezone *icomp;
50     const char *tail;
51     size_t len;
52     char *buffer;
53
54     icomp = icaltimezone_get_builtin_timezone (location);
55     if (icomp) {
56         return icaltimezone_get_tzid(icomp);
57     }
58
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);
63
64     if (buffer) {
65         memcpy(buffer, location, len);
66         buffer[len] = 0;
67         icomp = icaltimezone_get_builtin_timezone (buffer);
68         g_free(buffer);
69         if (icomp) {
70             return icaltimezone_get_tzid(icomp);
71         }
72     }
73
74     return NULL;
75 }
76
77 /**
78  * e_cal_match_tzid:
79  * matches a TZID against the system timezone definitions
80  * and returns the matching TZID, or NULL if none found
81  */
82 const char *e_cal_match_tzid(const char *tzid)
83 {
84     const char *location;
85     const char *systzid;
86     size_t len = strlen(tzid);
87     ssize_t eostr;
88
89     /*
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...
95      */
96     eostr = len - 1;
97     while (eostr >= 0 &&
98            isdigit(tzid[eostr])) {
99         eostr--;
100     }
101     while (eostr >= 0 &&
102            isspace(tzid[eostr])) {
103         eostr--;
104     }
105     if (eostr + 1 < len) {
106         char *strippedtzid = g_strndup(tzid, eostr + 1);
107         if (strippedtzid) {
108             systzid = e_cal_match_tzid(strippedtzid);
109             g_free(strippedtzid);
110             if (systzid) {
111                 return systzid;
112             }
113         }
114     }
115
116     /*
117      * old-style Evolution: /softwarestudio.org/Olson_20011030_5/America/Denver
118      *
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)
121      */
122     for (location = tzid;
123          location && location[0];
124          location = strchr(location + 1, '/')) {
125         systzid = e_cal_match_location(location[0] == '/' ?
126                                        location + 1 :
127                                        location);
128         if (systzid) {
129             return systzid;
130         }
131     }
132
133     /* TODO: lookup table for Exchange TZIDs */
134
135     return NULL;
136 }
137
138 static void patch_tzids(icalcomponent *subcomp,
139                         GHashTable *mapping)
140 {
141     char *tzid = NULL;
142
143     if (icalcomponent_isa(subcomp) != ICAL_VTIMEZONE_COMPONENT) {
144         icalproperty *prop = icalcomponent_get_first_property(subcomp,
145                                                               ICAL_ANY_PROPERTY);
146         while (prop) {
147             icalparameter *param = icalproperty_get_first_parameter(prop,
148                                                                     ICAL_TZID_PARAMETER);
149             while (param) {
150                 const char *oldtzid;
151                 const char *newtzid;
152
153                 g_free(tzid);
154                 tzid = g_strdup(icalparameter_get_tzid(param));
155
156                 if (!g_hash_table_lookup_extended(mapping,
157                                                   tzid,
158                                                   (gpointer *)&oldtzid,
159                                                   (gpointer *)&newtzid)) {
160                     /* Corresponding VTIMEZONE not seen before! */
161                     newtzid = e_cal_match_tzid(tzid);
162                 }
163                 if (newtzid) {
164                     icalparameter_set_tzid(param, newtzid);
165                 }
166                 param = icalproperty_get_next_parameter(prop,
167                                                         ICAL_TZID_PARAMETER);
168             }
169             prop = icalcomponent_get_next_property(subcomp,
170                                                    ICAL_ANY_PROPERTY);
171         }
172     }
173
174     g_free(tzid);
175 }
176
177 static void addsystemtz(gpointer key,
178                         gpointer value,
179                         gpointer user_data)
180 {
181     const char *tzid = key;
182     icalcomponent *comp = user_data;
183     icaltimezone *zone;
184
185     zone = icaltimezone_get_builtin_timezone_from_tzid(tzid);
186     if (zone) {
187         icalcomponent_add_component(comp,
188                                     icalcomponent_new_clone(icaltimezone_get_component(zone)));
189     }
190 }
191
192 /**
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
209  *
210  * This function cleans up VEVENT, VJOURNAL, VTODO and VTIMEZONE
211  * items which are to be imported into Evolution.
212  *
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).
222  *
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.
229  *
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).
236  *
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
240  * accordingly.
241  *
242  * Return value: TRUE if successful, FALSE otherwise.
243  */
244 gboolean e_cal_check_timezones(icalcomponent *comp,
245                                GList *comps,
246                                icaltimezone *(*tzlookup)(const char *tzid,
247                                                          const void *custom,
248                                                          GError **error),
249                                const void *custom,
250                                GError **error)
251 {
252     gboolean success = TRUE;
253     icalcomponent *subcomp = NULL;
254     icaltimezone *zone = icaltimezone_new();
255     char *key = NULL, *value = NULL;
256     char *buffer = NULL;
257     char *zonestr = NULL;
258     char *tzid = NULL;
259     GList *l;
260
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);
263
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);
266
267     *error = NULL;
268
269     if (!mapping || !zone) {
270         goto nomem;
271     }
272
273     /* iterate over all VTIMEZONE definitions */
274     subcomp = icalcomponent_get_first_component(comp,
275                                                 ICAL_VTIMEZONE_COMPONENT);
276     while (subcomp) {
277         if (icaltimezone_set_component(zone, subcomp)) {
278             g_free(tzid);
279             tzid = g_strdup(icaltimezone_get_tzid(zone));
280             if (tzid) {
281                 const char *newtzid = e_cal_match_tzid(tzid);
282                 if (newtzid) {
283                     /* matched against system time zone */
284                     g_free(key);
285                     key = g_strdup(tzid);
286                     if (!key) {
287                         goto nomem;
288                     }
289
290                     g_free(value);
291                     value = g_strdup(newtzid);
292                     if (!value) {
293                         goto nomem;
294                     }
295
296                     g_hash_table_insert(mapping, key, value);
297                     g_hash_table_insert(systemtzids, value, NULL);
298                     key =
299                         value = NULL;
300                 } else {
301                     zonestr = ical_strdup(icalcomponent_as_ical_string(subcomp));
302
303                     /* check for collisions with existing timezones */
304                     int counter;
305                     for (counter = 0;
306                          counter < 100 /* sanity limit */;
307                          counter++) {
308                         icaltimezone *existing_zone;
309
310                         if (counter) {
311                             g_free(value);
312                             value = g_strdup_printf("%s %d", tzid, counter);
313                         }
314                         existing_zone = tzlookup(counter ? value : tzid,
315                                                  custom,
316                                                  error);
317                         if (!existing_zone) {
318                             if (*error) {
319                                 goto failed;
320                             } else {
321                                 break;
322                             }
323                         }
324                         g_free(buffer);
325                         buffer = ical_strdup(icalcomponent_as_ical_string(icaltimezone_get_component(existing_zone)));
326
327                         if (counter) {
328                             char *fulltzid = g_strdup_printf("TZID:%s", value);
329                             size_t baselen = strlen("TZID:") + strlen(tzid);
330                             size_t fulllen = strlen(fulltzid);
331                             char *tzidprop;
332                             /*
333                              * Map TZID with counter suffix back to basename.
334                              */
335                             tzidprop = strstr(buffer, fulltzid);
336                             if (tzidprop) {
337                                 memmove(tzidprop + baselen,
338                                         tzidprop + fulllen,
339                                         strlen(tzidprop + fulllen) + 1);
340                             }
341                             g_free(fulltzid);
342                         }
343                             
344
345                         /*
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
353                          * practice.
354                          */
355                         if (!strcmp(zonestr, buffer)) {
356                             break;
357                         }
358                     }
359
360                     if (!counter) {
361                         /* does not exist, nothing to do */
362                     } else {
363                         /* timezone renamed */
364                         icalproperty *prop = icalcomponent_get_first_property(subcomp,
365                                                                               ICAL_TZID_PROPERTY);
366                         while (prop) {
367                             icalproperty_set_value_from_string(prop, value, "NO");
368                             prop = icalcomponent_get_next_property(subcomp,
369                                                                    ICAL_ANY_PROPERTY);
370                         }
371                         g_free(key);
372                         key = g_strdup(tzid);
373                         g_hash_table_insert(mapping, key, value);
374                         key =
375                             value = NULL;
376                     }
377                 }
378             }
379         }
380
381         subcomp = icalcomponent_get_next_component(comp,
382                                                    ICAL_VTIMEZONE_COMPONENT);
383     }
384
385     /*
386      * now replace all TZID parameters in place
387      */
388     subcomp = icalcomponent_get_first_component(comp,
389                                                 ICAL_ANY_COMPONENT);
390     while (subcomp) {
391         /*
392          * Leave VTIMEZONE unchanged, iterate over properties of
393          * everything else.
394          *
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
398          * added below.
399          */
400         patch_tzids (subcomp, mapping);
401         subcomp = icalcomponent_get_next_component(comp,
402                                                    ICAL_ANY_COMPONENT);
403     }
404
405     for (l = comps; l; l = l->next) {
406         patch_tzids (l->data, mapping);
407     }
408
409     /*
410      * add system time zones that we mapped to: adding them ensures
411      * that the VCALENDAR remains consistent
412      */
413     g_hash_table_foreach(systemtzids, addsystemtz, comp);
414     
415     goto done;
416  nomem:
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");
419     if (!*error) {
420         g_error("e_cal_check_timezones(): out of memory, cannot proceed - sorry!");
421     }
422  failed:
423     /* gerror should have been set already */
424     success = FALSE;
425  done:
426     if (mapping) {
427         g_hash_table_destroy(mapping);
428     }
429     if (systemtzids) {
430         g_hash_table_destroy(systemtzids);
431     }
432     if (zone) {
433         icaltimezone_free(zone, 1);
434     }
435     g_free(tzid);
436     g_free(zonestr);
437     g_free(buffer);
438     g_free(key);
439     g_free(value);
440     
441     return success;
442 }
443
444 /**
445  * e_cal_tzlookup_ecal:
446  * @custom: must be a valid ECal pointer
447  *
448  * An implementation of the tzlookup callback which clients
449  * can use. Calls #e_cal_get_timezone.
450  */
451 icaltimezone *e_cal_tzlookup_ecal(const char *tzid,
452                                   const void *custom,
453                                   GError **error)
454 {
455     ECal *ecal = (ECal *)custom;
456     icaltimezone *zone = NULL;
457
458     if (e_cal_get_timezone(ecal, tzid, &zone, error)) {
459         g_assert(*error == NULL);
460         return zone;
461     } else {
462         g_assert(*error);
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 */ )) {
466             /*
467              * we had to trigger this error to check for the timezone existance,
468              * clear it and return NULL
469              */
470             g_clear_error(error);
471         }
472         return NULL;
473     }
474 }
475
476 /**
477  * e_cal_tzlookup_icomp:
478  * @custom: must be a icalcomponent pointer which contains
479  *          either a VCALENDAR with VTIMEZONEs or VTIMEZONES
480  *          directly
481  *
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.
485  */
486 icaltimezone *e_cal_tzlookup_icomp(const char *tzid,
487                                    const void *custom,
488                                    GError **error)
489 {
490     icalcomponent *icomp = (icalcomponent *)custom;
491
492     return icalcomponent_get_timezone(icomp, (char *)tzid);
493 }