/* This file is part of the kcalcore library. Copyright (c) 2005-2007 David Jarvie This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include "icaltimezones.h" #include "icalformat.h" #include "icalformat_p.h" #include "recurrence.h" #include "recurrencerule.h" #include #include #include #include #include #include extern "C" { #include #include } #if defined(HAVE_UUID_UUID_H) #include #endif using namespace KCalCore; // Minimum repetition counts for VTIMEZONE RRULEs static const int minRuleCount = 5; // for any RRULE static const int minPhaseCount = 8; // for separate STANDARD/DAYLIGHT component // Convert an ical time to QDateTime, preserving the UTC indicator static QDateTime toQDateTime(const icaltimetype &t) { return QDateTime(QDate(t.year, t.month, t.day), QTime(t.hour, t.minute, t.second), (t.is_utc ? Qt::UTC : Qt::LocalTime)); } // Maximum date for time zone data. // It's not sensible to try to predict them very far in advance, because // they can easily change. Plus, it limits the processing required. static QDateTime MAX_DATE() { static QDateTime dt; if (!dt.isValid()) { dt = QDateTime(QDate::currentDate().addYears(20), QTime(0, 0, 0)); } return dt; } static icaltimetype writeLocalICalDateTime(const QDateTime &utc, int offset) { const QDateTime local = utc.addSecs(offset); icaltimetype t = icaltime_null_time(); t.year = local.date().year(); t.month = local.date().month(); t.day = local.date().day(); t.hour = local.time().hour(); t.minute = local.time().minute(); t.second = local.time().second(); t.is_date = 0; t.zone = 0; t.is_utc = 0; return t; } namespace KCalCore { /******************************************************************************/ //@cond PRIVATE class ICalTimeZonesPrivate { public: ICalTimeZonesPrivate() {} ICalTimeZones::ZoneMap zones; }; //@endcond ICalTimeZones::ICalTimeZones() : d(new ICalTimeZonesPrivate) { } ICalTimeZones::ICalTimeZones(const ICalTimeZones &rhs) : d(new ICalTimeZonesPrivate()) { d->zones = rhs.d->zones; } ICalTimeZones &ICalTimeZones::operator=(const ICalTimeZones &rhs) { // check for self assignment if (&rhs == this) { return *this; } d->zones = rhs.d->zones; return *this; } ICalTimeZones::~ICalTimeZones() { delete d; } const ICalTimeZones::ZoneMap ICalTimeZones::zones() const { return d->zones; } bool ICalTimeZones::add(const ICalTimeZone &zone) { if (!zone.isValid()) { return false; } if (d->zones.find(zone.name()) != d->zones.end()) { return false; // name already exists } d->zones.insert(zone.name(), zone); return true; } ICalTimeZone ICalTimeZones::remove(const ICalTimeZone &zone) { if (zone.isValid()) { for (ZoneMap::Iterator it = d->zones.begin(), end = d->zones.end(); it != end; ++it) { if (it.value() == zone) { d->zones.erase(it); return (zone == ICalTimeZone::utc()) ? ICalTimeZone() : zone; } } } return ICalTimeZone(); } ICalTimeZone ICalTimeZones::remove(const QString &name) { if (!name.isEmpty()) { ZoneMap::Iterator it = d->zones.find(name); if (it != d->zones.end()) { const ICalTimeZone zone = it.value(); d->zones.erase(it); return (zone == ICalTimeZone::utc()) ? ICalTimeZone() : zone; } } return ICalTimeZone(); } void ICalTimeZones::clear() { d->zones.clear(); } int ICalTimeZones::count() { return d->zones.count(); } ICalTimeZone ICalTimeZones::zone(const QString &name) const { if (!name.isEmpty()) { ZoneMap::ConstIterator it = d->zones.constFind(name); if (it != d->zones.constEnd()) { return it.value(); } } return ICalTimeZone(); // error } ICalTimeZone ICalTimeZones::zone(const ICalTimeZone &zone) const { if (zone.isValid()) { QMapIterator it(d->zones); while (it.hasNext()) { it.next(); const ICalTimeZone tz = it.value(); const QList list1 = tz.transitions(); const QList list2 = zone.transitions(); if (list1.size() == list2.size()) { int i = 0; int matches = 0; for (; i < list1.size(); ++i) { const KTimeZone::Transition t1 = list1[ i ]; const KTimeZone::Transition t2 = list2[ i ]; if ((t1.time() == t2.time()) && (t1.phase().utcOffset() == t2.phase().utcOffset()) && (t1.phase().isDst() == t2.phase().isDst())) { matches++; } } if (matches == i) { // Existing zone has all the transitions of the given zone. return tz; } } } } return ICalTimeZone(); // not found } /******************************************************************************/ ICalTimeZoneBackend::ICalTimeZoneBackend() : KTimeZoneBackend() {} ICalTimeZoneBackend::ICalTimeZoneBackend(ICalTimeZoneSource *source, const QString &name, const QString &countryCode, float latitude, float longitude, const QString &comment) : KTimeZoneBackend(source, name, countryCode, latitude, longitude, comment) {} ICalTimeZoneBackend::ICalTimeZoneBackend(const KTimeZone &tz, const QDate &earliest) : KTimeZoneBackend(0, tz.name(), tz.countryCode(), tz.latitude(), tz.longitude(), tz.comment()) { Q_UNUSED(earliest); } ICalTimeZoneBackend::~ICalTimeZoneBackend() {} KTimeZoneBackend *ICalTimeZoneBackend::clone() const { return new ICalTimeZoneBackend(*this); } QByteArray ICalTimeZoneBackend::type() const { return "ICalTimeZone"; } bool ICalTimeZoneBackend::hasTransitions(const KTimeZone *caller) const { Q_UNUSED(caller); return true; } void ICalTimeZoneBackend::virtual_hook(int id, void *data) { Q_UNUSED(id); Q_UNUSED(data); } /******************************************************************************/ ICalTimeZone::ICalTimeZone() : KTimeZone(new ICalTimeZoneBackend()) {} ICalTimeZone::ICalTimeZone(ICalTimeZoneSource *source, const QString &name, ICalTimeZoneData *data) : KTimeZone(new ICalTimeZoneBackend(source, name)) { setData(data); } ICalTimeZone::ICalTimeZone(const KTimeZone &tz, const QDate &earliest) : KTimeZone(new ICalTimeZoneBackend(0, tz.name(), tz.countryCode(), tz.latitude(), tz.longitude(), tz.comment())) { const KTimeZoneData *data = tz.data(true); if (data) { const ICalTimeZoneData *icaldata = dynamic_cast(data); if (icaldata) { setData(new ICalTimeZoneData(*icaldata)); } else { setData(new ICalTimeZoneData(*data, tz, earliest)); } } } ICalTimeZone::~ICalTimeZone() {} QString ICalTimeZone::city() const { const ICalTimeZoneData *dat = static_cast(data()); return dat ? dat->city() : QString(); } QByteArray ICalTimeZone::url() const { const ICalTimeZoneData *dat = static_cast(data()); return dat ? dat->url() : QByteArray(); } QDateTime ICalTimeZone::lastModified() const { const ICalTimeZoneData *dat = static_cast(data()); return dat ? dat->lastModified() : QDateTime(); } QByteArray ICalTimeZone::vtimezone() const { const ICalTimeZoneData *dat = static_cast(data()); return dat ? dat->vtimezone() : QByteArray(); } icaltimezone *ICalTimeZone::icalTimezone() const { const ICalTimeZoneData *dat = static_cast(data()); return dat ? dat->icalTimezone() : 0; } bool ICalTimeZone::update(const ICalTimeZone &other) { if (!updateBase(other)) { return false; } KTimeZoneData *otherData = other.data() ? other.data()->clone() : 0; setData(otherData, other.source()); return true; } ICalTimeZone ICalTimeZone::utc() { static ICalTimeZone utcZone; if (!utcZone.isValid()) { ICalTimeZoneSource tzs; utcZone = tzs.parse(icaltimezone_get_utc_timezone()); } return utcZone; } void ICalTimeZone::virtual_hook(int id, void *data) { Q_UNUSED(id); Q_UNUSED(data); } /******************************************************************************/ //@cond PRIVATE class ICalTimeZoneDataPrivate { public: ICalTimeZoneDataPrivate() : icalComponent(0) {} ~ICalTimeZoneDataPrivate() { if (icalComponent) { icalcomponent_free(icalComponent); } } icalcomponent *component() const { return icalComponent; } void setComponent(icalcomponent *c) { if (icalComponent) { icalcomponent_free(icalComponent); } icalComponent = c; } QString location; // name of city for this time zone QByteArray url; // URL of published VTIMEZONE definition (optional) QDateTime lastModified; // time of last modification of the VTIMEZONE component (optional) private: icalcomponent *icalComponent; // ical component representing this time zone }; //@endcond ICalTimeZoneData::ICalTimeZoneData() : d(new ICalTimeZoneDataPrivate()) { } ICalTimeZoneData::ICalTimeZoneData(const ICalTimeZoneData &rhs) : KTimeZoneData(rhs), d(new ICalTimeZoneDataPrivate()) { d->location = rhs.d->location; d->url = rhs.d->url; d->lastModified = rhs.d->lastModified; d->setComponent(icalcomponent_new_clone(rhs.d->component())); } ICalTimeZoneData::ICalTimeZoneData(const KTimeZoneData &rhs, const KTimeZone &tz, const QDate &earliest) : KTimeZoneData(rhs), d(new ICalTimeZoneDataPrivate()) { // VTIMEZONE RRULE types enum { DAY_OF_MONTH = 0x01, WEEKDAY_OF_MONTH = 0x02, LAST_WEEKDAY_OF_MONTH = 0x04 }; if (tz.type() == "KSystemTimeZone") { // Try to fetch a system time zone in preference, on the grounds // that system time zones are more likely to be up to date than // built-in libical ones. icalcomponent *c = 0; const KTimeZone ktz = KSystemTimeZones::readZone(tz.name()); if (ktz.isValid()) { if (ktz.data(true)) { const ICalTimeZone icaltz(ktz, earliest); icaltimezone *itz = icaltz.icalTimezone(); if (itz) { c = icalcomponent_new_clone(icaltimezone_get_component(itz)); icaltimezone_free(itz, 1); } } } if (!c) { // Try to fetch a built-in libical time zone. icaltimezone *itz = icaltimezone_get_builtin_timezone(tz.name().toUtf8()); c = icalcomponent_new_clone(icaltimezone_get_component(itz)); } if (c) { // TZID in built-in libical time zones has a standard prefix. // To make the VTIMEZONE TZID match TZID references in incidences // (as required by RFC2445), strip off the prefix. icalproperty *prop = icalcomponent_get_first_property(c, ICAL_TZID_PROPERTY); if (prop) { icalvalue *value = icalproperty_get_value(prop); const char *tzid = icalvalue_get_text(value); const QByteArray icalprefix = ICalTimeZoneSource::icalTzidPrefix(); const int len = icalprefix.size(); if (!strncmp(icalprefix, tzid, len)) { const char *s = strchr(tzid + len, '/'); // find third '/' if (s) { const QByteArray tzidShort(s + 1); // deep copy (needed by icalvalue_set_text()) icalvalue_set_text(value, tzidShort); // Remove the X-LIC-LOCATION property, which is only used by libical prop = icalcomponent_get_first_property(c, ICAL_X_PROPERTY); const char *xname = icalproperty_get_x_name(prop); if (xname && !strcmp(xname, "X-LIC-LOCATION")) { icalcomponent_remove_property(c, prop); icalproperty_free(prop); } } } } } d->setComponent(c); } else { // Write the time zone data into an iCal component icalcomponent *tzcomp = icalcomponent_new(ICAL_VTIMEZONE_COMPONENT); icalcomponent_add_property(tzcomp, icalproperty_new_tzid(tz.name().toUtf8())); // icalcomponent_add_property(tzcomp, icalproperty_new_location( tz.name().toUtf8() )); // Compile an ordered list of transitions so that we can know the phases // which occur before and after each transition. QList transits = transitions(); if (transits.isEmpty()) { // If there is no way to compile a complete list of transitions // transitions() can return an empty list // In that case try get one transition to write a valid VTIMEZONE entry. if (transits.isEmpty()) { kDebug() << "No transition information available VTIMEZONE will be invalid."; } } if (earliest.isValid()) { // Remove all transitions earlier than those we are interested in for (int i = 0, end = transits.count(); i < end; ++i) { if (transits.at(i).time().date() >= earliest) { if (i > 0) { transits.erase(transits.begin(), transits.begin() + i); } break; } } } int trcount = transits.count(); QVector transitionsDone(trcount); transitionsDone.fill(false); // Go through the list of transitions and create an iCal component for each // distinct combination of phase after and UTC offset before the transition. icaldatetimeperiodtype dtperiod; dtperiod.period = icalperiodtype_null_period(); for (; ;) { int i = 0; for (; i < trcount && transitionsDone[i]; ++i) { ; } if (i >= trcount) { break; } // Found a phase combination which hasn't yet been processed const int preOffset = (i > 0) ? transits.at(i - 1).phase().utcOffset() : rhs.previousUtcOffset(); const KTimeZone::Phase phase = transits.at(i).phase(); if (phase.utcOffset() == preOffset) { transitionsDone[i] = true; while (++i < trcount) { if (transitionsDone[i] || transits.at(i).phase() != phase || transits.at(i - 1).phase().utcOffset() != preOffset) { continue; } transitionsDone[i] = true; } continue; } icalcomponent *phaseComp = icalcomponent_new(phase.isDst() ? ICAL_XDAYLIGHT_COMPONENT : ICAL_XSTANDARD_COMPONENT); const QList abbrevs = phase.abbreviations(); for (int a = 0, aend = abbrevs.count(); a < aend; ++a) { icalcomponent_add_property(phaseComp, icalproperty_new_tzname( static_cast(abbrevs[a]))); } if (!phase.comment().isEmpty()) { icalcomponent_add_property(phaseComp, icalproperty_new_comment(phase.comment().toUtf8())); } icalcomponent_add_property(phaseComp, icalproperty_new_tzoffsetfrom(preOffset)); icalcomponent_add_property(phaseComp, icalproperty_new_tzoffsetto(phase.utcOffset())); // Create a component to hold initial RRULE if any, plus all RDATEs icalcomponent *phaseComp1 = icalcomponent_new_clone(phaseComp); icalcomponent_add_property(phaseComp1, icalproperty_new_dtstart( writeLocalICalDateTime(transits.at(i).time(), preOffset))); bool useNewRRULE = false; // Compile the list of UTC transition dates/times, and check // if the list can be reduced to an RRULE instead of multiple RDATEs. QTime time; QDate date; int year = 0, month = 0, daysInMonth = 0, dayOfMonth = 0; // avoid compiler warnings int dayOfWeek = 0; // Monday = 1 int nthFromStart = 0; // nth (weekday) of month int nthFromEnd = 0; // nth last (weekday) of month int newRule; int rule = 0; QList rdates;// dates which (probably) need to be written as RDATEs QList times; QDateTime qdt = transits.at(i).time(); // set 'qdt' for start of loop times += qdt; transitionsDone[i] = true; do { if (!rule) { // Initialise data for detecting a new rule rule = DAY_OF_MONTH | WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH; time = qdt.time(); date = qdt.date(); year = date.year(); month = date.month(); daysInMonth = date.daysInMonth(); dayOfWeek = date.dayOfWeek(); // Monday = 1 dayOfMonth = date.day(); nthFromStart = (dayOfMonth - 1) / 7 + 1; // nth (weekday) of month nthFromEnd = (daysInMonth - dayOfMonth) / 7 + 1; // nth last (weekday) of month } if (++i >= trcount) { newRule = 0; times += QDateTime(); // append a dummy value since last value in list is ignored } else { if (transitionsDone[i] || transits.at(i).phase() != phase || transits.at(i - 1).phase().utcOffset() != preOffset) { continue; } transitionsDone[i] = true; qdt = transits.at(i).time(); if (!qdt.isValid()) { continue; } newRule = rule; times += qdt; date = qdt.date(); if (qdt.time() != time || date.month() != month || date.year() != ++year) { newRule = 0; } else { const int day = date.day(); if ((newRule & DAY_OF_MONTH) && day != dayOfMonth) { newRule &= ~DAY_OF_MONTH; } if (newRule & (WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH)) { if (date.dayOfWeek() != dayOfWeek) { newRule &= ~(WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH); } else { if ((newRule & WEEKDAY_OF_MONTH) && (day - 1) / 7 + 1 != nthFromStart) { newRule &= ~WEEKDAY_OF_MONTH; } if ((newRule & LAST_WEEKDAY_OF_MONTH) && (daysInMonth - day) / 7 + 1 != nthFromEnd) { newRule &= ~LAST_WEEKDAY_OF_MONTH; } } } } } if (!newRule) { // The previous rule (if any) no longer applies. // Write all the times up to but not including the current one. // First check whether any of the last RDATE values fit this rule. int yr = times[0].date().year(); while (!rdates.isEmpty()) { qdt = rdates.last(); date = qdt.date(); if (qdt.time() != time || date.month() != month || date.year() != --yr) { break; } const int day = date.day(); if (rule & DAY_OF_MONTH) { if (day != dayOfMonth) { break; } } else { if (date.dayOfWeek() != dayOfWeek || ((rule & WEEKDAY_OF_MONTH) && (day - 1) / 7 + 1 != nthFromStart) || ((rule & LAST_WEEKDAY_OF_MONTH) && (daysInMonth - day) / 7 + 1 != nthFromEnd)) { break; } } times.prepend(qdt); rdates.pop_back(); } if (times.count() > (useNewRRULE ? minPhaseCount : minRuleCount)) { // There are enough dates to combine into an RRULE icalrecurrencetype r; icalrecurrencetype_clear(&r); r.freq = ICAL_YEARLY_RECURRENCE; r.count = (year >= 2030) ? 0 : times.count() - 1; r.by_month[0] = month; if (rule & DAY_OF_MONTH) { r.by_month_day[0] = dayOfMonth; } else if (rule & WEEKDAY_OF_MONTH) { r.by_day[0] = (dayOfWeek % 7 + 1) + (nthFromStart * 8); // Sunday = 1 } else if (rule & LAST_WEEKDAY_OF_MONTH) { r.by_day[0] = -(dayOfWeek % 7 + 1) - (nthFromEnd * 8); // Sunday = 1 } icalproperty *prop = icalproperty_new_rrule(r); if (useNewRRULE) { // This RRULE doesn't start from the phase start date, so set it into // a new STANDARD/DAYLIGHT component in the VTIMEZONE. icalcomponent *c = icalcomponent_new_clone(phaseComp); icalcomponent_add_property( c, icalproperty_new_dtstart(writeLocalICalDateTime(times[0], preOffset))); icalcomponent_add_property(c, prop); icalcomponent_add_component(tzcomp, c); } else { icalcomponent_add_property(phaseComp1, prop); } } else { // Save dates for writing as RDATEs for (int t = 0, tend = times.count() - 1; t < tend; ++t) { rdates += times[t]; } } useNewRRULE = true; // All date/time values but the last have been added to the VTIMEZONE. // Remove them from the list. qdt = times.last(); // set 'qdt' for start of loop times.clear(); times += qdt; } rule = newRule; } while (i < trcount); // Write remaining dates as RDATEs for (int rd = 0, rdend = rdates.count(); rd < rdend; ++rd) { dtperiod.time = writeLocalICalDateTime(rdates[rd], preOffset); icalcomponent_add_property(phaseComp1, icalproperty_new_rdate(dtperiod)); } icalcomponent_add_component(tzcomp, phaseComp1); icalcomponent_free(phaseComp); } d->setComponent(tzcomp); } } ICalTimeZoneData::~ICalTimeZoneData() { delete d; } ICalTimeZoneData &ICalTimeZoneData::operator=(const ICalTimeZoneData &rhs) { // check for self assignment if (&rhs == this) { return *this; } KTimeZoneData::operator=(rhs); d->location = rhs.d->location; d->url = rhs.d->url; d->lastModified = rhs.d->lastModified; d->setComponent(icalcomponent_new_clone(rhs.d->component())); return *this; } KTimeZoneData *ICalTimeZoneData::clone() const { return new ICalTimeZoneData(*this); } QString ICalTimeZoneData::city() const { return d->location; } QByteArray ICalTimeZoneData::url() const { return d->url; } QDateTime ICalTimeZoneData::lastModified() const { return d->lastModified; } QByteArray ICalTimeZoneData::vtimezone() const { const QByteArray result(icalcomponent_as_ical_string(d->component())); icalmemory_free_ring(); return result; } icaltimezone *ICalTimeZoneData::icalTimezone() const { icaltimezone *icaltz = icaltimezone_new(); if (!icaltz) { return 0; } icalcomponent *c = icalcomponent_new_clone(d->component()); if (!icaltimezone_set_component(icaltz, c)) { icalcomponent_free(c); icaltimezone_free(icaltz, 1); return 0; } return icaltz; } bool ICalTimeZoneData::hasTransitions() const { return true; } void ICalTimeZoneData::virtual_hook(int id, void *data) { Q_UNUSED(id); Q_UNUSED(data); } /******************************************************************************/ //@cond PRIVATE class ICalTimeZoneSourcePrivate { public: static QList parsePhase(icalcomponent *, bool daylight, int &prevOffset, KTimeZone::Phase &); static QByteArray icalTzidPrefix; #if defined(HAVE_UUID_UUID_H) static void parseTransitions(const MSSystemTime &date, const KTimeZone::Phase &phase, int prevOffset, QList &transitions); #endif }; QByteArray ICalTimeZoneSourcePrivate::icalTzidPrefix; //@endcond ICalTimeZoneSource::ICalTimeZoneSource() : KTimeZoneSource(false), d(0) { Q_UNUSED(d); } ICalTimeZoneSource::~ICalTimeZoneSource() { } bool ICalTimeZoneSource::parse(const QString &fileName, ICalTimeZones &zones) { QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { return false; } QTextStream ts(&file); ts.setCodec("ISO 8859-1"); const QByteArray text = ts.readAll().trimmed().toLatin1(); file.close(); bool result = false; icalcomponent *calendar = icalcomponent_new_from_string(text.data()); if (calendar) { if (icalcomponent_isa(calendar) == ICAL_VCALENDAR_COMPONENT) { result = parse(calendar, zones); } icalcomponent_free(calendar); } return result; } bool ICalTimeZoneSource::parse(icalcomponent *calendar, ICalTimeZones &zones) { for (icalcomponent *c = icalcomponent_get_first_component(calendar, ICAL_VTIMEZONE_COMPONENT); c; c = icalcomponent_get_next_component(calendar, ICAL_VTIMEZONE_COMPONENT)) { const ICalTimeZone zone = parse(c); if (!zone.isValid()) { return false; } ICalTimeZone oldzone = zones.zone(zone.name()); if (oldzone.isValid()) { // The zone already exists in the collection, so update the definition // of the zone rather than using a newly created one. oldzone.update(zone); } else if (!zones.add(zone)) { return false; } } return true; } ICalTimeZone ICalTimeZoneSource::parse(icalcomponent *vtimezone) { QString name; QString xlocation; ICalTimeZoneData *data = new ICalTimeZoneData(); // Read the fixed properties which can only appear once in VTIMEZONE icalproperty *p = icalcomponent_get_first_property(vtimezone, ICAL_ANY_PROPERTY); while (p) { icalproperty_kind kind = icalproperty_isa(p); switch (kind) { case ICAL_TZID_PROPERTY: name = QString::fromUtf8(icalproperty_get_tzid(p)); break; case ICAL_TZURL_PROPERTY: data->d->url = icalproperty_get_tzurl(p); break; case ICAL_LOCATION_PROPERTY: // This isn't mentioned in RFC2445, but libical reads it ... data->d->location = QString::fromUtf8(icalproperty_get_location(p)); break; case ICAL_X_PROPERTY: { // use X-LIC-LOCATION if LOCATION is missing const char *xname = icalproperty_get_x_name(p); if (xname && !strcmp(xname, "X-LIC-LOCATION")) { xlocation = QString::fromUtf8(icalproperty_get_x(p)); } break; } case ICAL_LASTMODIFIED_PROPERTY: { const icaltimetype t = icalproperty_get_lastmodified(p); if (t.is_utc) { data->d->lastModified = toQDateTime(t); } else { kDebug() << "LAST-MODIFIED not UTC"; } break; } default: break; } p = icalcomponent_get_next_property(vtimezone, ICAL_ANY_PROPERTY); } if (name.isEmpty()) { kDebug() << "TZID missing"; delete data; return ICalTimeZone(); } if (data->d->location.isEmpty() && !xlocation.isEmpty()) { data->d->location = xlocation; } const QString prefix = QString::fromUtf8(icalTzidPrefix()); if (name.startsWith(prefix)) { // Remove the prefix from libical built in time zone TZID const int i = name.indexOf(QLatin1Char('/'), prefix.length()); if (i > 0) { name = name.mid(i + 1); } } //kDebug() << "---zoneId: \"" << name << '"'; /* * Iterate through all time zone rules for this VTIMEZONE, * and create a Phase object containing details for each one. */ int prevOffset = 0; QList transitions; QDateTime earliest; QList phases; for (icalcomponent *c = icalcomponent_get_first_component(vtimezone, ICAL_ANY_COMPONENT); c; c = icalcomponent_get_next_component(vtimezone, ICAL_ANY_COMPONENT)) { int prevoff = 0; KTimeZone::Phase phase; QList times; icalcomponent_kind kind = icalcomponent_isa(c); switch (kind) { case ICAL_XSTANDARD_COMPONENT: //kDebug() << "---standard phase: found"; times = ICalTimeZoneSourcePrivate::parsePhase(c, false, prevoff, phase); break; case ICAL_XDAYLIGHT_COMPONENT: //kDebug() << "---daylight phase: found"; times = ICalTimeZoneSourcePrivate::parsePhase(c, true, prevoff, phase); break; default: kDebug() << "Unknown component:" << int(kind); break; } const int tcount = times.count(); if (tcount) { phases += phase; for (int t = 0; t < tcount; ++t) { transitions += KTimeZone::Transition(times[t], phase); } if (!earliest.isValid() || times[0] < earliest) { prevOffset = prevoff; earliest = times[0]; } } } // Set phases used by the time zone, but note that VTIMEZONE doesn't contain // time zone abbreviation before first transition. data->setPhases(phases, prevOffset); // Remove any "duplicate" transitions, i.e. those where two consecutive // transitions have the same phase. qSort(transitions); for (int t = 1, tend = transitions.count(); t < tend;) { if (transitions[t].phase() == transitions[t - 1].phase()) { transitions.removeAt(t); --tend; } else { ++t; } } data->setTransitions(transitions); data->d->setComponent(icalcomponent_new_clone(vtimezone)); //kDebug() << "VTIMEZONE" << name; return ICalTimeZone(this, name, data); } #if defined(HAVE_UUID_UUID_H) ICalTimeZone ICalTimeZoneSource::parse(MSTimeZone *tz, ICalTimeZones &zones) { const ICalTimeZone zone = parse(tz); if (!zone.isValid()) { return ICalTimeZone(); // error } const ICalTimeZone oldzone = zones.zone(zone); if (oldzone.isValid()) { // A similar zone already exists in the collection, so don't add this // new zone, return old zone instead. return oldzone; } else if (zones.add(zone)) { // No similar zone, add and return new one. return zone; } return ICalTimeZone(); // error } ICalTimeZone ICalTimeZoneSource::parse(MSTimeZone *tz) { ICalTimeZoneData kdata; // General properties. uuid_t uuid; char suuid[64]; uuid_generate_random(uuid); uuid_unparse(uuid, suuid); QString name = QString::fromLatin1(suuid); // Create phases. QList phases; QList standardAbbrevs; standardAbbrevs += tz->StandardName.toLatin1(); const KTimeZone::Phase standardPhase( (tz->Bias + tz->StandardBias) * -60, standardAbbrevs, false, QLatin1String("Microsoft TIME_ZONE_INFORMATION")); phases += standardPhase; QList daylightAbbrevs; daylightAbbrevs += tz->DaylightName.toLatin1(); const KTimeZone::Phase daylightPhase( (tz->Bias + tz->DaylightBias) * -60, daylightAbbrevs, true, QLatin1String("Microsoft TIME_ZONE_INFORMATION")); phases += daylightPhase; // Set phases used by the time zone, but note that previous time zone // abbreviation is not known. const int prevOffset = tz->Bias * -60; kdata.setPhases(phases, prevOffset); // Create transitions QList transitions; ICalTimeZoneSourcePrivate::parseTransitions( tz->StandardDate, standardPhase, prevOffset, transitions); ICalTimeZoneSourcePrivate::parseTransitions( tz->DaylightDate, daylightPhase, prevOffset, transitions); qSort(transitions); kdata.setTransitions(transitions); ICalTimeZoneData *idata = new ICalTimeZoneData(kdata, KTimeZone(name), QDate()); return ICalTimeZone(this, name, idata); } #endif // HAVE_UUID_UUID_H ICalTimeZone ICalTimeZoneSource::parse(const QString &name, const QStringList &tzList, ICalTimeZones &zones) { const ICalTimeZone zone = parse(name, tzList); if (!zone.isValid()) { return ICalTimeZone(); // error } ICalTimeZone oldzone = zones.zone(zone); // First off see if the zone is same as oldzone - _exactly_ same if (oldzone.isValid()) { return oldzone; } oldzone = zones.zone(name); if (oldzone.isValid()) { // The zone already exists, so update oldzone.update(zone); return zone; } else if (zones.add(zone)) { // No similar zone, add and return new one. return zone; } return ICalTimeZone(); // error } ICalTimeZone ICalTimeZoneSource::parse(const QString &name, const QStringList &tzList) { ICalTimeZoneData kdata; QList phases; QList transitions; bool daylight; for (QStringList::ConstIterator it = tzList.begin(); it != tzList.end(); ++it) { QString value = *it; daylight = false; const QString tzName = value.mid(0, value.indexOf(QLatin1String(";"))); value = value.mid((value.indexOf(QLatin1String(";")) + 1)); const QString tzOffset = value.mid(0, value.indexOf(QLatin1String(";"))); value = value.mid((value.indexOf(QLatin1String(";")) + 1)); const QString tzDaylight = value.mid(0, value.indexOf(QLatin1String(";"))); const KDateTime tzDate = KDateTime::fromString(value.mid((value.lastIndexOf(QLatin1String(";")) + 1))); if (tzDaylight == QLatin1String("true")) { daylight = true; } const KTimeZone::Phase tzPhase( tzOffset.toInt(), QByteArray(tzName.toLatin1()), daylight, QLatin1String("VCAL_TZ_INFORMATION")); phases += tzPhase; transitions += KTimeZone::Transition(tzDate.dateTime(), tzPhase); } kdata.setPhases(phases, 0); qSort(transitions); kdata.setTransitions(transitions); ICalTimeZoneData *idata = new ICalTimeZoneData(kdata, KTimeZone(name), QDate()); return ICalTimeZone(this, name, idata); } #if defined(HAVE_UUID_UUID_H) //@cond PRIVATE void ICalTimeZoneSourcePrivate::parseTransitions(const MSSystemTime &date, const KTimeZone::Phase &phase, int prevOffset, QList &transitions) { // NOTE that we need to set start and end times and they cannot be // to far in either direction to avoid bloating the transitions list const KDateTime klocalStart(QDateTime(QDate(2000, 1, 1), QTime(0, 0, 0)), KDateTime::Spec::ClockTime()); const KDateTime maxTime(MAX_DATE(), KDateTime::Spec::ClockTime()); if (date.wYear) { // Absolute change time. if (date.wYear >= 1601 && date.wYear <= 30827 && date.wMonth >= 1 && date.wMonth <= 12 && date.wDay >= 1 && date.wDay <= 31) { const QDate dt(date.wYear, date.wMonth, date.wDay); const QTime tm(date.wHour, date.wMinute, date.wSecond, date.wMilliseconds); const QDateTime datetime(dt, tm); if (datetime.isValid()) { transitions += KTimeZone::Transition(datetime, phase); } } } else { // The normal way, for example: 'First Sunday in April at 02:00'. if (date.wDayOfWeek >= 0 && date.wDayOfWeek <= 6 && date.wMonth >= 1 && date.wMonth <= 12 && date.wDay >= 1 && date.wDay <= 5) { RecurrenceRule r; r.setRecurrenceType(RecurrenceRule::rYearly); r.setDuration(-1); r.setFrequency(1); QList lst; lst.append(date.wMonth); r.setByMonths(lst); QList wdlst; RecurrenceRule::WDayPos pos; pos.setDay(date.wDayOfWeek ? date.wDayOfWeek : 7); pos.setPos(date.wDay < 5 ? date.wDay : -1); wdlst.append(pos); r.setByDays(wdlst); r.setStartDt(klocalStart); r.setWeekStart(1); const DateTimeList dtl = r.timesInInterval(klocalStart, maxTime); for (int i = 0, end = dtl.count(); i < end; ++i) { QDateTime utc = dtl[i].dateTime(); utc.setTimeSpec(Qt::UTC); transitions += KTimeZone::Transition(utc.addSecs(-prevOffset), phase); } } } } //@endcond #endif // HAVE_UUID_UUID_H ICalTimeZone ICalTimeZoneSource::parse(icaltimezone *tz) { /* Parse the VTIMEZONE component stored in the icaltimezone structure. * This is both easier and provides more complete information than * extracting already parsed data from icaltimezone. */ return tz ? parse(icaltimezone_get_component(tz)) : ICalTimeZone(); } //@cond PRIVATE QList ICalTimeZoneSourcePrivate::parsePhase(icalcomponent *c, bool daylight, int &prevOffset, KTimeZone::Phase &phase) { QList transitions; // Read the observance data for this standard/daylight savings phase QList abbrevs; QString comment; prevOffset = 0; int utcOffset = 0; bool recurs = false; bool found_dtstart = false; bool found_tzoffsetfrom = false; bool found_tzoffsetto = false; icaltimetype dtstart = icaltime_null_time(); // Now do the ical reading. icalproperty *p = icalcomponent_get_first_property(c, ICAL_ANY_PROPERTY); while (p) { icalproperty_kind kind = icalproperty_isa(p); switch (kind) { case ICAL_TZNAME_PROPERTY: // abbreviated name for this time offset { // TZNAME can appear multiple times in order to provide language // translations of the time zone offset name. // TODO: Does this cope with multiple language specifications? QByteArray tzname = icalproperty_get_tzname(p); // Outlook (2000) places "Standard Time" and "Daylight Time" in the TZNAME // strings, which is totally useless. So ignore those. if ((!daylight && tzname == "Standard Time") || (daylight && tzname == "Daylight Time")) { break; } if (!abbrevs.contains(tzname)) { abbrevs += tzname; } break; } case ICAL_DTSTART_PROPERTY: // local time at which phase starts dtstart = icalproperty_get_dtstart(p); found_dtstart = true; break; case ICAL_TZOFFSETFROM_PROPERTY: // UTC offset immediately before start of phase prevOffset = icalproperty_get_tzoffsetfrom(p); found_tzoffsetfrom = true; break; case ICAL_TZOFFSETTO_PROPERTY: utcOffset = icalproperty_get_tzoffsetto(p); found_tzoffsetto = true; break; case ICAL_COMMENT_PROPERTY: comment = QString::fromUtf8(icalproperty_get_comment(p)); break; case ICAL_RDATE_PROPERTY: case ICAL_RRULE_PROPERTY: recurs = true; break; default: kDebug() << "Unknown property:" << int(kind); break; } p = icalcomponent_get_next_property(c, ICAL_ANY_PROPERTY); } // Validate the phase data if (!found_dtstart || !found_tzoffsetfrom || !found_tzoffsetto) { kDebug() << "DTSTART/TZOFFSETFROM/TZOFFSETTO missing"; return transitions; } // Convert DTSTART to QDateTime, and from local time to UTC const QDateTime localStart = toQDateTime(dtstart); // local time dtstart.second -= prevOffset; dtstart.is_utc = 1; const QDateTime utcStart = toQDateTime(icaltime_normalize(dtstart)); // UTC transitions += utcStart; if (recurs) { /* RDATE or RRULE is specified. There should only be one or the other, but * it doesn't really matter - the code can cope with both. * Note that we had to get DTSTART, TZOFFSETFROM, TZOFFSETTO before reading * recurrences. */ const KDateTime klocalStart(localStart, KDateTime::Spec::ClockTime()); const KDateTime maxTime(MAX_DATE(), KDateTime::Spec::ClockTime()); Recurrence recur; icalproperty *p = icalcomponent_get_first_property(c, ICAL_ANY_PROPERTY); while (p) { icalproperty_kind kind = icalproperty_isa(p); switch (kind) { case ICAL_RDATE_PROPERTY: { icaltimetype t = icalproperty_get_rdate(p).time; if (icaltime_is_date(t)) { // RDATE with a DATE value inherits the (local) time from DTSTART t.hour = dtstart.hour; t.minute = dtstart.minute; t.second = dtstart.second; t.is_date = 0; t.is_utc = 0; // dtstart is in local time } // RFC2445 states that RDATE must be in local time, // but we support UTC as well to be safe. if (!t.is_utc) { t.second -= prevOffset; // convert to UTC t.is_utc = 1; t = icaltime_normalize(t); } transitions += toQDateTime(t); break; } case ICAL_RRULE_PROPERTY: { RecurrenceRule r; ICalFormat icf; ICalFormatImpl impl(&icf); impl.readRecurrence(icalproperty_get_rrule(p), &r); r.setStartDt(klocalStart); // The end date time specified in an RRULE should be in UTC. // Convert to local time to avoid timesInInterval() getting things wrong. if (r.duration() == 0) { KDateTime end(r.endDt()); if (end.timeSpec() == KDateTime::Spec::UTC()) { end.setTimeSpec(KDateTime::Spec::ClockTime()); r.setEndDt(end.addSecs(prevOffset)); } } const DateTimeList dts = r.timesInInterval(klocalStart, maxTime); for (int i = 0, end = dts.count(); i < end; ++i) { QDateTime utc = dts[i].dateTime(); utc.setTimeSpec(Qt::UTC); transitions += utc.addSecs(-prevOffset); } break; } default: break; } p = icalcomponent_get_next_property(c, ICAL_ANY_PROPERTY); } qSortUnique(transitions); } phase = KTimeZone::Phase(utcOffset, abbrevs, daylight, comment); return transitions; } //@endcond ICalTimeZone ICalTimeZoneSource::standardZone(const QString &zone, bool icalBuiltIn) { if (!icalBuiltIn) { // Try to fetch a system time zone in preference, on the grounds // that system time zones are more likely to be up to date than // built-in libical ones. QString tzid = zone; const QString prefix = QString::fromUtf8(icalTzidPrefix()); if (zone.startsWith(prefix)) { const int i = zone.indexOf(QLatin1Char('/'), prefix.length()); if (i > 0) { tzid = zone.mid(i + 1); // strip off the libical prefix } } const KTimeZone ktz = KSystemTimeZones::readZone(tzid); if (ktz.isValid()) { if (ktz.data(true)) { const ICalTimeZone icaltz(ktz); //kDebug() << zone << " read from system database"; return icaltz; } } } // Try to fetch a built-in libical time zone. // First try to look it up as a geographical location (e.g. Europe/London) const QByteArray zoneName = zone.toUtf8(); icaltimezone *icaltz = icaltimezone_get_builtin_timezone(zoneName); if (!icaltz) { // This will find it if it includes the libical prefix icaltz = icaltimezone_get_builtin_timezone_from_tzid(zoneName); if (!icaltz) { return ICalTimeZone(); } } return parse(icaltz); } QByteArray ICalTimeZoneSource::icalTzidPrefix() { if (ICalTimeZoneSourcePrivate::icalTzidPrefix.isEmpty()) { icaltimezone *icaltz = icaltimezone_get_builtin_timezone("Europe/London"); const QByteArray tzid = icaltimezone_get_tzid(icaltz); if (tzid.right(13) == "Europe/London") { int i = tzid.indexOf('/', 1); if (i > 0) { ICalTimeZoneSourcePrivate::icalTzidPrefix = tzid.left(i + 1); return ICalTimeZoneSourcePrivate::icalTzidPrefix; } } kError() << "failed to get libical TZID prefix"; } return ICalTimeZoneSourcePrivate::icalTzidPrefix; } void ICalTimeZoneSource::virtual_hook(int id, void *data) { Q_UNUSED(id); Q_UNUSED(data); Q_ASSERT(false); } } // namespace KCalCore