/* Evolution calendar utilities and types * * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) * * This library is free software: you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation. * * This library 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 library. If not, see . * * Authors: Federico Mena-Quintero */ #include "evolution-data-server-config.h" #include #include #include #include #include #include "e-cal-check-timezones.h" #include "e-cal-client.h" #include "e-cal-system-timezone.h" #include "e-cal-recur.h" #include "e-cal-util.h" #define _TIME_MIN ((time_t) 0) /* Min valid time_t */ #define _TIME_MAX ((time_t) INT_MAX) /** * e_cal_util_new_top_level: * * Creates a new VCALENDAR component. Free it with g_object_unref(), * when no longer needed. * * Returns: (transfer full): the newly created top level component. */ ICalComponent * e_cal_util_new_top_level (void) { ICalComponent *icalcomp; ICalProperty *prop; icalcomp = i_cal_component_new (I_CAL_VCALENDAR_COMPONENT); /* RFC 2445, section 4.7.1 */ prop = i_cal_property_new_calscale ("GREGORIAN"); i_cal_component_take_property (icalcomp, prop); /* RFC 2445, section 4.7.3 */ prop = i_cal_property_new_prodid ("-//Ximian//NONSGML Evolution Calendar//EN"); i_cal_component_take_property (icalcomp, prop); /* RFC 2445, section 4.7.4. This is the iCalendar spec version, *NOT* * the product version! Do not change this! */ prop = i_cal_property_new_version ("2.0"); i_cal_component_take_property (icalcomp, prop); return icalcomp; } /** * e_cal_util_new_component: * @kind: Kind of the component to create, as #ICalComponentKind. * * Creates a new #ICalComponent of the specified kind. Free it * with g_object_unref(), when no longer needed. * * Returns: (transfer full): the newly created component. */ ICalComponent * e_cal_util_new_component (ICalComponentKind kind) { ICalComponent *icalcomp; ICalTime *dtstamp; gchar *uid; icalcomp = i_cal_component_new (kind); uid = e_util_generate_uid (); i_cal_component_set_uid (icalcomp, uid); g_free (uid); dtstamp = i_cal_time_new_current_with_zone (i_cal_timezone_get_utc_timezone ()); i_cal_component_set_dtstamp (icalcomp, dtstamp); g_object_unref (dtstamp); return icalcomp; } /** * e_cal_util_copy_timezone: * @zone: an ICalTimezone * * Copies the @zone together with its inner component and * returns it as a new #ICalTimezone object. Free it with * g_object_unref(), when no longer needed. * * Returns: (transfer full): a copy of the @zone * * Since: 3.34 **/ ICalTimezone * e_cal_util_copy_timezone (const ICalTimezone *zone) { ICalComponent *comp; ICalTimezone *zone_copy; g_return_val_if_fail (zone != NULL, NULL); zone_copy = i_cal_timezone_copy (zone); if (!zone_copy) return NULL; /* If the original component is one of the built-in, then libcal loads it during the i_cal_timezone_get_component() call and assigns a component to it. */ comp = i_cal_timezone_get_component (zone_copy); if (comp) { g_object_unref (comp); return zone_copy; } comp = i_cal_timezone_get_component (zone); if (comp) { ICalComponent *comp_copy; comp_copy = i_cal_component_clone (comp); if (!i_cal_timezone_set_component (zone_copy, comp_copy)) g_clear_object (&zone_copy); g_object_unref (comp_copy); g_object_unref (comp); } return zone_copy; } static gchar * read_line (const gchar *string) { GString *line_str = NULL; for (; *string; string++) { if (!line_str) line_str = g_string_new (""); g_string_append_c (line_str, *string); if (*string == '\n') break; } return g_string_free (line_str, FALSE); } /** * e_cal_util_parse_ics_string: * @string: iCalendar string to be parsed. * * Parses an iCalendar string and returns a new #ICalComponent representing * that string. Note that this function deals with multiple VCALENDAR's in the * string, something that Mozilla used to do and which libical does not * support. * * Free the returned non-NULL component with g_object_unref(), when no longer needed. * * Returns: (transfer full) (nullable): a newly created #ICalComponent, or %NULL, * if the string isn't a valid iCalendar string. */ ICalComponent * e_cal_util_parse_ics_string (const gchar *string) { GString *comp_str = NULL; gchar *s; ICalComponent *icalcomp = NULL; g_return_val_if_fail (string != NULL, NULL); /* Split string into separated VCALENDAR's, if more than one */ s = g_strstr_len (string, strlen (string), "BEGIN:VCALENDAR"); if (s == NULL) return i_cal_parser_parse_string (string); while (*s != '\0') { gchar *line = read_line (s); if (!comp_str) comp_str = g_string_new (line); else g_string_append (comp_str, line); if (strncmp (line, "END:VCALENDAR", 13) == 0) { ICalComponent *tmp; tmp = i_cal_parser_parse_string (comp_str->str); if (tmp && i_cal_component_isa (tmp) == I_CAL_VCALENDAR_COMPONENT) { if (icalcomp) { i_cal_component_merge_component (icalcomp, tmp); g_object_unref (tmp); } else icalcomp = tmp; } else { g_warning ( "Could not merge the components, " "the component is either invalid " "or not a toplevel component \n"); } g_string_free (comp_str, TRUE); comp_str = NULL; } s += strlen (line); g_free (line); } if (comp_str) g_string_free (comp_str, TRUE); return icalcomp; } struct ics_file { FILE *file; gboolean bof; }; static gchar * get_line_fn (gchar *buf, gsize size, gpointer user_data) { struct ics_file *fl = user_data; /* Skip the UTF-8 marker at the beginning of the file */ if (fl->bof) { gchar *orig_buf = buf; gchar tmp[4]; fl->bof = FALSE; if (fread (tmp, sizeof (gchar), 3, fl->file) != 3 || feof (fl->file)) return NULL; if (((guchar) tmp[0]) != 0xEF || ((guchar) tmp[1]) != 0xBB || ((guchar) tmp[2]) != 0xBF) { if (size <= 3) return NULL; buf[0] = tmp[0]; buf[1] = tmp[1]; buf[2] = tmp[2]; buf += 3; size -= 3; } if (!fgets (buf, size, fl->file)) return NULL; return orig_buf; } return fgets (buf, size, fl->file); } /** * e_cal_util_parse_ics_file: * @filename: Name of the file to be parsed. * * Parses the given file, and, if it contains a valid iCalendar object, * parse it and return a new #ICalComponent. * * Free the returned non-NULL component with g_object_unref(), when no longer needed. * * Returns: (transfer full) (nullable): a newly created #ICalComponent, or %NULL, * if the file doesn't contain a valid iCalendar object. */ ICalComponent * e_cal_util_parse_ics_file (const gchar *filename) { ICalParser *parser; ICalComponent *icalcomp; struct ics_file fl; fl.file = g_fopen (filename, "rb"); if (!fl.file) return NULL; fl.bof = TRUE; parser = i_cal_parser_new (); icalcomp = i_cal_parser_parse (parser, get_line_fn, &fl); g_object_unref (parser); fclose (fl.file); return icalcomp; } /* Computes the range of time in which recurrences should be generated for a * component in order to compute alarm trigger times. */ static void compute_alarm_range (ECalComponent *comp, GSList *alarm_uids, time_t start, time_t end, time_t *alarm_start, time_t *alarm_end) { GSList *link; time_t repeat_time; *alarm_start = start; *alarm_end = end; repeat_time = 0; for (link = alarm_uids; link; link = g_slist_next (link)) { const gchar *auid; ECalComponentAlarm *alarm; ECalComponentAlarmTrigger *trigger; ICalDuration *dur; time_t dur_time; ECalComponentAlarmRepeat *repeat; auid = link->data; alarm = e_cal_component_get_alarm (comp, auid); g_return_if_fail (alarm != NULL); trigger = e_cal_component_alarm_get_trigger (alarm); repeat = e_cal_component_alarm_get_repeat (alarm); if (!trigger) { e_cal_component_alarm_free (alarm); continue; } switch (e_cal_component_alarm_trigger_get_kind (trigger)) { case E_CAL_COMPONENT_ALARM_TRIGGER_NONE: case E_CAL_COMPONENT_ALARM_TRIGGER_ABSOLUTE: break; case E_CAL_COMPONENT_ALARM_TRIGGER_RELATIVE_START: case E_CAL_COMPONENT_ALARM_TRIGGER_RELATIVE_END: dur = e_cal_component_alarm_trigger_get_duration (trigger); dur_time = i_cal_duration_as_int (dur); if (repeat && e_cal_component_alarm_repeat_get_repetitions (repeat) != 0) { gint rdur; rdur = e_cal_component_alarm_repeat_get_repetitions (repeat) * e_cal_component_alarm_repeat_get_interval_seconds (repeat); repeat_time = MAX (repeat_time, rdur); } if (i_cal_duration_is_neg (dur)) /* If the duration is negative then dur_time * will be negative as well; that is why we * subtract to expand the range. */ *alarm_end = MAX (*alarm_end, end - dur_time); else *alarm_start = MIN (*alarm_start, start - dur_time); break; } e_cal_component_alarm_free (alarm); } *alarm_start -= repeat_time; g_warn_if_fail (*alarm_start <= *alarm_end); } /* Closure data to generate alarm occurrences */ struct alarm_occurrence_data { ECalComponent *comp; /* These are the info we have */ GSList *alarm_uids; /* gchar * */ time_t start; time_t end; ECalComponentAlarmAction *omit; gboolean only_check; gboolean any_exists; /* This is what we compute */ GSList *triggers; /* ECalComponentAlarmInstance * */ }; static void add_trigger (struct alarm_occurrence_data *aod, ECalComponent *instance_comp, const gchar *auid, const gchar *rid, time_t instance_time, time_t occur_start, time_t occur_end) { ECalComponentAlarmInstance *instance; aod->any_exists = TRUE; if (aod->only_check) return; instance = e_cal_component_alarm_instance_new (auid, instance_time, occur_start, occur_end); if (rid && *rid) e_cal_component_alarm_instance_set_rid (instance, rid); e_cal_component_alarm_instance_set_component (instance, instance_comp); aod->triggers = g_slist_prepend (aod->triggers, instance); } /* Callback used from cal_recur_generate_instances(); generates triggers for all * of a component's RELATIVE alarms. */ static gboolean add_alarm_occurrences_cb (ICalComponent *icalcomp, ICalTime *instance_start, ICalTime *instance_end, gpointer user_data, GCancellable *cancellable, GError **error) { struct alarm_occurrence_data *aod = user_data; ECalComponent *comp; time_t start, end; GSList *link; gchar *rid; if (aod->comp) comp = g_object_ref (aod->comp); else comp = e_cal_component_new_from_icalcomponent (i_cal_component_clone (icalcomp)); g_return_val_if_fail (comp != NULL, FALSE); start = i_cal_time_as_timet_with_zone (instance_start, i_cal_time_get_timezone (instance_start)); end = i_cal_time_as_timet_with_zone (instance_end, i_cal_time_get_timezone (instance_end)); rid = e_cal_util_component_get_recurid_as_string (icalcomp); if (!rid || !*rid) { g_clear_pointer (&rid, g_free); rid = i_cal_time_as_ical_string (instance_start); } for (link = aod->alarm_uids; link && (!aod->only_check || !aod->any_exists); link = g_slist_next (link)) { const gchar *auid; ECalComponentAlarm *alarm; ECalComponentAlarmAction action; ECalComponentAlarmTrigger *trigger; ECalComponentAlarmRepeat *repeat; ICalDuration *dur; time_t dur_time; time_t occur_time, trigger_time; gint i; auid = link->data; alarm = e_cal_component_get_alarm (comp, auid); if (!alarm) continue; action = e_cal_component_alarm_get_action (alarm); trigger = e_cal_component_alarm_get_trigger (alarm); repeat = e_cal_component_alarm_get_repeat (alarm); if (!trigger) { e_cal_component_alarm_free (alarm); continue; } for (i = 0; aod->omit[i] != -1; i++) { if (aod->omit[i] == action) break; } if (aod->omit[i] != -1) { e_cal_component_alarm_free (alarm); continue; } if (e_cal_component_alarm_trigger_get_kind (trigger) != E_CAL_COMPONENT_ALARM_TRIGGER_RELATIVE_START && e_cal_component_alarm_trigger_get_kind (trigger) != E_CAL_COMPONENT_ALARM_TRIGGER_RELATIVE_END) { e_cal_component_alarm_free (alarm); continue; } dur = e_cal_component_alarm_trigger_get_duration (trigger); dur_time = i_cal_duration_as_int (dur); if (e_cal_component_alarm_trigger_get_kind (trigger) == E_CAL_COMPONENT_ALARM_TRIGGER_RELATIVE_START) occur_time = start; else occur_time = end; /* If dur->is_neg is true then dur_time will already be * negative. So we do not need to test for dur->is_neg here; we * can simply add the dur_time value to the occur_time and get * the correct result. */ trigger_time = occur_time + dur_time; /* Add repeating alarms */ if (repeat && e_cal_component_alarm_repeat_get_repetitions (repeat) != 0) { gint ii, repetitions; time_t repeat_time; repeat_time = e_cal_component_alarm_repeat_get_interval_seconds (repeat); repetitions = e_cal_component_alarm_repeat_get_repetitions (repeat); for (ii = 0; ii < repetitions; ii++) { time_t t; t = trigger_time + (ii + 1) * repeat_time; if (t >= aod->start && t < aod->end) add_trigger (aod, comp, auid, rid, t, start, end); } } /* Add the trigger itself */ if (trigger_time >= aod->start && trigger_time < aod->end) add_trigger (aod, comp, auid, rid, trigger_time, start, end); e_cal_component_alarm_free (alarm); } g_clear_object (&comp); g_free (rid); return !aod->only_check || !aod->any_exists; } /* Generates the absolute triggers for a component */ static void generate_absolute_triggers (ECalComponent *comp, struct alarm_occurrence_data *aod, ECalRecurResolveTimezoneCb resolve_tzid, gpointer user_data, ICalTimezone *default_timezone) { GSList *link; ECalComponentDateTime *dtstart, *dtend; time_t occur_start, occur_end; gchar *rid; dtstart = e_cal_component_get_dtstart (comp); dtend = e_cal_component_get_dtend (comp); rid = e_cal_component_get_recurid_as_string (comp); /* No particular occurrence, so just use the times from the * component */ if (dtstart && e_cal_component_datetime_get_value (dtstart)) { ICalTimezone *zone; const gchar *tzid = e_cal_component_datetime_get_tzid (dtstart); if (tzid && !i_cal_time_is_date (e_cal_component_datetime_get_value (dtstart))) zone = (* resolve_tzid) (tzid, user_data, NULL, NULL); else zone = default_timezone; occur_start = i_cal_time_as_timet_with_zone (e_cal_component_datetime_get_value (dtstart), zone); } else occur_start = -1; if (dtend && e_cal_component_datetime_get_value (dtend)) { ICalTimezone *zone; const gchar *tzid = e_cal_component_datetime_get_tzid (dtend); if (tzid && !i_cal_time_is_date (e_cal_component_datetime_get_value (dtend))) zone = (* resolve_tzid) (tzid, user_data, NULL, NULL); else zone = default_timezone; occur_end = i_cal_time_as_timet_with_zone (e_cal_component_datetime_get_value (dtend), zone); } else { e_cal_component_datetime_free (dtend); dtend = e_cal_component_get_due (comp); if (dtend && e_cal_component_datetime_get_value (dtend)) { ICalTimezone *zone; const gchar *tzid = e_cal_component_datetime_get_tzid (dtend); if (tzid && !i_cal_time_is_date (e_cal_component_datetime_get_value (dtend))) zone = (* resolve_tzid) (tzid, user_data, NULL, NULL); else zone = default_timezone; occur_end = i_cal_time_as_timet_with_zone (e_cal_component_datetime_get_value (dtend), zone); } else occur_end = -1; } for (link = aod->alarm_uids; link && (!aod->only_check || !aod->any_exists); link = g_slist_next (link)) { const gchar *auid; ECalComponentAlarm *alarm; ECalComponentAlarmAction action; ECalComponentAlarmRepeat *repeat; ECalComponentAlarmTrigger *trigger; time_t abs_time; ICalTimezone *zone; gint i; auid = link->data; alarm = e_cal_component_get_alarm (comp, auid); if (!alarm) continue; action = e_cal_component_alarm_get_action (alarm); trigger = e_cal_component_alarm_get_trigger (alarm); repeat = e_cal_component_alarm_get_repeat (alarm); for (i = 0; aod->omit[i] != -1; i++) { if (aod->omit[i] == action) break; } if (aod->omit[i] != -1) { e_cal_component_alarm_free (alarm); continue; } if (e_cal_component_alarm_trigger_get_kind (trigger) != E_CAL_COMPONENT_ALARM_TRIGGER_ABSOLUTE) { e_cal_component_alarm_free (alarm); continue; } /* Absolute triggers are always in UTC; * see RFC 2445 section 4.8.6.3 */ zone = i_cal_timezone_get_utc_timezone (); abs_time = i_cal_time_as_timet_with_zone (e_cal_component_alarm_trigger_get_absolute_time (trigger), zone); /* Add repeating alarms */ if (repeat && e_cal_component_alarm_repeat_get_repetitions (repeat) > 0) { gint ii, repetitions; time_t repeat_time; repeat_time = e_cal_component_alarm_repeat_get_interval_seconds (repeat); repetitions = e_cal_component_alarm_repeat_get_repetitions (repeat); for (ii = 0; ii < repetitions; ii++) { time_t tt; tt = abs_time + (ii + 1) * repeat_time; if (tt >= aod->start && tt < aod->end) add_trigger (aod, comp, auid, rid, tt, occur_start, occur_end); } } /* Add the trigger itself */ if (abs_time >= aod->start && abs_time < aod->end) add_trigger (aod, comp, auid, rid, abs_time, occur_start, occur_end); e_cal_component_alarm_free (alarm); } e_cal_component_datetime_free (dtstart); e_cal_component_datetime_free (dtend); g_free (rid); } /* Compares two alarm instances; called from g_slist_sort() */ static gint compare_alarm_instance (gconstpointer a, gconstpointer b) { const ECalComponentAlarmInstance *aia, *aib; time_t atime, btime; aia = a; aib = b; atime = e_cal_component_alarm_instance_get_time (aia); btime = e_cal_component_alarm_instance_get_time (aib); if (atime < btime) return -1; else if (atime > btime) return 1; else return 0; } static void ecu_generate_alarms_for_comp (ECalComponent *comp, time_t start, time_t end, ECalComponentAlarmAction *omit, ECalRecurResolveTimezoneCb resolve_tzid, gpointer user_data, ICalTimezone *default_timezone, gboolean *out_any_exists, ECalComponentAlarms **out_alarms) { GSList *alarm_uids; time_t alarm_start, alarm_end; struct alarm_occurrence_data aod; ICalTime *alarm_start_tt, *alarm_end_tt; if (out_any_exists) *out_any_exists = FALSE; if (out_alarms) *out_alarms = NULL; if (!e_cal_component_has_alarms (comp)) return; alarm_uids = e_cal_component_get_alarm_uids (comp); compute_alarm_range (comp, alarm_uids, start, end, &alarm_start, &alarm_end); aod.comp = comp; aod.alarm_uids = alarm_uids; aod.start = start; aod.end = end; aod.omit = omit; aod.only_check = !out_alarms; aod.any_exists = FALSE; aod.triggers = NULL; alarm_start_tt = i_cal_time_new_from_timet_with_zone (alarm_start, FALSE, i_cal_timezone_get_utc_timezone ()); alarm_end_tt = i_cal_time_new_from_timet_with_zone (alarm_end, FALSE, i_cal_timezone_get_utc_timezone ()); e_cal_recur_generate_instances_sync (e_cal_component_get_icalcomponent (comp), alarm_start_tt, alarm_end_tt, add_alarm_occurrences_cb, &aod, resolve_tzid, user_data, default_timezone, NULL, NULL); g_clear_object (&alarm_start_tt); g_clear_object (&alarm_end_tt); if (!aod.only_check || !aod.any_exists) { /* We add the ABSOLUTE triggers separately */ generate_absolute_triggers (comp, &aod, resolve_tzid, user_data, default_timezone); } g_slist_free_full (alarm_uids, g_free); if (out_any_exists) *out_any_exists = aod.any_exists; if (out_alarms && aod.triggers) { *out_alarms = e_cal_component_alarms_new (comp); e_cal_component_alarms_take_instances (*out_alarms, g_slist_sort (aod.triggers, compare_alarm_instance)); } else { g_warn_if_fail (aod.triggers == NULL); } } /** * e_cal_util_generate_alarms_for_comp: * @comp: The #ECalComponent to generate alarms from * @start: Start time * @end: End time * @omit: Alarm types to omit * @resolve_tzid: (closure user_data) (scope call): Callback for resolving * timezones * @user_data: (closure): Data to be passed to the resolve_tzid callback * @default_timezone: The timezone used to resolve DATE and floating DATE-TIME * values. * * Generates alarm instances for a calendar component. Returns the instances * structure, or %NULL if no alarm instances occurred in the specified time * range. Free the returned structure with e_cal_component_alarms_free(), * when no longer needed. * * See e_cal_util_generate_alarms_for_uid_sync() * * Returns: (transfer full) (nullable): a list of all the alarms found * for the given component in the given time range. */ ECalComponentAlarms * e_cal_util_generate_alarms_for_comp (ECalComponent *comp, time_t start, time_t end, ECalComponentAlarmAction *omit, ECalRecurResolveTimezoneCb resolve_tzid, gpointer user_data, ICalTimezone *default_timezone) { ECalComponentAlarms *alarms = NULL; ecu_generate_alarms_for_comp (comp, start, end, omit, resolve_tzid, user_data, default_timezone, NULL, &alarms); return alarms; } /** * e_cal_util_has_alarms_in_range: * @comp: an #ECalComponent to check alarms for * @start: start time * @end: end time * @omit: alarm types to omit * @resolve_tzid: (closure user_data) (scope call): Callback for resolving timezones * @user_data: (closure): Data to be passed to the resolve_tzid callback * @default_timezone: The timezone used to resolve DATE and floating DATE-TIME values. * * Checks whether the @comp has any alarms in the given time interval. * * Returns: %TRUE, when the #comp has any alarms in the given time interval * * Since: 3.48 **/ gboolean e_cal_util_has_alarms_in_range (ECalComponent *comp, time_t start, time_t end, ECalComponentAlarmAction *omit, ECalRecurResolveTimezoneCb resolve_tzid, gpointer user_data, ICalTimezone *default_timezone) { gboolean any_exists = FALSE; ecu_generate_alarms_for_comp (comp, start, end, omit, resolve_tzid, user_data, default_timezone, &any_exists, NULL); return any_exists; } /** * e_cal_util_generate_alarms_for_list: * @comps: (element-type ECalComponent): List of #ECalComponents * @start: Start time * @end: End time * @omit: Alarm types to omit * @comp_alarms: (out) (transfer full) (element-type ECalComponentAlarms): List * to be returned * @resolve_tzid: (closure user_data) (scope call): Callback for resolving * timezones * @user_data: (closure): Data to be passed to the resolve_tzid callback * @default_timezone: The timezone used to resolve DATE and floating DATE-TIME * values. * * Iterates through all the components in the @comps list and generates alarm * instances for them; putting them in the @comp_alarms list. Free the @comp_alarms * with g_slist_free_full (comp_alarms, e_cal_component_alarms_free);, when * no longer neeed. * * See e_cal_util_generate_alarms_for_uid_sync() * * Returns: the number of elements it added to the list */ gint e_cal_util_generate_alarms_for_list (GList *comps, time_t start, time_t end, ECalComponentAlarmAction *omit, GSList **comp_alarms, ECalRecurResolveTimezoneCb resolve_tzid, gpointer user_data, ICalTimezone *default_timezone) { GList *l; gint n; n = 0; for (l = comps; l; l = l->next) { ECalComponent *comp; ECalComponentAlarms *alarms; comp = E_CAL_COMPONENT (l->data); alarms = e_cal_util_generate_alarms_for_comp ( comp, start, end, omit, resolve_tzid, user_data, default_timezone); if (alarms) { *comp_alarms = g_slist_prepend (*comp_alarms, alarms); n++; } } return n; } /** * e_cal_util_generate_alarms_for_uid_sync: * @client: an #ECalClient * @uid: a component UID to generate alarms for * @start: start time * @end: end time * @omit: alarm types to omit * @resolve_tzid: (closure user_data) (scope call): Callback for resolving timezones * @user_data: (closure): Data to be passed to the resolve_tzid callback * @default_timezone: The timezone used to resolve DATE and floating DATE-TIME values * * Generates alarm instances for a calendar component with UID @uid, * which is stored within the @client. In contrast to e_cal_util_generate_alarms_for_comp(), * this function handles detached instances of recurring events properly. * * Returns the instances structure, or %NULL if no alarm instances occurred in the specified * time range. Free the returned structure with e_cal_component_alarms_free(), * when no longer needed. * * Returns: (transfer full) (nullable): a list of all the alarms found * for the given component in the given time range. * * Since: 3.48 **/ ECalComponentAlarms * e_cal_util_generate_alarms_for_uid_sync (ECalClient *client, const gchar *uid, time_t start, time_t end, ECalComponentAlarmAction *omit, ECalRecurResolveTimezoneCb resolve_tzid, gpointer user_data, ICalTimezone *default_timezone, GCancellable *cancellable, GError **error) { GHashTable *alarm_uids_hash; GSList *alarm_uids = NULL; GSList *objects = NULL, *link; time_t alarm_start = start, alarm_end = end; struct alarm_occurrence_data aod; ECalComponentAlarms *alarms; g_return_val_if_fail (E_IS_CAL_CLIENT (client), NULL); g_return_val_if_fail (uid != NULL, NULL); if (!e_cal_client_get_objects_for_uid_sync (client, uid, &objects, cancellable, error)) return NULL; alarm_uids_hash = g_hash_table_new (g_str_hash, g_str_equal); for (link = objects; link; link = g_slist_next (link)) { ECalComponent *comp = link->data; GSList *auids = e_cal_component_get_alarm_uids (comp); if (auids) { GSList *alink; compute_alarm_range (comp, auids, alarm_start, alarm_end, &alarm_start, &alarm_end); for (alink = auids; alink; alink = g_slist_next (alink)) { const gchar *auid = alink->data; if (auid && !g_hash_table_contains (alarm_uids_hash, auid)) { alarm_uids = g_slist_prepend (alarm_uids, (gpointer) auid); g_hash_table_add (alarm_uids_hash, (gpointer) auid); alink->data = NULL; } } g_slist_free_full (auids, g_free); } } g_clear_pointer (&alarm_uids_hash, g_hash_table_destroy); aod.comp = NULL; aod.alarm_uids = alarm_uids; aod.start = start; aod.end = end; aod.omit = omit; aod.only_check = FALSE; aod.any_exists = FALSE; aod.triggers = NULL; e_cal_client_generate_instances_for_uid_sync (client, uid, alarm_start, alarm_end, cancellable, add_alarm_occurrences_cb, &aod); for (link = objects; link; link = g_slist_next (link)) { ECalComponent *comp = link->data; /* We add the ABSOLUTE triggers separately */ generate_absolute_triggers (comp, &aod, resolve_tzid, user_data, default_timezone); } g_slist_free_full (objects, g_object_unref); g_slist_free_full (alarm_uids, g_free); if (!aod.triggers) return NULL; /* Create the component alarm instances structure */ alarms = e_cal_component_alarms_new (NULL); e_cal_component_alarms_take_instances (alarms, g_slist_sort (aod.triggers, compare_alarm_instance)); return alarms; } /** * e_cal_util_priority_to_string: * @priority: Priority value. * * Converts an iCalendar PRIORITY value to a translated string. Any unknown * priority value (i.e. not 0-9) will be returned as "" (undefined). * * Returns: a string representing the PRIORITY value. This value is a * constant, so it should never be freed. */ const gchar * e_cal_util_priority_to_string (gint priority) { const gchar *retval; if (priority <= 0) retval = ""; else if (priority <= 4) retval = C_("Priority", "High"); else if (priority == 5) retval = C_("Priority", "Normal"); else if (priority <= 9) retval = C_("Priority", "Low"); else retval = ""; return retval; } /** * e_cal_util_priority_from_string: * @string: A string representing the PRIORITY value. * * Converts a translated priority string to an iCalendar priority value. * * Returns: the priority (0-9) or -1 if the priority string is not valid. */ gint e_cal_util_priority_from_string (const gchar *string) { gint priority; /* An empty string is the same as 'None'. */ if (!string || !string[0] || !e_util_utf8_strcasecmp (string, C_("Priority", "Undefined"))) priority = 0; else if (!e_util_utf8_strcasecmp (string, C_("Priority", "High"))) priority = 3; else if (!e_util_utf8_strcasecmp (string, C_("Priority", "Normal"))) priority = 5; else if (!e_util_utf8_strcasecmp (string, C_("Priority", "Low"))) priority = 7; else priority = -1; return priority; } /** * e_cal_util_seconds_to_string: * @seconds: actual time, in seconds * * Converts time, in seconds, into a string representation readable by humans * and localized into the current locale. This can be used to convert event * duration to string or similar use cases. * * Free the returned string with g_free(), when no longer needed. * * Returns: (transfer full): a newly allocated string with localized description * of the given time in seconds. * * Since: 3.30 **/ gchar * e_cal_util_seconds_to_string (gint64 seconds) { gchar *times[6], *text; gint ii; ii = 0; if (seconds >= 7 * 24 * 3600) { gint weeks; weeks = seconds / (7 * 24 * 3600); seconds %= (7 * 24 * 3600); times[ii++] = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d week", "%d weeks", weeks), weeks); } if (seconds >= 24 * 3600) { gint days; days = seconds / (24 * 3600); seconds %= (24 * 3600); times[ii++] = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d day", "%d days", days), days); } if (seconds >= 3600) { gint hours; hours = seconds / 3600; seconds %= 3600; times[ii++] = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d hour", "%d hours", hours), hours); } if (seconds >= 60) { gint minutes; minutes = seconds / 60; seconds %= 60; times[ii++] = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d minute", "%d minutes", minutes), minutes); } if (seconds != 0) { /* Translators: here, "second" is the time division (like "minute"), not the ordinal number (like "third") */ times[ii++] = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d second", "%d seconds", seconds), (gint) seconds); } times[ii] = NULL; text = g_strjoinv (" ", times); while (ii > 0) { g_free (times[--ii]); } return text; } /* callback for icalcomponent_foreach_tzid */ typedef struct { ICalComponent *vcal_comp; ICalComponent *icalcomp; } ForeachTzidData; static void add_timezone_cb (ICalParameter *param, gpointer user_data) { ICalTimezone *tz; const gchar *tzid; ICalComponent *vtz_comp; ForeachTzidData *f_data = user_data; tzid = i_cal_parameter_get_tzid (param); if (!tzid) return; tz = i_cal_component_get_timezone (f_data->vcal_comp, tzid); if (tz) { g_object_unref (tz); return; } tz = i_cal_component_get_timezone (f_data->icalcomp, tzid); if (!tz) { tz = i_cal_timezone_get_builtin_timezone_from_tzid (tzid); if (!tz) return; g_object_ref (tz); } vtz_comp = i_cal_timezone_get_component (tz); if (vtz_comp) { i_cal_component_take_component (f_data->vcal_comp, i_cal_component_clone (vtz_comp)); g_object_unref (vtz_comp); } g_object_unref (tz); } /** * e_cal_util_add_timezones_from_component: * @vcal_comp: A VCALENDAR component. * @icalcomp: An iCalendar component, of any type. * * Adds VTIMEZONE components to a VCALENDAR for all tzid's * in the given @icalcomp. */ void e_cal_util_add_timezones_from_component (ICalComponent *vcal_comp, ICalComponent *icalcomp) { ForeachTzidData f_data; g_return_if_fail (vcal_comp != NULL); g_return_if_fail (icalcomp != NULL); f_data.vcal_comp = vcal_comp; f_data.icalcomp = icalcomp; i_cal_component_foreach_tzid (icalcomp, add_timezone_cb, &f_data); } /** * e_cal_util_property_has_parameter: * @prop: an #ICalProperty * @param_kind: a parameter kind to look for, as an %ICalParameterKind * * Returns, whether the @prop has a parameter of @param_kind. * * Returns: whether the @prop has a parameter of @prop_kind * * Since: 3.34 **/ gboolean e_cal_util_property_has_parameter (ICalProperty *prop, ICalParameterKind param_kind) { ICalParameter *param; g_return_val_if_fail (I_CAL_IS_PROPERTY (prop), FALSE); param = i_cal_property_get_first_parameter (prop, param_kind); if (!param) return FALSE; g_object_unref (param); return TRUE; } /** * e_cal_util_component_has_property: * @icalcomp: an #ICalComponent * @prop_kind: a property kind to look for, as an %ICalPropertyKind * * Returns, whether the @icalcomp has a property of @prop_kind. To check * for a specific X property use e_cal_util_component_has_x_property(). * * Returns: whether the @icalcomp has a property of @prop_kind * * Since: 3.34 **/ gboolean e_cal_util_component_has_property (ICalComponent *icalcomp, ICalPropertyKind prop_kind) { ICalProperty *prop; g_return_val_if_fail (I_CAL_IS_COMPONENT (icalcomp), FALSE); prop = i_cal_component_get_first_property (icalcomp, prop_kind); if (!prop) return FALSE; g_object_unref (prop); return TRUE; } /** * e_cal_util_component_is_instance: * @icalcomp: An #ICalComponent. * * Checks whether an #ICalComponent is an instance of a recurring appointment. * * Returns: TRUE if it is an instance, FALSE if not. */ gboolean e_cal_util_component_is_instance (ICalComponent *icalcomp) { g_return_val_if_fail (icalcomp != NULL, FALSE); return e_cal_util_component_has_property (icalcomp, I_CAL_RECURRENCEID_PROPERTY); } /** * e_cal_util_component_has_alarms: * @icalcomp: An #ICalComponent. * * Checks whether an #ICalComponent has any alarm. * * Returns: TRUE if it has alarms, FALSE otherwise. */ gboolean e_cal_util_component_has_alarms (ICalComponent *icalcomp) { ICalComponent *alarm; g_return_val_if_fail (icalcomp != NULL, FALSE); alarm = i_cal_component_get_first_component (icalcomp, I_CAL_VALARM_COMPONENT); if (!alarm) return FALSE; g_object_unref (alarm); return TRUE; } /** * e_cal_util_component_has_organizer: * @icalcomp: An #ICalComponent. * * Checks whether an #ICalComponent has an organizer. * * Returns: TRUE if there is an organizer, FALSE if not. */ gboolean e_cal_util_component_has_organizer (ICalComponent *icalcomp) { g_return_val_if_fail (icalcomp != NULL, FALSE); return e_cal_util_component_has_property (icalcomp, I_CAL_ORGANIZER_PROPERTY); } /** * e_cal_util_component_has_attendee: * @icalcomp: An #ICalComponent. * * Checks if an #ICalComponent has any attendees. * * Returns: TRUE if there are attendees, FALSE if not. */ gboolean e_cal_util_component_has_attendee (ICalComponent *icalcomp) { g_return_val_if_fail (icalcomp != NULL, FALSE); return e_cal_util_component_has_property (icalcomp, I_CAL_ATTENDEE_PROPERTY); } /** * e_cal_util_component_get_recurid_as_string: * @icalcomp: an #ICalComponent * * Returns: (transfer full) (nullable): a RECURRENCEID property as string, * or %NULL, when the @icalcomp is not an instance. Free the returned * string with g_free(), when no longer needed. * * Since: 3.34 **/ gchar * e_cal_util_component_get_recurid_as_string (ICalComponent *icalcomp) { ICalProperty *prop; ICalTime *recurid; gchar *rid; g_return_val_if_fail (icalcomp != NULL, NULL); prop = i_cal_component_get_first_property (icalcomp, I_CAL_RECURRENCEID_PROPERTY); if (!prop) return NULL; recurid = i_cal_property_get_recurrenceid (prop); if (!recurid || !i_cal_time_is_valid_time (recurid) || i_cal_time_is_null_time (recurid)) { rid = g_strdup ("0"); } else { rid = i_cal_time_as_ical_string (recurid); } g_clear_object (&recurid); g_object_unref (prop); return rid; } /** * e_cal_util_component_has_recurrences: * @icalcomp: An #ICalComponent. * * Checks if an #ICalComponent has recurrence dates or rules. * * Returns: TRUE if there are recurrence dates/rules, FALSE if not. */ gboolean e_cal_util_component_has_recurrences (ICalComponent *icalcomp) { g_return_val_if_fail (icalcomp != NULL, FALSE); return e_cal_util_component_has_rdates (icalcomp) || e_cal_util_component_has_rrules (icalcomp); } /** * e_cal_util_component_has_rdates: * @icalcomp: An #ICalComponent. * * Checks if an #ICalComponent has recurrence dates. * * Returns: TRUE if there are recurrence dates, FALSE if not. */ gboolean e_cal_util_component_has_rdates (ICalComponent *icalcomp) { g_return_val_if_fail (icalcomp != NULL, FALSE); return e_cal_util_component_has_property (icalcomp, I_CAL_RDATE_PROPERTY); } /** * e_cal_util_component_has_rrules: * @icalcomp: An #ICalComponent. * * Checks if an #ICalComponent has recurrence rules. * * Returns: TRUE if there are recurrence rules, FALSE if not. */ gboolean e_cal_util_component_has_rrules (ICalComponent *icalcomp) { g_return_val_if_fail (icalcomp != NULL, FALSE); return e_cal_util_component_has_property (icalcomp, I_CAL_RRULE_PROPERTY); } /* Individual instances management */ struct instance_data { time_t start; gboolean found; }; static void check_instance (ICalComponent *comp, ICalTimeSpan *span, gpointer user_data) { struct instance_data *instance = user_data; if (i_cal_time_span_get_start (span) == instance->start) instance->found = TRUE; } /** * e_cal_util_construct_instance: * @icalcomp: A recurring #ICalComponent * @rid: The RECURRENCE-ID to construct a component for * * This checks that @rid indicates a valid recurrence of @icalcomp, and * if so, generates a copy of @icalcomp containing a RECURRENCE-ID of @rid. * * Free the returned non-NULL component with g_object_unref(), when * no longer needed. * * Returns: (transfer full) (nullable): the instance as a new #ICalComponent, or %NULL. **/ ICalComponent * e_cal_util_construct_instance (ICalComponent *icalcomp, const ICalTime *rid) { struct instance_data instance; ICalTime *start, *end; g_return_val_if_fail (icalcomp != NULL, NULL); /* Make sure this is really recurring */ if (!e_cal_util_component_has_recurrences (icalcomp)) return NULL; /* Make sure the specified instance really exists */ start = i_cal_time_convert_to_zone ((ICalTime *) rid, i_cal_timezone_get_utc_timezone ()); end = i_cal_time_clone (start); i_cal_time_adjust (end, 0, 0, 0, 1); instance.start = i_cal_time_as_timet (start); instance.found = FALSE; i_cal_component_foreach_recurrence (icalcomp, start, end, check_instance, &instance); g_object_unref (start); g_object_unref (end); if (!instance.found) return NULL; /* Make the instance */ icalcomp = i_cal_component_clone (icalcomp); i_cal_component_set_recurrenceid (icalcomp, (ICalTime *) rid); return icalcomp; } static inline gboolean time_matches_rid (const ICalTime *itt, const ICalTime *rid, ECalObjModType mod) { gint compare; compare = i_cal_time_compare ((ICalTime *) itt, (ICalTime *) rid); if (compare == 0) return TRUE; else if (compare < 0 && (mod & E_CAL_OBJ_MOD_THIS_AND_PRIOR)) return TRUE; else if (compare > 0 && (mod & E_CAL_OBJ_MOD_THIS_AND_FUTURE)) return TRUE; return FALSE; } /** * e_cal_util_normalize_rrule_until_value: * @icalcomp: An #ICalComponent * @ttuntil: An UNTIL value to validate * @tz_cb: (closure tz_cb_data) (scope call): The #ECalRecurResolveTimezoneCb to call * @tz_cb_data: (closure): User data to be passed to the @tz_cb callback * * Makes sure the @ttuntil value matches the value type with * the DTSTART value, as required by RFC 5545 section 3.3.10. * Uses @tz_cb with @tz_cb_data to resolve time zones when needed. * * Since: 3.38 **/ void e_cal_util_normalize_rrule_until_value (ICalComponent *icalcomp, ICalTime *ttuntil, ECalRecurResolveTimezoneCb tz_cb, gpointer tz_cb_data) { ICalProperty *prop; g_return_if_fail (I_CAL_IS_COMPONENT (icalcomp)); g_return_if_fail (I_CAL_IS_TIME (ttuntil)); prop = i_cal_component_get_first_property (icalcomp, I_CAL_DTSTART_PROPERTY); if (prop) { ICalTime *dtstart; dtstart = i_cal_component_get_dtstart (icalcomp); if (dtstart) { if (i_cal_time_is_date (dtstart)) { i_cal_time_set_time (ttuntil, 0, 0, 0); i_cal_time_set_is_date (ttuntil, TRUE); } else { if (i_cal_time_is_date (ttuntil)) { gint hour = 0, minute = 0, second = 0; i_cal_time_set_is_date (ttuntil, FALSE); i_cal_time_get_time (dtstart, &hour, &minute, &second); i_cal_time_set_time (ttuntil, hour, minute, second); } if (!i_cal_time_is_utc (dtstart)) { ICalParameter *param; param = i_cal_property_get_first_parameter (prop, I_CAL_TZID_PARAMETER); if (param) { const gchar *tzid; tzid = i_cal_parameter_get_tzid (param); if (tzid && *tzid && g_ascii_strcasecmp (tzid, "UTC") != 0) { ICalTimezone *tz; tz = i_cal_time_get_timezone (dtstart); if (!tz && tz_cb) tz = tz_cb (tzid, tz_cb_data, NULL, NULL); if (tz) { i_cal_time_set_timezone (ttuntil, tz); i_cal_time_convert_to_zone_inplace (ttuntil, i_cal_timezone_get_utc_timezone ()); } } g_object_unref (param); } } } g_object_unref (dtstart); } g_object_unref (prop); } } static void e_cal_util_remove_instances_impl (ICalComponent *icalcomp, const ICalTime *rid, ECalObjModType mod, gboolean keep_rid, gboolean can_add_exrule, ECalRecurResolveTimezoneCb tz_cb, gpointer tz_cb_data) { ICalProperty *prop; ICalTime *itt, *recur; ICalRecurrence *rule; ICalRecurIterator *iter; GSList *remove_props = NULL, *rrules = NULL, *link; g_return_if_fail (icalcomp != NULL); g_return_if_fail (mod != E_CAL_OBJ_MOD_ALL); /* First remove RDATEs and EXDATEs in the indicated range. */ for (prop = i_cal_component_get_first_property (icalcomp, I_CAL_RDATE_PROPERTY); prop; g_object_unref (prop), prop = i_cal_component_get_next_property (icalcomp, I_CAL_RDATE_PROPERTY)) { ICalDatetimeperiod *period; ICalTime *period_time; period = i_cal_property_get_rdate (prop); if (!period) continue; period_time = i_cal_datetimeperiod_get_time (period); if (time_matches_rid (period_time, rid, mod) && (!keep_rid || i_cal_time_compare (period_time, (ICalTime *) rid) != 0)) remove_props = g_slist_prepend (remove_props, g_object_ref (prop)); g_clear_object (&period_time); g_object_unref (period); } for (prop = i_cal_component_get_first_property (icalcomp, I_CAL_EXDATE_PROPERTY); prop; g_object_unref (prop), prop = i_cal_component_get_next_property (icalcomp, I_CAL_EXDATE_PROPERTY)) { itt = i_cal_property_get_exdate (prop); if (!itt) continue; if (time_matches_rid (itt, rid, mod) && (!keep_rid || i_cal_time_compare (itt, (ICalTime *) rid) != 0)) remove_props = g_slist_prepend (remove_props, g_object_ref (prop)); g_object_unref (itt); } for (link = remove_props; link; link = g_slist_next (link)) { prop = link->data; i_cal_component_remove_property (icalcomp, prop); } g_slist_free_full (remove_props, g_object_unref); remove_props = NULL; /* If we're only removing one instance, just add an EXDATE. */ if (mod == E_CAL_OBJ_MOD_THIS) { prop = i_cal_property_new_exdate ((ICalTime *) rid); i_cal_component_take_property (icalcomp, prop); return; } /* Otherwise, iterate through RRULEs */ /* FIXME: this may generate duplicate EXRULEs */ for (prop = i_cal_component_get_first_property (icalcomp, I_CAL_RRULE_PROPERTY); prop; g_object_unref (prop), prop = i_cal_component_get_next_property (icalcomp, I_CAL_RRULE_PROPERTY)) { rrules = g_slist_prepend (rrules, g_object_ref (prop)); } for (link = rrules; link; link = g_slist_next (link)) { prop = link->data; rule = i_cal_property_get_rrule (prop); if (!rule) continue; iter = i_cal_recur_iterator_new (rule, (ICalTime *) rid); recur = i_cal_recur_iterator_next (iter); if (!recur) { g_object_unref (rule); g_object_unref (iter); continue; } if (mod & E_CAL_OBJ_MOD_THIS_AND_FUTURE) { /* Truncate the rule at rid. */ if (!i_cal_time_is_null_time (recur)) { gint rule_count = i_cal_recurrence_get_count (rule); /* Use count if it was used */ if (rule_count > 0) { gint occurrences_count = 0; ICalRecurIterator *count_iter; ICalTime *count_recur, *dtstart; dtstart = i_cal_component_get_dtstart (icalcomp); count_iter = i_cal_recur_iterator_new (rule, dtstart); while (count_recur = i_cal_recur_iterator_next (count_iter), count_recur && !i_cal_time_is_null_time (count_recur) && occurrences_count < rule_count) { if (i_cal_time_compare (count_recur, (ICalTime *) rid) >= 0) break; occurrences_count++; g_object_unref (count_recur); } if (keep_rid && count_recur && i_cal_time_compare (count_recur, (ICalTime *) rid) == 0) occurrences_count++; /* The caller should make sure that the remove will keep at least one instance */ g_warn_if_fail (occurrences_count > 0); i_cal_recurrence_set_count (rule, occurrences_count); g_clear_object (&count_recur); g_clear_object (&count_iter); g_clear_object (&dtstart); } else { ICalTime *ttuntil; gboolean is_date; if (keep_rid && i_cal_time_compare (recur, (ICalTime *) rid) == 0) { ICalDuration *dur; dur = i_cal_component_get_duration (icalcomp); ttuntil = i_cal_time_add ((ICalTime *) rid, dur); g_clear_object (&dur); } else { ttuntil = i_cal_time_clone (rid); } e_cal_util_normalize_rrule_until_value (icalcomp, ttuntil, tz_cb, tz_cb_data); is_date = i_cal_time_is_date (ttuntil); i_cal_time_adjust (ttuntil, is_date ? -1 : 0, 0, 0, is_date ? 0 : -1); i_cal_recurrence_set_until (rule, ttuntil); g_object_unref (ttuntil); } i_cal_property_set_rrule (prop, rule); i_cal_property_remove_parameter_by_name (prop, E_CAL_EVOLUTION_ENDDATE_PARAMETER); } } else { /* (If recur == rid, skip to the next occurrence) */ if (!keep_rid && i_cal_time_compare (recur, (ICalTime *) rid) == 0) { g_object_unref (recur); recur = i_cal_recur_iterator_next (iter); } /* If there is a recurrence after rid, add * an EXRULE to block instances up to rid. * Otherwise, just remove the RRULE. */ if (!i_cal_time_is_null_time (recur)) { if (can_add_exrule) { ICalTime *ttuntil; ICalDuration *dur = i_cal_component_get_duration (icalcomp); i_cal_recurrence_set_count (rule, 0); /* iCalendar says we should just use rid * here, but Outlook/Exchange handle * UNTIL incorrectly. */ if (keep_rid && i_cal_time_compare (recur, (ICalTime *) rid) == 0) { i_cal_duration_set_is_neg (dur, !i_cal_duration_is_neg (dur)); ttuntil = i_cal_time_add ((ICalTime *) rid, dur); } else { ttuntil = i_cal_time_add ((ICalTime *) rid, dur); } e_cal_util_normalize_rrule_until_value (icalcomp, ttuntil, tz_cb, tz_cb_data); i_cal_recurrence_set_until (rule, ttuntil); g_clear_object (&ttuntil); g_clear_object (&dur); prop = i_cal_property_new_exrule (rule); i_cal_component_take_property (icalcomp, prop); } } else { remove_props = g_slist_prepend (remove_props, g_object_ref (prop)); } } g_object_unref (recur); g_object_unref (rule); g_object_unref (iter); } for (link = remove_props; link; link = g_slist_next (link)) { prop = link->data; i_cal_component_remove_property (icalcomp, prop); } g_slist_free_full (remove_props, g_object_unref); g_slist_free_full (rrules, g_object_unref); } /** * e_cal_util_remove_instances: * @icalcomp: A (recurring) #ICalComponent * @rid: The base RECURRENCE-ID to remove * @mod: How to interpret @rid * * Removes one or more instances from @icalcomp according to @rid and @mod. * * Deprecated: 3.38: Use e_cal_util_remove_instances_ex() instead, with provided * timezone resolve function. **/ void e_cal_util_remove_instances (ICalComponent *icalcomp, const ICalTime *rid, ECalObjModType mod) { g_return_if_fail (icalcomp != NULL); g_return_if_fail (rid != NULL); g_return_if_fail (mod != E_CAL_OBJ_MOD_ALL); e_cal_util_remove_instances_ex (icalcomp, rid, mod, NULL, NULL); } /** * e_cal_util_remove_instances_ex: * @icalcomp: A (recurring) #ICalComponent * @rid: The base RECURRENCE-ID to remove * @mod: How to interpret @rid * @tz_cb: (closure tz_cb_data) (scope call): The #ECalRecurResolveTimezoneCb to call * @tz_cb_data: (closure): User data to be passed to the @tz_cb callback * * Removes one or more instances from @icalcomp according to @rid and @mod. * Uses @tz_cb with @tz_cb_data to resolve time zones when needed. * * Since: 3.38 **/ void e_cal_util_remove_instances_ex (ICalComponent *icalcomp, const ICalTime *rid, ECalObjModType mod, ECalRecurResolveTimezoneCb tz_cb, gpointer tz_cb_data) { g_return_if_fail (icalcomp != NULL); g_return_if_fail (rid != NULL); g_return_if_fail (mod != E_CAL_OBJ_MOD_ALL); e_cal_util_remove_instances_impl (icalcomp, rid, mod, FALSE, TRUE, tz_cb, tz_cb_data); } /** * e_cal_util_split_at_instance: * @icalcomp: A (recurring) #ICalComponent * @rid: The base RECURRENCE-ID to remove * @master_dtstart: (nullable): The DTSTART of the master object * * Splits a recurring @icalcomp into two at time @rid. The returned #ICalComponent * is modified @icalcomp which contains recurrences beginning at @rid, inclusive. * The instance identified by @rid should exist. The @master_dtstart can be * a null time, then it is read from the @icalcomp. * * Use e_cal_util_remove_instances_ex() with E_CAL_OBJ_MOD_THIS_AND_FUTURE mode * on the @icalcomp to remove the overlapping interval from it, if needed. * * Free the returned non-NULL component with g_object_unref(), when * done with it. * * Returns: (transfer full) (nullable): the split @icalcomp, or %NULL. * * Since: 3.16 * * Deprecated: 3.38: Use e_cal_util_split_at_instance_ex() instead, with provided * timezone resolve function. **/ ICalComponent * e_cal_util_split_at_instance (ICalComponent *icalcomp, const ICalTime *rid, const ICalTime *master_dtstart) { return e_cal_util_split_at_instance_ex (icalcomp, rid, master_dtstart, NULL, NULL); } /** * e_cal_util_split_at_instance_ex: * @icalcomp: A (recurring) #ICalComponent * @rid: The base RECURRENCE-ID to remove * @master_dtstart: (nullable): The DTSTART of the master object * @tz_cb: (closure tz_cb_data) (scope call): The #ECalRecurResolveTimezoneCb to call * @tz_cb_data: (closure): User data to be passed to the @tz_cb callback * * Splits a recurring @icalcomp into two at time @rid. The returned #ICalComponent * is modified @icalcomp which contains recurrences beginning at @rid, inclusive. * The instance identified by @rid should exist. The @master_dtstart can be * a null time, then it is read from the @icalcomp. * * Uses @tz_cb with @tz_cb_data to resolve time zones when needed. * * Use e_cal_util_remove_instances_ex() with E_CAL_OBJ_MOD_THIS_AND_FUTURE mode * on the @icalcomp to remove the overlapping interval from it, if needed. * * Free the returned non-NULL component with g_object_unref(), when * done with it. * * Returns: (transfer full) (nullable): the split @icalcomp, or %NULL. * * Since: 3.38 **/ ICalComponent * e_cal_util_split_at_instance_ex (ICalComponent *icalcomp, const ICalTime *rid, const ICalTime *master_dtstart, ECalRecurResolveTimezoneCb tz_cb, gpointer tz_cb_data) { ICalProperty *prop; struct instance_data instance; ICalTime *start, *end, *dtstart = NULL; ICalDuration *duration; GSList *remove_props = NULL, *link; g_return_val_if_fail (icalcomp != NULL, NULL); g_return_val_if_fail (rid != NULL, NULL); g_return_val_if_fail (!i_cal_time_is_null_time ((ICalTime *) rid), NULL); /* Make sure this is really recurring */ if (!e_cal_util_component_has_recurrences (icalcomp)) return NULL; /* Make sure the specified instance really exists */ start = i_cal_time_convert_to_zone ((ICalTime *) rid, i_cal_timezone_get_utc_timezone ()); end = i_cal_time_clone (start); i_cal_time_adjust (end, 0, 0, 0, 1); instance.start = i_cal_time_as_timet (start); instance.found = FALSE; i_cal_component_foreach_recurrence (icalcomp, start, end, check_instance, &instance); g_clear_object (&start); g_clear_object (&end); /* Make the copy */ icalcomp = i_cal_component_clone (icalcomp); e_cal_util_remove_instances_impl (icalcomp, rid, E_CAL_OBJ_MOD_THIS_AND_PRIOR, TRUE, FALSE, tz_cb, tz_cb_data); start = i_cal_time_clone ((ICalTime *) rid); if (!master_dtstart || i_cal_time_is_null_time ((ICalTime *) master_dtstart)) { dtstart = i_cal_component_get_dtstart (icalcomp); master_dtstart = dtstart; } duration = i_cal_component_get_duration (icalcomp); /* Expect that DTSTART and DTEND are already set when the instance could not be found */ if (instance.found) { ICalTime *dtend; dtend = i_cal_component_get_dtend (icalcomp); i_cal_component_set_dtstart (icalcomp, start); /* Update either DURATION or DTEND */ if (i_cal_time_is_null_time (dtend)) { i_cal_component_set_duration (icalcomp, duration); } else { end = i_cal_time_clone (start); if (i_cal_duration_is_neg (duration)) i_cal_time_adjust (end, -i_cal_duration_get_days (duration) - 7 * i_cal_duration_get_weeks (duration), -i_cal_duration_get_hours (duration), -i_cal_duration_get_minutes (duration), -i_cal_duration_get_seconds (duration)); else i_cal_time_adjust (end, i_cal_duration_get_days (duration) + 7 * i_cal_duration_get_weeks (duration), i_cal_duration_get_hours (duration), i_cal_duration_get_minutes (duration), i_cal_duration_get_seconds (duration)); i_cal_component_set_dtend (icalcomp, end); } g_clear_object (&dtend); } g_clear_object (&start); g_clear_object (&end); g_clear_object (&duration); /* any RRULE with 'count' should be shortened */ for (prop = i_cal_component_get_first_property (icalcomp, I_CAL_RRULE_PROPERTY); prop; g_object_unref (prop), prop = i_cal_component_get_next_property (icalcomp, I_CAL_RRULE_PROPERTY)) { ICalTime *recur; ICalRecurrence *rule; gint rule_count; rule = i_cal_property_get_rrule (prop); if (!rule) continue; rule_count = i_cal_recurrence_get_count (rule); if (rule_count != 0) { gint occurrences_count = 0; ICalRecurIterator *iter; iter = i_cal_recur_iterator_new (rule, (ICalTime *) master_dtstart); while (recur = i_cal_recur_iterator_next (iter), recur && !i_cal_time_is_null_time (recur) && occurrences_count < rule_count) { if (i_cal_time_compare (recur, (ICalTime *) rid) >= 0) break; occurrences_count++; g_object_unref (recur); } if (!recur || i_cal_time_is_null_time (recur)) { remove_props = g_slist_prepend (remove_props, g_object_ref (prop)); } else { i_cal_recurrence_set_count (rule, rule_count - occurrences_count); i_cal_property_set_rrule (prop, rule); i_cal_property_remove_parameter_by_name (prop, E_CAL_EVOLUTION_ENDDATE_PARAMETER); } g_clear_object (&iter); g_clear_object (&recur); } g_object_unref (rule); } for (link = remove_props; link; link = g_slist_next (link)) { prop = link->data; i_cal_component_remove_property (icalcomp, prop); } g_slist_free_full (remove_props, g_object_unref); g_clear_object (&dtstart); return icalcomp; } typedef struct { const ICalTime *rid; gboolean matches; } CheckFirstInstanceData; static gboolean check_first_instance_cb (ICalComponent *icalcomp, ICalTime *instance_start, ICalTime *instance_end, gpointer user_data, GCancellable *cancellable, GError **error) { CheckFirstInstanceData *ifs = user_data; ICalProperty *prop; ICalTime *rid; g_return_val_if_fail (ifs != NULL, FALSE); prop = i_cal_component_get_first_property (icalcomp, I_CAL_RECURRENCEID_PROPERTY); if (prop) { rid = i_cal_property_get_recurrenceid (prop); g_object_unref (prop); } else { ICalTime *dtstart; ICalTimezone *zone; dtstart = i_cal_component_get_dtstart (icalcomp); zone = i_cal_time_get_timezone (dtstart); rid = i_cal_time_new_from_timet_with_zone (i_cal_time_as_timet (instance_start), i_cal_time_is_date (dtstart), zone); g_clear_object (&dtstart); } ifs->matches = i_cal_time_compare ((ICalTime *) ifs->rid, rid) == 0; g_clear_object (&rid); return FALSE; } /** * e_cal_util_is_first_instance: * @comp: an #ECalComponent instance * @rid: a recurrence ID * @tz_cb: (closure tz_cb_data) (scope call): The #ECalRecurResolveTimezoneCb to call * @tz_cb_data: (closure): User data to be passed to the @tz_cb callback * * Returns whether the given @rid is the first instance of * the recurrence defined in the @comp. * * Return: Whether the @rid identifies the first instance of @comp. * * Since: 3.16 **/ gboolean e_cal_util_is_first_instance (ECalComponent *comp, const ICalTime *rid, ECalRecurResolveTimezoneCb tz_cb, gpointer tz_cb_data) { CheckFirstInstanceData ifs; ICalComponent *icalcomp; ICalTime *start, *end; g_return_val_if_fail (E_IS_CAL_COMPONENT (comp), FALSE); g_return_val_if_fail (rid && !i_cal_time_is_null_time ((ICalTime *) rid), FALSE); ifs.rid = rid; ifs.matches = FALSE; icalcomp = e_cal_component_get_icalcomponent (comp); start = i_cal_component_get_dtstart (icalcomp); i_cal_time_adjust (start, -1, 0, 0, 0); end = i_cal_component_get_dtend (icalcomp); i_cal_time_adjust (end, +1, 0, 0, 0); e_cal_recur_generate_instances_sync (e_cal_component_get_icalcomponent (comp), start, end, check_first_instance_cb, &ifs, tz_cb, tz_cb_data, i_cal_timezone_get_utc_timezone (), NULL, NULL); g_clear_object (&start); g_clear_object (&end); return ifs.matches; } /** * e_cal_util_get_system_timezone_location: * * Fetches system timezone localtion string. * * Returns: (transfer full) (nullable): system timezone location string, %NULL * on an error. * * Since: 2.28 **/ gchar * e_cal_util_get_system_timezone_location (void) { return e_cal_system_timezone_get_location (); } /** * e_cal_util_get_system_timezone: * * Fetches system timezone ICalTimezone object. * * The returned pointer is part of the built-in timezones and should not be freed. * * Returns: (transfer none) (nullable): The ICalTimezone object of the system timezone, or %NULL on an error. * * Since: 2.28 **/ ICalTimezone * e_cal_util_get_system_timezone (void) { gchar *location; ICalTimezone *zone; location = e_cal_system_timezone_get_location (); /* Can be NULL when failed to detect system time zone */ if (!location) return NULL; zone = i_cal_timezone_get_builtin_timezone (location); g_free (location); return zone; } static time_t componenttime_to_utc_timet (const ECalComponentDateTime *dt_time, ECalRecurResolveTimezoneCb tz_cb, gpointer tz_cb_data, const ICalTimezone *default_zone) { ICalTime *value = NULL; time_t timet = -1; if (dt_time) value = e_cal_component_datetime_get_value (dt_time); if (value) { ICalTimezone *zone = NULL; const gchar *tzid = e_cal_component_datetime_get_tzid (dt_time); if (tzid) zone = tz_cb (tzid, tz_cb_data, NULL, NULL); timet = i_cal_time_as_timet_with_zone (value, zone ? zone : (ICalTimezone *) default_zone); } return timet; } /** * e_cal_util_get_component_occur_times: * @comp: an #ECalComponent * @out_start: (out): Location to store the start time * @out_end: (out): Location to store the end time * @tz_cb: (closure tz_cb_data) (scope call): The #ECalRecurResolveTimezoneCb to call * @tz_cb_data: (closure): User data to be passed to the @tz_cb callback * @default_timezone: The default timezone * @kind: the type of component, indicated with an #ICalComponentKind * * Find out when the component starts and stops, being careful about * recurrences. * * Since: 2.32 **/ void e_cal_util_get_component_occur_times (ECalComponent *comp, time_t *out_start, time_t *out_end, ECalRecurResolveTimezoneCb tz_cb, gpointer tz_cb_data, const ICalTimezone *default_timezone, ICalComponentKind kind) { ICalTimezone *utc_zone; ECalComponentDateTime *dtstart, *dtend; time_t duration; g_return_if_fail (comp != NULL); g_return_if_fail (out_start != NULL); g_return_if_fail (out_end != NULL); utc_zone = i_cal_timezone_get_utc_timezone (); e_cal_recur_ensure_end_dates (comp, FALSE, tz_cb, tz_cb_data, NULL, NULL); /* Get dtstart of the component and convert it to UTC */ dtstart = e_cal_component_get_dtstart (comp); if ((*out_start = componenttime_to_utc_timet (dtstart, tz_cb, tz_cb_data, default_timezone)) == -1) *out_start = _TIME_MIN; e_cal_component_datetime_free (dtstart); dtend = e_cal_component_get_dtend (comp); duration = componenttime_to_utc_timet (dtend, tz_cb, tz_cb_data, default_timezone); if (duration <= 0 || *out_start == _TIME_MIN || *out_start > duration) duration = 0; else duration = duration - *out_start; e_cal_component_datetime_free (dtend); /* find out end date of component */ *out_end = _TIME_MAX; if (kind == I_CAL_VTODO_COMPONENT) { /* max from COMPLETED and DUE properties */ ICalTime *tt; time_t completed_time = -1, due_time = -1, max_time; ECalComponentDateTime *dtdue; tt = e_cal_component_get_completed (comp); if (tt) { /* COMPLETED must be in UTC. */ completed_time = i_cal_time_as_timet_with_zone (tt, utc_zone); g_object_unref (tt); } dtdue = e_cal_component_get_due (comp); if (dtdue) due_time = componenttime_to_utc_timet (dtdue, tz_cb, tz_cb_data, default_timezone); e_cal_component_datetime_free (dtdue); max_time = MAX (completed_time, due_time); if (max_time != -1) *out_end = max_time; } else { /* ALARMS, EVENTS: DTEND and recurrences */ time_t may_end = _TIME_MIN; if (e_cal_component_has_recurrences (comp)) { GSList *rrules = NULL; GSList *exrules = NULL; GSList *rdates = NULL; GSList *elem; /* Do the RRULEs, EXRULEs and RDATEs*/ rrules = e_cal_component_get_rrule_properties (comp); exrules = e_cal_component_get_exrule_properties (comp); rdates = e_cal_component_get_rdates (comp); for (elem = rrules; elem; elem = g_slist_next (elem)) { ICalProperty *prop = elem->data; ICalRecurrence *ir; time_t rule_end; ir = i_cal_property_get_rrule (prop); rule_end = e_cal_recur_obtain_enddate (ir, prop, utc_zone, TRUE); if (rule_end == -1) /* repeats forever */ may_end = _TIME_MAX; else if (rule_end + duration > may_end) /* new maximum */ may_end = rule_end + duration; g_clear_object (&ir); } /* Do the EXRULEs. */ for (elem = exrules; elem; elem = g_slist_next (elem)) { ICalProperty *prop = elem->data; ICalRecurrence *ir; time_t rule_end; ir = i_cal_property_get_exrule (prop); rule_end = e_cal_recur_obtain_enddate (ir, prop, utc_zone, TRUE); if (rule_end == -1) /* repeats forever */ may_end = _TIME_MAX; else if (rule_end + duration > may_end) may_end = rule_end + duration; g_clear_object (&ir); } /* Do the RDATEs */ for (elem = rdates; elem; elem = g_slist_next (elem)) { const ECalComponentPeriod *period = elem->data; time_t rdate_end = _TIME_MAX; /* FIXME: We currently assume RDATEs are in the same timezone * as DTSTART. We should get the RDATE timezone and convert * to the DTSTART timezone first. */ if (e_cal_component_period_get_kind (period) != E_CAL_COMPONENT_PERIOD_DATETIME) { ICalTime *tt; tt = i_cal_time_add (e_cal_component_period_get_start (period), e_cal_component_period_get_duration (period)); rdate_end = i_cal_time_as_timet (tt); g_object_unref (tt); } else if (e_cal_component_period_get_end (period)) { rdate_end = i_cal_time_as_timet (e_cal_component_period_get_end (period)); } else { rdate_end = (time_t) -1; } if (rdate_end == -1) /* repeats forever */ may_end = _TIME_MAX; else if (rdate_end > may_end) may_end = rdate_end; } g_slist_free_full (rrules, g_object_unref); g_slist_free_full (exrules, g_object_unref); g_slist_free_full (rdates, e_cal_component_period_free); } else if (*out_start != _TIME_MIN) { may_end = *out_start; } /* Get dtend of the component and convert it to UTC */ dtend = e_cal_component_get_dtend (comp); if (dtend) { time_t dtend_time; dtend_time = componenttime_to_utc_timet (dtend, tz_cb, tz_cb_data, default_timezone); if (dtend_time == -1 || (dtend_time > may_end)) may_end = dtend_time; } else { may_end = _TIME_MAX; } e_cal_component_datetime_free (dtend); *out_end = may_end == _TIME_MIN ? _TIME_MAX : may_end; } } /** * e_cal_util_component_has_x_property: * @icalcomp: an #ICalComponent * @x_name: name of the X property * * Returns, whether the @icalcomp contains X property named @x_name. To check * for standard property use e_cal_util_component_has_property(). * * Returns: whether the @icalcomp contains X property named @x_name * * Since: 3.34 **/ gboolean e_cal_util_component_has_x_property (ICalComponent *icalcomp, const gchar *x_name) { ICalProperty *prop; g_return_val_if_fail (I_CAL_IS_COMPONENT (icalcomp), FALSE); g_return_val_if_fail (x_name != NULL, FALSE); prop = e_cal_util_component_find_x_property (icalcomp, x_name); if (!prop) return FALSE; g_object_unref (prop); return TRUE; } /** * e_cal_util_component_find_x_property: * @icalcomp: an #ICalComponent * @x_name: name of the X property * * Searches for an X property named @x_name within X properties * of @icalcomp and returns it. Free the non-NULL object * with g_object_unref(), when no longer needed. * * Returns: (transfer full) (nullable): the first X ICalProperty named * @x_name, or %NULL, when none found. * * Since: 3.34 **/ ICalProperty * e_cal_util_component_find_x_property (ICalComponent *icalcomp, const gchar *x_name) { ICalProperty *prop; g_return_val_if_fail (icalcomp != NULL, NULL); g_return_val_if_fail (x_name != NULL, NULL); for (prop = i_cal_component_get_first_property (icalcomp, I_CAL_X_PROPERTY); prop; g_object_unref (prop), prop = i_cal_component_get_next_property (icalcomp, I_CAL_X_PROPERTY)) { const gchar *prop_name = i_cal_property_get_x_name (prop); if (g_strcmp0 (prop_name, x_name) == 0) break; } return prop; } /** * e_cal_util_component_dup_x_property: * @icalcomp: an #ICalComponent * @x_name: name of the X property * * Searches for an X property named @x_name within X properties * of @icalcomp and returns its value as a newly allocated string. * Free it with g_free(), when no longer needed. * * Returns: (nullable) (transfer full): Newly allocated value of the first @x_name * X property in @icalcomp, or %NULL, if not found. * * Since: 3.34 **/ gchar * e_cal_util_component_dup_x_property (ICalComponent *icalcomp, const gchar *x_name) { ICalProperty *prop; gchar *x_value; g_return_val_if_fail (icalcomp != NULL, NULL); g_return_val_if_fail (x_name != NULL, NULL); prop = e_cal_util_component_find_x_property (icalcomp, x_name); if (!prop) return NULL; x_value = i_cal_property_get_value_as_string (prop); g_object_unref (prop); return x_value; } /** * e_cal_util_component_set_x_property: * @icalcomp: an #ICalComponent * @x_name: name of the X property * @value: (nullable): a value to set, or %NULL * * Sets a value of the first X property named @x_name in @icalcomp, * if any such already exists, or adds a new property with this name * and value. As a special case, if @value is %NULL, then removes * the first X property named @x_name from @icalcomp instead. * * Since: 3.34 **/ void e_cal_util_component_set_x_property (ICalComponent *icalcomp, const gchar *x_name, const gchar *value) { ICalProperty *prop; g_return_if_fail (icalcomp != NULL); g_return_if_fail (x_name != NULL); if (!value) { e_cal_util_component_remove_x_property (icalcomp, x_name); return; } prop = e_cal_util_component_find_x_property (icalcomp, x_name); if (prop) { i_cal_property_set_value_from_string (prop, value, "NO"); g_object_unref (prop); } else { prop = i_cal_property_new_x (value); i_cal_property_set_x_name (prop, x_name); i_cal_component_take_property (icalcomp, prop); } } /** * e_cal_util_component_remove_x_property: * @icalcomp: an #ICalComponent * @x_name: name of the X property * * Removes the first X property named @x_name in @icalcomp. * * Returns: %TRUE, when any such had been found and removed, %FALSE otherwise. * * Since: 3.34 **/ gboolean e_cal_util_component_remove_x_property (ICalComponent *icalcomp, const gchar *x_name) { ICalProperty *prop; g_return_val_if_fail (icalcomp != NULL, FALSE); g_return_val_if_fail (x_name != NULL, FALSE); prop = e_cal_util_component_find_x_property (icalcomp, x_name); if (!prop) return FALSE; i_cal_component_remove_property (icalcomp, prop); g_object_unref (prop); return TRUE; } /** * e_cal_util_component_remove_property_by_kind: * @icalcomp: an #ICalComponent * @kind: the kind of the property to remove * @all: %TRUE to remove all, or %FALSE to remove only the first property of the @kind * * Removes all or only the first property of kind @kind in @icalcomp. * * Returns: How many properties had been removed. * * Since: 3.30 **/ guint e_cal_util_component_remove_property_by_kind (ICalComponent *icalcomp, ICalPropertyKind kind, gboolean all) { ICalProperty *prop; guint count = 0; g_return_val_if_fail (icalcomp != NULL, 0); while (prop = i_cal_component_get_first_property (icalcomp, kind), prop) { i_cal_component_remove_property (icalcomp, prop); g_object_unref (prop); count++; if (!all) break; } return count; } typedef struct _NextOccurrenceData { ICalTime *interval_start; ICalTime *next; gboolean found_next; gboolean any_hit; } NextOccurrenceData; static gboolean ecu_find_next_occurrence_cb (ICalComponent *comp, ICalTime *instance_start, ICalTime *instance_end, gpointer user_data, GCancellable *cancellable, GError **error) { NextOccurrenceData *nod = user_data; g_return_val_if_fail (nod != NULL, FALSE); nod->any_hit = TRUE; if (i_cal_time_compare (nod->interval_start, instance_start) < 0) { g_clear_object (&nod->next); nod->next = g_object_ref (instance_start); nod->found_next = TRUE; return FALSE; } return TRUE; } /* the returned FALSE means failure in timezone resolution, not in @out_time */ static gboolean e_cal_util_find_next_occurrence (ICalComponent *vtodo, ICalTime *for_time, ICalTime **out_time, /* set to NULL on failure */ ECalClient *cal_client, GCancellable *cancellable, GError **error) { NextOccurrenceData nod; ICalTime *interval_start, *interval_end = NULL, *orig_dtstart, *orig_due; gint advance_days = 8; ICalProperty *prop; gboolean success; GError *local_error = NULL; g_return_val_if_fail (vtodo != NULL, FALSE); g_return_val_if_fail (out_time != NULL, FALSE); g_return_val_if_fail (E_IS_CAL_CLIENT (cal_client), FALSE); orig_dtstart = i_cal_component_get_dtstart (vtodo); orig_due = i_cal_component_get_due (vtodo); e_cal_util_component_remove_property_by_kind (vtodo, I_CAL_DUE_PROPERTY, TRUE); if (for_time && !i_cal_time_is_null_time (for_time) && i_cal_time_is_valid_time (for_time)) { i_cal_component_set_dtstart (vtodo, for_time); } interval_start = i_cal_component_get_dtstart (vtodo); if (!interval_start || i_cal_time_is_null_time (interval_start) || !i_cal_time_is_valid_time (interval_start)) { g_clear_object (&interval_start); interval_start = i_cal_time_new_current_with_zone (e_cal_client_get_default_timezone (cal_client)); } prop = i_cal_component_get_first_property (vtodo, I_CAL_RRULE_PROPERTY); if (prop) { ICalRecurrence *rrule; rrule = i_cal_property_get_rrule (prop); if (rrule) { if (i_cal_recurrence_get_freq (rrule) == I_CAL_WEEKLY_RECURRENCE && i_cal_recurrence_get_interval (rrule) > 1) advance_days = (i_cal_recurrence_get_interval (rrule) * 7) + 1; else if (i_cal_recurrence_get_freq (rrule) == I_CAL_MONTHLY_RECURRENCE) advance_days = (i_cal_recurrence_get_interval (rrule) >= 1 ? i_cal_recurrence_get_interval (rrule) * 31 : 31) + 1; else if (i_cal_recurrence_get_freq (rrule) == I_CAL_YEARLY_RECURRENCE) advance_days = (i_cal_recurrence_get_interval (rrule) >= 1 ? i_cal_recurrence_get_interval (rrule) * 365 : 365) + 2; } g_clear_object (&rrule); g_clear_object (&prop); } nod.next = NULL; do { interval_end = i_cal_time_clone (interval_start); i_cal_time_adjust (interval_end, advance_days, 0, 0, 0); g_clear_object (&(nod.next)); nod.interval_start = interval_start; nod.next = i_cal_time_new_null_time (); nod.found_next = FALSE; nod.any_hit = FALSE; success = e_cal_recur_generate_instances_sync (vtodo, interval_start, interval_end, ecu_find_next_occurrence_cb, &nod, e_cal_client_tzlookup_cb, cal_client, e_cal_client_get_default_timezone (cal_client), cancellable, &local_error) || nod.found_next; g_object_unref (interval_start); interval_start = interval_end; interval_end = NULL; i_cal_time_adjust (interval_start, -1, 0, 0, 0); } while (!local_error && !g_cancellable_is_cancelled (cancellable) && !nod.found_next && nod.any_hit); if (success) *out_time = (nod.next && !i_cal_time_is_null_time (nod.next)) ? g_object_ref (nod.next) : NULL; if (local_error) g_propagate_error (error, local_error); if (for_time && !i_cal_time_is_null_time (for_time) && i_cal_time_is_valid_time (for_time)) { if (!orig_dtstart || i_cal_time_is_null_time (orig_dtstart) || !i_cal_time_is_valid_time (orig_dtstart)) e_cal_util_component_remove_property_by_kind (vtodo, I_CAL_DTSTART_PROPERTY, FALSE); else i_cal_component_set_dtstart (vtodo, orig_dtstart); } if (!orig_due || i_cal_time_is_null_time (orig_due) || !i_cal_time_is_valid_time (orig_due)) e_cal_util_component_remove_property_by_kind (vtodo, I_CAL_DUE_PROPERTY, FALSE); else i_cal_component_set_due (vtodo, orig_due); g_clear_object (&interval_start); g_clear_object (&interval_end); g_clear_object (&orig_dtstart); g_clear_object (&orig_due); g_clear_object (&(nod.next)); return success; } /** * e_cal_util_init_recur_task_sync: * @vtodo: a VTODO component * @cal_client: (type ECalClient): an #ECalClient to which the @vtodo belongs * @cancellable: optional #GCancellable object, or %NULL * @error: return location for a #GError, or %NULL * * Initializes properties of a recurring @vtodo, like normalizing * the Due date and eventually the Start date. The function does * nothing when the @vtodo is not recurring. * * The function doesn't change LAST-MODIFIED neither the SEQUENCE * property, it's up to the caller to do it. * * Note the @cal_client, @cancellable and @error is used only * for timezone resolution. The function doesn't store the @vtodo * to the @cal_client, it only updates the @vtodo component. * * Returns: Whether succeeded. * * Since: 3.30 **/ gboolean e_cal_util_init_recur_task_sync (ICalComponent *vtodo, ECalClient *cal_client, GCancellable *cancellable, GError **error) { ICalTime *dtstart, *due; gboolean success = TRUE; g_return_val_if_fail (vtodo != NULL, FALSE); g_return_val_if_fail (i_cal_component_isa (vtodo) == I_CAL_VTODO_COMPONENT, FALSE); g_return_val_if_fail (E_IS_CAL_CLIENT (cal_client), FALSE); if (!e_cal_util_component_has_recurrences (vtodo)) return TRUE; /* DTSTART is required for recurring components */ dtstart = i_cal_component_get_dtstart (vtodo); if (!dtstart || i_cal_time_is_null_time (dtstart) || !i_cal_time_is_valid_time (dtstart)) { g_clear_object (&dtstart); dtstart = i_cal_time_new_current_with_zone (e_cal_client_get_default_timezone (cal_client)); i_cal_component_set_dtstart (vtodo, dtstart); } due = i_cal_component_get_due (vtodo); if (!due || i_cal_time_is_null_time (due) || !i_cal_time_is_valid_time (due) || i_cal_time_compare (dtstart, due) < 0) { g_clear_object (&due); success = e_cal_util_find_next_occurrence (vtodo, NULL, &due, cal_client, cancellable, error); if (due && !i_cal_time_is_null_time (due) && i_cal_time_is_valid_time (due)) i_cal_component_set_due (vtodo, due); } g_clear_object (&dtstart); g_clear_object (&due); return success; } /** * e_cal_util_mark_task_complete_sync: * @vtodo: a VTODO component * @completed_time: completed time to set, or (time_t) -1 to use current time * @cal_client: (type ECalClient): an #ECalClient to which the @vtodo belongs * @cancellable: optional #GCancellable object, or %NULL * @error: return location for a #GError, or %NULL * * Marks the @vtodo as complete with eventual update of other * properties. This is useful also for recurring tasks, for which * it moves the @vtodo into the next occurrence according to * the recurrence rule. * * When the @vtodo is marked as completed, then the existing COMPLETED * date-time is preserved if exists, otherwise it's set either to @completed_time, * or to the current time, when the @completed_time is (time_t) -1. * * The function doesn't change LAST-MODIFIED neither the SEQUENCE * property, it's up to the caller to do it. * * Note the @cal_client, @cancellable and @error is used only * for timezone resolution. The function doesn't store the @vtodo * to the @cal_client, it only updates the @vtodo component. * * Returns: Whether succeeded. * * Since: 3.30 **/ gboolean e_cal_util_mark_task_complete_sync (ICalComponent *vtodo, time_t completed_time, ECalClient *cal_client, GCancellable *cancellable, GError **error) { ICalProperty *prop; g_return_val_if_fail (vtodo != NULL, FALSE); g_return_val_if_fail (i_cal_component_isa (vtodo) == I_CAL_VTODO_COMPONENT, FALSE); g_return_val_if_fail (E_IS_CAL_CLIENT (cal_client), FALSE); if (e_cal_util_component_has_recurrences (vtodo) && !e_client_check_capability (E_CLIENT (cal_client), E_CAL_STATIC_CAPABILITY_TASK_HANDLE_RECUR)) { gboolean is_last = FALSE, change_count = FALSE; ICalTime *new_dtstart = NULL, *new_due = NULL; for (prop = i_cal_component_get_first_property (vtodo, I_CAL_RRULE_PROPERTY); prop && !is_last; g_object_unref (prop), prop = i_cal_component_get_next_property (vtodo, I_CAL_RRULE_PROPERTY)) { ICalRecurrence *rrule; rrule = i_cal_property_get_rrule (prop); if (rrule && i_cal_recurrence_get_interval (rrule) > 0) { gint count = i_cal_recurrence_get_count (rrule); if (count > 0) { is_last = count == 1; change_count = TRUE; } } g_clear_object (&rrule); } g_clear_object (&prop); if (!is_last) { if (!e_cal_util_find_next_occurrence (vtodo, NULL, &new_dtstart, cal_client, cancellable, error)) { g_clear_object (&new_dtstart); return FALSE; } if (new_dtstart && !i_cal_time_is_null_time (new_dtstart) && i_cal_time_is_valid_time (new_dtstart)) { ICalTime *old_dtstart, *old_due; old_dtstart = i_cal_component_get_dtstart (vtodo); old_due = i_cal_component_get_due (vtodo); /* Move relatively also the DUE date, to keep the difference... */ if (old_due && !i_cal_time_is_null_time (old_due) && i_cal_time_is_valid_time (old_due)) { if (old_dtstart && !i_cal_time_is_null_time (old_dtstart) && i_cal_time_is_valid_time (old_dtstart)) { gint64 diff; diff = i_cal_time_as_timet (old_due) - i_cal_time_as_timet (old_dtstart); new_due = i_cal_time_clone (new_dtstart); i_cal_time_adjust (new_due, diff / (24 * 60 * 60), (diff / (60 * 60)) % 24, (diff / 60) % 60, diff % 60); } else if (!e_cal_util_find_next_occurrence (vtodo, old_due, &new_due, cal_client, cancellable, error)) { g_clear_object (&new_dtstart); g_clear_object (&new_due); g_clear_object (&old_dtstart); g_clear_object (&old_due); return FALSE; } } g_clear_object (&old_dtstart); /* ... otherwise set the new DUE as the next-next-DTSTART ... */ if (!new_due || i_cal_time_is_null_time (new_due) || !i_cal_time_is_valid_time (new_due)) { g_clear_object (&new_due); if (!e_cal_util_find_next_occurrence (vtodo, new_dtstart, &new_due, cal_client, cancellable, error)) { g_clear_object (&new_dtstart); g_clear_object (&new_due); g_clear_object (&old_due); return FALSE; } } /* ... eventually fallback to the new DTSTART for the new DUE */ if (!new_due || i_cal_time_is_null_time (new_due) || !i_cal_time_is_valid_time (new_due)) { g_clear_object (&new_due); new_due = i_cal_time_clone (new_dtstart); } g_clear_object (&old_due); } } if (!is_last && new_dtstart && new_due && !i_cal_time_is_null_time (new_dtstart) && i_cal_time_is_valid_time (new_dtstart) && !i_cal_time_is_null_time (new_due) && i_cal_time_is_valid_time (new_due)) { /* Move to the next occurrence */ if (change_count) { for (prop = i_cal_component_get_first_property (vtodo, I_CAL_RRULE_PROPERTY); prop; g_object_unref (prop), prop = i_cal_component_get_next_property (vtodo, I_CAL_RRULE_PROPERTY)) { ICalRecurrence *rrule; rrule = i_cal_property_get_rrule (prop); if (rrule && i_cal_recurrence_get_interval (rrule) > 0) { gint count = i_cal_recurrence_get_count (rrule); if (count > 0) { i_cal_recurrence_set_count (rrule, count - 1); i_cal_property_set_rrule (prop, rrule); } } g_clear_object (&rrule); } } i_cal_component_set_dtstart (vtodo, new_dtstart); i_cal_component_set_due (vtodo, new_due); e_cal_util_component_remove_property_by_kind (vtodo, I_CAL_COMPLETED_PROPERTY, TRUE); prop = i_cal_component_get_first_property (vtodo, I_CAL_PERCENTCOMPLETE_PROPERTY); if (prop) { i_cal_property_set_percentcomplete (prop, 0); g_object_unref (prop); } prop = i_cal_component_get_first_property (vtodo, I_CAL_STATUS_PROPERTY); if (prop) { i_cal_property_set_status (prop, I_CAL_STATUS_NEEDSACTION); g_object_unref (prop); } g_clear_object (&new_dtstart); g_clear_object (&new_due); return TRUE; } } prop = i_cal_component_get_first_property (vtodo, I_CAL_COMPLETED_PROPERTY); if (prop) { g_object_unref (prop); } else { ICalTime *tt; tt = completed_time != (time_t) -1 ? i_cal_time_new_from_timet_with_zone (completed_time, FALSE, i_cal_timezone_get_utc_timezone ()) : i_cal_time_new_current_with_zone (i_cal_timezone_get_utc_timezone ()); prop = i_cal_property_new_completed (tt); i_cal_component_take_property (vtodo, prop); g_object_unref (tt); } prop = i_cal_component_get_first_property (vtodo, I_CAL_PERCENTCOMPLETE_PROPERTY); if (prop) { i_cal_property_set_percentcomplete (prop, 100); g_object_unref (prop); } else { prop = i_cal_property_new_percentcomplete (100); i_cal_component_take_property (vtodo, prop); } prop = i_cal_component_get_first_property (vtodo, I_CAL_STATUS_PROPERTY); if (prop) { i_cal_property_set_status (prop, I_CAL_STATUS_COMPLETED); g_object_unref (prop); } else { prop = i_cal_property_new_status (I_CAL_STATUS_COMPLETED); i_cal_component_take_property (vtodo, prop); } return TRUE; } /** * e_cal_util_operation_flags_to_conflict_resolution: * @flags: bit-or of #ECalOperationFlags * * Decodes the #EConflictResolution from the bit-or of #ECalOperationFlags. * * Returns: an #EConflictResolution as stored in the @flags * * Since: 3.34 **/ EConflictResolution e_cal_util_operation_flags_to_conflict_resolution (guint32 flags) { if ((flags & E_CAL_OPERATION_FLAG_CONFLICT_FAIL) != 0) return E_CONFLICT_RESOLUTION_FAIL; else if ((flags & E_CAL_OPERATION_FLAG_CONFLICT_USE_NEWER) != 0) return E_CONFLICT_RESOLUTION_USE_NEWER; else if ((flags & E_CAL_OPERATION_FLAG_CONFLICT_KEEP_SERVER) != 0) return E_CONFLICT_RESOLUTION_KEEP_SERVER; else if ((flags & E_CAL_OPERATION_FLAG_CONFLICT_WRITE_COPY) != 0) return E_CONFLICT_RESOLUTION_WRITE_COPY; /* E_CAL_OPERATION_FLAG_CONFLICT_KEEP_LOCAL is the default */ return E_CONFLICT_RESOLUTION_KEEP_LOCAL; } /** * e_cal_util_conflict_resolution_to_operation_flags: * @conflict_resolution: an #EConflictResolution * * Encodes the #EConflictResolution into the bit-or of #ECalOperationFlags. * The returned value can be bit-or-ed with other #ECalOperationFlags values. * * Returns: a bit-or of #ECalOperationFlags, corresponding to the @conflict_resolution * * Since: 3.34 **/ guint32 e_cal_util_conflict_resolution_to_operation_flags (EConflictResolution conflict_resolution) { switch (conflict_resolution) { case E_CONFLICT_RESOLUTION_FAIL: return E_CAL_OPERATION_FLAG_CONFLICT_FAIL; case E_CONFLICT_RESOLUTION_USE_NEWER: return E_CAL_OPERATION_FLAG_CONFLICT_USE_NEWER; case E_CONFLICT_RESOLUTION_KEEP_SERVER: return E_CAL_OPERATION_FLAG_CONFLICT_KEEP_SERVER; case E_CONFLICT_RESOLUTION_KEEP_LOCAL: return E_CAL_OPERATION_FLAG_CONFLICT_KEEP_LOCAL; case E_CONFLICT_RESOLUTION_WRITE_COPY: return E_CAL_OPERATION_FLAG_CONFLICT_WRITE_COPY; } return E_CAL_OPERATION_FLAG_CONFLICT_KEEP_LOCAL; } static void ecu_remove_all_but_filename_parameter (ICalProperty *prop) { ICalParameter *param; g_return_if_fail (prop != NULL); while (param = i_cal_property_get_first_parameter (prop, I_CAL_ANY_PARAMETER), param) { if (i_cal_parameter_isa (param) == I_CAL_FILENAME_PARAMETER) { g_object_unref (param); param = i_cal_property_get_next_parameter (prop, I_CAL_ANY_PARAMETER); if (!param) break; } i_cal_property_remove_parameter_by_ref (prop, param); g_object_unref (param); } } /** * e_cal_util_inline_local_attachments_sync: * @component: an #ICalComponent to work with * @cancellable: optional #GCancellable object, or %NULL * @error: return location for a #GError, or %NULL * * Changes all URL attachments which point to a local file in @component * to inline attachments, aka adds the file content into the @component. * It also populates FILENAME parameter on the attachment. * * Returns: Whether succeeded. * * Since: 3.40 **/ gboolean e_cal_util_inline_local_attachments_sync (ICalComponent *component, GCancellable *cancellable, GError **error) { ICalProperty *prop; const gchar *uid; gboolean success = TRUE; g_return_val_if_fail (component != NULL, FALSE); uid = i_cal_component_get_uid (component); for (prop = i_cal_component_get_first_property (component, I_CAL_ATTACH_PROPERTY); prop && success; g_object_unref (prop), prop = i_cal_component_get_next_property (component, I_CAL_ATTACH_PROPERTY)) { ICalAttach *attach; attach = i_cal_property_get_attach (prop); if (attach && i_cal_attach_get_is_url (attach)) { const gchar *url; url = i_cal_attach_get_url (attach); if (g_str_has_prefix (url, "file://")) { GFile *file; gchar *basename; gchar *content; gsize len; file = g_file_new_for_uri (url); basename = g_file_get_basename (file); if (g_file_load_contents (file, cancellable, &content, &len, NULL, error)) { ICalAttach *new_attach; ICalParameter *param; gchar *base64; base64 = g_base64_encode ((const guchar *) content, len); new_attach = i_cal_attach_new_from_data (base64, (GFunc) g_free, NULL); g_free (content); ecu_remove_all_but_filename_parameter (prop); i_cal_property_set_attach (prop, new_attach); g_object_unref (new_attach); param = i_cal_parameter_new_value (I_CAL_VALUE_BINARY); i_cal_property_take_parameter (prop, param); param = i_cal_parameter_new_encoding (I_CAL_ENCODING_BASE64); i_cal_property_take_parameter (prop, param); /* Preserve existing FILENAME parameter */ if (!e_cal_util_property_has_parameter (prop, I_CAL_FILENAME_PARAMETER)) { const gchar *use_filename = basename; /* generated filename by Evolution */ if (uid && g_str_has_prefix (use_filename, uid) && use_filename[strlen (uid)] == '-') { use_filename += strlen (uid) + 1; } param = i_cal_parameter_new_filename (use_filename); i_cal_property_take_parameter (prop, param); } } else { success = FALSE; } g_object_unref (file); g_free (basename); } } g_clear_object (&attach); } g_clear_object (&prop); return success; } /** * e_cal_util_set_alarm_acknowledged: * @component: an #ECalComponent * @auid: an alarm UID to modify * @when: a time, in UTC, when to set the acknowledged property, or 0 for the current time * * Sets the ACKNOWLEDGED property on the @component's alarm with UID @auid * to the time @when (in UTC), or to the current time, when the @when is 0. * * Returns: Whether succeeded. * * Since: 3.40 **/ gboolean e_cal_util_set_alarm_acknowledged (ECalComponent *component, const gchar *auid, gint64 when) { ECalComponentAlarm *alarm, *copy; ICalTime *tt; g_return_val_if_fail (E_IS_CAL_COMPONENT (component), FALSE); g_return_val_if_fail (auid != NULL, FALSE); alarm = e_cal_component_get_alarm (component, auid); if (!alarm) return FALSE; if (when) tt = i_cal_time_new_from_timet_with_zone (when, 0, i_cal_timezone_get_utc_timezone ()); else tt = i_cal_time_new_current_with_zone (i_cal_timezone_get_utc_timezone ()); copy = e_cal_component_alarm_copy (alarm); e_cal_component_alarm_take_acknowledged (copy, tt); e_cal_component_remove_alarm (component, auid); e_cal_component_add_alarm (component, copy); e_cal_component_alarm_free (copy); e_cal_component_alarm_free (alarm); return TRUE; } static void e_cal_util_clamp_vtimezone_subcomps (ICalComponent *vtimezone, ICalComponentKind kind, const ICalTime *from, const ICalTime *to) { ICalComponent *subcomp; ICalComponent *nearest_from_comp = NULL, *nearest_to_comp = NULL; ICalTime *nearest_from_time = NULL, *nearest_to_time = NULL; GSList *remove = NULL, *link; for (subcomp = i_cal_component_get_first_component (vtimezone, kind); subcomp; g_object_unref (subcomp), subcomp = i_cal_component_get_next_component (vtimezone, kind)) { ICalTime *dtstart; dtstart = i_cal_component_get_dtstart (subcomp); if (dtstart && !i_cal_time_is_null_time (dtstart) && i_cal_time_is_valid_time (dtstart)) { gint cmp; cmp = i_cal_time_compare (dtstart, from); if (cmp < 0) { if (nearest_from_time) { if (i_cal_time_compare (dtstart, nearest_from_time) > 0) { g_clear_object (&nearest_from_time); nearest_from_time = g_object_ref (dtstart); remove = g_slist_prepend (remove, nearest_from_comp); nearest_from_comp = g_object_ref (subcomp); } else { remove = g_slist_prepend (remove, g_object_ref (subcomp)); } } else { nearest_from_time = g_object_ref (dtstart); nearest_from_comp = g_object_ref (subcomp); } } else if (cmp > 0 && to) { cmp = i_cal_time_compare (to, dtstart); if (cmp < 0) remove = g_slist_prepend (remove, g_object_ref (subcomp)); } } g_clear_object (&dtstart); } g_clear_object (&nearest_from_comp); g_clear_object (&nearest_from_time); g_clear_object (&nearest_to_comp); g_clear_object (&nearest_to_time); for (link = remove; link; link = g_slist_next (link)) { subcomp = link->data; i_cal_component_remove_component (vtimezone, subcomp); } g_slist_free_full (remove, g_object_unref); } /** * e_cal_util_clamp_vtimezone: * @vtimezone: (inout): a VTIMEZONE component to modify * @from: an #ICalTime for the minimum time * @to: (nullable): until which time to clamp, or %NULL for infinity * * Modifies the @vtimezone to include only subcomponents influencing * the passed-in time interval between @from and @to. * * Since: 3.40 **/ void e_cal_util_clamp_vtimezone (ICalComponent *vtimezone, const ICalTime *from, const ICalTime *to) { g_return_if_fail (I_CAL_IS_COMPONENT (vtimezone)); g_return_if_fail (i_cal_component_isa (vtimezone) == I_CAL_VTIMEZONE_COMPONENT); g_return_if_fail (I_CAL_IS_TIME ((ICalTime *) from)); if (to) { g_return_if_fail (I_CAL_IS_TIME ((ICalTime *) to)); if (i_cal_time_is_null_time (to) || !i_cal_time_is_valid_time (to)) to = NULL; } if (i_cal_time_is_null_time (from) || !i_cal_time_is_valid_time (from)) return; e_cal_util_clamp_vtimezone_subcomps (vtimezone, I_CAL_XSTANDARD_COMPONENT, from, to); e_cal_util_clamp_vtimezone_subcomps (vtimezone, I_CAL_XDAYLIGHT_COMPONENT, from, to); } /** * e_cal_util_clamp_vtimezone_by_component: * @vtimezone: (inout): a VTIMEZONE component to modify * @component: an #ICalComponent to read the times from * * Similar to e_cal_util_clamp_vtimezone(), only reads the clamp * times from the @component. * * Since: 3.40 **/ void e_cal_util_clamp_vtimezone_by_component (ICalComponent *vtimezone, ICalComponent *component) { ICalProperty *prop; ICalTime *dtstart, *dtend = NULL; g_return_if_fail (I_CAL_IS_COMPONENT (vtimezone)); g_return_if_fail (i_cal_component_isa (vtimezone) == I_CAL_VTIMEZONE_COMPONENT); g_return_if_fail (I_CAL_IS_COMPONENT (component)); dtstart = i_cal_component_get_dtstart (component); if (!dtstart) return; prop = i_cal_component_get_first_property (component, I_CAL_RECURRENCEID_PROPERTY); if (prop) { ICalTime *recurid; recurid = i_cal_property_get_recurrenceid (prop); dtend = i_cal_component_isa (component) == I_CAL_VEVENT_COMPONENT ? i_cal_component_get_dtend (component) : NULL; if (dtend && (i_cal_time_is_null_time (dtend) || !i_cal_time_is_valid_time (dtend))) g_clear_object (&dtend); if (!dtend) dtend = i_cal_component_get_due (component); if (dtend && i_cal_time_compare (recurid, dtend) >= 0) { g_clear_object (&dtend); dtend = recurid; recurid = NULL; } g_clear_object (&recurid); g_object_unref (prop); } else if (!e_cal_util_component_has_rrules (component)) { dtend = i_cal_component_isa (component) == I_CAL_VEVENT_COMPONENT ? i_cal_component_get_dtend (component) : NULL; if (dtend && (i_cal_time_is_null_time (dtend) || !i_cal_time_is_valid_time (dtend))) g_clear_object (&dtend); if (!dtend) dtend = i_cal_component_get_due (component); if (dtend && (i_cal_time_is_null_time (dtend) || !i_cal_time_is_valid_time (dtend))) g_clear_object (&dtend); if (!dtend) dtend = g_object_ref (dtstart); } if (i_cal_time_is_null_time (dtstart) || !i_cal_time_is_valid_time (dtstart)) { g_clear_object (&dtstart); if (dtend && !i_cal_time_is_null_time (dtend) && i_cal_time_is_valid_time (dtend)) dtstart = g_object_ref (dtend); } e_cal_util_clamp_vtimezone (vtimezone, dtstart, dtend); g_clear_object (&dtstart); g_clear_object (&dtend); } static gboolean locale_equals_language (const gchar *locale, const gchar *language) { guint ii; for (ii = 0; locale[ii] && language[ii]; ii++) { if ((locale[ii] == '-' || locale[ii] == '_') && (language[ii] == '-' || language[ii] == '_')) { continue; } if (g_ascii_tolower (locale[ii]) != g_ascii_tolower (language[ii])) break; } return !locale[ii] && !language[ii]; } /** * e_cal_util_component_find_property_for_locale: * @icalcomp: an #ICalComponent * @prop_kind: an #ICalPropertyKind to traverse * @locale: (nullable): a locale identifier, or %NULL * * Searches properties of kind @prop_kind in the @icalcomp and returns * one, which is usable for the @locale. When @locale is %NULL, * the current locale is assumed. If no such property for the locale * exists either the one with no language parameter or the first * found is returned. * * Free the returned non-NULL #ICalProperty with g_object_unref(), * when no longer needed. * * Returns: (transfer full) (nullable): a property of kind @prop_kind for the @locale, * %NULL if no such property is set on the @comp. * * Since: 3.46 **/ ICalProperty * e_cal_util_component_find_property_for_locale (ICalComponent *icalcomp, ICalPropertyKind prop_kind, const gchar *locale) { ICalProperty *prop; ICalProperty *result = NULL; ICalProperty *first = NULL; ICalProperty *nolang = NULL; ICalProperty *best = NULL; gint best_index = -1; gchar **locale_variants = NULL; const gchar *const *locales = NULL; g_return_val_if_fail (I_CAL_IS_COMPONENT (icalcomp), NULL); if (locale) { locale_variants = g_get_locale_variants (locale); locales = (const gchar * const *) locale_variants; } if (!locales) locales = g_get_language_names (); for (prop = i_cal_component_get_first_property (icalcomp, prop_kind); prop; g_object_unref (prop), prop = i_cal_component_get_next_property (icalcomp, prop_kind)) { ICalParameter *param; param = i_cal_property_get_first_parameter (prop, I_CAL_LANGUAGE_PARAMETER); if (param) { const gchar *language = i_cal_parameter_get_language (param); if (!language || !*language) { if (!best) { if (!first) first = g_object_ref (prop); if (!nolang) nolang = g_object_ref (prop); } } else { guint ii; for (ii = 0; locales && locales[ii] && (best_index == -1 || ii < best_index); ii++) { if (locale_equals_language (locales[ii], language)) { g_clear_object (&best); best = g_object_ref (prop); best_index = ii; break; } } if (!ii && best) { g_clear_object (¶m); g_clear_object (&prop); break; } if (!best && !first) first = g_object_ref (prop); } g_clear_object (¶m); } else if (!best) { if (!first) first = g_object_ref (prop); if (!nolang) nolang = g_object_ref (prop); } } if (best) result = g_steal_pointer (&best); else if (nolang) result = g_steal_pointer (&nolang); else if (first) result = g_steal_pointer (&first); g_clear_object (&first); g_clear_object (&nolang); g_clear_object (&best); g_clear_pointer (&locale_variants, g_strfreev); return result; } /** * e_cal_util_foreach_category: * @comp: an #ICalComponent * @func: (scope call): an #ECalUtilForeachCategoryFunc callback to call for each category * @user_data: user data passed to the @func * * Calls @func for each category stored in the @comp. * * Since: 3.48 **/ void e_cal_util_foreach_category (ICalComponent *comp, ECalUtilForeachCategoryFunc func, gpointer user_data) { ICalProperty *prop; const gchar *categories; const gchar *p; const gchar *cat_start; gchar *str; gboolean can_continue = TRUE; g_return_if_fail (I_CAL_IS_COMPONENT (comp)); g_return_if_fail (func != NULL); for (prop = i_cal_component_get_first_property (comp, I_CAL_CATEGORIES_PROPERTY); prop && can_continue; g_object_unref (prop), prop = i_cal_component_get_next_property (comp, I_CAL_CATEGORIES_PROPERTY)) { categories = i_cal_property_get_categories (prop); if (!categories) continue; cat_start = categories; for (p = categories; *p && can_continue; p++) { if (*p == ',') { if (p - cat_start > 0) { str = g_strstrip (g_strndup (cat_start, p - cat_start)); if (*str) can_continue = func (comp, &str, user_data); g_free (str); } cat_start = p + 1; } } if (can_continue && p - cat_start > 0) { str = g_strstrip (g_strndup (cat_start, p - cat_start)); if (*str) can_continue = func (comp, &str, user_data); g_free (str); } } g_clear_object (&prop); } static gboolean e_cal_util_extract_categories_cb (ICalComponent *comp, gchar **inout_category, gpointer user_data) { GHashTable **pcategories = user_data; g_return_val_if_fail (pcategories != NULL, FALSE); g_return_val_if_fail (inout_category != NULL, FALSE); g_return_val_if_fail (*inout_category != NULL, FALSE); if (!*pcategories) *pcategories = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); g_hash_table_insert (*pcategories, *inout_category, GINT_TO_POINTER (1)); *inout_category = NULL; return TRUE; } static GHashTable * e_cal_util_extract_categories (ICalComponent *comp) { GHashTable *categories = NULL; if (!comp) return NULL; g_return_val_if_fail (I_CAL_IS_COMPONENT (comp), NULL); e_cal_util_foreach_category (comp, e_cal_util_extract_categories_cb, &categories); return categories; } static gboolean e_cal_util_remove_matching_category_cb (gpointer key, gpointer value, gpointer user_data) { GHashTable *other_table = user_data; /* Remove from both tables those common */ return g_hash_table_remove (other_table, key); } /** * e_cal_util_diff_categories: * @old_comp: (nullable): an old #ICalComponent, or %NULL * @new_comp: (nullable): a new #ICalComponent, or %NULL * @out_added: (out) (transfer container) (element-type utf8 int): a #GHashTable with added categories * @out_removed: (out) (transfer container) (element-type utf8 int): a #GHashTable with removed categories * * Compares list of categories on the @old_comp with the list of categories * on the @new_comp and fills @out_added categories and @out_removed categories * accordingly, as if the @old_comp is replaced with the @new_comp. When either * of the components is %NULL, it's considered as having no categories set. * Rather than returning empty #GHashTable, the return argument is set to %NULL * when there are no added/removed categories. * * The key of the hash table is the category string, the value is an integer (1). * There is used the hash table only for speed. * * The returned #GHashTable-s should be freed with g_hash_table_unref(), * when no longer needed. * * Since: 3.48 **/ void e_cal_util_diff_categories (ICalComponent *old_comp, ICalComponent *new_comp, GHashTable **out_added, /* const gchar *category ~> 1 */ GHashTable **out_removed) /* const gchar *category ~> 1 */ { if (old_comp) g_return_if_fail (I_CAL_IS_COMPONENT (old_comp)); if (new_comp) g_return_if_fail (I_CAL_IS_COMPONENT (new_comp)); g_return_if_fail (out_added != NULL); g_return_if_fail (out_removed != NULL); *out_added = e_cal_util_extract_categories (new_comp); *out_removed = e_cal_util_extract_categories (old_comp); if (*out_added && *out_removed) { g_hash_table_foreach_remove (*out_added, e_cal_util_remove_matching_category_cb, *out_removed); if (!g_hash_table_size (*out_added)) { g_hash_table_unref (*out_added); *out_added = NULL; } if (!g_hash_table_size (*out_removed)) { g_hash_table_unref (*out_removed); *out_removed = NULL; } } }