Warning, file /frameworks/kcalendarcore/src/icaltimezones.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /*
0002   This file is part of the kcalcore library.
0003 
0004   SPDX-FileCopyrightText: 2005-2007 David Jarvie <djarvie@kde.org>
0005 
0006   SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "icalformat.h"
0010 #include "icalformat_p.h"
0011 #include "icaltimezones_p.h"
0012 #include "recurrence.h"
0013 #include "recurrencehelper_p.h"
0014 #include "recurrencerule.h"
0015 
0016 #include "kcalendarcore_debug.h"
0017 
0018 #include <QByteArray>
0019 #include <QDateTime>
0020 
0021 extern "C" {
0022 #include <libical/ical.h>
0023 #include <libical/icaltimezone.h>
0024 }
0025 
0026 using namespace KCalendarCore;
0027 
0028 // Minimum repetition counts for VTIMEZONE RRULEs
0029 static const int minRuleCount = 5; // for any RRULE
0030 static const int minPhaseCount = 8; // for separate STANDARD/DAYLIGHT component
0031 
0032 // Convert an ical time to QDateTime, preserving the UTC indicator
0033 static QDateTime toQDateTime(const icaltimetype &t)
0034 {
0035     return QDateTime(QDate(t.year, t.month, t.day),
0036                      QTime(t.hour, t.minute, t.second),
0037                      (icaltime_is_utc(t) ? Qt::UTC : Qt::LocalTime));
0038 }
0039 
0040 // Maximum date for time zone data.
0041 // It's not sensible to try to predict them very far in advance, because
0042 // they can easily change. Plus, it limits the processing required.
0043 static QDateTime MAX_DATE()
0044 {
0045     static QDateTime dt;
0046     if (!dt.isValid()) {
0047         dt = QDateTime(QDate::currentDate().addYears(20), QTime(0, 0, 0));
0048     }
0049     return dt;
0050 }
0051 
0052 static icaltimetype writeLocalICalDateTime(const QDateTime &utc, int offset)
0053 {
0054     const QDateTime local = utc.addSecs(offset);
0055     icaltimetype t = icaltime_null_time();
0056     t.year = local.date().year();
0057     t.month = local.date().month();
0058     t.day = local.date().day();
0059     t.hour = local.time().hour();
0060     t.minute = local.time().minute();
0061     t.second = local.time().second();
0062     t.is_date = 0;
0063     t.zone = nullptr;
0064     return t;
0065 }
0066 
0067 namespace KCalendarCore
0068 {
0069 void ICalTimeZonePhase::dump()
0070 {
0071     qDebug() << "       ~~~ ICalTimeZonePhase ~~~";
0072     qDebug() << "       Abbreviations:" << abbrevs;
0073     qDebug() << "       UTC offset:" << utcOffset;
0074     qDebug() << "       Transitions:" << transitions;
0075     qDebug() << "       ~~~~~~~~~~~~~~~~~~~~~~~~~";
0076 }
0077 
0078 void ICalTimeZone::dump()
0079 {
0080     qDebug() << "~~~ ICalTimeZone ~~~";
0081     qDebug() << "ID:" << id;
0082     qDebug() << "QZONE:" << qZone.id();
0083     qDebug() << "STD:";
0084     standard.dump();
0085     qDebug() << "DST:";
0086     daylight.dump();
0087     qDebug() << "~~~~~~~~~~~~~~~~~~~~";
0088 }
0089 
0090 ICalTimeZoneCache::ICalTimeZoneCache()
0091 {
0092 }
0093 
0094 void ICalTimeZoneCache::insert(const QByteArray &id, const ICalTimeZone &tz)
0095 {
0096     mCache.insert(id, tz);
0097 }
0098 
0099 namespace
0100 {
0101 template<typename T>
0102 typename T::const_iterator greatestSmallerThan(const T &c, const typename T::value_type &v)
0103 {
0104     auto it = std::lower_bound(c.cbegin(), c.cend(), v);
0105     if (it != c.cbegin()) {
0106         return --it;
0107     }
0108     return c.cend();
0109 }
0110 
0111 }
0112 
0113 QTimeZone ICalTimeZoneCache::tzForTime(const QDateTime &dt, const QByteArray &tzid) const
0114 {
0115     if (QTimeZone::isTimeZoneIdAvailable(tzid)) {
0116         return QTimeZone(tzid);
0117     }
0118 
0119     const ICalTimeZone tz = mCache.value(tzid);
0120     if (!tz.qZone.isValid()) {
0121         return QTimeZone();
0122     }
0123 
0124     // If the matched timezone is one of the UTC offset timezones, we need to make
0125     // sure it's in the correct DTS.
0126     // The lookup in ICalTimeZoneParser will only find TZ in standard time, but
0127     // if the datetim in question fits in the DTS zone, we need to use another UTC
0128     // offset timezone
0129     if (tz.qZone.id().startsWith("UTC")) { // krazy:exclude=strings
0130         // Find the nearest standard and DST transitions that occur BEFORE the "dt"
0131         const auto stdPrev = greatestSmallerThan(tz.standard.transitions, dt);
0132         const auto dstPrev = greatestSmallerThan(tz.daylight.transitions, dt);
0133         if (stdPrev != tz.standard.transitions.cend() && dstPrev != tz.daylight.transitions.cend()) {
0134             if (*dstPrev > *stdPrev) {
0135                 // Previous DTS is closer to "dt" than previous standard, which
0136                 // means we are in DTS right now
0137                 const auto tzids = QTimeZone::availableTimeZoneIds(tz.daylight.utcOffset);
0138                 auto dtsTzId = std::find_if(tzids.cbegin(), tzids.cend(), [](const QByteArray &id) {
0139                     return id.startsWith("UTC"); // krazy:exclude=strings
0140                 });
0141                 if (dtsTzId != tzids.cend()) {
0142                     return QTimeZone(*dtsTzId);
0143                 }
0144             }
0145         }
0146     }
0147 
0148     return tz.qZone;
0149 }
0150 
0151 ICalTimeZoneParser::ICalTimeZoneParser(ICalTimeZoneCache *cache)
0152     : mCache(cache)
0153 {
0154 }
0155 
0156 void ICalTimeZoneParser::updateTzEarliestDate(const IncidenceBase::Ptr &incidence, TimeZoneEarliestDate *earliest)
0157 {
0158     for (auto role : {IncidenceBase::RoleStartTimeZone, IncidenceBase::RoleEndTimeZone}) {
0159         const auto dt = incidence->dateTime(role);
0160         if (dt.isValid()) {
0161             if (dt.timeZone() == QTimeZone::utc()) {
0162                 continue;
0163             }
0164             const auto prev = earliest->value(incidence->dtStart().timeZone());
0165             if (!prev.isValid() || incidence->dtStart() < prev) {
0166                 earliest->insert(incidence->dtStart().timeZone(), prev);
0167             }
0168         }
0169     }
0170 }
0171 
0172 icalcomponent *ICalTimeZoneParser::icalcomponentFromQTimeZone(const QTimeZone &tz, const QDateTime &earliest)
0173 {
0174     // VTIMEZONE RRULE types
0175     enum {
0176         DAY_OF_MONTH = 0x01,
0177         WEEKDAY_OF_MONTH = 0x02,
0178         LAST_WEEKDAY_OF_MONTH = 0x04,
0179     };
0180 
0181     // Write the time zone data into an iCal component
0182     icalcomponent *tzcomp = icalcomponent_new(ICAL_VTIMEZONE_COMPONENT);
0183     icalcomponent_add_property(tzcomp, icalproperty_new_tzid(tz.id().constData()));
0184     //    icalcomponent_add_property(tzcomp, icalproperty_new_location( tz.name().toUtf8() ));
0185 
0186     // Compile an ordered list of transitions so that we can know the phases
0187     // which occur before and after each transition.
0188     QTimeZone::OffsetDataList transits = tz.transitions(QDateTime(), MAX_DATE());
0189     if (transits.isEmpty()) {
0190         // If there is no way to compile a complete list of transitions
0191         // transitions() can return an empty list
0192         // In that case try get one transition to write a valid VTIMEZONE entry.
0193         qCDebug(KCALCORE_LOG) << "No transition information available VTIMEZONE will be invalid.";
0194     }
0195     if (earliest.isValid()) {
0196         // Remove all transitions earlier than those we are interested in
0197         for (int i = 0, end = transits.count(); i < end; ++i) {
0198             if (transits.at(i).atUtc >= earliest) {
0199                 if (i > 0) {
0200                     transits.erase(transits.begin(), transits.begin() + i);
0201                 }
0202                 break;
0203             }
0204         }
0205     }
0206     int trcount = transits.count();
0207     QVector<bool> transitionsDone(trcount, false);
0208 
0209     // Go through the list of transitions and create an iCal component for each
0210     // distinct combination of phase after and UTC offset before the transition.
0211     icaldatetimeperiodtype dtperiod;
0212     dtperiod.period = icalperiodtype_null_period();
0213     for (;;) {
0214         int i = 0;
0215         for (; i < trcount && transitionsDone[i]; ++i) {
0216             ;
0217         }
0218         if (i >= trcount) {
0219             break;
0220         }
0221         // Found a phase combination which hasn't yet been processed
0222         const int preOffset = (i > 0) ? transits.at(i - 1).offsetFromUtc : 0;
0223         const auto &transit = transits.at(i);
0224         if (transit.offsetFromUtc == preOffset) {
0225             transitionsDone[i] = true;
0226             while (++i < trcount) {
0227                 if (transitionsDone[i] || transits.at(i).offsetFromUtc != transit.offsetFromUtc
0228                     || transits.at(i).daylightTimeOffset != transit.daylightTimeOffset || transits.at(i - 1).offsetFromUtc != preOffset) {
0229                     continue;
0230                 }
0231                 transitionsDone[i] = true;
0232             }
0233             continue;
0234         }
0235         const bool isDst = transit.daylightTimeOffset > 0;
0236         icalcomponent *phaseComp = icalcomponent_new(isDst ? ICAL_XDAYLIGHT_COMPONENT : ICAL_XSTANDARD_COMPONENT);
0237         if (!transit.abbreviation.isEmpty()) {
0238             icalcomponent_add_property(phaseComp, icalproperty_new_tzname(static_cast<const char *>(transit.abbreviation.toUtf8().constData())));
0239         }
0240         icalcomponent_add_property(phaseComp, icalproperty_new_tzoffsetfrom(preOffset));
0241         icalcomponent_add_property(phaseComp, icalproperty_new_tzoffsetto(transit.offsetFromUtc));
0242         // Create a component to hold initial RRULE if any, plus all RDATEs
0243         icalcomponent *phaseComp1 = icalcomponent_new_clone(phaseComp);
0244         icalcomponent_add_property(phaseComp1, icalproperty_new_dtstart(writeLocalICalDateTime(transits.at(i).atUtc, preOffset)));
0245         bool useNewRRULE = false;
0246 
0247         // Compile the list of UTC transition dates/times, and check
0248         // if the list can be reduced to an RRULE instead of multiple RDATEs.
0249         QTime time;
0250         QDate date;
0251         int year = 0;
0252         int month = 0;
0253         int daysInMonth = 0;
0254         int dayOfMonth = 0; // avoid compiler warnings
0255         int dayOfWeek = 0; // Monday = 1
0256         int nthFromStart = 0; // nth (weekday) of month
0257         int nthFromEnd = 0; // nth last (weekday) of month
0258         int newRule;
0259         int rule = 0;
0260         QList<QDateTime> rdates; // dates which (probably) need to be written as RDATEs
0261         QList<QDateTime> times;
0262         QDateTime qdt = transits.at(i).atUtc; // set 'qdt' for start of loop
0263         times += qdt;
0264         transitionsDone[i] = true;
0265         do {
0266             if (!rule) {
0267                 // Initialise data for detecting a new rule
0268                 rule = DAY_OF_MONTH | WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH;
0269                 time = qdt.time();
0270                 date = qdt.date();
0271                 year = date.year();
0272                 month = date.month();
0273                 daysInMonth = date.daysInMonth();
0274                 dayOfWeek = date.dayOfWeek(); // Monday = 1
0275                 dayOfMonth = date.day();
0276                 nthFromStart = (dayOfMonth - 1) / 7 + 1; // nth (weekday) of month
0277                 nthFromEnd = (daysInMonth - dayOfMonth) / 7 + 1; // nth last (weekday) of month
0278             }
0279             if (++i >= trcount) {
0280                 newRule = 0;
0281                 times += QDateTime(); // append a dummy value since last value in list is ignored
0282             } else {
0283                 if (transitionsDone[i] || transits.at(i).offsetFromUtc != transit.offsetFromUtc
0284                     || transits.at(i).daylightTimeOffset != transit.daylightTimeOffset || transits.at(i - 1).offsetFromUtc != preOffset) {
0285                     continue;
0286                 }
0287                 transitionsDone[i] = true;
0288                 qdt = transits.at(i).atUtc;
0289                 if (!qdt.isValid()) {
0290                     continue;
0291                 }
0292                 newRule = rule;
0293                 times += qdt;
0294                 date = qdt.date();
0295                 if (qdt.time() != time || date.month() != month || date.year() != ++year) {
0296                     newRule = 0;
0297                 } else {
0298                     const int day = date.day();
0299                     if ((newRule & DAY_OF_MONTH) && day != dayOfMonth) {
0300                         newRule &= ~DAY_OF_MONTH;
0301                     }
0302                     if (newRule & (WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH)) {
0303                         if (date.dayOfWeek() != dayOfWeek) {
0304                             newRule &= ~(WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH);
0305                         } else {
0306                             if ((newRule & WEEKDAY_OF_MONTH) && (day - 1) / 7 + 1 != nthFromStart) {
0307                                 newRule &= ~WEEKDAY_OF_MONTH;
0308                             }
0309                             if ((newRule & LAST_WEEKDAY_OF_MONTH) && (daysInMonth - day) / 7 + 1 != nthFromEnd) {
0310                                 newRule &= ~LAST_WEEKDAY_OF_MONTH;
0311                             }
0312                         }
0313                     }
0314                 }
0315             }
0316             if (!newRule) {
0317                 // The previous rule (if any) no longer applies.
0318                 // Write all the times up to but not including the current one.
0319                 // First check whether any of the last RDATE values fit this rule.
0320                 int yr = times[0].date().year();
0321                 while (!rdates.isEmpty()) {
0322                     qdt = rdates.last();
0323                     date = qdt.date();
0324                     if (qdt.time() != time || date.month() != month || date.year() != --yr) {
0325                         break;
0326                     }
0327                     const int day = date.day();
0328                     if (rule & DAY_OF_MONTH) {
0329                         if (day != dayOfMonth) {
0330                             break;
0331                         }
0332                     } else {
0333                         if (date.dayOfWeek() != dayOfWeek || ((rule & WEEKDAY_OF_MONTH) && (day - 1) / 7 + 1 != nthFromStart)
0334                             || ((rule & LAST_WEEKDAY_OF_MONTH) && (daysInMonth - day) / 7 + 1 != nthFromEnd)) {
0335                             break;
0336                         }
0337                     }
0338                     times.prepend(qdt);
0339                     rdates.pop_back();
0340                 }
0341                 if (times.count() > (useNewRRULE ? minPhaseCount : minRuleCount)) {
0342                     // There are enough dates to combine into an RRULE
0343                     icalrecurrencetype r;
0344                     icalrecurrencetype_clear(&r);
0345                     r.freq = ICAL_YEARLY_RECURRENCE;
0346                     r.by_month[0] = month;
0347                     if (rule & DAY_OF_MONTH) {
0348                         r.by_month_day[0] = dayOfMonth;
0349                     } else if (rule & WEEKDAY_OF_MONTH) {
0350                         r.by_day[0] = (dayOfWeek % 7 + 1) + (nthFromStart * 8); // Sunday = 1
0351                     } else if (rule & LAST_WEEKDAY_OF_MONTH) {
0352                         r.by_day[0] = -(dayOfWeek % 7 + 1) - (nthFromEnd * 8); // Sunday = 1
0353                     }
0354                     r.until = writeLocalICalDateTime(times.takeAt(times.size() - 1), preOffset);
0355                     icalproperty *prop = icalproperty_new_rrule(r);
0356                     if (useNewRRULE) {
0357                         // This RRULE doesn't start from the phase start date, so set it into
0358                         // a new STANDARD/DAYLIGHT component in the VTIMEZONE.
0359                         icalcomponent *c = icalcomponent_new_clone(phaseComp);
0360                         icalcomponent_add_property(c, icalproperty_new_dtstart(writeLocalICalDateTime(times[0], preOffset)));
0361                         icalcomponent_add_property(c, prop);
0362                         icalcomponent_add_component(tzcomp, c);
0363                     } else {
0364                         icalcomponent_add_property(phaseComp1, prop);
0365                     }
0366                 } else {
0367                     // Save dates for writing as RDATEs
0368                     for (int t = 0, tend = times.count() - 1; t < tend; ++t) {
0369                         rdates += times[t];
0370                     }
0371                 }
0372                 useNewRRULE = true;
0373                 // All date/time values but the last have been added to the VTIMEZONE.
0374                 // Remove them from the list.
0375                 qdt = times.last(); // set 'qdt' for start of loop
0376                 times.clear();
0377                 times += qdt;
0378             }
0379             rule = newRule;
0380         } while (i < trcount);
0381 
0382         // Write remaining dates as RDATEs
0383         for (int rd = 0, rdend = rdates.count(); rd < rdend; ++rd) {
0384             dtperiod.time = writeLocalICalDateTime(rdates[rd], preOffset);
0385             icalcomponent_add_property(phaseComp1, icalproperty_new_rdate(dtperiod));
0386         }
0387         icalcomponent_add_component(tzcomp, phaseComp1);
0388         icalcomponent_free(phaseComp);
0389     }
0390 
0391     return tzcomp;
0392 }
0393 
0394 icaltimezone *ICalTimeZoneParser::icaltimezoneFromQTimeZone(const QTimeZone &tz, const QDateTime &earliest)
0395 {
0396     auto itz = icaltimezone_new();
0397     icaltimezone_set_component(itz, icalcomponentFromQTimeZone(tz, earliest));
0398     return itz;
0399 }
0400 
0401 void ICalTimeZoneParser::parse(icalcomponent *calendar)
0402 {
0403     for (auto *c = icalcomponent_get_first_component(calendar, ICAL_VTIMEZONE_COMPONENT); c;
0404          c = icalcomponent_get_next_component(calendar, ICAL_VTIMEZONE_COMPONENT)) {
0405         auto icalZone = parseTimeZone(c);
0406         // icalZone.dump();
0407         if (!icalZone.id.isEmpty()) {
0408             if (!icalZone.qZone.isValid()) {
0409                 icalZone.qZone = resolveICalTimeZone(icalZone);
0410             }
0411             if (!icalZone.qZone.isValid()) {
0412                 qCWarning(KCALCORE_LOG) << "Failed to map" << icalZone.id << "to a known IANA timezone";
0413                 continue;
0414             }
0415             mCache->insert(icalZone.id, icalZone);
0416         }
0417     }
0418 }
0419 
0420 QTimeZone ICalTimeZoneParser::resolveICalTimeZone(const ICalTimeZone &icalZone)
0421 {
0422     const auto phase = icalZone.standard;
0423     const auto now = QDateTime::currentDateTimeUtc();
0424 
0425     const auto candidates = QTimeZone::availableTimeZoneIds(phase.utcOffset);
0426     QMap<int, QTimeZone> matchedCandidates;
0427     for (const auto &tzid : candidates) {
0428         const QTimeZone candidate(tzid);
0429         // This would be a fallback, candidate has transitions, but the phase does not
0430         if (candidate.hasTransitions() == phase.transitions.isEmpty()) {
0431             matchedCandidates.insert(0, candidate);
0432             continue;
0433         }
0434 
0435         // Without transitions, we can't do any more precise matching, so just
0436         // accept this candidate and be done with it
0437         if (!candidate.hasTransitions() && phase.transitions.isEmpty()) {
0438             return candidate;
0439         }
0440 
0441         // Calculate how many transitions this candidate shares with the phase.
0442         // The candidate with the most matching transitions will win.
0443         auto begin = std::lower_bound(phase.transitions.cbegin(), phase.transitions.cend(), now.addYears(-20));
0444         // If no transition older than 20 years is found, we will start from beginning
0445         if (begin == phase.transitions.cend()) {
0446             begin = phase.transitions.cbegin();
0447         }
0448         auto end = std::upper_bound(begin, phase.transitions.cend(), now);
0449         int matchedTransitions = 0;
0450         for (auto it = begin; it != end; ++it) {
0451             const auto &transition = *it;
0452             const QTimeZone::OffsetDataList candidateTransitions = candidate.transitions(transition, transition);
0453             if (candidateTransitions.isEmpty()) {
0454                 continue;
0455             }
0456             ++matchedTransitions; // 1 point for a matching transition
0457             const auto candidateTransition = candidateTransitions[0];
0458             // FIXME: THIS IS HOW IT SHOULD BE:
0459             // const auto abvs = transition.abbreviations();
0460             const auto abvs = phase.abbrevs;
0461             for (const auto &abv : abvs) {
0462                 if (candidateTransition.abbreviation == QString::fromUtf8(abv)) {
0463                     matchedTransitions += 1024; // lots of points for a transition with a matching abbreviation
0464                     break;
0465                 }
0466             }
0467         }
0468         matchedCandidates.insert(matchedTransitions, candidate);
0469     }
0470 
0471     if (!matchedCandidates.isEmpty()) {
0472         return matchedCandidates.value(matchedCandidates.lastKey());
0473     }
0474 
0475     return {};
0476 }
0477 
0478 ICalTimeZone ICalTimeZoneParser::parseTimeZone(icalcomponent *vtimezone)
0479 {
0480     ICalTimeZone icalTz;
0481 
0482     if (auto tzidProp = icalcomponent_get_first_property(vtimezone, ICAL_TZID_PROPERTY)) {
0483         icalTz.id = icalproperty_get_value_as_string(tzidProp);
0484 
0485         // If the VTIMEZONE is a known IANA time zone don't bother parsing the rest
0486         // of the VTIMEZONE, get QTimeZone directly from Qt
0487         if (QTimeZone::isTimeZoneIdAvailable(icalTz.id) || icalTz.id.startsWith("UTC")) {
0488             icalTz.qZone = QTimeZone(icalTz.id);
0489             return icalTz;
0490         } else {
0491             // Not IANA, but maybe we can match it from Windows ID?
0492             const auto ianaTzid = QTimeZone::windowsIdToDefaultIanaId(icalTz.id);
0493             if (!ianaTzid.isEmpty()) {
0494                 icalTz.qZone = QTimeZone(ianaTzid);
0495                 return icalTz;
0496             }
0497         }
0498     }
0499 
0500     for (icalcomponent *c = icalcomponent_get_first_component(vtimezone, ICAL_ANY_COMPONENT); c;
0501          c = icalcomponent_get_next_component(vtimezone, ICAL_ANY_COMPONENT)) {
0502         icalcomponent_kind kind = icalcomponent_isa(c);
0503         switch (kind) {
0504         case ICAL_XSTANDARD_COMPONENT:
0505             // qCDebug(KCALCORE_LOG) << "---standard phase: found";
0506             parsePhase(c, false, icalTz.standard);
0507             break;
0508         case ICAL_XDAYLIGHT_COMPONENT:
0509             // qCDebug(KCALCORE_LOG) << "---daylight phase: found";
0510             parsePhase(c, true, icalTz.daylight);
0511             break;
0512 
0513         default:
0514             qCDebug(KCALCORE_LOG) << "Unknown component:" << int(kind);
0515             break;
0516         }
0517     }
0518 
0519     return icalTz;
0520 }
0521 
0522 bool ICalTimeZoneParser::parsePhase(icalcomponent *c, bool daylight, ICalTimeZonePhase &phase)
0523 {
0524     // Read the observance data for this standard/daylight savings phase
0525     int utcOffset = 0;
0526     int prevOffset = 0;
0527     bool recurs = false;
0528     bool found_dtstart = false;
0529     bool found_tzoffsetfrom = false;
0530     bool found_tzoffsetto = false;
0531     icaltimetype dtstart = icaltime_null_time();
0532     QSet<QByteArray> abbrevs;
0533 
0534     // Now do the ical reading.
0535     icalproperty *p = icalcomponent_get_first_property(c, ICAL_ANY_PROPERTY);
0536     while (p) {
0537         icalproperty_kind kind = icalproperty_isa(p);
0538         switch (kind) {
0539         case ICAL_TZNAME_PROPERTY: { // abbreviated name for this time offset
0540             // TZNAME can appear multiple times in order to provide language
0541             // translations of the time zone offset name.
0542 
0543             // TODO: Does this cope with multiple language specifications?
0544             QByteArray name = icalproperty_get_tzname(p);
0545             // Outlook (2000) places "Standard Time" and "Daylight Time" in the TZNAME
0546             // strings, which is totally useless. So ignore those.
0547             if ((!daylight && name == "Standard Time") || (daylight && name == "Daylight Time")) {
0548                 break;
0549             }
0550             abbrevs.insert(name);
0551             break;
0552         }
0553         case ICAL_DTSTART_PROPERTY: // local time at which phase starts
0554             dtstart = icalproperty_get_dtstart(p);
0555             found_dtstart = true;
0556             break;
0557 
0558         case ICAL_TZOFFSETFROM_PROPERTY: // UTC offset immediately before start of phase
0559             prevOffset = icalproperty_get_tzoffsetfrom(p);
0560             found_tzoffsetfrom = true;
0561             break;
0562 
0563         case ICAL_TZOFFSETTO_PROPERTY:
0564             utcOffset = icalproperty_get_tzoffsetto(p);
0565             found_tzoffsetto = true;
0566             break;
0567 
0568         case ICAL_RDATE_PROPERTY:
0569         case ICAL_RRULE_PROPERTY:
0570             recurs = true;
0571             break;
0572 
0573         default:
0574             break;
0575         }
0576         p = icalcomponent_get_next_property(c, ICAL_ANY_PROPERTY);
0577     }
0578 
0579     // Validate the phase data
0580     if (!found_dtstart || !found_tzoffsetfrom || !found_tzoffsetto) {
0581         qCDebug(KCALCORE_LOG) << "DTSTART/TZOFFSETFROM/TZOFFSETTO missing";
0582         return false;
0583     }
0584 
0585     // Convert DTSTART to QDateTime, and from local time to UTC
0586     dtstart.second -= prevOffset;
0587     dtstart = icaltime_convert_to_zone(dtstart, icaltimezone_get_utc_timezone());
0588     const QDateTime utcStart = toQDateTime(icaltime_normalize(dtstart)); // UTC
0589 
0590     phase.abbrevs.unite(abbrevs);
0591     phase.utcOffset = utcOffset;
0592     phase.transitions += utcStart;
0593 
0594     if (recurs) {
0595         /* RDATE or RRULE is specified. There should only be one or the other, but
0596          * it doesn't really matter - the code can cope with both.
0597          * Note that we had to get DTSTART, TZOFFSETFROM, TZOFFSETTO before reading
0598          * recurrences.
0599          */
0600         const QDateTime maxTime(MAX_DATE());
0601         Recurrence recur;
0602         icalproperty *p = icalcomponent_get_first_property(c, ICAL_ANY_PROPERTY);
0603         while (p) {
0604             icalproperty_kind kind = icalproperty_isa(p);
0605             switch (kind) {
0606             case ICAL_RDATE_PROPERTY: {
0607                 icaltimetype t = icalproperty_get_rdate(p).time;
0608                 if (icaltime_is_date(t)) {
0609                     // RDATE with a DATE value inherits the (local) time from DTSTART
0610                     t.hour = dtstart.hour;
0611                     t.minute = dtstart.minute;
0612                     t.second = dtstart.second;
0613                     t.is_date = 0;
0614                 }
0615                 // RFC2445 states that RDATE must be in local time,
0616                 // but we support UTC as well to be safe.
0617                 if (!icaltime_is_utc(t)) {
0618                     t.second -= prevOffset; // convert to UTC
0619                     t = icaltime_convert_to_zone(t, icaltimezone_get_utc_timezone());
0620                     t = icaltime_normalize(t);
0621                 }
0622                 phase.transitions += toQDateTime(t);
0623                 break;
0624             }
0625             case ICAL_RRULE_PROPERTY: {
0626                 RecurrenceRule r;
0627                 ICalFormat icf;
0628                 ICalFormatImpl impl(&icf);
0629                 impl.readRecurrence(icalproperty_get_rrule(p), &r);
0630                 r.setStartDt(utcStart);
0631                 // The end date time specified in an RRULE must be in UTC.
0632                 // We can not guarantee correctness if this is not the case.
0633                 if (r.duration() == 0 && r.endDt().timeSpec() != Qt::UTC) {
0634                   qCWarning(KCALCORE_LOG) << "UNTIL in RRULE must be specified in UTC";
0635                   break;
0636                 }
0637                 const auto dts = r.timesInInterval(utcStart, maxTime);
0638                 for (int i = 0, end = dts.count(); i < end; ++i) {
0639                     phase.transitions += dts[i];
0640                 }
0641                 break;
0642             }
0643             default:
0644                 break;
0645             }
0646             p = icalcomponent_get_next_property(c, ICAL_ANY_PROPERTY);
0647         }
0648         sortAndRemoveDuplicates(phase.transitions);
0649     }
0650 
0651     return true;
0652 }
0653 
0654 QByteArray ICalTimeZoneParser::vcaltimezoneFromQTimeZone(const QTimeZone &qtz, const QDateTime &earliest)
0655 {
0656     auto icalTz = icalcomponentFromQTimeZone(qtz, earliest);
0657     const QByteArray result(icalcomponent_as_ical_string(icalTz));
0658     icalmemory_free_ring();
0659     icalcomponent_free(icalTz);
0660     return result;
0661 }
0662 
0663 } // namespace KCalendarCore