1/*
2 This file is part of the kcalcore library.
3
4 Copyright (c) 2001 Cornelius Schumacher <schumacher@kde.org>
5
6 This library is free software; you can redistribute it and/or
7 modify it under the terms of the GNU Library General Public
8 License as published by the Free Software Foundation; either
9 version 2 of the License, or (at your option) any later version.
10
11 This library is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 Library General Public License for more details.
15
16 You should have received a copy of the GNU Library General Public License
17 along with this library; see the file COPYING.LIB. If not, write to
18 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19 Boston, MA 02110-1301, USA.
20*/
21/**
22 @file
23 This file is part of the API for handling calendar data and
24 defines the ICalFormat class.
25
26 @brief
27 iCalendar format implementation: a layer of abstraction for libical.
28
29 @author Cornelius Schumacher \<schumacher@kde.org\>
30*/
31#include "icalformat.h"
32#include "icalformat_p.h"
33#include "icaltimezones.h"
34#include "freebusy.h"
35#include "memorycalendar.h"
36
37#include <KDebug>
38#include <KSaveFile>
39
40#include <QtCore/QFile>
41
42extern "C" {
43#include <libical/ical.h>
44#include <libical/icalss.h>
45#include <libical/icalparser.h>
46#include <libical/icalrestriction.h>
47#include <libical/icalmemory.h>
48}
49
50using namespace KCalCore;
51
52//@cond PRIVATE
53class KCalCore::ICalFormat::Private
54{
55public:
56 Private(ICalFormat *parent)
57 : mImpl(new ICalFormatImpl(parent)),
58 mTimeSpec(KDateTime::UTC)
59 {}
60 ~Private() {
61 delete mImpl;
62 }
63 ICalFormatImpl *mImpl;
64 KDateTime::Spec mTimeSpec;
65};
66//@endcond
67
68ICalFormat::ICalFormat()
69 : d(new Private(this))
70{
71}
72
73ICalFormat::~ICalFormat()
74{
75 icalmemory_free_ring();
76 delete d;
77}
78
79bool ICalFormat::load(const Calendar::Ptr &calendar, const QString &fileName)
80{
81 kDebug() << fileName;
82
83 clearException();
84
85 QFile file(fileName);
86 if (!file.open(QIODevice::ReadOnly)) {
87 kError() << "load error";
88 setException(new Exception(Exception::LoadError));
89 return false;
90 }
91 QTextStream ts(&file);
92 ts.setCodec("UTF-8");
93 QByteArray text = ts.readAll().trimmed().toUtf8();
94 file.close();
95
96 if (text.isEmpty()) {
97 // empty files are valid
98 return true;
99 } else {
100 return fromRawString(calendar, text, false, fileName);
101 }
102}
103
104bool ICalFormat::save(const Calendar::Ptr &calendar, const QString &fileName)
105{
106 kDebug() << fileName;
107
108 clearException();
109
110 QString text = toString(calendar);
111 if (text.isEmpty()) {
112 return false;
113 }
114
115 // Write backup file
116 KSaveFile::backupFile(fileName);
117
118 KSaveFile file(fileName);
119 if (!file.open()) {
120 kError() << "file open error: " << file.errorString() << ";filename=" << fileName;
121 setException(new Exception(Exception::SaveErrorOpenFile,
122 QStringList(fileName)));
123
124 return false;
125 }
126
127 // Convert to UTF8 and save
128 QByteArray textUtf8 = text.toUtf8();
129 file.write(textUtf8.data(), textUtf8.size());
130
131 if (!file.finalize()) {
132 kDebug() << "file finalize error:" << file.errorString();
133 setException(new Exception(Exception::SaveErrorSaveFile,
134 QStringList(fileName)));
135
136 return false;
137 }
138
139 return true;
140}
141
142bool ICalFormat::fromString(const Calendar::Ptr &cal, const QString &string,
143 bool deleted, const QString &notebook)
144{
145 return fromRawString(cal, string.toUtf8(), deleted, notebook);
146}
147
148bool ICalFormat::fromRawString(const Calendar::Ptr &cal, const QByteArray &string,
149 bool deleted, const QString &notebook)
150{
151 Q_UNUSED(notebook);
152 // Get first VCALENDAR component.
153 // TODO: Handle more than one VCALENDAR or non-VCALENDAR top components
154 icalcomponent *calendar;
155
156 // Let's defend const correctness until the very gates of hell^Wlibical
157 calendar = icalcomponent_new_from_string(const_cast<char*>((const char *)string));
158 if (!calendar) {
159 kError() << "parse error ; string is empty?" << string.isEmpty();
160 setException(new Exception(Exception::ParseErrorIcal));
161 return false;
162 }
163
164 bool success = true;
165
166 if (icalcomponent_isa(calendar) == ICAL_XROOT_COMPONENT) {
167 icalcomponent *comp;
168 for (comp = icalcomponent_get_first_component(calendar, ICAL_VCALENDAR_COMPONENT);
169 comp; comp = icalcomponent_get_next_component(calendar, ICAL_VCALENDAR_COMPONENT)) {
170 // put all objects into their proper places
171 if (!d->mImpl->populate(cal, comp, deleted)) {
172 kError() << "Could not populate calendar";
173 if (!exception()) {
174 setException(new Exception(Exception::ParseErrorKcal));
175 }
176 success = false;
177 } else {
178 setLoadedProductId(d->mImpl->loadedProductId());
179 }
180 }
181 } else if (icalcomponent_isa(calendar) != ICAL_VCALENDAR_COMPONENT) {
182 kDebug() << "No VCALENDAR component found";
183 setException(new Exception(Exception::NoCalendar));
184 success = false;
185 } else {
186 // put all objects into their proper places
187 if (!d->mImpl->populate(cal, calendar, deleted)) {
188 kDebug() << "Could not populate calendar";
189 if (!exception()) {
190 setException(new Exception(Exception::ParseErrorKcal));
191 }
192 success = false;
193 } else {
194 setLoadedProductId(d->mImpl->loadedProductId());
195 }
196 }
197
198 icalcomponent_free(calendar);
199 icalmemory_free_ring();
200
201 return success;
202}
203
204Incidence::Ptr ICalFormat::fromString(const QString &string)
205{
206 MemoryCalendar::Ptr cal(new MemoryCalendar(d->mTimeSpec));
207 fromString(cal, string);
208
209 const Incidence::List list = cal->incidences();
210 return !list.isEmpty() ? list.first() : Incidence::Ptr();
211}
212
213QString ICalFormat::toString(const Calendar::Ptr &cal,
214 const QString &notebook, bool deleted)
215{
216 icalcomponent *calendar = d->mImpl->createCalendarComponent(cal);
217 icalcomponent *component;
218
219 ICalTimeZones *tzlist = cal->timeZones(); // time zones possibly used in the calendar
220 ICalTimeZones tzUsedList; // time zones actually used in the calendar
221
222 // todos
223 Todo::List todoList = deleted ? cal->deletedTodos() : cal->rawTodos();
224 Todo::List::ConstIterator it;
225 for (it = todoList.constBegin(); it != todoList.constEnd(); ++it) {
226 if (!deleted || !cal->todo((*it)->uid(), (*it)->recurrenceId())) {
227 // use existing ones, or really deleted ones
228 if (notebook.isEmpty() ||
229 (!cal->notebook(*it).isEmpty() && notebook.endsWith(cal->notebook(*it)))) {
230 component = d->mImpl->writeTodo(*it, tzlist, &tzUsedList);
231 icalcomponent_add_component(calendar, component);
232 }
233 }
234 }
235 // events
236 Event::List events = deleted ? cal->deletedEvents() : cal->rawEvents();
237 Event::List::ConstIterator it2;
238 for (it2 = events.constBegin(); it2 != events.constEnd(); ++it2) {
239 if (!deleted || !cal->event((*it2)->uid(), (*it2)->recurrenceId())) {
240 // use existing ones, or really deleted ones
241 if (notebook.isEmpty() ||
242 (!cal->notebook(*it2).isEmpty() && notebook.endsWith(cal->notebook(*it2)))) {
243 component = d->mImpl->writeEvent(*it2, tzlist, &tzUsedList);
244 icalcomponent_add_component(calendar, component);
245 }
246 }
247 }
248
249 // journals
250 Journal::List journals = deleted ? cal->deletedJournals() : cal->rawJournals();
251 Journal::List::ConstIterator it3;
252 for (it3 = journals.constBegin(); it3 != journals.constEnd(); ++it3) {
253 if (!deleted || !cal->journal((*it3)->uid(), (*it3)->recurrenceId())) {
254 // use existing ones, or really deleted ones
255 if (notebook.isEmpty() ||
256 (!cal->notebook(*it3).isEmpty() && notebook.endsWith(cal->notebook(*it3)))) {
257 component = d->mImpl->writeJournal(*it3, tzlist, &tzUsedList);
258 icalcomponent_add_component(calendar, component);
259 }
260 }
261 }
262
263 // time zones
264 ICalTimeZones::ZoneMap zones = tzUsedList.zones();
265 if (todoList.isEmpty() && events.isEmpty() && journals.isEmpty()) {
266 // no incidences means no used timezones, use all timezones
267 // this will export a calendar having only timezone definitions
268 zones = tzlist->zones();
269 }
270 for (ICalTimeZones::ZoneMap::ConstIterator it = zones.constBegin();
271 it != zones.constEnd(); ++it) {
272 icaltimezone *tz = (*it).icalTimezone();
273 if (!tz) {
274 kError() << "bad time zone";
275 } else {
276 component = icalcomponent_new_clone(icaltimezone_get_component(tz));
277 icalcomponent_add_component(calendar, component);
278 icaltimezone_free(tz, 1);
279 }
280 }
281
282 char *const componentString = icalcomponent_as_ical_string_r(calendar);
283 const QString &text = QString::fromUtf8(componentString);
284 free(componentString);
285
286 icalcomponent_free(calendar);
287 icalmemory_free_ring();
288
289 if (text.isEmpty()) {
290 setException(new Exception(Exception::LibICalError));
291 }
292
293 return text;
294}
295
296QString ICalFormat::toICalString(const Incidence::Ptr &incidence)
297{
298 MemoryCalendar::Ptr cal(new MemoryCalendar(d->mTimeSpec));
299 cal->addIncidence(Incidence::Ptr(incidence->clone()));
300 return toString(cal.staticCast<Calendar>());
301}
302
303QString ICalFormat::toString(const Incidence::Ptr &incidence)
304{
305 return QString::fromUtf8(toRawString(incidence));
306}
307
308QByteArray ICalFormat::toRawString(const Incidence::Ptr &incidence)
309{
310 icalcomponent *component;
311 ICalTimeZones tzlist;
312 ICalTimeZones tzUsedList;
313
314 component = d->mImpl->writeIncidence(incidence, iTIPRequest, &tzlist, &tzUsedList);
315
316 QByteArray text = icalcomponent_as_ical_string(component);
317
318 // time zones
319 ICalTimeZones::ZoneMap zones = tzUsedList.zones();
320 for (ICalTimeZones::ZoneMap::ConstIterator it = zones.constBegin();
321 it != zones.constEnd(); ++it) {
322 icaltimezone *tz = (*it).icalTimezone();
323 if (!tz) {
324 kError() << "bad time zone";
325 } else {
326 icalcomponent *tzcomponent = icaltimezone_get_component(tz);
327 icalcomponent_add_component(component, component);
328 text.append(icalcomponent_as_ical_string(tzcomponent));
329 icaltimezone_free(tz, 1);
330 }
331 }
332
333 icalcomponent_free(component);
334
335 return text;
336}
337
338QString ICalFormat::toString(RecurrenceRule *recurrence)
339{
340 icalproperty *property;
341 property = icalproperty_new_rrule(d->mImpl->writeRecurrenceRule(recurrence));
342 QString text = QString::fromUtf8(icalproperty_as_ical_string(property));
343 icalproperty_free(property);
344 return text;
345}
346
347bool ICalFormat::fromString(RecurrenceRule *recurrence, const QString &rrule)
348{
349 if (!recurrence) {
350 return false;
351 }
352 bool success = true;
353 icalerror_clear_errno();
354 struct icalrecurrencetype recur = icalrecurrencetype_from_string(rrule.toLatin1());
355 if (icalerrno != ICAL_NO_ERROR) {
356 kDebug() << "Recurrence parsing error:" << icalerror_strerror(icalerrno);
357 success = false;
358 }
359
360 if (success) {
361 d->mImpl->readRecurrence(recur, recurrence);
362 }
363
364 return success;
365}
366
367QString ICalFormat::createScheduleMessage(const IncidenceBase::Ptr &incidence,
368 iTIPMethod method)
369{
370 icalcomponent *message = 0;
371
372 if (incidence->type() == Incidence::TypeEvent ||
373 incidence->type() == Incidence::TypeTodo) {
374
375 Incidence::Ptr i = incidence.staticCast<Incidence>();
376
377 // Recurring events need timezone information to allow proper calculations
378 // across timezones with different DST.
379 const bool useUtcTimes = !i->recurs();
380
381 const bool hasSchedulingId = (i->schedulingID() != i->uid());
382
383 const bool incidenceNeedChanges = (useUtcTimes || hasSchedulingId);
384
385 if (incidenceNeedChanges) {
386 // The incidence need changes, so clone it before we continue
387 i = Incidence::Ptr(i->clone());
388
389 // Handle conversion to UTC times
390 if (useUtcTimes) {
391 i->shiftTimes(KDateTime::Spec::UTC(), KDateTime::Spec::UTC());
392 }
393
394 // Handle scheduling ID being present
395 if (hasSchedulingId) {
396 // We have a separation of scheduling ID and UID
397 i->setSchedulingID(QString(), i->schedulingID());
398
399 }
400
401 // Build the message with the cloned incidence
402 message = d->mImpl->createScheduleComponent(i, method);
403 }
404 }
405
406 if (message == 0) {
407 message = d->mImpl->createScheduleComponent(incidence, method);
408 }
409
410 QString messageText = QString::fromUtf8(icalcomponent_as_ical_string(message));
411
412 icalcomponent_free(message);
413 return messageText;
414}
415
416FreeBusy::Ptr ICalFormat::parseFreeBusy(const QString &str)
417{
418 clearException();
419
420 icalcomponent *message;
421 message = icalparser_parse_string(str.toUtf8());
422
423 if (!message) {
424 return FreeBusy::Ptr();
425 }
426
427 FreeBusy::Ptr freeBusy;
428
429 icalcomponent *c;
430 for (c = icalcomponent_get_first_component(message, ICAL_VFREEBUSY_COMPONENT);
431 c != 0; c = icalcomponent_get_next_component(message, ICAL_VFREEBUSY_COMPONENT)) {
432 FreeBusy::Ptr fb = d->mImpl->readFreeBusy(c);
433
434 if (freeBusy) {
435 freeBusy->merge(fb);
436 } else {
437 freeBusy = fb;
438 }
439 }
440
441 if (!freeBusy) {
442 kDebug() << "object is not a freebusy.";
443 }
444
445 icalcomponent_free(message);
446
447 return freeBusy;
448}
449
450ScheduleMessage::Ptr ICalFormat::parseScheduleMessage(const Calendar::Ptr &cal,
451 const QString &messageText)
452{
453 setTimeSpec(cal->timeSpec());
454 clearException();
455
456 if (messageText.isEmpty()) {
457 setException(
458 new Exception(Exception::ParseErrorEmptyMessage));
459 return ScheduleMessage::Ptr();
460 }
461
462 icalcomponent *message;
463 message = icalparser_parse_string(messageText.toUtf8());
464
465 if (!message) {
466 setException(
467 new Exception(Exception::ParseErrorUnableToParse));
468
469 return ScheduleMessage::Ptr();
470 }
471
472 icalproperty *m =
473 icalcomponent_get_first_property(message, ICAL_METHOD_PROPERTY);
474 if (!m) {
475 setException(
476 new Exception(Exception::ParseErrorMethodProperty));
477
478 return ScheduleMessage::Ptr();
479 }
480
481 // Populate the message's time zone collection with all VTIMEZONE components
482 ICalTimeZones tzlist;
483 ICalTimeZoneSource tzs;
484 tzs.parse(message, tzlist);
485
486 icalcomponent *c;
487
488 IncidenceBase::Ptr incidence;
489 c = icalcomponent_get_first_component(message, ICAL_VEVENT_COMPONENT);
490 if (c) {
491 incidence = d->mImpl->readEvent(c, &tzlist).staticCast<IncidenceBase>();
492 }
493
494 if (!incidence) {
495 c = icalcomponent_get_first_component(message, ICAL_VTODO_COMPONENT);
496 if (c) {
497 incidence = d->mImpl->readTodo(c, &tzlist).staticCast<IncidenceBase>();
498 }
499 }
500
501 if (!incidence) {
502 c = icalcomponent_get_first_component(message, ICAL_VJOURNAL_COMPONENT);
503 if (c) {
504 incidence = d->mImpl->readJournal(c, &tzlist).staticCast<IncidenceBase>();
505 }
506 }
507
508 if (!incidence) {
509 c = icalcomponent_get_first_component(message, ICAL_VFREEBUSY_COMPONENT);
510 if (c) {
511 incidence = d->mImpl->readFreeBusy(c).staticCast<IncidenceBase>();
512 }
513 }
514
515 if (!incidence) {
516 kDebug() << "object is not a freebusy, event, todo or journal";
517 setException(new Exception(Exception::ParseErrorNotIncidence));
518
519 return ScheduleMessage::Ptr();
520 }
521
522 icalproperty_method icalmethod = icalproperty_get_method(m);
523 iTIPMethod method;
524
525 switch (icalmethod) {
526 case ICAL_METHOD_PUBLISH:
527 method = iTIPPublish;
528 break;
529 case ICAL_METHOD_REQUEST:
530 method = iTIPRequest;
531 break;
532 case ICAL_METHOD_REFRESH:
533 method = iTIPRefresh;
534 break;
535 case ICAL_METHOD_CANCEL:
536 method = iTIPCancel;
537 break;
538 case ICAL_METHOD_ADD:
539 method = iTIPAdd;
540 break;
541 case ICAL_METHOD_REPLY:
542 method = iTIPReply;
543 break;
544 case ICAL_METHOD_COUNTER:
545 method = iTIPCounter;
546 break;
547 case ICAL_METHOD_DECLINECOUNTER:
548 method = iTIPDeclineCounter;
549 break;
550 default:
551 method = iTIPNoMethod;
552 kDebug() << "Unknown method";
553 break;
554 }
555
556 if (!icalrestriction_check(message)) {
557 kWarning() << endl
558 << "kcalcore library reported a problem while parsing:";
559 kWarning() << ScheduleMessage::methodName(method) << ":" //krazy:exclude=kdebug
560 << d->mImpl->extractErrorProperty(c);
561 }
562
563 Incidence::Ptr existingIncidence = cal->incidence(incidence->uid());
564
565 icalcomponent *calendarComponent = 0;
566 if (existingIncidence) {
567 calendarComponent = d->mImpl->createCalendarComponent(cal);
568
569 // TODO: check, if cast is required, or if it can be done by virtual funcs.
570 // TODO: Use a visitor for this!
571 if (existingIncidence->type() == Incidence::TypeTodo) {
572 Todo::Ptr todo = existingIncidence.staticCast<Todo>();
573 icalcomponent_add_component(calendarComponent,
574 d->mImpl->writeTodo(todo));
575 }
576 if (existingIncidence->type() == Incidence::TypeEvent) {
577 Event::Ptr event = existingIncidence.staticCast<Event>();
578 icalcomponent_add_component(calendarComponent,
579 d->mImpl->writeEvent(event));
580 }
581 } else {
582 icalcomponent_free(message);
583 return ScheduleMessage::Ptr(new ScheduleMessage(incidence, method,
584 ScheduleMessage::Unknown));
585 }
586
587 icalproperty_xlicclass result =
588 icalclassify(message, calendarComponent, static_cast<const char *>(""));
589
590 ScheduleMessage::Status status;
591
592 switch (result) {
593 case ICAL_XLICCLASS_PUBLISHNEW:
594 status = ScheduleMessage::PublishNew;
595 break;
596 case ICAL_XLICCLASS_PUBLISHUPDATE:
597 status = ScheduleMessage::PublishUpdate;
598 break;
599 case ICAL_XLICCLASS_OBSOLETE:
600 status = ScheduleMessage::Obsolete;
601 break;
602 case ICAL_XLICCLASS_REQUESTNEW:
603 status = ScheduleMessage::RequestNew;
604 break;
605 case ICAL_XLICCLASS_REQUESTUPDATE:
606 status = ScheduleMessage::RequestUpdate;
607 break;
608 case ICAL_XLICCLASS_UNKNOWN:
609 default:
610 status = ScheduleMessage::Unknown;
611 break;
612 }
613
614 icalcomponent_free(message);
615 icalcomponent_free(calendarComponent);
616
617 return ScheduleMessage::Ptr(new ScheduleMessage(incidence, method, status));
618}
619
620void ICalFormat::setTimeSpec(const KDateTime::Spec &timeSpec)
621{
622 d->mTimeSpec = timeSpec;
623}
624
625KDateTime::Spec ICalFormat::timeSpec() const
626{
627 return d->mTimeSpec;
628}
629
630QString ICalFormat::timeZoneId() const
631{
632 KTimeZone tz = d->mTimeSpec.timeZone();
633 return tz.isValid() ? tz.name() : QString();
634}
635
636void ICalFormat::virtual_hook(int id, void *data)
637{
638 Q_UNUSED(id);
639 Q_UNUSED(data);
640 Q_ASSERT(false);
641}
642