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