diff --git a/src/icalformat_p.cpp b/src/icalformat_p.cpp index 7c83ae152..1a3c6bc04 100644 --- a/src/icalformat_p.cpp +++ b/src/icalformat_p.cpp @@ -1,3092 +1,3092 @@ /* This file is part of the kcalcore library. Copyright (c) 2001 Cornelius Schumacher Copyright (C) 2003-2004 Reinhold Kainhofer Copyright (c) 2006 David Jarvie Copyright (C) 2012 Christian Mollekopf 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. */ /** @file This file is part of the API for handling calendar data and defines the internal ICalFormat classes. @brief This class provides the libical dependent functions for ICalFormat. @author Cornelius Schumacher \ @author Reinhold Kainhofer \ @author David Jarvie \ */ #include "icalformat_p.h" #include "compat_p.h" #include "event.h" #include "freebusy.h" #include "icalformat.h" #include "icaltimezones_p.h" #include "incidencebase.h" #include "journal.h" #include "memorycalendar.h" #include "todo.h" #include "visitor.h" #include "utils.h" #include "kcalcore_debug.h" #include using namespace KCalCore; static const char APP_NAME_FOR_XPROPERTIES[] = "KCALCORE"; static const char ENABLED_ALARM_XPROPERTY[] = "ENABLED"; static const char IMPLEMENTATION_VERSION_XPROPERTY[] = "X-KDE-ICAL-IMPLEMENTATION-VERSION"; /* Static helpers */ /* static void _dumpIcaltime( const icaltimetype& t) { qCDebug(KCALCORE_LOG) << "--- Y:" << t.year << "M:" << t.month << "D:" << t.day; qCDebug(KCALCORE_LOG) << "--- H:" << t.hour << "M:" << t.minute << "S:" << t.second; qCDebug(KCALCORE_LOG) << "--- isUtc:" << icaltime_is_utc( t ); qCDebug(KCALCORE_LOG) << "--- zoneId:" << icaltimezone_get_tzid( const_cast( t.zone ) ); } */ //@cond PRIVATE template void removeAllICal(QVector< QSharedPointer > &c, const QSharedPointer &x) { if (c.count() < 1) { return; } int cnt = c.count(x); if (cnt != 1) { qCritical() << "There number of relatedTos for this incidence is " << cnt << " (there must be 1 relatedTo only)"; Q_ASSERT_X(false, "removeAllICal", "Count is not 1."); return; } c.remove(c.indexOf(x)); } #if !defined(USE_ICAL_3) static QString quoteForParam(const QString &text) { QString tmp = text; tmp.remove(QLatin1Char('"')); if (tmp.contains(QLatin1Char(';')) || tmp.contains(QLatin1Char(':')) || tmp.contains(QLatin1Char(','))) { return tmp; // libical quotes in this case already, see icalparameter_as_ical_string() } return QStringLiteral("\"") + tmp + QStringLiteral("\""); } #endif const int gSecondsPerMinute = 60; const int gSecondsPerHour = gSecondsPerMinute * 60; const int gSecondsPerDay = gSecondsPerHour * 24; const int gSecondsPerWeek = gSecondsPerDay * 7; class ToComponentVisitor : public Visitor { public: ToComponentVisitor(ICalFormatImpl *impl, iTIPMethod m, TimeZoneList *tzUsedList = nullptr) : mImpl(impl), mComponent(nullptr), mMethod(m), mTzUsedList(tzUsedList) { } ~ToComponentVisitor(); bool visit(const Event::Ptr &e) override { mComponent = mImpl->writeEvent(e, mTzUsedList); return true; } bool visit(const Todo::Ptr &t) override { mComponent = mImpl->writeTodo(t, mTzUsedList); return true; } bool visit(const Journal::Ptr &j) override { mComponent = mImpl->writeJournal(j, mTzUsedList); return true; } bool visit(const FreeBusy::Ptr &fb) override { mComponent = mImpl->writeFreeBusy(fb, mMethod); return true; } icalcomponent *component() { return mComponent; } private: ICalFormatImpl *mImpl = nullptr; icalcomponent *mComponent = nullptr; iTIPMethod mMethod; TimeZoneList *mTzUsedList = nullptr; }; ToComponentVisitor::~ToComponentVisitor() { } class Q_DECL_HIDDEN ICalFormatImpl::Private { public: Private(ICalFormatImpl *impl, ICalFormat *parent) : mImpl(impl), mParent(parent), mCompat(new Compat) {} ~Private() { delete mCompat; } void writeIncidenceBase(icalcomponent *parent, const IncidenceBase::Ptr &); void readIncidenceBase(icalcomponent *parent, const IncidenceBase::Ptr &); void writeCustomProperties(icalcomponent *parent, CustomProperties *); void readCustomProperties(icalcomponent *parent, CustomProperties *); ICalFormatImpl *mImpl = nullptr; ICalFormat *mParent = nullptr; QString mLoadedProductId; // PRODID string loaded from calendar file Event::List mEventsRelate; // events with relations Todo::List mTodosRelate; // todos with relations Compat *mCompat = nullptr; }; //@endcond inline icaltimetype ICalFormatImpl::writeICalUtcDateTime(const QDateTime &dt, bool dayOnly) { return writeICalDateTime(dt.toUTC(), dayOnly); } ICalFormatImpl::ICalFormatImpl(ICalFormat *parent) : d(new Private(this, parent)) { } ICalFormatImpl::~ICalFormatImpl() { delete d; } QString ICalFormatImpl::loadedProductId() const { return d->mLoadedProductId; } icalcomponent *ICalFormatImpl::writeIncidence(const IncidenceBase::Ptr &incidence, iTIPMethod method, TimeZoneList *tzUsedList) { ToComponentVisitor v(this, method, tzUsedList); if (incidence->accept(v, incidence)) { return v.component(); } else { return nullptr; } } icalcomponent *ICalFormatImpl::writeTodo(const Todo::Ptr &todo, TimeZoneList *tzUsedList) { icalcomponent *vtodo = icalcomponent_new(ICAL_VTODO_COMPONENT); writeIncidence(vtodo, todo.staticCast(), tzUsedList); // due date icalproperty *prop; if (todo->hasDueDate()) { icaltimetype due; if (todo->allDay()) { due = writeICalDate(todo->dtDue(true).date()); prop = icalproperty_new_due(due); } else { prop = writeICalDateTimeProperty(ICAL_DUE_PROPERTY, todo->dtDue(true), tzUsedList); } icalcomponent_add_property(vtodo, prop); } // start time if (todo->hasStartDate()) { icaltimetype start; if (todo->allDay()) { start = writeICalDate(todo->dtStart(true).date()); prop = icalproperty_new_dtstart(start); } else { prop = writeICalDateTimeProperty(ICAL_DTSTART_PROPERTY, todo->dtStart(true), tzUsedList); } icalcomponent_add_property(vtodo, prop); } // completion date (UTC) if (todo->isCompleted()) { if (!todo->hasCompletedDate()) { // If the todo was created by KOrganizer<2.2 it does not have // a correct completion date. Set one now. todo->setCompleted(QDateTime::currentDateTimeUtc()); } icaltimetype completed = writeICalUtcDateTime(todo->completed()); icalcomponent_add_property( vtodo, icalproperty_new_completed(completed)); } icalcomponent_add_property( vtodo, icalproperty_new_percentcomplete(todo->percentComplete())); if (todo->isCompleted()) { if (icalcomponent_count_properties(vtodo, ICAL_STATUS_PROPERTY)) { icalproperty *p = icalcomponent_get_first_property(vtodo, ICAL_STATUS_PROPERTY); icalcomponent_remove_property(vtodo, p); icalproperty_free(p); } icalcomponent_add_property(vtodo, icalproperty_new_status(ICAL_STATUS_COMPLETED)); } if (todo->recurs() && todo->dtDue().isValid()) { // dtDue( first = true ) returns the dtRecurrence() prop = writeICalDateTimeProperty(ICAL_X_PROPERTY, todo->dtDue(), tzUsedList); icalproperty_set_x_name(prop, "X-KDE-LIBKCAL-DTRECURRENCE"); icalcomponent_add_property(vtodo, prop); } return vtodo; } icalcomponent *ICalFormatImpl::writeEvent(const Event::Ptr &event, TimeZoneList *tzUsedList) { icalcomponent *vevent = icalcomponent_new(ICAL_VEVENT_COMPONENT); writeIncidence(vevent, event.staticCast(), tzUsedList); // start time icalproperty *prop = nullptr; icaltimetype start; QDateTime dt = event->dtStart(); if (dt.isValid()) { if (event->allDay()) { start = writeICalDate(event->dtStart().date()); prop = icalproperty_new_dtstart(start); } else { prop = writeICalDateTimeProperty(ICAL_DTSTART_PROPERTY, event->dtStart(), tzUsedList); } icalcomponent_add_property(vevent, prop); } if (event->hasEndDate()) { // End time. // RFC2445 says that if DTEND is present, it has to be greater than DTSTART. icaltimetype end; QDateTime dt = event->dtEnd(); if (event->allDay()) { // +1 day because end date is non-inclusive. end = writeICalDate(dt.date().addDays(1)); icalcomponent_add_property(vevent, icalproperty_new_dtend(end)); } else { if (dt != event->dtStart()) { icalcomponent_add_property( vevent, writeICalDateTimeProperty(ICAL_DTEND_PROPERTY, dt, tzUsedList)); } } } // TODO: resources #if 0 // resources QStringList tmpStrList = anEvent->resources(); QString tmpStr = tmpStrList.join(";"); if (!tmpStr.isEmpty()) { addPropValue(vevent, VCResourcesProp, tmpStr.toUtf8()); } #endif // Transparency switch (event->transparency()) { case Event::Transparent: icalcomponent_add_property( vevent, icalproperty_new_transp(ICAL_TRANSP_TRANSPARENT)); break; case Event::Opaque: icalcomponent_add_property( vevent, icalproperty_new_transp(ICAL_TRANSP_OPAQUE)); break; } return vevent; } icalcomponent *ICalFormatImpl::writeFreeBusy(const FreeBusy::Ptr &freebusy, iTIPMethod method) { icalcomponent *vfreebusy = icalcomponent_new(ICAL_VFREEBUSY_COMPONENT); d->writeIncidenceBase(vfreebusy, freebusy.staticCast()); icalcomponent_add_property( vfreebusy, icalproperty_new_dtstart(writeICalUtcDateTime(freebusy->dtStart()))); icalcomponent_add_property( vfreebusy, icalproperty_new_dtend(writeICalUtcDateTime(freebusy->dtEnd()))); Q_UNUSED(method); icalcomponent_add_property( vfreebusy, icalproperty_new_uid(freebusy->uid().toUtf8().constData())); //Loops through all the periods in the freebusy object FreeBusyPeriod::List list = freebusy->fullBusyPeriods(); icalperiodtype period = icalperiodtype_null_period(); for (int i = 0, count = list.count(); i < count; ++i) { const FreeBusyPeriod fbPeriod = list[i]; period.start = writeICalUtcDateTime(fbPeriod.start()); if (fbPeriod.hasDuration()) { period.duration = writeICalDuration(fbPeriod.duration()); } else { period.end = writeICalUtcDateTime(fbPeriod.end()); } icalproperty *property = icalproperty_new_freebusy(period); icalparameter_fbtype fbType; switch (fbPeriod.type()) { case FreeBusyPeriod::Free: fbType = ICAL_FBTYPE_FREE; break; case FreeBusyPeriod::Busy: fbType = ICAL_FBTYPE_BUSY; break; case FreeBusyPeriod::BusyTentative: fbType = ICAL_FBTYPE_BUSYTENTATIVE; break; case FreeBusyPeriod::BusyUnavailable: fbType = ICAL_FBTYPE_BUSYUNAVAILABLE; break; case FreeBusyPeriod::Unknown: fbType = ICAL_FBTYPE_X; break; default: fbType = ICAL_FBTYPE_NONE; break; } icalproperty_set_parameter(property, icalparameter_new_fbtype(fbType)); if (!fbPeriod.summary().isEmpty()) { icalparameter *param = icalparameter_new_x("X-SUMMARY"); icalparameter_set_xvalue(param, fbPeriod.summary().toUtf8().toBase64().constData()); icalproperty_set_parameter(property, param); } if (!fbPeriod.location().isEmpty()) { icalparameter *param = icalparameter_new_x("X-LOCATION"); icalparameter_set_xvalue(param, fbPeriod.location().toUtf8().toBase64().constData()); icalproperty_set_parameter(property, param); } icalcomponent_add_property(vfreebusy, property); } return vfreebusy; } icalcomponent *ICalFormatImpl::writeJournal(const Journal::Ptr &journal, TimeZoneList *tzUsedList) { icalcomponent *vjournal = icalcomponent_new(ICAL_VJOURNAL_COMPONENT); writeIncidence(vjournal, journal.staticCast(), tzUsedList); // start time icalproperty *prop = nullptr; QDateTime dt = journal->dtStart(); if (dt.isValid()) { icaltimetype start; if (journal->allDay()) { start = writeICalDate(dt.date()); prop = icalproperty_new_dtstart(start); } else { prop = writeICalDateTimeProperty(ICAL_DTSTART_PROPERTY, dt, tzUsedList); } icalcomponent_add_property(vjournal, prop); } return vjournal; } void ICalFormatImpl::writeIncidence(icalcomponent *parent, const Incidence::Ptr &incidence, TimeZoneList *tzUsedList) { if (incidence->schedulingID() != incidence->uid()) { // We need to store the UID in here. The rawSchedulingID will // go into the iCal UID component incidence->setCustomProperty("LIBKCAL", "ID", incidence->uid()); } else { incidence->removeCustomProperty("LIBKCAL", "ID"); } d->writeIncidenceBase(parent, incidence.staticCast()); // creation date in storage icalcomponent_add_property( parent, writeICalDateTimeProperty( ICAL_CREATED_PROPERTY, incidence->created())); // unique id // If the scheduling ID is different from the real UID, the real // one is stored on X-REALID above if (!incidence->schedulingID().isEmpty()) { icalcomponent_add_property( parent, icalproperty_new_uid(incidence->schedulingID().toUtf8().constData())); } // revision if (incidence->revision() > 0) { // 0 is default, so don't write that out icalcomponent_add_property( parent, icalproperty_new_sequence(incidence->revision())); } // last modification date if (incidence->lastModified().isValid()) { icalcomponent_add_property( parent, writeICalDateTimeProperty( ICAL_LASTMODIFIED_PROPERTY, incidence->lastModified())); } // description if (!incidence->description().isEmpty()) { icalcomponent_add_property( parent, writeDescription( incidence->description(), incidence->descriptionIsRich())); } // summary if (!incidence->summary().isEmpty()) { icalcomponent_add_property( parent, writeSummary( incidence->summary(), incidence->summaryIsRich())); } // location if (!incidence->location().isEmpty()) { icalcomponent_add_property( parent, writeLocation( incidence->location(), incidence->locationIsRich())); } // status icalproperty_status status = ICAL_STATUS_NONE; switch (incidence->status()) { case Incidence::StatusTentative: status = ICAL_STATUS_TENTATIVE; break; case Incidence::StatusConfirmed: status = ICAL_STATUS_CONFIRMED; break; case Incidence::StatusCompleted: status = ICAL_STATUS_COMPLETED; break; case Incidence::StatusNeedsAction: status = ICAL_STATUS_NEEDSACTION; break; case Incidence::StatusCanceled: status = ICAL_STATUS_CANCELLED; break; case Incidence::StatusInProcess: status = ICAL_STATUS_INPROCESS; break; case Incidence::StatusDraft: status = ICAL_STATUS_DRAFT; break; case Incidence::StatusFinal: status = ICAL_STATUS_FINAL; break; case Incidence::StatusX: { icalproperty *p = icalproperty_new_status(ICAL_STATUS_X); icalvalue_set_x(icalproperty_get_value(p), incidence->customStatus().toUtf8().constData()); icalcomponent_add_property(parent, p); break; } case Incidence::StatusNone: default: break; } if (status != ICAL_STATUS_NONE) { icalcomponent_add_property(parent, icalproperty_new_status(status)); } // secrecy icalproperty_class secClass; switch (incidence->secrecy()) { case Incidence::SecrecyPublic: secClass = ICAL_CLASS_PUBLIC; break; case Incidence::SecrecyConfidential: secClass = ICAL_CLASS_CONFIDENTIAL; break; case Incidence::SecrecyPrivate: default: secClass = ICAL_CLASS_PRIVATE; break; } if (secClass != ICAL_CLASS_PUBLIC) { icalcomponent_add_property(parent, icalproperty_new_class(secClass)); } // geo if (incidence->hasGeo()) { icalgeotype geo; geo.lat = incidence->geoLatitude(); geo.lon = incidence->geoLongitude(); icalcomponent_add_property(parent, icalproperty_new_geo(geo)); } // priority if (incidence->priority() > 0) { // 0 is undefined priority icalcomponent_add_property( parent, icalproperty_new_priority(incidence->priority())); } // categories QString categories = incidence->categories().join(QLatin1Char(',')); if (!categories.isEmpty()) { icalcomponent_add_property( parent, icalproperty_new_categories(categories.toUtf8().constData())); } // related event if (!incidence->relatedTo().isEmpty()) { icalcomponent_add_property( parent, icalproperty_new_relatedto(incidence->relatedTo().toUtf8().constData())); } // recurrenceid if (incidence->hasRecurrenceId()) { icalproperty *p = writeICalDateTimeProperty( ICAL_RECURRENCEID_PROPERTY, incidence->recurrenceId(), tzUsedList); if (incidence->thisAndFuture()) { icalproperty_add_parameter( p, icalparameter_new_range(ICAL_RANGE_THISANDFUTURE)); } icalcomponent_add_property(parent, p); } RecurrenceRule::List rrules(incidence->recurrence()->rRules()); RecurrenceRule::List::ConstIterator rit; for (rit = rrules.constBegin(); rit != rrules.constEnd(); ++rit) { icalcomponent_add_property( parent, icalproperty_new_rrule(writeRecurrenceRule((*rit)))); } RecurrenceRule::List exrules(incidence->recurrence()->exRules()); RecurrenceRule::List::ConstIterator exit; for (exit = exrules.constBegin(); exit != exrules.constEnd(); ++exit) { icalcomponent_add_property( parent, icalproperty_new_exrule(writeRecurrenceRule((*exit)))); } DateList dateList = incidence->recurrence()->exDates(); DateList::ConstIterator exIt; for (exIt = dateList.constBegin(); exIt != dateList.constEnd(); ++exIt) { icalcomponent_add_property( parent, icalproperty_new_exdate(writeICalDate(*exIt))); } auto dateTimeList = incidence->recurrence()->exDateTimes(); for (auto extIt = dateTimeList.constBegin(); extIt != dateTimeList.constEnd(); ++extIt) { icalcomponent_add_property( parent, writeICalDateTimeProperty(ICAL_EXDATE_PROPERTY, *extIt, tzUsedList)); } dateList = incidence->recurrence()->rDates(); DateList::ConstIterator rdIt; for (rdIt = dateList.constBegin(); rdIt != dateList.constEnd(); ++rdIt) { icalcomponent_add_property( parent, icalproperty_new_rdate(writeICalDatePeriod(*rdIt))); } dateTimeList = incidence->recurrence()->rDateTimes(); for (auto rdtIt = dateTimeList.constBegin(); rdtIt != dateTimeList.constEnd(); ++rdtIt) { icalcomponent_add_property( parent, writeICalDateTimeProperty(ICAL_RDATE_PROPERTY, *rdtIt, tzUsedList)); } // attachments Attachment::List attachments = incidence->attachments(); Attachment::List::ConstIterator atIt; for (atIt = attachments.constBegin(); atIt != attachments.constEnd(); ++atIt) { icalcomponent_add_property(parent, writeAttachment(*atIt)); } // alarms auto alarms = incidence->alarms(); for (auto it = alarms.cbegin(), end = alarms.cend(); it != end; ++it) { icalcomponent_add_component(parent, writeAlarm(*it)); } // duration if (incidence->hasDuration()) { icaldurationtype duration; duration = writeICalDuration(incidence->duration()); icalcomponent_add_property(parent, icalproperty_new_duration(duration)); } } //@cond PRIVATE void ICalFormatImpl::Private::writeIncidenceBase(icalcomponent *parent, const IncidenceBase::Ptr &incidenceBase) { // organizer stuff if (!incidenceBase->organizer()->isEmpty()) { icalproperty *p = mImpl->writeOrganizer(incidenceBase->organizer()); if (p) { icalcomponent_add_property(parent, p); } } icalcomponent_add_property( parent, icalproperty_new_dtstamp(writeICalUtcDateTime(incidenceBase->lastModified()))); // attendees if (incidenceBase->attendeeCount() > 0) { auto attendees = incidenceBase->attendees(); for (auto it = attendees.constBegin(); it != attendees.constEnd(); ++it) { icalproperty *p = mImpl->writeAttendee(*it); if (p) { icalcomponent_add_property(parent, p); } } } //contacts QStringList contacts = incidenceBase->contacts(); for (QStringList::const_iterator it = contacts.constBegin(); it != contacts.constEnd(); ++it) { icalcomponent_add_property(parent, icalproperty_new_contact((*it).toUtf8().constData())); } // comments QStringList comments = incidenceBase->comments(); for (QStringList::const_iterator it = comments.constBegin(); it != comments.constEnd(); ++it) { icalcomponent_add_property(parent, icalproperty_new_comment((*it).toUtf8().constData())); } // url const QUrl url = incidenceBase->url(); if (url.isValid()) { icalcomponent_add_property(parent, icalproperty_new_url(url.toString().toUtf8().constData())); } // custom properties writeCustomProperties(parent, incidenceBase.data()); } void ICalFormatImpl::Private::writeCustomProperties(icalcomponent *parent, CustomProperties *properties) { const QMap custom = properties->customProperties(); for (QMap::ConstIterator c = custom.begin(); c != custom.end(); ++c) { if (c.key().startsWith("X-KDE-VOLATILE")) { //krazy:exclude=strings // We don't write these properties to disk to disk continue; } icalproperty *p = icalproperty_new_x(c.value().toUtf8().constData()); QString parameters = properties->nonKDECustomPropertyParameters(c.key()); // Minimalist parameter handler: extract icalparameter's out of // the given input text (not really parsing as such) if (!parameters.isEmpty()) { const QStringList sl = parameters.split(QLatin1Char(';')); for (const QString ¶meter : sl) { icalparameter *param = icalparameter_new_from_string(parameter.toUtf8().constData()); if (param) { icalproperty_add_parameter(p, param); } } } icalproperty_set_x_name(p, c.key().constData()); icalcomponent_add_property(parent, p); } } //@endcond icalproperty *ICalFormatImpl::writeOrganizer(const Person::Ptr &organizer) { if (organizer->email().isEmpty()) { return nullptr; } icalproperty *p = icalproperty_new_organizer(QByteArray(QByteArray("MAILTO:") + organizer->email().toUtf8()).constData()); if (!organizer->name().isEmpty()) { icalproperty_add_parameter(p, #if defined(USE_ICAL_3) icalparameter_new_cn(organizer->name().toUtf8().constData())); #else icalparameter_new_cn(quoteForParam(organizer->name()).toUtf8().constData())); #endif } // TODO: Write dir, sent-by and language return p; } icalproperty *ICalFormatImpl::writeDescription(const QString &description, bool isRich) { icalproperty *p = icalproperty_new_description(description.toUtf8().constData()); if (isRich) { icalproperty_add_parameter(p, icalparameter_new_from_string("X-KDE-TEXTFORMAT=HTML")); } return p; } icalproperty *ICalFormatImpl::writeSummary(const QString &summary, bool isRich) { icalproperty *p = icalproperty_new_summary(summary.toUtf8().constData()); if (isRich) { icalproperty_add_parameter(p, icalparameter_new_from_string("X-KDE-TEXTFORMAT=HTML")); } return p; } icalproperty *ICalFormatImpl::writeLocation(const QString &location, bool isRich) { icalproperty *p = icalproperty_new_location(location.toUtf8().constData()); if (isRich) { icalproperty_add_parameter(p, icalparameter_new_from_string("X-KDE-TEXTFORMAT=HTML")); } return p; } icalproperty *ICalFormatImpl::writeAttendee(const Attendee::Ptr &attendee) { if (attendee->email().isEmpty()) { return nullptr; } icalproperty *p = icalproperty_new_attendee(QByteArray(QByteArray("mailto:") + attendee->email().toUtf8()).constData()); if (!attendee->name().isEmpty()) { icalproperty_add_parameter(p, #if defined(USE_ICAL_3) icalparameter_new_cn(attendee->name().toUtf8().constData())); #else icalparameter_new_cn(quoteForParam(attendee->name()).toUtf8().constData())); #endif } icalproperty_add_parameter( p, icalparameter_new_rsvp(attendee->RSVP() ? ICAL_RSVP_TRUE : ICAL_RSVP_FALSE)); icalparameter_partstat status = ICAL_PARTSTAT_NEEDSACTION; switch (attendee->status()) { default: case Attendee::NeedsAction: status = ICAL_PARTSTAT_NEEDSACTION; break; case Attendee::Accepted: status = ICAL_PARTSTAT_ACCEPTED; break; case Attendee::Declined: status = ICAL_PARTSTAT_DECLINED; break; case Attendee::Tentative: status = ICAL_PARTSTAT_TENTATIVE; break; case Attendee::Delegated: status = ICAL_PARTSTAT_DELEGATED; break; case Attendee::Completed: status = ICAL_PARTSTAT_COMPLETED; break; case Attendee::InProcess: status = ICAL_PARTSTAT_INPROCESS; break; } icalproperty_add_parameter(p, icalparameter_new_partstat(status)); icalparameter_role role = ICAL_ROLE_REQPARTICIPANT; switch (attendee->role()) { case Attendee::Chair: role = ICAL_ROLE_CHAIR; break; default: case Attendee::ReqParticipant: role = ICAL_ROLE_REQPARTICIPANT; break; case Attendee::OptParticipant: role = ICAL_ROLE_OPTPARTICIPANT; break; case Attendee::NonParticipant: role = ICAL_ROLE_NONPARTICIPANT; break; } icalproperty_add_parameter(p, icalparameter_new_role(role)); icalparameter_cutype cutype = ICAL_CUTYPE_INDIVIDUAL; switch (attendee->cuType()) { case Attendee::Unknown: cutype = ICAL_CUTYPE_UNKNOWN; break; default: case Attendee::Individual: cutype = ICAL_CUTYPE_INDIVIDUAL; break; case Attendee::Group: cutype = ICAL_CUTYPE_GROUP; break; case Attendee::Resource: cutype = ICAL_CUTYPE_RESOURCE; break; case Attendee::Room: cutype = ICAL_CUTYPE_ROOM; break; } icalproperty_add_parameter(p, icalparameter_new_cutype(cutype)); if (!attendee->uid().isEmpty()) { icalparameter *icalparameter_uid = icalparameter_new_x(attendee->uid().toUtf8().constData()); icalparameter_set_xname(icalparameter_uid, "X-UID"); icalproperty_add_parameter(p, icalparameter_uid); } if (!attendee->delegate().isEmpty()) { icalparameter *icalparameter_delegate = icalparameter_new_delegatedto(attendee->delegate().toUtf8().constData()); icalproperty_add_parameter(p, icalparameter_delegate); } if (!attendee->delegator().isEmpty()) { icalparameter *icalparameter_delegator = icalparameter_new_delegatedfrom(attendee->delegator().toUtf8().constData()); icalproperty_add_parameter(p, icalparameter_delegator); } return p; } icalproperty *ICalFormatImpl::writeAttachment(const Attachment::Ptr &att) { icalattach *attach; if (att->isUri()) { attach = icalattach_new_from_url(att->uri().toUtf8().data()); } else { attach = icalattach_new_from_data((const char *)att->data().constData(), nullptr, nullptr); } icalproperty *p = icalproperty_new_attach(attach); icalattach_unref(attach); if (!att->mimeType().isEmpty()) { icalproperty_add_parameter( p, icalparameter_new_fmttype(att->mimeType().toUtf8().data())); } if (att->isBinary()) { icalproperty_add_parameter(p, icalparameter_new_value(ICAL_VALUE_BINARY)); icalproperty_add_parameter(p, icalparameter_new_encoding(ICAL_ENCODING_BASE64)); } if (att->showInline()) { icalparameter *icalparameter_inline = icalparameter_new_x("inline"); icalparameter_set_xname(icalparameter_inline, "X-CONTENT-DISPOSITION"); icalproperty_add_parameter(p, icalparameter_inline); } if (!att->label().isEmpty()) { icalparameter *icalparameter_label = icalparameter_new_x(att->label().toUtf8().constData()); icalparameter_set_xname(icalparameter_label, "X-LABEL"); icalproperty_add_parameter(p, icalparameter_label); } if (att->isLocal()) { icalparameter *icalparameter_local = icalparameter_new_x("local"); icalparameter_set_xname(icalparameter_local, "X-KONTACT-TYPE"); icalproperty_add_parameter(p, icalparameter_local); } return p; } icalrecurrencetype ICalFormatImpl::writeRecurrenceRule(RecurrenceRule *recur) { icalrecurrencetype r; icalrecurrencetype_clear(&r); switch (recur->recurrenceType()) { case RecurrenceRule::rSecondly: r.freq = ICAL_SECONDLY_RECURRENCE; break; case RecurrenceRule::rMinutely: r.freq = ICAL_MINUTELY_RECURRENCE; break; case RecurrenceRule::rHourly: r.freq = ICAL_HOURLY_RECURRENCE; break; case RecurrenceRule::rDaily: r.freq = ICAL_DAILY_RECURRENCE; break; case RecurrenceRule::rWeekly: r.freq = ICAL_WEEKLY_RECURRENCE; break; case RecurrenceRule::rMonthly: r.freq = ICAL_MONTHLY_RECURRENCE; break; case RecurrenceRule::rYearly: r.freq = ICAL_YEARLY_RECURRENCE; break; default: r.freq = ICAL_NO_RECURRENCE; qCDebug(KCALCORE_LOG) << "no recurrence"; break; } int index = 0; QList bys; QList::ConstIterator it; // Now write out the BY* parts: bys = recur->bySeconds(); index = 0; for (it = bys.constBegin(); it != bys.constEnd(); ++it) { r.by_second[index++] = *it; r.by_second[index++] = static_cast(*it); } bys = recur->byMinutes(); index = 0; for (it = bys.constBegin(); it != bys.constEnd(); ++it) { r.by_minute[index++] = *it; r.by_minute[index++] = static_cast(*it); } bys = recur->byHours(); index = 0; for (it = bys.constBegin(); it != bys.constEnd(); ++it) { r.by_hour[index++] = *it; r.by_hour[index++] = static_cast(*it); } bys = recur->byMonthDays(); index = 0; for (it = bys.constBegin(); it != bys.constEnd(); ++it) { short dShort = static_cast((*it) * 8); r.by_month_day[index++] = static_cast(icalrecurrencetype_day_position(dShort)); } bys = recur->byYearDays(); index = 0; for (it = bys.constBegin(); it != bys.constEnd(); ++it) { r.by_year_day[index++] = static_cast(*it); } bys = recur->byWeekNumbers(); index = 0; for (it = bys.constBegin(); it != bys.constEnd(); ++it) { r.by_week_no[index++] = static_cast(*it); } bys = recur->byMonths(); index = 0; for (it = bys.constBegin(); it != bys.constEnd(); ++it) { r.by_month[index++] = static_cast(*it); } bys = recur->bySetPos(); index = 0; for (it = bys.constBegin(); it != bys.constEnd(); ++it) { r.by_set_pos[index++] = static_cast(*it); } - QList byd = recur->byDays(); + const QList &byd = recur->byDays(); int day; index = 0; for (QList::ConstIterator dit = byd.constBegin(); dit != byd.constEnd(); ++dit) { day = (*dit).day() % 7 + 1; // convert from Monday=1 to Sunday=1 if ((*dit).pos() < 0) { day += (-(*dit).pos()) * 8; day = -day; } else { day += (*dit).pos() * 8; } r.by_day[index++] = static_cast(day); } r.week_start = static_cast(recur->weekStart() % 7 + 1); if (recur->frequency() > 1) { // Dont' write out INTERVAL=1, because that's the default anyway r.interval = static_cast(recur->frequency()); } if (recur->duration() > 0) { r.count = recur->duration(); } else if (recur->duration() == -1) { r.count = 0; } else { if (recur->allDay()) { r.until = writeICalDate(recur->endDt().date()); } else { r.until = writeICalUtcDateTime(recur->endDt()); } } return r; } icalcomponent *ICalFormatImpl::writeAlarm(const Alarm::Ptr &alarm) { if (alarm->enabled()) { alarm->setCustomProperty(APP_NAME_FOR_XPROPERTIES, ENABLED_ALARM_XPROPERTY, QStringLiteral("TRUE")); } else { alarm->setCustomProperty(APP_NAME_FOR_XPROPERTIES, ENABLED_ALARM_XPROPERTY, QStringLiteral("FALSE")); } icalcomponent *a = icalcomponent_new(ICAL_VALARM_COMPONENT); icalproperty_action action; icalattach *attach = nullptr; switch (alarm->type()) { case Alarm::Procedure: action = ICAL_ACTION_PROCEDURE; attach = icalattach_new_from_url( QFile::encodeName(alarm->programFile()).data()); icalcomponent_add_property(a, icalproperty_new_attach(attach)); if (!alarm->programArguments().isEmpty()) { icalcomponent_add_property( a, icalproperty_new_description(alarm->programArguments().toUtf8().constData())); } break; case Alarm::Audio: action = ICAL_ACTION_AUDIO; if (!alarm->audioFile().isEmpty()) { attach = icalattach_new_from_url( QFile::encodeName(alarm->audioFile()).data()); icalcomponent_add_property(a, icalproperty_new_attach(attach)); } break; case Alarm::Email: { action = ICAL_ACTION_EMAIL; const Person::List addresses = alarm->mailAddresses(); for (Person::List::ConstIterator ad = addresses.constBegin(); ad != addresses.constEnd(); ++ad) { if (!(*ad)->email().isEmpty()) { icalproperty *p = icalproperty_new_attendee(QByteArray(QByteArray("MAILTO:") + (*ad)->email().toUtf8()).constData()); if (!(*ad)->name().isEmpty()) { icalproperty_add_parameter(p, #if defined(USE_ICAL_3) icalparameter_new_cn((*ad)->name().toUtf8().constData())); #else icalparameter_new_cn(quoteForParam((*ad)->name()).toUtf8().constData())); #endif } icalcomponent_add_property(a, p); } } icalcomponent_add_property( a, icalproperty_new_summary(alarm->mailSubject().toUtf8().constData())); icalcomponent_add_property( a, icalproperty_new_description(alarm->mailText().toUtf8().constData())); const QStringList attachments = alarm->mailAttachments(); if (!attachments.isEmpty()) { for (QStringList::const_iterator at = attachments.constBegin(), end = attachments.constEnd(); at != end; ++at) { attach = icalattach_new_from_url(QFile::encodeName(*at).data()); icalcomponent_add_property(a, icalproperty_new_attach(attach)); } } break; } case Alarm::Display: action = ICAL_ACTION_DISPLAY; icalcomponent_add_property( a, icalproperty_new_description(alarm->text().toUtf8().constData())); break; case Alarm::Invalid: default: qCDebug(KCALCORE_LOG) << "Unknown type of alarm"; action = ICAL_ACTION_NONE; break; } icalcomponent_add_property(a, icalproperty_new_action(action)); // Trigger time icaltriggertype trigger; if (alarm->hasTime()) { trigger.time = writeICalUtcDateTime(alarm->time(), false); trigger.duration = icaldurationtype_null_duration(); } else { trigger.time = icaltime_null_time(); Duration offset; if (alarm->hasStartOffset()) { offset = alarm->startOffset(); } else { offset = alarm->endOffset(); } trigger.duration = writeICalDuration(offset); } icalproperty *p = icalproperty_new_trigger(trigger); if (alarm->hasEndOffset()) { icalproperty_add_parameter(p, icalparameter_new_related(ICAL_RELATED_END)); } icalcomponent_add_property(a, p); // Repeat count and duration if (alarm->repeatCount()) { icalcomponent_add_property( a, icalproperty_new_repeat(alarm->repeatCount())); icalcomponent_add_property( a, icalproperty_new_duration(writeICalDuration(alarm->snoozeTime()))); } // Custom properties const QMap custom = alarm->customProperties(); for (QMap::ConstIterator c = custom.begin(); c != custom.end(); ++c) { icalproperty *p = icalproperty_new_x(c.value().toUtf8().constData()); icalproperty_set_x_name(p, c.key().constData()); icalcomponent_add_property(a, p); } icalattach_unref(attach); return a; } Todo::Ptr ICalFormatImpl::readTodo(icalcomponent *vtodo, const ICalTimeZoneCache *tzlist) { Todo::Ptr todo(new Todo); readIncidence(vtodo, todo, tzlist); icalproperty *p = icalcomponent_get_first_property(vtodo, ICAL_ANY_PROPERTY); while (p) { icalproperty_kind kind = icalproperty_isa(p); switch (kind) { case ICAL_DUE_PROPERTY: { // due date/time bool allDay = false; QDateTime kdt = readICalDateTimeProperty(p, tzlist, false, &allDay); todo->setDtDue(kdt, true); todo->setAllDay(allDay); break; } case ICAL_COMPLETED_PROPERTY: // completion date/time todo->setCompleted(readICalDateTimeProperty(p, tzlist)); break; case ICAL_PERCENTCOMPLETE_PROPERTY: // Percent completed todo->setPercentComplete(icalproperty_get_percentcomplete(p)); break; case ICAL_RELATEDTO_PROPERTY: // related todo (parent) todo->setRelatedTo(QString::fromUtf8(icalproperty_get_relatedto(p))); d->mTodosRelate.append(todo); break; case ICAL_DTSTART_PROPERTY: // Flag that todo has start date. Value is read in by readIncidence(). if (!todo->comments().filter(QStringLiteral("NoStartDate")).isEmpty()) { todo->setDtStart(QDateTime()); } break; case ICAL_X_PROPERTY: { const char *name = icalproperty_get_x_name(p); if (QLatin1String(name) == QLatin1String("X-KDE-LIBKCAL-DTRECURRENCE")) { const QDateTime dateTime = readICalDateTimeProperty(p, tzlist); if (dateTime.isValid()) { todo->setDtRecurrence(dateTime); } else { qCDebug(KCALCORE_LOG) << "Invalid dateTime"; } } } break; default: // TODO: do something about unknown properties? break; } p = icalcomponent_get_next_property(vtodo, ICAL_ANY_PROPERTY); } if (d->mCompat) { d->mCompat->fixEmptySummary(todo); } todo->resetDirtyFields(); return todo; } Event::Ptr ICalFormatImpl::readEvent(icalcomponent *vevent, const ICalTimeZoneCache *tzlist) { Event::Ptr event(new Event); readIncidence(vevent, event, tzlist); icalproperty *p = icalcomponent_get_first_property(vevent, ICAL_ANY_PROPERTY); bool dtEndProcessed = false; while (p) { icalproperty_kind kind = icalproperty_isa(p); switch (kind) { case ICAL_DTEND_PROPERTY: { // end date and time bool allDay = false; QDateTime kdt = readICalDateTimeProperty(p, tzlist, false, &allDay); if (allDay) { // End date is non-inclusive QDate endDate = kdt.date().addDays(-1); if (d->mCompat) { d->mCompat->fixFloatingEnd(endDate); } if (endDate < event->dtStart().date()) { endDate = event->dtStart().date(); } event->setDtEnd(QDateTime(endDate, {}, event->dtStart().timeZone())); event->setAllDay(true); } else { event->setDtEnd(kdt); event->setAllDay(false); } dtEndProcessed = true; break; } case ICAL_RELATEDTO_PROPERTY: // related event (parent) event->setRelatedTo(QString::fromUtf8(icalproperty_get_relatedto(p))); d->mEventsRelate.append(event); break; case ICAL_TRANSP_PROPERTY: { // Transparency icalproperty_transp transparency = icalproperty_get_transp(p); if (transparency == ICAL_TRANSP_TRANSPARENT) { event->setTransparency(Event::Transparent); } else { event->setTransparency(Event::Opaque); } break; } default: // TODO: do something about unknown properties? break; } p = icalcomponent_get_next_property(vevent, ICAL_ANY_PROPERTY); } // according to rfc2445 the dtend shouldn't be written when it equals // start date. so assign one equal to start date. if (!dtEndProcessed && !event->hasDuration()) { event->setDtEnd(event->dtStart()); } QString msade = event->nonKDECustomProperty("X-MICROSOFT-CDO-ALLDAYEVENT"); if (!msade.isEmpty()) { bool allDay = (msade == QLatin1String("TRUE")); event->setAllDay(allDay); } if (d->mCompat) { d->mCompat->fixEmptySummary(event); } event->resetDirtyFields(); return event; } FreeBusy::Ptr ICalFormatImpl::readFreeBusy(icalcomponent *vfreebusy) { FreeBusy::Ptr freebusy(new FreeBusy); d->readIncidenceBase(vfreebusy, freebusy); icalproperty *p = icalcomponent_get_first_property(vfreebusy, ICAL_ANY_PROPERTY); FreeBusyPeriod::List periods; while (p) { icalproperty_kind kind = icalproperty_isa(p); switch (kind) { case ICAL_DTSTART_PROPERTY: // start date and time (UTC) freebusy->setDtStart(readICalUtcDateTimeProperty(p, nullptr)); break; case ICAL_DTEND_PROPERTY: // end Date and Time (UTC) freebusy->setDtEnd(readICalUtcDateTimeProperty(p, nullptr)); break; case ICAL_FREEBUSY_PROPERTY: { //Any FreeBusy Times (UTC) icalperiodtype icalperiod = icalproperty_get_freebusy(p); QDateTime period_start = readICalUtcDateTime(p, icalperiod.start); FreeBusyPeriod period; if (!icaltime_is_null_time(icalperiod.end)) { QDateTime period_end = readICalUtcDateTime(p, icalperiod.end); period = FreeBusyPeriod(period_start, period_end); } else { Duration duration(readICalDuration(icalperiod.duration)); period = FreeBusyPeriod(period_start, duration); } icalparameter *param = icalproperty_get_first_parameter(p, ICAL_FBTYPE_PARAMETER); if (param) { icalparameter_fbtype fbType = icalparameter_get_fbtype(param); switch (fbType) { case ICAL_FBTYPE_FREE: period.setType(FreeBusyPeriod::Free); break; case ICAL_FBTYPE_BUSY: period.setType(FreeBusyPeriod::Busy); break; case ICAL_FBTYPE_BUSYTENTATIVE: period.setType(FreeBusyPeriod::BusyTentative); break; case ICAL_FBTYPE_BUSYUNAVAILABLE: period.setType(FreeBusyPeriod::BusyUnavailable); break; case ICAL_FBTYPE_X: period.setType(FreeBusyPeriod::Unknown); break; case ICAL_FBTYPE_NONE: period.setType(FreeBusyPeriod::Free); break; } } param = icalproperty_get_first_parameter(p, ICAL_X_PARAMETER); while (param) { if (strncmp(icalparameter_get_xname(param), "X-SUMMARY", 9) == 0) { period.setSummary(QString::fromUtf8( QByteArray::fromBase64(icalparameter_get_xvalue(param)))); } if (strncmp(icalparameter_get_xname(param), "X-LOCATION", 10) == 0) { period.setLocation(QString::fromUtf8( QByteArray::fromBase64(icalparameter_get_xvalue(param)))); } param = icalproperty_get_next_parameter(p, ICAL_X_PARAMETER); } periods.append(period); break; } default: // TODO: do something about unknown properties? break; } p = icalcomponent_get_next_property(vfreebusy, ICAL_ANY_PROPERTY); } freebusy->addPeriods(periods); freebusy->resetDirtyFields(); return freebusy; } Journal::Ptr ICalFormatImpl::readJournal(icalcomponent *vjournal, const ICalTimeZoneCache *tzList) { Journal::Ptr journal(new Journal); readIncidence(vjournal, journal, tzList); journal->resetDirtyFields(); return journal; } Attendee::Ptr ICalFormatImpl::readAttendee(icalproperty *attendee) { // the following is a hack to support broken calendars (like WebCalendar 1.0.x) // that include non-RFC-compliant attendees. Otherwise libical 0.42 asserts. if (!icalproperty_get_value(attendee)) { return Attendee::Ptr(); } icalparameter *p = nullptr; QString email = QString::fromUtf8(icalproperty_get_attendee(attendee)); if (email.startsWith(QLatin1String("mailto:"), Qt::CaseInsensitive)) { email = email.mid(7); } // libical may return everything after ATTENDEE tag if the rest is // not meaningful. Verify the address to filter out these cases. if (!Person::isValidEmail(email)) { return Attendee::Ptr(); } QString name; QString uid; p = icalproperty_get_first_parameter(attendee, ICAL_CN_PARAMETER); if (p) { name = QString::fromUtf8(icalparameter_get_cn(p)); } else { } bool rsvp = false; p = icalproperty_get_first_parameter(attendee, ICAL_RSVP_PARAMETER); if (p) { icalparameter_rsvp rsvpParameter = icalparameter_get_rsvp(p); if (rsvpParameter == ICAL_RSVP_TRUE) { rsvp = true; } } Attendee::PartStat status = Attendee::NeedsAction; p = icalproperty_get_first_parameter(attendee, ICAL_PARTSTAT_PARAMETER); if (p) { icalparameter_partstat partStatParameter = icalparameter_get_partstat(p); switch (partStatParameter) { default: case ICAL_PARTSTAT_NEEDSACTION: status = Attendee::NeedsAction; break; case ICAL_PARTSTAT_ACCEPTED: status = Attendee::Accepted; break; case ICAL_PARTSTAT_DECLINED: status = Attendee::Declined; break; case ICAL_PARTSTAT_TENTATIVE: status = Attendee::Tentative; break; case ICAL_PARTSTAT_DELEGATED: status = Attendee::Delegated; break; case ICAL_PARTSTAT_COMPLETED: status = Attendee::Completed; break; case ICAL_PARTSTAT_INPROCESS: status = Attendee::InProcess; break; } } Attendee::Role role = Attendee::ReqParticipant; p = icalproperty_get_first_parameter(attendee, ICAL_ROLE_PARAMETER); if (p) { icalparameter_role roleParameter = icalparameter_get_role(p); switch (roleParameter) { case ICAL_ROLE_CHAIR: role = Attendee::Chair; break; default: case ICAL_ROLE_REQPARTICIPANT: role = Attendee::ReqParticipant; break; case ICAL_ROLE_OPTPARTICIPANT: role = Attendee::OptParticipant; break; case ICAL_ROLE_NONPARTICIPANT: role = Attendee::NonParticipant; break; } } Attendee::CuType cuType = Attendee::Individual; p = icalproperty_get_first_parameter(attendee, ICAL_CUTYPE_PARAMETER); if (p) { icalparameter_cutype cutypeParameter = icalparameter_get_cutype(p); switch (cutypeParameter) { case ICAL_CUTYPE_X: case ICAL_CUTYPE_UNKNOWN: cuType = Attendee::Unknown; break; default: case ICAL_CUTYPE_NONE: case ICAL_CUTYPE_INDIVIDUAL: cuType = Attendee::Individual; break; case ICAL_CUTYPE_GROUP: cuType = Attendee::Group; break; case ICAL_CUTYPE_RESOURCE: cuType = Attendee::Resource; break; case ICAL_CUTYPE_ROOM: cuType = Attendee::Room; break; } } p = icalproperty_get_first_parameter(attendee, ICAL_X_PARAMETER); QMap custom; while (p) { QString xname = QString::fromLatin1(icalparameter_get_xname(p)).toUpper(); QString xvalue = QString::fromUtf8(icalparameter_get_xvalue(p)); if (xname == QLatin1String("X-UID")) { uid = xvalue; } else { custom[xname.toUtf8()] = xvalue; } p = icalproperty_get_next_parameter(attendee, ICAL_X_PARAMETER); } Attendee::Ptr a(new Attendee(name, email, rsvp, status, role, uid)); a->setCuType(cuType); a->customProperties().setCustomProperties(custom); p = icalproperty_get_first_parameter(attendee, ICAL_DELEGATEDTO_PARAMETER); if (p) { a->setDelegate(QLatin1String(icalparameter_get_delegatedto(p))); } p = icalproperty_get_first_parameter(attendee, ICAL_DELEGATEDFROM_PARAMETER); if (p) { a->setDelegator(QLatin1String(icalparameter_get_delegatedfrom(p))); } return a; } Person::Ptr ICalFormatImpl::readOrganizer(icalproperty *organizer) { QString email = QString::fromUtf8(icalproperty_get_organizer(organizer)); if (email.startsWith(QLatin1String("mailto:"), Qt::CaseInsensitive)) { email = email.mid(7); } QString cn; icalparameter *p = icalproperty_get_first_parameter(organizer, ICAL_CN_PARAMETER); if (p) { cn = QString::fromUtf8(icalparameter_get_cn(p)); } Person::Ptr org(new Person(cn, email)); // TODO: Treat sent-by, dir and language here, too return org; } Attachment::Ptr ICalFormatImpl::readAttachment(icalproperty *attach) { Attachment::Ptr attachment; QByteArray p; icalvalue *value = icalproperty_get_value(attach); switch (icalvalue_isa(value)) { case ICAL_ATTACH_VALUE: { icalattach *a = icalproperty_get_attach(attach); if (!icalattach_get_is_url(a)) { p = QByteArray(reinterpret_cast(icalattach_get_data(a))); if (!p.isEmpty()) { attachment = Attachment::Ptr(new Attachment(p)); } } else { p = icalattach_get_url(a); if (!p.isEmpty()) { attachment = Attachment::Ptr(new Attachment(QString::fromUtf8(p))); } } break; } case ICAL_BINARY_VALUE: { icalattach *a = icalproperty_get_attach(attach); p = QByteArray(reinterpret_cast(icalattach_get_data(a))); if (!p.isEmpty()) { attachment = Attachment::Ptr(new Attachment(p)); } break; } case ICAL_URI_VALUE: p = icalvalue_get_uri(value); attachment = Attachment::Ptr(new Attachment(QString::fromUtf8(p))); break; default: break; } if (attachment) { icalparameter *p = icalproperty_get_first_parameter(attach, ICAL_FMTTYPE_PARAMETER); if (p) { attachment->setMimeType(QLatin1String(icalparameter_get_fmttype(p))); } p = icalproperty_get_first_parameter(attach, ICAL_X_PARAMETER); while (p) { QString xname = QString::fromLatin1(icalparameter_get_xname(p)).toUpper(); QString xvalue = QString::fromUtf8(icalparameter_get_xvalue(p)); if (xname == QLatin1String("X-CONTENT-DISPOSITION")) { attachment->setShowInline(xvalue.toLower() == QLatin1String("inline")); } else if (xname == QLatin1String("X-LABEL")) { attachment->setLabel(xvalue); } else if (xname == QLatin1String("X-KONTACT-TYPE")) { attachment->setLocal(xvalue.toLower() == QLatin1String("local")); } p = icalproperty_get_next_parameter(attach, ICAL_X_PARAMETER); } p = icalproperty_get_first_parameter(attach, ICAL_X_PARAMETER); while (p) { if (strncmp(icalparameter_get_xname(p), "X-LABEL", 7) == 0) { attachment->setLabel(QString::fromUtf8(icalparameter_get_xvalue(p))); } p = icalproperty_get_next_parameter(attach, ICAL_X_PARAMETER); } } return attachment; } void ICalFormatImpl::readIncidence(icalcomponent *parent, const Incidence::Ptr &incidence, const ICalTimeZoneCache *tzlist) { d->readIncidenceBase(parent, incidence); icalproperty *p = icalcomponent_get_first_property(parent, ICAL_ANY_PROPERTY); const char *text; int intvalue, inttext; icaldurationtype icalduration; QDateTime kdt; QDateTime dtstamp; QStringList categories; while (p) { icalproperty_kind kind = icalproperty_isa(p); switch (kind) { case ICAL_CREATED_PROPERTY: incidence->setCreated(readICalDateTimeProperty(p, tzlist)); break; case ICAL_DTSTAMP_PROPERTY: dtstamp = readICalDateTimeProperty(p, tzlist); break; case ICAL_SEQUENCE_PROPERTY: // sequence intvalue = icalproperty_get_sequence(p); incidence->setRevision(intvalue); break; case ICAL_LASTMODIFIED_PROPERTY: // last modification UTC date/time incidence->setLastModified(readICalDateTimeProperty(p, tzlist)); break; case ICAL_DTSTART_PROPERTY: { // start date and time bool allDay = false; kdt = readICalDateTimeProperty(p, tzlist, false, &allDay); incidence->setDtStart(kdt); incidence->setAllDay(allDay); break; } case ICAL_DURATION_PROPERTY: // start date and time icalduration = icalproperty_get_duration(p); incidence->setDuration(readICalDuration(icalduration)); break; case ICAL_DESCRIPTION_PROPERTY: { // description QString textStr = QString::fromUtf8(icalproperty_get_description(p)); if (!textStr.isEmpty()) { QString valStr = QString::fromUtf8( icalproperty_get_parameter_as_string(p, "X-KDE-TEXTFORMAT")); if (!valStr.compare(QLatin1String("HTML"), Qt::CaseInsensitive)) { incidence->setDescription(textStr, true); } else { incidence->setDescription(textStr, false); } } } break; case ICAL_SUMMARY_PROPERTY: { // summary QString textStr = QString::fromUtf8(icalproperty_get_summary(p)); if (!textStr.isEmpty()) { QString valStr = QString::fromUtf8( icalproperty_get_parameter_as_string(p, "X-KDE-TEXTFORMAT")); if (!valStr.compare(QLatin1String("HTML"), Qt::CaseInsensitive)) { incidence->setSummary(textStr, true); } else { incidence->setSummary(textStr, false); } } } break; case ICAL_LOCATION_PROPERTY: { // location if (!icalproperty_get_value(p)) { //Fix for #191472. This is a pre-crash guard in case libical was //compiled in superstrict mode (--enable-icalerrors-are-fatal) //TODO: pre-crash guard other property getters too. break; } QString textStr = QString::fromUtf8(icalproperty_get_location(p)); if (!textStr.isEmpty()) { QString valStr = QString::fromUtf8( icalproperty_get_parameter_as_string(p, "X-KDE-TEXTFORMAT")); if (!valStr.compare(QLatin1String("HTML"), Qt::CaseInsensitive)) { incidence->setLocation(textStr, true); } else { incidence->setLocation(textStr, false); } } } break; case ICAL_STATUS_PROPERTY: { // status Incidence::Status stat; switch (icalproperty_get_status(p)) { case ICAL_STATUS_TENTATIVE: stat = Incidence::StatusTentative; break; case ICAL_STATUS_CONFIRMED: stat = Incidence::StatusConfirmed; break; case ICAL_STATUS_COMPLETED: stat = Incidence::StatusCompleted; break; case ICAL_STATUS_NEEDSACTION: stat = Incidence::StatusNeedsAction; break; case ICAL_STATUS_CANCELLED: stat = Incidence::StatusCanceled; break; case ICAL_STATUS_INPROCESS: stat = Incidence::StatusInProcess; break; case ICAL_STATUS_DRAFT: stat = Incidence::StatusDraft; break; case ICAL_STATUS_FINAL: stat = Incidence::StatusFinal; break; case ICAL_STATUS_X: incidence->setCustomStatus( QString::fromUtf8(icalvalue_get_x(icalproperty_get_value(p)))); stat = Incidence::StatusX; break; case ICAL_STATUS_NONE: default: stat = Incidence::StatusNone; break; } if (stat != Incidence::StatusX) { incidence->setStatus(stat); } break; } case ICAL_GEO_PROPERTY: { // geo icalgeotype geo = icalproperty_get_geo(p); incidence->setGeoLatitude(geo.lat); incidence->setGeoLongitude(geo.lon); incidence->setHasGeo(true); break; } case ICAL_PRIORITY_PROPERTY: // priority intvalue = icalproperty_get_priority(p); if (d->mCompat) { intvalue = d->mCompat->fixPriority(intvalue); } incidence->setPriority(intvalue); break; case ICAL_CATEGORIES_PROPERTY: { // categories // We have always supported multiple CATEGORIES properties per component // even though the RFC seems to indicate only 1 is permitted. // We can't change that -- in order to retain backwards compatibility. text = icalproperty_get_categories(p); const QString val = QString::fromUtf8(text); const QStringList lstVal = val.split(QLatin1Char(','), QString::SkipEmptyParts); for (const QString &cat : lstVal) { // ensure no duplicates if (!categories.contains(cat)) { categories.append(cat); } } break; } case ICAL_RECURRENCEID_PROPERTY: // recurrenceId kdt = readICalDateTimeProperty(p, tzlist); if (kdt.isValid()) { incidence->setRecurrenceId(kdt); const icalparameter *param = icalproperty_get_first_parameter(p, ICAL_RANGE_PARAMETER); if (param && icalparameter_get_range(param) == ICAL_RANGE_THISANDFUTURE) { incidence->setThisAndFuture(true); } else { // A workaround for a bug in libical (https://github.com/libical/libical/issues/185) // If a recurrenceId has both tzid and range, both parameters end up in the tzid. // This results in invalid tzid's like: "Europe/Berlin;RANGE=THISANDFUTURE" const icalparameter *param = icalproperty_get_first_parameter(p, ICAL_TZID_PARAMETER); QString tzid = QString::fromLatin1(icalparameter_get_tzid(param)); const QStringList parts = tzid.toLower().split(QLatin1Char(';')); for (const QString &part : parts) { if (part == QLatin1String("range=thisandfuture")) { incidence->setThisAndFuture(true); break; } } } } break; case ICAL_RRULE_PROPERTY: readRecurrenceRule(p, incidence); break; case ICAL_RDATE_PROPERTY: { bool allDay = false; kdt = readICalDateTimeProperty(p, tzlist, false, &allDay); if (kdt.isValid()) { if (allDay) { incidence->recurrence()->addRDate(kdt.date()); } else { incidence->recurrence()->addRDateTime(kdt); } } else { // TODO: RDates as period are not yet implemented! } break; } case ICAL_EXRULE_PROPERTY: readExceptionRule(p, incidence); break; case ICAL_EXDATE_PROPERTY: { bool allDay = false; kdt = readICalDateTimeProperty(p, tzlist, false, &allDay); if (allDay) { incidence->recurrence()->addExDate(kdt.date()); } else { incidence->recurrence()->addExDateTime(kdt); } break; } case ICAL_CLASS_PROPERTY: inttext = icalproperty_get_class(p); if (inttext == ICAL_CLASS_PUBLIC) { incidence->setSecrecy(Incidence::SecrecyPublic); } else if (inttext == ICAL_CLASS_CONFIDENTIAL) { incidence->setSecrecy(Incidence::SecrecyConfidential); } else { incidence->setSecrecy(Incidence::SecrecyPrivate); } break; case ICAL_ATTACH_PROPERTY: // attachments incidence->addAttachment(readAttachment(p)); break; default: // TODO: do something about unknown properties? break; } p = icalcomponent_get_next_property(parent, ICAL_ANY_PROPERTY); } // Set the scheduling ID const QString uid = incidence->customProperty("LIBKCAL", "ID"); if (!uid.isNull()) { // The UID stored in incidencebase is actually the scheduling ID // It has to be stored in the iCal UID component for compatibility // with other iCal applications incidence->setSchedulingID(incidence->uid(), uid); } // Now that recurrence and exception stuff is completely set up, // do any backwards compatibility adjustments. if (incidence->recurs() && d->mCompat) { d->mCompat->fixRecurrence(incidence); } // add categories incidence->setCategories(categories); // iterate through all alarms for (icalcomponent *alarm = icalcomponent_get_first_component(parent, ICAL_VALARM_COMPONENT); alarm; alarm = icalcomponent_get_next_component(parent, ICAL_VALARM_COMPONENT)) { readAlarm(alarm, incidence); } if (d->mCompat) { // Fix incorrect alarm settings by other applications (like outloook 9) d->mCompat->fixAlarms(incidence); d->mCompat->setCreatedToDtStamp(incidence, dtstamp); } } //@cond PRIVATE void ICalFormatImpl::Private::readIncidenceBase(icalcomponent *parent, const IncidenceBase::Ptr &incidenceBase) { icalproperty *p = icalcomponent_get_first_property(parent, ICAL_ANY_PROPERTY); bool uidProcessed = false; while (p) { icalproperty_kind kind = icalproperty_isa(p); switch (kind) { case ICAL_UID_PROPERTY: // unique id uidProcessed = true; incidenceBase->setUid(QString::fromUtf8(icalproperty_get_uid(p))); break; case ICAL_ORGANIZER_PROPERTY: // organizer incidenceBase->setOrganizer(mImpl->readOrganizer(p)); break; case ICAL_ATTENDEE_PROPERTY: // attendee incidenceBase->addAttendee(mImpl->readAttendee(p)); break; case ICAL_COMMENT_PROPERTY: incidenceBase->addComment( QString::fromUtf8(icalproperty_get_comment(p))); break; case ICAL_CONTACT_PROPERTY: incidenceBase->addContact( QString::fromUtf8(icalproperty_get_contact(p))); break; case ICAL_URL_PROPERTY: incidenceBase->setUrl( QUrl(QString::fromUtf8(icalproperty_get_url(p)))); break; default: break; } p = icalcomponent_get_next_property(parent, ICAL_ANY_PROPERTY); } if (!uidProcessed) { qCWarning(KCALCORE_LOG) << "The incidence didn't have any UID! Report a bug " << "to the application that generated this file." << endl; // Our in-memory incidence has a random uid generated in Event's ctor. // Make it empty so it matches what's in the file: incidenceBase->setUid(QString()); // Otherwise, next time we read the file, this function will return // an event with another random uid and we will have two events in the calendar. } // custom properties readCustomProperties(parent, incidenceBase.data()); } void ICalFormatImpl::Private::readCustomProperties(icalcomponent *parent, CustomProperties *properties) { QByteArray property; QString value, parameters; icalproperty *p = icalcomponent_get_first_property(parent, ICAL_X_PROPERTY); icalparameter *param = nullptr; while (p) { QString nvalue = QString::fromUtf8(icalproperty_get_x(p)); if (nvalue.isEmpty()) { icalvalue *value = icalproperty_get_value(p); if (icalvalue_isa(value) == ICAL_TEXT_VALUE) { // Calling icalvalue_get_text( value ) on a datetime value crashes. nvalue = QString::fromUtf8(icalvalue_get_text(value)); } else { p = icalcomponent_get_next_property(parent, ICAL_X_PROPERTY); continue; } } const char *name = icalproperty_get_x_name(p); QByteArray nproperty(name); if (property != nproperty) { // New property if (!property.isEmpty()) { properties->setNonKDECustomProperty(property, value, parameters); } property = name; value = nvalue; QStringList parametervalues; for (param = icalproperty_get_first_parameter(p, ICAL_ANY_PARAMETER); param; param = icalproperty_get_next_parameter(p, ICAL_ANY_PARAMETER)) { // 'c' is owned by ical library => all we need to do is just use it const char *c = icalparameter_as_ical_string(param); parametervalues.push_back(QLatin1String(c)); } parameters = parametervalues.join(QLatin1Char(';')); } else { value = value.append(QLatin1Char(',')).append(nvalue); } p = icalcomponent_get_next_property(parent, ICAL_X_PROPERTY); } if (!property.isEmpty()) { properties->setNonKDECustomProperty(property, value, parameters); } } //@endcond void ICalFormatImpl::readRecurrenceRule(icalproperty *rrule, const Incidence::Ptr &incidence) { Recurrence *recur = incidence->recurrence(); struct icalrecurrencetype r = icalproperty_get_rrule(rrule); // dumpIcalRecurrence(r); RecurrenceRule *recurrule = new RecurrenceRule(/*incidence*/); recurrule->setStartDt(incidence->dtStart()); readRecurrence(r, recurrule); recur->addRRule(recurrule); } void ICalFormatImpl::readExceptionRule(icalproperty *rrule, const Incidence::Ptr &incidence) { struct icalrecurrencetype r = icalproperty_get_exrule(rrule); // dumpIcalRecurrence(r); RecurrenceRule *recurrule = new RecurrenceRule(/*incidence*/); recurrule->setStartDt(incidence->dtStart()); readRecurrence(r, recurrule); Recurrence *recur = incidence->recurrence(); recur->addExRule(recurrule); } void ICalFormatImpl::readRecurrence(const struct icalrecurrencetype &r, RecurrenceRule *recur) { // Generate the RRULE string recur->setRRule( QLatin1String(icalrecurrencetype_as_string(const_cast(&r)))); // Period switch (r.freq) { case ICAL_SECONDLY_RECURRENCE: recur->setRecurrenceType(RecurrenceRule::rSecondly); break; case ICAL_MINUTELY_RECURRENCE: recur->setRecurrenceType(RecurrenceRule::rMinutely); break; case ICAL_HOURLY_RECURRENCE: recur->setRecurrenceType(RecurrenceRule::rHourly); break; case ICAL_DAILY_RECURRENCE: recur->setRecurrenceType(RecurrenceRule::rDaily); break; case ICAL_WEEKLY_RECURRENCE: recur->setRecurrenceType(RecurrenceRule::rWeekly); break; case ICAL_MONTHLY_RECURRENCE: recur->setRecurrenceType(RecurrenceRule::rMonthly); break; case ICAL_YEARLY_RECURRENCE: recur->setRecurrenceType(RecurrenceRule::rYearly); break; case ICAL_NO_RECURRENCE: default: recur->setRecurrenceType(RecurrenceRule::rNone); } // Frequency recur->setFrequency(r.interval); // Duration & End Date if (!icaltime_is_null_time(r.until)) { icaltimetype t = r.until; recur->setEndDt(readICalUtcDateTime(nullptr, t)); } else { if (r.count == 0) { recur->setDuration(-1); } else { recur->setDuration(r.count); } } // Week start setting short wkst = static_cast((r.week_start + 5) % 7 + 1); recur->setWeekStart(wkst); // And now all BY* QList lst; int i; int index = 0; //@cond PRIVATE #define readSetByList( rrulecomp, setfunc ) \ index = 0; \ lst.clear(); \ while ( ( i = r.rrulecomp[index++] ) != ICAL_RECURRENCE_ARRAY_MAX ) { \ lst.append( i ); \ } \ if ( !lst.isEmpty() ) { \ recur->setfunc( lst ); \ } //@endcond // BYSECOND, MINUTE and HOUR, MONTHDAY, YEARDAY, WEEKNUMBER, MONTH // and SETPOS are standard int lists, so we can treat them with the // same macro readSetByList(by_second, setBySeconds); readSetByList(by_minute, setByMinutes); readSetByList(by_hour, setByHours); readSetByList(by_month_day, setByMonthDays); readSetByList(by_year_day, setByYearDays); readSetByList(by_week_no, setByWeekNumbers); readSetByList(by_month, setByMonths); readSetByList(by_set_pos, setBySetPos); #undef readSetByList // BYDAY is a special case, since it's not an int list QList wdlst; short day; index = 0; while ((day = r.by_day[index++]) != ICAL_RECURRENCE_ARRAY_MAX) { RecurrenceRule::WDayPos pos; pos.setDay(static_cast((icalrecurrencetype_day_day_of_week(day) + 5) % 7 + 1)); pos.setPos(icalrecurrencetype_day_position(day)); wdlst.append(pos); } if (!wdlst.isEmpty()) { recur->setByDays(wdlst); } // TODO: Store all X- fields of the RRULE inside the recurrence (so they are // preserved } void ICalFormatImpl::readAlarm(icalcomponent *alarm, const Incidence::Ptr &incidence) { Alarm::Ptr ialarm = incidence->newAlarm(); ialarm->setRepeatCount(0); ialarm->setEnabled(true); // Determine the alarm's action type icalproperty *p = icalcomponent_get_first_property(alarm, ICAL_ACTION_PROPERTY); Alarm::Type type = Alarm::Display; icalproperty_action action = ICAL_ACTION_DISPLAY; if (!p) { qCDebug(KCALCORE_LOG) << "Unknown type of alarm, using default"; // TODO: do something about unknown alarm type? } else { action = icalproperty_get_action(p); switch (action) { case ICAL_ACTION_DISPLAY: type = Alarm::Display; break; case ICAL_ACTION_AUDIO: type = Alarm::Audio; break; case ICAL_ACTION_PROCEDURE: type = Alarm::Procedure; break; case ICAL_ACTION_EMAIL: type = Alarm::Email; break; default: break; // TODO: do something about invalid alarm type? } } ialarm->setType(type); p = icalcomponent_get_first_property(alarm, ICAL_ANY_PROPERTY); while (p) { icalproperty_kind kind = icalproperty_isa(p); switch (kind) { case ICAL_TRIGGER_PROPERTY: { icaltriggertype trigger = icalproperty_get_trigger(p); if (!icaltime_is_null_time(trigger.time)) { //set the trigger to a specific time (which is not in rfc2445, btw) ialarm->setTime(readICalUtcDateTime(p, trigger.time)); } else { //set the trigger to an offset from the incidence start or end time. if (!icaldurationtype_is_bad_duration(trigger.duration)) { Duration duration(readICalDuration(trigger.duration)); icalparameter *param = icalproperty_get_first_parameter(p, ICAL_RELATED_PARAMETER); if (param && icalparameter_get_related(param) == ICAL_RELATED_END) { ialarm->setEndOffset(duration); } else { ialarm->setStartOffset(duration); } } else { // a bad duration was encountered, just set a 0 duration from start ialarm->setStartOffset(Duration(0)); } } break; } case ICAL_DURATION_PROPERTY: { icaldurationtype duration = icalproperty_get_duration(p); ialarm->setSnoozeTime(readICalDuration(duration)); break; } case ICAL_REPEAT_PROPERTY: ialarm->setRepeatCount(icalproperty_get_repeat(p)); break; case ICAL_DESCRIPTION_PROPERTY: { // Only in DISPLAY and EMAIL and PROCEDURE alarms QString description = QString::fromUtf8(icalproperty_get_description(p)); switch (action) { case ICAL_ACTION_DISPLAY: ialarm->setText(description); break; case ICAL_ACTION_PROCEDURE: ialarm->setProgramArguments(description); break; case ICAL_ACTION_EMAIL: ialarm->setMailText(description); break; default: break; } break; } case ICAL_SUMMARY_PROPERTY: // Only in EMAIL alarm ialarm->setMailSubject(QString::fromUtf8(icalproperty_get_summary(p))); break; case ICAL_ATTENDEE_PROPERTY: { // Only in EMAIL alarm QString email = QString::fromUtf8(icalproperty_get_attendee(p)); if (email.startsWith(QLatin1String("mailto:"), Qt::CaseInsensitive)) { email = email.mid(7); } QString name; icalparameter *param = icalproperty_get_first_parameter(p, ICAL_CN_PARAMETER); if (param) { name = QString::fromUtf8(icalparameter_get_cn(param)); } ialarm->addMailAddress(Person::Ptr(new Person(name, email))); break; } case ICAL_ATTACH_PROPERTY: { // Only in AUDIO and EMAIL and PROCEDURE alarms Attachment::Ptr attach = readAttachment(p); if (attach && attach->isUri()) { switch (action) { case ICAL_ACTION_AUDIO: ialarm->setAudioFile(attach->uri()); break; case ICAL_ACTION_PROCEDURE: ialarm->setProgramFile(attach->uri()); break; case ICAL_ACTION_EMAIL: ialarm->addMailAttachment(attach->uri()); break; default: break; } } else { qCDebug(KCALCORE_LOG) << "Alarm attachments currently only support URIs," << "but no binary data"; } break; } default: break; } p = icalcomponent_get_next_property(alarm, ICAL_ANY_PROPERTY); } // custom properties d->readCustomProperties(alarm, ialarm.data()); QString locationRadius = ialarm->nonKDECustomProperty("X-LOCATION-RADIUS"); if (!locationRadius.isEmpty()) { ialarm->setLocationRadius(locationRadius.toInt()); ialarm->setHasLocationRadius(true); } if (ialarm->customProperty(APP_NAME_FOR_XPROPERTIES, ENABLED_ALARM_XPROPERTY) == QLatin1String("FALSE")) { ialarm->setEnabled(false); } // TODO: check for consistency of alarm properties } icaldatetimeperiodtype ICalFormatImpl::writeICalDatePeriod(const QDate &date) { icaldatetimeperiodtype t; t.time = writeICalDate(date); t.period = icalperiodtype_null_period(); return t; } icaltimetype ICalFormatImpl::writeICalDate(const QDate &date) { icaltimetype t = icaltime_null_time(); t.year = date.year(); t.month = date.month(); t.day = date.day(); t.hour = 0; t.minute = 0; t.second = 0; t.is_date = 1; #if !defined(USE_ICAL_3) t.is_utc = 0; #endif t.zone = nullptr; return t; } icaltimetype ICalFormatImpl::writeICalDateTime(const QDateTime &datetime, bool dateOnly) { icaltimetype t = icaltime_null_time(); t.year = datetime.date().year(); t.month = datetime.date().month(); t.day = datetime.date().day(); t.is_date = dateOnly; if (!t.is_date) { t.hour = datetime.time().hour(); t.minute = datetime.time().minute(); t.second = datetime.time().second(); } t.zone = nullptr; // zone is NOT set #if defined(USE_ICAL_3) if (datetime.timeSpec() == Qt::UTC || (datetime.timeSpec() == Qt::OffsetFromUTC && datetime.offsetFromUtc() == 0)) { t = icaltime_convert_to_zone(t, icaltimezone_get_utc_timezone()); } #else t.is_utc = datetime.timeSpec() == Qt::UTC || (datetime.timeSpec() == Qt::OffsetFromUTC && datetime.offsetFromUtc() == 0); #endif return t; } icalproperty *ICalFormatImpl::writeICalDateTimeProperty(const icalproperty_kind type, const QDateTime &dt, TimeZoneList *tzUsedList) { icaltimetype t; switch (type) { case ICAL_DTSTAMP_PROPERTY: case ICAL_CREATED_PROPERTY: case ICAL_LASTMODIFIED_PROPERTY: t = writeICalDateTime(dt.toUTC()); break; default: t = writeICalDateTime(dt); break; } icalproperty *p; switch (type) { case ICAL_DTSTAMP_PROPERTY: p = icalproperty_new_dtstamp(t); break; case ICAL_CREATED_PROPERTY: p = icalproperty_new_created(t); break; case ICAL_LASTMODIFIED_PROPERTY: p = icalproperty_new_lastmodified(t); break; case ICAL_DTSTART_PROPERTY: // start date and time p = icalproperty_new_dtstart(t); break; case ICAL_DTEND_PROPERTY: // end date and time p = icalproperty_new_dtend(t); break; case ICAL_DUE_PROPERTY: p = icalproperty_new_due(t); break; case ICAL_RECURRENCEID_PROPERTY: p = icalproperty_new_recurrenceid(t); break; case ICAL_EXDATE_PROPERTY: p = icalproperty_new_exdate(t); break; case ICAL_X_PROPERTY: { p = icalproperty_new_x(""); icaltimetype timeType = writeICalDateTime(dt); icalvalue *text = icalvalue_new_datetime(timeType); icalproperty_set_value(p, text); } break; default: { icaldatetimeperiodtype tp; tp.time = t; tp.period = icalperiodtype_null_period(); switch (type) { case ICAL_RDATE_PROPERTY: p = icalproperty_new_rdate(tp); break; default: return nullptr; } } } QTimeZone qtz; #if defined(USE_ICAL_3) if (!icaltime_is_utc(t)) { #else if (!t.is_utc) { #endif qtz = dt.timeZone(); } if (qtz.isValid()) { if (tzUsedList) { if (!tzUsedList->contains(qtz)) { tzUsedList->push_back(qtz); } } icalproperty_add_parameter( p, icalparameter_new_tzid(qtz.id().constData())); } return p; } QDateTime ICalFormatImpl::readICalDateTime(icalproperty *p, const icaltimetype &t, const ICalTimeZoneCache *tzCache, bool utc) { // qCDebug(KCALCORE_LOG); // _dumpIcaltime( t ); QTimeZone timeZone; #if defined(USE_ICAL_3) if (icaltime_is_utc(t) || t.zone == icaltimezone_get_utc_timezone()) { #else if (t.is_utc || t.zone == icaltimezone_get_utc_timezone()) { #endif timeZone = QTimeZone::utc(); // the time zone is UTC utc = false; // no need to convert to UTC } else { icalparameter *param = p ? icalproperty_get_first_parameter(p, ICAL_TZID_PARAMETER) : nullptr; QByteArray tzid = param ? QByteArray(icalparameter_get_tzid(param)) : QByteArray(); // A workaround for a bug in libical (https://github.com/libical/libical/issues/185) // If a recurrenceId has both tzid and range, both parameters end up in the tzid. // This results in invalid tzid's like: "Europe/Berlin;RANGE=THISANDFUTURE" QStringList parts = QString::fromLatin1(tzid).split(QLatin1Char(';')); if (parts.count() > 1) { tzid = parts.first().toLatin1(); } if (tzCache) { // First try to get the timezone from cache timeZone = tzCache->tzForTime(QDateTime({ t.year, t.month, t.day }, {}), tzid); } if (!timeZone.isValid()) { // Fallback to trying to match against Qt timezone timeZone = QTimeZone(tzid); } if (!timeZone.isValid()) { // Finally, give up and assume local timezone timeZone = QTimeZone::systemTimeZone(); } } QDateTime result; if (t.is_date) { result = QDateTime(QDate(t.year, t.month, t.day), {}, timeZone); } else { result = QDateTime(QDate(t.year, t.month, t.day), QTime(t.hour, t.minute, t.second), timeZone); } return utc ? result.toUTC() : result; } QDate ICalFormatImpl::readICalDate(const icaltimetype &t) { return QDate(t.year, t.month, t.day); } QDateTime ICalFormatImpl::readICalDateTimeProperty(icalproperty *p, const ICalTimeZoneCache *tzList, bool utc, bool *allDay) { icaldatetimeperiodtype tp; icalproperty_kind kind = icalproperty_isa(p); switch (kind) { case ICAL_CREATED_PROPERTY: // UTC date/time tp.time = icalproperty_get_created(p); utc = true; break; case ICAL_DTSTAMP_PROPERTY: // UTC date/time tp.time = icalproperty_get_dtstamp(p); utc = true; break; case ICAL_LASTMODIFIED_PROPERTY: // last modification UTC date/time tp.time = icalproperty_get_lastmodified(p); utc = true; break; case ICAL_DTSTART_PROPERTY: // start date and time (UTC for freebusy) tp.time = icalproperty_get_dtstart(p); break; case ICAL_DTEND_PROPERTY: // end date and time (UTC for freebusy) tp.time = icalproperty_get_dtend(p); break; case ICAL_DUE_PROPERTY: // due date/time tp.time = icalproperty_get_due(p); break; case ICAL_COMPLETED_PROPERTY: // UTC completion date/time tp.time = icalproperty_get_completed(p); utc = true; break; case ICAL_RECURRENCEID_PROPERTY: tp.time = icalproperty_get_recurrenceid(p); break; case ICAL_EXDATE_PROPERTY: tp.time = icalproperty_get_exdate(p); break; case ICAL_X_PROPERTY: { const char *name = icalproperty_get_x_name(p); if (QLatin1String(name) == QLatin1String("X-KDE-LIBKCAL-DTRECURRENCE")) { const char *value = icalvalue_as_ical_string(icalproperty_get_value(p)); icalvalue *v = icalvalue_new_from_string(ICAL_DATETIME_VALUE, value); tp.time = icalvalue_get_datetime(v); icalvalue_free(v); break; } } //end of ICAL_X_PROPERTY Q_FALLTHROUGH(); default: switch (kind) { case ICAL_RDATE_PROPERTY: tp = icalproperty_get_rdate(p); break; default: return QDateTime(); } if (!icaltime_is_valid_time(tp.time)) { return QDateTime(); // a time period was found (not implemented yet) } break; } if (allDay) { *allDay = tp.time.is_date; } if (tp.time.is_date) { return QDateTime(readICalDate(tp.time), QTime()); } else { return readICalDateTime(p, tp.time, tzList, utc); } } icaldurationtype ICalFormatImpl::writeICalDuration(const Duration &duration) { // should be able to use icaldurationtype_from_int(), except we know // that some older tools do not properly support weeks. So we never // set a week duration, only days icaldurationtype d; int value = duration.value(); d.is_neg = (value < 0) ? 1 : 0; if (value < 0) { value = -value; } // RFC2445 states that an ical duration value must be // EITHER weeks OR days/time, not both. if (duration.isDaily()) { if (!(value % 7)) { d.weeks = value / 7; d.days = 0; } else { d.weeks = 0; d.days = value; } d.hours = d.minutes = d.seconds = 0; } else { if (!(value % gSecondsPerWeek)) { d.weeks = value / gSecondsPerWeek; d.days = d.hours = d.minutes = d.seconds = 0; } else { d.weeks = 0; d.days = value / gSecondsPerDay; value %= gSecondsPerDay; d.hours = value / gSecondsPerHour; value %= gSecondsPerHour; d.minutes = value / gSecondsPerMinute; value %= gSecondsPerMinute; d.seconds = value; } } return d; } Duration ICalFormatImpl::readICalDuration(const icaldurationtype &d) { int days = d.weeks * 7; days += d.days; int seconds = d.hours * gSecondsPerHour; seconds += d.minutes * gSecondsPerMinute; seconds += d.seconds; if (seconds) { seconds += days * gSecondsPerDay; if (d.is_neg) { seconds = -seconds; } return Duration(seconds, Duration::Seconds); } else { if (d.is_neg) { days = -days; } return Duration(days, Duration::Days); } } icalcomponent *ICalFormatImpl::createCalendarComponent(const Calendar::Ptr &cal) { icalcomponent *calendar; // Root component calendar = icalcomponent_new(ICAL_VCALENDAR_COMPONENT); // Product Identifier icalproperty *p = icalproperty_new_prodid(CalFormat::productId().toUtf8().constData()); icalcomponent_add_property(calendar, p); // iCalendar version (2.0) p = icalproperty_new_version(const_cast(_ICAL_VERSION)); icalcomponent_add_property(calendar, p); // Implementation Version p = icalproperty_new_x(_ICAL_IMPLEMENTATION_VERSION); icalproperty_set_x_name(p, IMPLEMENTATION_VERSION_XPROPERTY); icalcomponent_add_property(calendar, p); // Add time zone // NOTE: Commented out since relevant timezones are added by the caller. // Previously we got some timezones listed twice in the ical file. /* if ( cal && cal->timeZones() ) { const ICalTimeZones::ZoneMap zmaps = cal->timeZones()->zones(); for ( ICalTimeZones::ZoneMap::ConstIterator it=zmaps.constBegin(); it != zmaps.constEnd(); ++it ) { icaltimezone *icaltz = (*it).icalTimezone(); if ( !icaltz ) { qCritical() << "bad time zone"; } else { icalcomponent *tz = icalcomponent_new_clone( icaltimezone_get_component( icaltz ) ); icalcomponent_add_component( calendar, tz ); icaltimezone_free( icaltz, 1 ); } } } */ // Custom properties if (cal != nullptr) { d->writeCustomProperties(calendar, cal.data()); } return calendar; } Incidence::Ptr ICalFormatImpl::readOneIncidence(icalcomponent *calendar, const ICalTimeZoneCache *tzlist) { if (!calendar) { qCWarning(KCALCORE_LOG) << "Populate called with empty calendar"; return Incidence::Ptr(); } icalcomponent *c = icalcomponent_get_first_component(calendar, ICAL_VEVENT_COMPONENT); if (c) { return readEvent(c, tzlist); } c = icalcomponent_get_first_component(calendar, ICAL_VTODO_COMPONENT); if (c) { return readTodo(c, tzlist); } c = icalcomponent_get_first_component(calendar, ICAL_VJOURNAL_COMPONENT); if (c) { return readJournal(c, tzlist); } qCWarning(KCALCORE_LOG) << "Found no incidence"; return Incidence::Ptr(); } // take a raw vcalendar (i.e. from a file on disk, clipboard, etc. etc. // and break it down from its tree-like format into the dictionary format // that is used internally in the ICalFormatImpl. bool ICalFormatImpl::populate(const Calendar::Ptr &cal, icalcomponent *calendar, bool deleted, const QString ¬ebook) { Q_UNUSED(notebook); // qCDebug(KCALCORE_LOG)<<"Populate called"; // this function will populate the caldict dictionary and other event // lists. It turns vevents into Events and then inserts them. if (!calendar) { qCWarning(KCALCORE_LOG) << "Populate called with empty calendar"; return false; } // TODO: check for METHOD icalproperty *p = icalcomponent_get_first_property(calendar, ICAL_X_PROPERTY); QString implementationVersion; while (p) { const char *name = icalproperty_get_x_name(p); QByteArray nproperty(name); if (nproperty == QByteArray(IMPLEMENTATION_VERSION_XPROPERTY)) { QString nvalue = QString::fromUtf8(icalproperty_get_x(p)); if (nvalue.isEmpty()) { icalvalue *value = icalproperty_get_value(p); if (icalvalue_isa(value) == ICAL_TEXT_VALUE) { nvalue = QString::fromUtf8(icalvalue_get_text(value)); } } implementationVersion = nvalue; icalcomponent_remove_property(calendar, p); icalproperty_free(p); } p = icalcomponent_get_next_property(calendar, ICAL_X_PROPERTY); } p = icalcomponent_get_first_property(calendar, ICAL_PRODID_PROPERTY); if (!p) { qCDebug(KCALCORE_LOG) << "No PRODID property found"; d->mLoadedProductId.clear(); } else { d->mLoadedProductId = QString::fromUtf8(icalproperty_get_prodid(p)); delete d->mCompat; d->mCompat = CompatFactory::createCompat(d->mLoadedProductId, implementationVersion); } p = icalcomponent_get_first_property(calendar, ICAL_VERSION_PROPERTY); if (!p) { qCDebug(KCALCORE_LOG) << "No VERSION property found"; d->mParent->setException(new Exception(Exception::CalVersionUnknown)); return false; } else { const char *version = icalproperty_get_version(p); if (!version) { qCDebug(KCALCORE_LOG) << "No VERSION property found"; d->mParent->setException(new Exception(Exception::VersionPropertyMissing)); return false; } if (strcmp(version, "1.0") == 0) { qCDebug(KCALCORE_LOG) << "Expected iCalendar, got vCalendar"; d->mParent->setException(new Exception(Exception::CalVersion1)); return false; } else if (strcmp(version, "2.0") != 0) { qCDebug(KCALCORE_LOG) << "Expected iCalendar, got unknown format"; d->mParent->setException(new Exception( Exception::CalVersionUnknown)); return false; } } // Populate the calendar's time zone collection with all VTIMEZONE components ICalTimeZoneCache timeZoneCache; ICalTimeZoneParser parser(&timeZoneCache); parser.parse(calendar); // custom properties d->readCustomProperties(calendar, cal.data()); // Store all events with a relatedTo property in a list for post-processing d->mEventsRelate.clear(); d->mTodosRelate.clear(); // TODO: make sure that only actually added events go to this lists. icalcomponent *c = icalcomponent_get_first_component(calendar, ICAL_VTODO_COMPONENT); while (c) { Todo::Ptr todo = readTodo(c, &timeZoneCache); if (todo) { // qCDebug(KCALCORE_LOG) << "todo is not zero and deleted is " << deleted; Todo::Ptr old = cal->todo(todo->uid(), todo->recurrenceId()); if (old) { if (old->uid().isEmpty()) { qCWarning(KCALCORE_LOG) << "Skipping invalid VTODO"; c = icalcomponent_get_next_component(calendar, ICAL_VTODO_COMPONENT); continue; } // qCDebug(KCALCORE_LOG) << "Found an old todo with uid " << old->uid(); if (deleted) { // qCDebug(KCALCORE_LOG) << "Todo " << todo->uid() << " already deleted"; cal->deleteTodo(old); // move old to deleted removeAllICal(d->mTodosRelate, old); } else if (todo->revision() > old->revision()) { // qCDebug(KCALCORE_LOG) << "Replacing old todo " << old.data() << " with this one " << todo.data(); cal->deleteTodo(old); // move old to deleted removeAllICal(d->mTodosRelate, old); cal->addTodo(todo); // and replace it with this one } } else if (deleted) { // qCDebug(KCALCORE_LOG) << "Todo " << todo->uid() << " already deleted"; old = cal->deletedTodo(todo->uid(), todo->recurrenceId()); if (!old) { cal->addTodo(todo); // add this one cal->deleteTodo(todo); // and move it to deleted } } else { // qCDebug(KCALCORE_LOG) << "Adding todo " << todo.data() << todo->uid(); cal->addTodo(todo); // just add this one } } c = icalcomponent_get_next_component(calendar, ICAL_VTODO_COMPONENT); } // Iterate through all events c = icalcomponent_get_first_component(calendar, ICAL_VEVENT_COMPONENT); while (c) { Event::Ptr event = readEvent(c, &timeZoneCache); if (event) { // qCDebug(KCALCORE_LOG) << "event is not zero and deleted is " << deleted; Event::Ptr old = cal->event(event->uid(), event->recurrenceId()); if (old) { if (old->uid().isEmpty()) { qCWarning(KCALCORE_LOG) << "Skipping invalid VEVENT"; c = icalcomponent_get_next_component(calendar, ICAL_VEVENT_COMPONENT); continue; } // qCDebug(KCALCORE_LOG) << "Found an old event with uid " << old->uid(); if (deleted) { // qCDebug(KCALCORE_LOG) << "Event " << event->uid() << " already deleted"; cal->deleteEvent(old); // move old to deleted removeAllICal(d->mEventsRelate, old); } else if (event->revision() > old->revision()) { // qCDebug(KCALCORE_LOG) << "Replacing old event " << old.data() // << " with this one " << event.data(); cal->deleteEvent(old); // move old to deleted removeAllICal(d->mEventsRelate, old); cal->addEvent(event); // and replace it with this one } } else if (deleted) { // qCDebug(KCALCORE_LOG) << "Event " << event->uid() << " already deleted"; old = cal->deletedEvent(event->uid(), event->recurrenceId()); if (!old) { cal->addEvent(event); // add this one cal->deleteEvent(event); // and move it to deleted } } else { // qCDebug(KCALCORE_LOG) << "Adding event " << event.data() << event->uid(); cal->addEvent(event); // just add this one } } c = icalcomponent_get_next_component(calendar, ICAL_VEVENT_COMPONENT); } // Iterate through all journals c = icalcomponent_get_first_component(calendar, ICAL_VJOURNAL_COMPONENT); while (c) { Journal::Ptr journal = readJournal(c, &timeZoneCache); if (journal) { Journal::Ptr old = cal->journal(journal->uid(), journal->recurrenceId()); if (old) { if (deleted) { cal->deleteJournal(old); // move old to deleted } else if (journal->revision() > old->revision()) { cal->deleteJournal(old); // move old to deleted cal->addJournal(journal); // and replace it with this one } } else if (deleted) { old = cal->deletedJournal(journal->uid(), journal->recurrenceId()); if (!old) { cal->addJournal(journal); // add this one cal->deleteJournal(journal); // and move it to deleted } } else { cal->addJournal(journal); // just add this one } } c = icalcomponent_get_next_component(calendar, ICAL_VJOURNAL_COMPONENT); } // TODO: Remove any previous time zones no longer referenced in the calendar return true; } QString ICalFormatImpl::extractErrorProperty(icalcomponent *c) { QString errorMessage; icalproperty *error = icalcomponent_get_first_property(c, ICAL_XLICERROR_PROPERTY); while (error) { errorMessage += QLatin1String(icalproperty_get_xlicerror(error)); errorMessage += QLatin1Char('\n'); error = icalcomponent_get_next_property(c, ICAL_XLICERROR_PROPERTY); } return errorMessage; } /* void ICalFormatImpl::dumpIcalRecurrence( const icalrecurrencetype &r ) { int i; qCDebug(KCALCORE_LOG) << " Freq:" << int( r.freq ); qCDebug(KCALCORE_LOG) << " Until:" << icaltime_as_ical_string( r.until ); qCDebug(KCALCORE_LOG) << " Count:" << r.count; if ( r.by_day[0] != ICAL_RECURRENCE_ARRAY_MAX ) { int index = 0; QString out = " By Day: "; while ( ( i = r.by_day[index++] ) != ICAL_RECURRENCE_ARRAY_MAX ) { out.append( QString::number( i ) + ' ' ); } qCDebug(KCALCORE_LOG) << out; } if ( r.by_month_day[0] != ICAL_RECURRENCE_ARRAY_MAX ) { int index = 0; QString out = " By Month Day: "; while ( ( i = r.by_month_day[index++] ) != ICAL_RECURRENCE_ARRAY_MAX ) { out.append( QString::number( i ) + ' ' ); } qCDebug(KCALCORE_LOG) << out; } if ( r.by_year_day[0] != ICAL_RECURRENCE_ARRAY_MAX ) { int index = 0; QString out = " By Year Day: "; while ( ( i = r.by_year_day[index++] ) != ICAL_RECURRENCE_ARRAY_MAX ) { out.append( QString::number( i ) + ' ' ); } qCDebug(KCALCORE_LOG) << out; } if ( r.by_month[0] != ICAL_RECURRENCE_ARRAY_MAX ) { int index = 0; QString out = " By Month: "; while ( ( i = r.by_month[index++] ) != ICAL_RECURRENCE_ARRAY_MAX ) { out.append( QString::number( i ) + ' ' ); } qCDebug(KCALCORE_LOG) << out; } if ( r.by_set_pos[0] != ICAL_RECURRENCE_ARRAY_MAX ) { int index = 0; QString out = " By Set Pos: "; while ( ( i = r.by_set_pos[index++] ) != ICAL_RECURRENCE_ARRAY_MAX ) { qCDebug(KCALCORE_LOG) << "=========" << i; out.append( QString::number( i ) + ' ' ); } qCDebug(KCALCORE_LOG) << out; } } */ icalcomponent *ICalFormatImpl::createScheduleComponent(const IncidenceBase::Ptr &incidence, iTIPMethod method) { icalcomponent *message = createCalendarComponent(); // Create VTIMEZONE components for this incidence TimeZoneList zones; if (incidence) { const QDateTime kd1 = incidence->dateTime(IncidenceBase::RoleStartTimeZone); const QDateTime kd2 = incidence->dateTime(IncidenceBase::RoleEndTimeZone); if (kd1.isValid() && kd1.timeZone() != QTimeZone::utc()) { zones << kd1.timeZone(); } if (kd2.isValid() && kd2.timeZone() != QTimeZone::utc() && kd1.timeZone() != kd2.timeZone()) { zones << kd2.timeZone(); } TimeZoneEarliestDate earliestTz; ICalTimeZoneParser::updateTzEarliestDate(incidence, &earliestTz); for (const auto &qtz : qAsConst(zones)) { icaltimezone *icaltz = ICalTimeZoneParser::icaltimezoneFromQTimeZone(qtz, earliestTz[qtz]); if (!icaltz) { qCritical() << "bad time zone"; } else { icalcomponent *tz = icalcomponent_new_clone(icaltimezone_get_component(icaltz)); icalcomponent_add_component(message, tz); icaltimezone_free(icaltz, 1); } } } else { qCDebug(KCALCORE_LOG) << "No incidence"; return message; } icalproperty_method icalmethod = ICAL_METHOD_NONE; switch (method) { case iTIPPublish: icalmethod = ICAL_METHOD_PUBLISH; break; case iTIPRequest: icalmethod = ICAL_METHOD_REQUEST; break; case iTIPRefresh: icalmethod = ICAL_METHOD_REFRESH; break; case iTIPCancel: icalmethod = ICAL_METHOD_CANCEL; break; case iTIPAdd: icalmethod = ICAL_METHOD_ADD; break; case iTIPReply: icalmethod = ICAL_METHOD_REPLY; break; case iTIPCounter: icalmethod = ICAL_METHOD_COUNTER; break; case iTIPDeclineCounter: icalmethod = ICAL_METHOD_DECLINECOUNTER; break; default: qCDebug(KCALCORE_LOG) << "Unknown method"; return message; } icalcomponent_add_property(message, icalproperty_new_method(icalmethod)); icalcomponent *inc = writeIncidence(incidence, method); if (method != KCalCore::iTIPNoMethod) { //Not very nice, but since dtstamp changes semantics if used in scheduling, we have to adapt icalcomponent_set_dtstamp( inc, writeICalUtcDateTime(QDateTime::currentDateTimeUtc())); } /* * RFC 2446 states in section 3.4.3 ( REPLY to a VTODO ), that * a REQUEST-STATUS property has to be present. For the other two, event and * free busy, it can be there, but is optional. Until we do more * fine grained handling, assume all is well. Note that this is the * status of the _request_, not the attendee. Just to avoid confusion. * - till */ if (icalmethod == ICAL_METHOD_REPLY) { struct icalreqstattype rst; rst.code = ICAL_2_0_SUCCESS_STATUS; rst.desc = nullptr; rst.debug = nullptr; icalcomponent_add_property(inc, icalproperty_new_requeststatus(rst)); } icalcomponent_add_component(message, inc); return message; } diff --git a/src/icaltimezones.cpp b/src/icaltimezones.cpp index 9c669759e..603fbe848 100644 --- a/src/icaltimezones.cpp +++ b/src/icaltimezones.cpp @@ -1,736 +1,735 @@ /* 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 "icaltimezones_p.h" #include "icalformat.h" #include "icalformat_p.h" #include "recurrence.h" #include "recurrencerule.h" #include "recurrencehelper_p.h" #include "utils.h" #include "kcalcore_debug.h" #include #include extern "C" { #include #include } 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), #if defined(USE_ICAL_3) (icaltime_is_utc(t) ? Qt::UTC : Qt::LocalTime)); #else (t.is_utc ? Qt::UTC : Qt::LocalTime)); #endif } // 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 = nullptr; #if !defined(USE_ICAL_3) t.is_utc = 0; #endif return t; } namespace KCalCore { void ICalTimeZonePhase::dump() { qDebug() << " ~~~ ICalTimeZonePhase ~~~"; qDebug() << " Abbreviations:" << abbrevs; qDebug() << " UTC offset:" << utcOffset; qDebug() << " Transitions:" << transitions; qDebug() << " ~~~~~~~~~~~~~~~~~~~~~~~~~"; } void ICalTimeZone::dump() { qDebug() << "~~~ ICalTimeZone ~~~"; qDebug() << "ID:" << id; qDebug() << "QZONE:" << qZone.id(); qDebug() << "STD:"; standard.dump(); qDebug() << "DST:"; daylight.dump(); qDebug() << "~~~~~~~~~~~~~~~~~~~~"; } ICalTimeZoneCache::ICalTimeZoneCache() { } void ICalTimeZoneCache::insert(const QByteArray &id, const ICalTimeZone &tz) { mCache.insert(id, tz); } namespace { template typename T::const_iterator greatestSmallerThan(const T &c, const typename T::value_type &v) { auto it = std::lower_bound(c.cbegin(), c.cend(), v); if (it != c.cbegin()) { return --it; } return c.cend(); } } QTimeZone ICalTimeZoneCache::tzForTime(const QDateTime &dt, const QByteArray &tzid) const { if (QTimeZone::isTimeZoneIdAvailable(tzid)) { return QTimeZone(tzid); } const ICalTimeZone tz = mCache.value(tzid); if (!tz.qZone.isValid()) { return QTimeZone::systemTimeZone(); } // If the matched timezone is one of the UTC offset timezones, we need to make // sure it's in the correct DTS. // The lookup in ICalTimeZoneParser will only find TZ in standard time, but // if the datetim in question fits in the DTS zone, we need to use another UTC // offset timezone if (tz.qZone.id().startsWith("UTC")) { //krazy:exclude=strings // Find the nearest standard and DST transitions that occur BEFORE the "dt" const auto stdPrev = greatestSmallerThan(tz.standard.transitions, dt); const auto dstPrev = greatestSmallerThan(tz.daylight.transitions, dt); if (stdPrev != tz.standard.transitions.cend() && dstPrev != tz.daylight.transitions.cend()) { if (*dstPrev > *stdPrev) { // Previous DTS is closer to "dt" than previous standard, which // means we are in DTS right now const auto tzids = QTimeZone::availableTimeZoneIds(tz.daylight.utcOffset); auto dtsTzId = std::find_if(tzids.cbegin(), tzids.cend(), [](const QByteArray &id) { return id.startsWith("UTC"); //krazy:exclude=strings }); if (dtsTzId != tzids.cend()) { return QTimeZone(*dtsTzId); } } } } return tz.qZone; } ICalTimeZoneParser::ICalTimeZoneParser(ICalTimeZoneCache *cache) : mCache(cache) { } void ICalTimeZoneParser::updateTzEarliestDate(const IncidenceBase::Ptr &incidence, TimeZoneEarliestDate *earliest) { for (auto role : { IncidenceBase::RoleStartTimeZone, IncidenceBase::RoleEndTimeZone }) { const auto dt = incidence->dateTime(role); if (dt.isValid()) { if (dt.timeZone() == QTimeZone::utc()) { continue; } const auto prev = earliest->value(incidence->dtStart().timeZone()); if (!prev.isValid() || incidence->dtStart() < prev) { earliest->insert(incidence->dtStart().timeZone(), prev); } } } } icalcomponent *ICalTimeZoneParser::icalcomponentFromQTimeZone(const QTimeZone &tz, const QDateTime &earliest) { // VTIMEZONE RRULE types enum { DAY_OF_MONTH = 0x01, WEEKDAY_OF_MONTH = 0x02, LAST_WEEKDAY_OF_MONTH = 0x04 }; // Write the time zone data into an iCal component icalcomponent *tzcomp = icalcomponent_new(ICAL_VTIMEZONE_COMPONENT); icalcomponent_add_property(tzcomp, icalproperty_new_tzid(tz.id().constData())); // 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. QTimeZone::OffsetDataList transits = tz.transitions(QDateTime(), MAX_DATE()); 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()) { qCDebug(KCALCORE_LOG) << "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).atUtc >= earliest) { if (i > 0) { transits.erase(transits.begin(), transits.begin() + i); } break; } } } int trcount = transits.count(); QVector transitionsDone(trcount, 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).offsetFromUtc : 0; - const auto transit = transits.at(i); + const auto &transit = transits.at(i); if (transit.offsetFromUtc == preOffset) { transitionsDone[i] = true; while (++i < trcount) { if (transitionsDone[i] || transits.at(i).offsetFromUtc != transit.offsetFromUtc || transits.at(i).daylightTimeOffset != transit.daylightTimeOffset || transits.at(i - 1).offsetFromUtc != preOffset) { continue; } transitionsDone[i] = true; } continue; } const bool isDst = transit.daylightTimeOffset > 0; icalcomponent *phaseComp = icalcomponent_new(isDst ? ICAL_XDAYLIGHT_COMPONENT : ICAL_XSTANDARD_COMPONENT); if (!transit.abbreviation.isEmpty()) { icalcomponent_add_property(phaseComp, icalproperty_new_tzname( static_cast(transit.abbreviation.toUtf8().constData()))); } icalcomponent_add_property(phaseComp, icalproperty_new_tzoffsetfrom(preOffset)); icalcomponent_add_property(phaseComp, icalproperty_new_tzoffsetto(transit.offsetFromUtc)); // 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).atUtc, 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).atUtc; // 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).offsetFromUtc != transit.offsetFromUtc || transits.at(i).daylightTimeOffset != transit.daylightTimeOffset || transits.at(i - 1).offsetFromUtc != preOffset) { continue; } transitionsDone[i] = true; qdt = transits.at(i).atUtc; 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 } r.until = writeLocalICalDateTime(times.takeAt(times.size() - 1), preOffset); 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); } return tzcomp; } icaltimezone *ICalTimeZoneParser::icaltimezoneFromQTimeZone(const QTimeZone &tz, const QDateTime &earliest) { auto itz = icaltimezone_new(); icaltimezone_set_component(itz, icalcomponentFromQTimeZone(tz, earliest)); return itz; } void ICalTimeZoneParser::parse(icalcomponent *calendar) { for (auto *c = icalcomponent_get_first_component(calendar, ICAL_VTIMEZONE_COMPONENT); c; c = icalcomponent_get_next_component(calendar, ICAL_VTIMEZONE_COMPONENT)) { auto icalZone = parseTimeZone(c); //icalZone.dump(); if (!icalZone.id.isEmpty()) { if (!icalZone.qZone.isValid()) { icalZone.qZone = resolveICalTimeZone(icalZone); } if (!icalZone.qZone.isValid()) { qCWarning(KCALCORE_LOG) << "Failed to map" << icalZone.id << "to a known IANA timezone"; continue; } mCache->insert(icalZone.id, icalZone); } } } QTimeZone ICalTimeZoneParser::resolveICalTimeZone(const ICalTimeZone &icalZone) { const auto phase = icalZone.standard; const auto now = QDateTime::currentDateTimeUtc(); const auto candidates = QTimeZone::availableTimeZoneIds(phase.utcOffset); QMap matchedCandidates; for (const auto &tzid : candidates) { const QTimeZone candidate(tzid); // This would be a fallback, candidate has transitions, but the phase does not if (candidate.hasTransitions() == phase.transitions.isEmpty()) { matchedCandidates.insert(0, candidate); continue; } // Without transitions, we can't do any more precise matching, so just // accept this candidate and be done with it if (!candidate.hasTransitions() && phase.transitions.isEmpty()) { return candidate; } // Calculate how many transitions this candidate shares with the phase. // The candidate with the most matching transitions will win. auto begin = std::lower_bound(phase.transitions.cbegin(), phase.transitions.cend(), now.addYears(-20)); // If no transition older than 20 years is found, we will start from beginning if (begin == phase.transitions.cend()) { begin = phase.transitions.cbegin(); } auto end = std::upper_bound(begin, phase.transitions.cend(), now); int matchedTransitions = 0; for (auto it = begin; it != end; ++it) { const auto &transition = *it; const QTimeZone::OffsetDataList candidateTransitions = candidate.transitions(transition, transition); if (candidateTransitions.isEmpty()) { continue; } ++matchedTransitions; // 1 point for a matching transition const auto candidateTransition = candidateTransitions[0]; // FIXME: THIS IS HOW IT SHOULD BE: //const auto abvs = transition.abbreviations(); const auto abvs = phase.abbrevs; for (const auto &abv : abvs) { if (candidateTransition.abbreviation == QString::fromUtf8(abv)) { matchedTransitions += 1024; // lots of points for a transition with a matching abbreviation break; } } } matchedCandidates.insert(matchedTransitions, candidate); } if (!matchedCandidates.isEmpty()) { return matchedCandidates.value(matchedCandidates.lastKey()); } return {}; } ICalTimeZone ICalTimeZoneParser::parseTimeZone(icalcomponent *vtimezone) { ICalTimeZone icalTz; if (auto tzidProp = icalcomponent_get_first_property(vtimezone, ICAL_TZID_PROPERTY)) { icalTz.id = icalproperty_get_value_as_string(tzidProp); // If the VTIMEZONE is a known IANA time zone don't bother parsing the rest // of the VTIMEZONE, get QTimeZone directly from Qt if (QTimeZone::isTimeZoneIdAvailable(icalTz.id)) { icalTz.qZone = QTimeZone(icalTz.id); return icalTz; } else { // Not IANA, but maybe we can match it from Windows ID? const auto ianaTzid = QTimeZone::windowsIdToDefaultIanaId(icalTz.id); if (!ianaTzid.isEmpty()) { icalTz.qZone = QTimeZone(ianaTzid); return icalTz; } } } for (icalcomponent *c = icalcomponent_get_first_component(vtimezone, ICAL_ANY_COMPONENT); c; c = icalcomponent_get_next_component(vtimezone, ICAL_ANY_COMPONENT)) { icalcomponent_kind kind = icalcomponent_isa(c); switch (kind) { case ICAL_XSTANDARD_COMPONENT: //qCDebug(KCALCORE_LOG) << "---standard phase: found"; parsePhase(c, false, icalTz.standard); break; case ICAL_XDAYLIGHT_COMPONENT: //qCDebug(KCALCORE_LOG) << "---daylight phase: found"; parsePhase(c, true, icalTz.daylight); break; default: qCDebug(KCALCORE_LOG) << "Unknown component:" << int(kind); break; } } return icalTz; } bool ICalTimeZoneParser::parsePhase(icalcomponent *c, bool daylight, ICalTimeZonePhase &phase) { // Read the observance data for this standard/daylight savings phase int utcOffset = 0; int prevOffset = 0; bool recurs = false; bool found_dtstart = false; bool found_tzoffsetfrom = false; bool found_tzoffsetto = false; icaltimetype dtstart = icaltime_null_time(); QSet abbrevs; // 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 name = 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 && name == "Standard Time") || (daylight && name == "Daylight Time")) { break; } abbrevs.insert(name); 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_RDATE_PROPERTY: case ICAL_RRULE_PROPERTY: recurs = true; break; default: break; } p = icalcomponent_get_next_property(c, ICAL_ANY_PROPERTY); } // Validate the phase data if (!found_dtstart || !found_tzoffsetfrom || !found_tzoffsetto) { qCDebug(KCALCORE_LOG) << "DTSTART/TZOFFSETFROM/TZOFFSETTO missing"; return false; } // Convert DTSTART to QDateTime, and from local time to UTC const QDateTime localStart = toQDateTime(dtstart); // local time dtstart.second -= prevOffset; #if defined(USE_ICAL_3) dtstart = icaltime_convert_to_zone(dtstart, icaltimezone_get_utc_timezone()); #else dtstart.is_utc = 1; #endif const QDateTime utcStart = toQDateTime(icaltime_normalize(dtstart)); // UTC phase.abbrevs.unite(abbrevs); phase.utcOffset = utcOffset; phase.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 QDateTime klocalStart(localStart); const QDateTime maxTime(MAX_DATE()); 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; #if !defined(USE_ICAL_3) t.is_utc = 0; // dtstart is in local time #endif t.is_date = 0; } // RFC2445 states that RDATE must be in local time, // but we support UTC as well to be safe. #if defined(USE_ICAL_3) if (!icaltime_is_utc(t)) { #else if (!t.is_utc) { #endif t.second -= prevOffset; // convert to UTC #if defined(USE_ICAL_3) t = icaltime_convert_to_zone(t, icaltimezone_get_utc_timezone()); #else t.is_utc = 1; #endif t = icaltime_normalize(t); } phase.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); + r.setStartDt(localStart); // 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) { QDateTime end(r.endDt()); if (end.timeSpec() == Qt::UTC) { end.setTimeSpec(Qt::LocalTime); r.setEndDt(end.addSecs(prevOffset)); } } - const auto dts = r.timesInInterval(klocalStart, maxTime); + const auto dts = r.timesInInterval(localStart, maxTime); for (int i = 0, end = dts.count(); i < end; ++i) { QDateTime utc = dts[i]; utc.setTimeSpec(Qt::UTC); phase.transitions += utc.addSecs(-prevOffset); } break; } default: break; } p = icalcomponent_get_next_property(c, ICAL_ANY_PROPERTY); } sortAndRemoveDuplicates(phase.transitions); } return true; } QByteArray ICalTimeZoneParser::vcaltimezoneFromQTimeZone(const QTimeZone &qtz, const QDateTime &earliest) { auto icalTz = icalcomponentFromQTimeZone(qtz, earliest); const QByteArray result(icalcomponent_as_ical_string(icalTz)); icalmemory_free_ring(); icalcomponent_free(icalTz); return result; } } // namespace KCalCore diff --git a/src/recurrence.cpp b/src/recurrence.cpp index b081de9cb..36507ff69 100644 --- a/src/recurrence.cpp +++ b/src/recurrence.cpp @@ -1,1555 +1,1555 @@ /* This file is part of kcalcore library. Copyright (c) 1998 Preston Brown Copyright (c) 2001 Cornelius Schumacher Copyright (c) 2002,2006 David Jarvie Copyright (C) 2005 Reinhold Kainhofer 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 "recurrence.h" #include "utils.h" #include "recurrencehelper_p.h" #include "kcalcore_debug.h" #include #include #include #include using namespace KCalCore; //@cond PRIVATE class Q_DECL_HIDDEN KCalCore::Recurrence::Private { public: Private() : mCachedType(rMax), mAllDay(false), mRecurReadOnly(false) { } Private(const Private &p) : mRDateTimes(p.mRDateTimes), mRDates(p.mRDates), mExDateTimes(p.mExDateTimes), mExDates(p.mExDates), mStartDateTime(p.mStartDateTime), mCachedType(p.mCachedType), mAllDay(p.mAllDay), mRecurReadOnly(p.mRecurReadOnly) { } bool operator==(const Private &p) const; RecurrenceRule::List mExRules; RecurrenceRule::List mRRules; QList mRDateTimes; DateList mRDates; QList mExDateTimes; DateList mExDates; QDateTime mStartDateTime; // date/time of first recurrence QList mObservers; // Cache the type of the recurrence with the old system (e.g. MonthlyPos) mutable ushort mCachedType; bool mAllDay = false; // the recurrence has no time, just a date bool mRecurReadOnly = false; }; bool Recurrence::Private::operator==(const Recurrence::Private &p) const { // qCDebug(KCALCORE_LOG) << mStartDateTime << p.mStartDateTime; if ((mStartDateTime != p.mStartDateTime && (mStartDateTime.isValid() || p.mStartDateTime.isValid())) || mAllDay != p.mAllDay || mRecurReadOnly != p.mRecurReadOnly || mExDates != p.mExDates || mExDateTimes != p.mExDateTimes || mRDates != p.mRDates || mRDateTimes != p.mRDateTimes) { return false; } // Compare the rrules, exrules! Assume they have the same order... This only // matters if we have more than one rule (which shouldn't be the default anyway) int i; int end = mRRules.count(); if (end != p.mRRules.count()) { return false; } for (i = 0; i < end; ++i) { if (*mRRules[i] != *p.mRRules[i]) { return false; } } end = mExRules.count(); if (end != p.mExRules.count()) { return false; } for (i = 0; i < end; ++i) { if (*mExRules[i] != *p.mExRules[i]) { return false; } } return true; } //@endcond Recurrence::Recurrence() : d(new KCalCore::Recurrence::Private()) { } Recurrence::Recurrence(const Recurrence &r) : RecurrenceRule::RuleObserver(), d(new KCalCore::Recurrence::Private(*r.d)) { int i, end; d->mRRules.reserve(r.d->mRRules.count()); for (i = 0, end = r.d->mRRules.count(); i < end; ++i) { RecurrenceRule *rule = new RecurrenceRule(*r.d->mRRules[i]); d->mRRules.append(rule); rule->addObserver(this); } d->mExRules.reserve(r.d->mExRules.count()); for (i = 0, end = r.d->mExRules.count(); i < end; ++i) { RecurrenceRule *rule = new RecurrenceRule(*r.d->mExRules[i]); d->mExRules.append(rule); rule->addObserver(this); } } Recurrence::~Recurrence() { qDeleteAll(d->mExRules); qDeleteAll(d->mRRules); delete d; } bool Recurrence::operator==(const Recurrence &recurrence) const { return *d == *recurrence.d; } Recurrence &Recurrence::operator=(const Recurrence &recurrence) { // check for self assignment if (&recurrence == this) { return *this; } *d = *recurrence.d; return *this; } void Recurrence::addObserver(RecurrenceObserver *observer) { if (!d->mObservers.contains(observer)) { d->mObservers.append(observer); } } void Recurrence::removeObserver(RecurrenceObserver *observer) { d->mObservers.removeAll(observer); } QDateTime Recurrence::startDateTime() const { return d->mStartDateTime; } bool Recurrence::allDay() const { return d->mAllDay; } void Recurrence::setAllDay(bool allDay) { if (d->mRecurReadOnly || allDay == d->mAllDay) { return; } d->mAllDay = allDay; for (int i = 0, end = d->mRRules.count(); i < end; ++i) { d->mRRules[i]->setAllDay(allDay); } for (int i = 0, end = d->mExRules.count(); i < end; ++i) { d->mExRules[i]->setAllDay(allDay); } updated(); } RecurrenceRule *Recurrence::defaultRRule(bool create) const { if (d->mRRules.isEmpty()) { if (!create || d->mRecurReadOnly) { return nullptr; } RecurrenceRule *rrule = new RecurrenceRule(); rrule->setStartDt(startDateTime()); const_cast(this)->addRRule(rrule); return rrule; } else { return d->mRRules[0]; } } RecurrenceRule *Recurrence::defaultRRuleConst() const { return d->mRRules.isEmpty() ? nullptr : d->mRRules[0]; } void Recurrence::updated() { // recurrenceType() re-calculates the type if it's rMax d->mCachedType = rMax; for (int i = 0, end = d->mObservers.count(); i < end; ++i) { if (d->mObservers[i]) { d->mObservers[i]->recurrenceUpdated(this); } } } bool Recurrence::recurs() const { return !d->mRRules.isEmpty() || !d->mRDates.isEmpty() || !d->mRDateTimes.isEmpty(); } ushort Recurrence::recurrenceType() const { if (d->mCachedType == rMax) { d->mCachedType = recurrenceType(defaultRRuleConst()); } return d->mCachedType; } ushort Recurrence::recurrenceType(const RecurrenceRule *rrule) { if (!rrule) { return rNone; } RecurrenceRule::PeriodType type = rrule->recurrenceType(); // BYSETPOS, BYWEEKNUMBER and BYSECOND were not supported in old versions if (!rrule->bySetPos().isEmpty() || !rrule->bySeconds().isEmpty() || !rrule->byWeekNumbers().isEmpty()) { return rOther; } // It wasn't possible to set BYMINUTES, BYHOUR etc. by the old code. So if // it's set, it's none of the old types if (!rrule->byMinutes().isEmpty() || !rrule->byHours().isEmpty()) { return rOther; } // Possible combinations were: // BYDAY: with WEEKLY, MONTHLY, YEARLY // BYMONTHDAY: with MONTHLY, YEARLY // BYMONTH: with YEARLY // BYYEARDAY: with YEARLY if ((!rrule->byYearDays().isEmpty() && type != RecurrenceRule::rYearly) || (!rrule->byMonths().isEmpty() && type != RecurrenceRule::rYearly)) { return rOther; } if (!rrule->byDays().isEmpty()) { if (type != RecurrenceRule::rYearly && type != RecurrenceRule::rMonthly && type != RecurrenceRule::rWeekly) { return rOther; } } switch (type) { case RecurrenceRule::rNone: return rNone; case RecurrenceRule::rMinutely: return rMinutely; case RecurrenceRule::rHourly: return rHourly; case RecurrenceRule::rDaily: return rDaily; case RecurrenceRule::rWeekly: return rWeekly; case RecurrenceRule::rMonthly: { if (rrule->byDays().isEmpty()) { return rMonthlyDay; } else if (rrule->byMonthDays().isEmpty()) { return rMonthlyPos; } else { return rOther; // both position and date specified } } case RecurrenceRule::rYearly: { // Possible combinations: // rYearlyMonth: [BYMONTH &] BYMONTHDAY // rYearlyDay: BYYEARDAY // rYearlyPos: [BYMONTH &] BYDAY if (!rrule->byDays().isEmpty()) { // can only by rYearlyPos if (rrule->byMonthDays().isEmpty() && rrule->byYearDays().isEmpty()) { return rYearlyPos; } else { return rOther; } } else if (!rrule->byYearDays().isEmpty()) { // Can only be rYearlyDay if (rrule->byMonths().isEmpty() && rrule->byMonthDays().isEmpty()) { return rYearlyDay; } else { return rOther; } } else { return rYearlyMonth; } } default: return rOther; } } bool Recurrence::recursOn(const QDate &qd, const QTimeZone &timeZone) const { // Don't waste time if date is before the start of the recurrence if (QDateTime(qd, QTime(23, 59, 59), timeZone) < d->mStartDateTime) { return false; } // First handle dates. Exrules override if (std::binary_search(d->mExDates.constBegin(), d->mExDates.constEnd(), qd)) { return false; } int i, end; // For all-day events a matching exrule excludes the whole day // since exclusions take precedence over inclusions, we know it can't occur on that day. if (allDay()) { for (i = 0, end = d->mExRules.count(); i < end; ++i) { if (d->mExRules[i]->recursOn(qd, timeZone)) { return false; } } } if (std::binary_search(d->mRDates.constBegin(), d->mRDates.constEnd(), qd)) { return true; } // Check if it might recur today at all. bool recurs = (startDate() == qd); for (i = 0, end = d->mRDateTimes.count(); i < end && !recurs; ++i) { recurs = (d->mRDateTimes[i].toTimeZone(timeZone).date() == qd); } for (i = 0, end = d->mRRules.count(); i < end && !recurs; ++i) { recurs = d->mRRules[i]->recursOn(qd, timeZone); } // If the event wouldn't recur at all, simply return false, don't check ex* if (!recurs) { return false; } // Check if there are any times for this day excluded, either by exdate or exrule: bool exon = false; for (i = 0, end = d->mExDateTimes.count(); i < end && !exon; ++i) { exon = (d->mExDateTimes[i].toTimeZone(timeZone).date() == qd); } if (!allDay()) { // we have already checked all-day times above for (i = 0, end = d->mExRules.count(); i < end && !exon; ++i) { exon = d->mExRules[i]->recursOn(qd, timeZone); } } if (!exon) { // Simple case, nothing on that day excluded, return the value from before return recurs; } else { // Harder part: I don't think there is any way other than to calculate the // whole list of items for that day. //TODO: consider whether it would be more efficient to call // Rule::recurTimesOn() instead of Rule::recursOn() from the start TimeList timesForDay(recurTimesOn(qd, timeZone)); return !timesForDay.isEmpty(); } } bool Recurrence::recursAt(const QDateTime &dt) const { // Convert to recurrence's time zone for date comparisons, and for more efficient time comparisons const auto dtrecur = dt.toTimeZone(d->mStartDateTime.timeZone()); // if it's excluded anyway, don't bother to check if it recurs at all. if (std::binary_search(d->mExDateTimes.constBegin(), d->mExDateTimes.constEnd(), dtrecur) || std::binary_search(d->mExDates.constBegin(), d->mExDates.constEnd(), dtrecur.date())) { return false; } int i, end; for (i = 0, end = d->mExRules.count(); i < end; ++i) { if (d->mExRules[i]->recursAt(dtrecur)) { return false; } } // Check explicit recurrences, then rrules. if (startDateTime() == dtrecur || std::binary_search(d->mRDateTimes.constBegin(), d->mRDateTimes.constEnd(), dtrecur)) { return true; } for (i = 0, end = d->mRRules.count(); i < end; ++i) { if (d->mRRules[i]->recursAt(dtrecur)) { return true; } } return false; } /** Calculates the cumulative end of the whole recurrence (rdates and rrules). If any rrule is infinite, or the recurrence doesn't have any rrules or rdates, an invalid date is returned. */ QDateTime Recurrence::endDateTime() const { QList dts; dts << startDateTime(); if (!d->mRDates.isEmpty()) { dts << QDateTime(d->mRDates.last(), QTime(0, 0, 0), d->mStartDateTime.timeZone()); } if (!d->mRDateTimes.isEmpty()) { dts << d->mRDateTimes.last(); } for (int i = 0, end = d->mRRules.count(); i < end; ++i) { auto rl = d->mRRules[i]->endDt(); // if any of the rules is infinite, the whole recurrence is if (!rl.isValid()) { return QDateTime(); } dts << rl; } sortAndRemoveDuplicates(dts); return dts.isEmpty() ? QDateTime() : dts.last(); } /** Calculates the cumulative end of the whole recurrence (rdates and rrules). If any rrule is infinite, or the recurrence doesn't have any rrules or rdates, an invalid date is returned. */ QDate Recurrence::endDate() const { QDateTime end(endDateTime()); return end.isValid() ? end.date() : QDate(); } void Recurrence::setEndDate(const QDate &date) { QDateTime dt(date, d->mStartDateTime.time(), d->mStartDateTime.timeZone()); if (allDay()) { dt.setTime(QTime(23, 59, 59)); } setEndDateTime(dt); } void Recurrence::setEndDateTime(const QDateTime &dateTime) { if (d->mRecurReadOnly) { return; } RecurrenceRule *rrule = defaultRRule(true); if (!rrule) { return; } // If the recurrence rule has a duration, and we're trying to set an invalid end date, // we have to skip setting it to avoid setting the field dirty. // The end date is already invalid since the duration is set and end date/duration // are mutually exclusive. // We can't use inequality check below, because endDt() also returns a valid date // for a duration (it is calculated from the duration). if (rrule->duration() > 0 && !dateTime.isValid()) { return; } if (dateTime != rrule->endDt()) { rrule->setEndDt(dateTime); updated(); } } int Recurrence::duration() const { RecurrenceRule *rrule = defaultRRuleConst(); return rrule ? rrule->duration() : 0; } int Recurrence::durationTo(const QDateTime &datetime) const { // Emulate old behavior: This is just an interface to the first rule! RecurrenceRule *rrule = defaultRRuleConst(); return rrule ? rrule->durationTo(datetime) : 0; } int Recurrence::durationTo(const QDate &date) const { return durationTo(QDateTime(date, QTime(23, 59, 59), d->mStartDateTime.timeZone())); } void Recurrence::setDuration(int duration) { if (d->mRecurReadOnly) { return; } RecurrenceRule *rrule = defaultRRule(true); if (!rrule) { return; } if (duration != rrule->duration()) { rrule->setDuration(duration); updated(); } } void Recurrence::shiftTimes(const QTimeZone &oldTz, const QTimeZone &newTz) { if (d->mRecurReadOnly) { return; } d->mStartDateTime = d->mStartDateTime.toTimeZone(oldTz); d->mStartDateTime.setTimeZone(newTz); int i, end; for (i = 0, end = d->mRDateTimes.count(); i < end; ++i) { d->mRDateTimes[i] = d->mRDateTimes[i].toTimeZone(oldTz); d->mRDateTimes[i].setTimeZone(newTz); } for (i = 0, end = d->mExDateTimes.count(); i < end; ++i) { d->mExDateTimes[i] = d->mExDateTimes[i].toTimeZone(oldTz); d->mExDateTimes[i].setTimeZone(newTz); } for (i = 0, end = d->mRRules.count(); i < end; ++i) { d->mRRules[i]->shiftTimes(oldTz, newTz); } for (i = 0, end = d->mExRules.count(); i < end; ++i) { d->mExRules[i]->shiftTimes(oldTz, newTz); } } void Recurrence::unsetRecurs() { if (d->mRecurReadOnly) { return; } qDeleteAll(d->mRRules); d->mRRules.clear(); updated(); } void Recurrence::clear() { if (d->mRecurReadOnly) { return; } qDeleteAll(d->mRRules); d->mRRules.clear(); qDeleteAll(d->mExRules); d->mExRules.clear(); d->mRDates.clear(); d->mRDateTimes.clear(); d->mExDates.clear(); d->mExDateTimes.clear(); d->mCachedType = rMax; updated(); } void Recurrence::setRecurReadOnly(bool readOnly) { d->mRecurReadOnly = readOnly; } bool Recurrence::recurReadOnly() const { return d->mRecurReadOnly; } QDate Recurrence::startDate() const { return d->mStartDateTime.date(); } void Recurrence::setStartDateTime(const QDateTime &start, bool isAllDay) { if (d->mRecurReadOnly) { return; } d->mStartDateTime = start; setAllDay(isAllDay); // set all RRULEs and EXRULEs int i, end; for (i = 0, end = d->mRRules.count(); i < end; ++i) { d->mRRules[i]->setStartDt(start); } for (i = 0, end = d->mExRules.count(); i < end; ++i) { d->mExRules[i]->setStartDt(start); } updated(); } int Recurrence::frequency() const { RecurrenceRule *rrule = defaultRRuleConst(); return rrule ? rrule->frequency() : 0; } // Emulate the old behaviour. Make this methods just an interface to the // first rrule void Recurrence::setFrequency(int freq) { if (d->mRecurReadOnly || freq <= 0) { return; } RecurrenceRule *rrule = defaultRRule(true); if (rrule) { rrule->setFrequency(freq); } updated(); } // WEEKLY int Recurrence::weekStart() const { RecurrenceRule *rrule = defaultRRuleConst(); return rrule ? rrule->weekStart() : 1; } // Emulate the old behavior QBitArray Recurrence::days() const { QBitArray days(7); days.fill(0); RecurrenceRule *rrule = defaultRRuleConst(); if (rrule) { - QList bydays = rrule->byDays(); + const QList &bydays = rrule->byDays(); for (int i = 0; i < bydays.size(); ++i) { if (bydays.at(i).pos() == 0) { days.setBit(bydays.at(i).day() - 1); } } } return days; } // MONTHLY // Emulate the old behavior QList Recurrence::monthDays() const { RecurrenceRule *rrule = defaultRRuleConst(); if (rrule) { return rrule->byMonthDays(); } else { return QList(); } } // Emulate the old behavior QList Recurrence::monthPositions() const { RecurrenceRule *rrule = defaultRRuleConst(); return rrule ? rrule->byDays() : QList(); } // YEARLY QList Recurrence::yearDays() const { RecurrenceRule *rrule = defaultRRuleConst(); return rrule ? rrule->byYearDays() : QList(); } QList Recurrence::yearDates() const { return monthDays(); } QList Recurrence::yearMonths() const { RecurrenceRule *rrule = defaultRRuleConst(); return rrule ? rrule->byMonths() : QList(); } QList Recurrence::yearPositions() const { return monthPositions(); } RecurrenceRule *Recurrence::setNewRecurrenceType(RecurrenceRule::PeriodType type, int freq) { if (d->mRecurReadOnly || freq <= 0) { return nullptr; } // Ignore the call if nothing has change if (defaultRRuleConst() && defaultRRuleConst()->recurrenceType() == type && frequency() == freq) { return nullptr; } qDeleteAll(d->mRRules); d->mRRules.clear(); updated(); RecurrenceRule *rrule = defaultRRule(true); if (!rrule) { return nullptr; } rrule->setRecurrenceType(type); rrule->setFrequency(freq); rrule->setDuration(-1); return rrule; } void Recurrence::setMinutely(int _rFreq) { if (setNewRecurrenceType(RecurrenceRule::rMinutely, _rFreq)) { updated(); } } void Recurrence::setHourly(int _rFreq) { if (setNewRecurrenceType(RecurrenceRule::rHourly, _rFreq)) { updated(); } } void Recurrence::setDaily(int _rFreq) { if (setNewRecurrenceType(RecurrenceRule::rDaily, _rFreq)) { updated(); } } void Recurrence::setWeekly(int freq, int weekStart) { RecurrenceRule *rrule = setNewRecurrenceType(RecurrenceRule::rWeekly, freq); if (!rrule) { return; } rrule->setWeekStart(weekStart); updated(); } void Recurrence::setWeekly(int freq, const QBitArray &days, int weekStart) { setWeekly(freq, weekStart); addMonthlyPos(0, days); } void Recurrence::addWeeklyDays(const QBitArray &days) { addMonthlyPos(0, days); } void Recurrence::setMonthly(int freq) { if (setNewRecurrenceType(RecurrenceRule::rMonthly, freq)) { updated(); } } void Recurrence::addMonthlyPos(short pos, const QBitArray &days) { // Allow 53 for yearly! if (d->mRecurReadOnly || pos > 53 || pos < -53) { return; } RecurrenceRule *rrule = defaultRRule(false); if (!rrule) { return; } bool changed = false; QList positions = rrule->byDays(); for (int i = 0; i < 7; ++i) { if (days.testBit(i)) { RecurrenceRule::WDayPos p(pos, i + 1); if (!positions.contains(p)) { changed = true; positions.append(p); } } } if (changed) { rrule->setByDays(positions); updated(); } } void Recurrence::addMonthlyPos(short pos, ushort day) { // Allow 53 for yearly! if (d->mRecurReadOnly || pos > 53 || pos < -53) { return; } RecurrenceRule *rrule = defaultRRule(false); if (!rrule) { return; } QList positions = rrule->byDays(); RecurrenceRule::WDayPos p(pos, day); if (!positions.contains(p)) { positions.append(p); setMonthlyPos(positions); } } void Recurrence::setMonthlyPos(const QList &monthlyDays) { if (d->mRecurReadOnly) { return; } RecurrenceRule *rrule = defaultRRule(true); if (!rrule) { return; } //TODO: sort lists // the position inside the list has no meaning, so sort the list before testing if it changed if (monthlyDays != rrule->byDays()) { rrule->setByDays(monthlyDays); updated(); } } void Recurrence::addMonthlyDate(short day) { if (d->mRecurReadOnly || day > 31 || day < -31) { return; } RecurrenceRule *rrule = defaultRRule(true); if (!rrule) { return; } QList monthDays = rrule->byMonthDays(); if (!monthDays.contains(day)) { monthDays.append(day); setMonthlyDate(monthDays); } } void Recurrence::setMonthlyDate(const QList< int > &monthlyDays) { if (d->mRecurReadOnly) { return; } RecurrenceRule *rrule = defaultRRule(true); if (!rrule) { return; } QList mD(monthlyDays); QList rbD(rrule->byMonthDays()); sortAndRemoveDuplicates(mD); sortAndRemoveDuplicates(rbD); if (mD != rbD) { rrule->setByMonthDays(monthlyDays); updated(); } } void Recurrence::setYearly(int freq) { if (setNewRecurrenceType(RecurrenceRule::rYearly, freq)) { updated(); } } // Daynumber within year void Recurrence::addYearlyDay(int day) { RecurrenceRule *rrule = defaultRRule(false); // It must already exist! if (!rrule) { return; } QList days = rrule->byYearDays(); if (!days.contains(day)) { days << day; setYearlyDay(days); } } void Recurrence::setYearlyDay(const QList &days) { RecurrenceRule *rrule = defaultRRule(false); // It must already exist! if (!rrule) { return; } QList d(days); QList bYD(rrule->byYearDays()); sortAndRemoveDuplicates(d); sortAndRemoveDuplicates(bYD); if (d != bYD) { rrule->setByYearDays(days); updated(); } } // day part of date within year void Recurrence::addYearlyDate(int day) { addMonthlyDate(day); } void Recurrence::setYearlyDate(const QList &dates) { setMonthlyDate(dates); } // day part of date within year, given as position (n-th weekday) void Recurrence::addYearlyPos(short pos, const QBitArray &days) { addMonthlyPos(pos, days); } void Recurrence::setYearlyPos(const QList &days) { setMonthlyPos(days); } // month part of date within year void Recurrence::addYearlyMonth(short month) { if (d->mRecurReadOnly || month < 1 || month > 12) { return; } RecurrenceRule *rrule = defaultRRule(false); if (!rrule) { return; } QList months = rrule->byMonths(); if (!months.contains(month)) { months << month; setYearlyMonth(months); } } void Recurrence::setYearlyMonth(const QList &months) { if (d->mRecurReadOnly) { return; } RecurrenceRule *rrule = defaultRRule(false); if (!rrule) { return; } QList m(months); QList bM(rrule->byMonths()); sortAndRemoveDuplicates(m); sortAndRemoveDuplicates(bM); if (m != bM) { rrule->setByMonths(months); updated(); } } TimeList Recurrence::recurTimesOn(const QDate &date, const QTimeZone &timeZone) const { // qCDebug(KCALCORE_LOG) << "recurTimesOn(" << date << ")"; int i, end; TimeList times; // The whole day is excepted if (std::binary_search(d->mExDates.constBegin(), d->mExDates.constEnd(), date)) { return times; } // EXRULE takes precedence over RDATE entries, so for all-day events, // a matching excule also excludes the whole day automatically if (allDay()) { for (i = 0, end = d->mExRules.count(); i < end; ++i) { if (d->mExRules[i]->recursOn(date, timeZone)) { return times; } } } QDateTime dt = startDateTime().toTimeZone(timeZone); if (dt.date() == date) { times << dt.time(); } bool foundDate = false; for (i = 0, end = d->mRDateTimes.count(); i < end; ++i) { dt = d->mRDateTimes[i].toTimeZone(timeZone); if (dt.date() == date) { times << dt.time(); foundDate = true; } else if (foundDate) { break; // <= Assume that the rdatetime list is sorted } } for (i = 0, end = d->mRRules.count(); i < end; ++i) { times += d->mRRules[i]->recurTimesOn(date, timeZone); } sortAndRemoveDuplicates(times); foundDate = false; TimeList extimes; for (i = 0, end = d->mExDateTimes.count(); i < end; ++i) { dt = d->mExDateTimes[i].toTimeZone(timeZone); if (dt.date() == date) { extimes << dt.time(); foundDate = true; } else if (foundDate) { break; } } if (!allDay()) { // we have already checked all-day times above for (i = 0, end = d->mExRules.count(); i < end; ++i) { extimes += d->mExRules[i]->recurTimesOn(date, timeZone); } } sortAndRemoveDuplicates(extimes); inplaceSetDifference(times, extimes); return times; } QList Recurrence::timesInInterval(const QDateTime &start, const QDateTime &end) const { int i, count; QList times; for (i = 0, count = d->mRRules.count(); i < count; ++i) { times += d->mRRules[i]->timesInInterval(start, end); } // add rdatetimes that fit in the interval for (i = 0, count = d->mRDateTimes.count(); i < count; ++i) { if (d->mRDateTimes[i] >= start && d->mRDateTimes[i] <= end) { times += d->mRDateTimes[i]; } } // add rdates that fit in the interval QDateTime kdt = d->mStartDateTime; for (i = 0, count = d->mRDates.count(); i < count; ++i) { kdt.setDate(d->mRDates[i]); if (kdt >= start && kdt <= end) { times += kdt; } } // Recurrence::timesInInterval(...) doesn't explicitly add mStartDateTime to the list // of times to be returned. It calls mRRules[i]->timesInInterval(...) which include // mStartDateTime. // So, If we have rdates/rdatetimes but don't have any rrule we must explicitly // add mStartDateTime to the list, otherwise we won't see the first occurrence. if ((!d->mRDates.isEmpty() || !d->mRDateTimes.isEmpty()) && d->mRRules.isEmpty() && start <= d->mStartDateTime && end >= d->mStartDateTime) { times += d->mStartDateTime; } sortAndRemoveDuplicates(times); // Remove excluded times int idt = 0; int enddt = times.count(); for (i = 0, count = d->mExDates.count(); i < count && idt < enddt; ++i) { while (idt < enddt && times[idt].date() < d->mExDates[i]) { ++idt; } while (idt < enddt && times[idt].date() == d->mExDates[i]) { times.removeAt(idt); --enddt; } } QList extimes; for (i = 0, count = d->mExRules.count(); i < count; ++i) { extimes += d->mExRules[i]->timesInInterval(start, end); } extimes += d->mExDateTimes; sortAndRemoveDuplicates(extimes); inplaceSetDifference(times, extimes); return times; } QDateTime Recurrence::getNextDateTime(const QDateTime &preDateTime) const { QDateTime nextDT = preDateTime; // prevent infinite loops, e.g. when an exrule extinguishes an rrule (e.g. // the exrule is identical to the rrule). If an occurrence is found, break // out of the loop by returning that QDateTime // TODO_Recurrence: Is a loop counter of 1000 really okay? I mean for secondly // recurrence, an exdate might exclude more than 1000 intervals! int loop = 0; while (loop < 1000) { // Outline of the algo: // 1) Find the next date/time after preDateTime when the event could recur // 1.0) Add the start date if it's after preDateTime // 1.1) Use the next occurrence from the explicit RDATE lists // 1.2) Add the next recurrence for each of the RRULEs // 2) Take the earliest recurrence of these = QDateTime nextDT // 3) If that date/time is not excluded, either explicitly by an EXDATE or // by an EXRULE, return nextDT as the next date/time of the recurrence // 4) If it's excluded, start all at 1), but starting at nextDT (instead // of preDateTime). Loop at most 1000 times. ++loop; // First, get the next recurrence from the RDate lists QList dates; if (nextDT < startDateTime()) { dates << startDateTime(); } // Assume that the rdatetime list is sorted const auto it = std::upper_bound(d->mRDateTimes.constBegin(), d->mRDateTimes.constEnd(), nextDT); if (it != d->mRDateTimes.constEnd()) { dates << *it; } QDateTime kdt(startDateTime()); for (const auto &date : qAsConst(d->mRDates)) { kdt.setDate(date); if (kdt > nextDT) { dates << kdt; break; } } // Add the next occurrences from all RRULEs. for (const auto &rule : qAsConst(d->mRRules)) { QDateTime dt = rule->getNextDate(nextDT); if (dt.isValid()) { dates << dt; } } // Take the first of these (all others can't be used later on) sortAndRemoveDuplicates(dates); if (dates.isEmpty()) { return QDateTime(); } nextDT = dates.first(); // Check if that date/time is excluded explicitly or by an exrule: if (!std::binary_search(d->mExDates.constBegin(), d->mExDates.constEnd(), nextDT.date()) && !std::binary_search(d->mExDateTimes.constBegin(), d->mExDateTimes.constEnd(), nextDT)) { bool allowed = true; for (const auto &rule : qAsConst(d->mExRules)) { allowed = allowed && !rule->recursAt(nextDT); } if (allowed) { return nextDT; } } } // Couldn't find a valid occurrences in 1000 loops, something is wrong! return QDateTime(); } QDateTime Recurrence::getPreviousDateTime(const QDateTime &afterDateTime) const { QDateTime prevDT = afterDateTime; // prevent infinite loops, e.g. when an exrule extinguishes an rrule (e.g. // the exrule is identical to the rrule). If an occurrence is found, break // out of the loop by returning that QDateTime int loop = 0; while (loop < 1000) { // Outline of the algo: // 1) Find the next date/time after preDateTime when the event could recur // 1.1) Use the next occurrence from the explicit RDATE lists // 1.2) Add the next recurrence for each of the RRULEs // 2) Take the earliest recurrence of these = QDateTime nextDT // 3) If that date/time is not excluded, either explicitly by an EXDATE or // by an EXRULE, return nextDT as the next date/time of the recurrence // 4) If it's excluded, start all at 1), but starting at nextDT (instead // of preDateTime). Loop at most 1000 times. ++loop; // First, get the next recurrence from the RDate lists QList dates; if (prevDT > startDateTime()) { dates << startDateTime(); } const auto it = strictLowerBound(d->mRDateTimes.constBegin(), d->mRDateTimes.constEnd(), prevDT); if (it != d->mRDateTimes.constEnd()) { dates << *it; } QDateTime kdt(startDateTime()); for (const auto &date : qAsConst(d->mRDates)) { kdt.setDate(date); if (kdt < prevDT) { dates << kdt; break; } } // Add the previous occurrences from all RRULEs. for (const auto &rule : qAsConst(d->mRRules)) { QDateTime dt = rule->getPreviousDate(prevDT); if (dt.isValid()) { dates << dt; } } // Take the last of these (all others can't be used later on) sortAndRemoveDuplicates(dates); if (dates.isEmpty()) { return QDateTime(); } prevDT = dates.last(); // Check if that date/time is excluded explicitly or by an exrule: if (!std::binary_search(d->mExDates.constBegin(), d->mExDates.constEnd(), prevDT.date()) && !std::binary_search(d->mExDateTimes.constBegin(), d->mExDateTimes.constEnd(), prevDT)) { bool allowed = true; for (const auto &rule : qAsConst(d->mExRules)) { allowed = allowed && !rule->recursAt(prevDT); } if (allowed) { return prevDT; } } } // Couldn't find a valid occurrences in 1000 loops, something is wrong! return QDateTime(); } /***************************** PROTECTED FUNCTIONS ***************************/ RecurrenceRule::List Recurrence::rRules() const { return d->mRRules; } void Recurrence::addRRule(RecurrenceRule *rrule) { if (d->mRecurReadOnly || !rrule) { return; } rrule->setAllDay(d->mAllDay); d->mRRules.append(rrule); rrule->addObserver(this); updated(); } void Recurrence::removeRRule(RecurrenceRule *rrule) { if (d->mRecurReadOnly) { return; } d->mRRules.removeAll(rrule); rrule->removeObserver(this); updated(); } void Recurrence::deleteRRule(RecurrenceRule *rrule) { if (d->mRecurReadOnly) { return; } d->mRRules.removeAll(rrule); delete rrule; updated(); } RecurrenceRule::List Recurrence::exRules() const { return d->mExRules; } void Recurrence::addExRule(RecurrenceRule *exrule) { if (d->mRecurReadOnly || !exrule) { return; } exrule->setAllDay(d->mAllDay); d->mExRules.append(exrule); exrule->addObserver(this); updated(); } void Recurrence::removeExRule(RecurrenceRule *exrule) { if (d->mRecurReadOnly) { return; } d->mExRules.removeAll(exrule); exrule->removeObserver(this); updated(); } void Recurrence::deleteExRule(RecurrenceRule *exrule) { if (d->mRecurReadOnly) { return; } d->mExRules.removeAll(exrule); delete exrule; updated(); } QList Recurrence::rDateTimes() const { return d->mRDateTimes; } void Recurrence::setRDateTimes(const QList &rdates) { if (d->mRecurReadOnly) { return; } d->mRDateTimes = rdates; sortAndRemoveDuplicates(d->mRDateTimes); updated(); } void Recurrence::addRDateTime(const QDateTime &rdate) { if (d->mRecurReadOnly) { return; } setInsert(d->mRDateTimes, rdate); updated(); } DateList Recurrence::rDates() const { return d->mRDates; } void Recurrence::setRDates(const DateList &rdates) { if (d->mRecurReadOnly) { return; } d->mRDates = rdates; sortAndRemoveDuplicates(d->mRDates); updated(); } void Recurrence::addRDate(const QDate &rdate) { if (d->mRecurReadOnly) { return; } setInsert(d->mRDates, rdate); updated(); } QList Recurrence::exDateTimes() const { return d->mExDateTimes; } void Recurrence::setExDateTimes(const QList &exdates) { if (d->mRecurReadOnly) { return; } d->mExDateTimes = exdates; sortAndRemoveDuplicates(d->mExDateTimes); } void Recurrence::addExDateTime(const QDateTime &exdate) { if (d->mRecurReadOnly) { return; } setInsert(d->mExDateTimes, exdate); updated(); } DateList Recurrence::exDates() const { return d->mExDates; } void Recurrence::setExDates(const DateList &exdates) { if (d->mRecurReadOnly) { return; } DateList l = exdates; sortAndRemoveDuplicates(l); if (d->mExDates != l) { d->mExDates = l; updated(); } } void Recurrence::addExDate(const QDate &exdate) { if (d->mRecurReadOnly) { return; } setInsert(d->mExDates, exdate); updated(); } void Recurrence::recurrenceChanged(RecurrenceRule *) { updated(); } // %%%%%%%%%%%%%%%%%% end:Recurrencerule %%%%%%%%%%%%%%%%%% void Recurrence::dump() const { int i; int count = d->mRRules.count(); qCDebug(KCALCORE_LOG) << " -)" << count << "RRULEs:"; for (i = 0; i < count; ++i) { qCDebug(KCALCORE_LOG) << " -) RecurrenceRule: "; d->mRRules[i]->dump(); } count = d->mExRules.count(); qCDebug(KCALCORE_LOG) << " -)" << count << "EXRULEs:"; for (i = 0; i < count; ++i) { qCDebug(KCALCORE_LOG) << " -) ExceptionRule :"; d->mExRules[i]->dump(); } count = d->mRDates.count(); qCDebug(KCALCORE_LOG) << " -)" << count << "Recurrence Dates:"; for (i = 0; i < count; ++i) { qCDebug(KCALCORE_LOG) << " " << d->mRDates[i]; } count = d->mRDateTimes.count(); qCDebug(KCALCORE_LOG) << " -)" << count << "Recurrence Date/Times:"; for (i = 0; i < count; ++i) { qCDebug(KCALCORE_LOG) << " " << d->mRDateTimes[i]; } count = d->mExDates.count(); qCDebug(KCALCORE_LOG) << " -)" << count << "Exceptions Dates:"; for (i = 0; i < count; ++i) { qCDebug(KCALCORE_LOG) << " " << d->mExDates[i]; } count = d->mExDateTimes.count(); qCDebug(KCALCORE_LOG) << " -)" << count << "Exception Date/Times:"; for (i = 0; i < count; ++i) { qCDebug(KCALCORE_LOG) << " " << d->mExDateTimes[i]; } } Recurrence::RecurrenceObserver::~RecurrenceObserver() { } KCALCORE_EXPORT QDataStream &KCalCore::operator<<(QDataStream &out, KCalCore::Recurrence *r) { if (!r) { return out; } serializeQDateTimeList(out, r->d->mRDateTimes); serializeQDateTimeList(out, r->d->mExDateTimes); out << r->d->mRDates; serializeQDateTimeAsKDateTime(out, r->d->mStartDateTime); out << r->d->mCachedType << r->d->mAllDay << r->d->mRecurReadOnly << r->d->mExDates << r->d->mExRules.count() << r->d->mRRules.count(); for (RecurrenceRule *rule : qAsConst(r->d->mExRules)) { out << rule; } for (RecurrenceRule *rule : qAsConst(r->d->mRRules)) { out << rule; } return out; } KCALCORE_EXPORT QDataStream &KCalCore::operator>>(QDataStream &in, KCalCore::Recurrence *r) { if (!r) { return in; } int rruleCount, exruleCount; deserializeQDateTimeList(in, r->d->mRDateTimes); deserializeQDateTimeList(in, r->d->mExDateTimes); in >> r->d->mRDates; deserializeKDateTimeAsQDateTime(in, r->d->mStartDateTime); in >> r->d->mCachedType >> r->d->mAllDay >> r->d->mRecurReadOnly >> r->d->mExDates >> exruleCount >> rruleCount; r->d->mExRules.clear(); r->d->mRRules.clear(); for (int i = 0; i < exruleCount; ++i) { RecurrenceRule *rule = new RecurrenceRule(); rule->addObserver(r); in >> rule; r->d->mExRules.append(rule); } for (int i = 0; i < rruleCount; ++i) { RecurrenceRule *rule = new RecurrenceRule(); rule->addObserver(r); in >> rule; r->d->mRRules.append(rule); } return in; } diff --git a/src/todo.cpp b/src/todo.cpp index 18a9bf6ae..e6ea3cce2 100644 --- a/src/todo.cpp +++ b/src/todo.cpp @@ -1,587 +1,585 @@ /* This file is part of the kcalcore library. Copyright (c) 2001-2003 Cornelius Schumacher Copyright (C) 2009 Allen Winter 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. */ /** @file This file is part of the API for handling calendar data and defines the Todo class. @brief Provides a To-do in the sense of RFC2445. @author Cornelius Schumacher \ @author Allen Winter \ */ #include "todo.h" #include "visitor.h" #include "recurrence.h" #include "utils.h" #include "kcalcore_debug.h" #include using namespace KCalCore; /** Private class that helps to provide binary compatibility between releases. @internal */ //@cond PRIVATE class Q_DECL_HIDDEN KCalCore::Todo::Private { public: Private() {} Private(const KCalCore::Todo::Private &other) { init(other); } void init(const KCalCore::Todo::Private &other); QDateTime mDtDue; // to-do due date (if there is one) // ALSO the first occurrence of a recurring to-do QDateTime mDtRecurrence; // next occurrence (for recurring to-dos) QDateTime mCompleted; // to-do completion date (if it has been completed) int mPercentComplete = 0; // to-do percent complete [0,100] /** Returns true if the todo got a new date, else false will be returned. */ bool recurTodo(Todo *todo); }; void KCalCore::Todo::Private::init(const KCalCore::Todo::Private &other) { mDtDue = other.mDtDue; mDtRecurrence = other.mDtRecurrence; mCompleted = other.mCompleted; mPercentComplete = other.mPercentComplete; } //@endcond Todo::Todo() : d(new KCalCore::Todo::Private) { } Todo::Todo(const Todo &other) : Incidence(other), d(new KCalCore::Todo::Private(*other.d)) { } Todo::Todo(const Incidence &other) : Incidence(other) , d(new KCalCore::Todo::Private) { } Todo::~Todo() { delete d; } Todo *Todo::clone() const { return new Todo(*this); } IncidenceBase &Todo::assign(const IncidenceBase &other) { if (&other != this) { Incidence::assign(other); const Todo *t = static_cast(&other); d->init(*(t->d)); } return *this; } bool Todo::equals(const IncidenceBase &todo) const { if (!Incidence::equals(todo)) { return false; } else { // If they weren't the same type IncidenceBase::equals would had returned false already const Todo *t = static_cast(&todo); return ((dtDue() == t->dtDue()) || (!dtDue().isValid() && !t->dtDue().isValid())) && hasDueDate() == t->hasDueDate() && hasStartDate() == t->hasStartDate() && ((completed() == t->completed()) || (!completed().isValid() && !t->completed().isValid())) && hasCompletedDate() == t->hasCompletedDate() && percentComplete() == t->percentComplete(); } } Incidence::IncidenceType Todo::type() const { return TypeTodo; } QByteArray Todo::typeStr() const { return QByteArrayLiteral("Todo"); } void Todo::setDtDue(const QDateTime &dtDue, bool first) { startUpdates(); //int diffsecs = d->mDtDue.secsTo(dtDue); /*if (mReadOnly) return; const Alarm::List& alarms = alarms(); for (Alarm *alarm = alarms.first(); alarm; alarm = alarms.next()) { if (alarm->enabled()) { alarm->setTime(alarm->time().addSecs(diffsecs)); } }*/ if (recurs() && !first) { d->mDtRecurrence = dtDue; } else { d->mDtDue = dtDue; } if (recurs() && dtDue.isValid() && (!dtStart().isValid() || dtDue < recurrence()->startDateTime())) { qCDebug(KCALCORE_LOG) << "To-do recurrences are now calculated against DTSTART. Fixing legacy to-do."; setDtStart(dtDue); } /*const Alarm::List& alarms = alarms(); for (Alarm *alarm = alarms.first(); alarm; alarm = alarms.next()) alarm->setAlarmStart(d->mDtDue);*/ setFieldDirty(FieldDtDue); endUpdates(); } QDateTime Todo::dtDue(bool first) const { if (!hasDueDate()) { return QDateTime(); } const QDateTime start = IncidenceBase::dtStart(); if (recurs() && !first && d->mDtRecurrence.isValid()) { if (start.isValid()) { // This is the normal case, recurring to-dos have a valid DTSTART. const qint64 duration = start.daysTo(d->mDtDue); QDateTime dt = d->mDtRecurrence.addDays(duration); dt.setTime(d->mDtDue.time()); return dt; } else { // This is a legacy case, where recurrence was calculated against DTDUE return d->mDtRecurrence; } } return d->mDtDue; } bool Todo::hasDueDate() const { return d->mDtDue.isValid(); } bool Todo::hasStartDate() const { return IncidenceBase::dtStart().isValid(); } QDateTime Todo::dtStart() const { return dtStart(/*first=*/false); } QDateTime Todo::dtStart(bool first) const { if (!hasStartDate()) { return QDateTime(); } if (recurs() && !first && d->mDtRecurrence.isValid()) { return d->mDtRecurrence; } else { return IncidenceBase::dtStart(); } } bool Todo::isCompleted() const { return d->mPercentComplete == 100 || status() == StatusCompleted; } void Todo::setCompleted(bool completed) { update(); if (completed) { d->mPercentComplete = 100; setStatus(StatusCompleted); } else { d->mPercentComplete = 0; d->mCompleted = QDateTime(); setStatus(StatusNone); } setFieldDirty(FieldCompleted); setFieldDirty(FieldStatus); updated(); } QDateTime Todo::completed() const { if (hasCompletedDate()) { return d->mCompleted; } else { return QDateTime(); } } void Todo::setCompleted(const QDateTime &completed) { update(); if (!d->recurTodo(this)) { d->mPercentComplete = 100; d->mCompleted = completed.toUTC(); setFieldDirty(FieldCompleted); } updated(); } bool Todo::hasCompletedDate() const { return d->mCompleted.isValid(); } int Todo::percentComplete() const { return d->mPercentComplete; } void Todo::setPercentComplete(int percent) { if (percent > 100) { percent = 100; } else if (percent < 0) { percent = 0; } update(); d->mPercentComplete = percent; if (percent != 100) { d->mCompleted = QDateTime(); } setFieldDirty(FieldPercentComplete); updated(); } bool Todo::isInProgress(bool first) const { if (isOverdue()) { return false; } if (d->mPercentComplete > 0) { return true; } if (hasStartDate() && hasDueDate()) { if (allDay()) { QDate currDate = QDate::currentDate(); if (dtStart(first).date() <= currDate && currDate < dtDue(first).date()) { return true; } } else { QDateTime currDate = QDateTime::currentDateTimeUtc(); if (dtStart(first) <= currDate && currDate < dtDue(first)) { return true; } } } return false; } bool Todo::isOpenEnded() const { if (!hasDueDate() && !isCompleted()) { return true; } return false; } bool Todo::isNotStarted(bool first) const { if (d->mPercentComplete > 0) { return false; } if (!hasStartDate()) { return false; } if (allDay()) { if (dtStart(first).date() >= QDate::currentDate()) { return false; } } else { if (dtStart(first) >= QDateTime::currentDateTimeUtc()) { return false; } } return true; } void Todo::shiftTimes(const QTimeZone &oldZone, const QTimeZone &newZone) { Incidence::shiftTimes(oldZone, newZone); d->mDtDue = d->mDtDue.toTimeZone(oldZone); d->mDtDue.setTimeZone(newZone); if (recurs()) { d->mDtRecurrence = d->mDtRecurrence.toTimeZone(oldZone); d->mDtRecurrence.setTimeZone(newZone); } if (hasCompletedDate()) { d->mCompleted = d->mCompleted.toTimeZone(oldZone); d->mCompleted.setTimeZone(newZone); } } void Todo::setDtRecurrence(const QDateTime &dt) { d->mDtRecurrence = dt; setFieldDirty(FieldRecurrence); } QDateTime Todo::dtRecurrence() const { return d->mDtRecurrence.isValid() ? d->mDtRecurrence : d->mDtDue; } bool Todo::recursOn(const QDate &date, const QTimeZone &timeZone) const { QDate today = QDate::currentDate(); return Incidence::recursOn(date, timeZone) && !(date < today && d->mDtRecurrence.date() < today && d->mDtRecurrence > recurrence()->startDateTime()); } bool Todo::isOverdue() const { if (!dtDue().isValid()) { return false; // if it's never due, it can't be overdue } const bool inPast = allDay() ? dtDue().date() < QDate::currentDate() : dtDue() < QDateTime::currentDateTimeUtc(); return inPast && !isCompleted(); } void Todo::setAllDay(bool allday) { if (allday != allDay() && !mReadOnly) { if (hasDueDate()) { setFieldDirty(FieldDtDue); } Incidence::setAllDay(allday); } } //@cond PRIVATE bool Todo::Private::recurTodo(Todo *todo) { if (todo && todo->recurs()) { Recurrence *r = todo->recurrence(); const QDateTime recurrenceEndDateTime = r->endDateTime(); QDateTime nextOccurrenceDateTime = r->getNextDateTime(todo->dtStart()); if ((r->duration() == -1 || (nextOccurrenceDateTime.isValid() && recurrenceEndDateTime.isValid() && nextOccurrenceDateTime <= recurrenceEndDateTime))) { // We convert to the same timeSpec so we get the correct .date() const auto rightNow = QDateTime::currentDateTimeUtc().toTimeZone(nextOccurrenceDateTime.timeZone()); const bool isDateOnly = todo->allDay(); /* Now we search for the occurrence that's _after_ the currentUtcDateTime, or * if it's dateOnly, the occurrrence that's _during or after today_. * The reason we use "<" for date only, but "<=" for ocurrences with time is that * if it's date only, the user can still complete that ocurrence today, so that's * the current ocurrence that needs completing. */ while (!todo->recursAt(nextOccurrenceDateTime) || (!isDateOnly && nextOccurrenceDateTime <= rightNow) || (isDateOnly && nextOccurrenceDateTime.date() < rightNow.date())) { if (!nextOccurrenceDateTime.isValid() || (nextOccurrenceDateTime > recurrenceEndDateTime && r->duration() != -1)) { return false; } nextOccurrenceDateTime = r->getNextDateTime(nextOccurrenceDateTime); } todo->setDtRecurrence(nextOccurrenceDateTime); todo->setCompleted(false); todo->setRevision(todo->revision() + 1); return true; } } return false; } //@endcond bool Todo::accept(Visitor &v, const IncidenceBase::Ptr &incidence) { return v.visit(incidence.staticCast()); } QDateTime Todo::dateTime(DateTimeRole role) const { switch (role) { case RoleAlarmStartOffset: return dtStart(); case RoleAlarmEndOffset: return dtDue(); case RoleSort: // Sorting to-dos first compares dtDue, then dtStart if // dtDue doesn't exist return hasDueDate() ? dtDue() : dtStart(); case RoleCalendarHashing: return dtDue(); case RoleStartTimeZone: return dtStart(); case RoleEndTimeZone: return dtDue(); case RoleEndRecurrenceBase: return dtDue(); case RoleDisplayStart: case RoleDisplayEnd: return dtDue().isValid() ? dtDue() : dtStart(); case RoleAlarm: if (alarms().isEmpty()) { return QDateTime(); } else { Alarm::Ptr alarm = alarms().at(0); if (alarm->hasStartOffset() && hasStartDate()) { return dtStart(); } else if (alarm->hasEndOffset() && hasDueDate()) { return dtDue(); } else { // The application shouldn't add alarms on to-dos without dates. return QDateTime(); } } case RoleRecurrenceStart: if (dtStart().isValid()) { return dtStart(); } return dtDue(); //For the sake of backwards compatibility //where we calculated recurrences based on dtDue case RoleEnd: return dtDue(); default: return QDateTime(); } } void Todo::setDateTime(const QDateTime &dateTime, DateTimeRole role) { switch (role) { case RoleDnD: setDtDue(dateTime); break; case RoleEnd: setDtDue(dateTime, true); break; default: qCDebug(KCALCORE_LOG) << "Unhandled role" << role; } } void Todo::virtual_hook(VirtualHook id, void *data) { switch (id) { case IncidenceBase::SerializerHook: serialize(*reinterpret_cast(data)); break; case IncidenceBase::DeserializerHook: deserialize(*reinterpret_cast(data)); break; default: Q_ASSERT(false); } } QLatin1String Todo::mimeType() const { return Todo::todoMimeType(); } QLatin1String Todo::todoMimeType() { return QLatin1String("application/x-vnd.akonadi.calendar.todo"); } QLatin1String Todo::iconName(const QDateTime &recurrenceId) const { - QDateTime occurrenceDT = recurrenceId; - - const bool usesCompletedTaskPixmap = isCompleted() || - (recurs() && occurrenceDT.isValid() && - occurrenceDT < dtDue(false)); + const bool usesCompletedTaskPixmap = + isCompleted() || + (recurs() && recurrenceId.isValid() && (recurrenceId < dtDue(false))); if (usesCompletedTaskPixmap) { return QLatin1String("task-complete"); } else { return QLatin1String("view-calendar-tasks"); } } void Todo::serialize(QDataStream &out) { Incidence::serialize(out); serializeQDateTimeAsKDateTime(out, d->mDtDue); serializeQDateTimeAsKDateTime(out, d->mDtRecurrence); serializeQDateTimeAsKDateTime(out, d->mCompleted); out << d->mPercentComplete; } void Todo::deserialize(QDataStream &in) { Incidence::deserialize(in); deserializeKDateTimeAsQDateTime(in, d->mDtDue); deserializeKDateTimeAsQDateTime(in, d->mDtRecurrence); deserializeKDateTimeAsQDateTime(in, d->mCompleted); in >> d->mPercentComplete; } bool Todo::supportsGroupwareCommunication() const { return true; }