1/*
2 This file is part of the kcal 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
32#include "icalformat.h"
33#include "icalformat_p.h"
34#include "calendar.h"
35#include "calendarlocal.h"
36#include "icaltimezones.h"
37
38extern "C" {
39 #include <libical/ical.h>
40 #include <libical/icalss.h>
41 #include <libical/icalparser.h>
42 #include <libical/icalrestriction.h>
43 #include <libical/icalmemory.h>
44}
45
46#include <QtCore/QString>
47#include <QtCore/QRegExp>
48#include <QtCore/QFile>
49#include <QtCore/QTextStream>
50#include <QtCore/QByteArray>
51#include <QClipboard>
52
53#include <kdebug.h>
54#include <klocalizedstring.h>
55#include <ksavefile.h>
56
57#include <stdio.h>
58
59using namespace KCal;
60
61//@cond PRIVATE
62class KCal::ICalFormat::Private
63{
64 public:
65 Private( ICalFormat *parent )
66 : mImpl( new ICalFormatImpl( parent ) ),
67 mTimeSpec( KDateTime::UTC )
68 {}
69 ~Private() { delete mImpl; }
70 ICalFormatImpl *mImpl;
71 KDateTime::Spec mTimeSpec;
72};
73//@endcond
74
75ICalFormat::ICalFormat()
76 : d( new Private( this ) )
77{
78}
79
80ICalFormat::~ICalFormat()
81{
82 delete d;
83}
84
85bool ICalFormat::load( Calendar *calendar, const QString &fileName )
86{
87 kDebug() << fileName;
88
89 clearException();
90
91 QFile file( fileName );
92 if ( !file.open( QIODevice::ReadOnly ) ) {
93 kDebug() << "load error";
94 setException( new ErrorFormat( ErrorFormat::LoadError ) );
95 return false;
96 }
97 QTextStream ts( &file );
98 ts.setCodec( "ISO 8859-1" );
99 QByteArray text = ts.readAll().trimmed().toLatin1();
100 file.close();
101
102 if ( text.isEmpty() ) {
103 // empty files are valid
104 return true;
105 } else {
106 return fromRawString( calendar, text );
107 }
108}
109
110bool ICalFormat::save( Calendar *calendar, const QString &fileName )
111{
112 kDebug() << fileName;
113
114 clearException();
115
116 QString text = toString( calendar );
117 if ( text.isEmpty() ) {
118 return false;
119 }
120
121 // Write backup file
122 KSaveFile::backupFile( fileName );
123
124 KSaveFile file( fileName );
125 if ( !file.open() ) {
126 kDebug() << "err:" << file.errorString();
127 setException( new ErrorFormat( ErrorFormat::SaveError,
128 i18n( "Error saving to '%1'.", fileName ) ) );
129 return false;
130 }
131
132 // Convert to UTF8 and save
133 QByteArray textUtf8 = text.toUtf8();
134 file.write( textUtf8.data(), textUtf8.size() );
135
136 if ( !file.finalize() ) {
137 kDebug() << "err:" << file.errorString();
138 setException( new ErrorFormat( ErrorFormat::SaveError,
139 i18n( "Could not save '%1'", fileName ) ) );
140 return false;
141 }
142
143 return true;
144}
145
146bool ICalFormat::fromString( Calendar *cal, const QString &string )
147{
148 return fromRawString( cal, string.toUtf8() );
149}
150
151bool ICalFormat::fromRawString( Calendar *cal, const QByteArray &string )
152{
153 // Get first VCALENDAR component.
154 // TODO: Handle more than one VCALENDAR or non-VCALENDAR top components
155 icalcomponent *calendar;
156
157 // Let's defend const correctness until the very gates of hell^Wlibical
158 calendar = icalcomponent_new_from_string( const_cast<char*>( ( const char * )string ) );
159 if ( !calendar ) {
160 kDebug() << "parse error";
161 setException( new ErrorFormat( ErrorFormat::ParseErrorIcal ) );
162 return false;
163 }
164
165 bool success = true;
166
167 if ( icalcomponent_isa( calendar ) == ICAL_XROOT_COMPONENT ) {
168 icalcomponent *comp;
169 for ( comp = icalcomponent_get_first_component( calendar, ICAL_VCALENDAR_COMPONENT );
170 comp; comp = icalcomponent_get_next_component( calendar, ICAL_VCALENDAR_COMPONENT ) ) {
171 // put all objects into their proper places
172 if ( !d->mImpl->populate( cal, comp ) ) {
173 kDebug() << "Could not populate calendar";
174 if ( !exception() ) {
175 setException( new ErrorFormat( ErrorFormat::ParseErrorKcal ) );
176 }
177 success = false;
178 } else {
179 setLoadedProductId( d->mImpl->loadedProductId() );
180 }
181 }
182 } else if ( icalcomponent_isa( calendar ) != ICAL_VCALENDAR_COMPONENT ) {
183 kDebug() << "No VCALENDAR component found";
184 setException( new ErrorFormat( ErrorFormat::NoCalendar ) );
185 success = false;
186 } else {
187 // put all objects into their proper places
188 if ( !d->mImpl->populate( cal, calendar ) ) {
189 kDebug() << "Could not populate calendar";
190 if ( !exception() ) {
191 setException( new ErrorFormat( ErrorFormat::ParseErrorKcal ) );
192 }
193 success = false;
194 } else {
195 setLoadedProductId( d->mImpl->loadedProductId() );
196 }
197 }
198
199 icalcomponent_free( calendar );
200 icalmemory_free_ring();
201
202 return success;
203}
204
205Incidence *ICalFormat::fromString( const QString &string )
206{
207 CalendarLocal cal( d->mTimeSpec );
208 fromString( &cal, string );
209
210 Incidence *ical = 0;
211 Event::List elist = cal.events();
212 if ( elist.count() > 0 ) {
213 ical = elist.first();
214 } else {
215 Todo::List tlist = cal.todos();
216 if ( tlist.count() > 0 ) {
217 ical = tlist.first();
218 } else {
219 Journal::List jlist = cal.journals();
220 if ( jlist.count() > 0 ) {
221 ical = jlist.first();
222 }
223 }
224 }
225
226 return ical ? ical->clone() : 0;
227}
228
229QString ICalFormat::toString( Calendar *cal )
230{
231 icalcomponent *calendar = d->mImpl->createCalendarComponent( cal );
232 icalcomponent *component;
233
234 ICalTimeZones *tzlist = cal->timeZones(); // time zones possibly used in the calendar
235 ICalTimeZones tzUsedList; // time zones actually used in the calendar
236
237 // todos
238 Todo::List todoList = cal->rawTodos();
239 Todo::List::ConstIterator it;
240 for ( it = todoList.constBegin(); it != todoList.constEnd(); ++it ) {
241 component = d->mImpl->writeTodo( *it, tzlist, &tzUsedList );
242 icalcomponent_add_component( calendar, component );
243 }
244
245 // events
246 Event::List events = cal->rawEvents();
247 Event::List::ConstIterator it2;
248 for ( it2 = events.constBegin(); it2 != events.constEnd(); ++it2 ) {
249 if ( *it2 ) {
250 component = d->mImpl->writeEvent( *it2, tzlist, &tzUsedList );
251 icalcomponent_add_component( calendar, component );
252 }
253 }
254
255 // journals
256 Journal::List journals = cal->journals();
257 Journal::List::ConstIterator it3;
258 for ( it3 = journals.constBegin(); it3 != journals.constEnd(); ++it3 ) {
259 component = d->mImpl->writeJournal( *it3, tzlist, &tzUsedList );
260 icalcomponent_add_component( calendar, component );
261 }
262
263 // time zones
264 const ICalTimeZones::ZoneMap zones = tzUsedList.zones();
265 for ( ICalTimeZones::ZoneMap::ConstIterator it=zones.constBegin();
266 it != zones.constEnd(); ++it ) {
267 icaltimezone *tz = (*it).icalTimezone();
268 if ( !tz ) {
269 kError() << "bad time zone";
270 } else {
271 component = icalcomponent_new_clone( icaltimezone_get_component( tz ) );
272 icalcomponent_add_component( calendar, component );
273 icaltimezone_free( tz, 1 );
274 }
275 }
276
277 QString text = QString::fromUtf8( icalcomponent_as_ical_string( calendar ) );
278
279 icalcomponent_free( calendar );
280 icalmemory_free_ring();
281
282 if ( text.isEmpty() ) {
283 setException( new ErrorFormat( ErrorFormat::SaveError,
284 i18n( "libical error" ) ) );
285 }
286
287 return text;
288}
289
290QString ICalFormat::toICalString( Incidence *incidence )
291{
292 CalendarLocal cal( d->mTimeSpec );
293 cal.addIncidence( incidence->clone() );
294 return toString( &cal );
295}
296
297QString ICalFormat::toString( Incidence *incidence )
298{
299 icalcomponent *component;
300
301 component = d->mImpl->writeIncidence( incidence );
302
303 QString text = QString::fromUtf8( icalcomponent_as_ical_string( component ) );
304
305 icalcomponent_free( component );
306
307 return text;
308}
309
310QString ICalFormat::toString( RecurrenceRule *recurrence )
311{
312 icalproperty *property;
313 property = icalproperty_new_rrule( d->mImpl->writeRecurrenceRule( recurrence ) );
314 QString text = QString::fromUtf8( icalproperty_as_ical_string( property ) );
315 icalproperty_free( property );
316 return text;
317}
318
319bool ICalFormat::fromString( RecurrenceRule *recurrence, const QString &rrule )
320{
321 if ( !recurrence ) {
322 return false;
323 }
324 bool success = true;
325 icalerror_clear_errno();
326 struct icalrecurrencetype recur = icalrecurrencetype_from_string( rrule.toLatin1() );
327 if ( icalerrno != ICAL_NO_ERROR ) {
328 kDebug() << "Recurrence parsing error:" << icalerror_strerror( icalerrno );
329 success = false;
330 }
331
332 if ( success ) {
333 d->mImpl->readRecurrence( recur, recurrence );
334 }
335
336 return success;
337}
338
339QString ICalFormat::createScheduleMessage( IncidenceBase *incidence,
340 iTIPMethod method )
341{
342 icalcomponent *message = 0;
343
344 // Handle scheduling ID being present
345 if ( incidence->type() == "Event" || incidence->type() == "Todo" ) {
346 Incidence *i = static_cast<Incidence*>( incidence );
347 if ( i->schedulingID() != i->uid() ) {
348 // We have a separation of scheduling ID and UID
349 i = i->clone();
350 i->setUid( i->schedulingID() );
351 i->setSchedulingID( QString() );
352
353 // Build the message with the cloned incidence
354 message = d->mImpl->createScheduleComponent( i, method );
355
356 // And clean up
357 delete i;
358 }
359 }
360
361 if ( message == 0 ) {
362 message = d->mImpl->createScheduleComponent( incidence, method );
363 }
364
365 QString messageText = QString::fromUtf8( icalcomponent_as_ical_string( message ) );
366
367 icalcomponent_free( message );
368 return messageText;
369}
370
371FreeBusy *ICalFormat::parseFreeBusy( const QString &str )
372{
373 clearException();
374
375 icalcomponent *message;
376 message = icalparser_parse_string( str.toUtf8() );
377
378 if ( !message ) {
379 return 0;
380 }
381
382 FreeBusy *freeBusy = 0;
383
384 icalcomponent *c;
385 for ( c = icalcomponent_get_first_component( message, ICAL_VFREEBUSY_COMPONENT );
386 c != 0; c = icalcomponent_get_next_component( message, ICAL_VFREEBUSY_COMPONENT ) ) {
387 FreeBusy *fb = d->mImpl->readFreeBusy( c );
388
389 if ( freeBusy ) {
390 freeBusy->merge( fb );
391 delete fb;
392 } else {
393 freeBusy = fb;
394 }
395 }
396
397 if ( !freeBusy ) {
398 kDebug() << "object is not a freebusy.";
399 }
400 return freeBusy;
401}
402
403ScheduleMessage *ICalFormat::parseScheduleMessage( Calendar *cal,
404 const QString &messageText )
405{
406 setTimeSpec( cal->timeSpec() );
407 clearException();
408
409 if ( messageText.isEmpty() ) {
410 setException(
411 new ErrorFormat( ErrorFormat::ParseErrorKcal,
412 QLatin1String( "messageText is empty, unable "
413 "to parse into a ScheduleMessage" ) ) );
414 return 0;
415 }
416
417 icalcomponent *message;
418 message = icalparser_parse_string( messageText.toUtf8() );
419
420 if ( !message ) {
421 setException(
422 new ErrorFormat( ErrorFormat::ParseErrorKcal,
423 QLatin1String( "icalparser is unable to parse "
424 "messageText into a ScheduleMessage" ) ) );
425 return 0;
426 }
427
428 icalproperty *m =
429 icalcomponent_get_first_property( message, ICAL_METHOD_PROPERTY );
430 if ( !m ) {
431 setException(
432 new ErrorFormat( ErrorFormat::ParseErrorKcal,
433 QLatin1String( "message does not contain an "
434 "ICAL_METHOD_PROPERTY" ) ) );
435 return 0;
436 }
437
438 // Populate the message's time zone collection with all VTIMEZONE components
439 ICalTimeZones tzlist;
440 ICalTimeZoneSource tzs;
441 tzs.parse( message, tzlist );
442
443 icalcomponent *c;
444
445 IncidenceBase *incidence = 0;
446 c = icalcomponent_get_first_component( message, ICAL_VEVENT_COMPONENT );
447 if ( c ) {
448 incidence = d->mImpl->readEvent( c, &tzlist );
449 }
450
451 if ( !incidence ) {
452 c = icalcomponent_get_first_component( message, ICAL_VTODO_COMPONENT );
453 if ( c ) {
454 incidence = d->mImpl->readTodo( c, &tzlist );
455 }
456 }
457
458 if ( !incidence ) {
459 c = icalcomponent_get_first_component( message, ICAL_VJOURNAL_COMPONENT );
460 if ( c ) {
461 incidence = d->mImpl->readJournal( c, &tzlist );
462 }
463 }
464
465 if ( !incidence ) {
466 c = icalcomponent_get_first_component( message, ICAL_VFREEBUSY_COMPONENT );
467 if ( c ) {
468 incidence = d->mImpl->readFreeBusy( c );
469 }
470 }
471
472 if ( !incidence ) {
473 kDebug() << "object is not a freebusy, event, todo or journal";
474 setException(
475 new ErrorFormat( ErrorFormat::ParseErrorKcal,
476 QLatin1String( "object is not a freebusy, event, "
477 "todo or journal" ) ) );
478 return 0;
479 }
480
481 icalproperty_method icalmethod = icalproperty_get_method( m );
482 iTIPMethod method;
483
484 switch ( icalmethod ) {
485 case ICAL_METHOD_PUBLISH:
486 method = iTIPPublish;
487 break;
488 case ICAL_METHOD_REQUEST:
489 method = iTIPRequest;
490 break;
491 case ICAL_METHOD_REFRESH:
492 method = iTIPRefresh;
493 break;
494 case ICAL_METHOD_CANCEL:
495 method = iTIPCancel;
496 break;
497 case ICAL_METHOD_ADD:
498 method = iTIPAdd;
499 break;
500 case ICAL_METHOD_REPLY:
501 method = iTIPReply;
502 break;
503 case ICAL_METHOD_COUNTER:
504 method = iTIPCounter;
505 break;
506 case ICAL_METHOD_DECLINECOUNTER:
507 method = iTIPDeclineCounter;
508 break;
509 default:
510 method = iTIPNoMethod;
511 kDebug() << "Unknown method";
512 break;
513 }
514
515 if ( !icalrestriction_check( message ) ) {
516 kWarning() << endl
517 << "kcal library reported a problem while parsing:";
518 kWarning() << Scheduler::translatedMethodName( method ) << ":"
519 << d->mImpl->extractErrorProperty( c );
520 }
521
522 Incidence *existingIncidence = cal->incidence( incidence->uid() );
523
524 icalcomponent *calendarComponent = 0;
525 if ( existingIncidence ) {
526 calendarComponent = d->mImpl->createCalendarComponent( cal );
527
528 // TODO: check, if cast is required, or if it can be done by virtual funcs.
529 // TODO: Use a visitor for this!
530 if ( existingIncidence->type() == "Todo" ) {
531 Todo *todo = static_cast<Todo *>( existingIncidence );
532 icalcomponent_add_component( calendarComponent,
533 d->mImpl->writeTodo( todo ) );
534 }
535 if ( existingIncidence->type() == "Event" ) {
536 Event *event = static_cast<Event *>( existingIncidence );
537 icalcomponent_add_component( calendarComponent,
538 d->mImpl->writeEvent( event ) );
539 }
540 } else {
541 icalcomponent_free( message );
542 return new ScheduleMessage( incidence, method, ScheduleMessage::Unknown );
543 }
544
545 icalproperty_xlicclass result =
546 icalclassify( message, calendarComponent, (const char *)"" );
547
548 ScheduleMessage::Status status;
549
550 switch ( result ) {
551 case ICAL_XLICCLASS_PUBLISHNEW:
552 status = ScheduleMessage::PublishNew;
553 break;
554 case ICAL_XLICCLASS_PUBLISHUPDATE:
555 status = ScheduleMessage::PublishUpdate;
556 break;
557 case ICAL_XLICCLASS_OBSOLETE:
558 status = ScheduleMessage::Obsolete;
559 break;
560 case ICAL_XLICCLASS_REQUESTNEW:
561 status = ScheduleMessage::RequestNew;
562 break;
563 case ICAL_XLICCLASS_REQUESTUPDATE:
564 status = ScheduleMessage::RequestUpdate;
565 break;
566 case ICAL_XLICCLASS_UNKNOWN:
567 default:
568 status = ScheduleMessage::Unknown;
569 break;
570 }
571
572 icalcomponent_free( message );
573 icalcomponent_free( calendarComponent );
574
575 return new ScheduleMessage( incidence, method, status );
576}
577
578void ICalFormat::setTimeSpec( const KDateTime::Spec &timeSpec )
579{
580 d->mTimeSpec = timeSpec;
581}
582
583KDateTime::Spec ICalFormat::timeSpec() const
584{
585 return d->mTimeSpec;
586}
587
588QString ICalFormat::timeZoneId() const
589{
590 KTimeZone tz = d->mTimeSpec.timeZone();
591 return tz.isValid() ? tz.name() : QString();
592}
593