02791abaf183723fedb810d2e127fa50cee2c285
[platform/upstream/syncevolution.git] / src / backends / evolution / e-cal-check-timezones.c
1 /*
2  * Copyright (C) 2008 Novell, Inc.
3  *
4  * Authors: Patrick Ohly <patrick.ohly@gmx.de>
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of version 2 of the GNU Lesser General Public
8  * License as published by the Free Software Foundation.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  */
19
20 #ifndef HANDLE_LIBICAL_MEMORY
21 # define HANDLE_LIBICAL_MEMORY 1
22 #endif
23 #include <libical/ical.h>
24
25 #ifdef LIBICAL_MEMFIXES
26 /* avoid dependency on icalstrdup.h, works when compiling as part of EDS >= 2.22 */
27 # define ical_strdup(_x) (_x)
28 #else
29 /* use icalstrdup.h to get runtime detection of memory fix patch */
30 # include "libical/icalstrdup.h"
31 #endif
32
33 #include "e-cal-check-timezones.h"
34 #include <libecal/e-cal.h>
35 #include <string.h>
36 #include <ctype.h>
37
38 /**
39  * Matches a location to a system timezone definition via a fuzzy
40  * search and returns the matching TZID, or NULL if none found.
41  *
42  * Currently simply strips a suffix introduced by a hyphen,
43  * as in "America/Denver-(Standard)".
44  */
45 static const char *e_cal_match_location(const char *location)
46 {
47     icaltimezone *icomp;
48     const char *tail;
49     size_t len;
50     char *buffer;
51
52     icomp = icaltimezone_get_builtin_timezone (location);
53     if (icomp) {
54         return icaltimezone_get_tzid(icomp);
55     }
56
57     /* try a bit harder by stripping trailing suffix */
58     tail = strrchr(location, '-');
59     len = tail ? (tail - location) : strlen(location);
60     buffer = g_malloc(len + 1);
61
62     if (buffer) {
63         memcpy(buffer, location, len);
64         buffer[len] = 0;
65         icomp = icaltimezone_get_builtin_timezone (buffer);
66         g_free(buffer);
67         if (icomp) {
68             return icaltimezone_get_tzid(icomp);
69         }
70     }
71
72     return NULL;
73 }
74
75 /**
76  * e_cal_match_tzid:
77  * matches a TZID against the system timezone definitions
78  * and returns the matching TZID, or NULL if none found
79  */
80 const char *e_cal_match_tzid(const char *tzid)
81 {
82     const char *location;
83     const char *systzid;
84     size_t len = strlen(tzid);
85     ssize_t eostr;
86
87     /*
88      * Try without any trailing spaces/digits: they might have been added
89      * by e_cal_check_timezones() in order to distinguish between
90      * different incompatible definitions. At that time mapping
91      * to system time zones must have failed, but perhaps now
92      * we have better code and it succeeds...
93      */
94     eostr = len - 1;
95     while (eostr >= 0 &&
96            isdigit(tzid[eostr])) {
97         eostr--;
98     }
99     while (eostr >= 0 &&
100            isspace(tzid[eostr])) {
101         eostr--;
102     }
103     if (eostr + 1 < len) {
104         char *strippedtzid = g_strndup(tzid, eostr + 1);
105         if (strippedtzid) {
106             systzid = e_cal_match_tzid(strippedtzid);
107             g_free(strippedtzid);
108             if (systzid) {
109                 return systzid;
110             }
111         }
112     }
113
114     /*
115      * old-style Evolution: /softwarestudio.org/Olson_20011030_5/America/Denver
116      *
117      * jump from one slash to the next and check whether the remainder
118      * is a known location; start with the whole string (just in case)
119      */
120     for (location = tzid;
121          location && location[0];
122          location = strchr(location + 1, '/')) {
123         systzid = e_cal_match_location(location[0] == '/' ?
124                                        location + 1 :
125                                        location);
126         if (systzid) {
127             return systzid;
128         }
129     }
130
131     /* TODO: lookup table for Exchange TZIDs */
132
133     return NULL;
134 }
135
136 static void patch_tzids(icalcomponent *subcomp,
137                         GHashTable *mapping)
138 {
139     char *tzid = NULL;
140
141     if (icalcomponent_isa(subcomp) != ICAL_VTIMEZONE_COMPONENT) {
142         icalproperty *prop = icalcomponent_get_first_property(subcomp,
143                                                               ICAL_ANY_PROPERTY);
144         while (prop) {
145             icalparameter *param = icalproperty_get_first_parameter(prop,
146                                                                     ICAL_TZID_PARAMETER);
147             while (param) {
148                 const char *oldtzid;
149                 const char *newtzid;
150
151                 g_free(tzid);
152                 tzid = g_strdup(icalparameter_get_tzid(param));
153
154                 if (!g_hash_table_lookup_extended(mapping,
155                                                   tzid,
156                                                   (gpointer *)&oldtzid,
157                                                   (gpointer *)&newtzid)) {
158                     /* Corresponding VTIMEZONE not seen before! */
159                     newtzid = e_cal_match_tzid(tzid);
160                 }
161                 if (newtzid) {
162                     icalparameter_set_tzid(param, newtzid);
163                 }
164                 param = icalproperty_get_next_parameter(prop,
165                                                         ICAL_TZID_PARAMETER);
166             }
167             prop = icalcomponent_get_next_property(subcomp,
168                                                    ICAL_ANY_PROPERTY);
169         }
170     }
171
172     g_free(tzid);
173 }
174
175 static void addsystemtz(gpointer key,
176                         gpointer value,
177                         gpointer user_data)
178 {
179     const char *tzid = key;
180     icalcomponent *comp = user_data;
181     icaltimezone *zone;
182
183     zone = icaltimezone_get_builtin_timezone_from_tzid(tzid);
184     if (zone) {
185         icalcomponent_add_component(comp,
186                                     icalcomponent_new_clone(icaltimezone_get_component(zone)));
187     }
188 }
189
190 /**
191  * e_cal_check_timezones:
192  * @comp:     a VCALENDAR containing a list of
193  *            VTIMEZONE and arbitrary other components, in
194  *            arbitrary order: these other components are
195  *            modified by this call
196  * @comps:    a list of icalcomponent instances which
197  *            also have to be patched; may be NULL
198  * @tzlookup: a callback function which is called to retrieve
199  *            a calendar's VTIMEZONE definition; the returned
200  *            definition is *not* freed by e_cal_check_timezones()
201  *            (to be compatible with e_cal_get_timezone());
202  *            NULL indicates that no such timezone exists
203  *            or an error occurred
204  * @custom:   an arbitrary pointer which is passed through to
205  *            the tzlookup function
206  * @error:    an error description in case of a failure
207  *
208  * This function cleans up VEVENT, VJOURNAL, VTODO and VTIMEZONE
209  * items which are to be imported into Evolution.
210  *
211  * Using VTIMEZONE definitions is problematic because they cannot be
212  * updated properly when timezone definitions change. They are also
213  * incomplete (for compatibility reason only one set of rules for
214  * summer saving changes can be included, even if different rules
215  * apply in different years). This function looks for matches of the
216  * used TZIDs against system timezones and replaces such TZIDs with
217  * the corresponding system timezone. This works for TZIDs containing
218  * a location (found via a fuzzy string search) and for Outlook TZIDs
219  * (via a hard-coded lookup table).
220  *
221  * Some programs generate broken meeting invitations with TZID, but
222  * without including the corresponding VTIMEZONE. Importing such
223  * invitations unchanged causes problems later on (meeting displayed
224  * incorrectly, #e_cal_get_component_as_string fails). The situation
225  * where this occurred in the past (found by a SyncEvolution user) is
226  * now handled via the location based mapping.
227  *
228  * If this mapping fails, this function also deals with VTIMEZONE
229  * conflicts: such conflicts occur when the calendar already contains
230  * an old VTIMEZONE definition with the same TZID, but different
231  * summer saving rules. Replacing the VTIMEZONE potentially breaks
232  * displaying of old events, whereas not replacing it breaks the new
233  * events (the behavior in Evolution <= 2.22.1).
234  *
235  * The way this problem is resolved is by renaming the new VTIMEZONE
236  * definition until the TZID is unique. A running count is appended to
237  * the TZID. All items referencing the renamed TZID are adapted
238  * accordingly.
239  *
240  * Return value: TRUE if successful, FALSE otherwise.
241  */
242 gboolean e_cal_check_timezones(icalcomponent *comp,
243                                GList *comps,
244                                icaltimezone *(*tzlookup)(const char *tzid,
245                                                          const void *custom,
246                                                          GError **error),
247                                const void *custom,
248                                GError **error)
249 {
250     gboolean success = TRUE;
251     icalcomponent *subcomp = NULL;
252     icaltimezone *zone = icaltimezone_new();
253     char *key = NULL, *value = NULL;
254     char *buffer = NULL;
255     char *zonestr = NULL;
256     char *tzid = NULL;
257     GList *l;
258
259     /** a hash from old to new tzid; strings dynamically allocated */
260     GHashTable *mapping = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
261
262     /** a hash of all system time zone IDs which have to be added; strings are shared with mapping hash */
263     GHashTable *systemtzids = g_hash_table_new(g_str_hash, g_str_equal);
264
265     *error = NULL;
266
267     if (!mapping || !zone) {
268         goto nomem;
269     }
270
271     /* iterate over all VTIMEZONE definitions */
272     subcomp = icalcomponent_get_first_component(comp,
273                                                 ICAL_VTIMEZONE_COMPONENT);
274     while (subcomp) {
275         if (icaltimezone_set_component(zone, subcomp)) {
276             g_free(tzid);
277             tzid = g_strdup(icaltimezone_get_tzid(zone));
278             if (tzid) {
279                 const char *newtzid = e_cal_match_tzid(tzid);
280                 if (newtzid) {
281                     /* matched against system time zone */
282                     g_free(key);
283                     key = g_strdup(tzid);
284                     if (!key) {
285                         goto nomem;
286                     }
287
288                     g_free(value);
289                     value = g_strdup(newtzid);
290                     if (!value) {
291                         goto nomem;
292                     }
293
294                     g_hash_table_insert(mapping, key, value);
295                     g_hash_table_insert(systemtzids, value, NULL);
296                     key =
297                         value = NULL;
298                 } else {
299                     zonestr = ical_strdup(icalcomponent_as_ical_string(subcomp));
300
301                     /* check for collisions with existing timezones */
302                     int counter;
303                     for (counter = 0;
304                          counter < 100 /* sanity limit */;
305                          counter++) {
306                         icaltimezone *existing_zone;
307
308                         if (counter) {
309                             g_free(value);
310                             value = g_strdup_printf("%s %d", tzid, counter);
311                         }
312                         existing_zone = tzlookup(counter ? value : tzid,
313                                                  custom,
314                                                  error);
315                         if (!existing_zone) {
316                             if (*error) {
317                                 goto failed;
318                             } else {
319                                 break;
320                             }
321                         }
322                         g_free(buffer);
323                         buffer = ical_strdup(icalcomponent_as_ical_string(icaltimezone_get_component(existing_zone)));
324
325                         if (counter) {
326                             char *fulltzid = g_strdup_printf("TZID:%s", value);
327                             size_t baselen = strlen("TZID:") + strlen(tzid);
328                             size_t fulllen = strlen(fulltzid);
329                             char *tzidprop;
330                             /*
331                              * Map TZID with counter suffix back to basename.
332                              */
333                             tzidprop = strstr(buffer, fulltzid);
334                             if (tzidprop) {
335                                 memmove(tzidprop + baselen,
336                                         tzidprop + fulllen,
337                                         strlen(tzidprop + fulllen) + 1);
338                             }
339                             g_free(fulltzid);
340                         }
341                             
342
343                         /*
344                          * If the strings are identical, then the
345                          * VTIMEZONE definitions are identical.  If
346                          * they are not identical, then VTIMEZONE
347                          * definitions might still be semantically
348                          * correct and we waste some space by
349                          * needlesly duplicating the VTIMEZONE. This
350                          * is expected to occur rarely (if at all) in
351                          * practice.
352                          */
353                         if (!strcmp(zonestr, buffer)) {
354                             break;
355                         }
356                     }
357
358                     if (!counter) {
359                         /* does not exist, nothing to do */
360                     } else {
361                         /* timezone renamed */
362                         icalproperty *prop = icalcomponent_get_first_property(subcomp,
363                                                                               ICAL_TZID_PROPERTY);
364                         while (prop) {
365                             icalproperty_set_value_from_string(prop, value, "NO");
366                             prop = icalcomponent_get_next_property(subcomp,
367                                                                    ICAL_ANY_PROPERTY);
368                         }
369                         g_free(key);
370                         key = g_strdup(tzid);
371                         g_hash_table_insert(mapping, key, value);
372                         key =
373                             value = NULL;
374                     }
375                 }
376             }
377         }
378
379         subcomp = icalcomponent_get_next_component(comp,
380                                                    ICAL_VTIMEZONE_COMPONENT);
381     }
382
383     /*
384      * now replace all TZID parameters in place
385      */
386     subcomp = icalcomponent_get_first_component(comp,
387                                                 ICAL_ANY_COMPONENT);
388     while (subcomp) {
389         /*
390          * Leave VTIMEZONE unchanged, iterate over properties of
391          * everything else.
392          *
393          * Note that no attempt is made to remove unused VTIMEZONE
394          * definitions. That would just make the code more complex for
395          * little additional gain. However, newly used time zones are
396          * added below.
397          */
398         patch_tzids (subcomp, mapping);
399         subcomp = icalcomponent_get_next_component(comp,
400                                                    ICAL_ANY_COMPONENT);
401     }
402
403     for (l = comps; l; l = l->next) {
404         patch_tzids (l->data, mapping);
405     }
406
407     /*
408      * add system time zones that we mapped to: adding them ensures
409      * that the VCALENDAR remains consistent
410      */
411     g_hash_table_foreach(systemtzids, addsystemtz, comp);
412     
413     goto done;
414  nomem:
415     /* set gerror for "out of memory" if possible, otherwise abort via g_error() */
416     *error = g_error_new(E_CALENDAR_ERROR, E_CALENDAR_STATUS_OTHER_ERROR, "out of memory");
417     if (!*error) {
418         g_error("e_cal_check_timezones(): out of memory, cannot proceed - sorry!");
419     }
420  failed:
421     /* gerror should have been set already */
422     success = FALSE;
423  done:
424     if (mapping) {
425         g_hash_table_destroy(mapping);
426     }
427     if (systemtzids) {
428         g_hash_table_destroy(systemtzids);
429     }
430     if (zone) {
431         icaltimezone_free(zone, 1);
432     }
433     g_free(tzid);
434     g_free(zonestr);
435     g_free(buffer);
436     g_free(key);
437     g_free(value);
438     
439     return success;
440 }
441
442 /**
443  * e_cal_tzlookup_ecal:
444  * @custom: must be a valid ECal pointer
445  *
446  * An implementation of the tzlookup callback which clients
447  * can use. Calls #e_cal_get_timezone.
448  */
449 icaltimezone *e_cal_tzlookup_ecal(const char *tzid,
450                                   const void *custom,
451                                   GError **error)
452 {
453     ECal *ecal = (ECal *)custom;
454     icaltimezone *zone = NULL;
455
456     if (e_cal_get_timezone(ecal, tzid, &zone, error)) {
457         g_assert(*error == NULL);
458         return zone;
459     } else {
460         g_assert(*error);
461         if ((*error)->domain == E_CALENDAR_ERROR &&
462             (*error)->code == E_CALENDAR_STATUS_OBJECT_NOT_FOUND) {
463             /*
464              * we had to trigger this error to check for the timezone existance,
465              * clear it and return NULL
466              */
467             g_clear_error(error);
468         }
469         return NULL;
470     }
471 }
472
473 /**
474  * e_cal_tzlookup_icomp:
475  * @custom: must be a icalcomponent pointer which contains
476  *          either a VCALENDAR with VTIMEZONEs or VTIMEZONES
477  *          directly
478  *
479  * An implementation of the tzlookup callback which backends
480  * like the file backend can use. Searches for the timezone
481  * in the component list.
482  */
483 icaltimezone *e_cal_tzlookup_icomp(const char *tzid,
484                                    const void *custom,
485                                    GError **error)
486 {
487     icalcomponent *icomp = (icalcomponent *)custom;
488
489     return icalcomponent_get_timezone(icomp, (char *)tzid);
490 }