File indexing completed on 2024-04-21 03:52:49

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