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 | |
38 | extern "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 | |
59 | using namespace KCal; |
60 | |
61 | //@cond PRIVATE |
62 | class 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 | |
75 | ICalFormat::ICalFormat() |
76 | : d( new Private( this ) ) |
77 | { |
78 | } |
79 | |
80 | ICalFormat::~ICalFormat() |
81 | { |
82 | delete d; |
83 | } |
84 | |
85 | bool 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 | |
110 | bool 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 | |
146 | bool ICalFormat::fromString( Calendar *cal, const QString &string ) |
147 | { |
148 | return fromRawString( cal, string.toUtf8() ); |
149 | } |
150 | |
151 | bool 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 | |
205 | Incidence *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 | |
229 | QString 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 | |
290 | QString ICalFormat::toICalString( Incidence *incidence ) |
291 | { |
292 | CalendarLocal cal( d->mTimeSpec ); |
293 | cal.addIncidence( incidence->clone() ); |
294 | return toString( &cal ); |
295 | } |
296 | |
297 | QString 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 | |
310 | QString 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 | |
319 | bool 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 | |
339 | QString 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 | |
371 | FreeBusy *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 | |
403 | ScheduleMessage *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 | |
578 | void ICalFormat::setTimeSpec( const KDateTime::Spec &timeSpec ) |
579 | { |
580 | d->mTimeSpec = timeSpec; |
581 | } |
582 | |
583 | KDateTime::Spec ICalFormat::timeSpec() const |
584 | { |
585 | return d->mTimeSpec; |
586 | } |
587 | |
588 | QString ICalFormat::timeZoneId() const |
589 | { |
590 | KTimeZone tz = d->mTimeSpec.timeZone(); |
591 | return tz.isValid() ? tz.name() : QString(); |
592 | } |
593 | |