KCalendarCore

icaltimezones.cpp
1 /*
2  This file is part of the kcalcore library.
3 
4  SPDX-FileCopyrightText: 2005-2007 David Jarvie <[email protected]>
5 
6  SPDX-License-Identifier: LGPL-2.0-or-later
7 */
8 
9 #include "icaltimezones_p.h"
10 #include "icalformat.h"
11 #include "icalformat_p.h"
12 #include "recurrence.h"
13 #include "recurrencerule.h"
14 #include "recurrencehelper_p.h"
15 
16 #include "kcalendarcore_debug.h"
17 
18 #include <QDateTime>
19 #include <QByteArray>
20 
21 extern "C" {
22 #include <libical/ical.h>
23 #include <icaltimezone.h>
24 }
25 
26 using namespace KCalendarCore;
27 
28 // Minimum repetition counts for VTIMEZONE RRULEs
29 static const int minRuleCount = 5; // for any RRULE
30 static const int minPhaseCount = 8; // for separate STANDARD/DAYLIGHT component
31 
32 // Convert an ical time to QDateTime, preserving the UTC indicator
33 static QDateTime toQDateTime(const icaltimetype &t)
34 {
35  return QDateTime(QDate(t.year, t.month, t.day),
36  QTime(t.hour, t.minute, t.second),
37 #if defined(USE_ICAL_3)
38  (icaltime_is_utc(t) ? Qt::UTC : Qt::LocalTime));
39 #else
40  (t.is_utc ? Qt::UTC : Qt::LocalTime));
41 #endif
42 }
43 
44 // Maximum date for time zone data.
45 // It's not sensible to try to predict them very far in advance, because
46 // they can easily change. Plus, it limits the processing required.
47 static QDateTime MAX_DATE()
48 {
49  static QDateTime dt;
50  if (!dt.isValid()) {
51  dt = QDateTime(QDate::currentDate().addYears(20), QTime(0, 0, 0));
52  }
53  return dt;
54 }
55 
56 static icaltimetype writeLocalICalDateTime(const QDateTime &utc, int offset)
57 {
58  const QDateTime local = utc.addSecs(offset);
59  icaltimetype t = icaltime_null_time();
60  t.year = local.date().year();
61  t.month = local.date().month();
62  t.day = local.date().day();
63  t.hour = local.time().hour();
64  t.minute = local.time().minute();
65  t.second = local.time().second();
66  t.is_date = 0;
67  t.zone = nullptr;
68 #if !defined(USE_ICAL_3)
69  t.is_utc = 0;
70 #endif
71  return t;
72 }
73 
74 namespace KCalendarCore
75 {
76 
77 void ICalTimeZonePhase::dump()
78 {
79  qDebug() << " ~~~ ICalTimeZonePhase ~~~";
80  qDebug() << " Abbreviations:" << abbrevs;
81  qDebug() << " UTC offset:" << utcOffset;
82  qDebug() << " Transitions:" << transitions;
83  qDebug() << " ~~~~~~~~~~~~~~~~~~~~~~~~~";
84 }
85 
86 void ICalTimeZone::dump() {
87  qDebug() << "~~~ ICalTimeZone ~~~";
88  qDebug() << "ID:" << id;
89  qDebug() << "QZONE:" << qZone.id();
90  qDebug() << "STD:";
91  standard.dump();
92  qDebug() << "DST:";
93  daylight.dump();
94  qDebug() << "~~~~~~~~~~~~~~~~~~~~";
95 }
96 
97 ICalTimeZoneCache::ICalTimeZoneCache()
98 {
99 }
100 
101 void ICalTimeZoneCache::insert(const QByteArray &id, const ICalTimeZone &tz)
102 {
103  mCache.insert(id, tz);
104 }
105 
106 namespace {
107 
108 template<typename T>
109 typename T::const_iterator greatestSmallerThan(const T &c, const typename T::value_type &v)
110 {
111  auto it = std::lower_bound(c.cbegin(), c.cend(), v);
112  if (it != c.cbegin()) {
113  return --it;
114  }
115  return c.cend();
116 }
117 
118 }
119 
120 QTimeZone ICalTimeZoneCache::tzForTime(const QDateTime &dt, const QByteArray &tzid) const
121 {
123  return QTimeZone(tzid);
124  }
125 
126  const ICalTimeZone tz = mCache.value(tzid);
127  if (!tz.qZone.isValid()) {
128  return QTimeZone::systemTimeZone();
129  }
130 
131  // If the matched timezone is one of the UTC offset timezones, we need to make
132  // sure it's in the correct DTS.
133  // The lookup in ICalTimeZoneParser will only find TZ in standard time, but
134  // if the datetim in question fits in the DTS zone, we need to use another UTC
135  // offset timezone
136  if (tz.qZone.id().startsWith("UTC")) { //krazy:exclude=strings
137  // Find the nearest standard and DST transitions that occur BEFORE the "dt"
138  const auto stdPrev = greatestSmallerThan(tz.standard.transitions, dt);
139  const auto dstPrev = greatestSmallerThan(tz.daylight.transitions, dt);
140  if (stdPrev != tz.standard.transitions.cend() && dstPrev != tz.daylight.transitions.cend()) {
141  if (*dstPrev > *stdPrev) {
142  // Previous DTS is closer to "dt" than previous standard, which
143  // means we are in DTS right now
144  const auto tzids = QTimeZone::availableTimeZoneIds(tz.daylight.utcOffset);
145  auto dtsTzId = std::find_if(tzids.cbegin(), tzids.cend(),
146  [](const QByteArray &id) {
147  return id.startsWith("UTC"); //krazy:exclude=strings
148  });
149  if (dtsTzId != tzids.cend()) {
150  return QTimeZone(*dtsTzId);
151  }
152  }
153  }
154  }
155 
156  return tz.qZone;
157 }
158 
159 ICalTimeZoneParser::ICalTimeZoneParser(ICalTimeZoneCache *cache)
160  : mCache(cache)
161 {
162 }
163 
164 void ICalTimeZoneParser::updateTzEarliestDate(const IncidenceBase::Ptr &incidence,
165  TimeZoneEarliestDate *earliest)
166 {
168  const auto dt = incidence->dateTime(role);
169  if (dt.isValid()) {
170  if (dt.timeZone() == QTimeZone::utc()) {
171  continue;
172  }
173  const auto prev = earliest->value(incidence->dtStart().timeZone());
174  if (!prev.isValid() || incidence->dtStart() < prev) {
175  earliest->insert(incidence->dtStart().timeZone(), prev);
176  }
177  }
178  }
179 }
180 
181 icalcomponent *ICalTimeZoneParser::icalcomponentFromQTimeZone(const QTimeZone &tz,
182  const QDateTime &earliest)
183 {
184  // VTIMEZONE RRULE types
185  enum {
186  DAY_OF_MONTH = 0x01,
187  WEEKDAY_OF_MONTH = 0x02,
188  LAST_WEEKDAY_OF_MONTH = 0x04
189  };
190 
191  // Write the time zone data into an iCal component
192  icalcomponent *tzcomp = icalcomponent_new(ICAL_VTIMEZONE_COMPONENT);
193  icalcomponent_add_property(tzcomp, icalproperty_new_tzid(tz.id().constData()));
194  // icalcomponent_add_property(tzcomp, icalproperty_new_location( tz.name().toUtf8() ));
195 
196  // Compile an ordered list of transitions so that we can know the phases
197  // which occur before and after each transition.
198  QTimeZone::OffsetDataList transits = tz.transitions(QDateTime(), MAX_DATE());
199  if (transits.isEmpty()) {
200  // If there is no way to compile a complete list of transitions
201  // transitions() can return an empty list
202  // In that case try get one transition to write a valid VTIMEZONE entry.
203  if (transits.isEmpty()) {
204  qCDebug(KCALCORE_LOG) << "No transition information available VTIMEZONE will be invalid.";
205  }
206  }
207  if (earliest.isValid()) {
208  // Remove all transitions earlier than those we are interested in
209  for (int i = 0, end = transits.count(); i < end; ++i) {
210  if (transits.at(i).atUtc >= earliest) {
211  if (i > 0) {
212  transits.erase(transits.begin(), transits.begin() + i);
213  }
214  break;
215  }
216  }
217  }
218  int trcount = transits.count();
219  QVector<bool> transitionsDone(trcount, false);
220 
221  // Go through the list of transitions and create an iCal component for each
222  // distinct combination of phase after and UTC offset before the transition.
223  icaldatetimeperiodtype dtperiod;
224  dtperiod.period = icalperiodtype_null_period();
225  for (;;) {
226  int i = 0;
227  for (; i < trcount && transitionsDone[i]; ++i) {
228  ;
229  }
230  if (i >= trcount) {
231  break;
232  }
233  // Found a phase combination which hasn't yet been processed
234  const int preOffset = (i > 0) ? transits.at(i - 1).offsetFromUtc : 0;
235  const auto &transit = transits.at(i);
236  if (transit.offsetFromUtc == preOffset) {
237  transitionsDone[i] = true;
238  while (++i < trcount) {
239  if (transitionsDone[i] ||
240  transits.at(i).offsetFromUtc != transit.offsetFromUtc ||
241  transits.at(i).daylightTimeOffset != transit.daylightTimeOffset ||
242  transits.at(i - 1).offsetFromUtc != preOffset) {
243  continue;
244  }
245  transitionsDone[i] = true;
246  }
247  continue;
248  }
249  const bool isDst = transit.daylightTimeOffset > 0;
250  icalcomponent *phaseComp =
251  icalcomponent_new(isDst ? ICAL_XDAYLIGHT_COMPONENT : ICAL_XSTANDARD_COMPONENT);
252  if (!transit.abbreviation.isEmpty()) {
253  icalcomponent_add_property(phaseComp,
254  icalproperty_new_tzname(
255  static_cast<const char *>(transit.abbreviation.toUtf8().constData())));
256  }
257  icalcomponent_add_property(phaseComp,
258  icalproperty_new_tzoffsetfrom(preOffset));
259  icalcomponent_add_property(phaseComp,
260  icalproperty_new_tzoffsetto(transit.offsetFromUtc));
261  // Create a component to hold initial RRULE if any, plus all RDATEs
262  icalcomponent *phaseComp1 = icalcomponent_new_clone(phaseComp);
263  icalcomponent_add_property(phaseComp1,
264  icalproperty_new_dtstart(
265  writeLocalICalDateTime(transits.at(i).atUtc,
266  preOffset)));
267  bool useNewRRULE = false;
268 
269  // Compile the list of UTC transition dates/times, and check
270  // if the list can be reduced to an RRULE instead of multiple RDATEs.
271  QTime time;
272  QDate date;
273  int year = 0, month = 0, daysInMonth = 0, dayOfMonth = 0; // avoid compiler warnings
274  int dayOfWeek = 0; // Monday = 1
275  int nthFromStart = 0; // nth (weekday) of month
276  int nthFromEnd = 0; // nth last (weekday) of month
277  int newRule;
278  int rule = 0;
279  QList<QDateTime> rdates;// dates which (probably) need to be written as RDATEs
280  QList<QDateTime> times;
281  QDateTime qdt = transits.at(i).atUtc; // set 'qdt' for start of loop
282  times += qdt;
283  transitionsDone[i] = true;
284  do {
285  if (!rule) {
286  // Initialise data for detecting a new rule
287  rule = DAY_OF_MONTH | WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH;
288  time = qdt.time();
289  date = qdt.date();
290  year = date.year();
291  month = date.month();
292  daysInMonth = date.daysInMonth();
293  dayOfWeek = date.dayOfWeek(); // Monday = 1
294  dayOfMonth = date.day();
295  nthFromStart = (dayOfMonth - 1) / 7 + 1; // nth (weekday) of month
296  nthFromEnd = (daysInMonth - dayOfMonth) / 7 + 1; // nth last (weekday) of month
297  }
298  if (++i >= trcount) {
299  newRule = 0;
300  times += QDateTime(); // append a dummy value since last value in list is ignored
301  } else {
302  if (transitionsDone[i] ||
303  transits.at(i).offsetFromUtc != transit.offsetFromUtc ||
304  transits.at(i).daylightTimeOffset != transit.daylightTimeOffset ||
305  transits.at(i - 1).offsetFromUtc != preOffset) {
306  continue;
307  }
308  transitionsDone[i] = true;
309  qdt = transits.at(i).atUtc;
310  if (!qdt.isValid()) {
311  continue;
312  }
313  newRule = rule;
314  times += qdt;
315  date = qdt.date();
316  if (qdt.time() != time ||
317  date.month() != month ||
318  date.year() != ++year) {
319  newRule = 0;
320  } else {
321  const int day = date.day();
322  if ((newRule & DAY_OF_MONTH) && day != dayOfMonth) {
323  newRule &= ~DAY_OF_MONTH;
324  }
325  if (newRule & (WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH)) {
326  if (date.dayOfWeek() != dayOfWeek) {
327  newRule &= ~(WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH);
328  } else {
329  if ((newRule & WEEKDAY_OF_MONTH) &&
330  (day - 1) / 7 + 1 != nthFromStart) {
331  newRule &= ~WEEKDAY_OF_MONTH;
332  }
333  if ((newRule & LAST_WEEKDAY_OF_MONTH) &&
334  (daysInMonth - day) / 7 + 1 != nthFromEnd) {
335  newRule &= ~LAST_WEEKDAY_OF_MONTH;
336  }
337  }
338  }
339  }
340  }
341  if (!newRule) {
342  // The previous rule (if any) no longer applies.
343  // Write all the times up to but not including the current one.
344  // First check whether any of the last RDATE values fit this rule.
345  int yr = times[0].date().year();
346  while (!rdates.isEmpty()) {
347  qdt = rdates.last();
348  date = qdt.date();
349  if (qdt.time() != time ||
350  date.month() != month ||
351  date.year() != --yr) {
352  break;
353  }
354  const int day = date.day();
355  if (rule & DAY_OF_MONTH) {
356  if (day != dayOfMonth) {
357  break;
358  }
359  } else {
360  if (date.dayOfWeek() != dayOfWeek ||
361  ((rule & WEEKDAY_OF_MONTH) &&
362  (day - 1) / 7 + 1 != nthFromStart) ||
363  ((rule & LAST_WEEKDAY_OF_MONTH) &&
364  (daysInMonth - day) / 7 + 1 != nthFromEnd)) {
365  break;
366  }
367  }
368  times.prepend(qdt);
369  rdates.pop_back();
370  }
371  if (times.count() > (useNewRRULE ? minPhaseCount : minRuleCount)) {
372  // There are enough dates to combine into an RRULE
373  icalrecurrencetype r;
374  icalrecurrencetype_clear(&r);
375  r.freq = ICAL_YEARLY_RECURRENCE;
376  r.by_month[0] = month;
377  if (rule & DAY_OF_MONTH) {
378  r.by_month_day[0] = dayOfMonth;
379  } else if (rule & WEEKDAY_OF_MONTH) {
380  r.by_day[0] = (dayOfWeek % 7 + 1) + (nthFromStart * 8); // Sunday = 1
381  } else if (rule & LAST_WEEKDAY_OF_MONTH) {
382  r.by_day[0] = -(dayOfWeek % 7 + 1) - (nthFromEnd * 8); // Sunday = 1
383  }
384  r.until = writeLocalICalDateTime(times.takeAt(times.size() - 1), preOffset);
385  icalproperty *prop = icalproperty_new_rrule(r);
386  if (useNewRRULE) {
387  // This RRULE doesn't start from the phase start date, so set it into
388  // a new STANDARD/DAYLIGHT component in the VTIMEZONE.
389  icalcomponent *c = icalcomponent_new_clone(phaseComp);
390  icalcomponent_add_property(
391  c, icalproperty_new_dtstart(writeLocalICalDateTime(times[0], preOffset)));
392  icalcomponent_add_property(c, prop);
393  icalcomponent_add_component(tzcomp, c);
394  } else {
395  icalcomponent_add_property(phaseComp1, prop);
396  }
397  } else {
398  // Save dates for writing as RDATEs
399  for (int t = 0, tend = times.count() - 1; t < tend; ++t) {
400  rdates += times[t];
401  }
402  }
403  useNewRRULE = true;
404  // All date/time values but the last have been added to the VTIMEZONE.
405  // Remove them from the list.
406  qdt = times.last(); // set 'qdt' for start of loop
407  times.clear();
408  times += qdt;
409  }
410  rule = newRule;
411  } while (i < trcount);
412 
413  // Write remaining dates as RDATEs
414  for (int rd = 0, rdend = rdates.count(); rd < rdend; ++rd) {
415  dtperiod.time = writeLocalICalDateTime(rdates[rd], preOffset);
416  icalcomponent_add_property(phaseComp1, icalproperty_new_rdate(dtperiod));
417  }
418  icalcomponent_add_component(tzcomp, phaseComp1);
419  icalcomponent_free(phaseComp);
420  }
421 
422  return tzcomp;
423 }
424 
425 icaltimezone *ICalTimeZoneParser::icaltimezoneFromQTimeZone(const QTimeZone &tz,
426  const QDateTime &earliest)
427 {
428  auto itz = icaltimezone_new();
429  icaltimezone_set_component(itz, icalcomponentFromQTimeZone(tz, earliest));
430  return itz;
431 }
432 
433 void ICalTimeZoneParser::parse(icalcomponent *calendar)
434 {
435  for (auto *c = icalcomponent_get_first_component(calendar, ICAL_VTIMEZONE_COMPONENT);
436  c; c = icalcomponent_get_next_component(calendar, ICAL_VTIMEZONE_COMPONENT)) {
437  auto icalZone = parseTimeZone(c);
438  //icalZone.dump();
439  if (!icalZone.id.isEmpty()) {
440  if (!icalZone.qZone.isValid()) {
441  icalZone.qZone = resolveICalTimeZone(icalZone);
442  }
443  if (!icalZone.qZone.isValid()) {
444  qCWarning(KCALCORE_LOG) << "Failed to map" << icalZone.id << "to a known IANA timezone";
445  continue;
446  }
447  mCache->insert(icalZone.id, icalZone);
448  }
449  }
450 }
451 
452 QTimeZone ICalTimeZoneParser::resolveICalTimeZone(const ICalTimeZone &icalZone)
453 {
454  const auto phase = icalZone.standard;
455  const auto now = QDateTime::currentDateTimeUtc();
456 
457  const auto candidates = QTimeZone::availableTimeZoneIds(phase.utcOffset);
458  QMap<int, QTimeZone> matchedCandidates;
459  for (const auto &tzid : candidates) {
460  const QTimeZone candidate(tzid);
461  // This would be a fallback, candidate has transitions, but the phase does not
462  if (candidate.hasTransitions() == phase.transitions.isEmpty()) {
463  matchedCandidates.insert(0, candidate);
464  continue;
465  }
466 
467  // Without transitions, we can't do any more precise matching, so just
468  // accept this candidate and be done with it
469  if (!candidate.hasTransitions() && phase.transitions.isEmpty()) {
470  return candidate;
471  }
472 
473  // Calculate how many transitions this candidate shares with the phase.
474  // The candidate with the most matching transitions will win.
475  auto begin = std::lower_bound(phase.transitions.cbegin(), phase.transitions.cend(), now.addYears(-20));
476  // If no transition older than 20 years is found, we will start from beginning
477  if (begin == phase.transitions.cend()) {
478  begin = phase.transitions.cbegin();
479  }
480  auto end = std::upper_bound(begin, phase.transitions.cend(), now);
481  int matchedTransitions = 0;
482  for (auto it = begin; it != end; ++it) {
483  const auto &transition = *it;
484  const QTimeZone::OffsetDataList candidateTransitions = candidate.transitions(transition, transition);
485  if (candidateTransitions.isEmpty()) {
486  continue;
487  }
488  ++matchedTransitions; // 1 point for a matching transition
489  const auto candidateTransition = candidateTransitions[0];
490  // FIXME: THIS IS HOW IT SHOULD BE:
491  //const auto abvs = transition.abbreviations();
492  const auto abvs = phase.abbrevs;
493  for (const auto &abv : abvs) {
494  if (candidateTransition.abbreviation == QString::fromUtf8(abv)) {
495  matchedTransitions += 1024; // lots of points for a transition with a matching abbreviation
496  break;
497  }
498  }
499  }
500  matchedCandidates.insert(matchedTransitions, candidate);
501  }
502 
503  if (!matchedCandidates.isEmpty()) {
504  return matchedCandidates.value(matchedCandidates.lastKey());
505  }
506 
507  return {};
508 }
509 
510 ICalTimeZone ICalTimeZoneParser::parseTimeZone(icalcomponent *vtimezone)
511 {
512  ICalTimeZone icalTz;
513 
514  if (auto tzidProp = icalcomponent_get_first_property(vtimezone, ICAL_TZID_PROPERTY)) {
515  icalTz.id = icalproperty_get_value_as_string(tzidProp);
516 
517  // If the VTIMEZONE is a known IANA time zone don't bother parsing the rest
518  // of the VTIMEZONE, get QTimeZone directly from Qt
519  if (QTimeZone::isTimeZoneIdAvailable(icalTz.id)) {
520  icalTz.qZone = QTimeZone(icalTz.id);
521  return icalTz;
522  } else {
523  // Not IANA, but maybe we can match it from Windows ID?
524  const auto ianaTzid = QTimeZone::windowsIdToDefaultIanaId(icalTz.id);
525  if (!ianaTzid.isEmpty()) {
526  icalTz.qZone = QTimeZone(ianaTzid);
527  return icalTz;
528  }
529  }
530  }
531 
532  for (icalcomponent *c = icalcomponent_get_first_component(vtimezone, ICAL_ANY_COMPONENT);
533  c; c = icalcomponent_get_next_component(vtimezone, ICAL_ANY_COMPONENT)) {
534 
535  icalcomponent_kind kind = icalcomponent_isa(c);
536  switch (kind) {
537  case ICAL_XSTANDARD_COMPONENT:
538  //qCDebug(KCALCORE_LOG) << "---standard phase: found";
539  parsePhase(c, false, icalTz.standard);
540  break;
541  case ICAL_XDAYLIGHT_COMPONENT:
542  //qCDebug(KCALCORE_LOG) << "---daylight phase: found";
543  parsePhase(c, true, icalTz.daylight);
544  break;
545 
546  default:
547  qCDebug(KCALCORE_LOG) << "Unknown component:" << int(kind);
548  break;
549  }
550  }
551 
552  return icalTz;
553 }
554 
555 bool ICalTimeZoneParser::parsePhase(icalcomponent *c, bool daylight, ICalTimeZonePhase &phase)
556 {
557  // Read the observance data for this standard/daylight savings phase
558  int utcOffset = 0;
559  int prevOffset = 0;
560  bool recurs = false;
561  bool found_dtstart = false;
562  bool found_tzoffsetfrom = false;
563  bool found_tzoffsetto = false;
564  icaltimetype dtstart = icaltime_null_time();
565  QSet<QByteArray> abbrevs;
566 
567  // Now do the ical reading.
568  icalproperty *p = icalcomponent_get_first_property(c, ICAL_ANY_PROPERTY);
569  while (p) {
570  icalproperty_kind kind = icalproperty_isa(p);
571  switch (kind) {
572  case ICAL_TZNAME_PROPERTY: { // abbreviated name for this time offset
573  // TZNAME can appear multiple times in order to provide language
574  // translations of the time zone offset name.
575 
576  // TODO: Does this cope with multiple language specifications?
577  QByteArray name = icalproperty_get_tzname(p);
578  // Outlook (2000) places "Standard Time" and "Daylight Time" in the TZNAME
579  // strings, which is totally useless. So ignore those.
580  if ((!daylight && name == "Standard Time") ||
581  (daylight && name == "Daylight Time")) {
582  break;
583  }
584  abbrevs.insert(name);
585  break;
586  }
587  case ICAL_DTSTART_PROPERTY: // local time at which phase starts
588  dtstart = icalproperty_get_dtstart(p);
589  found_dtstart = true;
590  break;
591 
592  case ICAL_TZOFFSETFROM_PROPERTY: // UTC offset immediately before start of phase
593  prevOffset = icalproperty_get_tzoffsetfrom(p);
594  found_tzoffsetfrom = true;
595  break;
596 
597  case ICAL_TZOFFSETTO_PROPERTY:
598  utcOffset = icalproperty_get_tzoffsetto(p);
599  found_tzoffsetto = true;
600  break;
601 
602  case ICAL_RDATE_PROPERTY:
603  case ICAL_RRULE_PROPERTY:
604  recurs = true;
605  break;
606 
607  default:
608  break;
609  }
610  p = icalcomponent_get_next_property(c, ICAL_ANY_PROPERTY);
611  }
612 
613  // Validate the phase data
614  if (!found_dtstart || !found_tzoffsetfrom || !found_tzoffsetto) {
615  qCDebug(KCALCORE_LOG) << "DTSTART/TZOFFSETFROM/TZOFFSETTO missing";
616  return false;
617  }
618 
619  // Convert DTSTART to QDateTime, and from local time to UTC
620  const QDateTime localStart = toQDateTime(dtstart); // local time
621  dtstart.second -= prevOffset;
622 #if defined(USE_ICAL_3)
623  dtstart = icaltime_convert_to_zone(dtstart, icaltimezone_get_utc_timezone());
624 #else
625  dtstart.is_utc = 1;
626 #endif
627  const QDateTime utcStart = toQDateTime(icaltime_normalize(dtstart)); // UTC
628 
629  phase.abbrevs.unite(abbrevs);
630  phase.utcOffset = utcOffset;
631  phase.transitions += utcStart;
632 
633  if (recurs) {
634  /* RDATE or RRULE is specified. There should only be one or the other, but
635  * it doesn't really matter - the code can cope with both.
636  * Note that we had to get DTSTART, TZOFFSETFROM, TZOFFSETTO before reading
637  * recurrences.
638  */
639  const QDateTime maxTime(MAX_DATE());
640  Recurrence recur;
641  icalproperty *p = icalcomponent_get_first_property(c, ICAL_ANY_PROPERTY);
642  while (p) {
643  icalproperty_kind kind = icalproperty_isa(p);
644  switch (kind) {
645 
646  case ICAL_RDATE_PROPERTY: {
647  icaltimetype t = icalproperty_get_rdate(p).time;
648  if (icaltime_is_date(t)) {
649  // RDATE with a DATE value inherits the (local) time from DTSTART
650  t.hour = dtstart.hour;
651  t.minute = dtstart.minute;
652  t.second = dtstart.second;
653 #if !defined(USE_ICAL_3)
654  t.is_utc = 0; // dtstart is in local time
655 #endif
656  t.is_date = 0;
657  }
658  // RFC2445 states that RDATE must be in local time,
659  // but we support UTC as well to be safe.
660 #if defined(USE_ICAL_3)
661  if (!icaltime_is_utc(t)) {
662 #else
663  if (!t.is_utc) {
664 #endif
665  t.second -= prevOffset; // convert to UTC
666 #if defined(USE_ICAL_3)
667  t = icaltime_convert_to_zone(t, icaltimezone_get_utc_timezone());
668 #else
669  t.is_utc = 1;
670 #endif
671  t = icaltime_normalize(t);
672  }
673  phase.transitions += toQDateTime(t);
674  break;
675  }
676  case ICAL_RRULE_PROPERTY: {
677  RecurrenceRule r;
678  ICalFormat icf;
679  ICalFormatImpl impl(&icf);
680  impl.readRecurrence(icalproperty_get_rrule(p), &r);
681  r.setStartDt(localStart);
682  // The end date time specified in an RRULE should be in UTC.
683  // Convert to local time to avoid timesInInterval() getting things wrong.
684  if (r.duration() == 0) {
685  QDateTime end(r.endDt());
686  if (end.timeSpec() == Qt::UTC) {
688  r.setEndDt(end.addSecs(prevOffset));
689  }
690  }
691  const auto dts = r.timesInInterval(localStart, maxTime);
692  for (int i = 0, end = dts.count(); i < end; ++i) {
693  QDateTime utc = dts[i];
694  utc.setTimeSpec(Qt::UTC);
695  phase.transitions += utc.addSecs(-prevOffset);
696  }
697  break;
698  }
699  default:
700  break;
701  }
702  p = icalcomponent_get_next_property(c, ICAL_ANY_PROPERTY);
703  }
704  sortAndRemoveDuplicates(phase.transitions);
705  }
706 
707  return true;
708 }
709 
710 QByteArray ICalTimeZoneParser::vcaltimezoneFromQTimeZone(const QTimeZone &qtz,
711  const QDateTime &earliest)
712 {
713  auto icalTz = icalcomponentFromQTimeZone(qtz, earliest);
714  const QByteArray result(icalcomponent_as_ical_string(icalTz));
715  icalmemory_free_ring();
716  icalcomponent_free(icalTz);
717  return result;
718 }
719 
720 } // namespace KCalendarCore
void clear()
int minute() const const
QString name(const QVariant &location)
void setStartDt(const QDateTime &start)
Sets the recurrence start date/time.
const Key & lastKey() const const
QTime time() const const
T takeAt(int i)
int day() const const
QDateTime endDt(bool *result=nullptr) const
Returns the date and time of the last recurrence.
QSet::iterator insert(const T &value)
const QList< QKeySequence > & begin()
int second() const const
int size() const const
QByteArray windowsIdToDefaultIanaId(const QByteArray &windowsId)
QList< QByteArray > availableTimeZoneIds()
void setTimeSpec(Qt::TimeSpec spec)
int dayOfWeek() const const
QTimeZone::OffsetDataList transitions(const QDateTime &fromDateTime, const QDateTime &toDateTime) const const
int count(const T &value) const const
Role for determining an incidence&#39;s starting timezone.
QString fromUtf8(const char *str, int size)
Role for determining an incidence&#39;s ending timezone.
QTimeZone utc()
bool isEmpty() const const
const char * constData() const const
QTimeZone systemTimeZone()
int daysInMonth() const const
void pop_back()
int hour() const const
QByteArray id() const const
bool isValid() const const
const QList< QKeySequence > & end()
This class represents a recurrence rule for a calendar incidence.
Definition: recurrence.h:76
QDate date() const const
iCalendar format implementation.
Definition: icalformat.h:44
QDate currentDate()
T & last()
void setEndDt(const QDateTime &endDateTime)
Sets the date and time of the last recurrence.
void prepend(const T &value)
QMap::iterator insert(const Key &key, const T &value)
bool isEmpty() const const
int duration() const
Returns -1 if the event recurs infinitely, 0 if the end date is set, otherwise the total number of re...
QList< QDateTime > timesInInterval(const QDateTime &start, const QDateTime &end) const
Returns a list of all the times at which the recurrence will occur between two specified times...
This class represents a recurrence rule for a calendar incidence.
QDateTime addSecs(qint64 s) const const
bool isTimeZoneIdAvailable(const QByteArray &ianaId)
int year() const const
typedef OffsetDataList
int month() const const
QDateTime currentDateTimeUtc()
This file is part of the API for handling calendar data and defines the ICalFormat class...
Namespace for all KCalendarCore types.
Definition: alarm.h:36
QTimeZone timeZone() const const
const T value(const Key &key, const T &defaultValue) const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2020 The KDE developers.
Generated on Thu Nov 26 2020 22:49:37 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.