KCalendarCore

xcalformat.cpp
1/*
2 SPDX-FileCopyrightText: 2024 Volker Krause <vkrause@kde.org>
3 SPDX-License-Identifier: LGPL-2.0-or-later
4*/
5
6#include "xcalformat.h"
7
8#include "calformat_p.h"
9#include "event.h"
10#include "icalformat.h"
11#include "icalformat_p.h"
12#include "kcalendarcore_debug.h"
13
14#include <QFile>
15#include <QXmlStreamReader>
16
17extern "C" {
18#include <libical/ical.h>
19}
20
21using namespace Qt::Literals;
22using namespace KCalendarCore;
23
24namespace KCalendarCore
25{
26
27// TODO why is this not in libical??
28struct {
29 icalproperty_class cls;
30 const char *name;
31} constexpr static inline const ical_class_map[] = {
32 {ICAL_CLASS_PUBLIC, "PUBLIC"},
33 {ICAL_CLASS_PRIVATE, "PRIVATE"},
34 {ICAL_CLASS_CONFIDENTIAL, "CONFIDENTIAL"},
35};
36
37icalproperty_class icalenum_string_to_class(const char *name)
38{
39 for (const auto &m : ical_class_map) {
40 if (std::strcmp(m.name, name) == 0) {
41 return m.cls;
42 }
43 }
44 return ICAL_CLASS_NONE;
45}
46
47icalproperty_transp icalenum_string_to_transp(const char *name)
48{
49 return std::strcmp("TRANSPARENT", name) == 0 ? ICAL_TRANSP_TRANSPARENT : ICAL_TRANSP_OPAQUE;
50}
51
52class XCalProperty
53{
54public:
55 QVariant value;
57
58 [[nodiscard]] QString toString() const;
59
60 [[nodiscard]] bool isDate() const;
61 [[nodiscard]] QDateTime toDateTime() const;
62 [[nodiscard]] QDate toDate() const;
63};
64
65class XCalFormatPrivate : public CalFormatPrivate
66{
67public:
68 enum {
69 Rfc6321, // https://datatracker.ietf.org/doc/html/rfc6321
70 Legacy // https://datatracker.ietf.org/doc/html/draft-royer-calsch-xcal-03
71 } m_format = Rfc6321;
72
73 void parseXCal(QXmlStreamReader &reader, const Calendar::Ptr &calendar);
74 void parseVcalendar(QXmlStreamReader &reader, const Calendar::Ptr &calendar, QStringView elemName);
75 void parseVevent(QXmlStreamReader &reader, const Event::Ptr &event, QStringView elemName);
76 void parseRRule(QXmlStreamReader &reader, RecurrenceRule *rrule, QStringView elemName);
77 XCalProperty parseProperty(QXmlStreamReader &reader);
78};
79}
80
81QString XCalProperty::toString() const
82{
83 return value.toString();
84}
85
86bool XCalProperty::isDate() const
87{
88 return value.typeId() == QMetaType::QDate || (value.typeId() == QMetaType::QString && value.toString().size() == 8);
89}
90
91QDateTime XCalProperty::toDateTime() const
92{
93 switch (value.typeId()) {
95 auto dt = value.toDateTime();
96 if (const auto tzId = params.value("tzid"_L1); !tzId.isEmpty()) {
97 dt.setTimeZone(QTimeZone(tzId.toUtf8()));
98 }
99 return dt;
100 }
102 if (value.toString().size() == 16) {
103 auto dt = QDateTime::fromString(value.toString(), u"yyyyMMddThhmmssZ");
104 dt.setTimeZone(QTimeZone::utc());
105 return dt;
106 }
107 return QDateTime::fromString(value.toString(), u"yyyyMMddThhmmss");
108 }
109 return {};
110}
111
112QDate XCalProperty::toDate() const
113{
114 switch (value.typeId()) {
115 case QMetaType::QDate:
116 return value.toDate();
118 return QDate::fromString(value.toString(), u"yyyyMMdd");
119 }
120 return {};
121}
122
123void XCalFormatPrivate::parseXCal(QXmlStreamReader &reader, const Calendar::Ptr &calendar)
124{
125 while (!reader.atEnd() && !reader.hasError()) {
126 if (reader.isEndDocument()) {
127 return;
128 }
129 if (!reader.isStartElement()) {
130 reader.readNext();
131 continue;
132 }
133
134 if (reader.name().compare("iCalendar"_L1, Qt::CaseInsensitive) == 0) {
135 for (const auto &a : reader.namespaceDeclarations()) {
136 if (a.namespaceUri() == "urn:ietf:params:xml:ns:icalendar-2.0"_L1) {
137 m_format = Rfc6321;
138 } else if (a.namespaceUri() == "urn:ietf:params:xml:ns:xcal"_L1) {
139 m_format = Legacy;
140 }
141 }
142 reader.readNextStartElement();
143 } else if (reader.name() == "vcalendar"_L1) {
144 parseVcalendar(reader, calendar, reader.name());
145 } else {
146 qCDebug(KCALCORE_LOG) << "unhandled xcal element" << reader.name();
147 reader.skipCurrentElement();
148 }
149 }
150}
151
152void XCalFormatPrivate::parseVcalendar(QXmlStreamReader &reader, const Calendar::Ptr &calendar, QStringView elemName)
153{
154 reader.readNext();
155 while (!reader.atEnd() && !reader.hasError()) {
156 if (reader.isEndElement() && reader.name() == elemName) {
157 return;
158 }
159 if (!reader.isStartElement()) {
160 reader.readNext();
161 continue;
162 }
163
164 if (reader.name() == "components"_L1 && m_format == Rfc6321) {
165 parseVcalendar(reader, calendar, reader.name());
166 } else if (reader.name() == "prodid"_L1) {
167 mProductId = parseProperty(reader).toString();
168 } else if (reader.name() == "properties"_L1 && m_format == Rfc6321) {
169 parseVcalendar(reader, calendar, reader.name());
170 } else if (reader.name() == "vevent"_L1) {
171 Event::Ptr event(new Event());
172 parseVevent(reader, event, reader.name());
173 calendar->addEvent(event);
174 } else {
175 qCDebug(KCALCORE_LOG) << "unhandled xcal element" << reader.name();
176 reader.skipCurrentElement();
177 }
178 }
179}
180
181void XCalFormatPrivate::parseVevent(QXmlStreamReader &reader, const Event::Ptr &event, QStringView elemName)
182{
183 reader.readNext();
184 while (!reader.atEnd() && !reader.hasError()) {
185 if (reader.isEndElement() && reader.name() == elemName) {
186 return;
187 }
188 if (!reader.isStartElement()) {
189 reader.readNext();
190 continue;
191 }
192
193 if (reader.name() == "attendee"_L1) {
194 Attendee a;
195 if (reader.attributes().value("rsvp"_L1).compare("true"_L1, Qt::CaseInsensitive) == 0) {
196 a.setRSVP(true);
197 }
198 if (const auto role = reader.attributes().value("role"_L1); !role.isEmpty()) {
199 a.setRole(ICalFormatImpl::fromIcalEnum((icalparameter_role)icalparameter_string_to_enum(role.toUtf8().constData())));
200 }
201 // TODO handle more attributes, handle RFC 6321 property parameters
202
203 const auto p = Person::fromFullName(parseProperty(reader).toString());
204 a.setName(p.name());
205 a.setEmail(p.email());
206
207 event->addAttendee(a);
208 } else if (reader.name() == "category"_L1) {
209 auto l = event->categories();
210 l.push_back(parseProperty(reader).toString());
211 event->setCategories(l);
212 } else if (reader.name() == "categories"_L1) {
213 event->setCategories(parseProperty(reader).toString().split(','_L1));
214 } else if (reader.name() == "class"_L1) {
215 event->setSecrecy(ICalFormatImpl::fromIcalEnum(icalenum_string_to_class(parseProperty(reader).toString().toUtf8().constData())));
216 } else if (reader.name() == "description"_L1) {
217 event->setDescription(parseProperty(reader).toString());
218 } else if (reader.name() == "dtend"_L1) {
219 const auto prop = parseProperty(reader);
220 if (prop.isDate()) {
221 event->setDtEnd(prop.toDate().endOfDay());
222 event->setAllDay(true);
223 } else {
224 event->setDtEnd(prop.toDateTime());
225 }
226 } else if (reader.name() == "dtstamp"_L1) {
227 event->setLastModified(parseProperty(reader).toDateTime());
228 } else if (reader.name() == "dtstart"_L1) {
229 const auto prop = parseProperty(reader);
230 if (prop.isDate()) {
231 event->setDtStart(prop.toDate().startOfDay());
232 event->setAllDay(true);
233 } else {
234 event->setDtStart(prop.toDateTime());
235 }
236 } else if (reader.name() == "duration"_L1 && m_format == Rfc6321) {
237 event->setDuration(parseProperty(reader).value.value<Duration>());
238 } else if (reader.name() == "location"_L1) {
239 event->setLocation(parseProperty(reader).toString());
240 } else if (reader.name() == "organizer"_L1) {
241 event->setOrganizer(Person::fromFullName(parseProperty(reader).toString()));
242 } else if (reader.name() == "properties"_L1 && m_format == Rfc6321) {
243 parseVevent(reader, event, reader.name());
244 } else if (reader.name() == "rdate"_L1) {
245 event->recurrence()->addRDateTimePeriod(parseProperty(reader).value.value<Period>());
246 } else if (reader.name() == "recurrence-id"_L1) {
247 event->setRecurrenceId(parseProperty(reader).toDateTime());
248 } else if (reader.name() == "rrule"_L1) {
249 if (m_format == Legacy) {
250 auto rrule = std::make_unique<RecurrenceRule>();
251 ICalFormat f;
252 if (f.fromString(rrule.get(), parseProperty(reader).toString())) {
253 event->recurrence()->addRRule(rrule.release());
254 }
255 } else if (m_format == Rfc6321) {
256 auto rrule = std::make_unique<RecurrenceRule>();
257 parseRRule(reader, rrule.get(), reader.name());
258 event->recurrence()->addRRule(rrule.release());
259 } else {
260 reader.skipCurrentElement();
261 }
262 } else if (reader.name() == "status"_L1) {
263 event->setStatus(ICalFormatImpl::fromIcalEnum(icalenum_string_to_status(parseProperty(reader).toString().toUtf8().constData())));
264 } else if (reader.name() == "summary"_L1) {
265 event->setSummary(parseProperty(reader).toString());
266 } else if (reader.name() == "transp"_L1) {
267 event->setTransparency(ICalFormatImpl::fromIcalEnum(icalenum_string_to_transp(parseProperty(reader).toString().toUtf8().constData())));
268 } else if (reader.name() == "uid"_L1) {
269 event->setUid(parseProperty(reader).toString());
270 } else if (reader.name() == "url"_L1) {
271 event->setUrl(QUrl(parseProperty(reader).toString()));
272 } else if (reader.name().startsWith("x-"_L1) && m_format == Legacy) {
273 event->setCustomProperties({{reader.name().toUtf8().toUpper(), parseProperty(reader).toString()}});
274 } else {
275 qCDebug(KCALCORE_LOG) << "unhandled xcal element" << reader.name();
276 reader.skipCurrentElement();
277 }
278 }
279}
280
281void XCalFormatPrivate::parseRRule(QXmlStreamReader &reader, RecurrenceRule *rrule, QStringView elemName)
282{
283 reader.readNext();
284 while (!reader.atEnd() && !reader.hasError()) {
285 if (reader.isEndElement() && reader.name() == elemName) {
286 return;
287 }
288 if (!reader.isStartElement()) {
289 reader.readNext();
290 continue;
291 }
292
293 // TODO misses more parameters
294 if (reader.name() == "bymonth"_L1) {
295 auto b = rrule->byMonths();
296 b.push_back(reader.readElementText().toInt());
297 rrule->setByMonths(b);
298 } else if (reader.name() == "count"_L1) {
299 rrule->setDuration(reader.readElementText().toInt());
300 } else if (reader.name() == "freq"_L1) {
301 rrule->setRecurrenceType(
302 ICalFormatImpl::fromIcalEnum((icalrecurrencetype_frequency)icalrecur_string_to_freq(reader.readElementText().toUtf8().constData())));
303 } else if (reader.name() == "interval"_L1) {
304 rrule->setFrequency(reader.readElementText().toInt());
305 } else if (reader.name() == "recur"_L1) {
306 parseRRule(reader, rrule, reader.name());
307 } else {
308 qCDebug(KCALCORE_LOG) << "unhandled xcal element" << reader.name();
309 reader.skipCurrentElement();
310 }
311 }
312}
313
314XCalProperty XCalFormatPrivate::parseProperty(QXmlStreamReader &reader)
315{
316 if (m_format == Legacy) {
317 return {reader.readElementText(), {}};
318 }
319
320 Duration periodDuration;
321 XCalProperty prop;
322 auto elemName = reader.name();
323 reader.readNext();
324 while (!reader.atEnd() && !reader.hasError()) {
325 if (reader.isEndElement() && reader.name() == elemName) {
326 break;
327 }
328 if (!reader.isStartElement()) {
329 reader.readNext();
330 continue;
331 }
332
333 if (reader.name() == "date"_L1) {
334 prop.value = QDate::fromString(reader.readElementText(), Qt::ISODate);
335 } else if (reader.name() == "date-time"_L1) {
336 prop.value = QDateTime::fromString(reader.readElementText(), Qt::ISODate);
337 } else if (reader.name() == "duration"_L1) {
338 ICalFormat f;
340 } else if (reader.name() == "parameters"_L1) {
341 reader.readNext();
342 while (!reader.atEnd() && !reader.hasError()) {
343 if (reader.isEndElement() && reader.name() == "parameters"_L1) {
344 break;
345 }
346 if (!reader.isStartElement()) {
347 reader.readNext();
348 continue;
349 }
350
351 prop.params.insert(reader.name().toString(), parseProperty(reader).toString());
352 }
353 } else if (reader.name() == "period"_L1) {
354 reader.readNext();
355 while (!reader.atEnd() && !reader.hasError()) {
356 if (reader.isEndElement() && reader.name() == "period"_L1) {
357 break;
358 }
359 if (!reader.isStartElement()) {
360 reader.readNext();
361 continue;
362 }
363
364 if (reader.name() == "start"_L1) {
365 prop.value = QDateTime::fromString(reader.readElementText(), Qt::ISODate);
366 } else if (reader.name() == "duration"_L1) {
367 ICalFormat f;
368 periodDuration = f.durationFromString(reader.readElementText());
369 } else {
370 qCDebug(KCALCORE_LOG) << "unhandled xcal element" << reader.name();
371 reader.skipCurrentElement();
372 }
373 }
374
375 } else if (reader.name() == "text"_L1) {
376 prop.value = reader.readElementText();
377 } else {
378 qCDebug(KCALCORE_LOG) << "unhandled xcal element" << reader.name();
379 reader.skipCurrentElement();
380 }
381 }
382
383 // we need parameters fully parsed to get the timezone right
384 if (!periodDuration.isNull()) {
385 prop.value = QVariant::fromValue(Period(prop.toDateTime(), periodDuration));
386 }
387
388 return prop;
389}
390
391XCalFormat::XCalFormat()
392 : CalFormat(new XCalFormatPrivate)
393{
394}
395
396XCalFormat::~XCalFormat() = default;
397
398bool XCalFormat::load(const Calendar::Ptr &calendar, const QString &fileName)
399{
402
403 QFile f(fileName);
404 if (!f.open(QFile::ReadOnly)) {
406 return false;
407 }
408
409 QXmlStreamReader reader(&f);
410 d->parseXCal(reader, calendar);
411
412 if (reader.hasError()) {
413 setException(new Exception(Exception::ParseErrorUnableToParse, {reader.errorString()}));
414 return false;
415 }
416
417 return true;
418}
419
420bool XCalFormat::save([[maybe_unused]] const Calendar::Ptr &calendar, [[maybe_unused]] const QString &fileName)
421{
422 qCWarning(KCALCORE_LOG) << "Exporting into xCalendar is not supported";
423 return false;
424}
425
426bool XCalFormat::fromRawString(const Calendar::Ptr &calendar, const QByteArray &string)
427{
430
431 QXmlStreamReader reader(string);
432 d->parseXCal(reader, calendar);
433
434 if (reader.hasError()) {
435 setException(new Exception(Exception::ParseErrorUnableToParse, {reader.errorString()}));
436 return false;
437 }
438
439 return true;
440}
441
442QString XCalFormat::toString([[maybe_unused]] const Calendar::Ptr &calendar)
443{
444 qCWarning(KCALCORE_LOG) << "Exporting into xCalendar is not supported";
445 return {};
446}
Represents information related to an attendee of an Calendar Incidence, typically a meeting or task (...
Definition attendee.h:45
void setName(const QString &name)
Sets the name of the attendee to name.
Definition attendee.cpp:167
void setRole(Role role)
Sets the Role of the attendee to role.
Definition attendee.cpp:235
void setEmail(const QString &email)
Sets the email address for this attendee to email.
Definition attendee.cpp:186
void setRSVP(bool rsvp)
Sets the RSVP flag of the attendee to rsvp.
Definition attendee.cpp:195
An abstract base class that provides an interface to various calendar formats.
Definition calformat.h:39
void clearException()
Clears the exception status.
Definition calformat.cpp:47
void setException(Exception *error)
Sets an exception that is to be used by the functions of this class to report errors.
Definition calformat.cpp:52
Represents a span of time measured in seconds or days.
Definition duration.h:44
bool isNull() const
Returns true if the duration is 0 seconds.
Definition duration.cpp:201
This class provides an Event in the sense of RFC2445.
Definition event.h:33
Exception base class, currently used as a fancy kind of error code and not as an C++ exception.
Definition exceptions.h:42
iCalendar format implementation.
Definition icalformat.h:45
Duration durationFromString(const QString &duration) const
Parses a string representation of a duration.
Incidence::Ptr fromString(const QString &string)
Parses a string, returning the first iCal component as an Incidence.
The period can be defined by either a start time and an end time or by a start time and a duration.
Definition period.h:38
static Person fromFullName(const QString &fullName)
Constructs a person with name and email address taken from fullName.
Definition person.cpp:362
This class represents a recurrence rule for a calendar incidence.
void setDuration(int duration)
Sets the total number of times the event is to occur, including both the first and last.
void setFrequency(int freq)
Sets the recurrence frequency, in terms of the recurrence time period type.
Read support for xCal events.
Definition xcalformat.h:27
bool load(const Calendar::Ptr &calendar, const QString &fileName) override
bool fromRawString(const Calendar::Ptr &calendar, const QByteArray &string) override
QString toString(const Calendar::Ptr &calendar) override
Does nothing.
bool save(const Calendar::Ptr &calendar, const QString &fileName) override
Does nothing.
This file is part of the API for handling calendar data and defines the Event class.
This file is part of the API for handling calendar data and defines the ICalFormat class.
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
char * toString(const EngineQuery &query)
Namespace for all KCalendarCore types.
Definition alarm.h:37
const char * constData() const const
QByteArray toUpper() const const
QDate fromString(QStringView string, QStringView format, QCalendar cal)
QDateTime fromString(QStringView string, QStringView format, QCalendar cal)
bool open(FILE *fh, OpenMode mode, FileHandleFlags handleFlags)
iterator insert(const Key &key, const T &value)
T value(const Key &key) const const
QString errorString() const const
qsizetype size() const const
int toInt(bool *ok, int base) const const
QByteArray toUtf8() const const
int compare(QChar ch) const const
bool isEmpty() const const
bool startsWith(QChar ch) const const
QString toString() const const
QByteArray toUtf8() const const
CaseInsensitive
QTimeZone utc()
QVariant fromValue(T &&value)
QDate toDate() const const
QDateTime toDateTime() const const
QString toString() const const
int typeId() const const
QStringView value(QAnyStringView namespaceUri, QAnyStringView name) const const
bool atEnd() const const
QXmlStreamAttributes attributes() const const
QString errorString() const const
bool hasError() const const
bool isEndDocument() const const
bool isEndElement() const const
bool isStartElement() const const
QStringView name() const const
QXmlStreamNamespaceDeclarations namespaceDeclarations() const const
QString readElementText(ReadElementTextBehaviour behaviour)
TokenType readNext()
bool readNextStartElement()
void skipCurrentElement()
Q_D(Todo)
Private class that helps to provide binary compatibility between releases.
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 16:57:05 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.