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 | |
26 | extern "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 | |
42 | using namespace KCal; |
43 | |
44 | // Minimum repetition counts for VTIMEZONE RRULEs |
45 | static const int minRuleCount = 5; // for any RRULE |
46 | static const int minPhaseCount = 8; // for separate STANDARD/DAYLIGHT component |
47 | |
48 | // Convert an ical time to QDateTime, preserving the UTC indicator |
49 | static 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. |
59 | static 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 | |
68 | static 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 | |
84 | namespace KCal { |
85 | |
86 | /******************************************************************************/ |
87 | |
88 | //@cond PRIVATE |
89 | class ICalTimeZonesPrivate |
90 | { |
91 | public: |
92 | ICalTimeZonesPrivate() {} |
93 | ICalTimeZones::ZoneMap zones; |
94 | }; |
95 | //@endcond |
96 | |
97 | ICalTimeZones::ICalTimeZones() |
98 | : d( new ICalTimeZonesPrivate ) |
99 | { |
100 | } |
101 | |
102 | ICalTimeZones::~ICalTimeZones() |
103 | { |
104 | delete d; |
105 | } |
106 | |
107 | const ICalTimeZones::ZoneMap ICalTimeZones::zones() const |
108 | { |
109 | return d->zones; |
110 | } |
111 | |
112 | bool 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 | |
125 | ICalTimeZone 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 | |
138 | ICalTimeZone 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 | |
151 | void ICalTimeZones::clear() |
152 | { |
153 | d->zones.clear(); |
154 | } |
155 | |
156 | ICalTimeZone 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 | |
169 | ICalTimeZoneBackend::ICalTimeZoneBackend() |
170 | : KTimeZoneBackend() |
171 | {} |
172 | |
173 | ICalTimeZoneBackend::ICalTimeZoneBackend( ICalTimeZoneSource *source, |
174 | const QString &name, |
175 | const QString &countryCode, |
176 | float latitude, float longitude, |
177 | const QString & ) |
178 | : KTimeZoneBackend( source, name, countryCode, latitude, longitude, comment ) |
179 | {} |
180 | |
181 | ICalTimeZoneBackend::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 | |
187 | ICalTimeZoneBackend::~ICalTimeZoneBackend() |
188 | {} |
189 | |
190 | KTimeZoneBackend *ICalTimeZoneBackend::clone() const |
191 | { |
192 | return new ICalTimeZoneBackend( *this ); |
193 | } |
194 | |
195 | QByteArray ICalTimeZoneBackend::type() const |
196 | { |
197 | return "ICalTimeZone" ; |
198 | } |
199 | |
200 | bool ICalTimeZoneBackend::hasTransitions( const KTimeZone *caller ) const |
201 | { |
202 | Q_UNUSED( caller ); |
203 | return true; |
204 | } |
205 | |
206 | /******************************************************************************/ |
207 | |
208 | ICalTimeZone::ICalTimeZone() |
209 | : KTimeZone( new ICalTimeZoneBackend() ) |
210 | {} |
211 | |
212 | ICalTimeZone::ICalTimeZone( ICalTimeZoneSource *source, const QString &name, |
213 | ICalTimeZoneData *data ) |
214 | : KTimeZone( new ICalTimeZoneBackend( source, name ) ) |
215 | { |
216 | setData( data ); |
217 | } |
218 | |
219 | ICalTimeZone::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 | |
235 | ICalTimeZone::~ICalTimeZone() |
236 | {} |
237 | |
238 | QString ICalTimeZone::city() const |
239 | { |
240 | const ICalTimeZoneData *dat = static_cast<const ICalTimeZoneData*>( data() ); |
241 | return dat ? dat->city() : QString(); |
242 | } |
243 | |
244 | QByteArray ICalTimeZone::url() const |
245 | { |
246 | const ICalTimeZoneData *dat = static_cast<const ICalTimeZoneData*>( data() ); |
247 | return dat ? dat->url() : QByteArray(); |
248 | } |
249 | |
250 | QDateTime ICalTimeZone::lastModified() const |
251 | { |
252 | const ICalTimeZoneData *dat = static_cast<const ICalTimeZoneData*>( data() ); |
253 | return dat ? dat->lastModified() : QDateTime(); |
254 | } |
255 | |
256 | QByteArray ICalTimeZone::vtimezone() const |
257 | { |
258 | const ICalTimeZoneData *dat = static_cast<const ICalTimeZoneData*>( data() ); |
259 | return dat ? dat->vtimezone() : QByteArray(); |
260 | } |
261 | |
262 | icaltimezone *ICalTimeZone::icalTimezone() const |
263 | { |
264 | const ICalTimeZoneData *dat = static_cast<const ICalTimeZoneData*>( data() ); |
265 | return dat ? dat->icalTimezone() : 0; |
266 | } |
267 | |
268 | bool 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 | |
279 | ICalTimeZone 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 |
292 | class 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 | |
318 | ICalTimeZoneData::ICalTimeZoneData() |
319 | : d ( new ICalTimeZoneDataPrivate() ) |
320 | { |
321 | } |
322 | |
323 | ICalTimeZoneData::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 | |
333 | ICalTimeZoneData::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 | |
621 | ICalTimeZoneData::~ICalTimeZoneData() |
622 | { |
623 | delete d; |
624 | } |
625 | |
626 | ICalTimeZoneData &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 | |
641 | KTimeZoneData *ICalTimeZoneData::clone() const |
642 | { |
643 | return new ICalTimeZoneData( *this ); |
644 | } |
645 | |
646 | QString ICalTimeZoneData::city() const |
647 | { |
648 | return d->location; |
649 | } |
650 | |
651 | QByteArray ICalTimeZoneData::url() const |
652 | { |
653 | return d->url; |
654 | } |
655 | |
656 | QDateTime ICalTimeZoneData::lastModified() const |
657 | { |
658 | return d->lastModified; |
659 | } |
660 | |
661 | QByteArray ICalTimeZoneData::vtimezone() const |
662 | { |
663 | QByteArray result( icalcomponent_as_ical_string( d->component() ) ); |
664 | icalmemory_free_ring(); |
665 | return result; |
666 | } |
667 | |
668 | icaltimezone *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 | |
683 | bool ICalTimeZoneData::hasTransitions() const |
684 | { |
685 | return true; |
686 | } |
687 | |
688 | /******************************************************************************/ |
689 | |
690 | //@cond PRIVATE |
691 | class ICalTimeZoneSourcePrivate |
692 | { |
693 | public: |
694 | static QList<QDateTime> parsePhase( icalcomponent *, bool daylight, |
695 | int &prevOffset, KTimeZone::Phase & ); |
696 | static QByteArray icalTzidPrefix; |
697 | }; |
698 | |
699 | QByteArray ICalTimeZoneSourcePrivate::icalTzidPrefix; |
700 | //@endcond |
701 | |
702 | ICalTimeZoneSource::ICalTimeZoneSource() |
703 | : KTimeZoneSource( false ), |
704 | d( 0 ) |
705 | { |
706 | } |
707 | |
708 | ICalTimeZoneSource::~ICalTimeZoneSource() |
709 | { |
710 | } |
711 | |
712 | bool 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 | |
734 | bool 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 | |
754 | ICalTimeZone 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 | |
883 | ICalTimeZone 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 |
893 | QList<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 ; |
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 | |
1051 | ICalTimeZone 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 | |
1088 | QByteArray 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 | |