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