1/*
2 This file is part of the kcal library.
3
4 Copyright (c) 2005-2007 David Jarvie <djarvie@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#include "icaltimezones.h"
23#include "icalformat.h"
24#include "icalformat_p.h"
25
26extern "C" {
27 #include <libical/ical.h>
28 #include <libical/icaltimezone.h>
29}
30#include <ksystemtimezone.h>
31#include <kdatetime.h>
32#include <kdebug.h>
33
34#include <QtCore/QDateTime>
35#include <QtCore/QString>
36#include <QtCore/QList>
37#include <QtCore/QVector>
38#include <QtCore/QSet>
39#include <QtCore/QFile>
40#include <QtCore/QTextStream>
41
42using namespace KCal;
43
44// Minimum repetition counts for VTIMEZONE RRULEs
45static const int minRuleCount = 5; // for any RRULE
46static const int minPhaseCount = 8; // for separate STANDARD/DAYLIGHT component
47
48// Convert an ical time to QDateTime, preserving the UTC indicator
49static QDateTime toQDateTime( const icaltimetype &t )
50{
51 return QDateTime( QDate( t.year, t.month, t.day ),
52 QTime( t.hour, t.minute, t.second ),
53 ( t.is_utc ? Qt::UTC : Qt::LocalTime ) );
54}
55
56// Maximum date for time zone data.
57// It's not sensible to try to predict them very far in advance, because
58// they can easily change. Plus, it limits the processing required.
59static QDateTime MAX_DATE()
60{
61 static QDateTime dt;
62 if ( !dt.isValid() ) {
63 dt = QDateTime( QDate::currentDate().addYears( 20 ), QTime( 0, 0, 0 ) );
64 }
65 return dt;
66}
67
68static icaltimetype writeLocalICalDateTime( const QDateTime &utc, int offset )
69{
70 QDateTime local = utc.addSecs( offset );
71 icaltimetype t = icaltime_null_time();
72 t.year = local.date().year();
73 t.month = local.date().month();
74 t.day = local.date().day();
75 t.hour = local.time().hour();
76 t.minute = local.time().minute();
77 t.second = local.time().second();
78 t.is_date = 0;
79 t.zone = 0;
80 t.is_utc = 0;
81 return t;
82}
83
84namespace KCal {
85
86/******************************************************************************/
87
88//@cond PRIVATE
89class ICalTimeZonesPrivate
90{
91 public:
92 ICalTimeZonesPrivate() {}
93 ICalTimeZones::ZoneMap zones;
94};
95//@endcond
96
97ICalTimeZones::ICalTimeZones()
98 : d( new ICalTimeZonesPrivate )
99{
100}
101
102ICalTimeZones::~ICalTimeZones()
103{
104 delete d;
105}
106
107const ICalTimeZones::ZoneMap ICalTimeZones::zones() const
108{
109 return d->zones;
110}
111
112bool ICalTimeZones::add( const ICalTimeZone &zone )
113{
114 if ( !zone.isValid() ) {
115 return false;
116 }
117 if ( d->zones.find( zone.name() ) != d->zones.end() ) {
118 return false; // name already exists
119 }
120
121 d->zones.insert( zone.name(), zone );
122 return true;
123}
124
125ICalTimeZone ICalTimeZones::remove( const ICalTimeZone &zone )
126{
127 if ( zone.isValid() ) {
128 for ( ZoneMap::Iterator it = d->zones.begin(), end = d->zones.end(); it != end; ++it ) {
129 if ( it.value() == zone ) {
130 d->zones.erase( it );
131 return ( zone == ICalTimeZone::utc() ) ? ICalTimeZone() : zone;
132 }
133 }
134 }
135 return ICalTimeZone();
136}
137
138ICalTimeZone ICalTimeZones::remove( const QString &name )
139{
140 if ( !name.isEmpty() ) {
141 ZoneMap::Iterator it = d->zones.find( name );
142 if ( it != d->zones.end() ) {
143 ICalTimeZone zone = it.value();
144 d->zones.erase(it);
145 return ( zone == ICalTimeZone::utc() ) ? ICalTimeZone() : zone;
146 }
147 }
148 return ICalTimeZone();
149}
150
151void ICalTimeZones::clear()
152{
153 d->zones.clear();
154}
155
156ICalTimeZone ICalTimeZones::zone( const QString &name ) const
157{
158 if ( !name.isEmpty() ) {
159 ZoneMap::ConstIterator it = d->zones.constFind( name );
160 if ( it != d->zones.constEnd() ) {
161 return it.value();
162 }
163 }
164 return ICalTimeZone(); // error
165}
166
167/******************************************************************************/
168
169ICalTimeZoneBackend::ICalTimeZoneBackend()
170 : KTimeZoneBackend()
171{}
172
173ICalTimeZoneBackend::ICalTimeZoneBackend( ICalTimeZoneSource *source,
174 const QString &name,
175 const QString &countryCode,
176 float latitude, float longitude,
177 const QString &comment )
178 : KTimeZoneBackend( source, name, countryCode, latitude, longitude, comment )
179{}
180
181ICalTimeZoneBackend::ICalTimeZoneBackend( const KTimeZone &tz, const QDate &earliest )
182 : KTimeZoneBackend( 0, tz.name(), tz.countryCode(), tz.latitude(), tz.longitude(), tz.comment() )
183{
184 Q_UNUSED( earliest );
185}
186
187ICalTimeZoneBackend::~ICalTimeZoneBackend()
188{}
189
190KTimeZoneBackend *ICalTimeZoneBackend::clone() const
191{
192 return new ICalTimeZoneBackend( *this );
193}
194
195QByteArray ICalTimeZoneBackend::type() const
196{
197 return "ICalTimeZone";
198}
199
200bool ICalTimeZoneBackend::hasTransitions( const KTimeZone *caller ) const
201{
202 Q_UNUSED( caller );
203 return true;
204}
205
206/******************************************************************************/
207
208ICalTimeZone::ICalTimeZone()
209 : KTimeZone( new ICalTimeZoneBackend() )
210{}
211
212ICalTimeZone::ICalTimeZone( ICalTimeZoneSource *source, const QString &name,
213 ICalTimeZoneData *data )
214 : KTimeZone( new ICalTimeZoneBackend( source, name ) )
215{
216 setData( data );
217}
218
219ICalTimeZone::ICalTimeZone( const KTimeZone &tz, const QDate &earliest )
220 : KTimeZone( new ICalTimeZoneBackend( 0, tz.name(), tz.countryCode(),
221 tz.latitude(), tz.longitude(),
222 tz.comment() ) )
223{
224 const KTimeZoneData *data = tz.data( true );
225 if ( data ) {
226 const ICalTimeZoneData *icaldata = dynamic_cast<const ICalTimeZoneData*>( data );
227 if ( icaldata ) {
228 setData( new ICalTimeZoneData( *icaldata ) );
229 } else {
230 setData( new ICalTimeZoneData( *data, tz, earliest ) );
231 }
232 }
233}
234
235ICalTimeZone::~ICalTimeZone()
236{}
237
238QString ICalTimeZone::city() const
239{
240 const ICalTimeZoneData *dat = static_cast<const ICalTimeZoneData*>( data() );
241 return dat ? dat->city() : QString();
242}
243
244QByteArray ICalTimeZone::url() const
245{
246 const ICalTimeZoneData *dat = static_cast<const ICalTimeZoneData*>( data() );
247 return dat ? dat->url() : QByteArray();
248}
249
250QDateTime ICalTimeZone::lastModified() const
251{
252 const ICalTimeZoneData *dat = static_cast<const ICalTimeZoneData*>( data() );
253 return dat ? dat->lastModified() : QDateTime();
254}
255
256QByteArray ICalTimeZone::vtimezone() const
257{
258 const ICalTimeZoneData *dat = static_cast<const ICalTimeZoneData*>( data() );
259 return dat ? dat->vtimezone() : QByteArray();
260}
261
262icaltimezone *ICalTimeZone::icalTimezone() const
263{
264 const ICalTimeZoneData *dat = static_cast<const ICalTimeZoneData*>( data() );
265 return dat ? dat->icalTimezone() : 0;
266}
267
268bool ICalTimeZone::update( const ICalTimeZone &other )
269{
270 if ( !updateBase( other ) ) {
271 return false;
272 }
273
274 KTimeZoneData *otherData = other.data() ? other.data()->clone() : 0;
275 setData( otherData, other.source() );
276 return true;
277}
278
279ICalTimeZone ICalTimeZone::utc()
280{
281 static ICalTimeZone utcZone;
282 if ( !utcZone.isValid() ) {
283 ICalTimeZoneSource tzs;
284 utcZone = tzs.parse( icaltimezone_get_utc_timezone() );
285 }
286 return utcZone;
287}
288
289/******************************************************************************/
290
291//@cond PRIVATE
292class ICalTimeZoneDataPrivate
293{
294 public:
295 ICalTimeZoneDataPrivate() : icalComponent(0) {}
296 ~ICalTimeZoneDataPrivate()
297 {
298 if ( icalComponent ) {
299 icalcomponent_free( icalComponent );
300 }
301 }
302 icalcomponent *component() const { return icalComponent; }
303 void setComponent( icalcomponent *c )
304 {
305 if ( icalComponent ) {
306 icalcomponent_free( icalComponent );
307 }
308 icalComponent = c;
309 }
310 QString location; // name of city for this time zone
311 QByteArray url; // URL of published VTIMEZONE definition (optional)
312 QDateTime lastModified; // time of last modification of the VTIMEZONE component (optional)
313 private:
314 icalcomponent *icalComponent; // ical component representing this time zone
315};
316//@endcond
317
318ICalTimeZoneData::ICalTimeZoneData()
319 : d ( new ICalTimeZoneDataPrivate() )
320{
321}
322
323ICalTimeZoneData::ICalTimeZoneData( const ICalTimeZoneData &rhs )
324 : KTimeZoneData( rhs ),
325 d( new ICalTimeZoneDataPrivate() )
326{
327 d->location = rhs.d->location;
328 d->url = rhs.d->url;
329 d->lastModified = rhs.d->lastModified;
330 d->setComponent( icalcomponent_new_clone( rhs.d->component() ) );
331}
332
333ICalTimeZoneData::ICalTimeZoneData( const KTimeZoneData &rhs,
334 const KTimeZone &tz, const QDate &earliest )
335 : KTimeZoneData( rhs ),
336 d( new ICalTimeZoneDataPrivate() )
337{
338 // VTIMEZONE RRULE types
339 enum {
340 DAY_OF_MONTH = 0x01,
341 WEEKDAY_OF_MONTH = 0x02,
342 LAST_WEEKDAY_OF_MONTH = 0x04
343 };
344
345 if ( tz.type() == "KSystemTimeZone" ) {
346 // Try to fetch a system time zone in preference, on the grounds
347 // that system time zones are more likely to be up to date than
348 // built-in libical ones.
349 icalcomponent *c = 0;
350 KTimeZone ktz = KSystemTimeZones::readZone( tz.name() );
351 if ( ktz.isValid() ) {
352 if ( ktz.data(true) ) {
353 ICalTimeZone icaltz( ktz, earliest );
354 icaltimezone *itz = icaltz.icalTimezone();
355 c = icalcomponent_new_clone( icaltimezone_get_component( itz ) );
356 icaltimezone_free( itz, 1 );
357 }
358 }
359 if ( !c ) {
360 // Try to fetch a built-in libical time zone.
361 icaltimezone *itz = icaltimezone_get_builtin_timezone( tz.name().toUtf8() );
362 c = icalcomponent_new_clone( icaltimezone_get_component( itz ) );
363 }
364 if ( c ) {
365 // TZID in built-in libical time zones has a standard prefix.
366 // To make the VTIMEZONE TZID match TZID references in incidences
367 // (as required by RFC2445), strip off the prefix.
368 icalproperty *prop = icalcomponent_get_first_property( c, ICAL_TZID_PROPERTY );
369 if ( prop ) {
370 icalvalue *value = icalproperty_get_value( prop );
371 const char *tzid = icalvalue_get_text( value );
372 QByteArray icalprefix = ICalTimeZoneSource::icalTzidPrefix();
373 int len = icalprefix.size();
374 if ( !strncmp( icalprefix, tzid, len ) ) {
375 const char *s = strchr( tzid + len, '/' ); // find third '/'
376 if ( s ) {
377 QByteArray tzidShort( s + 1 ); // deep copy of string (needed by icalvalue_set_text())
378 icalvalue_set_text( value, tzidShort );
379
380 // Remove the X-LIC-LOCATION property, which is only used by libical
381 prop = icalcomponent_get_first_property( c, ICAL_X_PROPERTY );
382 const char *xname = icalproperty_get_x_name( prop );
383 if ( xname && !strcmp( xname, "X-LIC-LOCATION" ) ) {
384 icalcomponent_remove_property( c, prop );
385 }
386 }
387 }
388 }
389 }
390 d->setComponent( c );
391 } else {
392 // Write the time zone data into an iCal component
393 icalcomponent *tzcomp = icalcomponent_new(ICAL_VTIMEZONE_COMPONENT);
394 icalcomponent_add_property( tzcomp, icalproperty_new_tzid( tz.name().toUtf8() ) );
395// icalcomponent_add_property(tzcomp, icalproperty_new_location( tz.name().toUtf8() ));
396
397 // Compile an ordered list of transitions so that we can know the phases
398 // which occur before and after each transition.
399 QList<KTimeZone::Transition> transits = transitions();
400 if ( earliest.isValid() ) {
401 // Remove all transitions earlier than those we are interested in
402 for ( int i = 0, end = transits.count(); i < end; ++i ) {
403 if ( transits[i].time().date() >= earliest ) {
404 if ( i > 0 ) {
405 transits.erase( transits.begin(), transits.begin() + i );
406 }
407 break;
408 }
409 }
410 }
411 int trcount = transits.count();
412 QVector<bool> transitionsDone(trcount);
413 transitionsDone.fill(false);
414
415 // Go through the list of transitions and create an iCal component for each
416 // distinct combination of phase after and UTC offset before the transition.
417 icaldatetimeperiodtype dtperiod;
418 dtperiod.period = icalperiodtype_null_period();
419 for ( ; ; ) {
420 int i = 0;
421 for ( ; i < trcount && transitionsDone[i]; ++i ) {
422 ;
423 }
424 if ( i >= trcount ) {
425 break;
426 }
427 // Found a phase combination which hasn't yet been processed
428 int preOffset = ( i > 0 ) ? transits[i - 1].phase().utcOffset() : rhs.previousUtcOffset();
429 KTimeZone::Phase phase = transits[i].phase();
430 if ( phase.utcOffset() == preOffset ) {
431 transitionsDone[i] = true;
432 while ( ++i < trcount ) {
433 if ( transitionsDone[i] ||
434 transits[i].phase() != phase ||
435 transits[i - 1].phase().utcOffset() != preOffset ) {
436 continue;
437 }
438 transitionsDone[i] = true;
439 }
440 continue;
441 }
442 icalcomponent *phaseComp =
443 icalcomponent_new( phase.isDst() ? ICAL_XDAYLIGHT_COMPONENT : ICAL_XSTANDARD_COMPONENT );
444 QList<QByteArray> abbrevs = phase.abbreviations();
445 for ( int a = 0, aend = abbrevs.count(); a < aend; ++a ) {
446 icalcomponent_add_property( phaseComp,
447 icalproperty_new_tzname(
448 static_cast<const char*>( abbrevs[a]) ) );
449 }
450 if ( !phase.comment().isEmpty() ) {
451 icalcomponent_add_property( phaseComp,
452 icalproperty_new_comment( phase.comment().toUtf8() ) );
453 }
454 icalcomponent_add_property( phaseComp,
455 icalproperty_new_tzoffsetfrom( preOffset ) );
456 icalcomponent_add_property( phaseComp,
457 icalproperty_new_tzoffsetto( phase.utcOffset() ) );
458 // Create a component to hold initial RRULE if any, plus all RDATEs
459 icalcomponent *phaseComp1 = icalcomponent_new_clone( phaseComp );
460 icalcomponent_add_property( phaseComp1,
461 icalproperty_new_dtstart(
462 writeLocalICalDateTime( transits[i].time(), preOffset ) ) );
463 bool useNewRRULE = false;
464
465 // Compile the list of UTC transition dates/times, and check
466 // if the list can be reduced to an RRULE instead of multiple RDATEs.
467 QTime time;
468 QDate date;
469 int year = 0, month = 0, daysInMonth = 0, dayOfMonth = 0; // avoid compiler warnings
470 int dayOfWeek = 0; // Monday = 1
471 int nthFromStart = 0; // nth (weekday) of month
472 int nthFromEnd = 0; // nth last (weekday) of month
473 int newRule;
474 int rule = 0;
475 QList<QDateTime> rdates;// dates which (probably) need to be written as RDATEs
476 QList<QDateTime> times;
477 QDateTime qdt = transits[i].time(); // set 'qdt' for start of loop
478 times += qdt;
479 transitionsDone[i] = true;
480 do {
481 if ( !rule ) {
482 // Initialise data for detecting a new rule
483 rule = DAY_OF_MONTH | WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH;
484 time = qdt.time();
485 date = qdt.date();
486 year = date.year();
487 month = date.month();
488 daysInMonth = date.daysInMonth();
489 dayOfWeek = date.dayOfWeek(); // Monday = 1
490 dayOfMonth = date.day();
491 nthFromStart = ( dayOfMonth - 1 ) / 7 + 1; // nth (weekday) of month
492 nthFromEnd = ( daysInMonth - dayOfMonth ) / 7 + 1; // nth last (weekday) of month
493 }
494 if ( ++i >= trcount ) {
495 newRule = 0;
496 times += QDateTime(); // append a dummy value since last value in list is ignored
497 } else {
498 if ( transitionsDone[i] ||
499 transits[i].phase() != phase ||
500 transits[i - 1].phase().utcOffset() != preOffset ) {
501 continue;
502 }
503 transitionsDone[i] = true;
504 qdt = transits[i].time();
505 if ( !qdt.isValid() ) {
506 continue;
507 }
508 newRule = rule;
509 times += qdt;
510 date = qdt.date();
511 if ( qdt.time() != time ||
512 date.month() != month ||
513 date.year() != ++year ) {
514 newRule = 0;
515 } else {
516 int day = date.day();
517 if ( ( newRule & DAY_OF_MONTH ) && day != dayOfMonth ) {
518 newRule &= ~DAY_OF_MONTH;
519 }
520 if ( newRule & ( WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH ) ) {
521 if ( date.dayOfWeek() != dayOfWeek ) {
522 newRule &= ~( WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH );
523 } else {
524 if ( ( newRule & WEEKDAY_OF_MONTH ) &&
525 ( day - 1 ) / 7 + 1 != nthFromStart ) {
526 newRule &= ~WEEKDAY_OF_MONTH;
527 }
528 if ( ( newRule & LAST_WEEKDAY_OF_MONTH ) &&
529 ( daysInMonth - day ) / 7 + 1 != nthFromEnd ) {
530 newRule &= ~LAST_WEEKDAY_OF_MONTH;
531 }
532 }
533 }
534 }
535 }
536 if ( !newRule ) {
537 // The previous rule (if any) no longer applies.
538 // Write all the times up to but not including the current one.
539 // First check whether any of the last RDATE values fit this rule.
540 int yr = times[0].date().year();
541 while ( !rdates.isEmpty() ) {
542 qdt = rdates.last();
543 date = qdt.date();
544 if ( qdt.time() != time ||
545 date.month() != month ||
546 date.year() != --yr ) {
547 break;
548 }
549 int day = date.day();
550 if ( rule & DAY_OF_MONTH ) {
551 if ( day != dayOfMonth ) {
552 break;
553 }
554 } else {
555 if ( date.dayOfWeek() != dayOfWeek ||
556 ( ( rule & WEEKDAY_OF_MONTH ) &&
557 ( day - 1 ) / 7 + 1 != nthFromStart ) ||
558 ( ( rule & LAST_WEEKDAY_OF_MONTH ) &&
559 ( daysInMonth - day ) / 7 + 1 != nthFromEnd ) ) {
560 break;
561 }
562 }
563 times.prepend( qdt );
564 rdates.pop_back();
565 }
566 if ( times.count() > ( useNewRRULE ? minPhaseCount : minRuleCount ) ) {
567 // There are enough dates to combine into an RRULE
568 icalrecurrencetype r;
569 icalrecurrencetype_clear( &r );
570 r.freq = ICAL_YEARLY_RECURRENCE;
571 r.count = ( year >= 2030 ) ? 0 : times.count() - 1;
572 r.by_month[0] = month;
573 if ( rule & DAY_OF_MONTH ) {
574 r.by_month_day[0] = dayOfMonth;
575 } else if ( rule & WEEKDAY_OF_MONTH ) {
576 r.by_day[0] = ( dayOfWeek % 7 + 1 ) + ( nthFromStart * 8 ); // Sunday = 1
577 } else if ( rule & LAST_WEEKDAY_OF_MONTH ) {
578 r.by_day[0] = -( dayOfWeek % 7 + 1 ) - ( nthFromEnd * 8 ); // Sunday = 1
579 }
580 icalproperty *prop = icalproperty_new_rrule( r );
581 if ( useNewRRULE ) {
582 // This RRULE doesn't start from the phase start date, so set it into
583 // a new STANDARD/DAYLIGHT component in the VTIMEZONE.
584 icalcomponent *c = icalcomponent_new_clone( phaseComp );
585 icalcomponent_add_property(
586 c, icalproperty_new_dtstart( writeLocalICalDateTime( times[0], preOffset ) ) );
587 icalcomponent_add_property( c, prop );
588 icalcomponent_add_component( tzcomp, c );
589 } else {
590 icalcomponent_add_property( phaseComp1, prop );
591 }
592 } else {
593 // Save dates for writing as RDATEs
594 for ( int t = 0, tend = times.count() - 1; t < tend; ++t ) {
595 rdates += times[t];
596 }
597 }
598 useNewRRULE = true;
599 // All date/time values but the last have been added to the VTIMEZONE.
600 // Remove them from the list.
601 qdt = times.last(); // set 'qdt' for start of loop
602 times.clear();
603 times += qdt;
604 }
605 rule = newRule;
606 } while ( i < trcount );
607
608 // Write remaining dates as RDATEs
609 for ( int rd = 0, rdend = rdates.count(); rd < rdend; ++rd ) {
610 dtperiod.time = writeLocalICalDateTime( rdates[rd], preOffset );
611 icalcomponent_add_property( phaseComp1, icalproperty_new_rdate( dtperiod ) );
612 }
613 icalcomponent_add_component( tzcomp, phaseComp1 );
614 icalcomponent_free( phaseComp );
615 }
616
617 d->setComponent( tzcomp );
618 }
619}
620
621ICalTimeZoneData::~ICalTimeZoneData()
622{
623 delete d;
624}
625
626ICalTimeZoneData &ICalTimeZoneData::operator=( const ICalTimeZoneData &rhs )
627{
628 // check for self assignment
629 if ( &rhs == this ) {
630 return *this;
631 }
632
633 KTimeZoneData::operator=( rhs );
634 d->location = rhs.d->location;
635 d->url = rhs.d->url;
636 d->lastModified = rhs.d->lastModified;
637 d->setComponent( icalcomponent_new_clone( rhs.d->component() ) );
638 return *this;
639}
640
641KTimeZoneData *ICalTimeZoneData::clone() const
642{
643 return new ICalTimeZoneData( *this );
644}
645
646QString ICalTimeZoneData::city() const
647{
648 return d->location;
649}
650
651QByteArray ICalTimeZoneData::url() const
652{
653 return d->url;
654}
655
656QDateTime ICalTimeZoneData::lastModified() const
657{
658 return d->lastModified;
659}
660
661QByteArray ICalTimeZoneData::vtimezone() const
662{
663 QByteArray result( icalcomponent_as_ical_string( d->component() ) );
664 icalmemory_free_ring();
665 return result;
666}
667
668icaltimezone *ICalTimeZoneData::icalTimezone() const
669{
670 icaltimezone *icaltz = icaltimezone_new();
671 if ( !icaltz ) {
672 return 0;
673 }
674 icalcomponent *c = icalcomponent_new_clone( d->component() );
675 if ( !icaltimezone_set_component( icaltz, c ) ) {
676 icalcomponent_free( c );
677 icaltimezone_free( icaltz, 1 );
678 return 0;
679 }
680 return icaltz;
681}
682
683bool ICalTimeZoneData::hasTransitions() const
684{
685 return true;
686}
687
688/******************************************************************************/
689
690//@cond PRIVATE
691class ICalTimeZoneSourcePrivate
692{
693 public:
694 static QList<QDateTime> parsePhase( icalcomponent *, bool daylight,
695 int &prevOffset, KTimeZone::Phase & );
696 static QByteArray icalTzidPrefix;
697};
698
699QByteArray ICalTimeZoneSourcePrivate::icalTzidPrefix;
700//@endcond
701
702ICalTimeZoneSource::ICalTimeZoneSource()
703 : KTimeZoneSource( false ),
704 d( 0 )
705{
706}
707
708ICalTimeZoneSource::~ICalTimeZoneSource()
709{
710}
711
712bool ICalTimeZoneSource::parse( const QString &fileName, ICalTimeZones &zones )
713{
714 QFile file( fileName );
715 if ( !file.open( QIODevice::ReadOnly ) ) {
716 return false;
717 }
718 QTextStream ts( &file );
719 ts.setCodec( "ISO 8859-1" );
720 QByteArray text = ts.readAll().trimmed().toLatin1();
721 file.close();
722
723 bool result = false;
724 icalcomponent *calendar = icalcomponent_new_from_string( text.data() );
725 if ( calendar ) {
726 if ( icalcomponent_isa( calendar ) == ICAL_VCALENDAR_COMPONENT ) {
727 result = parse( calendar, zones );
728 }
729 icalcomponent_free( calendar );
730 }
731 return result;
732}
733
734bool ICalTimeZoneSource::parse( icalcomponent *calendar, ICalTimeZones &zones )
735{
736 for ( icalcomponent *c = icalcomponent_get_first_component( calendar, ICAL_VTIMEZONE_COMPONENT );
737 c; c = icalcomponent_get_next_component( calendar, ICAL_VTIMEZONE_COMPONENT ) ) {
738 ICalTimeZone zone = parse( c );
739 if ( !zone.isValid() ) {
740 return false;
741 }
742 ICalTimeZone oldzone = zones.zone( zone.name() );
743 if ( oldzone.isValid() ) {
744 // The zone already exists in the collection, so update the definition
745 // of the zone rather than using a newly created one.
746 oldzone.update( zone );
747 } else if ( !zones.add( zone ) ) {
748 return false;
749 }
750 }
751 return true;
752}
753
754ICalTimeZone ICalTimeZoneSource::parse( icalcomponent *vtimezone )
755{
756 QString name;
757 QString xlocation;
758 ICalTimeZoneData *data = new ICalTimeZoneData();
759
760 // Read the fixed properties which can only appear once in VTIMEZONE
761 icalproperty *p = icalcomponent_get_first_property( vtimezone, ICAL_ANY_PROPERTY );
762 while ( p ) {
763 icalproperty_kind kind = icalproperty_isa( p );
764 switch ( kind ) {
765
766 case ICAL_TZID_PROPERTY:
767 name = QString::fromUtf8( icalproperty_get_tzid( p ) );
768 break;
769
770 case ICAL_TZURL_PROPERTY:
771 data->d->url = icalproperty_get_tzurl( p );
772 break;
773
774 case ICAL_LOCATION_PROPERTY:
775 // This isn't mentioned in RFC2445, but libical reads it ...
776 data->d->location = QString::fromUtf8( icalproperty_get_location( p ) );
777 break;
778
779 case ICAL_X_PROPERTY:
780 { // use X-LIC-LOCATION if LOCATION is missing
781 const char *xname = icalproperty_get_x_name( p );
782 if ( xname && !strcmp( xname, "X-LIC-LOCATION" ) ) {
783 xlocation = QString::fromUtf8( icalproperty_get_x( p ) );
784 }
785 break;
786 }
787 case ICAL_LASTMODIFIED_PROPERTY:
788 {
789 icaltimetype t = icalproperty_get_lastmodified(p);
790 if ( t.is_utc ) {
791 data->d->lastModified = toQDateTime( t );
792 } else {
793 kDebug() << "LAST-MODIFIED not UTC";
794 }
795 break;
796 }
797 default:
798 break;
799 }
800 p = icalcomponent_get_next_property( vtimezone, ICAL_ANY_PROPERTY );
801 }
802
803 if ( name.isEmpty() ) {
804 kDebug() << "TZID missing";
805 delete data;
806 return ICalTimeZone();
807 }
808 if ( data->d->location.isEmpty() && !xlocation.isEmpty() ) {
809 data->d->location = xlocation;
810 }
811 QString prefix = QString::fromUtf8( icalTzidPrefix() );
812 if ( name.startsWith( prefix ) ) {
813 // Remove the prefix from libical built in time zone TZID
814 int i = name.indexOf( '/', prefix.length() );
815 if ( i > 0 ) {
816 name = name.mid( i + 1 );
817 }
818 }
819 //kDebug() << "---zoneId: \"" << name << '"';
820
821 /*
822 * Iterate through all time zone rules for this VTIMEZONE,
823 * and create a Phase object containing details for each one.
824 */
825 int prevOffset = 0;
826 QList<KTimeZone::Transition> transitions;
827 QDateTime earliest;
828 QList<KTimeZone::Phase> phases;
829 for ( icalcomponent *c = icalcomponent_get_first_component( vtimezone, ICAL_ANY_COMPONENT );
830 c; c = icalcomponent_get_next_component( vtimezone, ICAL_ANY_COMPONENT ) )
831 {
832 int prevoff = 0;
833 KTimeZone::Phase phase;
834 QList<QDateTime> times;
835 icalcomponent_kind kind = icalcomponent_isa( c );
836 switch ( kind ) {
837
838 case ICAL_XSTANDARD_COMPONENT:
839 //kDebug() << "---standard phase: found";
840 times = ICalTimeZoneSourcePrivate::parsePhase( c, false, prevoff, phase );
841 break;
842
843 case ICAL_XDAYLIGHT_COMPONENT:
844 //kDebug() << "---daylight phase: found";
845 times = ICalTimeZoneSourcePrivate::parsePhase( c, true, prevoff, phase );
846 break;
847
848 default:
849 kDebug() << "Unknown component:" << kind;
850 break;
851 }
852 int tcount = times.count();
853 if ( tcount ) {
854 phases += phase;
855 for ( int t = 0; t < tcount; ++t ) {
856 transitions += KTimeZone::Transition( times[t], phase );
857 }
858 if ( !earliest.isValid() || times[0] < earliest ) {
859 prevOffset = prevoff;
860 earliest = times[0];
861 }
862 }
863 }
864 data->setPhases( phases, prevOffset );
865 // Remove any "duplicate" transitions, i.e. those where two consecutive
866 // transitions have the same phase.
867 qSort( transitions );
868 for ( int t = 1, tend = transitions.count(); t < tend; ) {
869 if ( transitions[t].phase() == transitions[t - 1].phase() ) {
870 transitions.removeAt( t );
871 --tend;
872 } else {
873 ++t;
874 }
875 }
876 data->setTransitions( transitions );
877
878 data->d->setComponent( icalcomponent_new_clone( vtimezone ) );
879 kDebug() << "VTIMEZONE" << name;
880 return ICalTimeZone( this, name, data );
881}
882
883ICalTimeZone ICalTimeZoneSource::parse( icaltimezone *tz )
884{
885 /* Parse the VTIMEZONE component stored in the icaltimezone structure.
886 * This is both easier and provides more complete information than
887 * extracting already parsed data from icaltimezone.
888 */
889 return tz ? parse( icaltimezone_get_component( tz ) ) : ICalTimeZone();
890}
891
892//@cond PRIVATE
893QList<QDateTime> ICalTimeZoneSourcePrivate::parsePhase( icalcomponent *c,
894 bool daylight,
895 int &prevOffset,
896 KTimeZone::Phase &phase )
897{
898 QList<QDateTime> transitions;
899
900 // Read the observance data for this standard/daylight savings phase
901 QList<QByteArray> abbrevs;
902 QString comment;
903 prevOffset = 0;
904 int utcOffset = 0;
905 bool recurs = false;
906 bool found_dtstart = false;
907 bool found_tzoffsetfrom = false;
908 bool found_tzoffsetto = false;
909 icaltimetype dtstart = icaltime_null_time();
910
911 // Now do the ical reading.
912 icalproperty *p = icalcomponent_get_first_property( c, ICAL_ANY_PROPERTY );
913 while ( p ) {
914 icalproperty_kind kind = icalproperty_isa( p );
915 switch ( kind ) {
916
917 case ICAL_TZNAME_PROPERTY: // abbreviated name for this time offset
918 {
919 // TZNAME can appear multiple times in order to provide language
920 // translations of the time zone offset name.
921
922 // TODO: Does this cope with multiple language specifications?
923 QByteArray tzname = icalproperty_get_tzname( p );
924 // Outlook (2000) places "Standard Time" and "Daylight Time" in the TZNAME
925 // strings, which is totally useless. So ignore those.
926 if ( ( !daylight && tzname == "Standard Time" ) ||
927 ( daylight && tzname == "Daylight Time" ) ) {
928 break;
929 }
930 if ( !abbrevs.contains( tzname ) ) {
931 abbrevs += tzname;
932 }
933 break;
934 }
935 case ICAL_DTSTART_PROPERTY: // local time at which phase starts
936 dtstart = icalproperty_get_dtstart( p );
937 found_dtstart = true;
938 break;
939
940 case ICAL_TZOFFSETFROM_PROPERTY: // UTC offset immediately before start of phase
941 prevOffset = icalproperty_get_tzoffsetfrom( p );
942 found_tzoffsetfrom = true;
943 break;
944
945 case ICAL_TZOFFSETTO_PROPERTY:
946 utcOffset = icalproperty_get_tzoffsetto( p );
947 found_tzoffsetto = true;
948 break;
949
950 case ICAL_COMMENT_PROPERTY:
951 comment = QString::fromUtf8( icalproperty_get_comment( p ) );
952 break;
953
954 case ICAL_RDATE_PROPERTY:
955 case ICAL_RRULE_PROPERTY:
956 recurs = true;
957 break;
958
959 default:
960 kDebug() << "Unknown property:" << kind;
961 break;
962 }
963 p = icalcomponent_get_next_property( c, ICAL_ANY_PROPERTY );
964 }
965
966 // Validate the phase data
967 if ( !found_dtstart || !found_tzoffsetfrom || !found_tzoffsetto ) {
968 kDebug() << "DTSTART/TZOFFSETFROM/TZOFFSETTO missing";
969 return transitions;
970 }
971
972 // Convert DTSTART to QDateTime, and from local time to UTC
973 QDateTime localStart = toQDateTime( dtstart ); // local time
974 dtstart.second -= prevOffset;
975 dtstart.is_utc = 1;
976 QDateTime utcStart = toQDateTime( icaltime_normalize( dtstart ) ); // UTC
977
978 transitions += utcStart;
979 if ( recurs ) {
980 /* RDATE or RRULE is specified. There should only be one or the other, but
981 * it doesn't really matter - the code can cope with both.
982 * Note that we had to get DTSTART, TZOFFSETFROM, TZOFFSETTO before reading
983 * recurrences.
984 */
985 KDateTime klocalStart( localStart, KDateTime::Spec::ClockTime() );
986 KDateTime maxTime( MAX_DATE(), KDateTime::Spec::ClockTime() );
987 Recurrence recur;
988 icalproperty *p = icalcomponent_get_first_property( c, ICAL_ANY_PROPERTY );
989 while ( p ) {
990 icalproperty_kind kind = icalproperty_isa( p );
991 switch ( kind ) {
992
993 case ICAL_RDATE_PROPERTY:
994 {
995 icaltimetype t = icalproperty_get_rdate(p).time;
996 if ( icaltime_is_date( t ) ) {
997 // RDATE with a DATE value inherits the (local) time from DTSTART
998 t.hour = dtstart.hour;
999 t.minute = dtstart.minute;
1000 t.second = dtstart.second;
1001 t.is_date = 0;
1002 t.is_utc = 0; // dtstart is in local time
1003 }
1004 // RFC2445 states that RDATE must be in local time,
1005 // but we support UTC as well to be safe.
1006 if ( !t.is_utc ) {
1007 t.second -= prevOffset; // convert to UTC
1008 t.is_utc = 1;
1009 t = icaltime_normalize( t );
1010 }
1011 transitions += toQDateTime( t );
1012 break;
1013 }
1014 case ICAL_RRULE_PROPERTY:
1015 {
1016 RecurrenceRule r;
1017 ICalFormat icf;
1018 ICalFormatImpl impl( &icf );
1019 impl.readRecurrence( icalproperty_get_rrule( p ), &r );
1020 r.setStartDt( klocalStart );
1021 // The end date time specified in an RRULE should be in UTC.
1022 // Convert to local time to avoid timesInInterval() getting things wrong.
1023 if ( r.duration() == 0 ) {
1024 KDateTime end( r.endDt() );
1025 if ( end.timeSpec() == KDateTime::Spec::UTC() ) {
1026 end.setTimeSpec( KDateTime::Spec::ClockTime() );
1027 r.setEndDt( end.addSecs( prevOffset ) );
1028 }
1029 }
1030 DateTimeList dts = r.timesInInterval( klocalStart, maxTime );
1031 for ( int i = 0, end = dts.count(); i < end; ++i ) {
1032 QDateTime utc = dts[i].dateTime();
1033 utc.setTimeSpec( Qt::UTC );
1034 transitions += utc.addSecs( -prevOffset );
1035 }
1036 break;
1037 }
1038 default:
1039 break;
1040 }
1041 p = icalcomponent_get_next_property( c, ICAL_ANY_PROPERTY );
1042 }
1043 qSortUnique( transitions );
1044 }
1045
1046 phase = KTimeZone::Phase( utcOffset, abbrevs, daylight, comment );
1047 return transitions;
1048}
1049//@endcond
1050
1051ICalTimeZone ICalTimeZoneSource::standardZone( const QString &zone, bool icalBuiltIn )
1052{
1053 if ( !icalBuiltIn ) {
1054 // Try to fetch a system time zone in preference, on the grounds
1055 // that system time zones are more likely to be up to date than
1056 // built-in libical ones.
1057 QString tzid = zone;
1058 QString prefix = QString::fromUtf8( icalTzidPrefix() );
1059 if ( zone.startsWith( prefix ) ) {
1060 int i = zone.indexOf( '/', prefix.length() );
1061 if ( i > 0 ) {
1062 tzid = zone.mid( i + 1 ); // strip off the libical prefix
1063 }
1064 }
1065 KTimeZone ktz = KSystemTimeZones::readZone( tzid );
1066 if ( ktz.isValid() ) {
1067 if ( ktz.data( true ) ) {
1068 ICalTimeZone icaltz( ktz );
1069 //kDebug() << zone << " read from system database";
1070 return icaltz;
1071 }
1072 }
1073 }
1074 // Try to fetch a built-in libical time zone.
1075 // First try to look it up as a geographical location (e.g. Europe/London)
1076 QByteArray zoneName = zone.toUtf8();
1077 icaltimezone *icaltz = icaltimezone_get_builtin_timezone( zoneName );
1078 if ( !icaltz ) {
1079 // This will find it if it includes the libical prefix
1080 icaltz = icaltimezone_get_builtin_timezone_from_tzid( zoneName );
1081 if ( !icaltz ) {
1082 return ICalTimeZone();
1083 }
1084 }
1085 return parse( icaltz );
1086}
1087
1088QByteArray ICalTimeZoneSource::icalTzidPrefix()
1089{
1090 if ( ICalTimeZoneSourcePrivate::icalTzidPrefix.isEmpty() ) {
1091 icaltimezone *icaltz = icaltimezone_get_builtin_timezone( "Europe/London" );
1092 QByteArray tzid = icaltimezone_get_tzid( icaltz );
1093 if ( tzid.right( 13 ) == "Europe/London" ) {
1094 int i = tzid.indexOf( '/', 1 );
1095 if ( i > 0 ) {
1096 ICalTimeZoneSourcePrivate::icalTzidPrefix = tzid.left( i + 1 );
1097 return ICalTimeZoneSourcePrivate::icalTzidPrefix;
1098 }
1099 }
1100 kError() << "failed to get libical TZID prefix";
1101 }
1102 return ICalTimeZoneSourcePrivate::icalTzidPrefix;
1103}
1104
1105} // namespace KCal
1106