diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b4c7082d4..37e117376 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,155 +1,155 @@ ########### next target ############### set(kcalcore_LIB_SRCS alarm.cpp attachment.cpp attendee.cpp calendar.cpp calfilter.cpp calformat.cpp calstorage.cpp compat.cpp customproperties.cpp duration.cpp event.cpp exceptions.cpp filestorage.cpp freebusy.cpp freebusycache.cpp freebusyperiod.cpp icalformat.cpp icalformat_p.cpp icaltimezones.cpp incidence.cpp incidencebase.cpp journal.cpp memorycalendar.cpp occurrenceiterator.cpp period.cpp person.cpp recurrence.cpp recurrencerule.cpp schedulemessage.cpp sorting.cpp todo.cpp utils.cpp vcalformat.cpp visitor.cpp ) -ecm_qt_declare_logging_category(kcalcore_LIB_SRCS HEADER kcalcore_debug.h IDENTIFIER KCALCORE_LOG CATEGORY_NAME org.kde.pim.kcalcore) +ecm_qt_declare_logging_category(kcalcore_LIB_SRCS HEADER kcalendarcore_debug.h IDENTIFIER KCALCORE_LOG CATEGORY_NAME org.kde.pim.kcalcore) add_library(KF5CalendarCore ${kcalcore_LIB_SRCS}) generate_export_header(KF5CalendarCore BASE_NAME kcalendarcore) add_library(KF5::CalendarCore ALIAS KF5CalendarCore) # backward compatibility with the old name # TODO: remove for KF6 option(KCALENDARCORE_NO_DEPRECATED_NAMESPACE "Disable deprecated KCalCore namespace" OFF) if (NOT KCALENDARCORE_NO_DEPRECATED_NAMESPACE) target_compile_definitions(KF5CalendarCore INTERFACE "-DKCalCore=KCalendarCore") endif() target_include_directories(KF5CalendarCore INTERFACE "$") target_include_directories(KF5CalendarCore PUBLIC "$") target_link_libraries(KF5CalendarCore PUBLIC Qt5::Core PRIVATE Qt5::Gui LibIcal ) set_target_properties(KF5CalendarCore PROPERTIES VERSION ${KCALCORE_VERSION_STRING} SOVERSION ${KCALCORE_SOVERSION} EXPORT_NAME CalendarCore ) install(TARGETS KF5CalendarCore EXPORT KF5CalendarCoreTargets ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) ########### Generate Headers ############### set(kcalendarcore_headers Alarm Attachment Attendee CalFilter CalFormat CalStorage Calendar CustomProperties Duration Event Exceptions FileStorage FreeBusy FreeBusyCache FreeBusyPeriod ICalFormat Incidence IncidenceBase Journal MemoryCalendar OccurrenceIterator Period Person Recurrence RecurrenceRule ScheduleMessage Sorting Todo VCalFormat Visitor ) ecm_generate_headers(KCalendarCore_CamelCase_HEADERS HEADER_NAMES ${kcalendarcore_headers} PREFIX KCalendarCore REQUIRED_HEADERS KCalendarCore_HEADERS ) if (NOT KCALENDARCORE_NO_DEPRECATED_NAMESPACE) ecm_generate_headers(KCalCore_CamelCase_HEADERS HEADER_NAMES ${kcalendarcore_headers} PREFIX KCalCore REQUIRED_HEADERS KCalCore_HEADERS ) endif() ########### install files ############### install(FILES ${CMAKE_CURRENT_BINARY_DIR}/kcalendarcore_export.h ${KCalendarCore_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KCalendarCore/kcalendarcore COMPONENT Devel ) install(FILES ${KCalendarCore_CamelCase_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KCalendarCore/KCalendarCore COMPONENT Devel ) if (NOT KCALENDARCORE_NO_DEPRECATED_NAMESPACE) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/kcalendarcore_export.h ${KCalCore_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KCalendarCore/kcalcore COMPONENT Devel ) install(FILES ${KCalCore_CamelCase_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KCalendarCore/KCalCore COMPONENT Devel ) endif() ecm_generate_pri_file(BASE_NAME KCalendarCore LIB_NAME KF5CalendarCore DEPS "Core" FILENAME_VAR PRI_FILENAME INCLUDE_INSTALL_DIR ${KDE_INSTALL_INCLUDEDIR_KF5}/KCalendarCore/KCalendarCore) install(FILES ${PRI_FILENAME} DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) diff --git a/src/calendar.cpp b/src/calendar.cpp index 2b5365ffe..c4d3fb4fe 100644 --- a/src/calendar.cpp +++ b/src/calendar.cpp @@ -1,1368 +1,1368 @@ /* This file is part of the kcalcore library. Copyright (c) 1998 Preston Brown Copyright (c) 2000-2004 Cornelius Schumacher Copyright (C) 2003-2004 Reinhold Kainhofer Copyright (c) 2006 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. */ /** @file This file is part of the API for handling calendar data and defines the Calendar class. @brief Represents the main calendar class. @author Preston Brown \ @author Cornelius Schumacher \ @author Reinhold Kainhofer \ @author David Jarvie \ */ #include "calendar.h" #include "calendar_p.h" #include "calfilter.h" #include "icaltimezones_p.h" #include "sorting.h" #include "visitor.h" -#include "kcalcore_debug.h" +#include "kcalendarcore_debug.h" #include extern "C" { #include } #include // for std::remove() using namespace KCalendarCore; /** Make a QHash::value that returns a QVector. */ template QVector values(const QMultiHash &c) { QVector v; v.reserve(c.size()); for (typename QMultiHash::const_iterator it = c.begin(), end = c.end(); it != end; ++it) { v.push_back(it.value()); } return v; } template QVector values(const QMultiHash &c, const K &x) { QVector v; typename QMultiHash::const_iterator it = c.find(x); while (it != c.end() && it.key() == x) { v.push_back(it.value()); ++it; } return v; } /** Template for a class that implements a visitor for adding an Incidence to a resource supporting addEvent(), addTodo() and addJournal() calls. */ template class AddVisitor : public Visitor { public: AddVisitor(T *r) : mResource(r) {} bool visit(const Event::Ptr &e) override { return mResource->addEvent(e); } bool visit(const Todo::Ptr &t) override { return mResource->addTodo(t); } bool visit(const Journal::Ptr &j) override { return mResource->addJournal(j); } bool visit(const FreeBusy::Ptr &) override { return false; } private: T *mResource; }; /** Template for a class that implements a visitor for deleting an Incidence from a resource supporting deleteEvent(), deleteTodo() and deleteJournal() calls. */ template class DeleteVisitor : public Visitor { public: DeleteVisitor(T *r) : mResource(r) {} bool visit(const Event::Ptr &e) override { mResource->deleteEvent(e); return true; } bool visit(const Todo::Ptr &t) override { mResource->deleteTodo(t); return true; } bool visit(const Journal::Ptr &j) override { mResource->deleteJournal(j); return true; } bool visit(const FreeBusy::Ptr &) override { return false; } private: T *mResource; }; //@endcond Calendar::Calendar(const QTimeZone &timeZone) : d(new KCalendarCore::Calendar::Private) { d->mTimeZone = timeZone; } Calendar::Calendar(const QByteArray &timeZoneId) : d(new KCalendarCore::Calendar::Private) { setTimeZoneId(timeZoneId); } Calendar::~Calendar() { delete d; } Person Calendar::owner() const { return d->mOwner; } void Calendar::setOwner(const Person &owner) { d->mOwner = owner; setModified(true); } void Calendar::setTimeZone(const QTimeZone &timeZone) { d->mTimeZone = timeZone; doSetTimeZone(d->mTimeZone); } QTimeZone Calendar::timeZone() const { return d->mTimeZone; } void Calendar::setTimeZoneId(const QByteArray &timeZoneId) { d->mTimeZone = d->timeZoneIdSpec(timeZoneId); doSetTimeZone(d->mTimeZone); //NOLINT false clang-analyzer-optin.cplusplus.VirtualCall } //@cond PRIVATE QTimeZone Calendar::Private::timeZoneIdSpec(const QByteArray &timeZoneId) { if (timeZoneId == QByteArrayLiteral("UTC")) { return QTimeZone::utc(); } auto tz = QTimeZone(timeZoneId); if (tz.isValid()) { return tz; } return QTimeZone::systemTimeZone(); } //@endcond QByteArray Calendar::timeZoneId() const { return d->mTimeZone.id(); } void Calendar::shiftTimes(const QTimeZone &oldZone, const QTimeZone &newZone) { setTimeZone(newZone); int i, end; Event::List ev = events(); for (i = 0, end = ev.count(); i < end; ++i) { ev[i]->shiftTimes(oldZone, newZone); } Todo::List to = todos(); for (i = 0, end = to.count(); i < end; ++i) { to[i]->shiftTimes(oldZone, newZone); } Journal::List jo = journals(); for (i = 0, end = jo.count(); i < end; ++i) { jo[i]->shiftTimes(oldZone, newZone); } } void Calendar::setFilter(CalFilter *filter) { if (filter) { d->mFilter = filter; } else { d->mFilter = d->mDefaultFilter; } emit filterChanged(); } CalFilter *Calendar::filter() const { return d->mFilter; } QStringList Calendar::categories() const { Incidence::List rawInc(rawIncidences()); QStringList cats, thisCats; // @TODO: For now just iterate over all incidences. In the future, // the list of categories should be built when reading the file. for (Incidence::List::ConstIterator i = rawInc.constBegin(); i != rawInc.constEnd(); ++i) { thisCats = (*i)->categories(); for (QStringList::ConstIterator si = thisCats.constBegin(); si != thisCats.constEnd(); ++si) { if (!cats.contains(*si)) { cats.append(*si); } } } return cats; } Incidence::List Calendar::incidences(const QDate &date) const { return mergeIncidenceList(events(date), todos(date), journals(date)); } Incidence::List Calendar::incidences() const { return mergeIncidenceList(events(), todos(), journals()); } Incidence::List Calendar::rawIncidences() const { return mergeIncidenceList(rawEvents(), rawTodos(), rawJournals()); } Incidence::List Calendar::instances(const Incidence::Ptr &incidence) const { if (incidence) { Event::List elist; Todo::List tlist; Journal::List jlist; if (incidence->type() == Incidence::TypeEvent) { elist = eventInstances(incidence); } else if (incidence->type() == Incidence::TypeTodo) { tlist = todoInstances(incidence); } else if (incidence->type() == Incidence::TypeJournal) { jlist = journalInstances(incidence); } return mergeIncidenceList(elist, tlist, jlist); } else { return Incidence::List(); } } Incidence::List Calendar::duplicates(const Incidence::Ptr &incidence) { if (incidence) { Incidence::List list; Incidence::List vals = values(d->mNotebookIncidences); Incidence::List::const_iterator it; for (it = vals.constBegin(); it != vals.constEnd(); ++it) { if (((incidence->dtStart() == (*it)->dtStart()) || (!incidence->dtStart().isValid() && !(*it)->dtStart().isValid())) && (incidence->summary() == (*it)->summary())) { list.append(*it); } } return list; } else { return Incidence::List(); } } bool Calendar::addNotebook(const QString ¬ebook, bool isVisible) { if (d->mNotebooks.contains(notebook)) { return false; } else { d->mNotebooks.insert(notebook, isVisible); return true; } } bool Calendar::updateNotebook(const QString ¬ebook, bool isVisible) { if (!d->mNotebooks.contains(notebook)) { return false; } else { d->mNotebooks.insert(notebook, isVisible); return true; } } bool Calendar::deleteNotebook(const QString ¬ebook) { if (!d->mNotebooks.contains(notebook)) { return false; } else { return d->mNotebooks.remove(notebook); } } bool Calendar::setDefaultNotebook(const QString ¬ebook) { if (!d->mNotebooks.contains(notebook)) { return false; } else { d->mDefaultNotebook = notebook; return true; } } QString Calendar::defaultNotebook() const { return d->mDefaultNotebook; } bool Calendar::hasValidNotebook(const QString ¬ebook) const { return d->mNotebooks.contains(notebook); } bool Calendar::isVisible(const Incidence::Ptr &incidence) const { if (d->mIncidenceVisibility.contains(incidence)) { return d->mIncidenceVisibility[incidence]; } const QString nuid = notebook(incidence); bool rv; if (d->mNotebooks.contains(nuid)) { rv = d->mNotebooks.value(nuid); } else { // NOTE returns true also for nonexisting notebooks for compatibility rv = true; } d->mIncidenceVisibility[incidence] = rv; return rv; } void Calendar::clearNotebookAssociations() { d->mNotebookIncidences.clear(); d->mUidToNotebook.clear(); d->mIncidenceVisibility.clear(); } bool Calendar::setNotebook(const Incidence::Ptr &inc, const QString ¬ebook) { if (!inc) { return false; } if (!notebook.isEmpty() && !incidence(inc->uid(), inc->recurrenceId())) { qCWarning(KCALCORE_LOG) << "cannot set notebook until incidence has been added"; return false; } if (d->mUidToNotebook.contains(inc->uid())) { QString old = d->mUidToNotebook.value(inc->uid()); if (!old.isEmpty() && notebook != old) { if (inc->hasRecurrenceId()) { qCWarning(KCALCORE_LOG) << "cannot set notebook for child incidences"; return false; } // Move all possible children also. Incidence::List list = instances(inc); Incidence::List::Iterator it; for (it = list.begin(); it != list.end(); ++it) { d->mNotebookIncidences.remove(old, *it); d->mNotebookIncidences.insert(notebook, *it); } notifyIncidenceChanged(inc); // for removing from old notebook // don not remove from mUidToNotebook to keep deleted incidences d->mNotebookIncidences.remove(old, inc); } } if (!notebook.isEmpty()) { d->mUidToNotebook.insert(inc->uid(), notebook); d->mNotebookIncidences.insert(notebook, inc); qCDebug(KCALCORE_LOG) << "setting notebook" << notebook << "for" << inc->uid(); notifyIncidenceChanged(inc); // for inserting into new notebook } return true; } QString Calendar::notebook(const Incidence::Ptr &incidence) const { if (incidence) { return d->mUidToNotebook.value(incidence->uid()); } else { return QString(); } } QString Calendar::notebook(const QString &uid) const { return d->mUidToNotebook.value(uid); } QStringList Calendar::notebooks() const { return d->mNotebookIncidences.uniqueKeys(); } Incidence::List Calendar::incidences(const QString ¬ebook) const { if (notebook.isEmpty()) { return values(d->mNotebookIncidences); } else { return values(d->mNotebookIncidences, notebook); } } /** static */ Event::List Calendar::sortEvents(const Event::List &eventList, EventSortField sortField, SortDirection sortDirection) { if (eventList.isEmpty()) { return Event::List(); } Event::List eventListSorted; // Notice we alphabetically presort Summaries first. // We do this so comparison "ties" stay in a nice order. eventListSorted = eventList; switch (sortField) { case EventSortUnsorted: break; case EventSortStartDate: if (sortDirection == SortDirectionAscending) { std::sort(eventListSorted.begin(), eventListSorted.end(), Events::startDateLessThan); } else { std::sort(eventListSorted.begin(), eventListSorted.end(), Events::startDateMoreThan); } break; case EventSortEndDate: if (sortDirection == SortDirectionAscending) { std::sort(eventListSorted.begin(), eventListSorted.end(), Events::endDateLessThan); } else { std::sort(eventListSorted.begin(), eventListSorted.end(), Events::endDateMoreThan); } break; case EventSortSummary: if (sortDirection == SortDirectionAscending) { std::sort(eventListSorted.begin(), eventListSorted.end(), Events::summaryLessThan); } else { std::sort(eventListSorted.begin(), eventListSorted.end(), Events::summaryMoreThan); } break; } return eventListSorted; } Event::List Calendar::events(const QDate &date, const QTimeZone &timeZone, EventSortField sortField, SortDirection sortDirection) const { Event::List el = rawEventsForDate(date, timeZone, sortField, sortDirection); d->mFilter->apply(&el); return el; } Event::List Calendar::events(const QDateTime &dt) const { Event::List el = rawEventsForDate(dt); d->mFilter->apply(&el); return el; } Event::List Calendar::events(const QDate &start, const QDate &end, const QTimeZone &timeZone, bool inclusive) const { Event::List el = rawEvents(start, end, timeZone, inclusive); d->mFilter->apply(&el); return el; } Event::List Calendar::events(EventSortField sortField, SortDirection sortDirection) const { Event::List el = rawEvents(sortField, sortDirection); d->mFilter->apply(&el); return el; } bool Calendar::addIncidence(const Incidence::Ptr &incidence) { if (!incidence) { return false; } AddVisitor v(this); return incidence->accept(v, incidence); } bool Calendar::deleteIncidence(const Incidence::Ptr &incidence) { if (!incidence) { return false; } if (beginChange(incidence)) { DeleteVisitor v(this); const bool result = incidence->accept(v, incidence); endChange(incidence); return result; } else { return false; } } Incidence::Ptr Calendar::createException(const Incidence::Ptr &incidence, const QDateTime &recurrenceId, bool thisAndFuture) { Q_ASSERT(recurrenceId.isValid()); if (!incidence || !incidence->recurs() || !recurrenceId.isValid()) { return Incidence::Ptr(); } Incidence::Ptr newInc(incidence->clone()); newInc->setCreated(QDateTime::currentDateTimeUtc()); newInc->setRevision(0); //Recurring exceptions are not support for now newInc->clearRecurrence(); newInc->setRecurrenceId(recurrenceId); newInc->setThisAndFuture(thisAndFuture); newInc->setDtStart(recurrenceId); // Calculate and set the new end of the incidence QDateTime end = incidence->dateTime(IncidenceBase::RoleEnd); if (end.isValid()) { if (incidence->allDay()) { qint64 offset = incidence->dtStart().daysTo(recurrenceId); end = end.addDays(offset); } else { qint64 offset = incidence->dtStart().secsTo(recurrenceId); end = end.addSecs(offset); } newInc->setDateTime(end, IncidenceBase::RoleEnd); } return newInc; } Incidence::Ptr Calendar::incidence(const QString &uid, const QDateTime &recurrenceId) const { Incidence::Ptr i = event(uid, recurrenceId); if (i) { return i; } i = todo(uid, recurrenceId); if (i) { return i; } i = journal(uid, recurrenceId); return i; } Incidence::Ptr Calendar::deleted(const QString &uid, const QDateTime &recurrenceId) const { Incidence::Ptr i = deletedEvent(uid, recurrenceId); if (i) { return i; } i = deletedTodo(uid, recurrenceId); if (i) { return i; } i = deletedJournal(uid, recurrenceId); return i; } Incidence::List Calendar::incidencesFromSchedulingID(const QString &sid) const { Incidence::List result; const Incidence::List incidences = rawIncidences(); Incidence::List::const_iterator it = incidences.begin(); for (; it != incidences.end(); ++it) { if ((*it)->schedulingID() == sid) { result.append(*it); } } return result; } Incidence::Ptr Calendar::incidenceFromSchedulingID(const QString &uid) const { const Incidence::List incidences = rawIncidences(); Incidence::List::const_iterator it = incidences.begin(); for (; it != incidences.end(); ++it) { if ((*it)->schedulingID() == uid) { // Touchdown, and the crowd goes wild return *it; } } // Not found return Incidence::Ptr(); } /** static */ Todo::List Calendar::sortTodos(const Todo::List &todoList, TodoSortField sortField, SortDirection sortDirection) { if (todoList.isEmpty()) { return Todo::List(); } Todo::List todoListSorted; // Notice we alphabetically presort Summaries first. // We do this so comparison "ties" stay in a nice order. // Note that To-dos may not have Start DateTimes nor due DateTimes. todoListSorted = todoList; switch (sortField) { case TodoSortUnsorted: break; case TodoSortStartDate: if (sortDirection == SortDirectionAscending) { std::sort(todoListSorted.begin(), todoListSorted.end(), Todos::startDateLessThan); } else { std::sort(todoListSorted.begin(), todoListSorted.end(), Todos::startDateMoreThan); } break; case TodoSortDueDate: if (sortDirection == SortDirectionAscending) { std::sort(todoListSorted.begin(), todoListSorted.end(), Todos::dueDateLessThan); } else { std::sort(todoListSorted.begin(), todoListSorted.end(), Todos::dueDateMoreThan); } break; case TodoSortPriority: if (sortDirection == SortDirectionAscending) { std::sort(todoListSorted.begin(), todoListSorted.end(), Todos::priorityLessThan); } else { std::sort(todoListSorted.begin(), todoListSorted.end(), Todos::priorityMoreThan); } break; case TodoSortPercentComplete: if (sortDirection == SortDirectionAscending) { std::sort(todoListSorted.begin(), todoListSorted.end(), Todos::percentLessThan); } else { std::sort(todoListSorted.begin(), todoListSorted.end(), Todos::percentMoreThan); } break; case TodoSortSummary: if (sortDirection == SortDirectionAscending) { std::sort(todoListSorted.begin(), todoListSorted.end(), Todos::summaryLessThan); } else { std::sort(todoListSorted.begin(), todoListSorted.end(), Todos::summaryMoreThan); } break; case TodoSortCreated: if (sortDirection == SortDirectionAscending) { std::sort(todoListSorted.begin(), todoListSorted.end(), Todos::createdLessThan); } else { std::sort(todoListSorted.begin(), todoListSorted.end(), Todos::createdMoreThan); } break; } return todoListSorted; } Todo::List Calendar::todos(TodoSortField sortField, SortDirection sortDirection) const { Todo::List tl = rawTodos(sortField, sortDirection); d->mFilter->apply(&tl); return tl; } Todo::List Calendar::todos(const QDate &date) const { Todo::List el = rawTodosForDate(date); d->mFilter->apply(&el); return el; } Todo::List Calendar::todos(const QDate &start, const QDate &end, const QTimeZone &timeZone, bool inclusive) const { Todo::List tl = rawTodos(start, end, timeZone, inclusive); d->mFilter->apply(&tl); return tl; } /** static */ Journal::List Calendar::sortJournals(const Journal::List &journalList, JournalSortField sortField, SortDirection sortDirection) { if (journalList.isEmpty()) { return Journal::List(); } Journal::List journalListSorted = journalList; switch (sortField) { case JournalSortUnsorted: break; case JournalSortDate: if (sortDirection == SortDirectionAscending) { std::sort(journalListSorted.begin(), journalListSorted.end(), Journals::dateLessThan); } else { std::sort(journalListSorted.begin(), journalListSorted.end(), Journals::dateMoreThan); } break; case JournalSortSummary: if (sortDirection == SortDirectionAscending) { std::sort(journalListSorted.begin(), journalListSorted.end(), Journals::summaryLessThan); } else { std::sort(journalListSorted.begin(), journalListSorted.end(), Journals::summaryMoreThan); } break; } return journalListSorted; } Journal::List Calendar::journals(JournalSortField sortField, SortDirection sortDirection) const { Journal::List jl = rawJournals(sortField, sortDirection); d->mFilter->apply(&jl); return jl; } Journal::List Calendar::journals(const QDate &date) const { Journal::List el = rawJournalsForDate(date); d->mFilter->apply(&el); return el; } // When this is called, the to-dos have already been added to the calendar. // This method is only about linking related to-dos. void Calendar::setupRelations(const Incidence::Ptr &forincidence) { if (!forincidence) { return; } const QString uid = forincidence->uid(); // First, go over the list of orphans and see if this is their parent Incidence::List l = values(d->mOrphans, uid); d->mOrphans.remove(uid); if (!l.isEmpty()) { Incidence::List &relations = d->mIncidenceRelations[uid]; relations.reserve(relations.count() + l.count()); for (int i = 0, end = l.count(); i < end; ++i) { relations.append(l[i]); d->mOrphanUids.remove(l[i]->uid()); } } // Now see about this incidences parent if (forincidence->relatedTo().isEmpty() && !forincidence->relatedTo().isEmpty()) { // Incidence has a uid it is related to but is not registered to it yet. // Try to find it Incidence::Ptr parent = incidence(forincidence->relatedTo()); if (parent) { // Found it // look for hierarchy loops if (isAncestorOf(forincidence, parent)) { forincidence->setRelatedTo(QString()); qCWarning(KCALCORE_LOG) << "hierarchy loop between " << forincidence->uid() << " and " << parent->uid(); } else { d->mIncidenceRelations[parent->uid()].append(forincidence); } } else { // Not found, put this in the mOrphans list // Note that the mOrphans dict might contain multiple entries with the // same key! which are multiple children that wait for the parent // incidence to be inserted. d->mOrphans.insert(forincidence->relatedTo(), forincidence); d->mOrphanUids.insert(forincidence->uid(), forincidence); } } } // If a to-do with sub-to-dos is deleted, move it's sub-to-dos to the orphan list void Calendar::removeRelations(const Incidence::Ptr &incidence) { if (!incidence) { qCDebug(KCALCORE_LOG) << "Warning: incidence is 0"; return; } const QString uid = incidence->uid(); for (const Incidence::Ptr &i : qAsConst(d->mIncidenceRelations[uid])) { if (!d->mOrphanUids.contains(i->uid())) { d->mOrphans.insert(uid, i); d->mOrphanUids.insert(i->uid(), i); i->setRelatedTo(uid); } } const QString parentUid = incidence->relatedTo(); // If this incidence is related to something else, tell that about it if (!parentUid.isEmpty()) { Incidence::List &relations = d->mIncidenceRelations[parentUid]; relations.erase( std::remove(relations.begin(), relations.end(), incidence), relations.end()); } // Remove this one from the orphans list if (d->mOrphanUids.remove(uid)) { // This incidence is located in the orphans list - it should be removed // Since the mOrphans dict might contain the same key (with different // child incidence pointers!) multiple times, take care that we remove // the correct one. So we need to remove all items with the given // parent UID, and readd those that are not for this item. Also, there // might be other entries with differnet UID that point to this // incidence (this might happen when the relatedTo of the item is // changed before its parent is inserted. This might happen with // groupware servers....). Remove them, too QStringList relatedToUids; // First, create a list of all keys in the mOrphans list which point // to the removed item relatedToUids << incidence->relatedTo(); for (QMultiHash::Iterator it = d->mOrphans.begin(); it != d->mOrphans.end(); ++it) { if (it.value()->uid() == uid) { relatedToUids << it.key(); } } // now go through all uids that have one entry that point to the incidence for (QStringList::const_iterator uidit = relatedToUids.constBegin(); uidit != relatedToUids.constEnd(); ++uidit) { Incidence::List tempList; // Remove all to get access to the remaining entries const Incidence::List l = values(d->mOrphans, *uidit); d->mOrphans.remove(*uidit); for (const Incidence::Ptr &i : l) { if (i != incidence) { tempList.append(i); } } // Readd those that point to a different orphan incidence for (Incidence::List::Iterator incit = tempList.begin(); incit != tempList.end(); ++incit) { d->mOrphans.insert(*uidit, *incit); } } } // Make sure the deleted incidence doesn't relate to a non-deleted incidence, // since that would cause trouble in MemoryCalendar::close(), as the deleted // incidences are destroyed after the non-deleted incidences. The destructor // of the deleted incidences would then try to access the already destroyed // non-deleted incidence, which would segfault. // // So in short: Make sure dead incidences don't point to alive incidences // via the relation. // // This crash is tested in MemoryCalendarTest::testRelationsCrash(). // incidence->setRelatedTo( Incidence::Ptr() ); } bool Calendar::isAncestorOf(const Incidence::Ptr &ancestor, const Incidence::Ptr &incidence) const { if (!incidence || incidence->relatedTo().isEmpty()) { return false; } else if (incidence->relatedTo() == ancestor->uid()) { return true; } else { return isAncestorOf(ancestor, this->incidence(incidence->relatedTo())); } } Incidence::List Calendar::relations(const QString &uid) const { return d->mIncidenceRelations[uid]; } Calendar::CalendarObserver::~CalendarObserver() { } void Calendar::CalendarObserver::calendarModified(bool modified, Calendar *calendar) { Q_UNUSED(modified); Q_UNUSED(calendar); } void Calendar::CalendarObserver::calendarIncidenceAdded(const Incidence::Ptr &incidence) { Q_UNUSED(incidence); } void Calendar::CalendarObserver::calendarIncidenceChanged(const Incidence::Ptr &incidence) { Q_UNUSED(incidence); } void Calendar::CalendarObserver::calendarIncidenceAboutToBeDeleted(const Incidence::Ptr &incidence) { Q_UNUSED(incidence); } void Calendar::CalendarObserver::calendarIncidenceDeleted(const Incidence::Ptr &incidence, const Calendar *calendar) { Q_UNUSED(incidence); Q_UNUSED(calendar); } void Calendar::CalendarObserver::calendarIncidenceAdditionCanceled(const Incidence::Ptr &incidence) { Q_UNUSED(incidence); } void Calendar::registerObserver(CalendarObserver *observer) { if (!observer) { return; } if (!d->mObservers.contains(observer)) { d->mObservers.append(observer); } else { d->mNewObserver = true; } } void Calendar::unregisterObserver(CalendarObserver *observer) { if (!observer) { return; } else { d->mObservers.removeAll(observer); } } bool Calendar::isSaving() const { return false; } void Calendar::setModified(bool modified) { if (modified != d->mModified || d->mNewObserver) { d->mNewObserver = false; for (CalendarObserver *observer : qAsConst(d->mObservers)) { observer->calendarModified(modified, this); } d->mModified = modified; } } bool Calendar::isModified() const { return d->mModified; } bool Calendar::save() { return true; } bool Calendar::reload() { return true; } void Calendar::incidenceUpdated(const QString &uid, const QDateTime &recurrenceId) { Incidence::Ptr inc = incidence(uid, recurrenceId); if (!inc) { return; } inc->setLastModified(QDateTime::currentDateTimeUtc()); // we should probably update the revision number here, // or internally in the Event itself when certain things change. // need to verify with ical documentation. notifyIncidenceChanged(inc); setModified(true); } void Calendar::doSetTimeZone(const QTimeZone &timeZone) { Q_UNUSED(timeZone); } void Calendar::notifyIncidenceAdded(const Incidence::Ptr &incidence) { if (!incidence) { return; } if (!d->mObserversEnabled) { return; } for (CalendarObserver *observer : qAsConst(d->mObservers)) { observer->calendarIncidenceAdded(incidence); } for (auto role : { IncidenceBase::RoleStartTimeZone, IncidenceBase::RoleEndTimeZone }) { const auto dt = incidence->dateTime(role); if (dt.isValid() && dt.timeZone() != QTimeZone::utc()) { if (!d->mTimeZones.contains(dt.timeZone())) { d->mTimeZones.push_back(dt.timeZone()); } } } } void Calendar::notifyIncidenceChanged(const Incidence::Ptr &incidence) { if (!incidence) { return; } if (!d->mObserversEnabled) { return; } for (CalendarObserver *observer : qAsConst(d->mObservers)) { observer->calendarIncidenceChanged(incidence); } } void Calendar::notifyIncidenceAboutToBeDeleted(const Incidence::Ptr &incidence) { if (!incidence) { return; } if (!d->mObserversEnabled) { return; } for (CalendarObserver *observer : qAsConst(d->mObservers)) { observer->calendarIncidenceAboutToBeDeleted(incidence); } } void Calendar::notifyIncidenceDeleted(const Incidence::Ptr &incidence) { if (!incidence) { return; } if (!d->mObserversEnabled) { return; } for (CalendarObserver *observer : qAsConst(d->mObservers)) { observer->calendarIncidenceDeleted(incidence, this); } } void Calendar::notifyIncidenceAdditionCanceled(const Incidence::Ptr &incidence) { if (!incidence) { return; } if (!d->mObserversEnabled) { return; } for (CalendarObserver *observer : qAsConst(d->mObservers)) { observer->calendarIncidenceAdditionCanceled(incidence); } } void Calendar::customPropertyUpdated() { setModified(true); } void Calendar::setProductId(const QString &id) { d->mProductId = id; } QString Calendar::productId() const { return d->mProductId; } /** static */ Incidence::List Calendar::mergeIncidenceList(const Event::List &events, const Todo::List &todos, const Journal::List &journals) { Incidence::List incidences; incidences.reserve(events.count() + todos.count() + journals.count()); int i, end; for (i = 0, end = events.count(); i < end; ++i) { incidences.append(events[i]); } for (i = 0, end = todos.count(); i < end; ++i) { incidences.append(todos[i]); } for (i = 0, end = journals.count(); i < end; ++i) { incidences.append(journals[i]); } return incidences; } bool Calendar::beginChange(const Incidence::Ptr &incidence) { Q_UNUSED(incidence); return true; } bool Calendar::endChange(const Incidence::Ptr &incidence) { Q_UNUSED(incidence); return true; } void Calendar::setObserversEnabled(bool enabled) { d->mObserversEnabled = enabled; } void Calendar::appendAlarms(Alarm::List &alarms, const Incidence::Ptr &incidence, const QDateTime &from, const QDateTime &to) const { QDateTime preTime = from.addSecs(-1); Alarm::List alarmlist = incidence->alarms(); for (int i = 0, iend = alarmlist.count(); i < iend; ++i) { if (alarmlist[i]->enabled()) { QDateTime dt = alarmlist[i]->nextRepetition(preTime); if (dt.isValid() && dt <= to) { qCDebug(KCALCORE_LOG) << incidence->summary() << "':" << dt.toString(); alarms.append(alarmlist[i]); } } } } void Calendar::appendRecurringAlarms(Alarm::List &alarms, const Incidence::Ptr &incidence, const QDateTime &from, const QDateTime &to) const { QDateTime dt; bool endOffsetValid = false; Duration endOffset(0); Duration period(from, to); Alarm::List alarmlist = incidence->alarms(); for (int i = 0, iend = alarmlist.count(); i < iend; ++i) { Alarm::Ptr a = alarmlist[i]; if (a->enabled()) { if (a->hasTime()) { // The alarm time is defined as an absolute date/time dt = a->nextRepetition(from.addSecs(-1)); if (!dt.isValid() || dt > to) { continue; } } else { // Alarm time is defined by an offset from the event start or end time. // Find the offset from the event start time, which is also used as the // offset from the recurrence time. Duration offset(0); if (a->hasStartOffset()) { offset = a->startOffset(); } else if (a->hasEndOffset()) { offset = a->endOffset(); if (!endOffsetValid) { endOffset = Duration(incidence->dtStart(), incidence->dateTime(Incidence::RoleAlarmEndOffset)); endOffsetValid = true; } } // Find the incidence's earliest alarm QDateTime alarmStart = offset.end(a->hasEndOffset() ? incidence->dateTime(Incidence::RoleAlarmEndOffset) : incidence->dtStart()); if (alarmStart > to) { continue; } QDateTime baseStart = incidence->dtStart(); if (from > alarmStart) { alarmStart = from; // don't look earlier than the earliest alarm baseStart = (-offset).end((-endOffset).end(alarmStart)); } // Adjust the 'alarmStart' date/time and find the next recurrence at or after it. // Treate the two offsets separately in case one is daily and the other not. dt = incidence->recurrence()->getNextDateTime(baseStart.addSecs(-1)); if (!dt.isValid() || (dt = endOffset.end(offset.end(dt))) > to) { // adjust 'dt' to get the alarm time // The next recurrence is too late. if (!a->repeatCount()) { continue; } // The alarm has repetitions, so check whether repetitions of previous // recurrences fall within the time period. bool found = false; Duration alarmDuration = a->duration(); for (QDateTime base = baseStart; (dt = incidence->recurrence()->getPreviousDateTime(base)).isValid(); base = dt) { if (a->duration().end(dt) < base) { break; // this recurrence's last repetition is too early, so give up } // The last repetition of this recurrence is at or after 'alarmStart' time. // Check if a repetition occurs between 'alarmStart' and 'to'. int snooze = a->snoozeTime().value(); // in seconds or days if (a->snoozeTime().isDaily()) { Duration toFromDuration(dt, base); int toFrom = toFromDuration.asDays(); if (a->snoozeTime().end(from) <= to || (toFromDuration.isDaily() && toFrom % snooze == 0) || (toFrom / snooze + 1) * snooze <= toFrom + period.asDays()) { found = true; #ifndef NDEBUG // for debug output dt = offset.end(dt).addDays(((toFrom - 1) / snooze + 1) * snooze); #endif break; } } else { int toFrom = dt.secsTo(base); if (period.asSeconds() >= snooze || toFrom % snooze == 0 || (toFrom / snooze + 1) * snooze <= toFrom + period.asSeconds()) { found = true; #ifndef NDEBUG // for debug output dt = offset.end(dt).addSecs(((toFrom - 1) / snooze + 1) * snooze); #endif break; } } } if (!found) { continue; } } } qCDebug(KCALCORE_LOG) << incidence->summary() << "':" << dt.toString(); alarms.append(a); } } } void Calendar::startBatchAdding() { d->batchAddingInProgress = true; } void Calendar::endBatchAdding() { d->batchAddingInProgress = false; } bool Calendar::batchAdding() const { return d->batchAddingInProgress; } void Calendar::setDeletionTracking(bool enable) { d->mDeletionTracking = enable; } bool Calendar::deletionTracking() const { return d->mDeletionTracking; } void Calendar::virtual_hook(int id, void *data) { Q_UNUSED(id); Q_UNUSED(data); Q_ASSERT(false); } diff --git a/src/compat.cpp b/src/compat.cpp index 16b21b8db..258021393 100644 --- a/src/compat.cpp +++ b/src/compat.cpp @@ -1,411 +1,411 @@ /* This file is part of the kcalcore library. Copyright (c) 2002 Cornelius Schumacher Copyright (C) 2003-2004 Reinhold Kainhofer 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 classes for managing compatibility between different calendar formats. @brief Classes that provide compatibility to older or "broken" calendar formats. @author Cornelius Schumacher \ @author Reinhold Kainhofer \ */ #include "compat_p.h" #include "incidence.h" -#include "kcalcore_debug.h" +#include "kcalendarcore_debug.h" #include #include #include using namespace KCalendarCore; Compat *CompatFactory::createCompat(const QString &productId, const QString &implementationVersion) { Compat *compat = nullptr; int korg = productId.indexOf(QLatin1String("KOrganizer")); int outl9 = productId.indexOf(QLatin1String("Outlook 9.0")); if (korg >= 0) { int versionStart = productId.indexOf(QLatin1Char(' '), korg); if (versionStart >= 0) { int versionStop = productId.indexOf(QRegExp(QStringLiteral("[ /]")), versionStart + 1); if (versionStop >= 0) { QString version = productId.mid(versionStart + 1, versionStop - versionStart - 1); int versionNum = version.section(QLatin1Char('.'), 0, 0).toInt() * 10000 + version.section(QLatin1Char('.'), 1, 1).toInt() * 100 + version.section(QLatin1Char('.'), 2, 2).toInt(); int releaseStop = productId.indexOf(QLatin1Char('/'), versionStop); QString release; if (releaseStop > versionStop) { release = productId.mid(versionStop + 1, releaseStop - versionStop - 1); } if (versionNum < 30100) { compat = new CompatPre31; } else if (versionNum < 30200) { compat = new CompatPre32; } else if (versionNum == 30200 && release == QLatin1String("pre")) { qCDebug(KCALCORE_LOG) << "Generating compat for KOrganizer 3.2 pre"; compat = new Compat32PrereleaseVersions; } else if (versionNum < 30400) { compat = new CompatPre34; } else if (versionNum < 30500) { compat = new CompatPre35; } } } } else if (outl9 >= 0) { qCDebug(KCALCORE_LOG) << "Generating compat for Outlook < 2000 (Outlook 9.0)"; compat = new CompatOutlook9; } if (!compat) { compat = new Compat; } // Older implementations lacked the implementation version, // so apply this fix if it is a file from kontact and the version is missing. if (implementationVersion.isEmpty() && (productId.contains(QStringLiteral("libkcal")) || productId.contains(QStringLiteral("KOrganizer")) || productId.contains(QStringLiteral("KAlarm")))) { compat = new CompatPre410(compat); } return compat; } Compat::Compat() : d(nullptr) { } Compat::~Compat() { } void Compat::fixEmptySummary(const Incidence::Ptr &incidence) { // some stupid vCal exporters ignore the standard and use Description // instead of Summary for the default field. Correct for this: Copy the // first line of the description to the summary (if summary is just one // line, move it) if (incidence->summary().isEmpty() && !(incidence->description().isEmpty())) { QString oldDescription = incidence->description().trimmed(); QString newSummary(oldDescription); newSummary.remove(QRegExp(QStringLiteral("\n.*"))); incidence->setSummary(newSummary); if (oldDescription == newSummary) { incidence->setDescription(QLatin1String("")); } } } void Compat::fixAlarms(const Incidence::Ptr &incidence) { Q_UNUSED(incidence); } void Compat::fixFloatingEnd(QDate &date) { Q_UNUSED(date); } void Compat::fixRecurrence(const Incidence::Ptr &incidence) { Q_UNUSED(incidence); // Prevent use of compatibility mode during subsequent changes by the application // incidence->recurrence()->setCompatVersion(); } int Compat::fixPriority(int priority) { return priority; } bool Compat::useTimeZoneShift() const { return true; } void Compat::setCreatedToDtStamp(const Incidence::Ptr &incidence, const QDateTime &dtstamp) { Q_UNUSED(incidence); Q_UNUSED(dtstamp); } class Q_DECL_HIDDEN CompatDecorator::Private { public: Compat *compat; }; CompatDecorator::CompatDecorator(Compat *compat) : d(new CompatDecorator::Private) { d->compat = compat; } CompatDecorator::~CompatDecorator() { delete d->compat; delete d; } void CompatDecorator::fixEmptySummary(const Incidence::Ptr &incidence) { d->compat->fixEmptySummary(incidence); } void CompatDecorator::fixAlarms(const Incidence::Ptr &incidence) { d->compat->fixAlarms(incidence); } void CompatDecorator::fixFloatingEnd(QDate &date) { d->compat->fixFloatingEnd(date); } void CompatDecorator::fixRecurrence(const Incidence::Ptr &incidence) { d->compat->fixRecurrence(incidence); } int CompatDecorator::fixPriority(int priority) { return d->compat->fixPriority(priority); } bool CompatDecorator::useTimeZoneShift() const { return d->compat->useTimeZoneShift(); } void CompatDecorator::setCreatedToDtStamp(const Incidence::Ptr &incidence, const QDateTime &dtstamp) { d->compat->setCreatedToDtStamp(incidence, dtstamp); } CompatPre35::CompatPre35() : d(nullptr) { } CompatPre35::~CompatPre35() { } void CompatPre35::fixRecurrence(const Incidence::Ptr &incidence) { Recurrence *recurrence = incidence->recurrence(); if (recurrence) { QDateTime start(incidence->dtStart()); // kde < 3.5 only had one rrule, so no need to loop over all RRULEs. RecurrenceRule *r = recurrence->defaultRRule(); if (r && !r->dateMatchesRules(start)) { recurrence->addExDateTime(start); } } // Call base class method now that everything else is done Compat::fixRecurrence(incidence); } CompatPre34::CompatPre34() : d(nullptr) { } CompatPre34::~CompatPre34() { } int CompatPre34::fixPriority(int priority) { if (0 < priority && priority < 6) { // adjust 1->1, 2->3, 3->5, 4->7, 5->9 return 2 * priority - 1; } else { return priority; } } CompatPre32::CompatPre32() : d(nullptr) { } CompatPre32::~CompatPre32() { } void CompatPre32::fixRecurrence(const Incidence::Ptr &incidence) { Recurrence *recurrence = incidence->recurrence(); if (recurrence->recurs() && recurrence->duration() > 0) { recurrence->setDuration(recurrence->duration() + incidence->recurrence()->exDates().count()); } // Call base class method now that everything else is done CompatPre35::fixRecurrence(incidence); } CompatPre31::CompatPre31() : d(nullptr) { } CompatPre31::~CompatPre31() { } void CompatPre31::fixFloatingEnd(QDate &endDate) { endDate = endDate.addDays(1); } void CompatPre31::fixRecurrence(const Incidence::Ptr &incidence) { CompatPre32::fixRecurrence(incidence); Recurrence *recur = incidence->recurrence(); RecurrenceRule *r = nullptr; if (recur) { r = recur->defaultRRule(); } if (recur && r) { int duration = r->duration(); if (duration > 0) { // Backwards compatibility for KDE < 3.1. // rDuration was set to the number of time periods to recur, // with week start always on a Monday. // Convert this to the number of occurrences. r->setDuration(-1); QDate end(r->startDt().date()); bool doNothing = false; // # of periods: int tmp = (duration - 1) * r->frequency(); switch (r->recurrenceType()) { case RecurrenceRule::rWeekly: { end = end.addDays(tmp * 7 + 7 - end.dayOfWeek()); break; } case RecurrenceRule::rMonthly: { int month = end.month() - 1 + tmp; end.setDate(end.year() + month / 12, month % 12 + 1, 31); break; } case RecurrenceRule::rYearly: { end.setDate(end.year() + tmp, 12, 31); break; } default: doNothing = true; break; } if (!doNothing) { duration = r->durationTo( QDateTime(end, QTime(0, 0, 0), incidence->dtStart().timeZone())); r->setDuration(duration); } } /* addYearlyNum */ // Dates were stored as day numbers, with a fiddle to take account of // leap years. Convert the day number to a month. QList days = r->byYearDays(); if (!days.isEmpty()) { QList months = r->byMonths(); for (int i = 0; i < months.size(); ++i) { int newmonth = QDate(r->startDt().date().year(), 1, 1).addDays(months.at(i) - 1).month(); if (!months.contains(newmonth)) { months.append(newmonth); } } r->setByMonths(months); days.clear(); r->setByYearDays(days); } } } CompatOutlook9::CompatOutlook9() : d(nullptr) { } CompatOutlook9::~CompatOutlook9() { } void CompatOutlook9::fixAlarms(const Incidence::Ptr &incidence) { if (!incidence) { return; } Alarm::List alarms = incidence->alarms(); Alarm::List::Iterator end(alarms.end()); for (Alarm::List::Iterator it = alarms.begin(); it != end; ++it) { Alarm::Ptr al = *it; if (al && al->hasStartOffset()) { Duration offsetDuration = al->startOffset(); int offs = offsetDuration.asSeconds(); if (offs > 0) { offsetDuration = Duration(-offs); } al->setStartOffset(offsetDuration); } } } Compat32PrereleaseVersions::Compat32PrereleaseVersions() : d(nullptr) { } Compat32PrereleaseVersions::~Compat32PrereleaseVersions() { } bool Compat32PrereleaseVersions::useTimeZoneShift() const { return false; } CompatPre410::CompatPre410(Compat *decoratedCompat) : CompatDecorator(decoratedCompat) , d(nullptr) { } CompatPre410::~CompatPre410() { } void CompatPre410::setCreatedToDtStamp(const Incidence::Ptr &incidence, const QDateTime &dtstamp) { if (dtstamp.isValid()) { incidence->setCreated(dtstamp); } } diff --git a/src/customproperties.cpp b/src/customproperties.cpp index 9237488c9..8525078ac 100644 --- a/src/customproperties.cpp +++ b/src/customproperties.cpp @@ -1,269 +1,269 @@ /* This file is part of the kcalcore library. Copyright (c) 2002,2006,2010 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. */ /** @file This file is part of the API for handling calendar data and defines the CustomProperties class. @brief A class to manage custom calendar properties. @author David Jarvie \ */ #include "customproperties.h" #include -#include "kcalcore_debug.h" +#include "kcalendarcore_debug.h" using namespace KCalendarCore; //@cond PRIVATE static bool checkName(const QByteArray &name); class Q_DECL_HIDDEN CustomProperties::Private { public: bool operator==(const Private &other) const; QMap mProperties; // custom calendar properties QMap mPropertyParameters; // Volatile properties are not written back to the serialized format and are not compared in operator== // They are only used for runtime purposes and are not part of the payload. QMap mVolatileProperties; bool isVolatileProperty(const QString &name) const { return name.startsWith(QLatin1String("X-KDE-VOLATILE")); } }; bool CustomProperties::Private::operator==(const CustomProperties::Private &other) const { if (mProperties.count() != other.mProperties.count()) { // qCDebug(KCALCORE_LOG) << "Property count is different:" << mProperties << other.mProperties; return false; } for (QMap::ConstIterator it = mProperties.begin(); it != mProperties.end(); ++it) { QMap::ConstIterator itOther = other.mProperties.find(it.key()); if (itOther == other.mProperties.end() || itOther.value() != it.value()) { return false; } } for (QMap::ConstIterator it = mPropertyParameters.begin(); it != mPropertyParameters.end(); ++it) { QMap::ConstIterator itOther = other.mPropertyParameters.find(it.key()); if (itOther == other.mPropertyParameters.end() || itOther.value() != it.value()) { return false; } } return true; } //@endcond CustomProperties::CustomProperties() : d(new Private) { } CustomProperties::CustomProperties(const CustomProperties &cp) : d(new Private(*cp.d)) { } CustomProperties &CustomProperties::operator=(const CustomProperties &other) { // check for self assignment if (&other == this) { return *this; } *d = *other.d; return *this; } CustomProperties::~CustomProperties() { delete d; } bool CustomProperties::operator==(const CustomProperties &other) const { return *d == *other.d; } void CustomProperties::setCustomProperty(const QByteArray &app, const QByteArray &key, const QString &value) { if (value.isNull() || key.isEmpty() || app.isEmpty()) { return; } QByteArray property = "X-KDE-" + app + '-' + key; if (!checkName(property)) { return; } customPropertyUpdate(); if (d->isVolatileProperty(QLatin1String(property))) { d->mVolatileProperties[property] = value; } else { d->mProperties[property] = value; } customPropertyUpdated(); } void CustomProperties::removeCustomProperty(const QByteArray &app, const QByteArray &key) { removeNonKDECustomProperty(QByteArray("X-KDE-" + app + '-' + key)); } QString CustomProperties::customProperty(const QByteArray &app, const QByteArray &key) const { return nonKDECustomProperty(QByteArray("X-KDE-" + app + '-' + key)); } QByteArray CustomProperties::customPropertyName(const QByteArray &app, const QByteArray &key) { QByteArray property("X-KDE-" + app + '-' + key); if (!checkName(property)) { return QByteArray(); } return property; } void CustomProperties::setNonKDECustomProperty(const QByteArray &name, const QString &value, const QString ¶meters) { if (value.isNull() || !checkName(name)) { return; } customPropertyUpdate(); d->mProperties[name] = value; d->mPropertyParameters[name] = parameters; customPropertyUpdated(); } void CustomProperties::removeNonKDECustomProperty(const QByteArray &name) { if (d->mProperties.contains(name)) { customPropertyUpdate(); d->mProperties.remove(name); d->mPropertyParameters.remove(name); customPropertyUpdated(); } else if (d->mVolatileProperties.contains(name)) { customPropertyUpdate(); d->mVolatileProperties.remove(name); customPropertyUpdated(); } } QString CustomProperties::nonKDECustomProperty(const QByteArray &name) const { return d->isVolatileProperty(QLatin1String(name)) ? d->mVolatileProperties.value(name) : d->mProperties.value(name); } QString CustomProperties::nonKDECustomPropertyParameters(const QByteArray &name) const { return d->mPropertyParameters.value(name); } void CustomProperties::setCustomProperties(const QMap &properties) { bool changed = false; for (QMap::ConstIterator it = properties.begin(); it != properties.end(); ++it) { // Validate the property name and convert any null string to empty string if (checkName(it.key())) { if (d->isVolatileProperty(QLatin1String(it.key()))) { d->mVolatileProperties[it.key()] = it.value().isNull() ? QLatin1String("") : it.value(); } else { d->mProperties[it.key()] = it.value().isNull() ? QLatin1String("") : it.value(); } if (!changed) { customPropertyUpdate(); } changed = true; } } if (changed) { customPropertyUpdated(); } } QMap CustomProperties::customProperties() const { QMap result; result.unite(d->mProperties); result.unite(d->mVolatileProperties); return result; } void CustomProperties::customPropertyUpdate() { } void CustomProperties::customPropertyUpdated() { } //@cond PRIVATE bool checkName(const QByteArray &name) { // Check that the property name starts with 'X-' and contains // only the permitted characters const char *n = name.constData(); int len = name.length(); if (len < 2 || n[0] != 'X' || n[1] != '-') { return false; } for (int i = 2; i < len; ++i) { char ch = n[i]; if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '-') { continue; } return false; // invalid character found } return true; } //@endcond QDataStream &KCalendarCore::operator<<(QDataStream &stream, const KCalendarCore::CustomProperties &properties) { return stream << properties.d->mProperties << properties.d->mPropertyParameters; } QDataStream &KCalendarCore::operator>>(QDataStream &stream, KCalendarCore::CustomProperties &properties) { properties.d->mVolatileProperties.clear(); return stream >> properties.d->mProperties >> properties.d->mPropertyParameters; } diff --git a/src/event.cpp b/src/event.cpp index 0aacac4d5..6e8d68ce9 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -1,355 +1,355 @@ /* This file is part of the kcalcore library. Copyright (c) 2001 Cornelius Schumacher 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 Event class. @brief This class provides an Event in the sense of RFC2445. @author Cornelius Schumacher \ */ #include "event.h" #include "visitor.h" #include "utils_p.h" -#include "kcalcore_debug.h" +#include "kcalendarcore_debug.h" #include using namespace KCalendarCore; /** Private class that helps to provide binary compatibility between releases. @internal */ //@cond PRIVATE class Q_DECL_HIDDEN KCalendarCore::Event::Private { public: Private() : mTransparency(Opaque), mMultiDayValid(false), mMultiDay(false) {} Private(const KCalendarCore::Event::Private &other) : mDtEnd(other.mDtEnd), mTransparency(other.mTransparency), mMultiDayValid(false), mMultiDay(false) {} QDateTime mDtEnd; Transparency mTransparency; bool mMultiDayValid = false; bool mMultiDay = false; }; //@endcond Event::Event() : d(new KCalendarCore::Event::Private) { } Event::Event(const Event &other) : Incidence(other), d(new KCalendarCore::Event::Private(*other.d)) { } Event::Event(const Incidence &other) : Incidence(other) , d(new KCalendarCore::Event::Private) { } Event::~Event() { delete d; } Event *Event::clone() const { return new Event(*this); } IncidenceBase &Event::assign(const IncidenceBase &other) { if (&other != this) { Incidence::assign(other); const Event *e = static_cast(&other); *d = *(e->d); } return *this; } bool Event::equals(const IncidenceBase &event) const { if (!Incidence::equals(event)) { return false; } else { // If they weren't the same type IncidenceBase::equals would had returned false already const Event *e = static_cast(&event); return ((dtEnd() == e->dtEnd()) || (!dtEnd().isValid() && !e->dtEnd().isValid())) && transparency() == e->transparency(); } } Incidence::IncidenceType Event::type() const { return TypeEvent; } QByteArray Event::typeStr() const { return QByteArrayLiteral("Event"); } void Event::setDtStart(const QDateTime &dt) { d->mMultiDayValid = false; Incidence::setDtStart(dt); } void Event::setDtEnd(const QDateTime &dtEnd) { if (mReadOnly) { return; } if (d->mDtEnd != dtEnd || hasDuration() == dtEnd.isValid()) { update(); d->mDtEnd = dtEnd; d->mMultiDayValid = false; setHasDuration(!dtEnd.isValid()); setFieldDirty(FieldDtEnd); updated(); } } QDateTime Event::dtEnd() const { if (d->mDtEnd.isValid()) { return d->mDtEnd; } if (hasDuration()) { if (allDay()) { // For all day events, dtEnd is always inclusive QDateTime end = duration().end(dtStart().addDays(-1)); return end >= dtStart() ? end : dtStart(); } else { return duration().end(dtStart()); } } // It is valid for a VEVENT to be without a DTEND. See RFC2445, Sect4.6.1. // Be careful to use Event::dateEnd() as appropriate due to this possibility. return dtStart(); } QDate Event::dateEnd() const { QDateTime end = dtEnd().toTimeZone(dtStart().timeZone()); if (allDay()) { return end.date(); } else { return end.addSecs(-1).date(); } } bool Event::hasEndDate() const { return d->mDtEnd.isValid(); } bool Event::isMultiDay(const QTimeZone &zone) const { // First off, if spec's not valid, we can check for cache if (!zone.isValid() && d->mMultiDayValid) { return d->mMultiDay; } // Not in cache -> do it the hard way QDateTime start, end; if (!zone.isValid()) { start = dtStart(); end = dtEnd(); } else { start = dtStart().toTimeZone(zone); end = dtEnd().toTimeZone(zone); } bool multi = (start < end && start.date() != end.date()); // End date is non inclusive // If we have an incidence that duration is one day and ends with a start of a new day // than it is not a multiday event if (multi && end.time() == QTime(0, 0, 0)) { multi = start.daysTo(end) > 1; } // Update the cache // Also update Cache if spec is invalid d->mMultiDayValid = true; d->mMultiDay = multi; return multi; } void Event::shiftTimes(const QTimeZone &oldZone, const QTimeZone &newZone) { Incidence::shiftTimes(oldZone, newZone); if (d->mDtEnd.isValid()) { d->mDtEnd = d->mDtEnd.toTimeZone(oldZone); d->mDtEnd.setTimeZone(newZone); } } void Event::setTransparency(Event::Transparency transparency) { if (mReadOnly) { return; } update(); d->mTransparency = transparency; setFieldDirty(FieldTransparency); updated(); } Event::Transparency Event::transparency() const { return d->mTransparency; } void Event::setDuration(const Duration &duration) { setDtEnd(QDateTime()); Incidence::setDuration(duration); } void Event::setAllDay(bool allday) { if (allday != allDay() && !mReadOnly) { setFieldDirty(FieldDtEnd); Incidence::setAllDay(allday); } } bool Event::accept(Visitor &v, const IncidenceBase::Ptr &incidence) { return v.visit(incidence.staticCast()); } QDateTime Event::dateTime(DateTimeRole role) const { switch (role) { case RoleRecurrenceStart: case RoleAlarmStartOffset: case RoleStartTimeZone: case RoleSort: return dtStart(); case RoleCalendarHashing: return !recurs() && !isMultiDay() ? dtStart() : QDateTime(); case RoleAlarmEndOffset: case RoleEndTimeZone: case RoleEndRecurrenceBase: case RoleEnd: case RoleDisplayEnd: return dtEnd(); case RoleDisplayStart: return dtStart(); case RoleAlarm: if (alarms().isEmpty()) { return QDateTime(); } else { Alarm::Ptr alarm = alarms().at(0); return alarm->hasStartOffset() ? dtStart() : dtEnd(); } default: return QDateTime(); } } void Event::setDateTime(const QDateTime &dateTime, DateTimeRole role) { switch (role) { case RoleDnD: { const qint64 duration = dtStart().secsTo(dtEnd()); setDtStart(dateTime); setDtEnd(dateTime.addSecs(duration <= 0 ? 3600 : duration)); break; } case RoleEnd: setDtEnd(dateTime); break; default: qCDebug(KCALCORE_LOG) << "Unhandled role" << role; } } void Event::virtual_hook(VirtualHook id, void *data) { Q_UNUSED(id); Q_UNUSED(data); } QLatin1String KCalendarCore::Event::mimeType() const { return Event::eventMimeType(); } QLatin1String Event::eventMimeType() { return QLatin1String("application/x-vnd.akonadi.calendar.event"); } QLatin1String Event::iconName(const QDateTime &) const { return QLatin1String("view-calendar-day"); } void Event::serialize(QDataStream &out) const { Incidence::serialize(out); serializeQDateTimeAsKDateTime(out, d->mDtEnd); out << hasEndDate() << static_cast(d->mTransparency) << d->mMultiDayValid << d->mMultiDay; } void Event::deserialize(QDataStream &in) { Incidence::deserialize(in); bool hasEndDateDummy = true; deserializeKDateTimeAsQDateTime(in, d->mDtEnd); in >> hasEndDateDummy; quint32 transp; in >> transp; d->mTransparency = static_cast(transp); in >> d->mMultiDayValid >> d->mMultiDay; } bool Event::supportsGroupwareCommunication() const { return true; } diff --git a/src/filestorage.cpp b/src/filestorage.cpp index b4c9f8751..0e03a2bc0 100644 --- a/src/filestorage.cpp +++ b/src/filestorage.cpp @@ -1,183 +1,183 @@ /* This file is part of the kcalcore library. Copyright (c) 2002 Cornelius Schumacher 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 FileStorage class. @brief This class provides a calendar storage as a local file. @author Cornelius Schumacher \ */ #include "filestorage.h" #include "exceptions.h" #include "icalformat.h" #include "memorycalendar.h" #include "vcalformat.h" -#include "kcalcore_debug.h" +#include "kcalendarcore_debug.h" using namespace KCalendarCore; /* Private class that helps to provide binary compatibility between releases. */ //@cond PRIVATE class Q_DECL_HIDDEN KCalendarCore::FileStorage::Private { public: Private(const QString &fileName, CalFormat *format) : mFileName(fileName), mSaveFormat(format) {} ~Private() { delete mSaveFormat; } QString mFileName; CalFormat *mSaveFormat = nullptr; }; //@endcond FileStorage::FileStorage(const Calendar::Ptr &cal, const QString &fileName, CalFormat *format) : CalStorage(cal), d(new Private(fileName, format)) { } FileStorage::~FileStorage() { delete d; } void FileStorage::setFileName(const QString &fileName) { d->mFileName = fileName; } QString FileStorage::fileName() const { return d->mFileName; } void FileStorage::setSaveFormat(CalFormat *format) { delete d->mSaveFormat; d->mSaveFormat = format; } CalFormat *FileStorage::saveFormat() const { return d->mSaveFormat; } bool FileStorage::open() { return true; } bool FileStorage::load() { if (d->mFileName.isEmpty()) { qCWarning(KCALCORE_LOG) << "Empty filename while trying to load"; return false; } // Always try to load with iCalendar. It will detect, if it is actually a // vCalendar file. bool success; QString productId; // First try the supplied format. Otherwise fall through to iCalendar, then // to vCalendar success = saveFormat() && saveFormat()->load(calendar(), d->mFileName); if (success) { productId = saveFormat()->loadedProductId(); } else { ICalFormat iCal; success = iCal.load(calendar(), d->mFileName); if (success) { productId = iCal.loadedProductId(); } else { if (iCal.exception()) { if (iCal.exception()->code() == Exception::CalVersion1) { // Expected non vCalendar file, but detected vCalendar qCDebug(KCALCORE_LOG) << "Fallback to VCalFormat"; VCalFormat vCal; success = vCal.load(calendar(), d->mFileName); productId = vCal.loadedProductId(); if (!success) { if (vCal.exception()) { qCWarning(KCALCORE_LOG) << "Exception while importing:" << vCal.exception()->code(); } return false; } } else { return false; } } else { qCWarning(KCALCORE_LOG) << "There should be an exception set."; return false; } } } calendar()->setProductId(productId); calendar()->setModified(false); return true; } bool FileStorage::save() { if (d->mFileName.isEmpty()) { return false; } CalFormat *format = d->mSaveFormat ? d->mSaveFormat : new ICalFormat; bool success = format->save(calendar(), d->mFileName); if (success) { calendar()->setModified(false); } else { if (!format->exception()) { qCDebug(KCALCORE_LOG) << "Error. There should be an expection set."; } else { qCDebug(KCALCORE_LOG) << int(format->exception()->code()); } } if (!d->mSaveFormat) { delete format; } return success; } bool FileStorage::close() { return true; } diff --git a/src/freebusy.cpp b/src/freebusy.cpp index 9f5f68b5b..d0d3b0fc9 100644 --- a/src/freebusy.cpp +++ b/src/freebusy.cpp @@ -1,433 +1,433 @@ /* This file is part of the kcalcore library. Copyright (c) 2001 Cornelius Schumacher Copyright (C) 2004 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. */ /** @file This file is part of the API for handling calendar data and defines the FreeBusy class. @brief Provides information about the free/busy time of a calendar user. @author Cornelius Schumacher \ @author Reinhold Kainhofer \ */ #include "freebusy.h" #include "visitor.h" #include "utils_p.h" #include "icalformat.h" -#include "kcalcore_debug.h" +#include "kcalendarcore_debug.h" #include using namespace KCalendarCore; //@cond PRIVATE class Q_DECL_HIDDEN KCalendarCore::FreeBusy::Private { private: FreeBusy *q; public: Private(FreeBusy *qq) : q(qq) {} Private(const KCalendarCore::FreeBusy::Private &other, FreeBusy *qq) : q(qq) { init(other); } Private(const FreeBusyPeriod::List &busyPeriods, FreeBusy *qq) : q(qq), mBusyPeriods(busyPeriods) {} void init(const KCalendarCore::FreeBusy::Private &other); void init(const Event::List &events, const QDateTime &start, const QDateTime &end); QDateTime mDtEnd; // end datetime FreeBusyPeriod::List mBusyPeriods; // list of periods // This is used for creating a freebusy object for the current user bool addLocalPeriod(FreeBusy *fb, const QDateTime &start, const QDateTime &end); }; void KCalendarCore::FreeBusy::Private::init(const KCalendarCore::FreeBusy::Private &other) { mDtEnd = other.mDtEnd; mBusyPeriods = other.mBusyPeriods; } //@endcond FreeBusy::FreeBusy() : d(new KCalendarCore::FreeBusy::Private(this)) { } FreeBusy::FreeBusy(const FreeBusy &other) : IncidenceBase(other), d(new KCalendarCore::FreeBusy::Private(*other.d, this)) { } FreeBusy::FreeBusy(const QDateTime &start, const QDateTime &end) : d(new KCalendarCore::FreeBusy::Private(this)) { setDtStart(start); //NOLINT false clang-analyzer-optin.cplusplus.VirtualCall setDtEnd(end); //NOLINT false clang-analyzer-optin.cplusplus.VirtualCall } FreeBusy::FreeBusy(const Event::List &events, const QDateTime &start, const QDateTime &end) : d(new KCalendarCore::FreeBusy::Private(this)) { setDtStart(start); //NOLINT false clang-analyzer-optin.cplusplus.VirtualCall setDtEnd(end); //NOLINT false clang-analyzer-optin.cplusplus.VirtualCall d->init(events, start, end); } //@cond PRIVATE void FreeBusy::Private::init(const Event::List &eventList, const QDateTime &start, const QDateTime &end) { qint64 extraDays; int i; int x; qint64 duration = start.daysTo(end); QDate day; QDateTime tmpStart; QDateTime tmpEnd; // Loops through every event in the calendar Event::List::ConstIterator it; for (it = eventList.constBegin(); it != eventList.constEnd(); ++it) { Event::Ptr event = *it; // If this event is transparent it shouldn't be in the freebusy list. if (event->transparency() == Event::Transparent) { continue; } // The code below can not handle all-day events. Fixing this resulted // in a lot of duplicated code. Instead, make a copy of the event and // set the period to the full day(s). This trick works for recurring, // multiday, and single day all-day events. Event::Ptr allDayEvent; if (event->allDay()) { // addDay event. Do the hack qCDebug(KCALCORE_LOG) << "All-day event"; allDayEvent = Event::Ptr(new Event(*event)); // Set the start and end times to be on midnight QDateTime st = allDayEvent->dtStart(); st.setTime(QTime(0, 0)); QDateTime nd = allDayEvent->dtEnd(); nd.setTime(QTime(23, 59, 59, 999)); allDayEvent->setAllDay(false); allDayEvent->setDtStart(st); allDayEvent->setDtEnd(nd); qCDebug(KCALCORE_LOG) << "Use:" << st.toString() << "to" << nd.toString(); // Finally, use this event for the setting below event = allDayEvent; } // This whole for loop is for recurring events, it loops through // each of the days of the freebusy request for (i = 0; i <= duration; ++i) { day = start.addDays(i).date(); tmpStart.setDate(day); tmpEnd.setDate(day); if (event->recurs()) { if (event->isMultiDay()) { // FIXME: This doesn't work for sub-daily recurrences or recurrences with // a different time than the original event. extraDays = event->dtStart().daysTo(event->dtEnd()); for (x = 0; x <= extraDays; ++x) { if (event->recursOn(day.addDays(-x), start.timeZone())) { tmpStart.setDate(day.addDays(-x)); tmpStart.setTime(event->dtStart().time()); tmpEnd = event->duration().end(tmpStart); addLocalPeriod(q, tmpStart, tmpEnd); break; } } } else { if (event->recursOn(day, start.timeZone())) { tmpStart.setTime(event->dtStart().time()); tmpEnd.setTime(event->dtEnd().time()); addLocalPeriod(q, tmpStart, tmpEnd); } } } } // Non-recurring events addLocalPeriod(q, event->dtStart(), event->dtEnd()); } q->sortList(); } //@endcond FreeBusy::FreeBusy(const Period::List &busyPeriods) : d(new KCalendarCore::FreeBusy::Private(this)) { addPeriods(busyPeriods); } FreeBusy::FreeBusy(const FreeBusyPeriod::List &busyPeriods) : d(new KCalendarCore::FreeBusy::Private(busyPeriods, this)) { } FreeBusy::~FreeBusy() { delete d; } IncidenceBase::IncidenceType FreeBusy::type() const { return TypeFreeBusy; } QByteArray FreeBusy::typeStr() const { return QByteArrayLiteral("FreeBusy"); } void FreeBusy::setDtStart(const QDateTime &start) { IncidenceBase::setDtStart(start.toUTC()); updated(); } void FreeBusy::setDtEnd(const QDateTime &end) { d->mDtEnd = end; } QDateTime FreeBusy::dtEnd() const { return d->mDtEnd; } Period::List FreeBusy::busyPeriods() const { Period::List res; res.reserve(d->mBusyPeriods.count()); for (const FreeBusyPeriod &p : qAsConst(d->mBusyPeriods)) { res << p; } return res; } FreeBusyPeriod::List FreeBusy::fullBusyPeriods() const { return d->mBusyPeriods; } void FreeBusy::sortList() { std::sort(d->mBusyPeriods.begin(), d->mBusyPeriods.end()); } void FreeBusy::addPeriods(const Period::List &list) { d->mBusyPeriods.reserve(d->mBusyPeriods.count() + list.count()); for (const Period &p : qAsConst(list)) { d->mBusyPeriods << FreeBusyPeriod(p); } sortList(); } void FreeBusy::addPeriods(const FreeBusyPeriod::List &list) { d->mBusyPeriods += list; sortList(); } void FreeBusy::addPeriod(const QDateTime &start, const QDateTime &end) { d->mBusyPeriods.append(FreeBusyPeriod(start, end)); sortList(); } void FreeBusy::addPeriod(const QDateTime &start, const Duration &duration) { d->mBusyPeriods.append(FreeBusyPeriod(start, duration)); sortList(); } void FreeBusy::merge(const FreeBusy::Ptr &freeBusy) { if (freeBusy->dtStart() < dtStart()) { setDtStart(freeBusy->dtStart()); } if (freeBusy->dtEnd() > dtEnd()) { setDtEnd(freeBusy->dtEnd()); } Period::List periods = freeBusy->busyPeriods(); Period::List::ConstIterator it; d->mBusyPeriods.reserve(d->mBusyPeriods.count() + periods.count()); for (it = periods.constBegin(); it != periods.constEnd(); ++it) { d->mBusyPeriods.append(FreeBusyPeriod((*it).start(), (*it).end())); } sortList(); } void FreeBusy::shiftTimes(const QTimeZone &oldZone, const QTimeZone &newZone) { if (oldZone.isValid() && newZone.isValid() && oldZone != newZone) { IncidenceBase::shiftTimes(oldZone, newZone); d->mDtEnd = d->mDtEnd.toTimeZone(oldZone); d->mDtEnd.setTimeZone(newZone); for (FreeBusyPeriod p : qAsConst(d->mBusyPeriods)) { p.shiftTimes(oldZone, newZone); } } } IncidenceBase &FreeBusy::assign(const IncidenceBase &other) { if (&other != this) { IncidenceBase::assign(other); const FreeBusy *f = static_cast(&other); d->init(*(f->d)); } return *this; } bool FreeBusy::equals(const IncidenceBase &freeBusy) const { if (!IncidenceBase::equals(freeBusy)) { return false; } else { // If they weren't the same type IncidenceBase::equals would had returned false already const FreeBusy *fb = static_cast(&freeBusy); return dtEnd() == fb->dtEnd() && d->mBusyPeriods == fb->d->mBusyPeriods; } } bool FreeBusy::accept(Visitor &v, const IncidenceBase::Ptr &incidence) { return v.visit(incidence.staticCast()); } QDateTime FreeBusy::dateTime(DateTimeRole role) const { Q_UNUSED(role); // No roles affecting freeBusy yet return QDateTime(); } void FreeBusy::setDateTime(const QDateTime &dateTime, DateTimeRole role) { Q_UNUSED(dateTime); Q_UNUSED(role); } void FreeBusy::virtual_hook(VirtualHook id, void *data) { Q_UNUSED(id); Q_UNUSED(data); Q_ASSERT(false); } //@cond PRIVATE bool FreeBusy::Private::addLocalPeriod(FreeBusy *fb, const QDateTime &eventStart, const QDateTime &eventEnd) { QDateTime tmpStart; QDateTime tmpEnd; //Check to see if the start *or* end of the event is //between the start and end of the freebusy dates. QDateTime start = fb->dtStart(); if (!(((start.secsTo(eventStart) >= 0) && (eventStart.secsTo(mDtEnd) >= 0)) || ((start.secsTo(eventEnd) >= 0) && (eventEnd.secsTo(mDtEnd) >= 0)))) { return false; } if (eventStart.secsTo(start) >= 0) { tmpStart = start; } else { tmpStart = eventStart; } if (eventEnd.secsTo(mDtEnd) <= 0) { tmpEnd = mDtEnd; } else { tmpEnd = eventEnd; } FreeBusyPeriod p(tmpStart, tmpEnd); mBusyPeriods.append(p); return true; } //@endcond QLatin1String FreeBusy::mimeType() const { return FreeBusy::freeBusyMimeType(); } QLatin1String KCalendarCore::FreeBusy::freeBusyMimeType() { return QLatin1String("application/x-vnd.akonadi.calendar.freebusy"); } QDataStream &KCalendarCore::operator<<(QDataStream &stream, const KCalendarCore::FreeBusy::Ptr &freebusy) { KCalendarCore::ICalFormat format; QString data = format.createScheduleMessage(freebusy, iTIPPublish); return stream << data; } QDataStream &KCalendarCore::operator>>(QDataStream &stream, KCalendarCore::FreeBusy::Ptr &freebusy) { QString freeBusyVCal; stream >> freeBusyVCal; KCalendarCore::ICalFormat format; freebusy = format.parseFreeBusy(freeBusyVCal); if (!freebusy) { qCDebug(KCALCORE_LOG) << "Error parsing free/busy"; qCDebug(KCALCORE_LOG) << freeBusyVCal; } return stream; } diff --git a/src/icalformat.cpp b/src/icalformat.cpp index 83e34c059..05706a5a2 100644 --- a/src/icalformat.cpp +++ b/src/icalformat.cpp @@ -1,672 +1,672 @@ /* This file is part of the kcalcore library. Copyright (c) 2001 Cornelius Schumacher 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 ICalFormat class. @brief iCalendar format implementation: a layer of abstraction for libical. @author Cornelius Schumacher \ */ #include "icalformat.h" #include "icalformat_p.h" #include "icaltimezones_p.h" #include "freebusy.h" #include "memorycalendar.h" -#include "kcalcore_debug.h" +#include "kcalendarcore_debug.h" #include "calendar_p.h" #include #include #include extern "C" { #include #include #include #include #include } using namespace KCalendarCore; //@cond PRIVATE class Q_DECL_HIDDEN KCalendarCore::ICalFormat::Private { public: Private(ICalFormat *parent) : mImpl(new ICalFormatImpl(parent)), mTimeZone(QTimeZone::utc()) {} ~Private() { delete mImpl; } ICalFormatImpl *mImpl = nullptr; QTimeZone mTimeZone; }; //@endcond ICalFormat::ICalFormat() : d(new Private(this)) { } ICalFormat::~ICalFormat() { icalmemory_free_ring(); delete d; } bool ICalFormat::load(const Calendar::Ptr &calendar, const QString &fileName) { qCDebug(KCALCORE_LOG) << fileName; clearException(); QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { qCritical() << "load error"; setException(new Exception(Exception::LoadError)); return false; } const QByteArray text = file.readAll().trimmed(); file.close(); if (text.isEmpty()) { // empty files are valid return true; } else { return fromRawString(calendar, text, false, fileName); } } bool ICalFormat::save(const Calendar::Ptr &calendar, const QString &fileName) { qCDebug(KCALCORE_LOG) << fileName; clearException(); QString text = toString(calendar); if (text.isEmpty()) { return false; } // Write backup file const QString backupFile = fileName + QLatin1Char('~'); QFile::remove(backupFile); QFile::copy(fileName, backupFile); QSaveFile file(fileName); if (!file.open(QIODevice::WriteOnly)) { qCritical() << "file open error: " << file.errorString() << ";filename=" << fileName; setException(new Exception(Exception::SaveErrorOpenFile, QStringList(fileName))); return false; } // Convert to UTF8 and save QByteArray textUtf8 = text.toUtf8(); file.write(textUtf8.data(), textUtf8.size()); if (!file.commit()) { qCDebug(KCALCORE_LOG) << "file finalize error:" << file.errorString(); setException(new Exception(Exception::SaveErrorSaveFile, QStringList(fileName))); return false; } return true; } bool ICalFormat::fromString(const Calendar::Ptr &cal, const QString &string, bool deleted, const QString ¬ebook) { return fromRawString(cal, string.toUtf8(), deleted, notebook); } Incidence::Ptr ICalFormat::readIncidence(const QByteArray &string) { // Let's defend const correctness until the very gates of hell^Wlibical icalcomponent *calendar = icalcomponent_new_from_string(const_cast(string.constData())); if (!calendar) { qCritical() << "parse error from icalcomponent_new_from_string. string=" << QString::fromLatin1(string); setException(new Exception(Exception::ParseErrorIcal)); return Incidence::Ptr(); } ICalTimeZoneCache tzCache; ICalTimeZoneParser parser(&tzCache); parser.parse(calendar); Incidence::Ptr incidence; if (icalcomponent_isa(calendar) == ICAL_VCALENDAR_COMPONENT) { incidence = d->mImpl->readOneIncidence(calendar, &tzCache); } else if (icalcomponent_isa(calendar) == ICAL_XROOT_COMPONENT) { icalcomponent *comp = icalcomponent_get_first_component(calendar, ICAL_VCALENDAR_COMPONENT); if (comp) { incidence = d->mImpl->readOneIncidence(comp, &tzCache); } } if (!incidence) { qCDebug(KCALCORE_LOG) << "No VCALENDAR component found"; setException(new Exception(Exception::NoCalendar)); } icalcomponent_free(calendar); icalmemory_free_ring(); return incidence; } bool ICalFormat::fromRawString(const Calendar::Ptr &cal, const QByteArray &string, bool deleted, const QString ¬ebook) { Q_UNUSED(notebook); // Get first VCALENDAR component. // TODO: Handle more than one VCALENDAR or non-VCALENDAR top components icalcomponent *calendar; // Let's defend const correctness until the very gates of hell^Wlibical calendar = icalcomponent_new_from_string(const_cast(string.constData())); if (!calendar) { qCritical() << "parse error from icalcomponent_new_from_string. string=" << QString::fromLatin1(string); setException(new Exception(Exception::ParseErrorIcal)); return false; } bool success = true; if (icalcomponent_isa(calendar) == ICAL_XROOT_COMPONENT) { icalcomponent *comp; for (comp = icalcomponent_get_first_component(calendar, ICAL_VCALENDAR_COMPONENT); comp; comp = icalcomponent_get_next_component(calendar, ICAL_VCALENDAR_COMPONENT)) { // put all objects into their proper places if (!d->mImpl->populate(cal, comp, deleted)) { qCritical() << "Could not populate calendar"; if (!exception()) { setException(new Exception(Exception::ParseErrorKcal)); } success = false; } else { setLoadedProductId(d->mImpl->loadedProductId()); } } } else if (icalcomponent_isa(calendar) != ICAL_VCALENDAR_COMPONENT) { qCDebug(KCALCORE_LOG) << "No VCALENDAR component found"; setException(new Exception(Exception::NoCalendar)); success = false; } else { // put all objects into their proper places if (!d->mImpl->populate(cal, calendar, deleted)) { qCDebug(KCALCORE_LOG) << "Could not populate calendar"; if (!exception()) { setException(new Exception(Exception::ParseErrorKcal)); } success = false; } else { setLoadedProductId(d->mImpl->loadedProductId()); } } icalcomponent_free(calendar); icalmemory_free_ring(); return success; } Incidence::Ptr ICalFormat::fromString(const QString &string) { MemoryCalendar::Ptr cal(new MemoryCalendar(d->mTimeZone)); fromString(cal, string); const Incidence::List list = cal->incidences(); return !list.isEmpty() ? list.first() : Incidence::Ptr(); } QString ICalFormat::toString(const Calendar::Ptr &cal, const QString ¬ebook, bool deleted) { icalcomponent *calendar = d->mImpl->createCalendarComponent(cal); icalcomponent *component; QVector tzUsedList; TimeZoneEarliestDate earliestTz; // todos Todo::List todoList = deleted ? cal->deletedTodos() : cal->rawTodos(); for (auto it = todoList.cbegin(), end = todoList.cend(); it != end; ++it) { if (!deleted || !cal->todo((*it)->uid(), (*it)->recurrenceId())) { // use existing ones, or really deleted ones if (notebook.isEmpty() || (!cal->notebook(*it).isEmpty() && notebook.endsWith(cal->notebook(*it)))) { component = d->mImpl->writeTodo(*it, &tzUsedList); icalcomponent_add_component(calendar, component); ICalTimeZoneParser::updateTzEarliestDate((*it), &earliestTz); } } } // events Event::List events = deleted ? cal->deletedEvents() : cal->rawEvents(); for (auto it = events.cbegin(), end = events.cend(); it != end; ++it) { if (!deleted || !cal->event((*it)->uid(), (*it)->recurrenceId())) { // use existing ones, or really deleted ones if (notebook.isEmpty() || (!cal->notebook(*it).isEmpty() && notebook.endsWith(cal->notebook(*it)))) { component = d->mImpl->writeEvent(*it, &tzUsedList); icalcomponent_add_component(calendar, component); ICalTimeZoneParser::updateTzEarliestDate((*it), &earliestTz); } } } // journals Journal::List journals = deleted ? cal->deletedJournals() : cal->rawJournals(); for (auto it = journals.cbegin(), end = journals.cend(); it != end; ++it) { if (!deleted || !cal->journal((*it)->uid(), (*it)->recurrenceId())) { // use existing ones, or really deleted ones if (notebook.isEmpty() || (!cal->notebook(*it).isEmpty() && notebook.endsWith(cal->notebook(*it)))) { component = d->mImpl->writeJournal(*it, &tzUsedList); icalcomponent_add_component(calendar, component); ICalTimeZoneParser::updateTzEarliestDate((*it), &earliestTz); } } } // time zones if (todoList.isEmpty() && events.isEmpty() && journals.isEmpty()) { // no incidences means no used timezones, use all timezones // this will export a calendar having only timezone definitions tzUsedList = cal->d->mTimeZones; } for (const auto &qtz : qAsConst(tzUsedList)) { if (qtz != QTimeZone::utc()) { icaltimezone *tz = ICalTimeZoneParser::icaltimezoneFromQTimeZone(qtz, earliestTz[qtz]); if (!tz) { qCritical() << "bad time zone"; } else { component = icalcomponent_new_clone(icaltimezone_get_component(tz)); icalcomponent_add_component(calendar, component); icaltimezone_free(tz, 1); } } } char *const componentString = icalcomponent_as_ical_string_r(calendar); const QString &text = QString::fromUtf8(componentString); free(componentString); icalcomponent_free(calendar); icalmemory_free_ring(); if (text.isEmpty()) { setException(new Exception(Exception::LibICalError)); } return text; } QString ICalFormat::toICalString(const Incidence::Ptr &incidence) { MemoryCalendar::Ptr cal(new MemoryCalendar(d->mTimeZone)); cal->addIncidence(Incidence::Ptr(incidence->clone())); return toString(cal.staticCast()); } QString ICalFormat::toString(const Incidence::Ptr &incidence) { return QString::fromUtf8(toRawString(incidence)); } QByteArray ICalFormat::toRawString(const Incidence::Ptr &incidence) { TimeZoneList tzUsedList; icalcomponent *component = d->mImpl->writeIncidence(incidence, iTIPRequest, &tzUsedList); QByteArray text = icalcomponent_as_ical_string(component); TimeZoneEarliestDate earliestTzDt; ICalTimeZoneParser::updateTzEarliestDate(incidence, &earliestTzDt); // time zones for (const auto &qtz : qAsConst(tzUsedList)) { if (qtz != QTimeZone::utc()) { icaltimezone *tz = ICalTimeZoneParser::icaltimezoneFromQTimeZone(qtz, earliestTzDt[qtz]); if (!tz) { qCritical() << "bad time zone"; } else { icalcomponent *tzcomponent = icaltimezone_get_component(tz); icalcomponent_add_component(component, component); text.append(icalcomponent_as_ical_string(tzcomponent)); icaltimezone_free(tz, 1); } } } icalcomponent_free(component); return text; } QString ICalFormat::toString(RecurrenceRule *recurrence) { icalproperty *property = icalproperty_new_rrule(d->mImpl->writeRecurrenceRule(recurrence)); QString text = QString::fromUtf8(icalproperty_as_ical_string(property)); icalproperty_free(property); return text; } bool ICalFormat::fromString(RecurrenceRule *recurrence, const QString &rrule) { if (!recurrence) { return false; } bool success = true; icalerror_clear_errno(); struct icalrecurrencetype recur = icalrecurrencetype_from_string(rrule.toLatin1().constData()); if (icalerrno != ICAL_NO_ERROR) { qCDebug(KCALCORE_LOG) << "Recurrence parsing error:" << icalerror_strerror(icalerrno); success = false; } if (success) { d->mImpl->readRecurrence(recur, recurrence); } return success; } QString ICalFormat::createScheduleMessage(const IncidenceBase::Ptr &incidence, iTIPMethod method) { icalcomponent *message = nullptr; if (incidence->type() == Incidence::TypeEvent || incidence->type() == Incidence::TypeTodo) { Incidence::Ptr i = incidence.staticCast(); // Recurring events need timezone information to allow proper calculations // across timezones with different DST. const bool useUtcTimes = !i->recurs(); const bool hasSchedulingId = (i->schedulingID() != i->uid()); const bool incidenceNeedChanges = (useUtcTimes || hasSchedulingId); if (incidenceNeedChanges) { // The incidence need changes, so clone it before we continue i = Incidence::Ptr(i->clone()); // Handle conversion to UTC times if (useUtcTimes) { i->shiftTimes(QTimeZone::utc(), QTimeZone::utc()); } // Handle scheduling ID being present if (hasSchedulingId) { // We have a separation of scheduling ID and UID i->setSchedulingID(QString(), i->schedulingID()); } // Build the message with the cloned incidence message = d->mImpl->createScheduleComponent(i, method); } } if (message == nullptr) { message = d->mImpl->createScheduleComponent(incidence, method); } QString messageText = QString::fromUtf8(icalcomponent_as_ical_string(message)); icalcomponent_free(message); return messageText; } FreeBusy::Ptr ICalFormat::parseFreeBusy(const QString &str) { clearException(); icalcomponent *message = icalparser_parse_string(str.toUtf8().constData()); if (!message) { return FreeBusy::Ptr(); } FreeBusy::Ptr freeBusy; icalcomponent *c = nullptr; for (c = icalcomponent_get_first_component(message, ICAL_VFREEBUSY_COMPONENT); c != nullptr; c = icalcomponent_get_next_component(message, ICAL_VFREEBUSY_COMPONENT)) { FreeBusy::Ptr fb = d->mImpl->readFreeBusy(c); if (freeBusy) { freeBusy->merge(fb); } else { freeBusy = fb; } } if (!freeBusy) { qCDebug(KCALCORE_LOG) << "object is not a freebusy."; } icalcomponent_free(message); return freeBusy; } ScheduleMessage::Ptr ICalFormat::parseScheduleMessage(const Calendar::Ptr &cal, const QString &messageText) { setTimeZone(cal->timeZone()); clearException(); if (messageText.isEmpty()) { setException( new Exception(Exception::ParseErrorEmptyMessage)); return ScheduleMessage::Ptr(); } icalcomponent *message = icalparser_parse_string(messageText.toUtf8().constData()); if (!message) { setException( new Exception(Exception::ParseErrorUnableToParse)); return ScheduleMessage::Ptr(); } icalproperty *m = icalcomponent_get_first_property(message, ICAL_METHOD_PROPERTY); if (!m) { setException( new Exception(Exception::ParseErrorMethodProperty)); return ScheduleMessage::Ptr(); } // Populate the message's time zone collection with all VTIMEZONE components ICalTimeZoneCache tzlist; ICalTimeZoneParser parser(&tzlist); parser.parse(message); IncidenceBase::Ptr incidence; icalcomponent *c = icalcomponent_get_first_component(message, ICAL_VEVENT_COMPONENT); if (c) { incidence = d->mImpl->readEvent(c, &tzlist).staticCast(); } if (!incidence) { c = icalcomponent_get_first_component(message, ICAL_VTODO_COMPONENT); if (c) { incidence = d->mImpl->readTodo(c, &tzlist).staticCast(); } } if (!incidence) { c = icalcomponent_get_first_component(message, ICAL_VJOURNAL_COMPONENT); if (c) { incidence = d->mImpl->readJournal(c, &tzlist).staticCast(); } } if (!incidence) { c = icalcomponent_get_first_component(message, ICAL_VFREEBUSY_COMPONENT); if (c) { incidence = d->mImpl->readFreeBusy(c).staticCast(); } } if (!incidence) { qCDebug(KCALCORE_LOG) << "object is not a freebusy, event, todo or journal"; setException(new Exception(Exception::ParseErrorNotIncidence)); return ScheduleMessage::Ptr(); } icalproperty_method icalmethod = icalproperty_get_method(m); iTIPMethod method; switch (icalmethod) { case ICAL_METHOD_PUBLISH: method = iTIPPublish; break; case ICAL_METHOD_REQUEST: method = iTIPRequest; break; case ICAL_METHOD_REFRESH: method = iTIPRefresh; break; case ICAL_METHOD_CANCEL: method = iTIPCancel; break; case ICAL_METHOD_ADD: method = iTIPAdd; break; case ICAL_METHOD_REPLY: method = iTIPReply; break; case ICAL_METHOD_COUNTER: method = iTIPCounter; break; case ICAL_METHOD_DECLINECOUNTER: method = iTIPDeclineCounter; break; default: method = iTIPNoMethod; qCDebug(KCALCORE_LOG) << "Unknown method"; break; } if (!icalrestriction_check(message)) { qCWarning(KCALCORE_LOG) << endl << "kcalcore library reported a problem while parsing:"; qCWarning(KCALCORE_LOG) << ScheduleMessage::methodName(method) << ":" << d->mImpl->extractErrorProperty(c); } Incidence::Ptr existingIncidence = cal->incidence(incidence->uid()); icalcomponent *calendarComponent = nullptr; if (existingIncidence) { calendarComponent = d->mImpl->createCalendarComponent(cal); // TODO: check, if cast is required, or if it can be done by virtual funcs. // TODO: Use a visitor for this! if (existingIncidence->type() == Incidence::TypeTodo) { Todo::Ptr todo = existingIncidence.staticCast(); icalcomponent_add_component(calendarComponent, d->mImpl->writeTodo(todo)); } if (existingIncidence->type() == Incidence::TypeEvent) { Event::Ptr event = existingIncidence.staticCast(); icalcomponent_add_component(calendarComponent, d->mImpl->writeEvent(event)); } } else { icalcomponent_free(message); return ScheduleMessage::Ptr(new ScheduleMessage(incidence, method, ScheduleMessage::Unknown)); } icalproperty_xlicclass result = icalclassify(message, calendarComponent, static_cast("")); ScheduleMessage::Status status; switch (result) { case ICAL_XLICCLASS_PUBLISHNEW: status = ScheduleMessage::PublishNew; break; case ICAL_XLICCLASS_PUBLISHUPDATE: status = ScheduleMessage::PublishUpdate; break; case ICAL_XLICCLASS_OBSOLETE: status = ScheduleMessage::Obsolete; break; case ICAL_XLICCLASS_REQUESTNEW: status = ScheduleMessage::RequestNew; break; case ICAL_XLICCLASS_REQUESTUPDATE: status = ScheduleMessage::RequestUpdate; break; case ICAL_XLICCLASS_UNKNOWN: default: status = ScheduleMessage::Unknown; break; } icalcomponent_free(message); icalcomponent_free(calendarComponent); return ScheduleMessage::Ptr(new ScheduleMessage(incidence, method, status)); } void ICalFormat::setTimeZone(const QTimeZone &timeZone) { d->mTimeZone = timeZone; } QTimeZone ICalFormat::timeZone() const { return d->mTimeZone; } QByteArray ICalFormat::timeZoneId() const { return d->mTimeZone.id(); } void ICalFormat::virtual_hook(int id, void *data) { Q_UNUSED(id); Q_UNUSED(data); Q_ASSERT(false); } diff --git a/src/icalformat_p.cpp b/src/icalformat_p.cpp index c9c483edf..38b71aa27 100644 --- a/src/icalformat_p.cpp +++ b/src/icalformat_p.cpp @@ -1,3091 +1,3091 @@ /* 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 "kcalcore_debug.h" +#include "kcalendarcore_debug.h" #include using namespace KCalendarCore; 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 &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 &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 &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); } 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 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 {}; } 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 {}; } 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 a(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 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 org(cn, email); // TODO: Treat sent-by, dir and language here, too return org; } Attachment ICalFormatImpl::readAttachment(icalproperty *attach) { Attachment 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(p); } } else { p = icalattach_get_url(a); if (!p.isEmpty()) { attachment = 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(p); } break; } case ICAL_URI_VALUE: p = icalvalue_get_uri(value); attachment = Attachment(QString::fromUtf8(p)); break; default: break; } if (!attachment.isEmpty()) { 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(name, email)); break; } case ICAL_ATTACH_PROPERTY: { // Only in AUDIO and EMAIL and PROCEDURE alarms Attachment attach = readAttachment(p); if (!attach.isEmpty() && 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 || !days) { // Create second-type duration for 0 delay durations. 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 != KCalendarCore::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 f84e0b8c1..c6d88918d 100644 --- a/src/icaltimezones.cpp +++ b/src/icaltimezones.cpp @@ -1,734 +1,734 @@ /* 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 "kcalcore_debug.h" +#include "kcalendarcore_debug.h" #include #include extern "C" { #include #include } using namespace KCalendarCore; // 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 KCalendarCore { 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); 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 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(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(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 KCalendarCore diff --git a/src/incidencebase.cpp b/src/incidencebase.cpp index 027e600dd..d127032f4 100644 --- a/src/incidencebase.cpp +++ b/src/incidencebase.cpp @@ -1,752 +1,752 @@ /* This file is part of the kcalcore library. Copyright (c) 2001,2004 Cornelius Schumacher Copyright (C) 2003-2004 Reinhold Kainhofer Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies). All rights reserved. Contact: Alvaro Manera 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 IncidenceBase class. @brief An abstract base class that provides a common base for all calendar incidence classes. @author Cornelius Schumacher \ @author Reinhold Kainhofer \ */ #include "incidencebase.h" #include "calformat.h" #include "visitor.h" #include "utils_p.h" #include -#include "kcalcore_debug.h" +#include "kcalendarcore_debug.h" #include #include #define KCALCORE_MAGIC_NUMBER 0xCA1C012E #define KCALCORE_SERIALIZATION_VERSION 1 using namespace KCalendarCore; /** Private class that helps to provide binary compatibility between releases. @internal */ //@cond PRIVATE class Q_DECL_HIDDEN KCalendarCore::IncidenceBase::Private { public: Private() : mUpdateGroupLevel(0) , mUpdatedPending(false) , mAllDay(false) , mHasDuration(false) { } Private(const Private &other) : mUpdateGroupLevel(0) , mUpdatedPending(false) , mAllDay(true) , mHasDuration(false) { init(other); } ~Private() { } void init(const Private &other); QDateTime mLastModified; // incidence last modified date QDateTime mDtStart; // incidence start time Person mOrganizer; // incidence person (owner) QString mUid; // incidence unique id Duration mDuration; // incidence duration int mUpdateGroupLevel; // if non-zero, suppresses update() calls bool mUpdatedPending = false; // true if an update has occurred since startUpdates() bool mAllDay = false; // true if the incidence is all-day bool mHasDuration = false; // true if the incidence has a duration Attendee::List mAttendees; // list of incidence attendees QStringList mComments; // list of incidence comments QStringList mContacts; // list of incidence contacts QList mObservers; // list of incidence observers QSet mDirtyFields; // Fields that changed since last time the incidence was created // or since resetDirtyFlags() was called QUrl mUrl; // incidence url property }; void IncidenceBase::Private::init(const Private &other) { mLastModified = other.mLastModified; mDtStart = other.mDtStart; mOrganizer = other.mOrganizer; mUid = other.mUid; mDuration = other.mDuration; mAllDay = other.mAllDay; mHasDuration = other.mHasDuration; mComments = other.mComments; mContacts = other.mContacts; mAttendees = other.mAttendees; mAttendees.reserve(other.mAttendees.count()); mUrl = other.mUrl; } //@endcond IncidenceBase::IncidenceBase() : d(new KCalendarCore::IncidenceBase::Private) { mReadOnly = false; setUid(CalFormat::createUniqueId()); } IncidenceBase::IncidenceBase(const IncidenceBase &i) : CustomProperties(i) , d(new KCalendarCore::IncidenceBase::Private(*i.d)) { mReadOnly = i.mReadOnly; } IncidenceBase::~IncidenceBase() { delete d; } IncidenceBase &IncidenceBase::operator=(const IncidenceBase &other) { Q_ASSERT(type() == other.type()); startUpdates(); // assign is virtual, will call the derived class's IncidenceBase &ret = assign(other); endUpdates(); return ret; } IncidenceBase &IncidenceBase::assign(const IncidenceBase &other) { CustomProperties::operator=(other); d->init(*other.d); mReadOnly = other.mReadOnly; d->mDirtyFields.clear(); d->mDirtyFields.insert(FieldUnknown); return *this; } bool IncidenceBase::operator==(const IncidenceBase &i2) const { if (i2.type() != type()) { return false; } else { // equals is virtual, so here we're calling the derived class method return equals(i2); } } bool IncidenceBase::operator!=(const IncidenceBase &i2) const { return !operator==(i2); } bool IncidenceBase::equals(const IncidenceBase &i2) const { if (attendees().count() != i2.attendees().count()) { // qCDebug(KCALCORE_LOG) << "Attendee count is different"; return false; } Attendee::List al1 = attendees(); Attendee::List al2 = i2.attendees(); Attendee::List::ConstIterator a1 = al1.constBegin(); Attendee::List::ConstIterator a2 = al2.constBegin(); //TODO Does the order of attendees in the list really matter? //Please delete this comment if you know it's ok, kthx for (; a1 != al1.constEnd() && a2 != al2.constEnd(); ++a1, ++a2) { if (!(*a1 == *a2)) { // qCDebug(KCALCORE_LOG) << "Attendees are different"; return false; } } if (!CustomProperties::operator==(i2)) { // qCDebug(KCALCORE_LOG) << "Properties are different"; return false; } // Don't compare lastModified, otherwise the operator is not // of much use. We are not comparing for identity, after all. // no need to compare mObserver bool a = ((dtStart() == i2.dtStart()) || (!dtStart().isValid() && !i2.dtStart().isValid())); bool b = organizer() == i2.organizer(); bool c = uid() == i2.uid(); bool d = allDay() == i2.allDay(); bool e = duration() == i2.duration(); bool f = hasDuration() == i2.hasDuration(); bool g = url() == i2.url(); //qCDebug(KCALCORE_LOG) << a << b << c << d << e << f << g; return a && b && c && d && e && f && g; } bool IncidenceBase::accept(Visitor &v, const IncidenceBase::Ptr &incidence) { Q_UNUSED(v); Q_UNUSED(incidence); return false; } void IncidenceBase::setUid(const QString &uid) { if (d->mUid != uid) { update(); d->mUid = uid; d->mDirtyFields.insert(FieldUid); updated(); } } QString IncidenceBase::uid() const { return d->mUid; } void IncidenceBase::setLastModified(const QDateTime &lm) { // DON'T! updated() because we call this from // Calendar::updateEvent(). d->mDirtyFields.insert(FieldLastModified); // Convert to UTC and remove milliseconds part. QDateTime current = lm.toUTC(); QTime t = current.time(); t.setHMS(t.hour(), t.minute(), t.second(), 0); current.setTime(t); d->mLastModified = current; } QDateTime IncidenceBase::lastModified() const { return d->mLastModified; } void IncidenceBase::setOrganizer(const Person &organizer) { update(); // we don't check for readonly here, because it is // possible that by setting the organizer we are changing // the event's readonly status... d->mOrganizer = organizer; d->mDirtyFields.insert(FieldOrganizer); updated(); } void IncidenceBase::setOrganizer(const QString &o) { QString mail(o); if (mail.startsWith(QLatin1String("MAILTO:"), Qt::CaseInsensitive)) { mail = mail.remove(0, 7); } // split the string into full name plus email. const Person organizer = Person::fromFullName(mail); setOrganizer(organizer); } Person IncidenceBase::organizer() const { return d->mOrganizer; } void IncidenceBase::setReadOnly(bool readOnly) { mReadOnly = readOnly; } bool IncidenceBase::isReadOnly() const { return mReadOnly; } void IncidenceBase::setDtStart(const QDateTime &dtStart) { // if ( mReadOnly ) return; if (!dtStart.isValid() && type() != IncidenceBase::TypeTodo) { qCWarning(KCALCORE_LOG) << "Invalid dtStart"; } if (d->mDtStart != dtStart) { update(); d->mDtStart = dtStart; d->mDirtyFields.insert(FieldDtStart); updated(); } } QDateTime IncidenceBase::dtStart() const { return d->mDtStart; } bool IncidenceBase::allDay() const { return d->mAllDay; } void IncidenceBase::setAllDay(bool f) { if (mReadOnly || f == d->mAllDay) { return; } update(); d->mAllDay = f; if (d->mDtStart.isValid()) { d->mDirtyFields.insert(FieldDtStart); } updated(); } void IncidenceBase::shiftTimes(const QTimeZone &oldZone, const QTimeZone &newZone) { update(); d->mDtStart = d->mDtStart.toTimeZone(oldZone); d->mDtStart.setTimeZone(newZone); d->mDirtyFields.insert(FieldDtStart); d->mDirtyFields.insert(FieldDtEnd); updated(); } void IncidenceBase::addComment(const QString &comment) { d->mComments += comment; } bool IncidenceBase::removeComment(const QString &comment) { bool found = false; QStringList::Iterator i; for (i = d->mComments.begin(); !found && i != d->mComments.end(); ++i) { if ((*i) == comment) { found = true; d->mComments.erase(i); } } if (found) { d->mDirtyFields.insert(FieldComment); } return found; } void IncidenceBase::clearComments() { d->mDirtyFields.insert(FieldComment); d->mComments.clear(); } QStringList IncidenceBase::comments() const { return d->mComments; } void IncidenceBase::addContact(const QString &contact) { if (!contact.isEmpty()) { d->mContacts += contact; d->mDirtyFields.insert(FieldContact); } } bool IncidenceBase::removeContact(const QString &contact) { bool found = false; QStringList::Iterator i; for (i = d->mContacts.begin(); !found && i != d->mContacts.end(); ++i) { if ((*i) == contact) { found = true; d->mContacts.erase(i); } } if (found) { d->mDirtyFields.insert(FieldContact); } return found; } void IncidenceBase::clearContacts() { d->mDirtyFields.insert(FieldContact); d->mContacts.clear(); } QStringList IncidenceBase::contacts() const { return d->mContacts; } void IncidenceBase::addAttendee(const Attendee &a, bool doupdate) { if (a.isNull() || mReadOnly) { return; } Q_ASSERT(!a.uid().isEmpty()); if (doupdate) { update(); } d->mAttendees.append(a); if (doupdate) { d->mDirtyFields.insert(FieldAttendees); updated(); } } Attendee::List IncidenceBase::attendees() const { return d->mAttendees; } int IncidenceBase::attendeeCount() const { return d->mAttendees.count(); } void IncidenceBase::setAttendees(const Attendee::List &attendees, bool doUpdate) { if (mReadOnly) { return; } if (doUpdate) { update(); } // don't simply assign, we need the logic in addAttendee here too clearAttendees(); d->mAttendees.reserve(attendees.size()); for (const auto &a : attendees) { addAttendee(a, false); } if (doUpdate) { d->mDirtyFields.insert(FieldAttendees); updated(); } } void IncidenceBase::clearAttendees() { if (mReadOnly) { return; } d->mDirtyFields.insert(FieldAttendees); d->mAttendees.clear(); } Attendee IncidenceBase::attendeeByMail(const QString &email) const { Attendee::List::ConstIterator it; for (it = d->mAttendees.constBegin(); it != d->mAttendees.constEnd(); ++it) { if ((*it).email() == email) { return *it; } } return {}; } Attendee IncidenceBase::attendeeByMails(const QStringList &emails, const QString &email) const { QStringList mails = emails; if (!email.isEmpty()) { mails.append(email); } Attendee::List::ConstIterator itA; for (itA = d->mAttendees.constBegin(); itA != d->mAttendees.constEnd(); ++itA) { for (QStringList::const_iterator it = mails.constBegin(); it != mails.constEnd(); ++it) { if ((*itA).email() == (*it)) { return *itA; } } } return {}; } Attendee IncidenceBase::attendeeByUid(const QString &uid) const { Attendee::List::ConstIterator it; for (it = d->mAttendees.constBegin(); it != d->mAttendees.constEnd(); ++it) { if ((*it).uid() == uid) { return *it; } } return {}; } void IncidenceBase::setDuration(const Duration &duration) { update(); d->mDuration = duration; setHasDuration(true); d->mDirtyFields.insert(FieldDuration); updated(); } Duration IncidenceBase::duration() const { return d->mDuration; } void IncidenceBase::setHasDuration(bool hasDuration) { d->mHasDuration = hasDuration; } bool IncidenceBase::hasDuration() const { return d->mHasDuration; } void IncidenceBase::setUrl(const QUrl &url) { d->mDirtyFields.insert(FieldUrl); d->mUrl = url; } QUrl IncidenceBase::url() const { return d->mUrl; } void IncidenceBase::registerObserver(IncidenceBase::IncidenceObserver *observer) { if (observer && !d->mObservers.contains(observer)) { d->mObservers.append(observer); } } void IncidenceBase::unRegisterObserver(IncidenceBase::IncidenceObserver *observer) { d->mObservers.removeAll(observer); } void IncidenceBase::update() { if (!d->mUpdateGroupLevel) { d->mUpdatedPending = true; const auto rid = recurrenceId(); for (IncidenceObserver *o : qAsConst(d->mObservers)) { o->incidenceUpdate(uid(), rid); } } } void IncidenceBase::updated() { if (d->mUpdateGroupLevel) { d->mUpdatedPending = true; } else { const auto rid = recurrenceId(); for (IncidenceObserver *o : qAsConst(d->mObservers)) { o->incidenceUpdated(uid(), rid); } } } void IncidenceBase::startUpdates() { update(); ++d->mUpdateGroupLevel; } void IncidenceBase::endUpdates() { if (d->mUpdateGroupLevel > 0) { if (--d->mUpdateGroupLevel == 0 && d->mUpdatedPending) { d->mUpdatedPending = false; updated(); } } } void IncidenceBase::customPropertyUpdate() { update(); } void IncidenceBase::customPropertyUpdated() { updated(); } QDateTime IncidenceBase::recurrenceId() const { return QDateTime(); } void IncidenceBase::resetDirtyFields() { d->mDirtyFields.clear(); } QSet IncidenceBase::dirtyFields() const { return d->mDirtyFields; } void IncidenceBase::setFieldDirty(IncidenceBase::Field field) { d->mDirtyFields.insert(field); } QUrl IncidenceBase::uri() const { return QUrl(QStringLiteral("urn:x-ical:") + uid()); } void IncidenceBase::setDirtyFields(const QSet &dirtyFields) { d->mDirtyFields = dirtyFields; } void IncidenceBase::serialize(QDataStream &out) const { Q_UNUSED(out); } void IncidenceBase::deserialize(QDataStream &in) { Q_UNUSED(in); } /** static */ quint32 IncidenceBase::magicSerializationIdentifier() { return KCALCORE_MAGIC_NUMBER; } QDataStream &KCalendarCore::operator<<(QDataStream &out, const KCalendarCore::IncidenceBase::Ptr &i) { if (!i) { return out; } out << static_cast(KCALCORE_MAGIC_NUMBER); // Magic number to identify KCalendarCore data out << static_cast(KCALCORE_SERIALIZATION_VERSION); out << static_cast(i->type()); out << *(static_cast(i.data())); serializeQDateTimeAsKDateTime(out, i->d->mLastModified); serializeQDateTimeAsKDateTime(out, i->d->mDtStart); out << i->organizer() << i->d->mUid << i->d->mDuration << i->d->mAllDay << i->d->mHasDuration << i->d->mComments << i->d->mContacts << i->d->mAttendees.count() << i->d->mUrl; for (const Attendee &attendee : qAsConst(i->d->mAttendees)) { out << attendee; } // Serialize the sub-class data. i->serialize(out); return out; } QDataStream &KCalendarCore::operator>>(QDataStream &in, KCalendarCore::IncidenceBase::Ptr &i) { if (!i) { return in; } qint32 attendeeCount, type; quint32 magic, version; in >> magic; if (magic != KCALCORE_MAGIC_NUMBER) { qCWarning(KCALCORE_LOG) << "Invalid magic on serialized data"; return in; } in >> version; if (version > KCALCORE_MAGIC_NUMBER) { qCWarning(KCALCORE_LOG) << "Invalid version on serialized data"; return in; } in >> type; in >> *(static_cast(i.data())); deserializeKDateTimeAsQDateTime(in, i->d->mLastModified); deserializeKDateTimeAsQDateTime(in, i->d->mDtStart); in >> i->d->mOrganizer >> i->d->mUid >> i->d->mDuration >> i->d->mAllDay >> i->d->mHasDuration >> i->d->mComments >> i->d->mContacts >> attendeeCount >> i->d->mUrl; i->d->mAttendees.clear(); i->d->mAttendees.reserve(attendeeCount); for (int it = 0; it < attendeeCount; it++) { Attendee attendee; in >> attendee; i->d->mAttendees.append(attendee); } // Deserialize the sub-class data. i->deserialize(in); return in; } IncidenceBase::IncidenceObserver::~IncidenceObserver() { } QVariantList IncidenceBase::attendeesVariant() const { QVariantList l; l.reserve(d->mAttendees.size()); std::transform(d->mAttendees.begin(), d->mAttendees.end(), std::back_inserter(l), [](const Attendee &a) { return QVariant::fromValue(a); }); return l; } diff --git a/src/journal.cpp b/src/journal.cpp index 03994674c..b827aab70 100644 --- a/src/journal.cpp +++ b/src/journal.cpp @@ -1,139 +1,139 @@ /* This file is part of the kcalcore library. Copyright (c) 2001 Cornelius Schumacher 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 Journal class. @brief Provides a Journal in the sense of RFC2445. @author Cornelius Schumacher \ */ #include "journal.h" #include "visitor.h" -#include "kcalcore_debug.h" +#include "kcalendarcore_debug.h" using namespace KCalendarCore; Journal::Journal() : d(nullptr) { } Journal::~Journal() { } Incidence::IncidenceType Journal::type() const { return TypeJournal; } QByteArray Journal::typeStr() const { return QByteArrayLiteral("Journal"); } Journal *Journal::clone() const { return new Journal(*this); } IncidenceBase &Journal::assign(const IncidenceBase &other) { Incidence::assign(other); return *this; } bool Journal::equals(const IncidenceBase &journal) const { return Incidence::equals(journal); } bool Journal::accept(Visitor &v, const IncidenceBase::Ptr &incidence) { return v.visit(incidence.staticCast()); } QDateTime Journal::dateTime(DateTimeRole role) const { switch (role) { case RoleEnd: case RoleEndTimeZone: return QDateTime(); case RoleDisplayStart: case RoleDisplayEnd: return dtStart(); default: return dtStart(); } } void Journal::setDateTime(const QDateTime &dateTime, DateTimeRole role) { switch (role) { case RoleDnD: { setDtStart(dateTime); break; } default: qCDebug(KCALCORE_LOG) << "Unhandled role" << role; } } void Journal::virtual_hook(VirtualHook id, void *data) { Q_UNUSED(id); Q_UNUSED(data); } QLatin1String Journal::mimeType() const { return Journal::journalMimeType(); } /* static */ QLatin1String Journal::journalMimeType() { return QLatin1String("application/x-vnd.akonadi.calendar.journal"); } QLatin1String Journal::iconName(const QDateTime &) const { return QLatin1String("view-pim-journal"); } void Journal::serialize(QDataStream &out) const { Incidence::serialize(out); } void Journal::deserialize(QDataStream &in) { Incidence::deserialize(in); } bool Journal::supportsGroupwareCommunication() const { return false; } diff --git a/src/memorycalendar.cpp b/src/memorycalendar.cpp index 8d3cd74d5..3e4cbc5ec 100644 --- a/src/memorycalendar.cpp +++ b/src/memorycalendar.cpp @@ -1,871 +1,871 @@ /* This file is part of the kcalcore library. Copyright (c) 1998 Preston Brown Copyright (c) 2001,2003,2004 Cornelius Schumacher Copyright (C) 2003-2004 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. */ /** @file This file is part of the API for handling calendar data and defines the MemoryCalendar class. @brief This class provides a calendar stored as a local file. @author Preston Brown \ @author Cornelius Schumacher \ */ #include "memorycalendar.h" -#include "kcalcore_debug.h" +#include "kcalendarcore_debug.h" #include "calformat.h" #include template static QVector values(const QMultiHash &c) { QVector v; v.reserve(c.size()); for (typename QMultiHash::const_iterator it = c.begin(), end = c.end(); it != end; ++it) { v.push_back(it.value()); } return v; } template static QVector values(const QMultiHash &c, const K &x) { QVector v; typename QMultiHash::const_iterator it = c.find(x); while (it != c.end() && it.key() == x) { v.push_back(it.value()); ++it; } return v; } using namespace KCalendarCore; /** Private class that helps to provide binary compatibility between releases. @internal */ //@cond PRIVATE class Q_DECL_HIDDEN KCalendarCore::MemoryCalendar::Private { public: Private(MemoryCalendar *qq) : q(qq), mFormat(nullptr) { } ~Private() { } MemoryCalendar *q; CalFormat *mFormat; // calendar format QString mIncidenceBeingUpdated; // Instance identifier of Incidence currently being updated /** * List of all incidences. * First indexed by incidence->type(), then by incidence->uid(); */ QMap > mIncidences; /** * Has all incidences, indexed by identifier. */ QHash mIncidencesByIdentifier; /** * List of all deleted incidences. * First indexed by incidence->type(), then by incidence->uid(); */ QMap > mDeletedIncidences; /** * Contains incidences ( to-dos; non-recurring, non-multiday events; journals; ) * indexed by start/due date. * * The QMap key is the incidence->type(). * The QMultiHash key is the dtStart/dtDue().toString() * * Note: We had 3 variables, mJournalsForDate, mTodosForDate and mEventsForDate * but i merged them into one (indexed by type) because it simplifies code using * it. No need to if else based on type. */ QMap > mIncidencesForDate; void insertIncidence(const Incidence::Ptr &incidence); Incidence::Ptr incidence(const QString &uid, IncidenceBase::IncidenceType type, const QDateTime &recurrenceId = {}) const; Incidence::Ptr deletedIncidence(const QString &uid, const QDateTime &recurrenceId, IncidenceBase::IncidenceType type) const; void deleteAllIncidences(IncidenceBase::IncidenceType type); }; //@endcond MemoryCalendar::MemoryCalendar(const QTimeZone &timeZone) : Calendar(timeZone), d(new KCalendarCore::MemoryCalendar::Private(this)) { } MemoryCalendar::MemoryCalendar(const QByteArray &timeZoneId) : Calendar(timeZoneId), d(new KCalendarCore::MemoryCalendar::Private(this)) { } MemoryCalendar::~MemoryCalendar() { close(); //NOLINT false clang-analyzer-optin.cplusplus.VirtualCall delete d; } void MemoryCalendar::close() { setObserversEnabled(false); // Don't call the virtual function deleteEvents() etc, the base class might have // other ways of deleting the data. d->deleteAllIncidences(Incidence::TypeEvent); d->deleteAllIncidences(Incidence::TypeTodo); d->deleteAllIncidences(Incidence::TypeJournal); d->mIncidencesByIdentifier.clear(); d->mDeletedIncidences.clear(); setModified(false); setObserversEnabled(true); } bool MemoryCalendar::deleteIncidence(const Incidence::Ptr &incidence) { // Handle orphaned children // relations is an Incidence's property, not a Todo's, so // we remove relations in deleteIncidence, not in deleteTodo. removeRelations(incidence); const Incidence::IncidenceType type = incidence->type(); const QString uid = incidence->uid(); if (d->mIncidences[type].contains(uid, incidence)) { // Notify while the incidence is still available, // this is necessary so korganizer still has time to query for exceptions notifyIncidenceAboutToBeDeleted(incidence); d->mIncidences[type].remove(uid, incidence); d->mIncidencesByIdentifier.remove(incidence->instanceIdentifier()); setModified(true); if (deletionTracking()) { d->mDeletedIncidences[type].insert(uid, incidence); } const QDateTime dt = incidence->dateTime(Incidence::RoleCalendarHashing); if (dt.isValid()) { d->mIncidencesForDate[type].remove(dt.date().toString(), incidence); } // Delete child-incidences. if (!incidence->hasRecurrenceId()) { deleteIncidenceInstances(incidence); } notifyIncidenceDeleted(incidence); return true; } else { qCWarning(KCALCORE_LOG) << incidence->typeStr() << " not found. uid=" << uid; return false; } } bool MemoryCalendar::deleteIncidenceInstances(const Incidence::Ptr &incidence) { const Incidence::IncidenceType type = incidence->type(); Incidence::List values = ::values(d->mIncidences[type], incidence->uid()); for (auto it = values.constBegin(); it != values.constEnd(); ++it) { Incidence::Ptr i = *it; if (i->hasRecurrenceId()) { qCDebug(KCALCORE_LOG) << "deleting child" << ", type=" << int(type) << ", uid=" << i->uid() // << ", start=" << i->dtStart() << " from calendar"; deleteIncidence(i); } } return true; } //@cond PRIVATE void MemoryCalendar::Private::deleteAllIncidences(Incidence::IncidenceType incidenceType) { QHashIteratori(mIncidences[incidenceType]); while (i.hasNext()) { i.next(); q->notifyIncidenceAboutToBeDeleted(i.value()); i.value()->unRegisterObserver(q); } mIncidences[incidenceType].clear(); mIncidencesForDate[incidenceType].clear(); } Incidence::Ptr MemoryCalendar::Private::incidence(const QString &uid, Incidence::IncidenceType type, const QDateTime &recurrenceId) const { Incidence::List values = ::values(mIncidences[type], uid); for (auto it = values.constBegin(); it != values.constEnd(); ++it) { Incidence::Ptr i = *it; if (recurrenceId.isNull()) { if (!i->hasRecurrenceId()) { return i; } } else { if (i->hasRecurrenceId() && i->recurrenceId() == recurrenceId) { return i; } } } return Incidence::Ptr(); } Incidence::Ptr MemoryCalendar::Private::deletedIncidence(const QString &uid, const QDateTime &recurrenceId, IncidenceBase::IncidenceType type) const { if (!q->deletionTracking()) { return Incidence::Ptr(); } Incidence::List values = ::values(mDeletedIncidences[type], uid); for (auto it = values.constBegin(); it != values.constEnd(); ++it) { Incidence::Ptr i = *it; if (recurrenceId.isNull()) { if (!i->hasRecurrenceId()) { return i; } } else { if (i->hasRecurrenceId() && i->recurrenceId() == recurrenceId) { return i; } } } return Incidence::Ptr(); } void MemoryCalendar::Private::insertIncidence(const Incidence::Ptr &incidence) { const QString uid = incidence->uid(); const Incidence::IncidenceType type = incidence->type(); if (!mIncidences[type].contains(uid, incidence)) { mIncidences[type].insert(uid, incidence); mIncidencesByIdentifier.insert(incidence->instanceIdentifier(), incidence); const QDateTime dt = incidence->dateTime(Incidence::RoleCalendarHashing); if (dt.isValid()) { mIncidencesForDate[type].insert(dt.date().toString(), incidence); } } else { #ifndef NDEBUG // if we already have an to-do with this UID, it must be the same incidence, // otherwise something's really broken Q_ASSERT(mIncidences[type].value(uid) == incidence); #endif } } //@endcond bool MemoryCalendar::addIncidence(const Incidence::Ptr &incidence) { d->insertIncidence(incidence); notifyIncidenceAdded(incidence); incidence->registerObserver(this); setupRelations(incidence); setModified(true); return true; } bool MemoryCalendar::addEvent(const Event::Ptr &event) { return addIncidence(event); } bool MemoryCalendar::deleteEvent(const Event::Ptr &event) { return deleteIncidence(event); } bool MemoryCalendar::deleteEventInstances(const Event::Ptr &event) { return deleteIncidenceInstances(event); } Event::Ptr MemoryCalendar::event(const QString &uid, const QDateTime &recurrenceId) const { return d->incidence(uid, Incidence::TypeEvent, recurrenceId).staticCast(); } Event::Ptr MemoryCalendar::deletedEvent(const QString &uid, const QDateTime &recurrenceId) const { return d->deletedIncidence(uid, recurrenceId, Incidence::TypeEvent).staticCast(); } bool MemoryCalendar::addTodo(const Todo::Ptr &todo) { return addIncidence(todo); } bool MemoryCalendar::deleteTodo(const Todo::Ptr &todo) { return deleteIncidence(todo); } bool MemoryCalendar::deleteTodoInstances(const Todo::Ptr &todo) { return deleteIncidenceInstances(todo); } Todo::Ptr MemoryCalendar::todo(const QString &uid, const QDateTime &recurrenceId) const { return d->incidence(uid, Incidence::TypeTodo, recurrenceId).staticCast(); } Todo::Ptr MemoryCalendar::deletedTodo(const QString &uid, const QDateTime &recurrenceId) const { return d->deletedIncidence(uid, recurrenceId, Incidence::TypeTodo).staticCast(); } Todo::List MemoryCalendar::rawTodos(TodoSortField sortField, SortDirection sortDirection) const { Todo::List todoList; todoList.reserve(d->mIncidences[Incidence::TypeTodo].count()); QHashIteratori(d->mIncidences[Incidence::TypeTodo]); while (i.hasNext()) { i.next(); todoList.append(i.value().staticCast()); } return Calendar::sortTodos(todoList, sortField, sortDirection); } Todo::List MemoryCalendar::deletedTodos(TodoSortField sortField, SortDirection sortDirection) const { if (!deletionTracking()) { return Todo::List(); } Todo::List todoList; todoList.reserve(d->mDeletedIncidences[Incidence::TypeTodo].count()); QHashIteratori(d->mDeletedIncidences[Incidence::TypeTodo]); while (i.hasNext()) { i.next(); todoList.append(i.value().staticCast()); } return Calendar::sortTodos(todoList, sortField, sortDirection); } Todo::List MemoryCalendar::todoInstances(const Incidence::Ptr &todo, TodoSortField sortField, SortDirection sortDirection) const { Todo::List list; Incidence::List values = ::values(d->mIncidences[Incidence::TypeTodo], todo->uid()); for (auto it = values.constBegin(); it != values.constEnd(); ++it) { Todo::Ptr t = (*it).staticCast(); if (t->hasRecurrenceId()) { list.append(t); } } return Calendar::sortTodos(list, sortField, sortDirection); } Todo::List MemoryCalendar::rawTodosForDate(const QDate &date) const { Todo::List todoList; Todo::Ptr t; const QString dateStr = date.toString(); QMultiHash::const_iterator it = d->mIncidencesForDate[Incidence::TypeTodo].constFind(dateStr); while (it != d->mIncidencesForDate[Incidence::TypeTodo].constEnd() && it.key() == dateStr) { t = it.value().staticCast(); todoList.append(t); ++it; } // Iterate over all todos. Look for recurring todoss that occur on this date QHashIteratori(d->mIncidences[Incidence::TypeTodo]); while (i.hasNext()) { i.next(); t = i.value().staticCast(); if (t->recurs()) { if (t->recursOn(date, timeZone())) { todoList.append(t); } } } return todoList; } Todo::List MemoryCalendar::rawTodos(const QDate &start, const QDate &end, const QTimeZone &timeZone, bool inclusive) const { Q_UNUSED(inclusive); // use only exact dtDue/dtStart, not dtStart and dtEnd Todo::List todoList; const auto ts = timeZone.isValid() ? timeZone : this->timeZone(); QDateTime st(start, QTime(0, 0, 0), ts); QDateTime nd(end, QTime(23, 59, 59, 999), ts); // Get todos QHashIteratori(d->mIncidences[Incidence::TypeTodo]); Todo::Ptr todo; while (i.hasNext()) { i.next(); todo = i.value().staticCast(); if (!isVisible(todo)) { continue; } QDateTime rStart = todo->hasDueDate() ? todo->dtDue() : todo->hasStartDate() ? todo->dtStart() : QDateTime(); if (!rStart.isValid()) { continue; } if (!todo->recurs()) { // non-recurring todos if (nd.isValid() && nd < rStart) { continue; } if (st.isValid() && rStart < st) { continue; } } else { // recurring events switch (todo->recurrence()->duration()) { case -1: // infinite break; case 0: // end date given default: // count given QDateTime rEnd(todo->recurrence()->endDate(), QTime(23, 59, 59, 999), ts); if (!rEnd.isValid()) { continue; } if (st.isValid() && rEnd < st) { continue; } break; } // switch(duration) } //if(recurs) todoList.append(todo); } return todoList; } Alarm::List MemoryCalendar::alarmsTo(const QDateTime &to) const { return alarms(QDateTime(QDate(1900, 1, 1), QTime(0, 0, 0)), to); } Alarm::List MemoryCalendar::alarms(const QDateTime &from, const QDateTime &to, bool excludeBlockedAlarms) const { Q_UNUSED(excludeBlockedAlarms); Alarm::List alarmList; QHashIteratorie(d->mIncidences[Incidence::TypeEvent]); Event::Ptr e; while (ie.hasNext()) { ie.next(); e = ie.value().staticCast(); if (e->recurs()) { appendRecurringAlarms(alarmList, e, from, to); } else { appendAlarms(alarmList, e, from, to); } } QHashIteratorit(d->mIncidences[Incidence::TypeTodo]); Todo::Ptr t; while (it.hasNext()) { it.next(); t = it.value().staticCast(); if (!t->isCompleted()) { appendAlarms(alarmList, t, from, to); if (t->recurs()) { appendRecurringAlarms(alarmList, t, from, to); } else { appendAlarms(alarmList, t, from, to); } } } return alarmList; } void MemoryCalendar::incidenceUpdate(const QString &uid, const QDateTime &recurrenceId) { Incidence::Ptr inc = incidence(uid, recurrenceId); if (inc) { if (!d->mIncidenceBeingUpdated.isEmpty()) { qCWarning(KCALCORE_LOG) << "Incidence::update() called twice without an updated() call in between."; } // Save it so we can detect changes to uid or recurringId. d->mIncidenceBeingUpdated = inc->instanceIdentifier(); const QDateTime dt = inc->dateTime(Incidence::RoleCalendarHashing); if (dt.isValid()) { const Incidence::IncidenceType type = inc->type(); d->mIncidencesForDate[type].remove(dt.date().toString(), inc); } } } void MemoryCalendar::incidenceUpdated(const QString &uid, const QDateTime &recurrenceId) { Incidence::Ptr inc = incidence(uid, recurrenceId); if (inc) { if (d->mIncidenceBeingUpdated.isEmpty()) { qCWarning(KCALCORE_LOG) << "Incidence::updated() called twice without an update() call in between."; } else if (inc->instanceIdentifier() != d->mIncidenceBeingUpdated) { // Instance identifier changed, update our hash table d->mIncidencesByIdentifier.remove(d->mIncidenceBeingUpdated); d->mIncidencesByIdentifier.insert(inc->instanceIdentifier(), inc); } d->mIncidenceBeingUpdated = QString(); inc->setLastModified(QDateTime::currentDateTimeUtc()); // we should probably update the revision number here, // or internally in the Event itself when certain things change. // need to verify with ical documentation. const QDateTime dt = inc->dateTime(Incidence::RoleCalendarHashing); if (dt.isValid()) { const Incidence::IncidenceType type = inc->type(); d->mIncidencesForDate[type].insert(dt.date().toString(), inc); } notifyIncidenceChanged(inc); setModified(true); } } Event::List MemoryCalendar::rawEventsForDate(const QDate &date, const QTimeZone &timeZone, EventSortField sortField, SortDirection sortDirection) const { Event::List eventList; if (!date.isValid()) { // There can't be events on invalid dates return eventList; } Event::Ptr ev; // Find the hash for the specified date const QString dateStr = date.toString(); QMultiHash::const_iterator it = d->mIncidencesForDate[Incidence::TypeEvent].constFind(dateStr); // Iterate over all non-recurring, single-day events that start on this date const auto ts = timeZone.isValid() ? timeZone : this->timeZone(); while (it != d->mIncidencesForDate[Incidence::TypeEvent].constEnd() && it.key() == dateStr) { ev = it.value().staticCast(); QDateTime end(ev->dtEnd().toTimeZone(ev->dtStart().timeZone())); if (ev->allDay()) { end.setTime(QTime()); } else { end = end.addSecs(-1); } if (end.date() >= date) { eventList.append(ev); } ++it; } // Iterate over all events. Look for recurring events that occur on this date QHashIteratori(d->mIncidences[Incidence::TypeEvent]); while (i.hasNext()) { i.next(); ev = i.value().staticCast(); if (ev->recurs()) { if (ev->isMultiDay()) { int extraDays = ev->dtStart().date().daysTo(ev->dtEnd().date()); for (int i = 0; i <= extraDays; ++i) { if (ev->recursOn(date.addDays(-i), ts)) { eventList.append(ev); break; } } } else { if (ev->recursOn(date, ts)) { eventList.append(ev); } } } else { if (ev->isMultiDay()) { if (ev->dtStart().date() <= date && ev->dtEnd().date() >= date) { eventList.append(ev); } } } } return Calendar::sortEvents(eventList, sortField, sortDirection); } Event::List MemoryCalendar::rawEvents(const QDate &start, const QDate &end, const QTimeZone &timeZone, bool inclusive) const { Event::List eventList; const auto ts = timeZone.isValid() ? timeZone : this->timeZone(); QDateTime st(start, QTime(0, 0, 0), ts); QDateTime nd(end, QTime(23, 59, 59, 999), ts); // Get non-recurring events QHashIteratori(d->mIncidences[Incidence::TypeEvent]); Event::Ptr event; while (i.hasNext()) { i.next(); event = i.value().staticCast(); QDateTime rStart = event->dtStart(); if (nd < rStart) { continue; } if (inclusive && rStart < st) { continue; } if (!event->recurs()) { // non-recurring events QDateTime rEnd = event->dtEnd(); if (rEnd < st) { continue; } if (inclusive && nd < rEnd) { continue; } } else { // recurring events switch (event->recurrence()->duration()) { case -1: // infinite if (inclusive) { continue; } break; case 0: // end date given default: // count given QDateTime rEnd(event->recurrence()->endDate(), QTime(23, 59, 59, 999), ts); if (!rEnd.isValid()) { continue; } if (rEnd < st) { continue; } if (inclusive && nd < rEnd) { continue; } break; } // switch(duration) } //if(recurs) eventList.append(event); } return eventList; } Event::List MemoryCalendar::rawEventsForDate(const QDateTime &kdt) const { return rawEventsForDate(kdt.date(), kdt.timeZone()); } Event::List MemoryCalendar::rawEvents(EventSortField sortField, SortDirection sortDirection) const { Event::List eventList; eventList.reserve(d->mIncidences[Incidence::TypeEvent].count()); QHashIterator i(d->mIncidences[Incidence::TypeEvent]); while (i.hasNext()) { i.next(); eventList.append(i.value().staticCast()); } return Calendar::sortEvents(eventList, sortField, sortDirection); } Event::List MemoryCalendar::deletedEvents(EventSortField sortField, SortDirection sortDirection) const { if (!deletionTracking()) { return Event::List(); } Event::List eventList; eventList.reserve(d->mDeletedIncidences[Incidence::TypeEvent].count()); QHashIteratori(d->mDeletedIncidences[Incidence::TypeEvent]); while (i.hasNext()) { i.next(); eventList.append(i.value().staticCast()); } return Calendar::sortEvents(eventList, sortField, sortDirection); } Event::List MemoryCalendar::eventInstances(const Incidence::Ptr &event, EventSortField sortField, SortDirection sortDirection) const { Event::List list; Incidence::List values = ::values(d->mIncidences[Incidence::TypeEvent], event->uid()); for (auto it = values.constBegin(); it != values.constEnd(); ++it) { Event::Ptr ev = (*it).staticCast(); if (ev->hasRecurrenceId()) { list.append(ev); } } return Calendar::sortEvents(list, sortField, sortDirection); } bool MemoryCalendar::addJournal(const Journal::Ptr &journal) { return addIncidence(journal); } bool MemoryCalendar::deleteJournal(const Journal::Ptr &journal) { return deleteIncidence(journal); } bool MemoryCalendar::deleteJournalInstances(const Journal::Ptr &journal) { return deleteIncidenceInstances(journal); } Journal::Ptr MemoryCalendar::journal(const QString &uid, const QDateTime &recurrenceId) const { return d->incidence(uid, Incidence::TypeJournal, recurrenceId).staticCast(); } Journal::Ptr MemoryCalendar::deletedJournal(const QString &uid, const QDateTime &recurrenceId) const { return d->deletedIncidence(uid, recurrenceId, Incidence::TypeJournal).staticCast(); } Journal::List MemoryCalendar::rawJournals(JournalSortField sortField, SortDirection sortDirection) const { Journal::List journalList; QHashIteratori(d->mIncidences[Incidence::TypeJournal]); while (i.hasNext()) { i.next(); journalList.append(i.value().staticCast()); } return Calendar::sortJournals(journalList, sortField, sortDirection); } Journal::List MemoryCalendar::deletedJournals(JournalSortField sortField, SortDirection sortDirection) const { if (!deletionTracking()) { return Journal::List(); } Journal::List journalList; journalList.reserve(d->mDeletedIncidences[Incidence::TypeJournal].count()); QHashIteratori(d->mDeletedIncidences[Incidence::TypeJournal]); while (i.hasNext()) { i.next(); journalList.append(i.value().staticCast()); } return Calendar::sortJournals(journalList, sortField, sortDirection); } Journal::List MemoryCalendar::journalInstances(const Incidence::Ptr &journal, JournalSortField sortField, SortDirection sortDirection) const { Journal::List list; Incidence::List values = ::values(d->mIncidences[Incidence::TypeJournal], journal->uid()); for (auto it = values.constBegin(); it != values.constEnd(); ++it) { Journal::Ptr j = (*it).staticCast(); if (j->hasRecurrenceId()) { list.append(j); } } return Calendar::sortJournals(list, sortField, sortDirection); } Journal::List MemoryCalendar::rawJournalsForDate(const QDate &date) const { Journal::List journalList; Journal::Ptr j; QString dateStr = date.toString(); QMultiHash::const_iterator it = d->mIncidencesForDate[Incidence::TypeJournal].constFind(dateStr); while (it != d->mIncidencesForDate[Incidence::TypeJournal].constEnd() && it.key() == dateStr) { j = it.value().staticCast(); journalList.append(j); ++it; } return journalList; } Incidence::Ptr MemoryCalendar::instance(const QString &identifier) const { return d->mIncidencesByIdentifier.value(identifier); } void MemoryCalendar::virtual_hook(int id, void *data) { Q_UNUSED(id); Q_UNUSED(data); Q_ASSERT(false); } diff --git a/src/recurrence.cpp b/src/recurrence.cpp index 1229a5fe0..c55d49867 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_p.h" #include "recurrencehelper_p.h" -#include "kcalcore_debug.h" +#include "kcalendarcore_debug.h" #include #include #include #include using namespace KCalendarCore; //@cond PRIVATE class Q_DECL_HIDDEN KCalendarCore::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 KCalendarCore::Recurrence::Private()) { } Recurrence::Recurrence(const Recurrence &r) : RecurrenceRule::RuleObserver(), d(new KCalendarCore::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) { 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() { } KCALENDARCORE_EXPORT QDataStream &KCalendarCore::operator<<(QDataStream &out, KCalendarCore::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; } KCALENDARCORE_EXPORT QDataStream &KCalendarCore::operator>>(QDataStream &in, KCalendarCore::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/recurrencerule.cpp b/src/recurrencerule.cpp index 4b9f69a64..25a61f117 100644 --- a/src/recurrencerule.cpp +++ b/src/recurrencerule.cpp @@ -1,2331 +1,2331 @@ /* This file is part of the kcalcore library. Copyright (c) 2005 Reinhold Kainhofer Copyright (c) 2006-2008 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 "recurrencerule.h" #include "utils_p.h" -#include "kcalcore_debug.h" +#include "kcalendarcore_debug.h" #include "recurrencehelper_p.h" #include #include #include #include #include using namespace KCalendarCore; // Maximum number of intervals to process const int LOOP_LIMIT = 10000; #ifndef NDEBUG static QString dumpTime(const QDateTime &dt, bool allDay); // for debugging #endif /*========================================================================= = = = IMPORTANT CODING NOTE: = = = = Recurrence handling code is time critical, especially for sub-daily = = recurrences. For example, if getNextDate() is called repeatedly to = = check all consecutive occurrences over a few years, on a slow machine = = this could take many seconds to complete in the worst case. Simple = = sub-daily recurrences are optimised by use of mTimedRepetition. = = = ==========================================================================*/ /************************************************************************** * DateHelper * **************************************************************************/ //@cond PRIVATE class DateHelper { public: #ifndef NDEBUG static QString dayName(short day); #endif static QDate getNthWeek(int year, int weeknumber, short weekstart = 1); static int weekNumbersInYear(int year, short weekstart = 1); static int getWeekNumber(const QDate &date, short weekstart, int *year = nullptr); static int getWeekNumberNeg(const QDate &date, short weekstart, int *year = nullptr); // Convert to QDate, allowing for day < 0. // month and day must be non-zero. static QDate getDate(int year, int month, int day) { if (day >= 0) { return QDate(year, month, day); } else { if (++month > 12) { month = 1; ++year; } return QDate(year, month, 1).addDays(day); } } }; #ifndef NDEBUG // TODO: Move to a general library / class, as we need the same in the iCal // generator and in the xcal format QString DateHelper::dayName(short day) { switch (day) { case 1: return QStringLiteral("MO"); case 2: return QStringLiteral("TU"); case 3: return QStringLiteral("WE"); case 4: return QStringLiteral("TH"); case 5: return QStringLiteral("FR"); case 6: return QStringLiteral("SA"); case 7: return QStringLiteral("SU"); default: return QStringLiteral("??"); } } #endif QDate DateHelper::getNthWeek(int year, int weeknumber, short weekstart) { if (weeknumber == 0) { return QDate(); } // Adjust this to the first day of week #1 of the year and add 7*weekno days. QDate dt(year, 1, 4); // Week #1 is the week that contains Jan 4 int adjust = -(7 + dt.dayOfWeek() - weekstart) % 7; if (weeknumber > 0) { dt = dt.addDays(7 * (weeknumber - 1) + adjust); } else if (weeknumber < 0) { dt = dt.addYears(1); dt = dt.addDays(7 * weeknumber + adjust); } return dt; } int DateHelper::getWeekNumber(const QDate &date, short weekstart, int *year) { int y = date.year(); QDate dt(y, 1, 4); // <= definitely in week #1 dt = dt.addDays(-(7 + dt.dayOfWeek() - weekstart) % 7); // begin of week #1 qint64 daysto = dt.daysTo(date); if (daysto < 0) { // in first week of year --y; dt = QDate(y, 1, 4); dt = dt.addDays(-(7 + dt.dayOfWeek() - weekstart) % 7); // begin of week #1 daysto = dt.daysTo(date); } else if (daysto > 355) { // near the end of the year - check if it's next year QDate dtn(y + 1, 1, 4); // <= definitely first week of next year dtn = dtn.addDays(-(7 + dtn.dayOfWeek() - weekstart) % 7); qint64 dayston = dtn.daysTo(date); if (dayston >= 0) { // in first week of next year; ++y; daysto = dayston; } } if (year) { *year = y; } return daysto / 7 + 1; } int DateHelper::weekNumbersInYear(int year, short weekstart) { QDate dt(year, 1, weekstart); QDate dt1(year + 1, 1, weekstart); return dt.daysTo(dt1) / 7; } // Week number from the end of the year int DateHelper::getWeekNumberNeg(const QDate &date, short weekstart, int *year) { int weekpos = getWeekNumber(date, weekstart, year); return weekNumbersInYear(*year, weekstart) - weekpos - 1; } //@endcond /************************************************************************** * WDayPos * **************************************************************************/ bool RecurrenceRule::WDayPos::operator==(const RecurrenceRule::WDayPos &pos2) const { return mDay == pos2.mDay && mPos == pos2.mPos; } bool RecurrenceRule::WDayPos::operator!=(const RecurrenceRule::WDayPos &pos2) const { return !operator==(pos2); } /************************************************************************** * Constraint * **************************************************************************/ //@cond PRIVATE class Constraint { public: typedef QVector List; Constraint() {} explicit Constraint(const QTimeZone &, int wkst = 1); Constraint(const QDateTime &dt, RecurrenceRule::PeriodType type, int wkst); void clear(); void setYear(int n) { year = n; useCachedDt = false; } void setMonth(int n) { month = n; useCachedDt = false; } void setDay(int n) { day = n; useCachedDt = false; } void setHour(int n) { hour = n; useCachedDt = false; } void setMinute(int n) { minute = n; useCachedDt = false; } void setSecond(int n) { second = n; useCachedDt = false; } void setWeekday(int n) { weekday = n; useCachedDt = false; } void setWeekdaynr(int n) { weekdaynr = n; useCachedDt = false; } void setWeeknumber(int n) { weeknumber = n; useCachedDt = false; } void setYearday(int n) { yearday = n; useCachedDt = false; } void setWeekstart(int n) { weekstart = n; useCachedDt = false; } int year; // 0 means unspecified int month; // 0 means unspecified int day; // 0 means unspecified int hour; // -1 means unspecified int minute; // -1 means unspecified int second; // -1 means unspecified int weekday; // 0 means unspecified int weekdaynr; // index of weekday in month/year (0=unspecified) int weeknumber; // 0 means unspecified int yearday; // 0 means unspecified int weekstart; // first day of week (1=monday, 7=sunday, 0=unspec.) QTimeZone timeZone; // time zone etc. to use bool readDateTime(const QDateTime &dt, RecurrenceRule::PeriodType type); bool matches(const QDate &dt, RecurrenceRule::PeriodType type) const; bool matches(const QDateTime &dt, RecurrenceRule::PeriodType type) const; bool merge(const Constraint &interval); bool isConsistent(RecurrenceRule::PeriodType period) const; bool increase(RecurrenceRule::PeriodType type, int freq); QDateTime intervalDateTime(RecurrenceRule::PeriodType type) const; QList dateTimes(RecurrenceRule::PeriodType type) const; void appendDateTime(const QDate &date, const QTime &time, QList &list) const; void dump() const; private: mutable bool useCachedDt; mutable QDateTime cachedDt; }; Constraint::Constraint(const QTimeZone &timeZone, int wkst) : weekstart(wkst), timeZone(timeZone) { clear(); } Constraint::Constraint(const QDateTime &dt, RecurrenceRule::PeriodType type, int wkst) : weekstart(wkst), timeZone(dt.timeZone()) { clear(); readDateTime(dt, type); } void Constraint::clear() { year = 0; month = 0; day = 0; hour = -1; minute = -1; second = -1; weekday = 0; weekdaynr = 0; weeknumber = 0; yearday = 0; useCachedDt = false; } bool Constraint::matches(const QDate &dt, RecurrenceRule::PeriodType type) const { // If the event recurs in week 53 or 1, the day might not belong to the same // year as the week it is in. E.g. Jan 1, 2005 is in week 53 of year 2004. // So we can't simply check the year in that case! if (weeknumber == 0) { if (year > 0 && year != dt.year()) { return false; } } else { int y = 0; if (weeknumber > 0 && weeknumber != DateHelper::getWeekNumber(dt, weekstart, &y)) { return false; } if (weeknumber < 0 && weeknumber != DateHelper::getWeekNumberNeg(dt, weekstart, &y)) { return false; } if (year > 0 && year != y) { return false; } } if (month > 0 && month != dt.month()) { return false; } if (day > 0 && day != dt.day()) { return false; } if (day < 0 && dt.day() != (dt.daysInMonth() + day + 1)) { return false; } if (weekday > 0) { if (weekday != dt.dayOfWeek()) { return false; } if (weekdaynr != 0) { // If it's a yearly recurrence and a month is given, the position is // still in the month, not in the year. if ((type == RecurrenceRule::rMonthly) || (type == RecurrenceRule::rYearly && month > 0)) { // Monthly if (weekdaynr > 0 && weekdaynr != (dt.day() - 1) / 7 + 1) { return false; } if (weekdaynr < 0 && weekdaynr != -((dt.daysInMonth() - dt.day()) / 7 + 1)) { return false; } } else { // Yearly if (weekdaynr > 0 && weekdaynr != (dt.dayOfYear() - 1) / 7 + 1) { return false; } if (weekdaynr < 0 && weekdaynr != -((dt.daysInYear() - dt.dayOfYear()) / 7 + 1)) { return false; } } } } if (yearday > 0 && yearday != dt.dayOfYear()) { return false; } if (yearday < 0 && yearday != dt.daysInYear() - dt.dayOfYear() + 1) { return false; } return true; } /* Check for a match with the specified date/time. * The date/time's time specification must correspond with that of the start date/time. */ bool Constraint::matches(const QDateTime &dt, RecurrenceRule::PeriodType type) const { if ((hour >= 0 && hour != dt.time().hour()) || (minute >= 0 && minute != dt.time().minute()) || (second >= 0 && second != dt.time().second()) || !matches(dt.date(), type)) { return false; } return true; } bool Constraint::isConsistent(RecurrenceRule::PeriodType /*period*/) const { // TODO: Check for consistency, e.g. byyearday=3 and bymonth=10 return true; } // Return a date/time set to the constraint values, but with those parts less // significant than the given period type set to 1 (for dates) or 0 (for times). QDateTime Constraint::intervalDateTime(RecurrenceRule::PeriodType type) const { if (useCachedDt) { return cachedDt; } QDate d; QTime t(0, 0, 0); bool subdaily = true; switch (type) { case RecurrenceRule::rSecondly: t.setHMS(hour, minute, second); break; case RecurrenceRule::rMinutely: t.setHMS(hour, minute, 0); break; case RecurrenceRule::rHourly: t.setHMS(hour, 0, 0); break; case RecurrenceRule::rDaily: break; case RecurrenceRule::rWeekly: d = DateHelper::getNthWeek(year, weeknumber, weekstart); subdaily = false; break; case RecurrenceRule::rMonthly: d.setDate(year, month, 1); subdaily = false; break; case RecurrenceRule::rYearly: d.setDate(year, 1, 1); subdaily = false; break; default: break; } if (subdaily) { d = DateHelper::getDate(year, (month > 0) ? month : 1, day ? day : 1); } cachedDt = QDateTime(d, t, timeZone); useCachedDt = true; return cachedDt; } bool Constraint::merge(const Constraint &interval) { #define mergeConstraint( name, cmparison ) \ if ( interval.name cmparison ) { \ if ( !( name cmparison ) ) { \ name = interval.name; \ } else if ( name != interval.name ) { \ return false;\ } \ } useCachedDt = false; mergeConstraint(year, > 0); mergeConstraint(month, > 0); mergeConstraint(day, != 0); mergeConstraint(hour, >= 0); mergeConstraint(minute, >= 0); mergeConstraint(second, >= 0); mergeConstraint(weekday, != 0); mergeConstraint(weekdaynr, != 0); mergeConstraint(weeknumber, != 0); mergeConstraint(yearday, != 0); #undef mergeConstraint return true; } // Y M D | H Mn S | WD #WD | WN | YD // required: // x | x x x | | | // 0) Trivial: Exact date given, maybe other restrictions // x x x | x x x | | | // 1) Easy case: no weekly restrictions -> at most a loop through possible dates // x + + | x x x | - - | - | - // 2) Year day is given -> date known // x | x x x | | | + // 3) week number is given -> loop through all days of that week. Further // restrictions will be applied in the end, when we check all dates for // consistency with the constraints // x | x x x | | + | (-) // 4) week day is specified -> // x | x x x | x ? | (-)| (-) // 5) All possiblecases have already been treated, so this must be an error! QList Constraint::dateTimes(RecurrenceRule::PeriodType type) const { QList result; if (!isConsistent(type)) { return result; } // TODO_Recurrence: Handle all-day QTime tm(hour, minute, second); bool done = false; if (day && month > 0) { appendDateTime(DateHelper::getDate(year, month, day), tm, result); done = true; } if (!done && weekday == 0 && weeknumber == 0 && yearday == 0) { // Easy case: date is given, not restrictions by week or yearday uint mstart = (month > 0) ? month : 1; uint mend = (month <= 0) ? 12 : month; for (uint m = mstart; m <= mend; ++m) { uint dstart, dend; if (day > 0) { dstart = dend = day; } else if (day < 0) { QDate date(year, month, 1); dstart = dend = date.daysInMonth() + day + 1; } else { QDate date(year, month, 1); dstart = 1; dend = date.daysInMonth(); } uint d = dstart; for (QDate dt(year, m, dstart);; dt = dt.addDays(1)) { appendDateTime(dt, tm, result); if (++d > dend) { break; } } } done = true; } // Else: At least one of the week / yearday restrictions was given... // If we have a yearday (and of course a year), we know the exact date if (!done && yearday != 0) { // yearday < 0 means from end of year, so we'll need Jan 1 of the next year QDate d(year + ((yearday > 0) ? 0 : 1), 1, 1); d = d.addDays(yearday - ((yearday > 0) ? 1 : 0)); appendDateTime(d, tm, result); done = true; } // Else: If we have a weeknumber, we have at most 7 possible dates, loop through them if (!done && weeknumber != 0) { QDate wst(DateHelper::getNthWeek(year, weeknumber, weekstart)); if (weekday != 0) { wst = wst.addDays((7 + weekday - weekstart) % 7); appendDateTime(wst, tm, result); } else { for (int i = 0; i < 7; ++i) { appendDateTime(wst, tm, result); wst = wst.addDays(1); } } done = true; } // weekday is given if (!done && weekday != 0) { QDate dt(year, 1, 1); // If type == yearly and month is given, pos is still in month not year! // TODO_Recurrence: Correct handling of n-th BYDAY... int maxloop = 53; bool inMonth = (type == RecurrenceRule::rMonthly) || (type == RecurrenceRule::rYearly && month > 0); if (inMonth && month > 0) { dt = QDate(year, month, 1); maxloop = 5; } if (weekdaynr < 0) { // From end of period (month, year) => relative to begin of next period if (inMonth) { dt = dt.addMonths(1); } else { dt = dt.addYears(1); } } int adj = (7 + weekday - dt.dayOfWeek()) % 7; dt = dt.addDays(adj); // correct first weekday of the period if (weekdaynr > 0) { dt = dt.addDays((weekdaynr - 1) * 7); appendDateTime(dt, tm, result); } else if (weekdaynr < 0) { dt = dt.addDays(weekdaynr * 7); appendDateTime(dt, tm, result); } else { // loop through all possible weeks, non-matching will be filtered later for (int i = 0; i < maxloop; ++i) { appendDateTime(dt, tm, result); dt = dt.addDays(7); } } } // weekday != 0 // Only use those times that really match all other constraints, too QList valid; for (int i = 0, iend = result.count(); i < iend; ++i) { if (matches(result[i], type)) { valid.append(result[i]); } } // Don't sort it here, would be unnecessary work. The results from all // constraints will be merged to one big list of the interval. Sort that one! return valid; } void Constraint::appendDateTime(const QDate &date, const QTime &time, QList &list) const { QDateTime dt(date, time, timeZone); if (dt.isValid()) { list.append(dt); } } bool Constraint::increase(RecurrenceRule::PeriodType type, int freq) { // convert the first day of the interval to QDateTime intervalDateTime(type); // Now add the intervals switch (type) { case RecurrenceRule::rSecondly: cachedDt = cachedDt.addSecs(freq); break; case RecurrenceRule::rMinutely: cachedDt = cachedDt.addSecs(60 * freq); break; case RecurrenceRule::rHourly: cachedDt = cachedDt.addSecs(3600 * freq); break; case RecurrenceRule::rDaily: cachedDt = cachedDt.addDays(freq); break; case RecurrenceRule::rWeekly: cachedDt = cachedDt.addDays(7 * freq); break; case RecurrenceRule::rMonthly: cachedDt = cachedDt.addMonths(freq); break; case RecurrenceRule::rYearly: cachedDt = cachedDt.addYears(freq); break; default: break; } // Convert back from QDateTime to the Constraint class readDateTime(cachedDt, type); useCachedDt = true; // readDateTime() resets this return true; } // Set the constraint's value appropriate to 'type', to the value contained in a date/time. bool Constraint::readDateTime(const QDateTime &dt, RecurrenceRule::PeriodType type) { switch (type) { // Really fall through! Only weekly needs to be treated differently! case RecurrenceRule::rSecondly: second = dt.time().second(); Q_FALLTHROUGH(); case RecurrenceRule::rMinutely: minute = dt.time().minute(); Q_FALLTHROUGH(); case RecurrenceRule::rHourly: hour = dt.time().hour(); Q_FALLTHROUGH(); case RecurrenceRule::rDaily: day = dt.date().day(); Q_FALLTHROUGH(); case RecurrenceRule::rMonthly: month = dt.date().month(); Q_FALLTHROUGH(); case RecurrenceRule::rYearly: year = dt.date().year(); break; case RecurrenceRule::rWeekly: // Determine start day of the current week, calculate the week number from that weeknumber = DateHelper::getWeekNumber(dt.date(), weekstart, &year); break; default: break; } useCachedDt = false; return true; } //@endcond /************************************************************************** * RecurrenceRule::Private * **************************************************************************/ //@cond PRIVATE class Q_DECL_HIDDEN KCalendarCore::RecurrenceRule::Private { public: Private(RecurrenceRule *parent) : mParent(parent), mPeriod(rNone), mFrequency(0), mDuration(-1), mWeekStart(1), mIsReadOnly(false), mAllDay(false) { setDirty(); } Private(RecurrenceRule *parent, const Private &p); Private &operator=(const Private &other); bool operator==(const Private &other) const; void clear(); void setDirty(); void buildConstraints(); bool buildCache() const; Constraint getNextValidDateInterval(const QDateTime &preDate, PeriodType type) const; Constraint getPreviousValidDateInterval(const QDateTime &afterDate, PeriodType type) const; QList datesForInterval(const Constraint &interval, PeriodType type) const; RecurrenceRule *mParent; QString mRRule; // RRULE string PeriodType mPeriod; QDateTime mDateStart; // start of recurrence (but mDateStart is not an occurrence // unless it matches the rule) uint mFrequency; /** how often it recurs: < 0 means no end date, 0 means an explicit end date, positive values give the number of occurrences */ int mDuration; QDateTime mDateEnd; QList mBySeconds; // values: second 0-59 QList mByMinutes; // values: minute 0-59 QList mByHours; // values: hour 0-23 QList mByDays; // n-th weekday of the month or year QList mByMonthDays; // values: day -31 to -1 and 1-31 QList mByYearDays; // values: day -366 to -1 and 1-366 QList mByWeekNumbers; // values: week -53 to -1 and 1-53 QList mByMonths; // values: month 1-12 QList mBySetPos; // values: position -366 to -1 and 1-366 short mWeekStart; // first day of the week (1=Monday, 7=Sunday) Constraint::List mConstraints; QList mObservers; // Cache for duration mutable QList mCachedDates; mutable QDateTime mCachedDateEnd; mutable QDateTime mCachedLastDate; // when mCachedDateEnd invalid, last date checked mutable bool mCached; bool mIsReadOnly; bool mAllDay; bool mNoByRules; // no BySeconds, ByMinutes, ... rules exist uint mTimedRepetition; // repeats at a regular number of seconds interval, or 0 }; RecurrenceRule::Private::Private(RecurrenceRule *parent, const Private &p) : mParent(parent), mRRule(p.mRRule), mPeriod(p.mPeriod), mDateStart(p.mDateStart), mFrequency(p.mFrequency), mDuration(p.mDuration), mDateEnd(p.mDateEnd), mBySeconds(p.mBySeconds), mByMinutes(p.mByMinutes), mByHours(p.mByHours), mByDays(p.mByDays), mByMonthDays(p.mByMonthDays), mByYearDays(p.mByYearDays), mByWeekNumbers(p.mByWeekNumbers), mByMonths(p.mByMonths), mBySetPos(p.mBySetPos), mWeekStart(p.mWeekStart), mIsReadOnly(p.mIsReadOnly), mAllDay(p.mAllDay), mNoByRules(p.mNoByRules) { setDirty(); } RecurrenceRule::Private &RecurrenceRule::Private::operator=(const Private &p) { // check for self assignment if (&p == this) { return *this; } mRRule = p.mRRule; mPeriod = p.mPeriod; mDateStart = p.mDateStart; mFrequency = p.mFrequency; mDuration = p.mDuration; mDateEnd = p.mDateEnd; mBySeconds = p.mBySeconds; mByMinutes = p.mByMinutes; mByHours = p.mByHours; mByDays = p.mByDays; mByMonthDays = p.mByMonthDays; mByYearDays = p.mByYearDays; mByWeekNumbers = p.mByWeekNumbers; mByMonths = p.mByMonths; mBySetPos = p.mBySetPos; mWeekStart = p.mWeekStart; mIsReadOnly = p.mIsReadOnly; mAllDay = p.mAllDay; mNoByRules = p.mNoByRules; setDirty(); return *this; } bool RecurrenceRule::Private::operator==(const Private &r) const { return mPeriod == r.mPeriod && ((mDateStart == r.mDateStart) || (!mDateStart.isValid() && !r.mDateStart.isValid())) && mDuration == r.mDuration && ((mDateEnd == r.mDateEnd) || (!mDateEnd.isValid() && !r.mDateEnd.isValid())) && mFrequency == r.mFrequency && mIsReadOnly == r.mIsReadOnly && mAllDay == r.mAllDay && mBySeconds == r.mBySeconds && mByMinutes == r.mByMinutes && mByHours == r.mByHours && mByDays == r.mByDays && mByMonthDays == r.mByMonthDays && mByYearDays == r.mByYearDays && mByWeekNumbers == r.mByWeekNumbers && mByMonths == r.mByMonths && mBySetPos == r.mBySetPos && mWeekStart == r.mWeekStart && mNoByRules == r.mNoByRules; } void RecurrenceRule::Private::clear() { if (mIsReadOnly) { return; } mPeriod = rNone; mBySeconds.clear(); mByMinutes.clear(); mByHours.clear(); mByDays.clear(); mByMonthDays.clear(); mByYearDays.clear(); mByWeekNumbers.clear(); mByMonths.clear(); mBySetPos.clear(); mWeekStart = 1; mNoByRules = false; setDirty(); } void RecurrenceRule::Private::setDirty() { buildConstraints(); mCached = false; mCachedDates.clear(); for (int i = 0, iend = mObservers.count(); i < iend; ++i) { if (mObservers[i]) { mObservers[i]->recurrenceChanged(mParent); } } } //@endcond /************************************************************************** * RecurrenceRule * **************************************************************************/ RecurrenceRule::RecurrenceRule() : d(new Private(this)) { } RecurrenceRule::RecurrenceRule(const RecurrenceRule &r) : d(new Private(this, *r.d)) { } RecurrenceRule::~RecurrenceRule() { delete d; } bool RecurrenceRule::operator==(const RecurrenceRule &r) const { return *d == *r.d; } RecurrenceRule &RecurrenceRule::operator=(const RecurrenceRule &r) { // check for self assignment if (&r == this) { return *this; } *d = *r.d; return *this; } void RecurrenceRule::addObserver(RuleObserver *observer) { if (!d->mObservers.contains(observer)) { d->mObservers.append(observer); } } void RecurrenceRule::removeObserver(RuleObserver *observer) { d->mObservers.removeAll(observer); } void RecurrenceRule::setRecurrenceType(PeriodType period) { if (isReadOnly()) { return; } d->mPeriod = period; d->setDirty(); } QDateTime RecurrenceRule::endDt(bool *result) const { if (result) { *result = false; } if (d->mPeriod == rNone) { return QDateTime(); } if (d->mDuration < 0) { return QDateTime(); } if (d->mDuration == 0) { if (result) { *result = true; } return d->mDateEnd; } // N occurrences. Check if we have a full cache. If so, return the cached end date. if (!d->mCached) { // If not enough occurrences can be found (i.e. inconsistent constraints) if (!d->buildCache()) { return QDateTime(); } } if (result) { *result = true; } return d->mCachedDateEnd; } void RecurrenceRule::setEndDt(const QDateTime &dateTime) { if (isReadOnly()) { return; } d->mDateEnd = dateTime; if (d->mDateEnd.isValid()) { d->mDuration = 0; // set to 0 because there is an end date/time } d->setDirty(); } void RecurrenceRule::setDuration(int duration) { if (isReadOnly()) { return; } d->mDuration = duration; d->setDirty(); } void RecurrenceRule::setAllDay(bool allDay) { if (isReadOnly()) { return; } d->mAllDay = allDay; d->setDirty(); } void RecurrenceRule::clear() { d->clear(); } void RecurrenceRule::setDirty() { d->setDirty(); } void RecurrenceRule::setStartDt(const QDateTime &start) { if (isReadOnly()) { return; } d->mDateStart = start; d->setDirty(); } void RecurrenceRule::setFrequency(int freq) { if (isReadOnly() || freq <= 0) { return; } d->mFrequency = freq; d->setDirty(); } void RecurrenceRule::setBySeconds(const QList &bySeconds) { if (isReadOnly()) { return; } d->mBySeconds = bySeconds; d->setDirty(); } void RecurrenceRule::setByMinutes(const QList &byMinutes) { if (isReadOnly()) { return; } d->mByMinutes = byMinutes; d->setDirty(); } void RecurrenceRule::setByHours(const QList &byHours) { if (isReadOnly()) { return; } d->mByHours = byHours; d->setDirty(); } void RecurrenceRule::setByDays(const QList &byDays) { if (isReadOnly()) { return; } d->mByDays = byDays; d->setDirty(); } void RecurrenceRule::setByMonthDays(const QList &byMonthDays) { if (isReadOnly()) { return; } d->mByMonthDays = byMonthDays; d->setDirty(); } void RecurrenceRule::setByYearDays(const QList &byYearDays) { if (isReadOnly()) { return; } d->mByYearDays = byYearDays; d->setDirty(); } void RecurrenceRule::setByWeekNumbers(const QList &byWeekNumbers) { if (isReadOnly()) { return; } d->mByWeekNumbers = byWeekNumbers; d->setDirty(); } void RecurrenceRule::setByMonths(const QList &byMonths) { if (isReadOnly()) { return; } d->mByMonths = byMonths; d->setDirty(); } void RecurrenceRule::setBySetPos(const QList &bySetPos) { if (isReadOnly()) { return; } d->mBySetPos = bySetPos; d->setDirty(); } void RecurrenceRule::setWeekStart(short weekStart) { if (isReadOnly()) { return; } d->mWeekStart = weekStart; d->setDirty(); } void RecurrenceRule::shiftTimes(const QTimeZone &oldTz, const QTimeZone &newTz) { d->mDateStart = d->mDateStart.toTimeZone(oldTz); d->mDateStart.setTimeZone(newTz); if (d->mDuration == 0) { d->mDateEnd = d->mDateEnd.toTimeZone(oldTz); d->mDateEnd.setTimeZone(newTz); } d->setDirty(); } // Taken from recurrence.cpp // int RecurrenceRule::maxIterations() const // { // /* Find the maximum number of iterations which may be needed to reach the // * next actual occurrence of a monthly or yearly recurrence. // * More than one iteration may be needed if, for example, it's the 29th February, // * the 31st day of the month or the 5th Monday, and the month being checked is // * February or a 30-day month. // * The following recurrences may never occur: // * - For rMonthlyDay: if the frequency is a whole number of years. // * - For rMonthlyPos: if the frequency is an even whole number of years. // * - For rYearlyDay, rYearlyMonth: if the frequeny is a multiple of 4 years. // * - For rYearlyPos: if the frequency is an even number of years. // * The maximum number of iterations needed, assuming that it does actually occur, // * was found empirically. // */ // switch (recurs) { // case rMonthlyDay: // return (rFreq % 12) ? 6 : 8; // // case rMonthlyPos: // if (rFreq % 12 == 0) { // // Some of these frequencies may never occur // return (rFreq % 84 == 0) ? 364 // frequency = multiple of 7 years // : (rFreq % 48 == 0) ? 7 // frequency = multiple of 4 years // : (rFreq % 24 == 0) ? 14 : 28; // frequency = multiple of 2 or 1 year // } // // All other frequencies will occur sometime // if (rFreq > 120) // return 364; // frequencies of > 10 years will hit the date limit first // switch (rFreq) { // case 23: return 50; // case 46: return 38; // case 56: return 138; // case 66: return 36; // case 89: return 54; // case 112: return 253; // default: return 25; // most frequencies will need < 25 iterations // } // // case rYearlyMonth: // case rYearlyDay: // return 8; // only 29th Feb or day 366 will need more than one iteration // // case rYearlyPos: // if (rFreq % 7 == 0) // return 364; // frequencies of a multiple of 7 years will hit the date limit first // if (rFreq % 2 == 0) { // // Some of these frequencies may never occur // return (rFreq % 4 == 0) ? 7 : 14; // frequency = even number of years // } // return 28; // } // return 1; // } //@cond PRIVATE void RecurrenceRule::Private::buildConstraints() { mTimedRepetition = 0; mNoByRules = mBySetPos.isEmpty(); mConstraints.clear(); Constraint con(mDateStart.timeZone()); if (mWeekStart > 0) { con.setWeekstart(mWeekStart); } mConstraints.append(con); int c, cend; int i, iend; Constraint::List tmp; #define intConstraint( list, setElement ) \ if ( !list.isEmpty() ) { \ mNoByRules = false; \ iend = list.count(); \ if ( iend == 1 ) { \ for ( c = 0, cend = mConstraints.count(); c < cend; ++c ) { \ mConstraints[c].setElement( list[0] ); \ } \ } else { \ tmp.reserve(mConstraints.count() * iend); \ for ( c = 0, cend = mConstraints.count(); c < cend; ++c ) { \ for ( i = 0; i < iend; ++i ) { \ con = mConstraints[c]; \ con.setElement( list[i] ); \ tmp.append( con ); \ } \ } \ mConstraints = tmp; \ tmp.clear(); \ } \ } intConstraint(mBySeconds, setSecond); intConstraint(mByMinutes, setMinute); intConstraint(mByHours, setHour); intConstraint(mByMonthDays, setDay); intConstraint(mByMonths, setMonth); intConstraint(mByYearDays, setYearday); intConstraint(mByWeekNumbers, setWeeknumber); #undef intConstraint if (!mByDays.isEmpty()) { mNoByRules = false; tmp.reserve(mConstraints.count() * mByDays.count()); for (c = 0, cend = mConstraints.count(); c < cend; ++c) { for (i = 0, iend = mByDays.count(); i < iend; ++i) { con = mConstraints[c]; con.setWeekday(mByDays[i].day()); con.setWeekdaynr(mByDays[i].pos()); tmp.append(con); } } mConstraints = tmp; tmp.clear(); } #define fixConstraint( setElement, value ) \ { \ for ( c = 0, cend = mConstraints.count(); c < cend; ++c ) { \ mConstraints[c].setElement( value ); \ } \ } // Now determine missing values from DTSTART. This can speed up things, // because we have more restrictions and save some loops. // TODO: Does RFC 2445 intend to restrict the weekday in all cases of weekly? if (mPeriod == rWeekly && mByDays.isEmpty()) { fixConstraint(setWeekday, mDateStart.date().dayOfWeek()); } // Really fall through in the cases, because all smaller time intervals are // constrained from dtstart switch (mPeriod) { case rYearly: if (mByDays.isEmpty() && mByWeekNumbers.isEmpty() && mByYearDays.isEmpty() && mByMonths.isEmpty()) { fixConstraint(setMonth, mDateStart.date().month()); } Q_FALLTHROUGH(); case rMonthly: if (mByDays.isEmpty() && mByWeekNumbers.isEmpty() && mByYearDays.isEmpty() && mByMonthDays.isEmpty()) { fixConstraint(setDay, mDateStart.date().day()); } Q_FALLTHROUGH(); case rWeekly: case rDaily: if (mByHours.isEmpty()) { fixConstraint(setHour, mDateStart.time().hour()); } Q_FALLTHROUGH(); case rHourly: if (mByMinutes.isEmpty()) { fixConstraint(setMinute, mDateStart.time().minute()); } Q_FALLTHROUGH(); case rMinutely: if (mBySeconds.isEmpty()) { fixConstraint(setSecond, mDateStart.time().second()); } Q_FALLTHROUGH(); case rSecondly: default: break; } #undef fixConstraint if (mNoByRules) { switch (mPeriod) { case rHourly: mTimedRepetition = mFrequency * 3600; break; case rMinutely: mTimedRepetition = mFrequency * 60; break; case rSecondly: mTimedRepetition = mFrequency; break; default: break; } } else { for (c = 0, cend = mConstraints.count(); c < cend;) { if (mConstraints[c].isConsistent(mPeriod)) { ++c; } else { mConstraints.removeAt(c); --cend; } } } } // Build and cache a list of all occurrences. // Only call buildCache() if mDuration > 0. bool RecurrenceRule::Private::buildCache() const { Q_ASSERT(mDuration > 0); // Build the list of all occurrences of this event (we need that to determine // the end date!) Constraint interval(getNextValidDateInterval(mDateStart, mPeriod)); auto dts = datesForInterval(interval, mPeriod); // Only use dates after the event has started (start date is only included // if it matches) const auto it = strictLowerBound(dts.begin(), dts.end(), mDateStart); if (it != dts.end()) { dts.erase(dts.begin(), it + 1); } // some validity checks to avoid infinite loops (i.e. if we have // done this loop already 10000 times, bail out ) for (int loopnr = 0; loopnr < LOOP_LIMIT && dts.count() < mDuration; ++loopnr) { interval.increase(mPeriod, mFrequency); // The returned date list is already sorted! dts += datesForInterval(interval, mPeriod); } if (dts.count() > mDuration) { // we have picked up more occurrences than necessary, remove them dts.erase(dts.begin() + mDuration, dts.end()); } mCached = true; mCachedDates = dts; // it = dts.begin(); // while ( it != dts.end() ) { // qCDebug(KCALCORE_LOG) << " -=>" << dumpTime(*it); // ++it; // } if (int(dts.count()) == mDuration) { mCachedDateEnd = dts.last(); return true; } else { // The cached date list is incomplete mCachedDateEnd = QDateTime(); mCachedLastDate = interval.intervalDateTime(mPeriod); return false; } } //@endcond bool RecurrenceRule::dateMatchesRules(const QDateTime &kdt) const { QDateTime dt = kdt.toTimeZone(d->mDateStart.timeZone()); for (int i = 0, iend = d->mConstraints.count(); i < iend; ++i) { if (d->mConstraints[i].matches(dt, recurrenceType())) { return true; } } return false; } bool RecurrenceRule::recursOn(const QDate &qd, const QTimeZone &timeZone) const { int i, iend; if (!qd.isValid() || !d->mDateStart.isValid()) { // There can't be recurrences on invalid dates return false; } if (allDay()) { // It's a date-only rule, so it has no time specification. // Therefore ignore 'timeSpec'. if (qd < d->mDateStart.date()) { return false; } // Start date is only included if it really matches QDate endDate; if (d->mDuration >= 0) { endDate = endDt().date(); if (qd > endDate) { return false; } } // The date must be in an appropriate interval (getNextValidDateInterval), // Plus it must match at least one of the constraints bool match = false; for (i = 0, iend = d->mConstraints.count(); i < iend && !match; ++i) { match = d->mConstraints[i].matches(qd, recurrenceType()); } if (!match) { return false; } QDateTime start(qd, QTime(0, 0, 0), timeZone); //d->mDateStart.timeZone()); Constraint interval(d->getNextValidDateInterval(start, recurrenceType())); // Constraint::matches is quite efficient, so first check if it can occur at // all before we calculate all actual dates. if (!interval.matches(qd, recurrenceType())) { return false; } // We really need to obtain the list of dates in this interval, since // otherwise BYSETPOS will not work (i.e. the date will match the interval, // but BYSETPOS selects only one of these matching dates! QDateTime end = start.addDays(1); do { auto dts = d->datesForInterval(interval, recurrenceType()); for (i = 0, iend = dts.count(); i < iend; ++i) { if (dts[i].date() >= qd) { return dts[i].date() == qd; } } interval.increase(recurrenceType(), frequency()); } while (interval.intervalDateTime(recurrenceType()) < end); return false; } // It's a date-time rule, so we need to take the time specification into account. QDateTime start(qd, QTime(0, 0, 0), timeZone); QDateTime end = start.addDays(1).toTimeZone(d->mDateStart.timeZone()); start = start.toTimeZone(d->mDateStart.timeZone()); if (end < d->mDateStart) { return false; } if (start < d->mDateStart) { start = d->mDateStart; } // Start date is only included if it really matches if (d->mDuration >= 0) { QDateTime endRecur = endDt(); if (endRecur.isValid()) { if (start > endRecur) { return false; } if (end > endRecur) { end = endRecur; // limit end-of-day time to end of recurrence rule } } } if (d->mTimedRepetition) { // It's a simple sub-daily recurrence with no constraints int n = static_cast((d->mDateStart.secsTo(start) - 1) % d->mTimedRepetition); return start.addSecs(d->mTimedRepetition - n) < end; } // Find the start and end dates in the time spec for the rule QDate startDay = start.date(); QDate endDay = end.addSecs(-1).date(); int dayCount = startDay.daysTo(endDay) + 1; // The date must be in an appropriate interval (getNextValidDateInterval), // Plus it must match at least one of the constraints bool match = false; for (i = 0, iend = d->mConstraints.count(); i < iend && !match; ++i) { match = d->mConstraints[i].matches(startDay, recurrenceType()); for (int day = 1; day < dayCount && !match; ++day) { match = d->mConstraints[i].matches(startDay.addDays(day), recurrenceType()); } } if (!match) { return false; } Constraint interval(d->getNextValidDateInterval(start, recurrenceType())); // Constraint::matches is quite efficient, so first check if it can occur at // all before we calculate all actual dates. Constraint intervalm = interval; do { match = intervalm.matches(startDay, recurrenceType()); for (int day = 1; day < dayCount && !match; ++day) { match = intervalm.matches(startDay.addDays(day), recurrenceType()); } if (match) { break; } intervalm.increase(recurrenceType(), frequency()); } while (intervalm.intervalDateTime(recurrenceType()).isValid() && intervalm.intervalDateTime(recurrenceType()) < end); if (!match) { return false; } // We really need to obtain the list of dates in this interval, since // otherwise BYSETPOS will not work (i.e. the date will match the interval, // but BYSETPOS selects only one of these matching dates! do { auto dts = d->datesForInterval(interval, recurrenceType()); const auto it = std::lower_bound(dts.constBegin(), dts.constEnd(), start); if (it != dts.constEnd()) { return *it <= end; } interval.increase(recurrenceType(), frequency()); } while (interval.intervalDateTime(recurrenceType()).isValid() && interval.intervalDateTime(recurrenceType()) < end); return false; } bool RecurrenceRule::recursAt(const QDateTime &kdt) const { // Convert to the time spec used by this recurrence rule QDateTime dt(kdt.toTimeZone(d->mDateStart.timeZone())); if (allDay()) { return recursOn(dt.date(), dt.timeZone()); } if (dt < d->mDateStart) { return false; } // Start date is only included if it really matches if (d->mDuration >= 0 && dt > endDt()) { return false; } if (d->mTimedRepetition) { // It's a simple sub-daily recurrence with no constraints return !(d->mDateStart.secsTo(dt) % d->mTimedRepetition); } // The date must be in an appropriate interval (getNextValidDateInterval), // Plus it must match at least one of the constraints if (!dateMatchesRules(dt)) { return false; } // if it recurs every interval, speed things up... // if ( d->mFrequency == 1 && d->mBySetPos.isEmpty() && d->mByDays.isEmpty() ) return true; Constraint interval(d->getNextValidDateInterval(dt, recurrenceType())); // TODO_Recurrence: Does this work with BySetPos??? if (interval.matches(dt, recurrenceType())) { return true; } return false; } TimeList RecurrenceRule::recurTimesOn(const QDate &date, const QTimeZone &timeZone) const { TimeList lst; if (allDay()) { return lst; } QDateTime start(date, QTime(0, 0, 0), timeZone); QDateTime end = start.addDays(1).addSecs(-1); auto dts = timesInInterval(start, end); // returns between start and end inclusive for (int i = 0, iend = dts.count(); i < iend; ++i) { lst += dts[i].toTimeZone(timeZone).time(); } return lst; } /** Returns the number of recurrences up to and including the date/time specified. */ int RecurrenceRule::durationTo(const QDateTime &dt) const { // Convert to the time spec used by this recurrence rule QDateTime toDate(dt.toTimeZone(d->mDateStart.timeZone())); // Easy cases: // either before start, or after all recurrences and we know their number if (toDate < d->mDateStart) { return 0; } // Start date is only included if it really matches if (d->mDuration > 0 && toDate >= endDt()) { return d->mDuration; } if (d->mTimedRepetition) { // It's a simple sub-daily recurrence with no constraints return static_cast(d->mDateStart.secsTo(toDate) / d->mTimedRepetition); } return timesInInterval(d->mDateStart, toDate).count(); } int RecurrenceRule::durationTo(const QDate &date) const { return durationTo(QDateTime(date, QTime(23, 59, 59), d->mDateStart.timeZone())); } QDateTime RecurrenceRule::getPreviousDate(const QDateTime &afterDate) const { // Convert to the time spec used by this recurrence rule QDateTime toDate(afterDate.toTimeZone(d->mDateStart.timeZone())); // Invalid starting point, or beyond end of recurrence if (!toDate.isValid() || toDate < d->mDateStart) { return QDateTime(); } if (d->mTimedRepetition) { // It's a simple sub-daily recurrence with no constraints QDateTime prev = toDate; if (d->mDuration >= 0 && endDt().isValid() && toDate > endDt()) { prev = endDt().addSecs(1).toTimeZone(d->mDateStart.timeZone()); } int n = static_cast((d->mDateStart.secsTo(prev) - 1) % d->mTimedRepetition); if (n < 0) { return QDateTime(); // before recurrence start } prev = prev.addSecs(-n - 1); return prev >= d->mDateStart ? prev : QDateTime(); } // If we have a cache (duration given), use that if (d->mDuration > 0) { if (!d->mCached) { d->buildCache(); } const auto it = strictLowerBound(d->mCachedDates.constBegin(), d->mCachedDates.constEnd(), toDate); if (it != d->mCachedDates.constEnd()) { return *it; } return QDateTime(); } QDateTime prev = toDate; if (d->mDuration >= 0 && endDt().isValid() && toDate > endDt()) { prev = endDt().addSecs(1).toTimeZone(d->mDateStart.timeZone()); } Constraint interval(d->getPreviousValidDateInterval(prev, recurrenceType())); const auto dts = d->datesForInterval(interval, recurrenceType()); const auto it = strictLowerBound(dts.begin(), dts.end(), prev); if (it != dts.end()) { return ((*it) >= d->mDateStart) ? (*it) : QDateTime(); } // Previous interval. As soon as we find an occurrence, we're done. while (interval.intervalDateTime(recurrenceType()) > d->mDateStart) { interval.increase(recurrenceType(), -int(frequency())); // The returned date list is sorted auto dts = d->datesForInterval(interval, recurrenceType()); // The list is sorted, so take the last one. if (!dts.isEmpty()) { prev = dts.last(); if (prev.isValid() && prev >= d->mDateStart) { return prev; } else { return QDateTime(); } } } return QDateTime(); } QDateTime RecurrenceRule::getNextDate(const QDateTime &preDate) const { // Convert to the time spec used by this recurrence rule QDateTime fromDate(preDate.toTimeZone(d->mDateStart.timeZone())); // Beyond end of recurrence if (d->mDuration >= 0 && endDt().isValid() && fromDate >= endDt()) { return QDateTime(); } // Start date is only included if it really matches if (fromDate < d->mDateStart) { fromDate = d->mDateStart.addSecs(-1); } if (d->mTimedRepetition) { // It's a simple sub-daily recurrence with no constraints int n = static_cast((d->mDateStart.secsTo(fromDate) + 1) % d->mTimedRepetition); QDateTime next = fromDate.addSecs(d->mTimedRepetition - n + 1); return d->mDuration < 0 || !endDt().isValid() || next <= endDt() ? next : QDateTime(); } if (d->mDuration > 0) { if (!d->mCached) { d->buildCache(); } const auto it = std::upper_bound(d->mCachedDates.constBegin(), d->mCachedDates.constEnd(), fromDate); if (it != d->mCachedDates.constEnd()) { return *it; } } QDateTime end = endDt(); Constraint interval(d->getNextValidDateInterval(fromDate, recurrenceType())); const auto dts = d->datesForInterval(interval, recurrenceType()); const auto it = std::upper_bound(dts.begin(), dts.end(), fromDate); if (it != dts.end()) { return (d->mDuration < 0 || *it <= end) ? *it : QDateTime(); } interval.increase(recurrenceType(), frequency()); if (d->mDuration >= 0 && interval.intervalDateTime(recurrenceType()) > end) { return QDateTime(); } // Increase the interval. The first occurrence that we find is the result (if // if's before the end date). // TODO: some validity checks to avoid infinite loops for contradictory constraints int loop = 0; do { auto dts = d->datesForInterval(interval, recurrenceType()); if (!dts.isEmpty()) { QDateTime ret(dts[0]); if (d->mDuration >= 0 && ret > end) { return QDateTime(); } else { return ret; } } interval.increase(recurrenceType(), frequency()); } while (++loop < LOOP_LIMIT && (d->mDuration < 0 || interval.intervalDateTime(recurrenceType()) < end)); return QDateTime(); } QList RecurrenceRule::timesInInterval(const QDateTime &dtStart, const QDateTime &dtEnd) const { const QDateTime start = dtStart.toTimeZone(d->mDateStart.timeZone()); const QDateTime end = dtEnd.toTimeZone(d->mDateStart.timeZone()); QList result; if (end < d->mDateStart) { return result; // before start of recurrence } QDateTime enddt = end; if (d->mDuration >= 0) { const QDateTime endRecur = endDt(); if (endRecur.isValid()) { if (start > endRecur) { return result; // beyond end of recurrence } if (end >= endRecur) { enddt = endRecur; // limit end time to end of recurrence rule } } } if (d->mTimedRepetition) { // It's a simple sub-daily recurrence with no constraints //Seconds to add to interval start, to get first occurrence which is within interval qint64 offsetFromNextOccurrence; if (d->mDateStart < start) { offsetFromNextOccurrence = d->mTimedRepetition - (d->mDateStart.secsTo(start) % d->mTimedRepetition); } else { offsetFromNextOccurrence = -(d->mDateStart.secsTo(start) % d->mTimedRepetition); } QDateTime dt = start.addSecs(offsetFromNextOccurrence); if (dt <= enddt) { int numberOfOccurrencesWithinInterval = static_cast(dt.secsTo(enddt) / d->mTimedRepetition) + 1; // limit n by a sane value else we can "explode". numberOfOccurrencesWithinInterval = qMin(numberOfOccurrencesWithinInterval, LOOP_LIMIT); for (int i = 0; i < numberOfOccurrencesWithinInterval; dt = dt.addSecs(d->mTimedRepetition), ++i) { result += dt; } } return result; } QDateTime st = start; bool done = false; if (d->mDuration > 0) { if (!d->mCached) { d->buildCache(); } if (d->mCachedDateEnd.isValid() && start > d->mCachedDateEnd) { return result; // beyond end of recurrence } const auto it = std::lower_bound(d->mCachedDates.constBegin(), d->mCachedDates.constEnd(), start); if (it != d->mCachedDates.constEnd()) { const auto itEnd = std::upper_bound(it, d->mCachedDates.constEnd(), enddt); if (itEnd != d->mCachedDates.constEnd()) { done = true; } std::copy(it, itEnd, std::back_inserter(result)); } if (d->mCachedDateEnd.isValid()) { done = true; } else if (!result.isEmpty()) { result += QDateTime(); // indicate that the returned list is incomplete done = true; } if (done) { return result; } // We don't have any result yet, but we reached the end of the incomplete cache st = d->mCachedLastDate.addSecs(1); } Constraint interval(d->getNextValidDateInterval(st, recurrenceType())); int loop = 0; do { auto dts = d->datesForInterval(interval, recurrenceType()); auto it = dts.begin(); auto itEnd = dts.end(); if (loop == 0) { it = std::lower_bound(dts.begin(), dts.end(), st); } itEnd = std::upper_bound(it, dts.end(), enddt); if (itEnd != dts.end()) { loop = LOOP_LIMIT; } std::copy(it, itEnd, std::back_inserter(result)); // Increase the interval. interval.increase(recurrenceType(), frequency()); } while (++loop < LOOP_LIMIT && interval.intervalDateTime(recurrenceType()) < end); return result; } //@cond PRIVATE // Find the date/time of the occurrence at or before a date/time, // for a given period type. // Return a constraint whose value appropriate to 'type', is set to // the value contained in the date/time. Constraint RecurrenceRule::Private::getPreviousValidDateInterval(const QDateTime &dt, PeriodType type) const { long periods = 0; QDateTime start = mDateStart; QDateTime nextValid(start); int modifier = 1; QDateTime toDate(dt.toTimeZone(start.timeZone())); // for super-daily recurrences, don't care about the time part // Find the #intervals since the dtstart and round to the next multiple of // the frequency switch (type) { // Really fall through for sub-daily, since the calculations only differ // by the factor 60 and 60*60! Same for weekly and daily (factor 7) case rHourly: modifier *= 60; Q_FALLTHROUGH(); case rMinutely: modifier *= 60; Q_FALLTHROUGH(); case rSecondly: periods = static_cast(start.secsTo(toDate) / modifier); // round it down to the next lower multiple of frequency: if (mFrequency > 0) { periods = (periods / mFrequency) * mFrequency; } nextValid = start.addSecs(modifier * periods); break; case rWeekly: toDate = toDate.addDays(-(7 + toDate.date().dayOfWeek() - mWeekStart) % 7); start = start.addDays(-(7 + start.date().dayOfWeek() - mWeekStart) % 7); modifier *= 7; Q_FALLTHROUGH(); case rDaily: periods = start.daysTo(toDate) / modifier; // round it down to the next lower multiple of frequency: if (mFrequency > 0) { periods = (periods / mFrequency) * mFrequency; } nextValid = start.addDays(modifier * periods); break; case rMonthly: { periods = 12 * (toDate.date().year() - start.date().year()) + (toDate.date().month() - start.date().month()); // round it down to the next lower multiple of frequency: if (mFrequency > 0) { periods = (periods / mFrequency) * mFrequency; } // set the day to the first day of the month, so we don't have problems // with non-existent days like Feb 30 or April 31 start.setDate(QDate(start.date().year(), start.date().month(), 1)); nextValid.setDate(start.date().addMonths(periods)); break; } case rYearly: periods = (toDate.date().year() - start.date().year()); // round it down to the next lower multiple of frequency: if (mFrequency > 0) { periods = (periods / mFrequency) * mFrequency; } nextValid.setDate(start.date().addYears(periods)); break; default: break; } return Constraint(nextValid, type, mWeekStart); } // Find the date/time of the next occurrence at or after a date/time, // for a given period type. // Return a constraint whose value appropriate to 'type', is set to the // value contained in the date/time. Constraint RecurrenceRule::Private::getNextValidDateInterval(const QDateTime &dt, PeriodType type) const { // TODO: Simplify this! long periods = 0; QDateTime start = mDateStart; QDateTime nextValid(start); int modifier = 1; QDateTime toDate(dt.toTimeZone(start.timeZone())); // for super-daily recurrences, don't care about the time part // Find the #intervals since the dtstart and round to the next multiple of // the frequency switch (type) { // Really fall through for sub-daily, since the calculations only differ // by the factor 60 and 60*60! Same for weekly and daily (factor 7) case rHourly: modifier *= 60; Q_FALLTHROUGH(); case rMinutely: modifier *= 60; Q_FALLTHROUGH(); case rSecondly: periods = static_cast(start.secsTo(toDate) / modifier); periods = qMax(0L, periods); if (periods > 0 && mFrequency > 0) { periods += (mFrequency - 1 - ((periods - 1) % mFrequency)); } nextValid = start.addSecs(modifier * periods); break; case rWeekly: // correct both start date and current date to start of week toDate = toDate.addDays(-(7 + toDate.date().dayOfWeek() - mWeekStart) % 7); start = start.addDays(-(7 + start.date().dayOfWeek() - mWeekStart) % 7); modifier *= 7; Q_FALLTHROUGH(); case rDaily: periods = start.daysTo(toDate) / modifier; periods = qMax(0L, periods); if (periods > 0 && mFrequency > 0) { periods += (mFrequency - 1 - ((periods - 1) % mFrequency)); } nextValid = start.addDays(modifier * periods); break; case rMonthly: { periods = 12 * (toDate.date().year() - start.date().year()) + (toDate.date().month() - start.date().month()); periods = qMax(0L, periods); if (periods > 0 && mFrequency > 0) { periods += (mFrequency - 1 - ((periods - 1) % mFrequency)); } // set the day to the first day of the month, so we don't have problems // with non-existent days like Feb 30 or April 31 start.setDate(QDate(start.date().year(), start.date().month(), 1)); nextValid.setDate(start.date().addMonths(periods)); break; } case rYearly: periods = (toDate.date().year() - start.date().year()); periods = qMax(0L, periods); if (periods > 0 && mFrequency > 0) { periods += (mFrequency - 1 - ((periods - 1) % mFrequency)); } nextValid.setDate(start.date().addYears(periods)); break; default: break; } return Constraint(nextValid, type, mWeekStart); } QList RecurrenceRule::Private::datesForInterval(const Constraint &interval, PeriodType type) const { /* -) Loop through constraints, -) merge interval with each constraint -) if merged constraint is not consistent => ignore that constraint -) if complete => add that one date to the date list -) Loop through all missing fields => For each add the resulting */ QList lst; for (int i = 0, iend = mConstraints.count(); i < iend; ++i) { Constraint merged(interval); if (merged.merge(mConstraints[i])) { // If the information is incomplete, we can't use this constraint if (merged.year > 0 && merged.hour >= 0 && merged.minute >= 0 && merged.second >= 0) { // We have a valid constraint, so get all datetimes that match it andd // append it to all date/times of this interval QList lstnew = merged.dateTimes(type); lst += lstnew; } } } // Sort it so we can apply the BySetPos. Also some logic relies on this being sorted sortAndRemoveDuplicates(lst); /*if ( lst.isEmpty() ) { qCDebug(KCALCORE_LOG) << " No Dates in Interval"; } else { qCDebug(KCALCORE_LOG) << " Dates:"; for ( int i = 0, iend = lst.count(); i < iend; ++i ) { qCDebug(KCALCORE_LOG)<< " -)" << dumpTime(lst[i]); } qCDebug(KCALCORE_LOG) << " ---------------------"; }*/ if (!mBySetPos.isEmpty()) { auto tmplst = lst; lst.clear(); for (int i = 0, iend = mBySetPos.count(); i < iend; ++i) { int pos = mBySetPos[i]; if (pos > 0) { --pos; } if (pos < 0) { pos += tmplst.count(); } if (pos >= 0 && pos < tmplst.count()) { lst.append(tmplst[pos]); } } sortAndRemoveDuplicates(lst); } return lst; } //@endcond void RecurrenceRule::dump() const { #ifndef NDEBUG if (!d->mRRule.isEmpty()) { qCDebug(KCALCORE_LOG) << " RRULE=" << d->mRRule; } qCDebug(KCALCORE_LOG) << " Read-Only:" << isReadOnly(); qCDebug(KCALCORE_LOG) << " Period type:" << int(recurrenceType()) << ", frequency:" << frequency(); qCDebug(KCALCORE_LOG) << " #occurrences:" << duration(); qCDebug(KCALCORE_LOG) << " start date:" << dumpTime(startDt(), allDay()) << ", end date:" << dumpTime(endDt(), allDay()); #define dumpByIntList(list,label) \ if ( !list.isEmpty() ) {\ QStringList lst;\ for ( int i = 0, iend = list.count(); i < iend; ++i ) {\ lst.append( QString::number( list[i] ) );\ }\ qCDebug(KCALCORE_LOG) << " " << label << lst.join( QStringLiteral(", ") );\ } dumpByIntList(d->mBySeconds, QStringLiteral("BySeconds: ")); dumpByIntList(d->mByMinutes, QStringLiteral("ByMinutes: ")); dumpByIntList(d->mByHours, QStringLiteral("ByHours: ")); if (!d->mByDays.isEmpty()) { QStringList lst; for (int i = 0, iend = d->mByDays.count(); i < iend; ++i) { lst.append((d->mByDays[i].pos() ? QString::number(d->mByDays[i].pos()) : QLatin1String("")) + DateHelper::dayName(d->mByDays[i].day())); } qCDebug(KCALCORE_LOG) << " ByDays: " << lst.join(QStringLiteral(", ")); } dumpByIntList(d->mByMonthDays, QStringLiteral("ByMonthDays:")); dumpByIntList(d->mByYearDays, QStringLiteral("ByYearDays: ")); dumpByIntList(d->mByWeekNumbers, QStringLiteral("ByWeekNr: ")); dumpByIntList(d->mByMonths, QStringLiteral("ByMonths: ")); dumpByIntList(d->mBySetPos, QStringLiteral("BySetPos: ")); #undef dumpByIntList qCDebug(KCALCORE_LOG) << " Week start:" << DateHelper::dayName(d->mWeekStart); qCDebug(KCALCORE_LOG) << " Constraints:"; // dump constraints for (int i = 0, iend = d->mConstraints.count(); i < iend; ++i) { d->mConstraints[i].dump(); } #endif } //@cond PRIVATE void Constraint::dump() const { qCDebug(KCALCORE_LOG) << " ~> Y=" << year << ", M=" << month << ", D=" << day << ", H=" << hour << ", m=" << minute << ", S=" << second << ", wd=" << weekday << ",#wd=" << weekdaynr << ", #w=" << weeknumber << ", yd=" << yearday; } //@endcond QString dumpTime(const QDateTime &dt, bool isAllDay) { #ifndef NDEBUG if (!dt.isValid()) { return QString(); } QString result; if (isAllDay) { result = dt.toString(QStringLiteral("ddd yyyy-MM-dd t")); } else { result = dt.toString(QStringLiteral("ddd yyyy-MM-dd hh:mm:ss t")); } return result; #else Q_UNUSED(dt); Q_UNUSED(isAllDay); return QString(); #endif } QDateTime RecurrenceRule::startDt() const { return d->mDateStart; } RecurrenceRule::PeriodType RecurrenceRule::recurrenceType() const { return d->mPeriod; } uint RecurrenceRule::frequency() const { return d->mFrequency; } int RecurrenceRule::duration() const { return d->mDuration; } QString RecurrenceRule::rrule() const { return d->mRRule; } void RecurrenceRule::setRRule(const QString &rrule) { d->mRRule = rrule; } bool RecurrenceRule::isReadOnly() const { return d->mIsReadOnly; } void RecurrenceRule::setReadOnly(bool readOnly) { d->mIsReadOnly = readOnly; } bool RecurrenceRule::recurs() const { return d->mPeriod != rNone; } bool RecurrenceRule::allDay() const { return d->mAllDay; } const QList &RecurrenceRule::bySeconds() const { return d->mBySeconds; } const QList &RecurrenceRule::byMinutes() const { return d->mByMinutes; } const QList &RecurrenceRule::byHours() const { return d->mByHours; } const QList &RecurrenceRule::byDays() const { return d->mByDays; } const QList &RecurrenceRule::byMonthDays() const { return d->mByMonthDays; } const QList &RecurrenceRule::byYearDays() const { return d->mByYearDays; } const QList &RecurrenceRule::byWeekNumbers() const { return d->mByWeekNumbers; } const QList &RecurrenceRule::byMonths() const { return d->mByMonths; } const QList &RecurrenceRule::bySetPos() const { return d->mBySetPos; } short RecurrenceRule::weekStart() const { return d->mWeekStart; } RecurrenceRule::RuleObserver::~RuleObserver() { } RecurrenceRule::WDayPos::WDayPos(int ps, short dy) : mDay(dy), mPos(ps) { } void RecurrenceRule::WDayPos::setDay(short dy) { mDay = dy; } short RecurrenceRule::WDayPos::day() const { return mDay; } void RecurrenceRule::WDayPos::setPos(int ps) { mPos = ps; } int RecurrenceRule::WDayPos::pos() const { return mPos; } QDataStream &operator<<(QDataStream &out, const Constraint &c) { out << c.year << c.month << c.day << c.hour << c.minute << c.second << c.weekday << c.weekdaynr << c.weeknumber << c.yearday << c.weekstart; serializeQTimeZoneAsSpec(out, c.timeZone); out << false; // for backwards compatibility return out; } QDataStream &operator>>(QDataStream &in, Constraint &c) { bool secondOccurrence; // no longer used in >> c.year >> c.month >> c.day >> c.hour >> c.minute >> c.second >> c.weekday >> c.weekdaynr >> c.weeknumber >> c.yearday >> c.weekstart; deserializeSpecAsQTimeZone(in, c.timeZone); in >> secondOccurrence; return in; } KCALENDARCORE_EXPORT QDataStream &KCalendarCore::operator<<(QDataStream &out, const KCalendarCore::RecurrenceRule::WDayPos &w) { out << w.mDay << w.mPos; return out; } KCALENDARCORE_EXPORT QDataStream &KCalendarCore::operator>>(QDataStream &in, KCalendarCore::RecurrenceRule::WDayPos &w) { in >> w.mDay >> w.mPos; return in; } KCALENDARCORE_EXPORT QDataStream &KCalendarCore::operator<<(QDataStream &out, const KCalendarCore::RecurrenceRule *r) { if (!r) { return out; } RecurrenceRule::Private *d = r->d; out << d->mRRule << static_cast(d->mPeriod); serializeQDateTimeAsKDateTime(out, d->mDateStart); out << d->mFrequency << d->mDuration; serializeQDateTimeAsKDateTime(out, d->mDateEnd); out << d->mBySeconds << d->mByMinutes << d->mByHours << d->mByDays << d->mByMonthDays << d->mByYearDays << d->mByWeekNumbers << d->mByMonths << d->mBySetPos << d->mWeekStart << d->mConstraints << d->mAllDay << d->mNoByRules << d->mTimedRepetition << d->mIsReadOnly; return out; } KCALENDARCORE_EXPORT QDataStream &KCalendarCore::operator>>(QDataStream &in, const KCalendarCore::RecurrenceRule *r) { if (!r) { return in; } RecurrenceRule::Private *d = r->d; quint32 period; in >> d->mRRule >> period; deserializeKDateTimeAsQDateTime(in, d->mDateStart); in >> d->mFrequency >> d->mDuration; deserializeKDateTimeAsQDateTime(in, d->mDateEnd); in >> d->mBySeconds >> d->mByMinutes >> d->mByHours >> d->mByDays >> d->mByMonthDays >> d->mByYearDays >> d->mByWeekNumbers >> d->mByMonths >> d->mBySetPos >> d->mWeekStart >> d->mConstraints >> d->mAllDay >> d->mNoByRules >> d->mTimedRepetition >> d->mIsReadOnly; d->mPeriod = static_cast(period); return in; } diff --git a/src/todo.cpp b/src/todo.cpp index bb8c6c647..92ba30437 100644 --- a/src/todo.cpp +++ b/src/todo.cpp @@ -1,577 +1,577 @@ /* 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_p.h" -#include "kcalcore_debug.h" +#include "kcalendarcore_debug.h" #include using namespace KCalendarCore; /** Private class that helps to provide binary compatibility between releases. @internal */ //@cond PRIVATE class Q_DECL_HIDDEN KCalendarCore::Todo::Private { public: Private() {} Private(const KCalendarCore::Todo::Private &other) { init(other); } void init(const KCalendarCore::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 KCalendarCore::Todo::Private::init(const KCalendarCore::Todo::Private &other) { mDtDue = other.mDtDue; mDtRecurrence = other.mDtRecurrence; mCompleted = other.mCompleted; mPercentComplete = other.mPercentComplete; } //@endcond Todo::Todo() : d(new KCalendarCore::Todo::Private) { } Todo::Todo(const Todo &other) : Incidence(other), d(new KCalendarCore::Todo::Private(*other.d)) { } Todo::Todo(const Incidence &other) : Incidence(other) , d(new KCalendarCore::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) { Q_UNUSED(id); Q_UNUSED(data); } 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 { 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) const { 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; } diff --git a/src/vcalformat.cpp b/src/vcalformat.cpp index 212fd040f..a6007fff9 100644 --- a/src/vcalformat.cpp +++ b/src/vcalformat.cpp @@ -1,1727 +1,1727 @@ /* This file is part of the kcalcore library. Copyright (c) 1998 Preston Brown Copyright (c) 2001 Cornelius Schumacher 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 VCalFormat base class. This class implements the vCalendar format. It provides methods for loading/saving/converting vCalendar format data into the internal representation as Calendar and Incidences. @brief vCalendar format implementation. @author Preston Brown \ @author Cornelius Schumacher \ */ #include "vcalformat.h" #include "calendar.h" #include "event.h" #include "exceptions.h" #include "todo.h" -#include "kcalcore_debug.h" +#include "kcalendarcore_debug.h" extern "C" { #include #include } #include #include #include // for .toHtmlEscaped() and Qt::mightBeRichText() #include using namespace KCalendarCore; /** Private class that helps to provide binary compatibility between releases. @internal */ //@cond PRIVATE template void removeAllVCal(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, "removeAllVCal", "Count is not 1."); return; } c.remove(c.indexOf(x)); } class Q_DECL_HIDDEN KCalendarCore::VCalFormat::Private { public: Calendar::Ptr mCalendar; Event::List mEventsRelate; // Events with relations Todo::List mTodosRelate; // To-dos with relations QSet mManuallyWrittenExtensionFields; // X- fields that are manually dumped }; //@endcond VCalFormat::VCalFormat() : d(new KCalendarCore::VCalFormat::Private) { } VCalFormat::~VCalFormat() { delete d; } bool VCalFormat::load(const Calendar::Ptr &calendar, const QString &fileName) { d->mCalendar = calendar; clearException(); // this is not necessarily only 1 vcal. Could be many vcals, or include // a vcard... VObject *vcal = Parse_MIME_FromFileName(const_cast(QFile::encodeName(fileName).data())); if (!vcal) { setException(new Exception(Exception::CalVersionUnknown)); return false; } // any other top-level calendar stuff should be added/initialized here // put all vobjects into their proper places auto savedTimeZoneId = d->mCalendar->timeZoneId(); populate(vcal, false, fileName); d->mCalendar->setTimeZoneId(savedTimeZoneId); // clean up from vcal API stuff cleanVObjects(vcal); cleanStrTbl(); return true; } bool VCalFormat::save(const Calendar::Ptr &calendar, const QString &fileName) { Q_UNUSED(calendar); Q_UNUSED(fileName); qCWarning(KCALCORE_LOG) << "Saving VCAL is not supported"; return false; } bool VCalFormat::fromString(const Calendar::Ptr &calendar, const QString &string, bool deleted, const QString ¬ebook) { return fromRawString(calendar, string.toUtf8(), deleted, notebook); } bool VCalFormat::fromRawString(const Calendar::Ptr &calendar, const QByteArray &string, bool deleted, const QString ¬ebook) { d->mCalendar = calendar; if (!string.size()) { return false; } VObject *vcal = Parse_MIME(string.data(), string.size()); if (!vcal) { return false; } VObjectIterator i; initPropIterator(&i, vcal); // put all vobjects into their proper places auto savedTimeZoneId = d->mCalendar->timeZoneId(); populate(vcal, deleted, notebook); d->mCalendar->setTimeZoneId(savedTimeZoneId); // clean up from vcal API stuff cleanVObjects(vcal); cleanStrTbl(); return true; } QString VCalFormat::toString(const Calendar::Ptr &calendar, const QString ¬ebook, bool deleted) { Q_UNUSED(calendar); Q_UNUSED(notebook); Q_UNUSED(deleted); qCWarning(KCALCORE_LOG) << "Exporting into VCAL is not supported"; return {}; } Todo::Ptr VCalFormat::VTodoToEvent(VObject *vtodo) { VObject *vo = nullptr; VObjectIterator voi; char *s = nullptr; Todo::Ptr anEvent(new Todo); // creation date if ((vo = isAPropertyOf(vtodo, VCDCreatedProp)) != nullptr) { anEvent->setCreated(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo))))); deleteStr(s); } // unique id vo = isAPropertyOf(vtodo, VCUniqueStringProp); // while the UID property is preferred, it is not required. We'll use the // default Event UID if none is given. if (vo) { anEvent->setUid(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo)))); deleteStr(s); } // last modification date if ((vo = isAPropertyOf(vtodo, VCLastModifiedProp)) != nullptr) { anEvent->setLastModified(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo))))); deleteStr(s); } else { anEvent->setLastModified(QDateTime::currentDateTimeUtc()); } // organizer // if our extension property for the event's ORGANIZER exists, add it. if ((vo = isAPropertyOf(vtodo, ICOrganizerProp)) != nullptr) { anEvent->setOrganizer(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo)))); deleteStr(s); } else { if (d->mCalendar->owner().name() != QLatin1String("Unknown Name")) { anEvent->setOrganizer(d->mCalendar->owner()); } } // attendees. initPropIterator(&voi, vtodo); while (moreIteration(&voi)) { vo = nextVObject(&voi); if (strcmp(vObjectName(vo), VCAttendeeProp) == 0) { Attendee a; VObject *vp; s = fakeCString(vObjectUStringZValue(vo)); QString tmpStr = QString::fromUtf8(s); deleteStr(s); tmpStr = tmpStr.simplified(); int emailPos1; if ((emailPos1 = tmpStr.indexOf(QLatin1Char('<'))) > 0) { // both email address and name int emailPos2 = tmpStr.lastIndexOf(QLatin1Char('>')); a = Attendee(tmpStr.left(emailPos1 - 1), tmpStr.mid(emailPos1 + 1, emailPos2 - (emailPos1 + 1))); } else if (tmpStr.indexOf(QLatin1Char('@')) > 0) { // just an email address a = Attendee(QString(), tmpStr); } else { // just a name // WTF??? Replacing the spaces of a name and using this as email? QString email = tmpStr.replace(QLatin1Char(' '), QLatin1Char('.')); a = Attendee(tmpStr, email); } // is there an RSVP property? if ((vp = isAPropertyOf(vo, VCRSVPProp)) != nullptr) { a.setRSVP(vObjectStringZValue(vp)); } // is there a status property? if ((vp = isAPropertyOf(vo, VCStatusProp)) != nullptr) { a.setStatus(readStatus(vObjectStringZValue(vp))); } // add the attendee anEvent->addAttendee(a); } } // description for todo if ((vo = isAPropertyOf(vtodo, VCDescriptionProp)) != nullptr) { s = fakeCString(vObjectUStringZValue(vo)); anEvent->setDescription(QString::fromUtf8(s), Qt::mightBeRichText(QString::fromUtf8(s))); deleteStr(s); } // summary if ((vo = isAPropertyOf(vtodo, VCSummaryProp))) { s = fakeCString(vObjectUStringZValue(vo)); anEvent->setSummary(QString::fromUtf8(s), Qt::mightBeRichText(QString::fromUtf8(s))); deleteStr(s); } // location if ((vo = isAPropertyOf(vtodo, VCLocationProp)) != nullptr) { s = fakeCString(vObjectUStringZValue(vo)); anEvent->setLocation(QString::fromUtf8(s), Qt::mightBeRichText(QString::fromUtf8(s))); deleteStr(s); } // completed // was: status if ((vo = isAPropertyOf(vtodo, VCStatusProp)) != nullptr) { s = fakeCString(vObjectUStringZValue(vo)); if (s && strcmp(s, "COMPLETED") == 0) { anEvent->setCompleted(true); } else { anEvent->setCompleted(false); } deleteStr(s); } else { anEvent->setCompleted(false); } // completion date if ((vo = isAPropertyOf(vtodo, VCCompletedProp)) != nullptr) { anEvent->setCompleted(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo))))); deleteStr(s); } // priority if ((vo = isAPropertyOf(vtodo, VCPriorityProp))) { s = fakeCString(vObjectUStringZValue(vo)); if (s) { anEvent->setPriority(atoi(s)); deleteStr(s); } } anEvent->setAllDay(false); // due date if ((vo = isAPropertyOf(vtodo, VCDueProp)) != nullptr) { anEvent->setDtDue(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo))))); deleteStr(s); if (anEvent->dtDue().time().hour() == 0 && anEvent->dtDue().time().minute() == 0 && anEvent->dtDue().time().second() == 0) { anEvent->setAllDay(true); } } else { anEvent->setDtDue(QDateTime()); } // start time if ((vo = isAPropertyOf(vtodo, VCDTstartProp)) != nullptr) { anEvent->setDtStart(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo))))); deleteStr(s); if (anEvent->dtStart().time().hour() == 0 && anEvent->dtStart().time().minute() == 0 && anEvent->dtStart().time().second() == 0) { anEvent->setAllDay(true); } } else { anEvent->setDtStart(QDateTime()); } // recurrence stuff if ((vo = isAPropertyOf(vtodo, VCRRuleProp)) != nullptr) { uint recurrenceType = Recurrence::rNone; int recurrenceTypeAbbrLen = 0; QString tmpStr = (QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo)))); deleteStr(s); tmpStr = tmpStr.simplified(); const int tmpStrLen = tmpStr.length(); if (tmpStrLen > 0) { tmpStr = tmpStr.toUpper(); // first, read the type of the recurrence recurrenceTypeAbbrLen = 1; if (tmpStr.at(0) == QLatin1String("D")) { recurrenceType = Recurrence::rDaily; } else if (tmpStr.at(0) == QLatin1String("W")) { recurrenceType = Recurrence::rWeekly; } else if (tmpStrLen > 1) { recurrenceTypeAbbrLen = 2; if (tmpStr.leftRef(2) == QLatin1String("MP")) { recurrenceType = Recurrence::rMonthlyPos; } else if (tmpStr.leftRef(2) == QLatin1String("MD")) { recurrenceType = Recurrence::rMonthlyDay; } else if (tmpStr.leftRef(2) == QLatin1String("YM")) { recurrenceType = Recurrence::rYearlyMonth; } else if (tmpStr.leftRef(2) == QLatin1String("YD")) { recurrenceType = Recurrence::rYearlyDay; } } } if (recurrenceType != Recurrence::rNone) { // Immediately after the type is the frequency int index = tmpStr.indexOf(QLatin1Char(' ')); int last = tmpStr.lastIndexOf(QLatin1Char(' ')) + 1; // find last entry int rFreq = tmpStr.midRef(recurrenceTypeAbbrLen, (index - 1)).toInt(); ++index; // advance to beginning of stuff after freq // Read the type-specific settings switch (recurrenceType) { case Recurrence::rDaily: anEvent->recurrence()->setDaily(rFreq); break; case Recurrence::rWeekly: { QBitArray qba(7); QString dayStr; if (index == last) { // e.g. W1 #0 qba.setBit(anEvent->dtStart().date().dayOfWeek() - 1); } else { // e.g. W1 SU #0 while (index < last) { dayStr = tmpStr.mid(index, 3); int dayNum = numFromDay(dayStr); if (dayNum >= 0) { qba.setBit(dayNum); } index += 3; // advance to next day, or possibly "#" } } anEvent->recurrence()->setWeekly(rFreq, qba); break; } case Recurrence::rMonthlyPos: { anEvent->recurrence()->setMonthly(rFreq); QBitArray qba(7); short tmpPos; if (index == last) { // e.g. MP1 #0 tmpPos = anEvent->dtStart().date().day() / 7 + 1; if (tmpPos == 5) { tmpPos = -1; } qba.setBit(anEvent->dtStart().date().dayOfWeek() - 1); anEvent->recurrence()->addMonthlyPos(tmpPos, qba); } else { // e.g. MP1 1+ SU #0 while (index < last) { tmpPos = tmpStr.mid(index, 1).toShort(); index += 1; if (tmpStr.mid(index, 1) == QLatin1String("-")) { // convert tmpPos to negative tmpPos = 0 - tmpPos; } index += 2; // advance to day(s) while (numFromDay(tmpStr.mid(index, 3)) >= 0) { int dayNum = numFromDay(tmpStr.mid(index, 3)); qba.setBit(dayNum); index += 3; // advance to next day, or possibly pos or "#" } anEvent->recurrence()->addMonthlyPos(tmpPos, qba); qba.detach(); qba.fill(false); // clear out } // while != "#" } break; } case Recurrence::rMonthlyDay: anEvent->recurrence()->setMonthly(rFreq); if (index == last) { // e.g. MD1 #0 short tmpDay = anEvent->dtStart().date().day(); anEvent->recurrence()->addMonthlyDate(tmpDay); } else { // e.g. MD1 3 #0 while (index < last) { int index2 = tmpStr.indexOf(QLatin1Char(' '), index); if ((tmpStr.mid((index2 - 1), 1) == QLatin1String("-")) || (tmpStr.mid((index2 - 1), 1) == QLatin1String("+"))) { index2 = index2 - 1; } short tmpDay = tmpStr.mid(index, (index2 - index)).toShort(); index = index2; if (tmpStr.mid(index, 1) == QLatin1String("-")) { tmpDay = 0 - tmpDay; } index += 2; // advance the index; anEvent->recurrence()->addMonthlyDate(tmpDay); } // while != # } break; case Recurrence::rYearlyMonth: anEvent->recurrence()->setYearly(rFreq); if (index == last) { // e.g. YM1 #0 short tmpMonth = anEvent->dtStart().date().month(); anEvent->recurrence()->addYearlyMonth(tmpMonth); } else { // e.g. YM1 3 #0 while (index < last) { int index2 = tmpStr.indexOf(QLatin1Char(' '), index); short tmpMonth = tmpStr.mid(index, (index2 - index)).toShort(); index = index2 + 1; anEvent->recurrence()->addYearlyMonth(tmpMonth); } // while != # } break; case Recurrence::rYearlyDay: anEvent->recurrence()->setYearly(rFreq); if (index == last) { // e.g. YD1 #0 short tmpDay = anEvent->dtStart().date().dayOfYear(); anEvent->recurrence()->addYearlyDay(tmpDay); } else { // e.g. YD1 123 #0 while (index < last) { int index2 = tmpStr.indexOf(QLatin1Char(' '), index); short tmpDay = tmpStr.mid(index, (index2 - index)).toShort(); index = index2 + 1; anEvent->recurrence()->addYearlyDay(tmpDay); } // while != # } break; default: break; } // find the last field, which is either the duration or the end date index = last; if (tmpStr.mid(index, 1) == QLatin1String("#")) { // Nr of occurrences index++; int rDuration = tmpStr.midRef(index, tmpStr.length() - index).toInt(); if (rDuration > 0) { anEvent->recurrence()->setDuration(rDuration); } } else if (tmpStr.indexOf(QLatin1Char('T'), index) != -1) { QDateTime rEndDate = ISOToQDateTime(tmpStr.mid(index, tmpStr.length() - index)); anEvent->recurrence()->setEndDateTime(rEndDate); } } else { qCDebug(KCALCORE_LOG) << "we don't understand this type of recurrence!"; } // if known recurrence type } // repeats // recurrence exceptions if ((vo = isAPropertyOf(vtodo, VCExpDateProp)) != nullptr) { s = fakeCString(vObjectUStringZValue(vo)); QStringList exDates = QString::fromUtf8(s).split(QLatin1Char(',')); QStringList::ConstIterator it; for (it = exDates.constBegin(); it != exDates.constEnd(); ++it) { QDateTime exDate = ISOToQDateTime(*it); if (exDate.time().hour() == 0 && exDate.time().minute() == 0 && exDate.time().second() == 0) { anEvent->recurrence()->addExDate(ISOToQDate(*it)); } else { anEvent->recurrence()->addExDateTime(exDate); } } deleteStr(s); } // alarm stuff if ((vo = isAPropertyOf(vtodo, VCDAlarmProp))) { Alarm::Ptr alarm; VObject *a = isAPropertyOf(vo, VCRunTimeProp); VObject *b = isAPropertyOf(vo, VCDisplayStringProp); if (a || b) { alarm = anEvent->newAlarm(); if (a) { alarm->setTime(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(a))))); deleteStr(s); } alarm->setEnabled(true); if (b) { s = fakeCString(vObjectUStringZValue(b)); alarm->setDisplayAlarm(QString::fromUtf8(s)); deleteStr(s); } else { alarm->setDisplayAlarm(QString()); } } } if ((vo = isAPropertyOf(vtodo, VCAAlarmProp))) { Alarm::Ptr alarm; VObject *a; VObject *b; a = isAPropertyOf(vo, VCRunTimeProp); b = isAPropertyOf(vo, VCAudioContentProp); if (a || b) { alarm = anEvent->newAlarm(); if (a) { alarm->setTime(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(a))))); deleteStr(s); } alarm->setEnabled(true); if (b) { s = fakeCString(vObjectUStringZValue(b)); alarm->setAudioAlarm(QFile::decodeName(s)); deleteStr(s); } else { alarm->setAudioAlarm(QString()); } } } if ((vo = isAPropertyOf(vtodo, VCPAlarmProp))) { Alarm::Ptr alarm; VObject *a = isAPropertyOf(vo, VCRunTimeProp); VObject *b = isAPropertyOf(vo, VCProcedureNameProp); if (a || b) { alarm = anEvent->newAlarm(); if (a) { alarm->setTime(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(a))))); deleteStr(s); } alarm->setEnabled(true); if (b) { s = fakeCString(vObjectUStringZValue(b)); alarm->setProcedureAlarm(QFile::decodeName(s)); deleteStr(s); } else { alarm->setProcedureAlarm(QString()); } } } // related todo if ((vo = isAPropertyOf(vtodo, VCRelatedToProp)) != nullptr) { anEvent->setRelatedTo(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo)))); deleteStr(s); d->mTodosRelate.append(anEvent); } // secrecy Incidence::Secrecy secrecy = Incidence::SecrecyPublic; if ((vo = isAPropertyOf(vtodo, VCClassProp)) != nullptr) { s = fakeCString(vObjectUStringZValue(vo)); if (s && strcmp(s, "PRIVATE") == 0) { secrecy = Incidence::SecrecyPrivate; } else if (s && strcmp(s, "CONFIDENTIAL") == 0) { secrecy = Incidence::SecrecyConfidential; } deleteStr(s); } anEvent->setSecrecy(secrecy); // categories if ((vo = isAPropertyOf(vtodo, VCCategoriesProp)) != nullptr) { s = fakeCString(vObjectUStringZValue(vo)); QString categories = QString::fromUtf8(s); deleteStr(s); QStringList tmpStrList = categories.split(QLatin1Char(';')); anEvent->setCategories(tmpStrList); } return anEvent; } Event::Ptr VCalFormat::VEventToEvent(VObject *vevent) { VObject *vo = nullptr; VObjectIterator voi; char *s = nullptr; Event::Ptr anEvent(new Event); // creation date if ((vo = isAPropertyOf(vevent, VCDCreatedProp)) != nullptr) { anEvent->setCreated(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo))))); deleteStr(s); } // unique id vo = isAPropertyOf(vevent, VCUniqueStringProp); // while the UID property is preferred, it is not required. We'll use the // default Event UID if none is given. if (vo) { anEvent->setUid(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo)))); deleteStr(s); } // revision // again NSCAL doesn't give us much to work with, so we improvise... anEvent->setRevision(0); if ((vo = isAPropertyOf(vevent, VCSequenceProp)) != nullptr) { s = fakeCString(vObjectUStringZValue(vo)); if (s) { anEvent->setRevision(atoi(s)); deleteStr(s); } } // last modification date if ((vo = isAPropertyOf(vevent, VCLastModifiedProp)) != nullptr) { anEvent->setLastModified(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo))))); deleteStr(s); } else { anEvent->setLastModified(QDateTime::currentDateTimeUtc()); } // organizer // if our extension property for the event's ORGANIZER exists, add it. if ((vo = isAPropertyOf(vevent, ICOrganizerProp)) != nullptr) { // FIXME: Also use the full name, not just the email address anEvent->setOrganizer(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo)))); deleteStr(s); } else { if (d->mCalendar->owner().name() != QStringLiteral("Unknown Name")) { anEvent->setOrganizer(d->mCalendar->owner()); } } // deal with attendees. initPropIterator(&voi, vevent); while (moreIteration(&voi)) { vo = nextVObject(&voi); if (strcmp(vObjectName(vo), VCAttendeeProp) == 0) { Attendee a; VObject *vp = nullptr; s = fakeCString(vObjectUStringZValue(vo)); QString tmpStr = QString::fromUtf8(s); deleteStr(s); tmpStr = tmpStr.simplified(); int emailPos1; if ((emailPos1 = tmpStr.indexOf(QLatin1Char('<'))) > 0) { // both email address and name int emailPos2 = tmpStr.lastIndexOf(QLatin1Char('>')); a = Attendee(tmpStr.left(emailPos1 - 1), tmpStr.mid(emailPos1 + 1, emailPos2 - (emailPos1 + 1))); } else if (tmpStr.indexOf(QLatin1Char('@')) > 0) { // just an email address a = Attendee(QString(), tmpStr); } else { // just a name QString email = tmpStr.replace(QLatin1Char(' '), QLatin1Char('.')); a = Attendee(tmpStr, email); } // is there an RSVP property? if ((vp = isAPropertyOf(vo, VCRSVPProp)) != nullptr) { a.setRSVP(vObjectStringZValue(vp)); } // is there a status property? if ((vp = isAPropertyOf(vo, VCStatusProp)) != nullptr) { a.setStatus(readStatus(vObjectStringZValue(vp))); } // add the attendee anEvent->addAttendee(a); } } // This isn't strictly true. An event that doesn't have a start time // or an end time isn't all-day, it has an anchor in time but it doesn't // "take up" any time. /*if ((isAPropertyOf(vevent, VCDTstartProp) == 0) || (isAPropertyOf(vevent, VCDTendProp) == 0)) { anEvent->setAllDay(true); } else { }*/ anEvent->setAllDay(false); // start time if ((vo = isAPropertyOf(vevent, VCDTstartProp)) != nullptr) { anEvent->setDtStart(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo))))); deleteStr(s); if (anEvent->dtStart().time().hour() == 0 && anEvent->dtStart().time().minute() == 0 && anEvent->dtStart().time().second() == 0) { anEvent->setAllDay(true); } } // stop time if ((vo = isAPropertyOf(vevent, VCDTendProp)) != nullptr) { anEvent->setDtEnd(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo))))); deleteStr(s); if (anEvent->dtEnd().time().hour() == 0 && anEvent->dtEnd().time().minute() == 0 && anEvent->dtEnd().time().second() == 0) { anEvent->setAllDay(true); } } // at this point, there should be at least a start or end time. // fix up for events that take up no time but have a time associated if (!isAPropertyOf(vevent, VCDTstartProp)) { anEvent->setDtStart(anEvent->dtEnd()); } if (! isAPropertyOf(vevent, VCDTendProp)) { anEvent->setDtEnd(anEvent->dtStart()); } /////////////////////////////////////////////////////////////////////////// // recurrence stuff if ((vo = isAPropertyOf(vevent, VCRRuleProp)) != nullptr) { uint recurrenceType = Recurrence::rNone; int recurrenceTypeAbbrLen = 0; QString tmpStr = (QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo)))); deleteStr(s); tmpStr = tmpStr.simplified(); const int tmpStrLen = tmpStr.length(); if (tmpStrLen > 0) { tmpStr = tmpStr.toUpper(); // first, read the type of the recurrence recurrenceTypeAbbrLen = 1; if (tmpStr.at(0) == QLatin1String("D")) { recurrenceType = Recurrence::rDaily; } else if (tmpStr.at(0) == QLatin1String("W")) { recurrenceType = Recurrence::rWeekly; } else if (tmpStrLen > 1){ recurrenceTypeAbbrLen = 2; if (tmpStr.leftRef(2) == QLatin1String("MP")) { recurrenceType = Recurrence::rMonthlyPos; } else if (tmpStr.leftRef(2) == QLatin1String("MD")) { recurrenceType = Recurrence::rMonthlyDay; } else if (tmpStr.leftRef(2) == QLatin1String("YM")) { recurrenceType = Recurrence::rYearlyMonth; } else if (tmpStr.leftRef(2) == QLatin1String("YD")) { recurrenceType = Recurrence::rYearlyDay; } } } if (recurrenceType != Recurrence::rNone) { // Immediately after the type is the frequency int index = tmpStr.indexOf(QLatin1Char(' ')); int last = tmpStr.lastIndexOf(QLatin1Char(' ')) + 1; // find last entry int rFreq = tmpStr.midRef(recurrenceTypeAbbrLen, (index - 1)).toInt(); ++index; // advance to beginning of stuff after freq // Read the type-specific settings switch (recurrenceType) { case Recurrence::rDaily: anEvent->recurrence()->setDaily(rFreq); break; case Recurrence::rWeekly: { QBitArray qba(7); QString dayStr; if (index == last) { // e.g. W1 #0 qba.setBit(anEvent->dtStart().date().dayOfWeek() - 1); } else { // e.g. W1 SU #0 while (index < last) { dayStr = tmpStr.mid(index, 3); int dayNum = numFromDay(dayStr); if (dayNum >= 0) { qba.setBit(dayNum); } index += 3; // advance to next day, or possibly "#" } } anEvent->recurrence()->setWeekly(rFreq, qba); break; } case Recurrence::rMonthlyPos: { anEvent->recurrence()->setMonthly(rFreq); QBitArray qba(7); short tmpPos; if (index == last) { // e.g. MP1 #0 tmpPos = anEvent->dtStart().date().day() / 7 + 1; if (tmpPos == 5) { tmpPos = -1; } qba.setBit(anEvent->dtStart().date().dayOfWeek() - 1); anEvent->recurrence()->addMonthlyPos(tmpPos, qba); } else { // e.g. MP1 1+ SU #0 while (index < last) { tmpPos = tmpStr.mid(index, 1).toShort(); index += 1; if (tmpStr.mid(index, 1) == QStringLiteral("-")) { // convert tmpPos to negative tmpPos = 0 - tmpPos; } index += 2; // advance to day(s) while (numFromDay(tmpStr.mid(index, 3)) >= 0) { int dayNum = numFromDay(tmpStr.mid(index, 3)); qba.setBit(dayNum); index += 3; // advance to next day, or possibly pos or "#" } anEvent->recurrence()->addMonthlyPos(tmpPos, qba); qba.detach(); qba.fill(false); // clear out } // while != "#" } break; } case Recurrence::rMonthlyDay: anEvent->recurrence()->setMonthly(rFreq); if (index == last) { // e.g. MD1 #0 short tmpDay = anEvent->dtStart().date().day(); anEvent->recurrence()->addMonthlyDate(tmpDay); } else { // e.g. MD1 3 #0 while (index < last) { int index2 = tmpStr.indexOf(QLatin1Char(' '), index); if ((tmpStr.mid((index2 - 1), 1) == QStringLiteral("-")) || (tmpStr.mid((index2 - 1), 1) == QStringLiteral("+"))) { index2 = index2 - 1; } short tmpDay = tmpStr.mid(index, (index2 - index)).toShort(); index = index2; if (tmpStr.mid(index, 1) == QStringLiteral("-")) { tmpDay = 0 - tmpDay; } index += 2; // advance the index; anEvent->recurrence()->addMonthlyDate(tmpDay); } // while != # } break; case Recurrence::rYearlyMonth: anEvent->recurrence()->setYearly(rFreq); if (index == last) { // e.g. YM1 #0 short tmpMonth = anEvent->dtStart().date().month(); anEvent->recurrence()->addYearlyMonth(tmpMonth); } else { // e.g. YM1 3 #0 while (index < last) { int index2 = tmpStr.indexOf(QLatin1Char(' '), index); short tmpMonth = tmpStr.mid(index, (index2 - index)).toShort(); index = index2 + 1; anEvent->recurrence()->addYearlyMonth(tmpMonth); } // while != # } break; case Recurrence::rYearlyDay: anEvent->recurrence()->setYearly(rFreq); if (index == last) { // e.g. YD1 #0 const int tmpDay = anEvent->dtStart().date().dayOfYear(); anEvent->recurrence()->addYearlyDay(tmpDay); } else { // e.g. YD1 123 #0 while (index < last) { int index2 = tmpStr.indexOf(QLatin1Char(' '), index); short tmpDay = tmpStr.mid(index, (index2 - index)).toShort(); index = index2 + 1; anEvent->recurrence()->addYearlyDay(tmpDay); } // while != # } break; default: break; } // find the last field, which is either the duration or the end date index = last; if (tmpStr.mid(index, 1) == QLatin1String("#")) { // Nr of occurrences index++; int rDuration = tmpStr.midRef(index, tmpStr.length() - index).toInt(); if (rDuration > 0) { anEvent->recurrence()->setDuration(rDuration); } } else if (tmpStr.indexOf(QLatin1Char('T'), index) != -1) { QDateTime rEndDate = ISOToQDateTime(tmpStr.mid(index, tmpStr.length() - index)); anEvent->recurrence()->setEndDateTime(rEndDate); } // anEvent->recurrence()->dump(); } else { qCDebug(KCALCORE_LOG) << "we don't understand this type of recurrence!"; } // if known recurrence type } // repeats // recurrence exceptions if ((vo = isAPropertyOf(vevent, VCExpDateProp)) != nullptr) { s = fakeCString(vObjectUStringZValue(vo)); QStringList exDates = QString::fromUtf8(s).split(QLatin1Char(',')); QStringList::ConstIterator it; for (it = exDates.constBegin(); it != exDates.constEnd(); ++it) { QDateTime exDate = ISOToQDateTime(*it); if (exDate.time().hour() == 0 && exDate.time().minute() == 0 && exDate.time().second() == 0) { anEvent->recurrence()->addExDate(ISOToQDate(*it)); } else { anEvent->recurrence()->addExDateTime(exDate); } } deleteStr(s); } // summary if ((vo = isAPropertyOf(vevent, VCSummaryProp))) { s = fakeCString(vObjectUStringZValue(vo)); anEvent->setSummary(QString::fromUtf8(s), Qt::mightBeRichText(QString::fromUtf8(s))); deleteStr(s); } // description if ((vo = isAPropertyOf(vevent, VCDescriptionProp)) != nullptr) { s = fakeCString(vObjectUStringZValue(vo)); bool isRich = Qt::mightBeRichText(QString::fromUtf8(s)); if (!anEvent->description().isEmpty()) { anEvent->setDescription( anEvent->description() + QLatin1Char('\n') + QString::fromUtf8(s), isRich); } else { anEvent->setDescription(QString::fromUtf8(s), isRich); } deleteStr(s); } // location if ((vo = isAPropertyOf(vevent, VCLocationProp)) != nullptr) { s = fakeCString(vObjectUStringZValue(vo)); anEvent->setLocation(QString::fromUtf8(s), Qt::mightBeRichText(QString::fromUtf8(s))); deleteStr(s); } // some stupid vCal exporters ignore the standard and use Description // instead of Summary for the default field. Correct for this. if (anEvent->summary().isEmpty() && !(anEvent->description().isEmpty())) { QString tmpStr = anEvent->description().simplified(); anEvent->setDescription(QString()); anEvent->setSummary(tmpStr); } #if 0 // status if ((vo = isAPropertyOf(vevent, VCStatusProp)) != 0) { QString tmpStr(s = fakeCString(vObjectUStringZValue(vo))); deleteStr(s); // TODO: Define Event status // anEvent->setStatus( tmpStr ); } else { // anEvent->setStatus( "NEEDS ACTION" ); } #endif // secrecy Incidence::Secrecy secrecy = Incidence::SecrecyPublic; if ((vo = isAPropertyOf(vevent, VCClassProp)) != nullptr) { s = fakeCString(vObjectUStringZValue(vo)); if (s && strcmp(s, "PRIVATE") == 0) { secrecy = Incidence::SecrecyPrivate; } else if (s && strcmp(s, "CONFIDENTIAL") == 0) { secrecy = Incidence::SecrecyConfidential; } deleteStr(s); } anEvent->setSecrecy(secrecy); // categories if ((vo = isAPropertyOf(vevent, VCCategoriesProp)) != nullptr) { s = fakeCString(vObjectUStringZValue(vo)); QString categories = QString::fromUtf8(s); deleteStr(s); QStringList tmpStrList = categories.split(QLatin1Char(',')); anEvent->setCategories(tmpStrList); } // attachments initPropIterator(&voi, vevent); while (moreIteration(&voi)) { vo = nextVObject(&voi); if (strcmp(vObjectName(vo), VCAttachProp) == 0) { s = fakeCString(vObjectUStringZValue(vo)); anEvent->addAttachment(Attachment(QString::fromUtf8(s))); deleteStr(s); } } // resources if ((vo = isAPropertyOf(vevent, VCResourcesProp)) != nullptr) { QString resources = (QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo)))); deleteStr(s); QStringList tmpStrList = resources.split(QLatin1Char(';')); anEvent->setResources(tmpStrList); } // alarm stuff if ((vo = isAPropertyOf(vevent, VCDAlarmProp))) { Alarm::Ptr alarm; VObject *a = isAPropertyOf(vo, VCRunTimeProp); VObject *b = isAPropertyOf(vo, VCDisplayStringProp); if (a || b) { alarm = anEvent->newAlarm(); if (a) { alarm->setTime(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(a))))); deleteStr(s); } alarm->setEnabled(true); if (b) { s = fakeCString(vObjectUStringZValue(b)); alarm->setDisplayAlarm(QString::fromUtf8(s)); deleteStr(s); } else { alarm->setDisplayAlarm(QString()); } } } if ((vo = isAPropertyOf(vevent, VCAAlarmProp))) { Alarm::Ptr alarm; VObject *a; VObject *b; a = isAPropertyOf(vo, VCRunTimeProp); b = isAPropertyOf(vo, VCAudioContentProp); if (a || b) { alarm = anEvent->newAlarm(); if (a) { alarm->setTime(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(a))))); deleteStr(s); } alarm->setEnabled(true); if (b) { s = fakeCString(vObjectUStringZValue(b)); alarm->setAudioAlarm(QFile::decodeName(s)); deleteStr(s); } else { alarm->setAudioAlarm(QString()); } } } if ((vo = isAPropertyOf(vevent, VCPAlarmProp))) { Alarm::Ptr alarm; VObject *a; VObject *b; a = isAPropertyOf(vo, VCRunTimeProp); b = isAPropertyOf(vo, VCProcedureNameProp); if (a || b) { alarm = anEvent->newAlarm(); if (a) { alarm->setTime(ISOToQDateTime(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(a))))); deleteStr(s); } alarm->setEnabled(true); if (b) { s = fakeCString(vObjectUStringZValue(b)); alarm->setProcedureAlarm(QFile::decodeName(s)); deleteStr(s); } else { alarm->setProcedureAlarm(QString()); } } } // priority if ((vo = isAPropertyOf(vevent, VCPriorityProp))) { s = fakeCString(vObjectUStringZValue(vo)); if (s) { anEvent->setPriority(atoi(s)); deleteStr(s); } } // transparency if ((vo = isAPropertyOf(vevent, VCTranspProp)) != nullptr) { s = fakeCString(vObjectUStringZValue(vo)); if (s) { int i = atoi(s); anEvent->setTransparency(i == 1 ? Event::Transparent : Event::Opaque); deleteStr(s); } } // related event if ((vo = isAPropertyOf(vevent, VCRelatedToProp)) != nullptr) { anEvent->setRelatedTo(QString::fromUtf8(s = fakeCString(vObjectUStringZValue(vo)))); deleteStr(s); d->mEventsRelate.append(anEvent); } /* Rest of the custom properties */ readCustomProperties(vevent, anEvent); return anEvent; } QString VCalFormat::parseTZ(const QByteArray &timezone) const { // qCDebug(KCALCORE_LOG) << timezone; QString pZone = QString::fromUtf8(timezone.mid(timezone.indexOf("TZID:VCAL") + 9)); return pZone.mid(0, pZone.indexOf(QLatin1Char('\n'))); } QString VCalFormat::parseDst(QByteArray &timezone) const { if (!timezone.contains("BEGIN:DAYLIGHT")) { return QString(); } timezone = timezone.mid(timezone.indexOf("BEGIN:DAYLIGHT")); timezone = timezone.mid(timezone.indexOf("TZNAME:") + 7); QString sStart = QString::fromUtf8(timezone.mid(0, (timezone.indexOf("COMMENT:")))); sStart.chop(2); timezone = timezone.mid(timezone.indexOf("TZOFFSETTO:") + 11); QString sOffset = QString::fromUtf8(timezone.mid(0, (timezone.indexOf("DTSTART:")))); sOffset.chop(2); sOffset.insert(3, QLatin1Char(':')); timezone = timezone.mid(timezone.indexOf("TZNAME:") + 7); QString sEnd = QString::fromUtf8(timezone.mid(0, (timezone.indexOf("COMMENT:")))); sEnd.chop(2); return QStringLiteral("TRUE;") + sOffset + QLatin1Char(';') + sStart + QLatin1Char(';') + sEnd + QLatin1String(";;"); } QString VCalFormat::qDateToISO(const QDate &qd) { if (!qd.isValid()) { return QString(); } return QString::asprintf("%.2d%.2d%.2d", qd.year(), qd.month(), qd.day()); } QString VCalFormat::qDateTimeToISO(const QDateTime &dt, bool zulu) { if (!dt.isValid()) { return QString(); } QDateTime tmpDT; if (zulu) { tmpDT = dt.toUTC(); } else { tmpDT = dt.toTimeZone(d->mCalendar->timeZone()); } QString tmpStr = QString::asprintf("%.2d%.2d%.2dT%.2d%.2d%.2d", tmpDT.date().year(), tmpDT.date().month(), tmpDT.date().day(), tmpDT.time().hour(), tmpDT.time().minute(), tmpDT.time().second()); if (zulu || dt.timeZone() == QTimeZone::utc()) { tmpStr += QLatin1Char('Z'); } return tmpStr; } QDateTime VCalFormat::ISOToQDateTime(const QString &dtStr) { QDate tmpDate; QTime tmpTime; QString tmpStr; int year, month, day, hour, minute, second; tmpStr = dtStr; year = tmpStr.leftRef(4).toInt(); month = tmpStr.midRef(4, 2).toInt(); day = tmpStr.midRef(6, 2).toInt(); hour = tmpStr.midRef(9, 2).toInt(); minute = tmpStr.midRef(11, 2).toInt(); second = tmpStr.midRef(13, 2).toInt(); tmpDate.setDate(year, month, day); tmpTime.setHMS(hour, minute, second); if (tmpDate.isValid() && tmpTime.isValid()) { // correct for GMT if string is in Zulu format if (dtStr.at(dtStr.length() - 1) == QLatin1Char('Z')) { return QDateTime(tmpDate, tmpTime, Qt::UTC); } else { return QDateTime(tmpDate, tmpTime, d->mCalendar->timeZone()); } } else { return QDateTime(); } } QDate VCalFormat::ISOToQDate(const QString &dateStr) { int year, month, day; year = dateStr.leftRef(4).toInt(); month = dateStr.midRef(4, 2).toInt(); day = dateStr.midRef(6, 2).toInt(); return QDate(year, month, day); } bool VCalFormat::parseTZOffsetISO8601(const QString &s, int &result) { // ISO8601 format(s): // +- hh : mm // +- hh mm // +- hh // We also accept broken one without + int mod = 1; int v = 0; QString str = s.trimmed(); int ofs = 0; result = 0; // Check for end if (str.size() <= ofs) { return false; } if (str[ofs] == QLatin1Char('-')) { mod = -1; ofs++; } else if (str[ofs] == QLatin1Char('+')) { ofs++; } if (str.size() <= ofs) { return false; } // Make sure next two values are numbers bool ok; if (str.size() < (ofs + 2)) { return false; } v = str.midRef(ofs, 2).toInt(&ok) * 60; if (!ok) { return false; } ofs += 2; if (str.size() > ofs) { if (str[ofs] == QLatin1Char(':')) { ofs++; } if (str.size() > ofs) { if (str.size() < (ofs + 2)) { return false; } v += str.midRef(ofs, 2).toInt(&ok); if (!ok) { return false; } } } result = v * mod * 60; return true; } // take a raw vcalendar (i.e. from a file on disk, clipboard, etc. etc. // and break it down from it's tree-like format into the dictionary format // that is used internally in the VCalFormat. void VCalFormat::populate(VObject *vcal, bool deleted, const QString ¬ebook) { Q_UNUSED(notebook); // this function will populate the caldict dictionary and other event // lists. It turns vevents into Events and then inserts them. VObjectIterator i; VObject *curVO; Event::Ptr anEvent; bool hasTimeZone = false; //The calendar came with a TZ and not UTC QTimeZone previousZone; //If we add a new TZ we should leave the spec as it was before if ((curVO = isAPropertyOf(vcal, ICMethodProp)) != nullptr) { char *methodType = fakeCString(vObjectUStringZValue(curVO)); // qCDebug(KCALCORE_LOG) << "This calendar is an iTIP transaction of type '" << methodType << "'"; deleteStr(methodType); } // warn the user that we might have trouble reading non-known calendar. if ((curVO = isAPropertyOf(vcal, VCProdIdProp)) != nullptr) { char *s = fakeCString(vObjectUStringZValue(curVO)); if (!s || strcmp(productId().toUtf8().constData(), s) != 0) { qCDebug(KCALCORE_LOG) << "This vCalendar file was not created by KOrganizer or" << "any other product we support. Loading anyway..."; } setLoadedProductId(QString::fromUtf8(s)); deleteStr(s); } // warn the user we might have trouble reading this unknown version. if ((curVO = isAPropertyOf(vcal, VCVersionProp)) != nullptr) { char *s = fakeCString(vObjectUStringZValue(curVO)); if (!s || strcmp(_VCAL_VERSION, s) != 0) { qCDebug(KCALCORE_LOG) << "This vCalendar file has version" << s << "We only support" << _VCAL_VERSION; } deleteStr(s); } // set the time zone (this is a property of the view, so just discard!) if ((curVO = isAPropertyOf(vcal, VCTimeZoneProp)) != nullptr) { char *s = fakeCString(vObjectUStringZValue(curVO)); QString ts = QString::fromUtf8(s); QString name = QStringLiteral("VCAL") + ts; deleteStr(s); // TODO: While using the timezone-offset + vcal as timezone is is // most likely unique, we should REALLY actually create something // like vcal-tzoffset-daylightoffsets, or better yet, // vcal-hash QStringList tzList; QString tz; int utcOffset; int utcOffsetDst; if (parseTZOffsetISO8601(ts, utcOffset)) { // qCDebug(KCALCORE_LOG) << "got standard offset" << ts << utcOffset; // standard from tz // starting date for now 01011900 QDateTime dt = QDateTime(QDateTime(QDate(1900, 1, 1), QTime(0, 0, 0))); tz = QStringLiteral("STD;%1;false;%2").arg(QString::number(utcOffset), dt.toString()); tzList.append(tz); // go through all the daylight tags initPropIterator(&i, vcal); while (moreIteration(&i)) { curVO = nextVObject(&i); if (strcmp(vObjectName(curVO), VCDayLightProp) == 0) { char *s = fakeCString(vObjectUStringZValue(curVO)); QString dst = QLatin1String(s); QStringList argl = dst.split(QLatin1Char(',')); deleteStr(s); // Too short -> not interesting if (argl.size() < 4) { continue; } // We don't care about the non-DST periods if (argl[0] != QLatin1String("TRUE")) { continue; } if (parseTZOffsetISO8601(argl[1], utcOffsetDst)) { // qCDebug(KCALCORE_LOG) << "got DST offset" << argl[1] << utcOffsetDst; // standard QString strEndDate = argl[3]; QDateTime endDate = ISOToQDateTime(strEndDate); // daylight QString strStartDate = argl[2]; QDateTime startDate = ISOToQDateTime(strStartDate); QString strRealEndDate = strEndDate; QString strRealStartDate = strStartDate; QDateTime realEndDate = endDate; QDateTime realStartDate = startDate; // if we get dates for some reason in wrong order, earlier is used for dst if (endDate < startDate) { strRealEndDate = strStartDate; strRealStartDate = strEndDate; realEndDate = startDate; realStartDate = endDate; } tz = QStringLiteral("%1;%2;false;%3"). arg(strRealEndDate, QString::number(utcOffset), realEndDate.toString()); tzList.append(tz); tz = QStringLiteral("%1;%2;true;%3"). arg(strRealStartDate, QString::number(utcOffsetDst), realStartDate.toString()); tzList.append(tz); } else { qCDebug(KCALCORE_LOG) << "unable to parse dst" << argl[1]; } } } if (!QTimeZone::isTimeZoneIdAvailable(name.toLatin1())) { qCDebug(KCALCORE_LOG) << "zone is not valid, parsing error" << tzList; } else { previousZone = d->mCalendar->timeZone(); d->mCalendar->setTimeZoneId(name.toUtf8()); hasTimeZone = true; } } else { qCDebug(KCALCORE_LOG) << "unable to parse tzoffset" << ts; } } // Store all events with a relatedTo property in a list for post-processing d->mEventsRelate.clear(); d->mTodosRelate.clear(); initPropIterator(&i, vcal); // go through all the vobjects in the vcal while (moreIteration(&i)) { curVO = nextVObject(&i); /************************************************************************/ // now, check to see that the object is an event or todo. if (strcmp(vObjectName(curVO), VCEventProp) == 0) { if (!isAPropertyOf(curVO, VCDTstartProp) && !isAPropertyOf(curVO, VCDTendProp)) { qCDebug(KCALCORE_LOG) << "found a VEvent with no DTSTART and no DTEND! Skipping..."; goto SKIP; } anEvent = VEventToEvent(curVO); if (anEvent) { if (hasTimeZone && !anEvent->allDay() && anEvent->dtStart().timeZone() == QTimeZone::utc()) { //This sounds stupid but is how others are doing it, so here //we go. If there is a TZ in the VCALENDAR even if the dtStart //and dtend are in UTC, clients interpret it using also the TZ defined //in the Calendar. I know it sounds braindead but oh well int utcOffSet = anEvent->dtStart().offsetFromUtc(); QDateTime dtStart(anEvent->dtStart().addSecs(utcOffSet)); dtStart.setTimeZone(d->mCalendar->timeZone()); QDateTime dtEnd(anEvent->dtEnd().addSecs(utcOffSet)); dtEnd.setTimeZone(d->mCalendar->timeZone()); anEvent->setDtStart(dtStart); anEvent->setDtEnd(dtEnd); } Event::Ptr old = !anEvent->hasRecurrenceId() ? d->mCalendar->event(anEvent->uid()) : d->mCalendar->event(anEvent->uid(), anEvent->recurrenceId()); if (old) { if (deleted) { d->mCalendar->deleteEvent(old); // move old to deleted removeAllVCal(d->mEventsRelate, old); } else if (anEvent->revision() > old->revision()) { d->mCalendar->deleteEvent(old); // move old to deleted removeAllVCal(d->mEventsRelate, old); d->mCalendar->addEvent(anEvent); // and replace it with this one } } else if (deleted) { old = !anEvent->hasRecurrenceId() ? d->mCalendar->deletedEvent(anEvent->uid()) : d->mCalendar->deletedEvent(anEvent->uid(), anEvent->recurrenceId()); if (!old) { d->mCalendar->addEvent(anEvent); // add this one d->mCalendar->deleteEvent(anEvent); // and move it to deleted } } else { d->mCalendar->addEvent(anEvent); // just add this one } } } else if (strcmp(vObjectName(curVO), VCTodoProp) == 0) { Todo::Ptr aTodo = VTodoToEvent(curVO); if (aTodo) { if (hasTimeZone && !aTodo->allDay() && aTodo->dtStart().timeZone() == QTimeZone::utc()) { //This sounds stupid but is how others are doing it, so here //we go. If there is a TZ in the VCALENDAR even if the dtStart //and dtend are in UTC, clients interpret it usint alse the TZ defined //in the Calendar. I know it sounds braindead but oh well int utcOffSet = aTodo->dtStart().offsetFromUtc(); QDateTime dtStart(aTodo->dtStart().addSecs(utcOffSet)); dtStart.setTimeZone(d->mCalendar->timeZone()); aTodo->setDtStart(dtStart); if (aTodo->hasDueDate()) { QDateTime dtDue(aTodo->dtDue().addSecs(utcOffSet)); dtDue.setTimeZone(d->mCalendar->timeZone()); aTodo->setDtDue(dtDue); } } Todo::Ptr old = !aTodo->hasRecurrenceId() ? d->mCalendar->todo(aTodo->uid()) : d->mCalendar->todo(aTodo->uid(), aTodo->recurrenceId()); if (old) { if (deleted) { d->mCalendar->deleteTodo(old); // move old to deleted removeAllVCal(d->mTodosRelate, old); } else if (aTodo->revision() > old->revision()) { d->mCalendar->deleteTodo(old); // move old to deleted removeAllVCal(d->mTodosRelate, old); d->mCalendar->addTodo(aTodo); // and replace it with this one } } else if (deleted) { old = d->mCalendar->deletedTodo(aTodo->uid(), aTodo->recurrenceId()); if (!old) { d->mCalendar->addTodo(aTodo); // add this one d->mCalendar->deleteTodo(aTodo); // and move it to deleted } } else { d->mCalendar->addTodo(aTodo); // just add this one } } } else if ((strcmp(vObjectName(curVO), VCVersionProp) == 0) || (strcmp(vObjectName(curVO), VCProdIdProp) == 0) || (strcmp(vObjectName(curVO), VCTimeZoneProp) == 0)) { // do nothing, we know these properties and we want to skip them. // we have either already processed them or are ignoring them. ; } else if (strcmp(vObjectName(curVO), VCDayLightProp) == 0) { // do nothing daylights are already processed ; } else { qCDebug(KCALCORE_LOG) << "Ignoring unknown vObject \"" << vObjectName(curVO) << "\""; } SKIP: ; } // while // Post-Process list of events with relations, put Event objects in relation Event::List::ConstIterator eIt; for (eIt = d->mEventsRelate.constBegin(); eIt != d->mEventsRelate.constEnd(); ++eIt) { (*eIt)->setRelatedTo((*eIt)->relatedTo()); } Todo::List::ConstIterator tIt; for (tIt = d->mTodosRelate.constBegin(); tIt != d->mTodosRelate.constEnd(); ++tIt) { (*tIt)->setRelatedTo((*tIt)->relatedTo()); } //Now lets put the TZ back as it was if we have changed it. if (hasTimeZone) { d->mCalendar->setTimeZone(previousZone); } } int VCalFormat::numFromDay(const QString &day) { if (day == QLatin1String("MO ")) { return 0; } if (day == QLatin1String("TU ")) { return 1; } if (day == QLatin1String("WE ")) { return 2; } if (day == QLatin1String("TH ")) { return 3; } if (day == QLatin1String("FR ")) { return 4; } if (day == QLatin1String("SA ")) { return 5; } if (day == QLatin1String("SU ")) { return 6; } return -1; // something bad happened. :) } Attendee::PartStat VCalFormat::readStatus(const char *s) const { QString statStr = QString::fromUtf8(s); statStr = statStr.toUpper(); Attendee::PartStat status; if (statStr == QLatin1String("X-ACTION")) { status = Attendee::NeedsAction; } else if (statStr == QLatin1String("NEEDS ACTION")) { status = Attendee::NeedsAction; } else if (statStr == QLatin1String("ACCEPTED")) { status = Attendee::Accepted; } else if (statStr == QLatin1String("SENT")) { status = Attendee::NeedsAction; } else if (statStr == QLatin1String("TENTATIVE")) { status = Attendee::Tentative; } else if (statStr == QLatin1String("CONFIRMED")) { status = Attendee::Accepted; } else if (statStr == QLatin1String("DECLINED")) { status = Attendee::Declined; } else if (statStr == QLatin1String("COMPLETED")) { status = Attendee::Completed; } else if (statStr == QLatin1String("DELEGATED")) { status = Attendee::Delegated; } else { qCDebug(KCALCORE_LOG) << "error setting attendee mStatus, unknown mStatus!"; status = Attendee::NeedsAction; } return status; } QByteArray VCalFormat::writeStatus(Attendee::PartStat status) const { switch (status) { default: case Attendee::NeedsAction: return "NEEDS ACTION"; case Attendee::Accepted: return "ACCEPTED"; case Attendee::Declined: return "DECLINED"; case Attendee::Tentative: return "TENTATIVE"; case Attendee::Delegated: return "DELEGATED"; case Attendee::Completed: return "COMPLETED"; case Attendee::InProcess: return "NEEDS ACTION"; } } void VCalFormat::readCustomProperties(VObject *o, const Incidence::Ptr &i) { VObjectIterator iter; char *s; initPropIterator(&iter, o); while (moreIteration(&iter)) { VObject *cur = nextVObject(&iter); const char *curname = vObjectName(cur); Q_ASSERT(curname); if ((curname[0] == 'X' && curname[1] == '-') && strcmp(curname, ICOrganizerProp) != 0) { // TODO - for the time being, we ignore the parameters part // and just do the value handling here i->setNonKDECustomProperty( curname, QString::fromUtf8(s = fakeCString(vObjectUStringZValue(cur)))); deleteStr(s); } } } void VCalFormat::writeCustomProperties(VObject *o, const Incidence::Ptr &i) { const QMap custom = i->customProperties(); for (QMap::ConstIterator c = custom.begin(); c != custom.end(); ++c) { if (d->mManuallyWrittenExtensionFields.contains(c.key()) || c.key().startsWith("X-KDE-VOLATILE")) { //krazy:exclude=strings continue; } addPropValue(o, c.key().constData(), c.value().toUtf8().constData()); } } void VCalFormat::virtual_hook(int id, void *data) { Q_UNUSED(id); Q_UNUSED(data); Q_ASSERT(false); }